diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 6a35affbff..0fbc67feb3 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -110,7 +110,7 @@ jobs: working-directory: backend/tests - name: RUN TEST - uses: kohlerdominik/docker-run-action@v1.0.2 + uses: kohlerdominik/docker-run-action@v1.1.0 with: image: squidex/build environment: | @@ -124,7 +124,7 @@ jobs: run: dotnet test /src/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter Category!=NotAutomated - name: RUN TEST on path - uses: kohlerdominik/docker-run-action@v1.0.2 + uses: kohlerdominik/docker-run-action@v1.1.0 with: image: squidex/build environment: | @@ -138,7 +138,7 @@ jobs: run: dotnet test /src/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter Category!=NotAutomated - name: RUN TEST with dedicated collections - uses: kohlerdominik/docker-run-action@v1.0.2 + uses: kohlerdominik/docker-run-action@v1.1.0 with: image: squidex/build environment: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3f27a5495..8ce51c3ab7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: working-directory: backend/tests - name: RUN TEST - uses: kohlerdominik/docker-run-action@v1.0.2 + uses: kohlerdominik/docker-run-action@v1.1.0 with: image: squidex/build environment: | @@ -107,7 +107,7 @@ jobs: run: dotnet test /src/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter Category!=NotAutomated - name: RUN TEST on path - uses: kohlerdominik/docker-run-action@v1.0.2 + uses: kohlerdominik/docker-run-action@v1.1.0 with: image: squidex/build environment: | @@ -121,7 +121,7 @@ jobs: run: dotnet test /src/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter Category!=NotAutomated - name: RUN TEST with dedicated collections - uses: kohlerdominik/docker-run-action@v1.0.2 + uses: kohlerdominik/docker-run-action@v1.1.0 with: image: squidex/build environment: | diff --git a/backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs b/backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs index 95d33d1c3d..566faa477a 100644 --- a/backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs +++ b/backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs @@ -12,35 +12,34 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.APM.ApplicationInsights +namespace Squidex.Extensions.APM.ApplicationInsights; + +public sealed class ApplicationInsightsPlugin : IPlugin { - public sealed class ApplicationInsightsPlugin : IPlugin + private sealed class Configurator : ITelemetryConfigurator { - private sealed class Configurator : ITelemetryConfigurator - { - private readonly IConfiguration config; + private readonly IConfiguration config; - public Configurator(IConfiguration config) - { - this.config = config; - } + public Configurator(IConfiguration config) + { + this.config = config; + } - public void Configure(TracerProviderBuilder builder) + public void Configure(TracerProviderBuilder builder) + { + builder.AddAzureMonitorTraceExporter(options => { - builder.AddAzureMonitorTraceExporter(options => - { - config.GetSection("logging:applicationInsights").Bind(options); - }); - } + config.GetSection("logging:applicationInsights").Bind(options); + }); } + } - public void ConfigureServices(IServiceCollection services, IConfiguration config) + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + if (config.GetValue("logging:applicationInsights:enabled")) { - if (config.GetValue("logging:applicationInsights:enabled")) - { - services.AddSingleton(); - } + services.AddSingleton(); } } } diff --git a/backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs b/backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs index 4de4269e5d..5f9ab21dca 100644 --- a/backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs +++ b/backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs @@ -11,38 +11,37 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.APM.Otlp +namespace Squidex.Extensions.APM.Otlp; + +public sealed class OtlpPlugin : IPlugin { - public sealed class OtlpPlugin : IPlugin + private sealed class Configurator : ITelemetryConfigurator { - private sealed class Configurator : ITelemetryConfigurator + private readonly IConfiguration config; + + public Configurator(IConfiguration config) { - private readonly IConfiguration config; + this.config = config; + } - public Configurator(IConfiguration config) - { - this.config = config; - } + public void Configure(TracerProviderBuilder builder) + { + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - public void Configure(TracerProviderBuilder builder) + builder.AddOtlpExporter(options => { - // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - - builder.AddOtlpExporter(options => - { - config.GetSection("logging:otlp").Bind(options); - }); - } + config.GetSection("logging:otlp").Bind(options); + }); } + } - public void ConfigureServices(IServiceCollection services, IConfiguration config) + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + if (config.GetValue("logging:otlp:enabled")) { - if (config.GetValue("logging:otlp:enabled")) - { - services.AddSingleton(); - } + services.AddSingleton(); } } } diff --git a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs index b0aacfaedc..ade8722969 100644 --- a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs +++ b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs @@ -11,58 +11,57 @@ using Squidex.Infrastructure; using Squidex.Log; -namespace Squidex.Extensions.APM.Stackdriver +namespace Squidex.Extensions.APM.Stackdriver; + +internal sealed class StackdriverExceptionHandler : ILogAppender { - internal sealed class StackdriverExceptionHandler : ILogAppender + private readonly IContextExceptionLogger logger; + private readonly HttpContextWrapper httpContextWrapper; + + public sealed class HttpContextWrapper : IContextWrapper { - private readonly IContextExceptionLogger logger; - private readonly HttpContextWrapper httpContextWrapper; + private readonly IHttpContextAccessor httpContextAccessor; - public sealed class HttpContextWrapper : IContextWrapper + internal HttpContextWrapper(IHttpContextAccessor httpContextAccessor) { - private readonly IHttpContextAccessor httpContextAccessor; - - internal HttpContextWrapper(IHttpContextAccessor httpContextAccessor) - { - this.httpContextAccessor = httpContextAccessor; - } - - public string GetHttpMethod() - { - return httpContextAccessor.HttpContext?.Request?.Method ?? string.Empty; - } - - public string GetUri() - { - return httpContextAccessor.HttpContext?.Request?.GetDisplayUrl() ?? string.Empty; - } + this.httpContextAccessor = httpContextAccessor; + } - public string GetUserAgent() - { - return httpContextAccessor.HttpContext?.Request?.Headers["User-Agent"].ToString() ?? string.Empty; - } + public string GetHttpMethod() + { + return httpContextAccessor.HttpContext?.Request?.Method ?? string.Empty; } - public StackdriverExceptionHandler(IContextExceptionLogger logger, IHttpContextAccessor httpContextAccessor) + public string GetUri() { - this.logger = logger; + return httpContextAccessor.HttpContext?.Request?.GetDisplayUrl() ?? string.Empty; + } - httpContextWrapper = new HttpContextWrapper(httpContextAccessor); + public string GetUserAgent() + { + return httpContextAccessor.HttpContext?.Request?.Headers["User-Agent"].ToString() ?? string.Empty; } + } - public void Append(IObjectWriter writer, SemanticLogLevel logLevel, Exception exception) + public StackdriverExceptionHandler(IContextExceptionLogger logger, IHttpContextAccessor httpContextAccessor) + { + this.logger = logger; + + httpContextWrapper = new HttpContextWrapper(httpContextAccessor); + } + + public void Append(IObjectWriter writer, SemanticLogLevel logLevel, Exception exception) + { + try { - try - { - if (exception != null && exception is not DomainException && exception is not OperationCanceledException) - { - logger.Log(exception, httpContextWrapper); - } - } - catch + if (exception != null && exception is not DomainException && exception is not OperationCanceledException) { - return; + logger.Log(exception, httpContextWrapper); } } + catch + { + return; + } } } diff --git a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs index 9d95e57d7e..c12e1d1cfa 100644 --- a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs +++ b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs @@ -14,54 +14,53 @@ using Squidex.Infrastructure.Plugins; using Squidex.Log; -namespace Squidex.Extensions.APM.Stackdriver +namespace Squidex.Extensions.APM.Stackdriver; + +public sealed class StackdriverPlugin : IPlugin { - public sealed class StackdriverPlugin : IPlugin + private sealed class Configurator : ITelemetryConfigurator { - private sealed class Configurator : ITelemetryConfigurator - { - private readonly string projectId; + private readonly string projectId; - public Configurator(string projectId) - { - this.projectId = projectId; - } - - public void Configure(TracerProviderBuilder builder) - { - builder.UseStackdriverExporter(projectId); - } + public Configurator(string projectId) + { + this.projectId = projectId; } - public void ConfigureServices(IServiceCollection services, IConfiguration config) + public void Configure(TracerProviderBuilder builder) { - var isEnabled = config.GetValue("logging:stackdriver:enabled"); + builder.UseStackdriverExporter(projectId); + } + } - if (!isEnabled) - { - return; - } + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + var isEnabled = config.GetValue("logging:stackdriver:enabled"); - var projectId = config.GetValue("logging:stackdriver:projectId"); + if (!isEnabled) + { + return; + } + + var projectId = config.GetValue("logging:stackdriver:projectId"); - if (string.IsNullOrWhiteSpace(projectId)) - { - return; - } + if (string.IsNullOrWhiteSpace(projectId)) + { + return; + } - services.AddSingleton( - new Configurator(projectId)); + services.AddSingleton( + new Configurator(projectId)); - services.AddSingleton(); + services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); - var serviceName = config.GetValue("logging:name") ?? "Squidex"; - var serviceVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString(); + var serviceName = config.GetValue("logging:name") ?? "Squidex"; + var serviceVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString(); - services.AddSingleton(c => ContextExceptionLogger.Create(projectId, serviceVersion, serviceVersion, null)); - } + services.AddSingleton(c => ContextExceptionLogger.Create(projectId, serviceVersion, serviceVersion, null)); } } diff --git a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverSeverityLogAppender.cs b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverSeverityLogAppender.cs index f94de0da8f..059f5e4b14 100644 --- a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverSeverityLogAppender.cs +++ b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverSeverityLogAppender.cs @@ -7,36 +7,35 @@ using Squidex.Log; -namespace Squidex.Extensions.APM.Stackdriver +namespace Squidex.Extensions.APM.Stackdriver; + +public sealed class StackdriverSeverityLogAppender : ILogAppender { - public sealed class StackdriverSeverityLogAppender : ILogAppender + public void Append(IObjectWriter writer, SemanticLogLevel logLevel, Exception exception) { - public void Append(IObjectWriter writer, SemanticLogLevel logLevel, Exception exception) - { - var severity = GetSeverity(logLevel); + var severity = GetSeverity(logLevel); - writer.WriteProperty(nameof(severity), severity); - } + writer.WriteProperty(nameof(severity), severity); + } - private static string GetSeverity(SemanticLogLevel logLevel) + private static string GetSeverity(SemanticLogLevel logLevel) + { + switch (logLevel) { - switch (logLevel) - { - case SemanticLogLevel.Trace: - return "DEBUG"; - case SemanticLogLevel.Debug: - return "DEBUG"; - case SemanticLogLevel.Information: - return "INFO"; - case SemanticLogLevel.Warning: - return "WARNING"; - case SemanticLogLevel.Error: - return "ERROR"; - case SemanticLogLevel.Fatal: - return "CRITICAL"; - default: - return "DEFAULT"; - } + case SemanticLogLevel.Trace: + return "DEBUG"; + case SemanticLogLevel.Debug: + return "DEBUG"; + case SemanticLogLevel.Information: + return "INFO"; + case SemanticLogLevel.Warning: + return "WARNING"; + case SemanticLogLevel.Error: + return "ERROR"; + case SemanticLogLevel.Fatal: + return "CRITICAL"; + default: + return "DEFAULT"; } } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs index 5af9b4e4b6..eea5b7701f 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs @@ -10,42 +10,41 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Algolia +namespace Squidex.Extensions.Actions.Algolia; + +[RuleAction( + Title = "Algolia", + IconImage = "", + IconColor = "#0d9bf9", + Display = "Populate Algolia index", + Description = "Populate a full text search index in Algolia.", + ReadMore = "https://www.algolia.com/")] +public sealed record AlgoliaAction : RuleAction { - [RuleAction( - Title = "Algolia", - IconImage = "", - IconColor = "#0d9bf9", - Display = "Populate Algolia index", - Description = "Populate a full text search index in Algolia.", - ReadMore = "https://www.algolia.com/")] - public sealed record AlgoliaAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Application Id", Description = "The application ID.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string AppId { get; set; } + [LocalizedRequired] + [Display(Name = "Application Id", Description = "The application ID.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string AppId { get; set; } - [LocalizedRequired] - [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string ApiKey { get; set; } + [LocalizedRequired] + [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string ApiKey { get; set; } - [LocalizedRequired] - [Display(Name = "Index Name", Description = "The name of the index.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string IndexName { get; set; } + [LocalizedRequired] + [Display(Name = "Index Name", Description = "The name of the index.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string IndexName { get; set; } - [Display(Name = "Document", Description = "The optional custom document.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Document { get; set; } + [Display(Name = "Document", Description = "The optional custom document.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Document { get; set; } - [Display(Name = "Deletion", Description = "The condition when to delete the entry.")] - [Editor(RuleFieldEditor.Text)] - public string Delete { get; set; } - } + [Display(Name = "Deletion", Description = "The condition when to delete the entry.")] + [Editor(RuleFieldEditor.Text)] + public string Delete { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs index 3a05869c39..0693a74a18 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs @@ -16,147 +16,146 @@ #pragma warning disable IDE0059 // Value assigned to symbol is never used #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Algolia +namespace Squidex.Extensions.Actions.Algolia; + +public sealed class AlgoliaActionHandler : RuleActionHandler { - public sealed class AlgoliaActionHandler : RuleActionHandler - { - private readonly ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex> clients; - private readonly IScriptEngine scriptEngine; - private readonly IJsonSerializer serializer; + private readonly ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex> clients; + private readonly IScriptEngine scriptEngine; + private readonly IJsonSerializer serializer; - public AlgoliaActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) - : base(formatter) + public AlgoliaActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) + : base(formatter) + { + clients = new ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex>(key => { - clients = new ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex>(key => - { - var client = new SearchClient(key.AppId, key.ApiKey); + var client = new SearchClient(key.AppId, key.ApiKey); - return client.InitIndex(key.IndexName); - }); + return client.InitIndex(key.IndexName); + }); - this.scriptEngine = scriptEngine; - this.serializer = serializer; - } + this.scriptEngine = scriptEngine; + this.serializer = serializer; + } - protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(EnrichedEvent @event, AlgoliaAction action) + protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(EnrichedEvent @event, AlgoliaAction action) + { + if (@event is IEnrichedEntityEvent entityEvent) { - if (@event is IEnrichedEntityEvent entityEvent) - { - var delete = @event.ShouldDelete(scriptEngine, action.Delete); + var delete = @event.ShouldDelete(scriptEngine, action.Delete); - var ruleDescription = string.Empty; - var contentId = entityEvent.Id.ToString(); - var content = (AlgoliaContent)null; + var ruleDescription = string.Empty; + var contentId = entityEvent.Id.ToString(); + var content = (AlgoliaContent)null; - if (delete) - { - ruleDescription = $"Delete entry from Algolia index: {action.IndexName}"; - } - else + if (delete) + { + ruleDescription = $"Delete entry from Algolia index: {action.IndexName}"; + } + else + { + ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; + + try { - ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; + string jsonString; - try + if (!string.IsNullOrEmpty(action.Document)) { - string jsonString; - - if (!string.IsNullOrEmpty(action.Document)) - { - jsonString = await FormatAsync(action.Document, @event); - jsonString = jsonString?.Trim(); - } - else - { - jsonString = ToJson(@event); - } - - content = serializer.Deserialize(jsonString); + jsonString = await FormatAsync(action.Document, @event); + jsonString = jsonString?.Trim(); } - catch (Exception ex) + else { - content = new AlgoliaContent - { - More = new Dictionary - { - ["error"] = $"Invalid JSON: {ex.Message}" - } - }; + jsonString = ToJson(@event); } - content.ObjectID = contentId; + content = serializer.Deserialize(jsonString); } - - var ruleJob = new AlgoliaJob + catch (Exception ex) { - AppId = action.AppId, - ApiKey = action.ApiKey, - Content = serializer.Serialize(content, true), - ContentId = contentId, - IndexName = await FormatAsync(action.IndexName, @event) - }; + content = new AlgoliaContent + { + More = new Dictionary + { + ["error"] = $"Invalid JSON: {ex.Message}" + } + }; + } - return (ruleDescription, ruleJob); + content.ObjectID = contentId; } - return ("Ignore", new AlgoliaJob()); + var ruleJob = new AlgoliaJob + { + AppId = action.AppId, + ApiKey = action.ApiKey, + Content = serializer.Serialize(content, true), + ContentId = contentId, + IndexName = await FormatAsync(action.IndexName, @event) + }; + + return (ruleDescription, ruleJob); } - protected override async Task ExecuteJobAsync(AlgoliaJob job, - CancellationToken ct = default) + return ("Ignore", new AlgoliaJob()); + } + + protected override async Task ExecuteJobAsync(AlgoliaJob job, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(job.AppId)) { - if (string.IsNullOrWhiteSpace(job.AppId)) - { - return Result.Ignored(); - } + return Result.Ignored(); + } - var index = await clients.GetClientAsync((job.AppId, job.ApiKey, job.IndexName)); + var index = await clients.GetClientAsync((job.AppId, job.ApiKey, job.IndexName)); - try + try + { + if (job.Content != null) { - if (job.Content != null) + var raw = new[] { - var raw = new[] - { - new JRaw(job.Content) - }; + new JRaw(job.Content) + }; - var response = await index.SaveObjectsAsync(raw, null, ct, true); + var response = await index.SaveObjectsAsync(raw, null, ct, true); - return Result.Success(serializer.Serialize(response, true)); - } - else - { - var response = await index.DeleteObjectAsync(job.ContentId, null, ct); - - return Result.Success(serializer.Serialize(response, true)); - } + return Result.Success(serializer.Serialize(response, true)); } - catch (Exception ex) + else { - return Result.Failed(ex); + var response = await index.DeleteObjectAsync(job.ContentId, null, ct); + + return Result.Success(serializer.Serialize(response, true)); } } + catch (Exception ex) + { + return Result.Failed(ex); + } } +} - public sealed class AlgoliaContent - { - [JsonPropertyName("objectID")] - public string ObjectID { get; set; } +public sealed class AlgoliaContent +{ + [JsonPropertyName("objectID")] + public string ObjectID { get; set; } - [JsonExtensionData] - public Dictionary More { get; set; } = new Dictionary(); - } + [JsonExtensionData] + public Dictionary More { get; set; } = new Dictionary(); +} - public sealed class AlgoliaJob - { - public string AppId { get; set; } +public sealed class AlgoliaJob +{ + public string AppId { get; set; } - public string ApiKey { get; set; } + public string ApiKey { get; set; } - public string ContentId { get; set; } + public string ContentId { get; set; } - public string IndexName { get; set; } + public string IndexName { get; set; } - public string Content { get; set; } - } + public string Content { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs index c668ba0035..4dcae76cb1 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Algolia +namespace Squidex.Extensions.Actions.Algolia; + +public sealed class AlgoliaPlugin : IPlugin { - public sealed class AlgoliaPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs index fad817de33..1a4fe27d71 100644 --- a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs @@ -11,40 +11,39 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.AzureQueue +namespace Squidex.Extensions.Actions.AzureQueue; + +[RuleAction( + Title = "Azure Queue", + IconImage = "", + IconColor = "#0d9bf9", + Display = "Send to Azure Queue", + Description = "Send an event to azure queue storage.", + ReadMore = "https://azure.microsoft.com/en-us/services/storage/queues/")] +public sealed record AzureQueueAction : RuleAction { - [RuleAction( - Title = "Azure Queue", - IconImage = "", - IconColor = "#0d9bf9", - Display = "Send to Azure Queue", - Description = "Send an event to azure queue storage.", - ReadMore = "https://azure.microsoft.com/en-us/services/storage/queues/")] - public sealed record AzureQueueAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Connection", Description = "The connection string to the storage account.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string ConnectionString { get; set; } + [LocalizedRequired] + [Display(Name = "Connection", Description = "The connection string to the storage account.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string ConnectionString { get; set; } - [LocalizedRequired] - [Display(Name = "Queue", Description = "The name of the queue.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string Queue { get; set; } + [LocalizedRequired] + [Display(Name = "Queue", Description = "The name of the queue.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string Queue { get; set; } - [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Payload { get; set; } + [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Payload { get; set; } - protected override IEnumerable CustomValidate() + protected override IEnumerable CustomValidate() + { + if (!string.IsNullOrWhiteSpace(Queue) && !Regex.IsMatch(Queue, "^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$")) { - if (!string.IsNullOrWhiteSpace(Queue) && !Regex.IsMatch(Queue, "^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$")) - { - yield return new ValidationError("Queue must be valid azure queue name.", nameof(Queue)); - } + yield return new ValidationError("Queue must be valid azure queue name.", nameof(Queue)); } } } diff --git a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs index f796f713e0..2319d3c921 100644 --- a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs @@ -12,69 +12,68 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.AzureQueue +namespace Squidex.Extensions.Actions.AzureQueue; + +public sealed class AzureQueueActionHandler : RuleActionHandler { - public sealed class AzureQueueActionHandler : RuleActionHandler - { - private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients; + private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients; - public AzureQueueActionHandler(RuleEventFormatter formatter) - : base(formatter) + public AzureQueueActionHandler(RuleEventFormatter formatter) + : base(formatter) + { + clients = new ClientPool<(string ConnectionString, string QueueName), CloudQueue>(key => { - clients = new ClientPool<(string ConnectionString, string QueueName), CloudQueue>(key => - { - var storageAccount = CloudStorageAccount.Parse(key.ConnectionString); + var storageAccount = CloudStorageAccount.Parse(key.ConnectionString); - var queueClient = storageAccount.CreateCloudQueueClient(); - var queueRef = queueClient.GetQueueReference(key.QueueName); + var queueClient = storageAccount.CreateCloudQueueClient(); + var queueRef = queueClient.GetQueueReference(key.QueueName); - return queueRef; - }); - } + return queueRef; + }); + } - protected override async Task<(string Description, AzureQueueJob Data)> CreateJobAsync(EnrichedEvent @event, AzureQueueAction action) + protected override async Task<(string Description, AzureQueueJob Data)> CreateJobAsync(EnrichedEvent @event, AzureQueueAction action) + { + var queueName = await FormatAsync(action.Queue, @event); + + string requestBody; + + if (!string.IsNullOrEmpty(action.Payload)) { - var queueName = await FormatAsync(action.Queue, @event); - - string requestBody; - - if (!string.IsNullOrEmpty(action.Payload)) - { - requestBody = await FormatAsync(action.Payload, @event); - } - else - { - requestBody = ToEnvelopeJson(@event); - } - - var ruleDescription = $"Send AzureQueueJob to azure queue '{queueName}'"; - var ruleJob = new AzureQueueJob - { - QueueConnectionString = action.ConnectionString, - QueueName = queueName, - MessageBodyV2 = requestBody - }; - - return (ruleDescription, ruleJob); + requestBody = await FormatAsync(action.Payload, @event); } - - protected override async Task ExecuteJobAsync(AzureQueueJob job, - CancellationToken ct = default) + else { - var queue = await clients.GetClientAsync((job.QueueConnectionString, job.QueueName)); + requestBody = ToEnvelopeJson(@event); + } - await queue.AddMessageAsync(new CloudQueueMessage(job.MessageBodyV2), null, null, null, null, ct); + var ruleDescription = $"Send AzureQueueJob to azure queue '{queueName}'"; + var ruleJob = new AzureQueueJob + { + QueueConnectionString = action.ConnectionString, + QueueName = queueName, + MessageBodyV2 = requestBody + }; - return Result.Complete(); - } + return (ruleDescription, ruleJob); } - public sealed class AzureQueueJob + protected override async Task ExecuteJobAsync(AzureQueueJob job, + CancellationToken ct = default) { - public string QueueConnectionString { get; set; } + var queue = await clients.GetClientAsync((job.QueueConnectionString, job.QueueName)); - public string QueueName { get; set; } + await queue.AddMessageAsync(new CloudQueueMessage(job.MessageBodyV2), null, null, null, null, ct); - public string MessageBodyV2 { get; set; } + return Result.Complete(); } } + +public sealed class AzureQueueJob +{ + public string QueueConnectionString { get; set; } + + public string QueueName { get; set; } + + public string MessageBodyV2 { get; set; } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs index 4dce57e2ba..114fb6749a 100644 --- a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.AzureQueue +namespace Squidex.Extensions.Actions.AzureQueue; + +public sealed class AzureQueuePlugin : IPlugin { - public sealed class AzureQueuePlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/ClientPool.cs b/backend/extensions/Squidex.Extensions/Actions/ClientPool.cs index a3d56f6741..7a052f4d84 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ClientPool.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ClientPool.cs @@ -10,34 +10,33 @@ #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Extensions.Actions +namespace Squidex.Extensions.Actions; + +internal sealed class ClientPool { - internal sealed class ClientPool - { - private static readonly TimeSpan TimeToLive = TimeSpan.FromMinutes(30); - private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly Func> factory; + private static readonly TimeSpan TimeToLive = TimeSpan.FromMinutes(30); + private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly Func> factory; - public ClientPool(Func factory) - { - this.factory = x => Task.FromResult(factory(x)); - } + public ClientPool(Func factory) + { + this.factory = x => Task.FromResult(factory(x)); + } - public ClientPool(Func> factory) - { - this.factory = factory; - } + public ClientPool(Func> factory) + { + this.factory = factory; + } - public async Task GetClientAsync(TKey key) + public async Task GetClientAsync(TKey key) + { + if (!memoryCache.TryGetValue(key, out var client)) { - if (!memoryCache.TryGetValue(key, out var client)) - { - client = await factory(key); - - memoryCache.Set(key, client, TimeToLive); - } + client = await factory(key); - return client; + memoryCache.Set(key, client, TimeToLive); } + + return client; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentAction.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentAction.cs index 599bc8a519..e52ede976a 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentAction.cs @@ -10,24 +10,23 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Comment +namespace Squidex.Extensions.Actions.Comment; + +[RuleAction( + Title = "Comment", + IconImage = "", + IconColor = "#3389ff", + Display = "Create comment", + Description = "Create a comment for a content event.")] +public sealed record CommentAction : RuleAction { - [RuleAction( - Title = "Comment", - IconImage = "", - IconColor = "#3389ff", - Display = "Create comment", - Description = "Create a comment for a content event.")] - public sealed record CommentAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Text", Description = "The comment text.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Text { get; set; } + [LocalizedRequired] + [Display(Name = "Text", Description = "The comment text.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Text { get; set; } - [Display(Name = "Client", Description = "An optional client name.")] - [Editor(RuleFieldEditor.Text)] - public string Client { get; set; } - } + [Display(Name = "Client", Description = "An optional client name.")] + [Editor(RuleFieldEditor.Text)] + public string Client { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs index cdff9d7a71..a53cb1f5e2 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs @@ -11,62 +11,61 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Extensions.Actions.Comment +namespace Squidex.Extensions.Actions.Comment; + +public sealed class CommentActionHandler : RuleActionHandler { - public sealed class CommentActionHandler : RuleActionHandler - { - private const string Description = "Send a Comment"; - private readonly ICommandBus commandBus; + private const string Description = "Send a Comment"; + private readonly ICommandBus commandBus; - public CommentActionHandler(RuleEventFormatter formatter, ICommandBus commandBus) - : base(formatter) - { - this.commandBus = commandBus; - } + public CommentActionHandler(RuleEventFormatter formatter, ICommandBus commandBus) + : base(formatter) + { + this.commandBus = commandBus; + } - protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action) + protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action) + { + if (@event is EnrichedContentEvent contentEvent) { - if (@event is EnrichedContentEvent contentEvent) + var ruleJob = new CreateComment { - var ruleJob = new CreateComment - { - AppId = contentEvent.AppId - }; - - ruleJob.Text = await FormatAsync(action.Text, @event); - - if (!string.IsNullOrEmpty(action.Client)) - { - ruleJob.Actor = RefToken.Client(action.Client); - } - else - { - ruleJob.Actor = contentEvent.Actor; - } + AppId = contentEvent.AppId + }; - ruleJob.CommentsId = contentEvent.Id; + ruleJob.Text = await FormatAsync(action.Text, @event); - return (Description, ruleJob); + if (!string.IsNullOrEmpty(action.Client)) + { + ruleJob.Actor = RefToken.Client(action.Client); + } + else + { + ruleJob.Actor = contentEvent.Actor; } - return ("Ignore", new CreateComment()); + ruleJob.CommentsId = contentEvent.Id; + + return (Description, ruleJob); } - protected override async Task ExecuteJobAsync(CreateComment job, - CancellationToken ct = default) - { - var command = job; + return ("Ignore", new CreateComment()); + } - if (command.CommentsId == default) - { - return Result.Ignored(); - } + protected override async Task ExecuteJobAsync(CreateComment job, + CancellationToken ct = default) + { + var command = job; - command.FromRule = true; + if (command.CommentsId == default) + { + return Result.Ignored(); + } - await commandBus.PublishAsync(command, ct); + command.FromRule = true; - return Result.Success($"Commented: {command.Text}"); - } + await commandBus.PublishAsync(command, ct); + + return Result.Success($"Commented: {command.Text}"); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentPlugin.cs index 013cbc42db..6207261c44 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Comment +namespace Squidex.Extensions.Actions.Comment; + +public sealed class CommentPlugin : IPlugin { - public sealed class CommentPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs index 9d7cfa6d8f..cc5ee7a719 100644 --- a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs @@ -10,33 +10,32 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.CreateContent +namespace Squidex.Extensions.Actions.CreateContent; + +[RuleAction( + Title = "CreateContent", + IconImage = "", + IconColor = "#3389ff", + Display = "Create content", + Description = "Create a a new content item for any schema.")] +public sealed record CreateContentAction : RuleAction { - [RuleAction( - Title = "CreateContent", - IconImage = "", - IconColor = "#3389ff", - Display = "Create content", - Description = "Create a a new content item for any schema.")] - public sealed record CreateContentAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Data", Description = "The content data.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Data { get; set; } + [LocalizedRequired] + [Display(Name = "Data", Description = "The content data.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Data { get; set; } - [LocalizedRequired] - [Display(Name = "Schema", Description = "The name of the schema.")] - [Editor(RuleFieldEditor.Text)] - public string Schema { get; set; } + [LocalizedRequired] + [Display(Name = "Schema", Description = "The name of the schema.")] + [Editor(RuleFieldEditor.Text)] + public string Schema { get; set; } - [Display(Name = "Client", Description = "An optional client name.")] - [Editor(RuleFieldEditor.Text)] - public string Client { get; set; } + [Display(Name = "Client", Description = "An optional client name.")] + [Editor(RuleFieldEditor.Text)] + public string Client { get; set; } - [Display(Name = "Publish", Description = "Publish the content.")] - [Editor(RuleFieldEditor.Text)] - public bool Publish { get; set; } - } + [Display(Name = "Publish", Description = "Publish the content.")] + [Editor(RuleFieldEditor.Text)] + public bool Publish { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs index 924a81ef9b..2bc28199b2 100644 --- a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs @@ -15,70 +15,69 @@ using Squidex.Infrastructure.Json; using Command = Squidex.Domain.Apps.Entities.Contents.Commands.CreateContent; -namespace Squidex.Extensions.Actions.CreateContent +namespace Squidex.Extensions.Actions.CreateContent; + +public sealed class CreateContentActionHandler : RuleActionHandler { - public sealed class CreateContentActionHandler : RuleActionHandler - { - private const string Description = "Create a content"; - private readonly ICommandBus commandBus; - private readonly IAppProvider appProvider; - private readonly IJsonSerializer jsonSerializer; + private const string Description = "Create a content"; + private readonly ICommandBus commandBus; + private readonly IAppProvider appProvider; + private readonly IJsonSerializer jsonSerializer; - public CreateContentActionHandler(RuleEventFormatter formatter, IAppProvider appProvider, ICommandBus commandBus, IJsonSerializer jsonSerializer) - : base(formatter) - { - this.appProvider = appProvider; - this.commandBus = commandBus; - this.jsonSerializer = jsonSerializer; - } + public CreateContentActionHandler(RuleEventFormatter formatter, IAppProvider appProvider, ICommandBus commandBus, IJsonSerializer jsonSerializer) + : base(formatter) + { + this.appProvider = appProvider; + this.commandBus = commandBus; + this.jsonSerializer = jsonSerializer; + } - protected override async Task<(string Description, Command Data)> CreateJobAsync(EnrichedEvent @event, CreateContentAction action) + protected override async Task<(string Description, Command Data)> CreateJobAsync(EnrichedEvent @event, CreateContentAction action) + { + var ruleJob = new Command { - var ruleJob = new Command - { - AppId = @event.AppId - }; - - var schema = await appProvider.GetSchemaAsync(@event.AppId.Id, action.Schema, true); + AppId = @event.AppId + }; - if (schema == null) - { - throw new InvalidOperationException($"Cannot find schema '{action.Schema}'"); - } + var schema = await appProvider.GetSchemaAsync(@event.AppId.Id, action.Schema, true); - ruleJob.SchemaId = schema.NamedId(); - - var json = await FormatAsync(action.Data, @event); + if (schema == null) + { + throw new InvalidOperationException($"Cannot find schema '{action.Schema}'"); + } - ruleJob.Data = jsonSerializer.Deserialize(json); + ruleJob.SchemaId = schema.NamedId(); - if (!string.IsNullOrEmpty(action.Client)) - { - ruleJob.Actor = RefToken.Client(action.Client); - } - else if (@event is EnrichedUserEventBase userEvent) - { - ruleJob.Actor = userEvent.Actor; - } + var json = await FormatAsync(action.Data, @event); - if (action.Publish) - { - ruleJob.Status = Status.Published; - } + ruleJob.Data = jsonSerializer.Deserialize(json); - return (Description, ruleJob); + if (!string.IsNullOrEmpty(action.Client)) + { + ruleJob.Actor = RefToken.Client(action.Client); + } + else if (@event is EnrichedUserEventBase userEvent) + { + ruleJob.Actor = userEvent.Actor; } - protected override async Task ExecuteJobAsync(Command job, - CancellationToken ct = default) + if (action.Publish) { - var command = job; + ruleJob.Status = Status.Published; + } - command.FromRule = true; + return (Description, ruleJob); + } - await commandBus.PublishAsync(command, ct); + protected override async Task ExecuteJobAsync(Command job, + CancellationToken ct = default) + { + var command = job; - return Result.Success($"Created to: {command.SchemaId.Name}"); - } + command.FromRule = true; + + await commandBus.PublishAsync(command, ct); + + return Result.Success($"Created to: {command.SchemaId.Name}"); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs index 644917108b..7a6d685f24 100644 --- a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.CreateContent +namespace Squidex.Extensions.Actions.CreateContent; + +public sealed class CreateContentPlugin : IPlugin { - public sealed class CreateContentPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs index 6db4a90f29..22df74ced1 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs @@ -10,50 +10,49 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Discourse +namespace Squidex.Extensions.Actions.Discourse; + +[RuleAction( + Title = "Discourse", + IconImage = "", + IconColor = "#eB6121", + Display = "Post to discourse", + Description = "Create a post or topic at discourse.", + ReadMore = "https://www.discourse.org/")] +public sealed record DiscourseAction : RuleAction { - [RuleAction( - Title = "Discourse", - IconImage = "", - IconColor = "#eB6121", - Display = "Post to discourse", - Description = "Create a post or topic at discourse.", - ReadMore = "https://www.discourse.org/")] - public sealed record DiscourseAction : RuleAction - { - [AbsoluteUrl] - [LocalizedRequired] - [Display(Name = "Server Url", Description = "The url to the discourse server.")] - [Editor(RuleFieldEditor.Url)] - public Uri Url { get; set; } - - [LocalizedRequired] - [Display(Name = "Api Key", Description = "The api key to authenticate to your discourse server.")] - [Editor(RuleFieldEditor.Text)] - public string ApiKey { get; set; } - - [LocalizedRequired] - [Display(Name = "Api User", Description = "The api username to authenticate to your discourse server.")] - [Editor(RuleFieldEditor.Text)] - public string ApiUsername { get; set; } - - [LocalizedRequired] - [Display(Name = "Text", Description = "The text as markdown.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Text { get; set; } - - [Display(Name = "Title", Description = "The optional title when creating new topics.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string Title { get; set; } - - [Display(Name = "Topic", Description = "The optional topic id.")] - [Editor(RuleFieldEditor.Text)] - public int? Topic { get; set; } - - [Display(Name = "Category", Description = "The optional category id.")] - [Editor(RuleFieldEditor.Text)] - public int? Category { get; set; } - } + [AbsoluteUrl] + [LocalizedRequired] + [Display(Name = "Server Url", Description = "The url to the discourse server.")] + [Editor(RuleFieldEditor.Url)] + public Uri Url { get; set; } + + [LocalizedRequired] + [Display(Name = "Api Key", Description = "The api key to authenticate to your discourse server.")] + [Editor(RuleFieldEditor.Text)] + public string ApiKey { get; set; } + + [LocalizedRequired] + [Display(Name = "Api User", Description = "The api username to authenticate to your discourse server.")] + [Editor(RuleFieldEditor.Text)] + public string ApiUsername { get; set; } + + [LocalizedRequired] + [Display(Name = "Text", Description = "The text as markdown.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Text { get; set; } + + [Display(Name = "Title", Description = "The optional title when creating new topics.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string Title { get; set; } + + [Display(Name = "Topic", Description = "The optional topic id.")] + [Editor(RuleFieldEditor.Text)] + public int? Topic { get; set; } + + [Display(Name = "Category", Description = "The optional category id.")] + [Editor(RuleFieldEditor.Text)] + public int? Category { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs index 18a3499934..58f5d296fa 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs @@ -11,87 +11,86 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Discourse +namespace Squidex.Extensions.Actions.Discourse; + +public sealed class DiscourseActionHandler : RuleActionHandler { - public sealed class DiscourseActionHandler : RuleActionHandler + private const string DescriptionCreatePost = "Create discourse Post"; + private const string DescriptionCreateTopic = "Create discourse Topic"; + + private readonly IHttpClientFactory httpClientFactory; + + public DiscourseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) { - private const string DescriptionCreatePost = "Create discourse Post"; - private const string DescriptionCreateTopic = "Create discourse Topic"; + this.httpClientFactory = httpClientFactory; + } - private readonly IHttpClientFactory httpClientFactory; + protected override async Task<(string Description, DiscourseJob Data)> CreateJobAsync(EnrichedEvent @event, DiscourseAction action) + { + var url = $"{action.Url.ToString().TrimEnd('/')}/posts.json?api_key={action.ApiKey}&api_username={action.ApiUsername}"; - public DiscourseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) + var json = new Dictionary { - this.httpClientFactory = httpClientFactory; - } + ["title"] = await FormatAsync(action.Title, @event) + }; - protected override async Task<(string Description, DiscourseJob Data)> CreateJobAsync(EnrichedEvent @event, DiscourseAction action) + if (action.Topic != null) { - var url = $"{action.Url.ToString().TrimEnd('/')}/posts.json?api_key={action.ApiKey}&api_username={action.ApiUsername}"; - - var json = new Dictionary - { - ["title"] = await FormatAsync(action.Title, @event) - }; - - if (action.Topic != null) - { - json.Add("topic_id", action.Topic.Value); - } + json.Add("topic_id", action.Topic.Value); + } - if (action.Category != null) - { - json.Add("category", action.Category.Value); - } + if (action.Category != null) + { + json.Add("category", action.Category.Value); + } - json["raw"] = await FormatAsync(action.Text, @event); + json["raw"] = await FormatAsync(action.Text, @event); - var requestBody = ToJson(json); + var requestBody = ToJson(json); - var ruleJob = new DiscourseJob - { - ApiKey = action.ApiKey, - ApiUserName = action.ApiUsername, - RequestUrl = url, - RequestBody = requestBody - }; - - var description = - action.Topic != null ? - DescriptionCreateTopic : - DescriptionCreatePost; - - return (description, ruleJob); - } + var ruleJob = new DiscourseJob + { + ApiKey = action.ApiKey, + ApiUserName = action.ApiUsername, + RequestUrl = url, + RequestBody = requestBody + }; + + var description = + action.Topic != null ? + DescriptionCreateTopic : + DescriptionCreatePost; + + return (description, ruleJob); + } - protected override async Task ExecuteJobAsync(DiscourseJob job, - CancellationToken ct = default) + protected override async Task ExecuteJobAsync(DiscourseJob job, + CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) { - using (var httpClient = httpClientFactory.CreateClient()) + using (var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) { - using (var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) - { - Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") - }) - { - request.Headers.TryAddWithoutValidation("Api-Key", job.ApiKey); - request.Headers.TryAddWithoutValidation("Api-Username", job.ApiUserName); - - return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); - } + Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") + }) + { + request.Headers.TryAddWithoutValidation("Api-Key", job.ApiKey); + request.Headers.TryAddWithoutValidation("Api-Username", job.ApiUserName); + + return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); } } } +} - public sealed class DiscourseJob - { - public string ApiKey { get; set; } +public sealed class DiscourseJob +{ + public string ApiKey { get; set; } - public string ApiUserName { get; set; } + public string ApiUserName { get; set; } - public string RequestUrl { get; set; } + public string RequestUrl { get; set; } - public string RequestBody { get; set; } - } + public string RequestBody { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs index e78e04d543..a0c38a8cdc 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Discourse +namespace Squidex.Extensions.Actions.Discourse; + +public sealed class DiscoursePlugin : IPlugin { - public sealed class DiscoursePlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs index c9fb7d43a0..2afbf555e4 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs @@ -10,44 +10,43 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.ElasticSearch +namespace Squidex.Extensions.Actions.ElasticSearch; + +[RuleAction( + Title = "ElasticSearch", + IconImage = "", + IconColor = "#1e5470", + Display = "Populate ElasticSearch index", + Description = "Populate a full text search index in ElasticSearch.", + ReadMore = "https://www.elastic.co/")] +public sealed record ElasticSearchAction : RuleAction { - [RuleAction( - Title = "ElasticSearch", - IconImage = "", - IconColor = "#1e5470", - Display = "Populate ElasticSearch index", - Description = "Populate a full text search index in ElasticSearch.", - ReadMore = "https://www.elastic.co/")] - public sealed record ElasticSearchAction : RuleAction - { - [AbsoluteUrl] - [LocalizedRequired] - [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] - [Editor(RuleFieldEditor.Url)] - public Uri Host { get; set; } - - [LocalizedRequired] - [Display(Name = "Index Name", Description = "The name of the index.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string IndexName { get; set; } - - [Display(Name = "Username", Description = "The optional username.")] - [Editor(RuleFieldEditor.Text)] - public string Username { get; set; } - - [Display(Name = "Password", Description = "The optional password.")] - [Editor(RuleFieldEditor.Text)] - public string Password { get; set; } - - [Display(Name = "Document", Description = "The optional custom document.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Document { get; set; } - - [Display(Name = "Deletion", Description = "The condition when to delete the document.")] - [Editor(RuleFieldEditor.Text)] - public string Delete { get; set; } - } + [AbsoluteUrl] + [LocalizedRequired] + [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] + [Editor(RuleFieldEditor.Url)] + public Uri Host { get; set; } + + [LocalizedRequired] + [Display(Name = "Index Name", Description = "The name of the index.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string IndexName { get; set; } + + [Display(Name = "Username", Description = "The optional username.")] + [Editor(RuleFieldEditor.Text)] + public string Username { get; set; } + + [Display(Name = "Password", Description = "The optional password.")] + [Editor(RuleFieldEditor.Text)] + public string Password { get; set; } + + [Display(Name = "Document", Description = "The optional custom document.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Document { get; set; } + + [Display(Name = "Deletion", Description = "The condition when to delete the document.")] + [Editor(RuleFieldEditor.Text)] + public string Delete { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs index 868823d9dc..e8c1614cc6 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs @@ -16,154 +16,153 @@ #pragma warning disable IDE0059 // Value assigned to symbol is never used #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.ElasticSearch +namespace Squidex.Extensions.Actions.ElasticSearch; + +public sealed class ElasticSearchActionHandler : RuleActionHandler { - public sealed class ElasticSearchActionHandler : RuleActionHandler - { - private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients; - private readonly IScriptEngine scriptEngine; - private readonly IJsonSerializer serializer; + private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients; + private readonly IScriptEngine scriptEngine; + private readonly IJsonSerializer serializer; - public ElasticSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) - : base(formatter) + public ElasticSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) + : base(formatter) + { + clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key => { - clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key => + var config = new ConnectionConfiguration(key.Host); + + if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) { - var config = new ConnectionConfiguration(key.Host); + config = config.BasicAuthentication(key.Username, key.Password); + } - if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) - { - config = config.BasicAuthentication(key.Username, key.Password); - } + return new ElasticLowLevelClient(config); + }); - return new ElasticLowLevelClient(config); - }); + this.scriptEngine = scriptEngine; + this.serializer = serializer; + } - this.scriptEngine = scriptEngine; - this.serializer = serializer; - } + protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(EnrichedEvent @event, ElasticSearchAction action) + { + var delete = @event.ShouldDelete(scriptEngine, action.Delete); - protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(EnrichedEvent @event, ElasticSearchAction action) - { - var delete = @event.ShouldDelete(scriptEngine, action.Delete); + string contentId; - string contentId; + if (@event is IEnrichedEntityEvent enrichedEntityEvent) + { + contentId = enrichedEntityEvent.Id.ToString(); + } + else + { + contentId = DomainId.NewGuid().ToString(); + } - if (@event is IEnrichedEntityEvent enrichedEntityEvent) - { - contentId = enrichedEntityEvent.Id.ToString(); - } - else - { - contentId = DomainId.NewGuid().ToString(); - } + var ruleDescription = string.Empty; + var ruleJob = new ElasticSearchJob + { + IndexName = await FormatAsync(action.IndexName, @event), + ServerHost = action.Host.ToString(), + ServerUser = action.Username, + ServerPassword = action.Password, + ContentId = contentId + }; + + if (delete) + { + ruleDescription = $"Delete entry index: {action.IndexName}"; + } + else + { + ruleDescription = $"Upsert to index: {action.IndexName}"; - var ruleDescription = string.Empty; - var ruleJob = new ElasticSearchJob - { - IndexName = await FormatAsync(action.IndexName, @event), - ServerHost = action.Host.ToString(), - ServerUser = action.Username, - ServerPassword = action.Password, - ContentId = contentId - }; - - if (delete) - { - ruleDescription = $"Delete entry index: {action.IndexName}"; - } - else + ElasticSearchContent content; + try { - ruleDescription = $"Upsert to index: {action.IndexName}"; + string jsonString; - ElasticSearchContent content; - try + if (!string.IsNullOrEmpty(action.Document)) { - string jsonString; - - if (!string.IsNullOrEmpty(action.Document)) - { - jsonString = await FormatAsync(action.Document, @event); - jsonString = jsonString?.Trim(); - } - else - { - jsonString = ToJson(@event); - } - - content = serializer.Deserialize(jsonString); + jsonString = await FormatAsync(action.Document, @event); + jsonString = jsonString?.Trim(); } - catch (Exception ex) + else { - content = new ElasticSearchContent - { - More = new Dictionary - { - ["error"] = $"Invalid JSON: {ex.Message}" - } - }; + jsonString = ToJson(@event); } - content.ContentId = contentId; - - ruleJob.Content = serializer.Serialize(content, true); + content = serializer.Deserialize(jsonString); } + catch (Exception ex) + { + content = new ElasticSearchContent + { + More = new Dictionary + { + ["error"] = $"Invalid JSON: {ex.Message}" + } + }; + } + + content.ContentId = contentId; - return (ruleDescription, ruleJob); + ruleJob.Content = serializer.Serialize(content, true); } - protected override async Task ExecuteJobAsync(ElasticSearchJob job, - CancellationToken ct = default) + return (ruleDescription, ruleJob); + } + + protected override async Task ExecuteJobAsync(ElasticSearchJob job, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(job.ServerHost)) { - if (string.IsNullOrWhiteSpace(job.ServerHost)) - { - return Result.Ignored(); - } + return Result.Ignored(); + } - var client = await clients.GetClientAsync((new Uri(job.ServerHost, UriKind.Absolute), job.ServerUser, job.ServerPassword)); + var client = await clients.GetClientAsync((new Uri(job.ServerHost, UriKind.Absolute), job.ServerUser, job.ServerPassword)); - try + try + { + if (job.Content != null) { - if (job.Content != null) - { - var response = await client.IndexAsync(job.IndexName, job.ContentId, job.Content, ctx: ct); + var response = await client.IndexAsync(job.IndexName, job.ContentId, job.Content, ctx: ct); - return Result.SuccessOrFailed(response.OriginalException, response.Body); - } - else - { - var response = await client.DeleteAsync(job.IndexName, job.ContentId, ctx: ct); - - return Result.SuccessOrFailed(response.OriginalException, response.Body); - } + return Result.SuccessOrFailed(response.OriginalException, response.Body); } - catch (ElasticsearchClientException ex) + else { - return Result.Failed(ex); + var response = await client.DeleteAsync(job.IndexName, job.ContentId, ctx: ct); + + return Result.SuccessOrFailed(response.OriginalException, response.Body); } } + catch (ElasticsearchClientException ex) + { + return Result.Failed(ex); + } } +} - public sealed class ElasticSearchContent - { - public string ContentId { get; set; } +public sealed class ElasticSearchContent +{ + public string ContentId { get; set; } - [JsonExtensionData] - public Dictionary More { get; set; } = new Dictionary(); - } + [JsonExtensionData] + public Dictionary More { get; set; } = new Dictionary(); +} - public sealed class ElasticSearchJob - { - public string ServerHost { get; set; } +public sealed class ElasticSearchJob +{ + public string ServerHost { get; set; } - public string ServerUser { get; set; } + public string ServerUser { get; set; } - public string ServerPassword { get; set; } + public string ServerPassword { get; set; } - public string ContentId { get; set; } + public string ContentId { get; set; } - public string Content { get; set; } + public string Content { get; set; } - public string IndexName { get; set; } - } + public string IndexName { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs index a40f610b0d..44fcf26149 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.ElasticSearch +namespace Squidex.Extensions.Actions.ElasticSearch; + +public sealed class ElasticSearchPlugin : IPlugin { - public sealed class ElasticSearchPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs index 5274c0a76e..0c786363f8 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs @@ -10,60 +10,59 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Email +namespace Squidex.Extensions.Actions.Email; + +[RuleAction( + Title = "Email", + IconImage = "", + IconColor = "#333300", + Display = "Send an email", + Description = "Send an email with a custom SMTP server.", + ReadMore = "https://en.wikipedia.org/wiki/Email")] +public sealed record EmailAction : RuleAction { - [RuleAction( - Title = "Email", - IconImage = "", - IconColor = "#333300", - Display = "Send an email", - Description = "Send an email with a custom SMTP server.", - ReadMore = "https://en.wikipedia.org/wiki/Email")] - public sealed record EmailAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Server Host", Description = "The IP address or host to the SMTP server.")] - [Editor(RuleFieldEditor.Text)] - public string ServerHost { get; set; } + [LocalizedRequired] + [Display(Name = "Server Host", Description = "The IP address or host to the SMTP server.")] + [Editor(RuleFieldEditor.Text)] + public string ServerHost { get; set; } - [LocalizedRequired] - [Display(Name = "Server Port", Description = "The port to the SMTP server.")] - [Editor(RuleFieldEditor.Text)] - public int ServerPort { get; set; } + [LocalizedRequired] + [Display(Name = "Server Port", Description = "The port to the SMTP server.")] + [Editor(RuleFieldEditor.Text)] + public int ServerPort { get; set; } - [LocalizedRequired] - [Display(Name = "Username", Description = "The username for the SMTP server.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string ServerUsername { get; set; } + [LocalizedRequired] + [Display(Name = "Username", Description = "The username for the SMTP server.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string ServerUsername { get; set; } - [LocalizedRequired] - [Display(Name = "Password", Description = "The password for the SMTP server.")] - [Editor(RuleFieldEditor.Password)] - public string ServerPassword { get; set; } + [LocalizedRequired] + [Display(Name = "Password", Description = "The password for the SMTP server.")] + [Editor(RuleFieldEditor.Password)] + public string ServerPassword { get; set; } - [LocalizedRequired] - [Display(Name = "From Address", Description = "The email sending address.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string MessageFrom { get; set; } + [LocalizedRequired] + [Display(Name = "From Address", Description = "The email sending address.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string MessageFrom { get; set; } - [LocalizedRequired] - [Display(Name = "To Address", Description = "The email message will be sent to.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string MessageTo { get; set; } + [LocalizedRequired] + [Display(Name = "To Address", Description = "The email message will be sent to.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string MessageTo { get; set; } - [LocalizedRequired] - [Display(Name = "Subject", Description = "The subject line for this email message.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string MessageSubject { get; set; } + [LocalizedRequired] + [Display(Name = "Subject", Description = "The subject line for this email message.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string MessageSubject { get; set; } - [LocalizedRequired] - [Display(Name = "Body", Description = "The message body.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string MessageBody { get; set; } - } + [LocalizedRequired] + [Display(Name = "Body", Description = "The message body.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string MessageBody { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs index fee3d2e9ef..c394f48418 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs @@ -13,81 +13,80 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Email +namespace Squidex.Extensions.Actions.Email; + +public sealed class EmailActionHandler : RuleActionHandler { - public sealed class EmailActionHandler : RuleActionHandler + public EmailActionHandler(RuleEventFormatter formatter) + : base(formatter) { - public EmailActionHandler(RuleEventFormatter formatter) - : base(formatter) - { - } + } - protected override async Task<(string Description, EmailJob Data)> CreateJobAsync(EnrichedEvent @event, EmailAction action) + protected override async Task<(string Description, EmailJob Data)> CreateJobAsync(EnrichedEvent @event, EmailAction action) + { + var ruleJob = new EmailJob { - var ruleJob = new EmailJob - { - ServerHost = action.ServerHost, - ServerPassword = action.ServerPassword, - ServerPort = action.ServerPort, - ServerUsername = await FormatAsync(action.ServerUsername, @event), - MessageFrom = await FormatAsync(action.MessageFrom, @event), - MessageTo = await FormatAsync(action.MessageTo, @event), - MessageSubject = await FormatAsync(action.MessageSubject, @event), - MessageBody = await FormatAsync(action.MessageBody, @event) - }; - - var description = $"Send an email to {action.MessageTo}"; - - return (description, ruleJob); - } + ServerHost = action.ServerHost, + ServerPassword = action.ServerPassword, + ServerPort = action.ServerPort, + ServerUsername = await FormatAsync(action.ServerUsername, @event), + MessageFrom = await FormatAsync(action.MessageFrom, @event), + MessageTo = await FormatAsync(action.MessageTo, @event), + MessageSubject = await FormatAsync(action.MessageSubject, @event), + MessageBody = await FormatAsync(action.MessageBody, @event) + }; + + var description = $"Send an email to {action.MessageTo}"; + + return (description, ruleJob); + } - protected override async Task ExecuteJobAsync(EmailJob job, - CancellationToken ct = default) + protected override async Task ExecuteJobAsync(EmailJob job, + CancellationToken ct = default) + { + using (var smtpClient = new SmtpClient()) { - using (var smtpClient = new SmtpClient()) - { - await smtpClient.ConnectAsync(job.ServerHost, job.ServerPort, cancellationToken: ct); - - await smtpClient.AuthenticateAsync(job.ServerUsername, job.ServerPassword, ct); + await smtpClient.ConnectAsync(job.ServerHost, job.ServerPort, cancellationToken: ct); - var smtpMessage = new MimeMessage(); + await smtpClient.AuthenticateAsync(job.ServerUsername, job.ServerPassword, ct); - smtpMessage.From.Add(MailboxAddress.Parse( - job.MessageFrom)); + var smtpMessage = new MimeMessage(); - smtpMessage.To.Add(MailboxAddress.Parse( - job.MessageTo)); + smtpMessage.From.Add(MailboxAddress.Parse( + job.MessageFrom)); - smtpMessage.Body = new TextPart(TextFormat.Html) - { - Text = job.MessageBody - }; + smtpMessage.To.Add(MailboxAddress.Parse( + job.MessageTo)); - smtpMessage.Subject = job.MessageSubject; + smtpMessage.Body = new TextPart(TextFormat.Html) + { + Text = job.MessageBody + }; - await smtpClient.SendAsync(smtpMessage, ct); - } + smtpMessage.Subject = job.MessageSubject; - return Result.Complete(); + await smtpClient.SendAsync(smtpMessage, ct); } + + return Result.Complete(); } +} - public sealed class EmailJob - { - public int ServerPort { get; set; } +public sealed class EmailJob +{ + public int ServerPort { get; set; } - public string ServerHost { get; set; } + public string ServerHost { get; set; } - public string ServerUsername { get; set; } + public string ServerUsername { get; set; } - public string ServerPassword { get; set; } + public string ServerPassword { get; set; } - public string MessageFrom { get; set; } + public string MessageFrom { get; set; } - public string MessageTo { get; set; } + public string MessageTo { get; set; } - public string MessageSubject { get; set; } + public string MessageSubject { get; set; } - public string MessageBody { get; set; } - } + public string MessageBody { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs index dd9140866c..fc47a69061 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Email +namespace Squidex.Extensions.Actions.Email; + +public sealed class EmailPlugin : IPlugin { - public sealed class EmailPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs index 3743f96131..92b5237d1b 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs @@ -10,25 +10,24 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Fastly +namespace Squidex.Extensions.Actions.Fastly; + +[RuleAction( + Title = "Fastly", + IconImage = "", + IconColor = "#e23335", + Display = "Purge fastly cache", + Description = "Remove entries from the fastly CDN cache.", + ReadMore = "https://www.fastly.com/")] +public sealed record FastlyAction : RuleAction { - [RuleAction( - Title = "Fastly", - IconImage = "", - IconColor = "#e23335", - Display = "Purge fastly cache", - Description = "Remove entries from the fastly CDN cache.", - ReadMore = "https://www.fastly.com/")] - public sealed record FastlyAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] - [Editor(RuleFieldEditor.Text)] - public string ApiKey { get; set; } + [LocalizedRequired] + [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] + [Editor(RuleFieldEditor.Text)] + public string ApiKey { get; set; } - [LocalizedRequired] - [Display(Name = "Service Id", Description = "The ID of the fastly service.")] - [Editor(RuleFieldEditor.Text)] - public string ServiceId { get; set; } - } + [LocalizedRequired] + [Display(Name = "Service Id", Description = "The ID of the fastly service.")] + [Editor(RuleFieldEditor.Text)] + public string ServiceId { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs index bd0e3c2e7e..2d00ffa6fa 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs @@ -11,64 +11,63 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Fastly +namespace Squidex.Extensions.Actions.Fastly; + +public sealed class FastlyActionHandler : RuleActionHandler { - public sealed class FastlyActionHandler : RuleActionHandler + private const string Description = "Purge key in fastly"; + + private readonly IHttpClientFactory httpClientFactory; + + public FastlyActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) { - private const string Description = "Purge key in fastly"; + this.httpClientFactory = httpClientFactory; + } - private readonly IHttpClientFactory httpClientFactory; + protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) + { + var id = string.Empty; - public FastlyActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) + if (@event is IEnrichedEntityEvent entityEvent) { - this.httpClientFactory = httpClientFactory; + id = DomainId.Combine(@event.AppId.Id, entityEvent.Id).ToString(); } - protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) + var ruleJob = new FastlyJob { - var id = string.Empty; - - if (@event is IEnrichedEntityEvent entityEvent) - { - id = DomainId.Combine(@event.AppId.Id, entityEvent.Id).ToString(); - } - - var ruleJob = new FastlyJob - { - Key = id, - FastlyApiKey = action.ApiKey, - FastlyServiceID = action.ServiceId - }; + Key = id, + FastlyApiKey = action.ApiKey, + FastlyServiceID = action.ServiceId + }; - return (Description, ruleJob); - } + return (Description, ruleJob); + } - protected override async Task ExecuteJobAsync(FastlyJob job, - CancellationToken ct = default) + protected override async Task ExecuteJobAsync(FastlyJob job, + CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) { - using (var httpClient = httpClientFactory.CreateClient()) - { - httpClient.Timeout = TimeSpan.FromSeconds(2); + httpClient.Timeout = TimeSpan.FromSeconds(2); - var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}"; + var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}"; - using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)) - { - request.Headers.Add("Fastly-Key", job.FastlyApiKey); + using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)) + { + request.Headers.Add("Fastly-Key", job.FastlyApiKey); - return await httpClient.OneWayRequestAsync(request, ct: ct); - } + return await httpClient.OneWayRequestAsync(request, ct: ct); } } } +} - public sealed class FastlyJob - { - public string FastlyApiKey { get; set; } +public sealed class FastlyJob +{ + public string FastlyApiKey { get; set; } - public string FastlyServiceID { get; set; } + public string FastlyServiceID { get; set; } - public string Key { get; set; } - } + public string Key { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs index d5e4f76b93..6c8205c6a1 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Fastly +namespace Squidex.Extensions.Actions.Fastly; + +public sealed class FastlyPlugin : IPlugin { - public sealed class FastlyPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs index 59b0b4f0f1..fc1ada0e51 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs @@ -10,49 +10,48 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Kafka +namespace Squidex.Extensions.Actions.Kafka; + +[RuleAction( + Title = "Kafka", + IconImage = "", + IconColor = "#404244", + Display = "Push to kafka", + Description = "Connect to Kafka stream and push data to that stream.", + ReadMore = "https://kafka.apache.org/quickstart")] +public sealed record KafkaAction : RuleAction { - [RuleAction( - Title = "Kafka", - IconImage = "", - IconColor = "#404244", - Display = "Push to kafka", - Description = "Connect to Kafka stream and push data to that stream.", - ReadMore = "https://kafka.apache.org/quickstart")] - public sealed record KafkaAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Topic Name", Description = "The name of the topic.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string TopicName { get; set; } - - [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Payload { get; set; } - - [Display(Name = "Key", Description = "The message key, commonly used for partitioning.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string Key { get; set; } - - [Display(Name = "Partition Key", Description = "The partition key, only used when we don't want to define partiontionig with key.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string PartitionKey { get; set; } - - [Display(Name = "Partition Count", Description = "Define the number of partitions for specific topic.")] - [Editor(RuleFieldEditor.Text)] - public int PartitionCount { get; set; } - - [Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Headers { get; set; } - - [Display(Name = "Schema (Optional)", Description = "Define a specific AVRO schema in JSON format.")] - [Editor(RuleFieldEditor.TextArea)] - public string Schema { get; set; } - } + [LocalizedRequired] + [Display(Name = "Topic Name", Description = "The name of the topic.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string TopicName { get; set; } + + [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Payload { get; set; } + + [Display(Name = "Key", Description = "The message key, commonly used for partitioning.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string Key { get; set; } + + [Display(Name = "Partition Key", Description = "The partition key, only used when we don't want to define partiontionig with key.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string PartitionKey { get; set; } + + [Display(Name = "Partition Count", Description = "Define the number of partitions for specific topic.")] + [Editor(RuleFieldEditor.Text)] + public int PartitionCount { get; set; } + + [Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Headers { get; set; } + + [Display(Name = "Schema (Optional)", Description = "Define a specific AVRO schema in JSON format.")] + [Editor(RuleFieldEditor.TextArea)] + public string Schema { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs index edf6fd1739..76d42ff1f6 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs @@ -10,114 +10,113 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Kafka +namespace Squidex.Extensions.Actions.Kafka; + +public sealed class KafkaActionHandler : RuleActionHandler { - public sealed class KafkaActionHandler : RuleActionHandler + private const string Description = "Push to Kafka"; + private readonly KafkaProducer kafkaProducer; + + public KafkaActionHandler(RuleEventFormatter formatter, KafkaProducer kafkaProducer) + : base(formatter) + { + this.kafkaProducer = kafkaProducer; + } + + protected override async Task<(string Description, KafkaJob Data)> CreateJobAsync(EnrichedEvent @event, KafkaAction action) { - private const string Description = "Push to Kafka"; - private readonly KafkaProducer kafkaProducer; + string value, key; - public KafkaActionHandler(RuleEventFormatter formatter, KafkaProducer kafkaProducer) - : base(formatter) + if (!string.IsNullOrEmpty(action.Payload)) { - this.kafkaProducer = kafkaProducer; + value = await FormatAsync(action.Payload, @event); } - - protected override async Task<(string Description, KafkaJob Data)> CreateJobAsync(EnrichedEvent @event, KafkaAction action) + else { - string value, key; + value = ToEnvelopeJson(@event); + } - if (!string.IsNullOrEmpty(action.Payload)) - { - value = await FormatAsync(action.Payload, @event); - } - else - { - value = ToEnvelopeJson(@event); - } + if (!string.IsNullOrEmpty(action.Key)) + { + key = await FormatAsync(action.Key, @event); + } + else + { + key = @event.Name; + } - if (!string.IsNullOrEmpty(action.Key)) - { - key = await FormatAsync(action.Key, @event); - } - else - { - key = @event.Name; - } + var ruleJob = new KafkaJob + { + TopicName = action.TopicName, + MessageKey = key, + MessageValue = value, + Headers = await ParseHeadersAsync(action.Headers, @event), + Schema = action.Schema, + PartitionKey = await FormatAsync(action.PartitionKey, @event), + PartitionCount = action.PartitionCount + }; + + return (Description, ruleJob); + } - var ruleJob = new KafkaJob - { - TopicName = action.TopicName, - MessageKey = key, - MessageValue = value, - Headers = await ParseHeadersAsync(action.Headers, @event), - Schema = action.Schema, - PartitionKey = await FormatAsync(action.PartitionKey, @event), - PartitionCount = action.PartitionCount - }; - - return (Description, ruleJob); + private async Task> ParseHeadersAsync(string headers, EnrichedEvent @event) + { + if (string.IsNullOrWhiteSpace(headers)) + { + return null; } - private async Task> ParseHeadersAsync(string headers, EnrichedEvent @event) - { - if (string.IsNullOrWhiteSpace(headers)) - { - return null; - } + var headersDictionary = new Dictionary(); - var headersDictionary = new Dictionary(); + var lines = headers.Split('\n'); - var lines = headers.Split('\n'); + foreach (var line in lines) + { + var indexEqual = line.IndexOf('=', StringComparison.Ordinal); - foreach (var line in lines) + if (indexEqual > 0 && indexEqual < line.Length - 1) { - var indexEqual = line.IndexOf('=', StringComparison.Ordinal); + var key = line[..indexEqual]; + var val = line[(indexEqual + 1)..]; - if (indexEqual > 0 && indexEqual < line.Length - 1) - { - var key = line[..indexEqual]; - var val = line[(indexEqual + 1)..]; + val = await FormatAsync(val, @event); - val = await FormatAsync(val, @event); - - headersDictionary[key] = val; - } + headersDictionary[key] = val; } - - return headersDictionary; } - protected override async Task ExecuteJobAsync(KafkaJob job, - CancellationToken ct = default) + return headersDictionary; + } + + protected override async Task ExecuteJobAsync(KafkaJob job, + CancellationToken ct = default) + { + try { - try - { - await kafkaProducer.SendAsync(job, ct); + await kafkaProducer.SendAsync(job, ct); - return Result.Success($"Event pushed to {job.TopicName} kafka topic with {job.MessageKey} message key."); - } - catch (Exception ex) - { - return Result.Failed(ex, $"Push to Kafka failed: {ex}"); - } + return Result.Success($"Event pushed to {job.TopicName} kafka topic with {job.MessageKey} message key."); + } + catch (Exception ex) + { + return Result.Failed(ex, $"Push to Kafka failed: {ex}"); } } +} - public sealed class KafkaJob - { - public string TopicName { get; set; } +public sealed class KafkaJob +{ + public string TopicName { get; set; } - public string MessageKey { get; set; } + public string MessageKey { get; set; } - public string MessageValue { get; set; } + public string MessageValue { get; set; } - public string Schema { get; set; } + public string Schema { get; set; } - public string PartitionKey { get; set; } + public string PartitionKey { get; set; } - public Dictionary Headers { get; set; } + public Dictionary Headers { get; set; } - public int PartitionCount { get; set; } - } + public int PartitionCount { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs index 580de48ee6..7761d4d43e 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs @@ -10,21 +10,20 @@ using Microsoft.Extensions.Options; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Kafka +namespace Squidex.Extensions.Actions.Kafka; + +public sealed class KafkaPlugin : IPlugin { - public sealed class KafkaPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - var options = config.GetSection("kafka").Get() ?? new (); + var options = config.GetSection("kafka").Get() ?? new (); - if (options.IsProducerConfigured()) - { - services.AddRuleAction(); + if (options.IsProducerConfigured()) + { + services.AddRuleAction(); - services.AddSingleton(); - services.AddSingleton(Options.Create(options)); - } + services.AddSingleton(); + services.AddSingleton(Options.Create(options)); } } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs index 0987e52349..1f8dc5467b 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs @@ -17,267 +17,266 @@ using Squidex.Infrastructure.Json.Objects; using Schema = Avro.Schema; -namespace Squidex.Extensions.Actions.Kafka -{ - public sealed class KafkaProducer - { - private readonly IProducer textProducer; - private readonly IProducer avroProducer; - private readonly ISchemaRegistryClient schemaRegistry; - private readonly IJsonSerializer jsonSerializer; +namespace Squidex.Extensions.Actions.Kafka; - public KafkaProducer(IOptions options, IJsonSerializer jsonSerializer, - ILogger log) - { - this.jsonSerializer = jsonSerializer; +public sealed class KafkaProducer +{ + private readonly IProducer textProducer; + private readonly IProducer avroProducer; + private readonly ISchemaRegistryClient schemaRegistry; + private readonly IJsonSerializer jsonSerializer; - textProducer = new ProducerBuilder(options.Value) - .SetErrorHandler((p, error) => - { - LogError(log, error); - }) - .SetLogHandler((p, message) => - { - LogMessage(log, message); - }) - .SetKeySerializer(Serializers.Utf8) - .SetValueSerializer(Serializers.Utf8) - .Build(); + public KafkaProducer(IOptions options, IJsonSerializer jsonSerializer, + ILogger log) + { + this.jsonSerializer = jsonSerializer; - if (options.Value.IsSchemaRegistryConfigured()) + textProducer = new ProducerBuilder(options.Value) + .SetErrorHandler((p, error) => { - schemaRegistry = new CachedSchemaRegistryClient(options.Value.SchemaRegistry); - - avroProducer = new ProducerBuilder(options.Value) - .SetErrorHandler((p, error) => - { - LogError(log, error); - }) - .SetLogHandler((p, message) => - { - LogMessage(log, message); - }) - .SetKeySerializer(Serializers.Utf8) - .SetValueSerializer(new AvroSerializer(schemaRegistry, options.Value.AvroSerializer)) - .Build(); - } - } - - private static void LogMessage(ILogger log, LogMessage message) - { - var level = LogLevel.Information; - - switch (message.Level) + LogError(log, error); + }) + .SetLogHandler((p, message) => { - case SyslogLevel.Emergency: - level = LogLevel.Error; - break; - case SyslogLevel.Alert: - level = LogLevel.Error; - break; - case SyslogLevel.Critical: - level = LogLevel.Error; - break; - case SyslogLevel.Error: - level = LogLevel.Error; - break; - case SyslogLevel.Warning: - level = LogLevel.Warning; - break; - case SyslogLevel.Notice: - level = LogLevel.Information; - break; - case SyslogLevel.Info: - level = LogLevel.Information; - break; - case SyslogLevel.Debug: - level = LogLevel.Debug; - break; - } + LogMessage(log, message); + }) + .SetKeySerializer(Serializers.Utf8) + .SetValueSerializer(Serializers.Utf8) + .Build(); - log.Log(level, "Kafka log {name}: {message}.", message.Name, message.Message); + if (options.Value.IsSchemaRegistryConfigured()) + { + schemaRegistry = new CachedSchemaRegistryClient(options.Value.SchemaRegistry); + + avroProducer = new ProducerBuilder(options.Value) + .SetErrorHandler((p, error) => + { + LogError(log, error); + }) + .SetLogHandler((p, message) => + { + LogMessage(log, message); + }) + .SetKeySerializer(Serializers.Utf8) + .SetValueSerializer(new AvroSerializer(schemaRegistry, options.Value.AvroSerializer)) + .Build(); } + } + + private static void LogMessage(ILogger log, LogMessage message) + { + var level = LogLevel.Information; - private static void LogError(ILogger log, Error error) + switch (message.Level) { - log.LogWarning("Kafka error with {code} and {reason}.", error.Code, error.Reason); + case SyslogLevel.Emergency: + level = LogLevel.Error; + break; + case SyslogLevel.Alert: + level = LogLevel.Error; + break; + case SyslogLevel.Critical: + level = LogLevel.Error; + break; + case SyslogLevel.Error: + level = LogLevel.Error; + break; + case SyslogLevel.Warning: + level = LogLevel.Warning; + break; + case SyslogLevel.Notice: + level = LogLevel.Information; + break; + case SyslogLevel.Info: + level = LogLevel.Information; + break; + case SyslogLevel.Debug: + level = LogLevel.Debug; + break; } - public async Task SendAsync(KafkaJob job, - CancellationToken ct) + log.Log(level, "Kafka log {name}: {message}.", message.Name, message.Message); + } + + private static void LogError(ILogger log, Error error) + { + log.LogWarning("Kafka error with {code} and {reason}.", error.Code, error.Reason); + } + + public async Task SendAsync(KafkaJob job, + CancellationToken ct) + { + if (!string.IsNullOrWhiteSpace(job.Schema)) { - if (!string.IsNullOrWhiteSpace(job.Schema)) - { - var value = CreateAvroRecord(job.MessageValue, job.Schema); + var value = CreateAvroRecord(job.MessageValue, job.Schema); - var message = new Message { Value = value }; + var message = new Message { Value = value }; - await ProduceAsync(avroProducer, message, job, ct); - } - else - { - var message = new Message { Value = job.MessageValue }; + await ProduceAsync(avroProducer, message, job, ct); + } + else + { + var message = new Message { Value = job.MessageValue }; - await ProduceAsync(textProducer, message, job, ct); - } + await ProduceAsync(textProducer, message, job, ct); } + } + + private static async Task ProduceAsync(IProducer producer, Message message, KafkaJob job, + CancellationToken ct) + { + message.Key = job.MessageKey; - private static async Task ProduceAsync(IProducer producer, Message message, KafkaJob job, - CancellationToken ct) + if (job.Headers?.Count > 0) { - message.Key = job.MessageKey; + message.Headers = new Headers(); - if (job.Headers?.Count > 0) + foreach (var header in job.Headers) { - message.Headers = new Headers(); - - foreach (var header in job.Headers) - { - message.Headers.Add(header.Key, Encoding.UTF8.GetBytes(header.Value)); - } + message.Headers.Add(header.Key, Encoding.UTF8.GetBytes(header.Value)); } + } - if (!string.IsNullOrWhiteSpace(job.PartitionKey) && job.PartitionCount > 0) - { - var partition = Math.Abs(job.PartitionKey.GetHashCode(StringComparison.Ordinal)) % job.PartitionCount; + if (!string.IsNullOrWhiteSpace(job.PartitionKey) && job.PartitionCount > 0) + { + var partition = Math.Abs(job.PartitionKey.GetHashCode(StringComparison.Ordinal)) % job.PartitionCount; - await producer.ProduceAsync(new TopicPartition(job.TopicName, partition), message, ct); - } - else - { - await producer.ProduceAsync(job.TopicName, message, ct); - } + await producer.ProduceAsync(new TopicPartition(job.TopicName, partition), message, ct); + } + else + { + await producer.ProduceAsync(job.TopicName, message, ct); } + } - private GenericRecord CreateAvroRecord(string json, string avroSchema) + private GenericRecord CreateAvroRecord(string json, string avroSchema) + { + try { - try - { - var schema = (RecordSchema)Schema.Parse(avroSchema); + var schema = (RecordSchema)Schema.Parse(avroSchema); - var jsonObject = jsonSerializer.Deserialize(json); + var jsonObject = jsonSerializer.Deserialize(json); - var result = (GenericRecord)GetValue(jsonObject, schema); + var result = (GenericRecord)GetValue(jsonObject, schema); - return result; - } - catch (JsonException ex) - { - throw new InvalidOperationException($"Failed to parse json: {json}, got {ex.Message}", ex); - } + return result; } - - public void Dispose() + catch (JsonException ex) { - textProducer?.Dispose(); - avroProducer?.Dispose(); + throw new InvalidOperationException($"Failed to parse json: {json}, got {ex.Message}", ex); } + } + + public void Dispose() + { + textProducer?.Dispose(); + avroProducer?.Dispose(); + } - private static object GetValue(JsonValue value, Schema schema) + private static object GetValue(JsonValue value, Schema schema) + { + switch (value.Value) { - switch (value.Value) - { - case bool b when IsTypeOrUnionWith(schema, Schema.Type.Boolean): - return b; - case double d when IsTypeOrUnionWith(schema, Schema.Type.Long): - return (long)d; - case double d when IsTypeOrUnionWith(schema, Schema.Type.Float): - return (float)d; - case double d when IsTypeOrUnionWith(schema, Schema.Type.Int): - return (int)d; - case double d when IsTypeOrUnionWith(schema, Schema.Type.Double): - return d; - case string s when IsTypeOrUnionWith(schema, Schema.Type.String): - return s; - case JsonObject o when IsTypeOrUnionWith(schema, Schema.Type.Map): + case bool b when IsTypeOrUnionWith(schema, Schema.Type.Boolean): + return b; + case double d when IsTypeOrUnionWith(schema, Schema.Type.Long): + return (long)d; + case double d when IsTypeOrUnionWith(schema, Schema.Type.Float): + return (float)d; + case double d when IsTypeOrUnionWith(schema, Schema.Type.Int): + return (int)d; + case double d when IsTypeOrUnionWith(schema, Schema.Type.Double): + return d; + case string s when IsTypeOrUnionWith(schema, Schema.Type.String): + return s; + case JsonObject o when IsTypeOrUnionWith(schema, Schema.Type.Map): + { + var mapResult = new Dictionary(); + + if (schema is UnionSchema union) { - var mapResult = new Dictionary(); + var map = (MapSchema)union.Schemas.FirstOrDefault(x => x.Tag == Schema.Type.Map); - if (schema is UnionSchema union) + foreach (var (key, childValue) in o) { - var map = (MapSchema)union.Schemas.FirstOrDefault(x => x.Tag == Schema.Type.Map); - - foreach (var (key, childValue) in o) - { - mapResult.Add(key, GetValue(childValue, map?.ValueSchema)); - } + mapResult.Add(key, GetValue(childValue, map?.ValueSchema)); } - else if (schema is MapSchema map) + } + else if (schema is MapSchema map) + { + foreach (var (key, childValue) in o) { - foreach (var (key, childValue) in o) - { - mapResult.Add(key, GetValue(childValue, map?.ValueSchema)); - } + mapResult.Add(key, GetValue(childValue, map?.ValueSchema)); } - - return mapResult; } - case JsonObject o when IsTypeOrUnionWith(schema, Schema.Type.Record): - { - GenericRecord result = null; + return mapResult; + } - if (schema is UnionSchema union) - { - var record = (RecordSchema)union.Schemas.FirstOrDefault(x => x.Tag == Schema.Type.Record); + case JsonObject o when IsTypeOrUnionWith(schema, Schema.Type.Record): + { + GenericRecord result = null; - result = new GenericRecord(record); + if (schema is UnionSchema union) + { + var record = (RecordSchema)union.Schemas.FirstOrDefault(x => x.Tag == Schema.Type.Record); + + result = new GenericRecord(record); - foreach (var (key, childValue) in o) + foreach (var (key, childValue) in o) + { + if (record != null && record.TryGetField(key, out var field)) { - if (record != null && record.TryGetField(key, out var field)) - { - result.Add(key, GetValue(childValue, field.Schema)); - } + result.Add(key, GetValue(childValue, field.Schema)); } } - else if (schema is RecordSchema record) - { - result = new GenericRecord(record); + } + else if (schema is RecordSchema record) + { + result = new GenericRecord(record); - foreach (var (key, childValue) in o) + foreach (var (key, childValue) in o) + { + if (record.TryGetField(key, out var field)) { - if (record.TryGetField(key, out var field)) - { - result.Add(key, GetValue(childValue, field.Schema)); - } + result.Add(key, GetValue(childValue, field.Schema)); } } - - return result; } - case JsonArray a when IsTypeOrUnionWith(schema, Schema.Type.Array): + return result; + } + + case JsonArray a when IsTypeOrUnionWith(schema, Schema.Type.Array): + { + var result = new List(); + + if (schema is UnionSchema union) { - var result = new List(); + var arraySchema = (ArraySchema)union.Schemas.FirstOrDefault(x => x.Tag == Schema.Type.Array); - if (schema is UnionSchema union) + foreach (var item in a) { - var arraySchema = (ArraySchema)union.Schemas.FirstOrDefault(x => x.Tag == Schema.Type.Array); - - foreach (var item in a) - { - result.Add(GetValue(item, arraySchema?.ItemSchema)); - } + result.Add(GetValue(item, arraySchema?.ItemSchema)); } - else if (schema is ArraySchema array) + } + else if (schema is ArraySchema array) + { + foreach (var item in a) { - foreach (var item in a) - { - result.Add(GetValue(item, array.ItemSchema)); - } + result.Add(GetValue(item, array.ItemSchema)); } - - return result.ToArray(); } - } - return null; + return result.ToArray(); + } } - private static bool IsTypeOrUnionWith(Schema schema, Schema.Type expected) - { - return schema.Tag == expected || (schema is UnionSchema union && union.Schemas.Any(x => x.Tag == expected)); - } + return null; + } + + private static bool IsTypeOrUnionWith(Schema schema, Schema.Type expected) + { + return schema.Tag == expected || (schema is UnionSchema union && union.Schemas.Any(x => x.Tag == expected)); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs index 36b81de0d2..654593c224 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs @@ -9,22 +9,21 @@ using Confluent.SchemaRegistry; using Confluent.SchemaRegistry.Serdes; -namespace Squidex.Extensions.Actions.Kafka +namespace Squidex.Extensions.Actions.Kafka; + +public class KafkaProducerOptions : ProducerConfig { - public class KafkaProducerOptions : ProducerConfig - { - public SchemaRegistryConfig SchemaRegistry { get; set; } + public SchemaRegistryConfig SchemaRegistry { get; set; } - public AvroSerializerConfig AvroSerializer { get; set; } + public AvroSerializerConfig AvroSerializer { get; set; } - public bool IsProducerConfigured() - { - return !string.IsNullOrWhiteSpace(BootstrapServers); - } + public bool IsProducerConfigured() + { + return !string.IsNullOrWhiteSpace(BootstrapServers); + } - public bool IsSchemaRegistryConfigured() - { - return !string.IsNullOrWhiteSpace(SchemaRegistry?.Url); - } + public bool IsSchemaRegistryConfigured() + { + return !string.IsNullOrWhiteSpace(SchemaRegistry?.Url); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs index 12658840c1..8308245407 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs @@ -10,50 +10,49 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Medium +namespace Squidex.Extensions.Actions.Medium; + +[RuleAction( + Title = "Medium", + IconImage = "", + IconColor = "#00ab6c", + Display = "Post to Medium", + Description = "Create a new story or post at medium.", + ReadMore = "https://medium.com/")] +public sealed record MediumAction : RuleAction { - [RuleAction( - Title = "Medium", - IconImage = "", - IconColor = "#00ab6c", - Display = "Post to Medium", - Description = "Create a new story or post at medium.", - ReadMore = "https://medium.com/")] - public sealed record MediumAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Access Token", Description = "The self issued access token.")] - [Editor(RuleFieldEditor.Text)] - public string AccessToken { get; set; } - - [LocalizedRequired] - [Display(Name = "Title", Description = "The title, used for the url.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string Title { get; set; } - - [LocalizedRequired] - [Display(Name = "Content", Description = "The content, either html or markdown.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Content { get; set; } - - [Display(Name = "Canonical Url", Description = "The original home of this content, if it was originally published elsewhere.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string CanonicalUrl { get; set; } - - [Display(Name = "Tags", Description = "The optional comma separated list of tags.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string Tags { get; set; } - - [Display(Name = "Publication Id", Description = "Optional publication id.")] - [Editor(RuleFieldEditor.Text)] - public string PublicationId { get; set; } - - [Display(Name = "Is Html", Description = "Indicates whether the content is markdown or html.")] - [Editor(RuleFieldEditor.Text)] - public bool IsHtml { get; set; } - } + [LocalizedRequired] + [Display(Name = "Access Token", Description = "The self issued access token.")] + [Editor(RuleFieldEditor.Text)] + public string AccessToken { get; set; } + + [LocalizedRequired] + [Display(Name = "Title", Description = "The title, used for the url.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string Title { get; set; } + + [LocalizedRequired] + [Display(Name = "Content", Description = "The content, either html or markdown.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Content { get; set; } + + [Display(Name = "Canonical Url", Description = "The original home of this content, if it was originally published elsewhere.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string CanonicalUrl { get; set; } + + [Display(Name = "Tags", Description = "The optional comma separated list of tags.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string Tags { get; set; } + + [Display(Name = "Publication Id", Description = "Optional publication id.")] + [Editor(RuleFieldEditor.Text)] + public string PublicationId { get; set; } + + [Display(Name = "Is Html", Description = "Indicates whether the content is markdown or html.")] + [Editor(RuleFieldEditor.Text)] + public bool IsHtml { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs index 1554d665d1..c1dfa1beee 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs @@ -13,142 +13,141 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Medium +namespace Squidex.Extensions.Actions.Medium; + +public sealed class MediumActionHandler : RuleActionHandler { - public sealed class MediumActionHandler : RuleActionHandler + private const string Description = "Post to medium"; + + private readonly IHttpClientFactory httpClientFactory; + private readonly IJsonSerializer serializer; + + private sealed class UserResponse { - private const string Description = "Post to medium"; + public UserResponseData Data { get; set; } + } - private readonly IHttpClientFactory httpClientFactory; - private readonly IJsonSerializer serializer; + private sealed class UserResponseData + { + public string Id { get; set; } + } - private sealed class UserResponse + public MediumActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory, IJsonSerializer serializer) + : base(formatter) + { + this.httpClientFactory = httpClientFactory; + + this.serializer = serializer; + } + + protected override async Task<(string Description, MediumJob Data)> CreateJobAsync(EnrichedEvent @event, MediumAction action) + { + var ruleJob = new MediumJob { AccessToken = action.AccessToken, PublicationId = action.PublicationId }; + + var requestBody = new { - public UserResponseData Data { get; set; } - } + title = await FormatAsync(action.Title, @event), + contentFormat = action.IsHtml ? "html" : "markdown", + content = await FormatAsync(action.Content, @event), + canonicalUrl = await FormatAsync(action.CanonicalUrl, @event), + tags = await ParseTagsAsync(@event, action) + }; - private sealed class UserResponseData + ruleJob.RequestBody = ToJson(requestBody); + + return (Description, ruleJob); + } + + private async Task ParseTagsAsync(EnrichedEvent @event, MediumAction action) + { + if (string.IsNullOrWhiteSpace(action.Tags)) { - public string Id { get; set; } + return null; } - public MediumActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory, IJsonSerializer serializer) - : base(formatter) + try { - this.httpClientFactory = httpClientFactory; + var jsonTags = await FormatAsync(action.Tags, @event); - this.serializer = serializer; + return serializer.Deserialize(jsonTags); } - - protected override async Task<(string Description, MediumJob Data)> CreateJobAsync(EnrichedEvent @event, MediumAction action) + catch { - var ruleJob = new MediumJob { AccessToken = action.AccessToken, PublicationId = action.PublicationId }; - - var requestBody = new - { - title = await FormatAsync(action.Title, @event), - contentFormat = action.IsHtml ? "html" : "markdown", - content = await FormatAsync(action.Content, @event), - canonicalUrl = await FormatAsync(action.CanonicalUrl, @event), - tags = await ParseTagsAsync(@event, action) - }; - - ruleJob.RequestBody = ToJson(requestBody); - - return (Description, ruleJob); + return action.Tags.Split(','); } + } - private async Task ParseTagsAsync(EnrichedEvent @event, MediumAction action) + protected override async Task ExecuteJobAsync(MediumJob job, + CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) { - if (string.IsNullOrWhiteSpace(action.Tags)) - { - return null; - } + httpClient.Timeout = TimeSpan.FromSeconds(4); + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + httpClient.DefaultRequestHeaders.Add("Accept-Charset", "utf-8"); + httpClient.DefaultRequestHeaders.Add("User-Agent", "Squidex Headless CMS"); - try - { - var jsonTags = await FormatAsync(action.Tags, @event); + string path; - return serializer.Deserialize(jsonTags); - } - catch + if (!string.IsNullOrWhiteSpace(job.PublicationId)) { - return action.Tags.Split(','); + path = $"v1/publications/{job.PublicationId}/posts"; } - } - - protected override async Task ExecuteJobAsync(MediumJob job, - CancellationToken ct = default) - { - using (var httpClient = httpClientFactory.CreateClient()) + else { - httpClient.Timeout = TimeSpan.FromSeconds(4); - httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); - httpClient.DefaultRequestHeaders.Add("Accept-Charset", "utf-8"); - httpClient.DefaultRequestHeaders.Add("User-Agent", "Squidex Headless CMS"); - - string path; + HttpResponseMessage response = null; - if (!string.IsNullOrWhiteSpace(job.PublicationId)) + var meRequest = BuildMeRequest(job); + try { - path = $"v1/publications/{job.PublicationId}/posts"; - } - else - { - HttpResponseMessage response = null; - - var meRequest = BuildMeRequest(job); - try - { - response = await httpClient.SendAsync(meRequest, ct); - - var responseString = await response.Content.ReadAsStringAsync(ct); - var responseJson = serializer.Deserialize(responseString); + response = await httpClient.SendAsync(meRequest, ct); - var id = responseJson.Data?.Id; + var responseString = await response.Content.ReadAsStringAsync(ct); + var responseJson = serializer.Deserialize(responseString); - path = $"v1/users/{id}/posts"; - } - catch (Exception ex) - { - var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString()); + var id = responseJson.Data?.Id; - return Result.Failed(ex, requestDump); - } + path = $"v1/users/{id}/posts"; } + catch (Exception ex) + { + var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString()); - return await httpClient.OneWayRequestAsync(BuildPostRequest(job, path), job.RequestBody, ct); + return Result.Failed(ex, requestDump); + } } - } - private static HttpRequestMessage BuildPostRequest(MediumJob job, string path) - { - var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.medium.com/{path}") - { - Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") - }; - - request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); - - return request; + return await httpClient.OneWayRequestAsync(BuildPostRequest(job, path), job.RequestBody, ct); } + } - private static HttpRequestMessage BuildMeRequest(MediumJob job) + private static HttpRequestMessage BuildPostRequest(MediumJob job, string path) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.medium.com/{path}") { - var request = new HttpRequestMessage(HttpMethod.Get, "https://api.medium.com/v1/me"); + Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") + }; - request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); + request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); - return request; - } + return request; } - public sealed class MediumJob + private static HttpRequestMessage BuildMeRequest(MediumJob job) { - public string RequestBody { get; set; } + var request = new HttpRequestMessage(HttpMethod.Get, "https://api.medium.com/v1/me"); - public string PublicationId { get; set; } + request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); - public string AccessToken { get; set; } + return request; } } + +public sealed class MediumJob +{ + public string RequestBody { get; set; } + + public string PublicationId { get; set; } + + public string AccessToken { get; set; } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs index 5d8839f899..138d369fd3 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Medium +namespace Squidex.Extensions.Actions.Medium; + +public sealed class MediumPlugin : IPlugin { - public sealed class MediumPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs index 9fed53b130..c019ed3284 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs @@ -10,34 +10,33 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Notification +namespace Squidex.Extensions.Actions.Notification; + +[RuleAction( + Title = "Notification", + IconImage = "", + IconColor = "#3389ff", + Display = "Send a notification", + Description = "Send an integrated notification to a user.")] +public sealed record NotificationAction : RuleAction { - [RuleAction( - Title = "Notification", - IconImage = "", - IconColor = "#3389ff", - Display = "Send a notification", - Description = "Send an integrated notification to a user.")] - public sealed record NotificationAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "User", Description = "The user id or email.")] - [Editor(RuleFieldEditor.Text)] - public string User { get; set; } + [LocalizedRequired] + [Display(Name = "User", Description = "The user id or email.")] + [Editor(RuleFieldEditor.Text)] + public string User { get; set; } - [LocalizedRequired] - [Display(Name = "Title", Description = "The text to send.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Text { get; set; } + [LocalizedRequired] + [Display(Name = "Title", Description = "The text to send.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Text { get; set; } - [Display(Name = "Url", Description = "The optional url to attach to the notification.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string Url { get; set; } + [Display(Name = "Url", Description = "The optional url to attach to the notification.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string Url { get; set; } - [Display(Name = "Client", Description = "An optional client name.")] - [Editor(RuleFieldEditor.Text)] - public string Client { get; set; } - } + [Display(Name = "Client", Description = "An optional client name.")] + [Editor(RuleFieldEditor.Text)] + public string Client { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs index 2afa53582c..7d933b418a 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs @@ -12,79 +12,78 @@ using Squidex.Infrastructure.Commands; using Squidex.Shared.Users; -namespace Squidex.Extensions.Actions.Notification +namespace Squidex.Extensions.Actions.Notification; + +public sealed class NotificationActionHandler : RuleActionHandler { - public sealed class NotificationActionHandler : RuleActionHandler - { - private const string Description = "Send a Notification"; - private readonly ICommandBus commandBus; - private readonly IUserResolver userResolver; + private const string Description = "Send a Notification"; + private readonly ICommandBus commandBus; + private readonly IUserResolver userResolver; - public NotificationActionHandler(RuleEventFormatter formatter, ICommandBus commandBus, IUserResolver userResolver) - : base(formatter) - { - this.commandBus = commandBus; + public NotificationActionHandler(RuleEventFormatter formatter, ICommandBus commandBus, IUserResolver userResolver) + : base(formatter) + { + this.commandBus = commandBus; - this.userResolver = userResolver; - } + this.userResolver = userResolver; + } - protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action) + protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action) + { + if (@event is EnrichedUserEventBase userEvent) { - if (@event is EnrichedUserEventBase userEvent) + var user = await userResolver.FindByIdOrEmailAsync(action.User); + + if (user == null) { - var user = await userResolver.FindByIdOrEmailAsync(action.User); + throw new InvalidOperationException($"Cannot find user by '{action.User}'"); + } - if (user == null) - { - throw new InvalidOperationException($"Cannot find user by '{action.User}'"); - } + var actor = userEvent.Actor; - var actor = userEvent.Actor; + if (!string.IsNullOrEmpty(action.Client)) + { + actor = RefToken.Client(action.Client); + } - if (!string.IsNullOrEmpty(action.Client)) - { - actor = RefToken.Client(action.Client); - } + var ruleJob = new CreateComment + { + AppId = CommentsCommand.NoApp, + Actor = actor, + CommentId = DomainId.NewGuid(), + CommentsId = DomainId.Create(user.Id), + FromRule = true, + Text = await FormatAsync(action.Text, @event) + }; + + if (!string.IsNullOrWhiteSpace(action.Url)) + { + var url = await FormatAsync(action.Url, @event); - var ruleJob = new CreateComment + if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri)) { - AppId = CommentsCommand.NoApp, - Actor = actor, - CommentId = DomainId.NewGuid(), - CommentsId = DomainId.Create(user.Id), - FromRule = true, - Text = await FormatAsync(action.Text, @event) - }; - - if (!string.IsNullOrWhiteSpace(action.Url)) - { - var url = await FormatAsync(action.Url, @event); - - if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri)) - { - ruleJob.Url = uri; - } + ruleJob.Url = uri; } - - return (Description, ruleJob); } - return ("Ignore", new CreateComment()); + return (Description, ruleJob); } - protected override async Task ExecuteJobAsync(CreateComment job, - CancellationToken ct = default) - { - var command = job; - - if (command.CommentsId == default) - { - return Result.Ignored(); - } + return ("Ignore", new CreateComment()); + } - await commandBus.PublishAsync(command, ct); + protected override async Task ExecuteJobAsync(CreateComment job, + CancellationToken ct = default) + { + var command = job; - return Result.Success($"Notified: {command.Text}"); + if (command.CommentsId == default) + { + return Result.Ignored(); } + + await commandBus.PublishAsync(command, ct); + + return Result.Success($"Notified: {command.Text}"); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs index 7a3006b27d..67e987b746 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Notification +namespace Squidex.Extensions.Actions.Notification; + +public sealed class NotificationPlugin : IPlugin { - public sealed class NotificationPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs index e22acf0e07..2d8c18cfb8 100644 --- a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs @@ -10,44 +10,43 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.OpenSearch +namespace Squidex.Extensions.Actions.OpenSearch; + +[RuleAction( + Title = "OpenSearch", + IconImage = "", + IconColor = "#005EB8", + Display = "Populate OpenSearch index", + Description = "Populate a full text search index in OpenSearch.", + ReadMore = "https://opensearch.org/")] +public sealed record OpenSearchAction : RuleAction { - [RuleAction( - Title = "OpenSearch", - IconImage = "", - IconColor = "#005EB8", - Display = "Populate OpenSearch index", - Description = "Populate a full text search index in OpenSearch.", - ReadMore = "https://opensearch.org/")] - public sealed record OpenSearchAction : RuleAction - { - [AbsoluteUrl] - [LocalizedRequired] - [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] - [Editor(RuleFieldEditor.Url)] - public Uri Host { get; set; } - - [LocalizedRequired] - [Display(Name = "Index Name", Description = "The name of the index.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string IndexName { get; set; } - - [Display(Name = "Username", Description = "The optional username.")] - [Editor(RuleFieldEditor.Text)] - public string Username { get; set; } - - [Display(Name = "Password", Description = "The optional password.")] - [Editor(RuleFieldEditor.Text)] - public string Password { get; set; } - - [Display(Name = "Document", Description = "The optional custom document.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Document { get; set; } - - [Display(Name = "Deletion", Description = "The condition when to delete the document.")] - [Editor(RuleFieldEditor.Text)] - public string Delete { get; set; } - } + [AbsoluteUrl] + [LocalizedRequired] + [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] + [Editor(RuleFieldEditor.Url)] + public Uri Host { get; set; } + + [LocalizedRequired] + [Display(Name = "Index Name", Description = "The name of the index.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string IndexName { get; set; } + + [Display(Name = "Username", Description = "The optional username.")] + [Editor(RuleFieldEditor.Text)] + public string Username { get; set; } + + [Display(Name = "Password", Description = "The optional password.")] + [Editor(RuleFieldEditor.Text)] + public string Password { get; set; } + + [Display(Name = "Document", Description = "The optional custom document.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Document { get; set; } + + [Display(Name = "Deletion", Description = "The condition when to delete the document.")] + [Editor(RuleFieldEditor.Text)] + public string Delete { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs index c43f105094..09591fe114 100644 --- a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs @@ -16,154 +16,153 @@ #pragma warning disable IDE0059 // Value assigned to symbol is never used #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.OpenSearch +namespace Squidex.Extensions.Actions.OpenSearch; + +public sealed class OpenSearchActionHandler : RuleActionHandler { - public sealed class OpenSearchActionHandler : RuleActionHandler - { - private readonly ClientPool<(Uri Host, string Username, string Password), OpenSearchLowLevelClient> clients; - private readonly IScriptEngine scriptEngine; - private readonly IJsonSerializer serializer; + private readonly ClientPool<(Uri Host, string Username, string Password), OpenSearchLowLevelClient> clients; + private readonly IScriptEngine scriptEngine; + private readonly IJsonSerializer serializer; - public OpenSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) - : base(formatter) + public OpenSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) + : base(formatter) + { + clients = new ClientPool<(Uri Host, string Username, string Password), OpenSearchLowLevelClient>(key => { - clients = new ClientPool<(Uri Host, string Username, string Password), OpenSearchLowLevelClient>(key => + var config = new ConnectionConfiguration(key.Host); + + if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) { - var config = new ConnectionConfiguration(key.Host); + config = config.BasicAuthentication(key.Username, key.Password); + } - if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) - { - config = config.BasicAuthentication(key.Username, key.Password); - } + return new OpenSearchLowLevelClient(config); + }); - return new OpenSearchLowLevelClient(config); - }); + this.scriptEngine = scriptEngine; + this.serializer = serializer; + } - this.scriptEngine = scriptEngine; - this.serializer = serializer; - } + protected override async Task<(string Description, OpenSearchJob Data)> CreateJobAsync(EnrichedEvent @event, OpenSearchAction action) + { + var delete = @event.ShouldDelete(scriptEngine, action.Delete); - protected override async Task<(string Description, OpenSearchJob Data)> CreateJobAsync(EnrichedEvent @event, OpenSearchAction action) - { - var delete = @event.ShouldDelete(scriptEngine, action.Delete); + string contentId; - string contentId; + if (@event is IEnrichedEntityEvent enrichedEntityEvent) + { + contentId = enrichedEntityEvent.Id.ToString(); + } + else + { + contentId = DomainId.NewGuid().ToString(); + } - if (@event is IEnrichedEntityEvent enrichedEntityEvent) - { - contentId = enrichedEntityEvent.Id.ToString(); - } - else - { - contentId = DomainId.NewGuid().ToString(); - } + var ruleDescription = string.Empty; + var ruleJob = new OpenSearchJob + { + IndexName = await FormatAsync(action.IndexName, @event), + ServerHost = action.Host.ToString(), + ServerUser = action.Username, + ServerPassword = action.Password, + ContentId = contentId + }; + + if (delete) + { + ruleDescription = $"Delete entry index: {action.IndexName}"; + } + else + { + ruleDescription = $"Upsert to index: {action.IndexName}"; - var ruleDescription = string.Empty; - var ruleJob = new OpenSearchJob - { - IndexName = await FormatAsync(action.IndexName, @event), - ServerHost = action.Host.ToString(), - ServerUser = action.Username, - ServerPassword = action.Password, - ContentId = contentId - }; - - if (delete) - { - ruleDescription = $"Delete entry index: {action.IndexName}"; - } - else + OpenSearchContent content; + try { - ruleDescription = $"Upsert to index: {action.IndexName}"; + string jsonString; - OpenSearchContent content; - try + if (!string.IsNullOrEmpty(action.Document)) { - string jsonString; - - if (!string.IsNullOrEmpty(action.Document)) - { - jsonString = await FormatAsync(action.Document, @event); - jsonString = jsonString?.Trim(); - } - else - { - jsonString = ToJson(@event); - } - - content = serializer.Deserialize(jsonString); + jsonString = await FormatAsync(action.Document, @event); + jsonString = jsonString?.Trim(); } - catch (Exception ex) + else { - content = new OpenSearchContent - { - More = new Dictionary - { - ["error"] = $"Invalid JSON: {ex.Message}" - } - }; + jsonString = ToJson(@event); } - content.ContentId = contentId; - - ruleJob.Content = serializer.Serialize(content, true); + content = serializer.Deserialize(jsonString); } + catch (Exception ex) + { + content = new OpenSearchContent + { + More = new Dictionary + { + ["error"] = $"Invalid JSON: {ex.Message}" + } + }; + } + + content.ContentId = contentId; - return (ruleDescription, ruleJob); + ruleJob.Content = serializer.Serialize(content, true); } - protected override async Task ExecuteJobAsync(OpenSearchJob job, - CancellationToken ct = default) + return (ruleDescription, ruleJob); + } + + protected override async Task ExecuteJobAsync(OpenSearchJob job, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(job.ServerHost)) { - if (string.IsNullOrWhiteSpace(job.ServerHost)) - { - return Result.Ignored(); - } + return Result.Ignored(); + } - var client = await clients.GetClientAsync((new Uri(job.ServerHost, UriKind.Absolute), job.ServerUser, job.ServerPassword)); + var client = await clients.GetClientAsync((new Uri(job.ServerHost, UriKind.Absolute), job.ServerUser, job.ServerPassword)); - try + try + { + if (job.Content != null) { - if (job.Content != null) - { - var response = await client.IndexAsync(job.IndexName, job.ContentId, job.Content, ctx: ct); + var response = await client.IndexAsync(job.IndexName, job.ContentId, job.Content, ctx: ct); - return Result.SuccessOrFailed(response.OriginalException, response.Body); - } - else - { - var response = await client.DeleteAsync(job.IndexName, job.ContentId, ctx: ct); - - return Result.SuccessOrFailed(response.OriginalException, response.Body); - } + return Result.SuccessOrFailed(response.OriginalException, response.Body); } - catch (OpenSearchClientException ex) + else { - return Result.Failed(ex); + var response = await client.DeleteAsync(job.IndexName, job.ContentId, ctx: ct); + + return Result.SuccessOrFailed(response.OriginalException, response.Body); } } + catch (OpenSearchClientException ex) + { + return Result.Failed(ex); + } } +} - public sealed class OpenSearchContent - { - public string ContentId { get; set; } +public sealed class OpenSearchContent +{ + public string ContentId { get; set; } - [JsonExtensionData] - public Dictionary More { get; set; } = new Dictionary(); - } + [JsonExtensionData] + public Dictionary More { get; set; } = new Dictionary(); +} - public sealed class OpenSearchJob - { - public string ServerHost { get; set; } +public sealed class OpenSearchJob +{ + public string ServerHost { get; set; } - public string ServerUser { get; set; } + public string ServerUser { get; set; } - public string ServerPassword { get; set; } + public string ServerPassword { get; set; } - public string ContentId { get; set; } + public string ContentId { get; set; } - public string Content { get; set; } + public string Content { get; set; } - public string IndexName { get; set; } - } + public string IndexName { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchPlugin.cs index 4a4435c256..371302fa3c 100644 --- a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.OpenSearch +namespace Squidex.Extensions.Actions.OpenSearch; + +public sealed class OpenSearchPlugin : IPlugin { - public sealed class OpenSearchPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs index 287b7c3868..4e4719ca9a 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs @@ -10,26 +10,25 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Prerender +namespace Squidex.Extensions.Actions.Prerender; + +[RuleAction( + Title = "Prerender", + IconImage = "", + IconColor = "#2c3e50", + Display = "Recache URL", + Description = "Prerender a javascript website for bots.", + ReadMore = "https://prerender.io")] +public sealed record PrerenderAction : RuleAction { - [RuleAction( - Title = "Prerender", - IconImage = "", - IconColor = "#2c3e50", - Display = "Recache URL", - Description = "Prerender a javascript website for bots.", - ReadMore = "https://prerender.io")] - public sealed record PrerenderAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Token", Description = "The prerender token from your account.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string Token { get; set; } + [LocalizedRequired] + [Display(Name = "Token", Description = "The prerender token from your account.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string Token { get; set; } - [LocalizedRequired] - [Display(Name = "Url", Description = "The url to recache.")] - [Editor(RuleFieldEditor.Text)] - public string Url { get; set; } - } + [LocalizedRequired] + [Display(Name = "Url", Description = "The url to recache.")] + [Editor(RuleFieldEditor.Text)] + public string Url { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs index f4827cc1c8..1690d371b8 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs @@ -11,45 +11,44 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Prerender +namespace Squidex.Extensions.Actions.Prerender; + +public sealed class PrerenderActionHandler : RuleActionHandler { - public sealed class PrerenderActionHandler : RuleActionHandler - { - private readonly IHttpClientFactory httpClientFactory; + private readonly IHttpClientFactory httpClientFactory; - public PrerenderActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) - { - this.httpClientFactory = httpClientFactory; - } + public PrerenderActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) + { + this.httpClientFactory = httpClientFactory; + } - protected override async Task<(string Description, PrerenderJob Data)> CreateJobAsync(EnrichedEvent @event, PrerenderAction action) - { - var url = await FormatAsync(action.Url, @event); + protected override async Task<(string Description, PrerenderJob Data)> CreateJobAsync(EnrichedEvent @event, PrerenderAction action) + { + var url = await FormatAsync(action.Url, @event); - var request = new { prerenderToken = action.Token, url }; - var requestBody = ToJson(request); + var request = new { prerenderToken = action.Token, url }; + var requestBody = ToJson(request); - return ($"Recache {url}", new PrerenderJob { RequestBody = requestBody }); - } + return ($"Recache {url}", new PrerenderJob { RequestBody = requestBody }); + } - protected override async Task ExecuteJobAsync(PrerenderJob job, - CancellationToken ct = default) + protected override async Task ExecuteJobAsync(PrerenderJob job, + CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) { - using (var httpClient = httpClientFactory.CreateClient()) + var request = new HttpRequestMessage(HttpMethod.Post, "https://api.prerender.io/recache") { - var request = new HttpRequestMessage(HttpMethod.Post, "https://api.prerender.io/recache") - { - Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") - }; + Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") + }; - return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); - } + return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); } } +} - public sealed class PrerenderJob - { - public string RequestBody { get; set; } - } +public sealed class PrerenderJob +{ + public string RequestBody { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs index 608cb32d47..8ab112f585 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Prerender +namespace Squidex.Extensions.Actions.Prerender; + +public sealed class PrerenderPlugin : IPlugin { - public sealed class PrerenderPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs b/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs index 31b21abb50..d0cd7836ba 100644 --- a/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs +++ b/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs @@ -10,64 +10,63 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure.Http; -namespace Squidex.Extensions.Actions +namespace Squidex.Extensions.Actions; + +public static class RuleHelper { - public static class RuleHelper + public static bool ShouldDelete(this EnrichedEvent @event, IScriptEngine scriptEngine, string expression) { - public static bool ShouldDelete(this EnrichedEvent @event, IScriptEngine scriptEngine, string expression) + if (!string.IsNullOrWhiteSpace(expression)) { - if (!string.IsNullOrWhiteSpace(expression)) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new EventScriptVars { - // Script vars are just wrappers over dictionaries for better performance. - var vars = new EventScriptVars - { - Event = @event - }; - - return scriptEngine.Evaluate(vars, expression); - } + Event = @event + }; - return IsContentDeletion(@event) || IsAssetDeletion(@event); + return scriptEngine.Evaluate(vars, expression); } - public static bool IsContentDeletion(this EnrichedEvent @event) - { - return @event is EnrichedContentEvent { Type: EnrichedContentEventType.Deleted or EnrichedContentEventType.Unpublished }; - } + return IsContentDeletion(@event) || IsAssetDeletion(@event); + } - public static bool IsAssetDeletion(this EnrichedEvent @event) - { - return @event is EnrichedAssetEvent { Type: EnrichedAssetEventType.Deleted }; - } + public static bool IsContentDeletion(this EnrichedEvent @event) + { + return @event is EnrichedContentEvent { Type: EnrichedContentEventType.Deleted or EnrichedContentEventType.Unpublished }; + } - public static async Task OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null, - CancellationToken ct = default) - { - HttpResponseMessage response = null; - try - { - response = await client.SendAsync(request, ct); + public static bool IsAssetDeletion(this EnrichedEvent @event) + { + return @event is EnrichedAssetEvent { Type: EnrichedAssetEventType.Deleted }; + } - var responseString = await response.Content.ReadAsStringAsync(ct); - var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString); + public static async Task OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null, + CancellationToken ct = default) + { + HttpResponseMessage response = null; + try + { + response = await client.SendAsync(request, ct); - if (!response.IsSuccessStatusCode) - { - var ex = new HttpRequestException($"Response code does not indicate success: {(int)response.StatusCode} ({response.StatusCode})."); + var responseString = await response.Content.ReadAsStringAsync(ct); + var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString); - return Result.Failed(ex, requestDump); - } - else - { - return Result.Success(requestDump); - } - } - catch (Exception ex) + if (!response.IsSuccessStatusCode) { - var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString()); + var ex = new HttpRequestException($"Response code does not indicate success: {(int)response.StatusCode} ({response.StatusCode})."); return Result.Failed(ex, requestDump); } + else + { + return Result.Success(requestDump); + } + } + catch (Exception ex) + { + var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString()); + + return Result.Failed(ex, requestDump); } } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptAction.cs b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptAction.cs index 68640c58e1..2923377af9 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptAction.cs @@ -10,20 +10,19 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Script +namespace Squidex.Extensions.Actions.Script; + +[RuleAction( + Title = "Script", + IconImage = "", + IconColor = "#f0be25", + Display = "Run a Script", + Description = "Runs a custom Javascript")] +public sealed record ScriptAction : RuleAction { - [RuleAction( - Title = "Script", - IconImage = "", - IconColor = "#f0be25", - Display = "Run a Script", - Description = "Runs a custom Javascript")] - public sealed record ScriptAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Script", Description = "The script to render.")] - [Editor(RuleFieldEditor.Javascript)] - [Formattable] - public string Script { get; set; } - } + [LocalizedRequired] + [Display(Name = "Script", Description = "The script to render.")] + [Editor(RuleFieldEditor.Javascript)] + [Formattable] + public string Script { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs index 6c31380792..a0921ea896 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs @@ -11,44 +11,43 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Script +namespace Squidex.Extensions.Actions.Script; + +public sealed class ScriptActionHandler : RuleActionHandler { - public sealed class ScriptActionHandler : RuleActionHandler - { - private readonly IScriptEngine scriptEngine; + private readonly IScriptEngine scriptEngine; - public ScriptActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine) - : base(formatter) - { - this.scriptEngine = scriptEngine; - } + public ScriptActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine) + : base(formatter) + { + this.scriptEngine = scriptEngine; + } - protected override Task<(string Description, ScriptJob Data)> CreateJobAsync(EnrichedEvent @event, ScriptAction action) - { - var job = new ScriptJob { Script = action.Script, Event = @event }; + protected override Task<(string Description, ScriptJob Data)> CreateJobAsync(EnrichedEvent @event, ScriptAction action) + { + var job = new ScriptJob { Script = action.Script, Event = @event }; - return Task.FromResult(($"Run a script", job)); - } + return Task.FromResult(($"Run a script", job)); + } - protected override async Task ExecuteJobAsync(ScriptJob job, - CancellationToken ct = default) + protected override async Task ExecuteJobAsync(ScriptJob job, + CancellationToken ct = default) + { + // Script vars are just wrappers over dictionaries for better performance. + var vars = new EventScriptVars { - // Script vars are just wrappers over dictionaries for better performance. - var vars = new EventScriptVars - { - Event = job.Event - }; + Event = job.Event + }; - var result = await scriptEngine.ExecuteAsync(vars, job.Script, ct: ct); + var result = await scriptEngine.ExecuteAsync(vars, job.Script, ct: ct); - return Result.Success(result.ToString()); - } + return Result.Success(result.ToString()); } +} - public sealed class ScriptJob - { - public EnrichedEvent Event { get; set; } +public sealed class ScriptJob +{ + public EnrichedEvent Event { get; set; } - public string Script { get; set; } - } + public string Script { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptPlugin.cs index 6ea680e284..49ef4777d7 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Script +namespace Squidex.Extensions.Actions.Script; + +public sealed class ScriptPlugin : IPlugin { - public sealed class ScriptPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRAction.cs b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRAction.cs index 8824941a45..6fedfb2fb0 100644 --- a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRAction.cs @@ -13,65 +13,64 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.SignalR +namespace Squidex.Extensions.Actions.SignalR; + +[RuleAction( + Title = "Azure SignalR", + IconImage = "", + IconColor = "#1566BF", + Display = "Send to Azure SignalR", + Description = "Send a message to Azure SignalR.", + ReadMore = "https://azure.microsoft.com/fr-fr/services/signalr-service/")] +public sealed record SignalRAction : RuleAction { - [RuleAction( - Title = "Azure SignalR", - IconImage = "", - IconColor = "#1566BF", - Display = "Send to Azure SignalR", - Description = "Send a message to Azure SignalR.", - ReadMore = "https://azure.microsoft.com/fr-fr/services/signalr-service/")] - public sealed record SignalRAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Connection", Description = "The connection string to the Azure SignalR.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string ConnectionString { get; set; } + [LocalizedRequired] + [Display(Name = "Connection", Description = "The connection string to the Azure SignalR.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string ConnectionString { get; set; } - [LocalizedRequired] - [Display(Name = "Hub Name", Description = "The name of the hub.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string HubName { get; set; } + [LocalizedRequired] + [Display(Name = "Hub Name", Description = "The name of the hub.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string HubName { get; set; } - [LocalizedRequired] - [Display(Name = "Action", Description = "* Broadcast = send to all users.\n * User = send to all target users(s).\n * Group = send to all target group(s).")] - public ActionTypeEnum Action { get; set; } + [LocalizedRequired] + [Display(Name = "Action", Description = "* Broadcast = send to all users.\n * User = send to all target users(s).\n * Group = send to all target group(s).")] + public ActionTypeEnum Action { get; set; } - [Display(Name = "Methode Name", Description = "Set the Name of the hub method received by the customer.")] - [Editor(RuleFieldEditor.Text)] - public string MethodName { get; set; } + [Display(Name = "Methode Name", Description = "Set the Name of the hub method received by the customer.")] + [Editor(RuleFieldEditor.Text)] + public string MethodName { get; set; } - [Display(Name = "Target (Optional)", Description = "Define target users or groups by id or name. One item per line. Not needed for Broadcast action.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Target { get; set; } + [Display(Name = "Target (Optional)", Description = "Define target users or groups by id or name. One item per line. Not needed for Broadcast action.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Target { get; set; } - [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Payload { get; set; } + [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Payload { get; set; } - protected override IEnumerable CustomValidate() + protected override IEnumerable CustomValidate() + { + if (HubName != null && !Regex.IsMatch(HubName, "^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$")) { - if (HubName != null && !Regex.IsMatch(HubName, "^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$")) - { - yield return new ValidationError("Hub must be valid azure hub name.", nameof(HubName)); - } + yield return new ValidationError("Hub must be valid azure hub name.", nameof(HubName)); + } - if (Action != ActionTypeEnum.Broadcast && string.IsNullOrWhiteSpace(Target)) - { - yield return new ValidationError("Target must be specified with 'User' or 'Group' Action.", nameof(HubName)); - } + if (Action != ActionTypeEnum.Broadcast && string.IsNullOrWhiteSpace(Target)) + { + yield return new ValidationError("Target must be specified with 'User' or 'Group' Action.", nameof(HubName)); } } +} - public enum ActionTypeEnum - { - Broadcast, - User, - Group - } +public enum ActionTypeEnum +{ + Broadcast, + User, + Group } diff --git a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs index c2a1ae044c..76d30db4b8 100644 --- a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs @@ -12,100 +12,99 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.SignalR +namespace Squidex.Extensions.Actions.SignalR; + +public sealed class SignalRActionHandler : RuleActionHandler { - public sealed class SignalRActionHandler : RuleActionHandler + private readonly ClientPool<(string ConnectionString, string HubName), ServiceManager> clients; + + public SignalRActionHandler(RuleEventFormatter formatter) + : base(formatter) + { + clients = new ClientPool<(string ConnectionString, string HubName), ServiceManager>(key => + { + var serviceManager = new ServiceManagerBuilder() + .WithOptions(option => + { + option.ConnectionString = key.ConnectionString; + option.ServiceTransportType = ServiceTransportType.Transient; + }) + .BuildServiceManager(); + + return serviceManager; + }); + } + + protected override async Task<(string Description, SignalRJob Data)> CreateJobAsync(EnrichedEvent @event, SignalRAction action) { - private readonly ClientPool<(string ConnectionString, string HubName), ServiceManager> clients; + var hubName = await FormatAsync(action.HubName, @event); - public SignalRActionHandler(RuleEventFormatter formatter) - : base(formatter) + string requestBody; + + if (!string.IsNullOrWhiteSpace(action.Payload)) { - clients = new ClientPool<(string ConnectionString, string HubName), ServiceManager>(key => - { - var serviceManager = new ServiceManagerBuilder() - .WithOptions(option => - { - option.ConnectionString = key.ConnectionString; - option.ServiceTransportType = ServiceTransportType.Transient; - }) - .BuildServiceManager(); - - return serviceManager; - }); + requestBody = await FormatAsync(action.Payload, @event); } - - protected override async Task<(string Description, SignalRJob Data)> CreateJobAsync(EnrichedEvent @event, SignalRAction action) + else { - var hubName = await FormatAsync(action.HubName, @event); - - string requestBody; + requestBody = ToEnvelopeJson(@event); + } - if (!string.IsNullOrWhiteSpace(action.Payload)) - { - requestBody = await FormatAsync(action.Payload, @event); - } - else - { - requestBody = ToEnvelopeJson(@event); - } + var target = (await FormatAsync(action.Target, @event)) ?? string.Empty; - var target = (await FormatAsync(action.Target, @event)) ?? string.Empty; + var ruleDescription = $"Send SignalRJob to signalR hub '{hubName}'"; - var ruleDescription = $"Send SignalRJob to signalR hub '{hubName}'"; + var ruleJob = new SignalRJob + { + Action = action.Action, + ConnectionString = action.ConnectionString, + HubName = hubName, + MethodName = action.MethodName, + MethodPayload = requestBody, + Targets = target.Split("\n") + }; + + return (ruleDescription, ruleJob); + } - var ruleJob = new SignalRJob - { - Action = action.Action, - ConnectionString = action.ConnectionString, - HubName = hubName, - MethodName = action.MethodName, - MethodPayload = requestBody, - Targets = target.Split("\n") - }; - - return (ruleDescription, ruleJob); - } + protected override async Task ExecuteJobAsync(SignalRJob job, + CancellationToken ct = default) + { + var signalR = await clients.GetClientAsync((job.ConnectionString, job.HubName)); - protected override async Task ExecuteJobAsync(SignalRJob job, - CancellationToken ct = default) + await using (var signalRContext = await signalR.CreateHubContextAsync(job.HubName, cancellationToken: ct)) { - var signalR = await clients.GetClientAsync((job.ConnectionString, job.HubName)); + var methodeName = !string.IsNullOrWhiteSpace(job.MethodName) ? job.MethodName : "push"; - await using (var signalRContext = await signalR.CreateHubContextAsync(job.HubName, cancellationToken: ct)) + switch (job.Action) { - var methodeName = !string.IsNullOrWhiteSpace(job.MethodName) ? job.MethodName : "push"; - - switch (job.Action) - { - case ActionTypeEnum.Broadcast: - await signalRContext.Clients.All.SendAsync(methodeName, job.MethodPayload, ct); - break; - case ActionTypeEnum.User: - await signalRContext.Clients.Users(job.Targets).SendAsync(methodeName, job.MethodPayload, ct); - break; - case ActionTypeEnum.Group: - await signalRContext.Clients.Groups(job.Targets).SendAsync(methodeName, job.MethodPayload, ct); - break; - } + case ActionTypeEnum.Broadcast: + await signalRContext.Clients.All.SendAsync(methodeName, job.MethodPayload, ct); + break; + case ActionTypeEnum.User: + await signalRContext.Clients.Users(job.Targets).SendAsync(methodeName, job.MethodPayload, ct); + break; + case ActionTypeEnum.Group: + await signalRContext.Clients.Groups(job.Targets).SendAsync(methodeName, job.MethodPayload, ct); + break; } - - return Result.Complete(); } + + return Result.Complete(); } +} - public sealed class SignalRJob - { - public string ConnectionString { get; set; } +public sealed class SignalRJob +{ + public string ConnectionString { get; set; } - public string HubName { get; set; } + public string HubName { get; set; } - public ActionTypeEnum Action { get; set; } + public ActionTypeEnum Action { get; set; } - public string MethodName { get; set; } + public string MethodName { get; set; } - public string MethodPayload { get; set; } + public string MethodPayload { get; set; } - public string[] Targets { get; set; } - } + public string[] Targets { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRPlugin.cs index e771cf13e2..f0a41aa3e9 100644 --- a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.SignalR +namespace Squidex.Extensions.Actions.SignalR; + +public sealed class SignalRPlugin : IPlugin { - public sealed class SignalRPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs index fe11679abe..a2b81f0a1a 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs @@ -10,27 +10,26 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Slack +namespace Squidex.Extensions.Actions.Slack; + +[RuleAction( + Title = "Slack", + IconImage = "", + IconColor = "#5c3a58", + Display = "Send to Slack", + Description = "Create a status update to a slack channel.", + ReadMore = "https://slack.com")] +public sealed record SlackAction : RuleAction { - [RuleAction( - Title = "Slack", - IconImage = "", - IconColor = "#5c3a58", - Display = "Send to Slack", - Description = "Create a status update to a slack channel.", - ReadMore = "https://slack.com")] - public sealed record SlackAction : RuleAction - { - [AbsoluteUrl] - [LocalizedRequired] - [Display(Name = "Webhook Url", Description = "The slack webhook url.")] - [Editor(RuleFieldEditor.Text)] - public Uri WebhookUrl { get; set; } + [AbsoluteUrl] + [LocalizedRequired] + [Display(Name = "Webhook Url", Description = "The slack webhook url.")] + [Editor(RuleFieldEditor.Text)] + public Uri WebhookUrl { get; set; } - [LocalizedRequired] - [Display(Name = "Text", Description = "The text that is sent as message to slack.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Text { get; set; } - } + [LocalizedRequired] + [Display(Name = "Text", Description = "The text that is sent as message to slack.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Text { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs index 75ae57fa47..ec92ef45db 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs @@ -11,54 +11,53 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Slack +namespace Squidex.Extensions.Actions.Slack; + +public sealed class SlackActionHandler : RuleActionHandler { - public sealed class SlackActionHandler : RuleActionHandler + private const string Description = "Send message to slack"; + + private readonly IHttpClientFactory httpClientFactory; + + public SlackActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) { - private const string Description = "Send message to slack"; + this.httpClientFactory = httpClientFactory; + } - private readonly IHttpClientFactory httpClientFactory; + protected override async Task<(string Description, SlackJob Data)> CreateJobAsync(EnrichedEvent @event, SlackAction action) + { + var body = new { text = await FormatAsync(action.Text, @event) }; - public SlackActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) + var ruleJob = new SlackJob { - this.httpClientFactory = httpClientFactory; - } + RequestUrl = action.WebhookUrl.ToString(), + RequestBody = ToJson(body) + }; + + return (Description, ruleJob); + } - protected override async Task<(string Description, SlackJob Data)> CreateJobAsync(EnrichedEvent @event, SlackAction action) + protected override async Task ExecuteJobAsync(SlackJob job, + CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) { - var body = new { text = await FormatAsync(action.Text, @event) }; + httpClient.Timeout = TimeSpan.FromSeconds(2); - var ruleJob = new SlackJob + var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) { - RequestUrl = action.WebhookUrl.ToString(), - RequestBody = ToJson(body) + Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") }; - return (Description, ruleJob); - } - - protected override async Task ExecuteJobAsync(SlackJob job, - CancellationToken ct = default) - { - using (var httpClient = httpClientFactory.CreateClient()) - { - httpClient.Timeout = TimeSpan.FromSeconds(2); - - var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) - { - Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") - }; - - return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); - } + return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); } } +} - public sealed class SlackJob - { - public string RequestUrl { get; set; } +public sealed class SlackJob +{ + public string RequestUrl { get; set; } - public string RequestBody { get; set; } - } + public string RequestBody { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs index 15549a3493..b1ec0236e0 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Slack +namespace Squidex.Extensions.Actions.Slack; + +public sealed class SlackPlugin : IPlugin { - public sealed class SlackPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs index 7aa9671499..4b63cf2406 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs @@ -10,31 +10,30 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Twitter +namespace Squidex.Extensions.Actions.Twitter; + +[RuleAction( + Title = "Twitter", + IconImage = "", + IconColor = "#1da1f2", + Display = "Tweet", + Description = "Tweet an update with your twitter account.", + ReadMore = "https://twitter.com")] +public sealed record TweetAction : RuleAction { - [RuleAction( - Title = "Twitter", - IconImage = "", - IconColor = "#1da1f2", - Display = "Tweet", - Description = "Tweet an update with your twitter account.", - ReadMore = "https://twitter.com")] - public sealed record TweetAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Access Token", Description = " The generated access token.")] - [Editor(RuleFieldEditor.Text)] - public string AccessToken { get; set; } + [LocalizedRequired] + [Display(Name = "Access Token", Description = " The generated access token.")] + [Editor(RuleFieldEditor.Text)] + public string AccessToken { get; set; } - [LocalizedRequired] - [Display(Name = "Access Secret", Description = " The generated access secret.")] - [Editor(RuleFieldEditor.Text)] - public string AccessSecret { get; set; } + [LocalizedRequired] + [Display(Name = "Access Secret", Description = " The generated access secret.")] + [Editor(RuleFieldEditor.Text)] + public string AccessSecret { get; set; } - [LocalizedRequired] - [Display(Name = "Text", Description = "The text that is sent as tweet to twitter.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Text { get; set; } - } + [LocalizedRequired] + [Display(Name = "Text", Description = "The text that is sent as tweet to twitter.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Text { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs index 0ed64e0a4d..834116f1d1 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs @@ -12,58 +12,57 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Twitter +namespace Squidex.Extensions.Actions.Twitter; + +public sealed class TweetActionHandler : RuleActionHandler { - public sealed class TweetActionHandler : RuleActionHandler - { - private const string Description = "Send a tweet"; + private const string Description = "Send a tweet"; - private readonly TwitterOptions twitterOptions; + private readonly TwitterOptions twitterOptions; - public TweetActionHandler(RuleEventFormatter formatter, IOptions twitterOptions) - : base(formatter) - { - this.twitterOptions = twitterOptions.Value; - } + public TweetActionHandler(RuleEventFormatter formatter, IOptions twitterOptions) + : base(formatter) + { + this.twitterOptions = twitterOptions.Value; + } - protected override async Task<(string Description, TweetJob Data)> CreateJobAsync(EnrichedEvent @event, TweetAction action) + protected override async Task<(string Description, TweetJob Data)> CreateJobAsync(EnrichedEvent @event, TweetAction action) + { + var ruleJob = new TweetJob { - var ruleJob = new TweetJob - { - Text = await FormatAsync(action.Text, @event), - AccessToken = action.AccessToken, - AccessSecret = action.AccessSecret - }; + Text = await FormatAsync(action.Text, @event), + AccessToken = action.AccessToken, + AccessSecret = action.AccessSecret + }; + + return (Description, ruleJob); + } - return (Description, ruleJob); - } + protected override async Task ExecuteJobAsync(TweetJob job, + CancellationToken ct = default) + { + var tokens = Tokens.Create( + twitterOptions.ClientId, + twitterOptions.ClientSecret, + job.AccessToken, + job.AccessSecret); - protected override async Task ExecuteJobAsync(TweetJob job, - CancellationToken ct = default) + var request = new Dictionary { - var tokens = Tokens.Create( - twitterOptions.ClientId, - twitterOptions.ClientSecret, - job.AccessToken, - job.AccessSecret); + ["status"] = job.Text + }; - var request = new Dictionary - { - ["status"] = job.Text - }; + await tokens.Statuses.UpdateAsync(request, ct); - await tokens.Statuses.UpdateAsync(request, ct); - - return Result.Success($"Tweeted: {job.Text}"); - } + return Result.Success($"Tweeted: {job.Text}"); } +} - public sealed class TweetJob - { - public string AccessToken { get; set; } +public sealed class TweetJob +{ + public string AccessToken { get; set; } - public string AccessSecret { get; set; } + public string AccessSecret { get; set; } - public string Text { get; set; } - } + public string Text { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs index de1de90ea8..6877ea3db3 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Extensions.Actions.Twitter +namespace Squidex.Extensions.Actions.Twitter; + +public sealed class TwitterOptions { - public sealed class TwitterOptions - { - public string ClientId { get; set; } + public string ClientId { get; set; } - public string ClientSecret { get; set; } - } + public string ClientSecret { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs index 2d987b3c44..9f6bbad340 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs @@ -9,16 +9,15 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Twitter +namespace Squidex.Extensions.Actions.Twitter; + +public sealed class TwitterPlugin : IPlugin { - public sealed class TwitterPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.Configure( - config.GetSection("twitter")); + services.Configure( + config.GetSection("twitter")); - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseAction.cs b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseAction.cs index 980fc62727..4b43d1df2b 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseAction.cs @@ -10,41 +10,40 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.Validation; -namespace Squidex.Extensions.Actions.Typesense +namespace Squidex.Extensions.Actions.Typesense; + +[RuleAction( + Title = "Typesense", + IconImage = "", + IconColor = "#1035bc", + Display = "Populate Typesense index", + Description = "Populate a full text search index in Typesense.", + ReadMore = "https://www.elastic.co/")] +public sealed record TypesenseAction : RuleAction { - [RuleAction( - Title = "Typesense", - IconImage = "", - IconColor = "#1035bc", - Display = "Populate Typesense index", - Description = "Populate a full text search index in Typesense.", - ReadMore = "https://www.elastic.co/")] - public sealed record TypesenseAction : RuleAction - { - [AbsoluteUrl] - [LocalizedRequired] - [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] - [Editor(RuleFieldEditor.Url)] - public Uri Host { get; set; } + [AbsoluteUrl] + [LocalizedRequired] + [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] + [Editor(RuleFieldEditor.Url)] + public Uri Host { get; set; } - [LocalizedRequired] - [Display(Name = "Index Name", Description = "The name of the index.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public string IndexName { get; set; } + [LocalizedRequired] + [Display(Name = "Index Name", Description = "The name of the index.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string IndexName { get; set; } - [LocalizedRequired] - [Display(Name = "Api Key", Description = "The api key.")] - [Editor(RuleFieldEditor.Text)] - public string ApiKey { get; set; } + [LocalizedRequired] + [Display(Name = "Api Key", Description = "The api key.")] + [Editor(RuleFieldEditor.Text)] + public string ApiKey { get; set; } - [Display(Name = "Document", Description = "The optional custom document.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Document { get; set; } + [Display(Name = "Document", Description = "The optional custom document.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Document { get; set; } - [Display(Name = "Deletion", Description = "The condition when to delete the document.")] - [Editor(RuleFieldEditor.Text)] - public string Delete { get; set; } - } + [Display(Name = "Deletion", Description = "The condition when to delete the document.")] + [Editor(RuleFieldEditor.Text)] + public string Delete { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs index b93370d1bb..b8519c5441 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs @@ -16,140 +16,139 @@ #pragma warning disable IDE0059 // Value assigned to symbol is never used #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Typesense +namespace Squidex.Extensions.Actions.Typesense; + +public sealed class TypesenseActionHandler : RuleActionHandler { - public sealed class TypesenseActionHandler : RuleActionHandler + private readonly IScriptEngine scriptEngine; + private readonly IHttpClientFactory httpClientFactory; + private readonly IJsonSerializer serializer; + + public TypesenseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory, IScriptEngine scriptEngine, IJsonSerializer serializer) + : base(formatter) + { + this.scriptEngine = scriptEngine; + this.httpClientFactory = httpClientFactory; + this.serializer = serializer; + } + + protected override async Task<(string Description, TypesenseJob Data)> CreateJobAsync(EnrichedEvent @event, TypesenseAction action) { - private readonly IScriptEngine scriptEngine; - private readonly IHttpClientFactory httpClientFactory; - private readonly IJsonSerializer serializer; + var delete = @event.ShouldDelete(scriptEngine, action.Delete); + + string contentId; - public TypesenseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory, IScriptEngine scriptEngine, IJsonSerializer serializer) - : base(formatter) + if (@event is IEnrichedEntityEvent enrichedEntityEvent) { - this.scriptEngine = scriptEngine; - this.httpClientFactory = httpClientFactory; - this.serializer = serializer; + contentId = enrichedEntityEvent.Id.ToString(); } - - protected override async Task<(string Description, TypesenseJob Data)> CreateJobAsync(EnrichedEvent @event, TypesenseAction action) + else { - var delete = @event.ShouldDelete(scriptEngine, action.Delete); - - string contentId; + contentId = DomainId.NewGuid().ToString(); + } - if (@event is IEnrichedEntityEvent enrichedEntityEvent) - { - contentId = enrichedEntityEvent.Id.ToString(); - } - else - { - contentId = DomainId.NewGuid().ToString(); - } + var indexName = await FormatAsync(action.IndexName, @event); - var indexName = await FormatAsync(action.IndexName, @event); + var ruleDescription = string.Empty; + var ruleJob = new TypesenseJob + { + ServerUrl = $"{action.Host.ToString().TrimEnd('/')}/collections/{indexName}/documents", + ServerKey = action.ApiKey, + ContentId = contentId + }; - var ruleDescription = string.Empty; - var ruleJob = new TypesenseJob - { - ServerUrl = $"{action.Host.ToString().TrimEnd('/')}/collections/{indexName}/documents", - ServerKey = action.ApiKey, - ContentId = contentId - }; + if (delete) + { + ruleDescription = $"Delete entry index: {action.IndexName}"; + } + else + { + ruleDescription = $"Upsert to index: {action.IndexName}"; - if (delete) - { - ruleDescription = $"Delete entry index: {action.IndexName}"; - } - else + TypesenseContent content; + try { - ruleDescription = $"Upsert to index: {action.IndexName}"; + string jsonString; - TypesenseContent content; - try + if (!string.IsNullOrEmpty(action.Document)) { - string jsonString; - - if (!string.IsNullOrEmpty(action.Document)) - { - jsonString = await FormatAsync(action.Document, @event); - jsonString = jsonString?.Trim(); - } - else - { - jsonString = ToJson(@event); - } - - content = serializer.Deserialize(jsonString); + jsonString = await FormatAsync(action.Document, @event); + jsonString = jsonString?.Trim(); } - catch (Exception ex) + else { - content = new TypesenseContent - { - More = new Dictionary - { - ["error"] = $"Invalid JSON: {ex.Message}" - } - }; + jsonString = ToJson(@event); } - content.Id = contentId; - - ruleJob.Content = serializer.Serialize(content, true); + content = serializer.Deserialize(jsonString); } + catch (Exception ex) + { + content = new TypesenseContent + { + More = new Dictionary + { + ["error"] = $"Invalid JSON: {ex.Message}" + } + }; + } + + content.Id = contentId; - return (ruleDescription, ruleJob); + ruleJob.Content = serializer.Serialize(content, true); } - protected override async Task ExecuteJobAsync(TypesenseJob job, - CancellationToken ct = default) + return (ruleDescription, ruleJob); + } + + protected override async Task ExecuteJobAsync(TypesenseJob job, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(job.ServerUrl)) { - if (string.IsNullOrWhiteSpace(job.ServerUrl)) - { - return Result.Ignored(); - } + return Result.Ignored(); + } - var httpClient = httpClientFactory.CreateClient(); + var httpClient = httpClientFactory.CreateClient(); - HttpRequestMessage request; + HttpRequestMessage request; - if (job.Content != null) - { - request = new HttpRequestMessage(HttpMethod.Post, $"{job.ServerUrl}?action=upsert") - { - Content = new StringContent(job.Content, Encoding.UTF8, "application/json") - }; - } - else + if (job.Content != null) + { + request = new HttpRequestMessage(HttpMethod.Post, $"{job.ServerUrl}?action=upsert") { - request = new HttpRequestMessage(HttpMethod.Delete, $"{job.ServerUrl}/{job.ContentId}"); - } + Content = new StringContent(job.Content, Encoding.UTF8, "application/json") + }; + } + else + { + request = new HttpRequestMessage(HttpMethod.Delete, $"{job.ServerUrl}/{job.ContentId}"); + } - using (request) - { - request.Headers.TryAddWithoutValidation("X-Typesense-Api-Key", job.ServerKey); + using (request) + { + request.Headers.TryAddWithoutValidation("X-Typesense-Api-Key", job.ServerKey); - return await httpClient.OneWayRequestAsync(request, job.Content, ct); - } + return await httpClient.OneWayRequestAsync(request, job.Content, ct); } } +} - public sealed class TypesenseContent - { - public string Id { get; set; } +public sealed class TypesenseContent +{ + public string Id { get; set; } - [JsonExtensionData] - public Dictionary More { get; set; } = new Dictionary(); - } + [JsonExtensionData] + public Dictionary More { get; set; } = new Dictionary(); +} - public sealed class TypesenseJob - { - public string ServerUrl { get; set; } +public sealed class TypesenseJob +{ + public string ServerUrl { get; set; } - public string ServerKey { get; set; } + public string ServerKey { get; set; } - public string Content { get; set; } + public string Content { get; set; } - public string ContentId { get; set; } - } + public string ContentId { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs index 70d051c68d..86b706a9c3 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Typesense +namespace Squidex.Extensions.Actions.Typesense; + +public sealed class TypesensePlugin : IPlugin { - public sealed class TypesensePlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs index e6bf824713..b63cce08f7 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs @@ -12,51 +12,50 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Webhook +namespace Squidex.Extensions.Actions.Webhook; + +[RuleAction( + Title = "Webhook", + IconImage = "", + IconColor = "#4bb958", + Display = "Send webhook", + Description = "Invoke HTTP endpoints on a target system.", + ReadMore = "https://en.wikipedia.org/wiki/Webhook")] +public sealed record WebhookAction : RuleAction { - [RuleAction( - Title = "Webhook", - IconImage = "", - IconColor = "#4bb958", - Display = "Send webhook", - Description = "Invoke HTTP endpoints on a target system.", - ReadMore = "https://en.wikipedia.org/wiki/Webhook")] - public sealed record WebhookAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Url", Description = "The url to the webhook.")] - [Editor(RuleFieldEditor.Text)] - [Formattable] - public Uri Url { get; set; } - - [LocalizedRequired] - [Display(Name = "Method", Description = "The type of the request.")] - public WebhookMethod Method { get; set; } - - [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] - [Editor(RuleFieldEditor.TextArea)] - [Formattable] - public string Payload { get; set; } - - [Display(Name = "Payload Type", Description = "The mime type of the payload.")] - [Editor(RuleFieldEditor.Text)] - public string PayloadType { get; set; } - - [Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")] - [Editor(RuleFieldEditor.TextArea)] - public string Headers { get; set; } - - [Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the payload signature.")] - [Editor(RuleFieldEditor.Text)] - public string SharedSecret { get; set; } - } - - public enum WebhookMethod - { - POST, - PUT, - GET, - DELETE, - PATCH - } + [LocalizedRequired] + [Display(Name = "Url", Description = "The url to the webhook.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public Uri Url { get; set; } + + [LocalizedRequired] + [Display(Name = "Method", Description = "The type of the request.")] + public WebhookMethod Method { get; set; } + + [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Payload { get; set; } + + [Display(Name = "Payload Type", Description = "The mime type of the payload.")] + [Editor(RuleFieldEditor.Text)] + public string PayloadType { get; set; } + + [Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")] + [Editor(RuleFieldEditor.TextArea)] + public string Headers { get; set; } + + [Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the payload signature.")] + [Editor(RuleFieldEditor.Text)] + public string SharedSecret { get; set; } +} + +public enum WebhookMethod +{ + POST, + PUT, + GET, + DELETE, + PATCH } diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs index 157e5ccf47..924111c5ff 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs @@ -12,146 +12,145 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Extensions.Actions.Webhook +namespace Squidex.Extensions.Actions.Webhook; + +public sealed class WebhookActionHandler : RuleActionHandler { - public sealed class WebhookActionHandler : RuleActionHandler + private readonly IHttpClientFactory httpClientFactory; + + public WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) { - private readonly IHttpClientFactory httpClientFactory; + this.httpClientFactory = httpClientFactory; + } - public WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) - : base(formatter) - { - this.httpClientFactory = httpClientFactory; - } + protected override async Task<(string Description, WebhookJob Data)> CreateJobAsync(EnrichedEvent @event, WebhookAction action) + { + var requestUrl = await FormatAsync(action.Url, @event); + var requestBody = string.Empty; + var requestSignature = string.Empty; - protected override async Task<(string Description, WebhookJob Data)> CreateJobAsync(EnrichedEvent @event, WebhookAction action) + if (action.Method != WebhookMethod.GET) { - var requestUrl = await FormatAsync(action.Url, @event); - var requestBody = string.Empty; - var requestSignature = string.Empty; - - if (action.Method != WebhookMethod.GET) + if (!string.IsNullOrEmpty(action.Payload)) { - if (!string.IsNullOrEmpty(action.Payload)) - { - requestBody = await FormatAsync(action.Payload, @event); - } - else - { - requestBody = ToEnvelopeJson(@event); - } - - requestSignature = $"{requestBody}{action.SharedSecret}".ToSha256Base64(); + requestBody = await FormatAsync(action.Payload, @event); } - - var ruleDescription = $"Send event to webhook '{requestUrl}'"; - var ruleJob = new WebhookJob + else { - Method = action.Method, - RequestUrl = await FormatAsync(action.Url.ToString(), @event), - RequestSignature = requestSignature, - RequestBody = requestBody, - RequestBodyType = action.PayloadType, - Headers = await ParseHeadersAsync(action.Headers, @event) - }; - - return (ruleDescription, ruleJob); + requestBody = ToEnvelopeJson(@event); + } + + requestSignature = $"{requestBody}{action.SharedSecret}".ToSha256Base64(); } - private async Task> ParseHeadersAsync(string headers, EnrichedEvent @event) + var ruleDescription = $"Send event to webhook '{requestUrl}'"; + var ruleJob = new WebhookJob { - if (string.IsNullOrWhiteSpace(headers)) - { - return null; - } + Method = action.Method, + RequestUrl = await FormatAsync(action.Url.ToString(), @event), + RequestSignature = requestSignature, + RequestBody = requestBody, + RequestBodyType = action.PayloadType, + Headers = await ParseHeadersAsync(action.Headers, @event) + }; + + return (ruleDescription, ruleJob); + } - var headersDictionary = new Dictionary(); + private async Task> ParseHeadersAsync(string headers, EnrichedEvent @event) + { + if (string.IsNullOrWhiteSpace(headers)) + { + return null; + } - var lines = headers.Split('\n'); + var headersDictionary = new Dictionary(); - foreach (var line in lines) - { - var indexEqual = line.IndexOf('=', StringComparison.Ordinal); + var lines = headers.Split('\n'); - if (indexEqual > 0 && indexEqual < line.Length - 1) - { - var headerKey = line[..indexEqual]; - var headerValue = line[(indexEqual + 1)..]; + foreach (var line in lines) + { + var indexEqual = line.IndexOf('=', StringComparison.Ordinal); - headerValue = await FormatAsync(headerValue, @event); + if (indexEqual > 0 && indexEqual < line.Length - 1) + { + var headerKey = line[..indexEqual]; + var headerValue = line[(indexEqual + 1)..]; - headersDictionary[headerKey] = headerValue; - } - } + headerValue = await FormatAsync(headerValue, @event); - return headersDictionary; + headersDictionary[headerKey] = headerValue; + } } - protected override async Task ExecuteJobAsync(WebhookJob job, - CancellationToken ct = default) - { - var httpClient = httpClientFactory.CreateClient(); + return headersDictionary; + } - var method = HttpMethod.Post; + protected override async Task ExecuteJobAsync(WebhookJob job, + CancellationToken ct = default) + { + var httpClient = httpClientFactory.CreateClient(); - switch (job.Method) - { - case WebhookMethod.PUT: - method = HttpMethod.Put; - break; - case WebhookMethod.GET: - method = HttpMethod.Get; - break; - case WebhookMethod.DELETE: - method = HttpMethod.Delete; - break; - case WebhookMethod.PATCH: - method = HttpMethod.Patch; - break; - } + var method = HttpMethod.Post; - using var request = new HttpRequestMessage(method, job.RequestUrl); + switch (job.Method) + { + case WebhookMethod.PUT: + method = HttpMethod.Put; + break; + case WebhookMethod.GET: + method = HttpMethod.Get; + break; + case WebhookMethod.DELETE: + method = HttpMethod.Delete; + break; + case WebhookMethod.PATCH: + method = HttpMethod.Patch; + break; + } - if (!string.IsNullOrEmpty(job.RequestBody) && job.Method != WebhookMethod.GET) - { - var mediaType = job.RequestBodyType.Or("application/json"); + using var request = new HttpRequestMessage(method, job.RequestUrl); - request.Content = new StringContent(job.RequestBody, Encoding.UTF8, mediaType); - } + if (!string.IsNullOrEmpty(job.RequestBody) && job.Method != WebhookMethod.GET) + { + var mediaType = job.RequestBodyType.Or("application/json"); - request.Headers.Add("User-Agent", "Squidex Webhook"); + request.Content = new StringContent(job.RequestBody, Encoding.UTF8, mediaType); + } - if (job.Headers != null) - { - foreach (var (key, value) in job.Headers) - { - request.Headers.TryAddWithoutValidation(key, value); - } - } + request.Headers.Add("User-Agent", "Squidex Webhook"); - if (!string.IsNullOrWhiteSpace(job.RequestSignature)) + if (job.Headers != null) + { + foreach (var (key, value) in job.Headers) { - request.Headers.Add("X-Signature", job.RequestSignature); + request.Headers.TryAddWithoutValidation(key, value); } + } - request.Headers.Add("X-Application", "Squidex Webhook"); - - return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); + if (!string.IsNullOrWhiteSpace(job.RequestSignature)) + { + request.Headers.Add("X-Signature", job.RequestSignature); } + + request.Headers.Add("X-Application", "Squidex Webhook"); + + return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); } +} - public sealed class WebhookJob - { - public WebhookMethod Method { get; set; } +public sealed class WebhookJob +{ + public WebhookMethod Method { get; set; } - public string RequestUrl { get; set; } + public string RequestUrl { get; set; } - public string RequestSignature { get; set; } + public string RequestSignature { get; set; } - public string RequestBody { get; set; } + public string RequestBody { get; set; } - public string RequestBodyType { get; set; } + public string RequestBodyType { get; set; } - public Dictionary Headers { get; set; } - } + public Dictionary Headers { get; set; } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs index e258571231..c0e66a41ba 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs @@ -9,13 +9,12 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Actions.Webhook +namespace Squidex.Extensions.Actions.Webhook; + +public sealed class WebhookPlugin : IPlugin { - public sealed class WebhookPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddRuleAction(); - } + services.AddRuleAction(); } } diff --git a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs index 3b675b5d28..6280668505 100644 --- a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs +++ b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs @@ -14,83 +14,82 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Extensions.Assets.Azure +namespace Squidex.Extensions.Assets.Azure; + +public sealed class AzureMetadataSource : IAssetMetadataSource { - public sealed class AzureMetadataSource : IAssetMetadataSource + private const long MaxSize = 5 * 1025 * 1024; + private readonly ILogger log; + private readonly ComputerVisionClient client; + private readonly char[] trimChars = { - private const long MaxSize = 5 * 1025 * 1024; - private readonly ILogger log; - private readonly ComputerVisionClient client; - private readonly char[] trimChars = - { - ' ', - '_', - '-' - }; - private readonly List features = new List - { - VisualFeatureTypes.Categories, - VisualFeatureTypes.Description, - VisualFeatureTypes.Color - }; + ' ', + '_', + '-' + }; + private readonly List features = new List + { + VisualFeatureTypes.Categories, + VisualFeatureTypes.Description, + VisualFeatureTypes.Color + }; - public int Order => int.MaxValue; + public int Order => int.MaxValue; - public AzureMetadataSource(IOptions options, - ILogger log) + public AzureMetadataSource(IOptions options, + ILogger log) + { + client = new ComputerVisionClient(new ApiKeyServiceClientCredentials(options.Value.ApiKey)) { - client = new ComputerVisionClient(new ApiKeyServiceClientCredentials(options.Value.ApiKey)) - { - Endpoint = options.Value.Endpoint - }; + Endpoint = options.Value.Endpoint + }; - this.log = log; - } + this.log = log; + } - public async Task EnhanceAsync(UploadAssetCommand command, - CancellationToken ct) + public async Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) + { + try { - try + if (command.Type == AssetType.Image && command.File.FileSize <= MaxSize) { - if (command.Type == AssetType.Image && command.File.FileSize <= MaxSize) + await using (var stream = command.File.OpenRead()) { - await using (var stream = command.File.OpenRead()) - { - var result = await client.AnalyzeImageInStreamAsync(stream, features, cancellationToken: ct); + var result = await client.AnalyzeImageInStreamAsync(stream, features, cancellationToken: ct); - command.Tags ??= new HashSet(); + command.Tags ??= new HashSet(); - if (result.Color?.DominantColorForeground != null) - { - command.Tags.Add($"color/{result.Color.DominantColorForeground.Trim(trimChars).ToLowerInvariant()}"); - } + if (result.Color?.DominantColorForeground != null) + { + command.Tags.Add($"color/{result.Color.DominantColorForeground.Trim(trimChars).ToLowerInvariant()}"); + } - if (result.Categories != null) + if (result.Categories != null) + { + foreach (var category in result.Categories.OrderByDescending(x => x.Score).Take(3)) { - foreach (var category in result.Categories.OrderByDescending(x => x.Score).Take(3)) - { - command.Tags.Add($"category/{category.Name.Trim(trimChars).ToLowerInvariant()}"); - } + command.Tags.Add($"category/{category.Name.Trim(trimChars).ToLowerInvariant()}"); } + } - var description = result.Description?.Captions.MaxBy(x => x.Confidence)?.Text; + var description = result.Description?.Captions.MaxBy(x => x.Confidence)?.Text; - if (description != null) - { - command.Metadata["caption"] = JsonValue.Create(description); - } + if (description != null) + { + command.Metadata["caption"] = JsonValue.Create(description); } } } - catch (Exception ex) - { - log.LogError(ex, "Failed to enrich asset."); - } } - - public IEnumerable Format(IAssetEntity asset) + catch (Exception ex) { - yield break; + log.LogError(ex, "Failed to enrich asset."); } } + + public IEnumerable Format(IAssetEntity asset) + { + yield break; + } } diff --git a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSourceOptions.cs b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSourceOptions.cs index 01376610a8..10e4c74051 100644 --- a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSourceOptions.cs +++ b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSourceOptions.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Extensions.Assets.Azure +namespace Squidex.Extensions.Assets.Azure; + +public sealed class AzureMetadataSourceOptions { - public sealed class AzureMetadataSourceOptions - { - public string Endpoint { get; set; } + public string Endpoint { get; set; } - public string ApiKey { get; set; } + public string ApiKey { get; set; } - public bool IsConfigured() - { - return - !string.IsNullOrWhiteSpace(Endpoint) && - !string.IsNullOrWhiteSpace(ApiKey); - } + public bool IsConfigured() + { + return + !string.IsNullOrWhiteSpace(Endpoint) && + !string.IsNullOrWhiteSpace(ApiKey); } } diff --git a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSourcePlugin.cs b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSourcePlugin.cs index f7a9adc78c..28e4ac5167 100644 --- a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSourcePlugin.cs +++ b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSourcePlugin.cs @@ -11,19 +11,18 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Assets.Azure +namespace Squidex.Extensions.Assets.Azure; + +public sealed class AzureMetadataSourcePlugin : IPlugin { - public sealed class AzureMetadataSourcePlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - var options = config.GetSection("assets:azurecognitive").Get() ?? new (); + var options = config.GetSection("assets:azurecognitive").Get() ?? new (); - if (options.IsConfigured()) - { - services.AddSingleton(); - services.AddSingleton(Options.Create(options)); - } + if (options.IsConfigured()) + { + services.AddSingleton(); + services.AddSingleton(Options.Create(options)); } } } diff --git a/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs b/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs index 376e656f2c..7d7f1fe6e0 100644 --- a/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs +++ b/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs @@ -13,44 +13,43 @@ using Squidex.Assets; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Samples.AssetStore +namespace Squidex.Extensions.Samples.AssetStore; + +public sealed class MemoryAssetStorePlugin : IPlugin, IStartupFilter { - public sealed class MemoryAssetStorePlugin : IPlugin, IStartupFilter + public Action Configure(Action next) { - public Action Configure(Action next) + return builder => { - return builder => + builder.Use(async (context, next) => { - builder.Use(async (context, next) => + if (context.Request.Path.StartsWithSegments("/api/assets/memory", StringComparison.Ordinal)) { - if (context.Request.Path.StartsWithSegments("/api/assets/memory", StringComparison.Ordinal)) - { - context.Response.StatusCode = 200; - - await context.Response.WriteAsync("Memory Asset Store used."); - } - else - { - await next(); - } - }); - - next(builder); - }; - } + context.Response.StatusCode = 200; - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - var storeType = config.GetValue("assetStore:type"); + await context.Response.WriteAsync("Memory Asset Store used."); + } + else + { + await next(); + } + }); + + next(builder); + }; + } - var isMemoryAssetsUsed = string.Equals(storeType, "Memory", StringComparison.OrdinalIgnoreCase); + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + var storeType = config.GetValue("assetStore:type"); - if (isMemoryAssetsUsed) - { - services.AddSingleton(this); + var isMemoryAssetsUsed = string.Equals(storeType, "Memory", StringComparison.OrdinalIgnoreCase); + + if (isMemoryAssetsUsed) + { + services.AddSingleton(this); - services.AddSingleton(); - } + services.AddSingleton(); } } } diff --git a/backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs b/backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs index e2ae585cdb..57219606cc 100644 --- a/backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs +++ b/backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs @@ -9,19 +9,18 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Extensions.Samples.Controllers +namespace Squidex.Extensions.Samples.Controllers; + +public sealed class PluginController : ApiController { - public sealed class PluginController : ApiController + public PluginController(ICommandBus commandBus) + : base(commandBus) { - public PluginController(ICommandBus commandBus) - : base(commandBus) - { - } + } - [Route("plugins/sample")] - public IActionResult Test() - { - return Ok(new { text = "I am Plugin" }); - } + [Route("plugins/sample")] + public IActionResult Test() + { + return Ok(new { text = "I am Plugin" }); } } diff --git a/backend/extensions/Squidex.Extensions/Samples/Middleware/DoubleLinkedContentMiddleware.cs b/backend/extensions/Squidex.Extensions/Samples/Middleware/DoubleLinkedContentMiddleware.cs index c9973fd6a5..fbb69d7c9a 100644 --- a/backend/extensions/Squidex.Extensions/Samples/Middleware/DoubleLinkedContentMiddleware.cs +++ b/backend/extensions/Squidex.Extensions/Samples/Middleware/DoubleLinkedContentMiddleware.cs @@ -12,103 +12,102 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Extensions.Samples.Middleware +namespace Squidex.Extensions.Samples.Middleware; + +public sealed class DoubleLinkedContentMiddleware : ICustomCommandMiddleware { - public sealed class DoubleLinkedContentMiddleware : ICustomCommandMiddleware + private readonly IContentLoader contentLoader; + + public DoubleLinkedContentMiddleware(IContentLoader contentLoader) { - private readonly IContentLoader contentLoader; + this.contentLoader = contentLoader; + } - public DoubleLinkedContentMiddleware(IContentLoader contentLoader) - { - this.contentLoader = contentLoader; - } + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + await next(context, ct); - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + if (context.Command is UpdateContent update && context.IsCompleted && update.SchemaId.Name == "source") { - await next(context, ct); + // After a change is made, the content is put to the command context. + var content = context.Result(); + + var contentPrevious = + await contentLoader.GetAsync( + content.AppId.Id, + content.Id, + content.Version - 1, + ct); + + // The data might have been changed within the domain object. Therefore we do not use the data fro mthe command. + var oldReferenceId = GetReference(contentPrevious?.Data); + var newReferenceId = GetReference(content.Data); + + // If nothing has been changed we can just stop here. + if (newReferenceId == oldReferenceId) + { + return; + } - if (context.Command is UpdateContent update && context.IsCompleted && update.SchemaId.Name == "source") + if (oldReferenceId != null) { - // After a change is made, the content is put to the command context. - var content = context.Result(); - - var contentPrevious = - await contentLoader.GetAsync( - content.AppId.Id, - content.Id, - content.Version - 1, - ct); - - // The data might have been changed within the domain object. Therefore we do not use the data fro mthe command. - var oldReferenceId = GetReference(contentPrevious?.Data); - var newReferenceId = GetReference(content.Data); - - // If nothing has been changed we can just stop here. - if (newReferenceId == oldReferenceId) - { - return; - } + var oldReferenced = await contentLoader.GetAsync(content.AppId.Id, DomainId.Create(oldReferenceId), ct: ct); - if (oldReferenceId != null) + if (oldReferenced != null) { - var oldReferenced = await contentLoader.GetAsync(content.AppId.Id, DomainId.Create(oldReferenceId), ct: ct); + var data = oldReferenced.Data.Clone(); - if (oldReferenced != null) - { - var data = oldReferenced.Data.Clone(); - - // Remove the reference from the old referenced content. - data.Remove("referencing"); + // Remove the reference from the old referenced content. + data.Remove("referencing"); - await UpdateReferencing(context, oldReferenced, data, ct); - } + await UpdateReferencing(context, oldReferenced, data, ct); } + } - if (newReferenceId != null) + if (newReferenceId != null) + { + var newReferenced = await contentLoader.GetAsync(content.AppId.Id, DomainId.Create(newReferenceId), ct: ct); + + if (newReferenced != null) { - var newReferenced = await contentLoader.GetAsync(content.AppId.Id, DomainId.Create(newReferenceId), ct: ct); + var data = newReferenced.Data.Clone(); - if (newReferenced != null) + // Add the reference to the new referenced content. + data["referencing"] = new ContentFieldData { - var data = newReferenced.Data.Clone(); + ["iv"] = JsonValue.Array(content.Id) + }; - // Add the reference to the new referenced content. - data["referencing"] = new ContentFieldData - { - ["iv"] = JsonValue.Array(content.Id) - }; - - await UpdateReferencing(context, newReferenced, data, ct); - } + await UpdateReferencing(context, newReferenced, data, ct); } } } + } - private static async Task UpdateReferencing(CommandContext context, IContentEntity reference, ContentData data, - CancellationToken ct) + private static async Task UpdateReferencing(CommandContext context, IContentEntity reference, ContentData data, + CancellationToken ct) + { + // Also set the expected version, otherwise it will be overriden with the version from the request. + await context.CommandBus.PublishAsync(new UpdateContent { - // Also set the expected version, otherwise it will be overriden with the version from the request. - await context.CommandBus.PublishAsync(new UpdateContent - { - AppId = reference.AppId, - SchemaId = reference.SchemaId, - ContentId = reference.Id, - DoNotScript = true, - DoNotValidate = true, - Data = data, - ExpectedVersion = reference.Version - }, ct); - } + AppId = reference.AppId, + SchemaId = reference.SchemaId, + ContentId = reference.Id, + DoNotScript = true, + DoNotValidate = true, + Data = data, + ExpectedVersion = reference.Version + }, ct); + } - private static string GetReference(ContentData data) + private static string GetReference(ContentData data) + { + if (data != null && data.TryGetValue("reference", out ContentFieldData fieldData)) { - if (data != null && data.TryGetValue("reference", out ContentFieldData fieldData)) - { - return fieldData.Values.OfType().SelectMany(x => x).SingleOrDefault().ToString(); - } - - return null; + return fieldData.Values.OfType().SelectMany(x => x).SingleOrDefault().ToString(); } + + return null; } } diff --git a/backend/extensions/Squidex.Extensions/Text/Azure/AzureIndexDefinition.cs b/backend/extensions/Squidex.Extensions/Text/Azure/AzureIndexDefinition.cs index b03a3977b8..cbe4713ee6 100644 --- a/backend/extensions/Squidex.Extensions/Text/Azure/AzureIndexDefinition.cs +++ b/backend/extensions/Squidex.Extensions/Text/Azure/AzureIndexDefinition.cs @@ -8,133 +8,132 @@ using System.Reflection; using Azure.Search.Documents.Indexes.Models; -namespace Squidex.Extensions.Text.Azure +namespace Squidex.Extensions.Text.Azure; + +public static class AzureIndexDefinition { - public static class AzureIndexDefinition + private static readonly Dictionary FieldAnalyzers = new (StringComparer.OrdinalIgnoreCase) { - private static readonly Dictionary FieldAnalyzers = new (StringComparer.OrdinalIgnoreCase) - { - ["iv"] = ("iv", LexicalAnalyzerName.StandardLucene.ToString()), - ["zh"] = ("zh", LexicalAnalyzerName.ZhHansLucene.ToString()) - }; + ["iv"] = ("iv", LexicalAnalyzerName.StandardLucene.ToString()), + ["zh"] = ("zh", LexicalAnalyzerName.ZhHansLucene.ToString()) + }; - static AzureIndexDefinition() - { - var analyzers = - typeof(LexicalAnalyzerName) - .GetProperties(BindingFlags.Public | BindingFlags.Static) - .Select(x => x.GetValue(null)) - .Select(x => x.ToString()) - .OrderBy(x => x) - .ToList(); + static AzureIndexDefinition() + { + var analyzers = + typeof(LexicalAnalyzerName) + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .Select(x => x.GetValue(null)) + .Select(x => x.ToString()) + .OrderBy(x => x) + .ToList(); - var addedLanguage = new HashSet(); + var addedLanguage = new HashSet(); - foreach (var analyzer in analyzers) - { - var indexOfDot = analyzer.IndexOf('.', StringComparison.Ordinal); + foreach (var analyzer in analyzers) + { + var indexOfDot = analyzer.IndexOf('.', StringComparison.Ordinal); - if (indexOfDot > 0) - { - var language = analyzer[..indexOfDot]; + if (indexOfDot > 0) + { + var language = analyzer[..indexOfDot]; - var isValidLanguage = - language.Length == 2 || - language.StartsWith("zh-", StringComparison.Ordinal); + var isValidLanguage = + language.Length == 2 || + language.StartsWith("zh-", StringComparison.Ordinal); - if (isValidLanguage && addedLanguage.Add(language)) - { - var fieldName = language.Replace('-', '_'); + if (isValidLanguage && addedLanguage.Add(language)) + { + var fieldName = language.Replace('-', '_'); - FieldAnalyzers[language] = (fieldName, analyzer); - } + FieldAnalyzers[language] = (fieldName, analyzer); } } } + } - public static string GetFieldName(string key) + public static string GetFieldName(string key) + { + if (FieldAnalyzers.TryGetValue(key, out var analyzer)) { - if (FieldAnalyzers.TryGetValue(key, out var analyzer)) - { - return analyzer.Field; - } + return analyzer.Field; + } - if (key.Length > 0) - { - var language = key[2..]; + if (key.Length > 0) + { + var language = key[2..]; - if (FieldAnalyzers.TryGetValue(language, out analyzer)) - { - return analyzer.Field; - } + if (FieldAnalyzers.TryGetValue(language, out analyzer)) + { + return analyzer.Field; } - - return "iv"; } - public static SearchIndex Create(string indexName) + return "iv"; + } + + public static SearchIndex Create(string indexName) + { + var fields = new List { - var fields = new List + new SimpleField("docId", SearchFieldDataType.String) { - new SimpleField("docId", SearchFieldDataType.String) - { - IsKey = true - }, - new SimpleField("appId", SearchFieldDataType.String) - { - IsFilterable = true - }, - new SimpleField("appName", SearchFieldDataType.String) - { - IsFilterable = false - }, - new SimpleField("contentId", SearchFieldDataType.String) - { - IsFilterable = false - }, - new SimpleField("schemaId", SearchFieldDataType.String) - { - IsFilterable = true - }, - new SimpleField("schemaName", SearchFieldDataType.String) - { - IsFilterable = false - }, - new SimpleField("serveAll", SearchFieldDataType.Boolean) - { - IsFilterable = true - }, - new SimpleField("servePublished", SearchFieldDataType.Boolean) - { - IsFilterable = true - }, - new SimpleField("geoObject", SearchFieldDataType.GeographyPoint) - { - IsFilterable = true - }, - new SimpleField("geoField", SearchFieldDataType.String) - { - IsFilterable = true - } - }; - - foreach (var (field, analyzer) in FieldAnalyzers.Values) + IsKey = true + }, + new SimpleField("appId", SearchFieldDataType.String) { - fields.Add( - new SearchableField(field) - { - IsFilterable = false, - IsFacetable = false, - AnalyzerName = analyzer - }); - } - - var index = new SearchIndex(indexName) + IsFilterable = true + }, + new SimpleField("appName", SearchFieldDataType.String) + { + IsFilterable = false + }, + new SimpleField("contentId", SearchFieldDataType.String) + { + IsFilterable = false + }, + new SimpleField("schemaId", SearchFieldDataType.String) { - Fields = fields - }; + IsFilterable = true + }, + new SimpleField("schemaName", SearchFieldDataType.String) + { + IsFilterable = false + }, + new SimpleField("serveAll", SearchFieldDataType.Boolean) + { + IsFilterable = true + }, + new SimpleField("servePublished", SearchFieldDataType.Boolean) + { + IsFilterable = true + }, + new SimpleField("geoObject", SearchFieldDataType.GeographyPoint) + { + IsFilterable = true + }, + new SimpleField("geoField", SearchFieldDataType.String) + { + IsFilterable = true + } + }; - return index; + foreach (var (field, analyzer) in FieldAnalyzers.Values) + { + fields.Add( + new SearchableField(field) + { + IsFilterable = false, + IsFacetable = false, + AnalyzerName = analyzer + }); } + + var index = new SearchIndex(indexName) + { + Fields = fields + }; + + return index; } } diff --git a/backend/extensions/Squidex.Extensions/Text/Azure/AzureTextIndex.cs b/backend/extensions/Squidex.Extensions/Text/Azure/AzureTextIndex.cs index 6d36cd28fb..6d3629c752 100644 --- a/backend/extensions/Squidex.Extensions/Text/Azure/AzureTextIndex.cs +++ b/backend/extensions/Squidex.Extensions/Text/Azure/AzureTextIndex.cs @@ -15,170 +15,169 @@ using Squidex.Hosting; using Squidex.Infrastructure; -namespace Squidex.Extensions.Text.Azure +namespace Squidex.Extensions.Text.Azure; + +public sealed class AzureTextIndex : IInitializable, ITextIndex { - public sealed class AzureTextIndex : IInitializable, ITextIndex + private readonly SearchIndexClient indexClient; + private readonly SearchClient searchClient; + private readonly QueryParser queryParser = new QueryParser(AzureIndexDefinition.GetFieldName); + + public AzureTextIndex( + string serviceEndpoint, + string serviceApiKey, + string indexName) { - private readonly SearchIndexClient indexClient; - private readonly SearchClient searchClient; - private readonly QueryParser queryParser = new QueryParser(AzureIndexDefinition.GetFieldName); - - public AzureTextIndex( - string serviceEndpoint, - string serviceApiKey, - string indexName) - { - indexClient = new SearchIndexClient(new Uri(serviceEndpoint), new AzureKeyCredential(serviceApiKey)); + indexClient = new SearchIndexClient(new Uri(serviceEndpoint), new AzureKeyCredential(serviceApiKey)); - searchClient = indexClient.GetSearchClient(indexName); - } - - public async Task InitializeAsync( - CancellationToken ct) - { - await CreateIndexAsync(ct); - } + searchClient = indexClient.GetSearchClient(indexName); + } - public async Task ClearAsync( - CancellationToken ct = default) - { - await indexClient.DeleteIndexAsync(searchClient.IndexName, ct); + public async Task InitializeAsync( + CancellationToken ct) + { + await CreateIndexAsync(ct); + } - await CreateIndexAsync(ct); - } + public async Task ClearAsync( + CancellationToken ct = default) + { + await indexClient.DeleteIndexAsync(searchClient.IndexName, ct); - private async Task CreateIndexAsync( - CancellationToken ct) - { - var index = AzureIndexDefinition.Create(searchClient.IndexName); + await CreateIndexAsync(ct); + } - await indexClient.CreateOrUpdateIndexAsync(index, true, true, ct); - } + private async Task CreateIndexAsync( + CancellationToken ct) + { + var index = AzureIndexDefinition.Create(searchClient.IndexName); - public async Task ExecuteAsync(IndexCommand[] commands, - CancellationToken ct = default) - { - var batch = IndexDocumentsBatch.Create(); + await indexClient.CreateOrUpdateIndexAsync(index, true, true, ct); + } - commands.Foreach(x => CommandFactory.CreateCommands(x, batch.Actions)); + public async Task ExecuteAsync(IndexCommand[] commands, + CancellationToken ct = default) + { + var batch = IndexDocumentsBatch.Create(); - if (batch.Actions.Count == 0) - { - return; - } + commands.Foreach(x => CommandFactory.CreateCommands(x, batch.Actions)); - await searchClient.IndexDocumentsAsync(batch, cancellationToken: ct); + if (batch.Actions.Count == 0) + { + return; } - public async Task> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, - CancellationToken ct = default) - { - Guard.NotNull(app); - Guard.NotNull(query); + await searchClient.IndexDocumentsAsync(batch, cancellationToken: ct); + } - var result = new List<(DomainId Id, double Score)>(); + public async Task> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); - await SearchAsync(result, "*", BuildGeoQuery(query, scope), query.Take, 1, ct); + var result = new List<(DomainId Id, double Score)>(); - return result.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList(); - } + await SearchAsync(result, "*", BuildGeoQuery(query, scope), query.Take, 1, ct); - public async Task> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, - CancellationToken ct = default) - { - Guard.NotNull(app); - Guard.NotNull(query); + return result.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList(); + } - var parsed = queryParser.Parse(query.Text); + public async Task> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); - if (parsed == null) - { - return null; - } + var parsed = queryParser.Parse(query.Text); - var result = new List<(DomainId Id, double Score)>(); + if (parsed == null) + { + return null; + } - if (query.RequiredSchemaIds?.Count > 0) - { - await SearchBySchemaAsync(result, parsed.Text, query.RequiredSchemaIds, scope, query.Take, 1, ct); - } - else if (query.PreferredSchemaId == null) - { - await SearchByAppAsync(result, parsed.Text, app, scope, query.Take, 1, ct); - } - else - { - var halfTake = query.Take / 2; + var result = new List<(DomainId Id, double Score)>(); - var schemaIds = Enumerable.Repeat(query.PreferredSchemaId.Value, 1); + if (query.RequiredSchemaIds?.Count > 0) + { + await SearchBySchemaAsync(result, parsed.Text, query.RequiredSchemaIds, scope, query.Take, 1, ct); + } + else if (query.PreferredSchemaId == null) + { + await SearchByAppAsync(result, parsed.Text, app, scope, query.Take, 1, ct); + } + else + { + var halfTake = query.Take / 2; - await SearchBySchemaAsync(result, parsed.Text, schemaIds, scope, halfTake, 1.1, ct); - await SearchByAppAsync(result, parsed.Text, app, scope, halfTake, 1, ct); - } + var schemaIds = Enumerable.Repeat(query.PreferredSchemaId.Value, 1); - return result.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList(); + await SearchBySchemaAsync(result, parsed.Text, schemaIds, scope, halfTake, 1.1, ct); + await SearchByAppAsync(result, parsed.Text, app, scope, halfTake, 1, ct); } - private Task SearchBySchemaAsync(List<(DomainId, double)> result, string text, IEnumerable schemaIds, SearchScope scope, int take, double factor, - CancellationToken ct = default) - { - var searchField = GetServeField(scope); + return result.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList(); + } - var filter = $"{string.Join(" or ", schemaIds.Select(x => $"schemaId eq '{x}'"))} and {searchField} eq true"; + private Task SearchBySchemaAsync(List<(DomainId, double)> result, string text, IEnumerable schemaIds, SearchScope scope, int take, double factor, + CancellationToken ct = default) + { + var searchField = GetServeField(scope); - return SearchAsync(result, text, filter, take, factor, ct); - } + var filter = $"{string.Join(" or ", schemaIds.Select(x => $"schemaId eq '{x}'"))} and {searchField} eq true"; - private Task SearchByAppAsync(List<(DomainId, double)> result, string text, IAppEntity app, SearchScope scope, int take, double factor, - CancellationToken ct = default) - { - var searchField = GetServeField(scope); + return SearchAsync(result, text, filter, take, factor, ct); + } + + private Task SearchByAppAsync(List<(DomainId, double)> result, string text, IAppEntity app, SearchScope scope, int take, double factor, + CancellationToken ct = default) + { + var searchField = GetServeField(scope); - var filter = $"appId eq '{app.Id}' and {searchField} eq true"; + var filter = $"appId eq '{app.Id}' and {searchField} eq true"; - return SearchAsync(result, text, filter, take, factor, ct); - } + return SearchAsync(result, text, filter, take, factor, ct); + } - private async Task SearchAsync(List<(DomainId, double)> result, string text, string filter, int take, double factor, - CancellationToken ct = default) + private async Task SearchAsync(List<(DomainId, double)> result, string text, string filter, int take, double factor, + CancellationToken ct = default) + { + var searchOptions = new SearchOptions { - var searchOptions = new SearchOptions - { - Filter = filter - }; + Filter = filter + }; - searchOptions.Select.Add("contentId"); - searchOptions.Size = take; - searchOptions.QueryType = SearchQueryType.Full; + searchOptions.Select.Add("contentId"); + searchOptions.Size = take; + searchOptions.QueryType = SearchQueryType.Full; - var results = await searchClient.SearchAsync(text, searchOptions, ct); + var results = await searchClient.SearchAsync(text, searchOptions, ct); - await foreach (var item in results.Value.GetResultsAsync().WithCancellation(ct)) + await foreach (var item in results.Value.GetResultsAsync().WithCancellation(ct)) + { + if (item != null) { - if (item != null) - { - var id = DomainId.Create(item.Document["contentId"].ToString()); + var id = DomainId.Create(item.Document["contentId"].ToString()); - result.Add((id, factor * item.Score ?? 0)); - } + result.Add((id, factor * item.Score ?? 0)); } } + } - private static string BuildGeoQuery(GeoQuery query, SearchScope scope) - { - var (schema, field, lat, lng, radius, _) = query; + private static string BuildGeoQuery(GeoQuery query, SearchScope scope) + { + var (schema, field, lat, lng, radius, _) = query; - var searchField = GetServeField(scope); - var searchDistance = radius / 1000; + var searchField = GetServeField(scope); + var searchDistance = radius / 1000; - return $"schemaId eq '{schema}' and geoField eq '{field}' and geo.distance(geoObject, geography'POINT({lng} {lat})') lt {searchDistance} and {searchField} eq true"; - } + return $"schemaId eq '{schema}' and geoField eq '{field}' and geo.distance(geoObject, geography'POINT({lng} {lat})') lt {searchDistance} and {searchField} eq true"; + } - private static string GetServeField(SearchScope scope) - { - return scope == SearchScope.Published ? - "servePublished" : - "serveAll"; - } + private static string GetServeField(SearchScope scope) + { + return scope == SearchScope.Published ? + "servePublished" : + "serveAll"; } } diff --git a/backend/extensions/Squidex.Extensions/Text/Azure/AzureTextPlugin.cs b/backend/extensions/Squidex.Extensions/Text/Azure/AzureTextPlugin.cs index 89fae82d31..6c5f7b7666 100644 --- a/backend/extensions/Squidex.Extensions/Text/Azure/AzureTextPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Text/Azure/AzureTextPlugin.cs @@ -12,50 +12,49 @@ using Squidex.Hosting.Configuration; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Text.Azure +namespace Squidex.Extensions.Text.Azure; + +public sealed class AzureTextPlugin : IPlugin { - public sealed class AzureTextPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) + var fullTextType = config.GetValue("fullText:type"); + + if (string.Equals(fullTextType, "Azure", StringComparison.OrdinalIgnoreCase)) { - var fullTextType = config.GetValue("fullText:type"); + var serviceEndpoint = config.GetValue("fullText:azure:serviceEndpoint"); - if (string.Equals(fullTextType, "Azure", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(serviceEndpoint)) { - var serviceEndpoint = config.GetValue("fullText:azure:serviceEndpoint"); - - if (string.IsNullOrWhiteSpace(serviceEndpoint)) - { - var error = new ConfigurationError("Value is required.", "fullText:azure:serviceEndpoint"); + var error = new ConfigurationError("Value is required.", "fullText:azure:serviceEndpoint"); - throw new ConfigurationException(error); - } + throw new ConfigurationException(error); + } - var serviceApiKey = config.GetValue("fullText:azure:apiKey"); + var serviceApiKey = config.GetValue("fullText:azure:apiKey"); - if (string.IsNullOrWhiteSpace(serviceApiKey)) - { - var error = new ConfigurationError("Value is required.", "fullText:azure:apiKey"); + if (string.IsNullOrWhiteSpace(serviceApiKey)) + { + var error = new ConfigurationError("Value is required.", "fullText:azure:apiKey"); - throw new ConfigurationException(error); - } + throw new ConfigurationException(error); + } - var indexName = config.GetValue("fullText:azure:indexName"); + var indexName = config.GetValue("fullText:azure:indexName"); - if (string.IsNullOrWhiteSpace(indexName)) - { - indexName = "squidex-index"; - } + if (string.IsNullOrWhiteSpace(indexName)) + { + indexName = "squidex-index"; + } - services.AddSingleton( - c => new AzureTextIndex(serviceEndpoint, serviceApiKey, indexName)); + services.AddSingleton( + c => new AzureTextIndex(serviceEndpoint, serviceApiKey, indexName)); - services.AddSingleton( - c => c.GetRequiredService()); + services.AddSingleton( + c => c.GetRequiredService()); - services.AddSingleton( - c => c.GetRequiredService()); - } + services.AddSingleton( + c => c.GetRequiredService()); } } } diff --git a/backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs b/backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs index 44416388b9..aac1de14ea 100644 --- a/backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs +++ b/backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs @@ -10,106 +10,105 @@ using NetTopologySuite.Geometries; using Squidex.Domain.Apps.Entities.Contents.Text; -namespace Squidex.Extensions.Text.Azure +namespace Squidex.Extensions.Text.Azure; + +public static class CommandFactory { - public static class CommandFactory + public static void CreateCommands(IndexCommand command, IList> batch) { - public static void CreateCommands(IndexCommand command, IList> batch) + switch (command) { - switch (command) - { - case UpsertIndexEntry upsert: - UpsertTextEntry(upsert, batch); - break; - case UpdateIndexEntry update: - UpdateEntry(update, batch); - break; - case DeleteIndexEntry delete: - DeleteEntry(delete, batch); - break; - } + case UpsertIndexEntry upsert: + UpsertTextEntry(upsert, batch); + break; + case UpdateIndexEntry update: + UpdateEntry(update, batch); + break; + case DeleteIndexEntry delete: + DeleteEntry(delete, batch); + break; } + } - private static void UpsertTextEntry(UpsertIndexEntry upsert, IList> batch) - { - var geoField = string.Empty; - var geoObject = (object)null; + private static void UpsertTextEntry(UpsertIndexEntry upsert, IList> batch) + { + var geoField = string.Empty; + var geoObject = (object)null; - if (upsert.GeoObjects != null) + if (upsert.GeoObjects != null) + { + foreach (var (key, value) in upsert.GeoObjects) { - foreach (var (key, value) in upsert.GeoObjects) + if (value is Point point) { - if (value is Point point) + geoField = key; + geoObject = new { - geoField = key; - geoObject = new + type = "Point", + coordinates = new[] { - type = "Point", - coordinates = new[] - { - point.Coordinate.X, - point.Coordinate.Y - } - }; - break; - } + point.Coordinate.X, + point.Coordinate.Y + } + }; + break; } } + } - if (upsert.Texts != null || geoObject != null) + if (upsert.Texts != null || geoObject != null) + { + var document = new SearchDocument { - var document = new SearchDocument - { - ["docId"] = upsert.DocId.ToBase64(), - ["appId"] = upsert.AppId.Id.ToString(), - ["appName"] = upsert.AppId.Name, - ["contentId"] = upsert.ContentId.ToString(), - ["schemaId"] = upsert.SchemaId.Id.ToString(), - ["schemaName"] = upsert.SchemaId.Name, - ["serveAll"] = upsert.ServeAll, - ["servePublished"] = upsert.ServePublished, - ["geoField"] = geoField, - ["geoObject"] = geoObject - }; - - foreach (var (key, value) in upsert.Texts) - { - var text = value; + ["docId"] = upsert.DocId.ToBase64(), + ["appId"] = upsert.AppId.Id.ToString(), + ["appName"] = upsert.AppId.Name, + ["contentId"] = upsert.ContentId.ToString(), + ["schemaId"] = upsert.SchemaId.Id.ToString(), + ["schemaName"] = upsert.SchemaId.Name, + ["serveAll"] = upsert.ServeAll, + ["servePublished"] = upsert.ServePublished, + ["geoField"] = geoField, + ["geoObject"] = geoObject + }; - var languageCode = AzureIndexDefinition.GetFieldName(key); + foreach (var (key, value) in upsert.Texts) + { + var text = value; - if (document.TryGetValue(languageCode, out var existing)) - { - text = $"{existing} {value}"; - } + var languageCode = AzureIndexDefinition.GetFieldName(key); - document[languageCode] = text; + if (document.TryGetValue(languageCode, out var existing)) + { + text = $"{existing} {value}"; } - batch.Add(IndexDocumentsAction.MergeOrUpload(document)); + document[languageCode] = text; } - } - - private static void UpdateEntry(UpdateIndexEntry update, IList> batch) - { - var document = new SearchDocument - { - ["docId"] = update.DocId.ToBase64(), - ["serveAll"] = update.ServeAll, - ["servePublished"] = update.ServePublished - }; batch.Add(IndexDocumentsAction.MergeOrUpload(document)); } + } - private static void DeleteEntry(DeleteIndexEntry delete, IList> batch) + private static void UpdateEntry(UpdateIndexEntry update, IList> batch) + { + var document = new SearchDocument { - batch.Add(IndexDocumentsAction.Delete("docId", delete.DocId.ToBase64())); - } + ["docId"] = update.DocId.ToBase64(), + ["serveAll"] = update.ServeAll, + ["servePublished"] = update.ServePublished + }; - private static string ToBase64(this string value) - { - return Convert.ToBase64String(Encoding.Default.GetBytes(value)); - } + batch.Add(IndexDocumentsAction.MergeOrUpload(document)); + } + + private static void DeleteEntry(DeleteIndexEntry delete, IList> batch) + { + batch.Add(IndexDocumentsAction.Delete("docId", delete.DocId.ToBase64())); + } + + private static string ToBase64(this string value) + { + return Convert.ToBase64String(Encoding.Default.GetBytes(value)); } } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs index 3d929e1fb1..72c5cd1fdf 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs @@ -8,122 +8,121 @@ using NetTopologySuite.Geometries; using Squidex.Domain.Apps.Entities.Contents.Text; -namespace Squidex.Extensions.Text.ElasticSearch +namespace Squidex.Extensions.Text.ElasticSearch; + +public static class CommandFactory { - public static class CommandFactory + public static void CreateCommands(IndexCommand command, List args, string indexName) { - public static void CreateCommands(IndexCommand command, List args, string indexName) + switch (command) { - switch (command) - { - case UpsertIndexEntry upsert: - UpsertEntry(upsert, args, indexName); - break; - case UpdateIndexEntry update: - UpdateEntry(update, args, indexName); - break; - case DeleteIndexEntry delete: - DeleteEntry(delete, args, indexName); - break; - } + case UpsertIndexEntry upsert: + UpsertEntry(upsert, args, indexName); + break; + case UpdateIndexEntry update: + UpdateEntry(update, args, indexName); + break; + case DeleteIndexEntry delete: + DeleteEntry(delete, args, indexName); + break; } + } - private static void UpsertEntry(UpsertIndexEntry upsert, List args, string indexName) - { - var geoField = string.Empty; - var geoObject = (object)null; + private static void UpsertEntry(UpsertIndexEntry upsert, List args, string indexName) + { + var geoField = string.Empty; + var geoObject = (object)null; - if (upsert.GeoObjects != null) + if (upsert.GeoObjects != null) + { + foreach (var (key, value) in upsert.GeoObjects) { - foreach (var (key, value) in upsert.GeoObjects) + if (value is Point point) { - if (value is Point point) + geoField = key; + geoObject = new { - geoField = key; - geoObject = new - { - lat = point.Coordinate.X, - lon = point.Coordinate.Y - }; - break; - } + lat = point.Coordinate.X, + lon = point.Coordinate.Y + }; + break; } } + } - if (upsert.Texts != null || geoObject != null) + if (upsert.Texts != null || geoObject != null) + { + args.Add(new { - args.Add(new + index = new { - index = new - { - _id = upsert.DocId, - _index = indexName - } - }); - - var texts = new Dictionary(); + _id = upsert.DocId, + _index = indexName + } + }); - foreach (var (key, value) in upsert.Texts) - { - var text = value; + var texts = new Dictionary(); - var languageCode = ElasticSearchIndexDefinition.GetFieldName(key); + foreach (var (key, value) in upsert.Texts) + { + var text = value; - if (texts.TryGetValue(languageCode, out var existing)) - { - text = $"{existing} {value}"; - } + var languageCode = ElasticSearchIndexDefinition.GetFieldName(key); - texts[languageCode] = text; + if (texts.TryGetValue(languageCode, out var existing)) + { + text = $"{existing} {value}"; } - args.Add(new - { - appId = upsert.AppId.Id.ToString(), - appName = upsert.AppId.Name, - contentId = upsert.ContentId.ToString(), - schemaId = upsert.SchemaId.Id.ToString(), - schemaName = upsert.SchemaId.Name, - serveAll = upsert.ServeAll, - servePublished = upsert.ServePublished, - texts, - geoField, - geoObject - }); + texts[languageCode] = text; } - } - private static void UpdateEntry(UpdateIndexEntry update, List args, string indexName) - { args.Add(new { - update = new - { - _id = update.DocId, - _index = indexName - } + appId = upsert.AppId.Id.ToString(), + appName = upsert.AppId.Name, + contentId = upsert.ContentId.ToString(), + schemaId = upsert.SchemaId.Id.ToString(), + schemaName = upsert.SchemaId.Name, + serveAll = upsert.ServeAll, + servePublished = upsert.ServePublished, + texts, + geoField, + geoObject }); + } + } - args.Add(new + private static void UpdateEntry(UpdateIndexEntry update, List args, string indexName) + { + args.Add(new + { + update = new { - doc = new - { - serveAll = update.ServeAll, - servePublished = update.ServePublished - } - }); - } + _id = update.DocId, + _index = indexName + } + }); - private static void DeleteEntry(DeleteIndexEntry delete, List args, string indexName) + args.Add(new { - args.Add(new + doc = new { - delete = new - { - _id = delete.DocId, - _index = indexName - } - }); - } + serveAll = update.ServeAll, + servePublished = update.ServePublished + } + }); + } + + private static void DeleteEntry(DeleteIndexEntry delete, List args, string indexName) + { + args.Add(new + { + delete = new + { + _id = delete.DocId, + _index = indexName + } + }); } } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchClient.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchClient.cs index 2987e75134..fd2dfb53c6 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchClient.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchClient.cs @@ -7,72 +7,71 @@ using Elasticsearch.Net; -namespace Squidex.Extensions.Text.ElasticSearch +namespace Squidex.Extensions.Text.ElasticSearch; + +public sealed class ElasticSearchClient : IElasticSearchClient { - public sealed class ElasticSearchClient : IElasticSearchClient + private readonly IElasticLowLevelClient elasticSearch; + + public ElasticSearchClient(string configurationString) { - private readonly IElasticLowLevelClient elasticSearch; + var config = new ConnectionConfiguration(new Uri(configurationString)); - public ElasticSearchClient(string configurationString) - { - var config = new ConnectionConfiguration(new Uri(configurationString)); + elasticSearch = new ElasticLowLevelClient(config); + } - elasticSearch = new ElasticLowLevelClient(config); - } + public async Task CreateIndexAsync(string indexName, T request, + CancellationToken ct) + { + var result = await elasticSearch.Indices.PutMappingAsync(indexName, CreatePost(request), ctx: ct); - public async Task CreateIndexAsync(string indexName, T request, - CancellationToken ct) + if (!result.Success) { - var result = await elasticSearch.Indices.PutMappingAsync(indexName, CreatePost(request), ctx: ct); - - if (!result.Success) - { - throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); - } + throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); } + } - public async Task BulkAsync(List requests, - CancellationToken ct) - { - var result = await elasticSearch.BulkAsync(CreatePost(requests), ctx: ct); + public async Task BulkAsync(List requests, + CancellationToken ct) + { + var result = await elasticSearch.BulkAsync(CreatePost(requests), ctx: ct); - if (!result.Success) - { - throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); - } + if (!result.Success) + { + throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); } + } - public async Task> SearchAsync(string indexName, T request, - CancellationToken ct) - { - var result = await elasticSearch.SearchAsync(indexName, CreatePost(request), ctx: ct); + public async Task> SearchAsync(string indexName, T request, + CancellationToken ct) + { + var result = await elasticSearch.SearchAsync(indexName, CreatePost(request), ctx: ct); - if (!result.Success) - { - throw result.OriginalException; - } + if (!result.Success) + { + throw result.OriginalException; + } - var hits = new List(); + var hits = new List(); - foreach (var item in result.Body.hits.hits) + foreach (var item in result.Body.hits.hits) + { + if (item != null) { - if (item != null) - { - hits.Add(item); - } + hits.Add(item); } - - return hits; } - private static PostData CreatePost(List requests) - { - return PostData.MultiJson(requests.OfType()); - } + return hits; + } - private static PostData CreatePost(T data) - { - return new SerializableData(data); - } + private static PostData CreatePost(List requests) + { + return PostData.MultiJson(requests.OfType()); + } + + private static PostData CreatePost(T data) + { + return new SerializableData(data); } } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchIndexDefinition.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchIndexDefinition.cs index 2f204c7fc5..5d50035def 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchIndexDefinition.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchIndexDefinition.cs @@ -5,119 +5,118 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Extensions.Text.ElasticSearch +namespace Squidex.Extensions.Text.ElasticSearch; + +public static class ElasticSearchIndexDefinition { - public static class ElasticSearchIndexDefinition + private static readonly Dictionary FieldPaths; + private static readonly Dictionary FieldAnalyzers = new Dictionary { - private static readonly Dictionary FieldPaths; - private static readonly Dictionary FieldAnalyzers = new Dictionary - { - ["ar"] = "arabic", - ["hy"] = "armenian", - ["eu"] = "basque", - ["bn"] = "bengali", - ["br"] = "brazilian", - ["bg"] = "bulgarian", - ["ca"] = "catalan", - ["zh"] = "cjk", - ["ja"] = "cjk", - ["ko"] = "cjk", - ["cs"] = "czech", - ["da"] = "danish", - ["nl"] = "dutch", - ["en"] = "english", - ["fi"] = "finnish", - ["fr"] = "french", - ["gl"] = "galician", - ["de"] = "german", - ["el"] = "greek", - ["hi"] = "hindi", - ["hu"] = "hungarian", - ["id"] = "indonesian", - ["ga"] = "irish", - ["it"] = "italian", - ["lv"] = "latvian", - ["lt"] = "lithuanian", - ["no"] = "norwegian", - ["pt"] = "portuguese", - ["ro"] = "romanian", - ["ru"] = "russian", - ["ku"] = "sorani", - ["es"] = "spanish", - ["sv"] = "swedish", - ["tr"] = "turkish", - ["th"] = "thai" - }; + ["ar"] = "arabic", + ["hy"] = "armenian", + ["eu"] = "basque", + ["bn"] = "bengali", + ["br"] = "brazilian", + ["bg"] = "bulgarian", + ["ca"] = "catalan", + ["zh"] = "cjk", + ["ja"] = "cjk", + ["ko"] = "cjk", + ["cs"] = "czech", + ["da"] = "danish", + ["nl"] = "dutch", + ["en"] = "english", + ["fi"] = "finnish", + ["fr"] = "french", + ["gl"] = "galician", + ["de"] = "german", + ["el"] = "greek", + ["hi"] = "hindi", + ["hu"] = "hungarian", + ["id"] = "indonesian", + ["ga"] = "irish", + ["it"] = "italian", + ["lv"] = "latvian", + ["lt"] = "lithuanian", + ["no"] = "norwegian", + ["pt"] = "portuguese", + ["ro"] = "romanian", + ["ru"] = "russian", + ["ku"] = "sorani", + ["es"] = "spanish", + ["sv"] = "swedish", + ["tr"] = "turkish", + ["th"] = "thai" + }; - static ElasticSearchIndexDefinition() + static ElasticSearchIndexDefinition() + { + FieldPaths = FieldAnalyzers.ToDictionary(x => x.Key, x => $"texts.{x.Key}"); + } + + public static string GetFieldName(string key) + { + if (FieldAnalyzers.ContainsKey(key)) { - FieldPaths = FieldAnalyzers.ToDictionary(x => x.Key, x => $"texts.{x.Key}"); + return key; } - public static string GetFieldName(string key) + if (key.Length > 0) { - if (FieldAnalyzers.ContainsKey(key)) - { - return key; - } + var language = key[2..]; - if (key.Length > 0) + if (FieldAnalyzers.ContainsKey(language)) { - var language = key[2..]; - - if (FieldAnalyzers.ContainsKey(language)) - { - return language; - } + return language; } + } - return "iv"; + return "iv"; + } + + public static string GetFieldPath(string key) + { + if (FieldPaths.TryGetValue(key, out var path)) + { + return path; } - public static string GetFieldPath(string key) + if (key.Length > 0) { - if (FieldPaths.TryGetValue(key, out var path)) + var language = key[2..]; + + if (FieldPaths.TryGetValue(language, out path)) { return path; } + } - if (key.Length > 0) - { - var language = key[2..]; + return "texts.iv"; + } - if (FieldPaths.TryGetValue(language, out path)) + public static Task ApplyAsync(IElasticSearchClient client, string indexName, + CancellationToken ct = default) + { + var query = new + { + properties = new Dictionary + { + ["geoObject"] = new { - return path; + type = "geo_point" } } + }; - return "texts.iv"; - } - - public static Task ApplyAsync(IElasticSearchClient client, string indexName, - CancellationToken ct = default) + foreach (var (key, analyzer) in FieldAnalyzers) { - var query = new + query.properties[GetFieldPath(key)] = new { - properties = new Dictionary - { - ["geoObject"] = new - { - type = "geo_point" - } - } + type = "text", + analyzer }; - - foreach (var (key, analyzer) in FieldAnalyzers) - { - query.properties[GetFieldPath(key)] = new - { - type = "text", - analyzer - }; - } - - return client.CreateIndexAsync(indexName, query, ct); } + + return client.CreateIndexAsync(indexName, query, ct); } } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs index c54f23db65..1d41d97cc9 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs @@ -13,219 +13,218 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json; -namespace Squidex.Extensions.Text.ElasticSearch +namespace Squidex.Extensions.Text.ElasticSearch; + +public sealed class ElasticSearchTextIndex : ITextIndex, IInitializable { - public sealed class ElasticSearchTextIndex : ITextIndex, IInitializable + private static readonly Regex LanguageRegex = new Regex(@"[^\w]+([a-z\-_]{2,}):", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly Regex LanguageRegexStart = new Regex(@"$^([a-z\-_]{2,}):", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private readonly IJsonSerializer jsonSerializer; + private readonly IElasticSearchClient elasticClient; + private readonly QueryParser queryParser = new QueryParser(ElasticSearchIndexDefinition.GetFieldPath); + private readonly string indexName; + + public ElasticSearchTextIndex(IElasticSearchClient elasticClient, string indexName, IJsonSerializer jsonSerializer) { - private static readonly Regex LanguageRegex = new Regex(@"[^\w]+([a-z\-_]{2,}):", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - private static readonly Regex LanguageRegexStart = new Regex(@"$^([a-z\-_]{2,}):", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - private readonly IJsonSerializer jsonSerializer; - private readonly IElasticSearchClient elasticClient; - private readonly QueryParser queryParser = new QueryParser(ElasticSearchIndexDefinition.GetFieldPath); - private readonly string indexName; - - public ElasticSearchTextIndex(IElasticSearchClient elasticClient, string indexName, IJsonSerializer jsonSerializer) - { - this.elasticClient = elasticClient; - this.indexName = indexName; - this.jsonSerializer = jsonSerializer; - } + this.elasticClient = elasticClient; + this.indexName = indexName; + this.jsonSerializer = jsonSerializer; + } + + public Task InitializeAsync( + CancellationToken ct) + { + return ElasticSearchIndexDefinition.ApplyAsync(elasticClient, indexName, ct); + } + + public Task ClearAsync( + CancellationToken ct = default) + { + return Task.CompletedTask; + } - public Task InitializeAsync( - CancellationToken ct) + public Task ExecuteAsync(IndexCommand[] commands, + CancellationToken ct = default) + { + var args = new List(); + + foreach (var command in commands) { - return ElasticSearchIndexDefinition.ApplyAsync(elasticClient, indexName, ct); + CommandFactory.CreateCommands(command, args, indexName); } - public Task ClearAsync( - CancellationToken ct = default) + if (args.Count == 0) { return Task.CompletedTask; } - public Task ExecuteAsync(IndexCommand[] commands, - CancellationToken ct = default) - { - var args = new List(); - - foreach (var command in commands) - { - CommandFactory.CreateCommands(command, args, indexName); - } + return elasticClient.BulkAsync(args, ct); + } - if (args.Count == 0) - { - return Task.CompletedTask; - } + public async Task> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); - return elasticClient.BulkAsync(args, ct); - } + var serveField = GetServeField(scope); - public async Task> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, - CancellationToken ct = default) + var elasticQuery = new { - Guard.NotNull(app); - Guard.NotNull(query); - - var serveField = GetServeField(scope); - - var elasticQuery = new + query = new { - query = new + @bool = new { - @bool = new + filter = new object[] { - filter = new object[] + new { - new + term = new Dictionary { - term = new Dictionary - { - ["schemaId.keyword"] = query.SchemaId.ToString() - } - }, - new + ["schemaId.keyword"] = query.SchemaId.ToString() + } + }, + new + { + term = new Dictionary { - term = new Dictionary - { - ["geoField.keyword"] = query.Field - } - }, - new + ["geoField.keyword"] = query.Field + } + }, + new + { + term = new Dictionary { - term = new Dictionary - { - [serveField] = "true" - } - }, - new + [serveField] = "true" + } + }, + new + { + geo_distance = new { - geo_distance = new + geoObject = new { - geoObject = new - { - lat = query.Latitude, - lon = query.Longitude - }, - distance = $"{query.Radius}m" - } + lat = query.Latitude, + lon = query.Longitude + }, + distance = $"{query.Radius}m" } } } - }, - _source = new[] - { - "contentId" - }, - size = query.Take - }; + } + }, + _source = new[] + { + "contentId" + }, + size = query.Take + }; - return await SearchAsync(elasticQuery, ct); - } + return await SearchAsync(elasticQuery, ct); + } - public async Task> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, - CancellationToken ct = default) - { - Guard.NotNull(app); - Guard.NotNull(query); + public async Task> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); - var parsed = queryParser.Parse(query.Text); + var parsed = queryParser.Parse(query.Text); - if (parsed == null) - { - return null; - } + if (parsed == null) + { + return null; + } - var serveField = GetServeField(scope); + var serveField = GetServeField(scope); - var elasticQuery = new + var elasticQuery = new + { + query = new { - query = new + @bool = new { - @bool = new + filter = new List { - filter = new List + new { - new - { - term = new Dictionary - { - ["appId.keyword"] = app.Id.ToString() - } - }, - new + term = new Dictionary { - term = new Dictionary - { - [serveField] = "true" - } + ["appId.keyword"] = app.Id.ToString() } }, - must = new + new { - query_string = new + term = new Dictionary { - query = parsed.Text + [serveField] = "true" } - }, - should = new List() - } - }, - _source = new[] - { - "contentId" - }, - size = query.Take - }; + } + }, + must = new + { + query_string = new + { + query = parsed.Text + } + }, + should = new List() + } + }, + _source = new[] + { + "contentId" + }, + size = query.Take + }; - if (query.RequiredSchemaIds?.Count > 0) + if (query.RequiredSchemaIds?.Count > 0) + { + var bySchema = new { - var bySchema = new + terms = new Dictionary { - terms = new Dictionary - { - ["schemaId.keyword"] = query.RequiredSchemaIds.Select(x => x.ToString()).ToArray() - } - }; + ["schemaId.keyword"] = query.RequiredSchemaIds.Select(x => x.ToString()).ToArray() + } + }; - elasticQuery.query.@bool.filter.Add(bySchema); - } - else if (query.PreferredSchemaId.HasValue) + elasticQuery.query.@bool.filter.Add(bySchema); + } + else if (query.PreferredSchemaId.HasValue) + { + var bySchema = new { - var bySchema = new + terms = new Dictionary { - terms = new Dictionary - { - ["schemaId.keyword"] = query.PreferredSchemaId.ToString() - } - }; - - elasticQuery.query.@bool.should.Add(bySchema); - } - - var json = jsonSerializer.Serialize(elasticQuery, true); + ["schemaId.keyword"] = query.PreferredSchemaId.ToString() + } + }; - return await SearchAsync(elasticQuery, ct); + elasticQuery.query.@bool.should.Add(bySchema); } - private async Task> SearchAsync(object query, - CancellationToken ct) - { - var hits = await elasticClient.SearchAsync(indexName, query, ct); + var json = jsonSerializer.Serialize(elasticQuery, true); - var ids = new List(); + return await SearchAsync(elasticQuery, ct); + } - foreach (var item in hits) - { - ids.Add(DomainId.Create(item["_source"]["contentId"])); - } + private async Task> SearchAsync(object query, + CancellationToken ct) + { + var hits = await elasticClient.SearchAsync(indexName, query, ct); - return ids; - } + var ids = new List(); - private static string GetServeField(SearchScope scope) + foreach (var item in hits) { - return scope == SearchScope.Published ? "servePublished" : "serveAll"; + ids.Add(DomainId.Create(item["_source"]["contentId"])); } + + return ids; + } + + private static string GetServeField(SearchScope scope) + { + return scope == SearchScope.Published ? "servePublished" : "serveAll"; } } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs index 813b526981..151b9ccf0d 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs @@ -13,56 +13,55 @@ using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Text.ElasticSearch +namespace Squidex.Extensions.Text.ElasticSearch; + +public sealed class ElasticSearchTextPlugin : IPlugin { - public sealed class ElasticSearchTextPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) + var fullTextType = config.GetValue("fullText:type"); + + if (string.Equals(fullTextType, "elastic", StringComparison.OrdinalIgnoreCase)) { - var fullTextType = config.GetValue("fullText:type"); + var configuration = config.GetValue("fullText:elastic:configuration"); - if (string.Equals(fullTextType, "elastic", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(configuration)) { - var configuration = config.GetValue("fullText:elastic:configuration"); + var error = new ConfigurationError("Value is required.", "fullText:elastic:configuration"); - if (string.IsNullOrWhiteSpace(configuration)) - { - var error = new ConfigurationError("Value is required.", "fullText:elastic:configuration"); + throw new ConfigurationException(error); + } - throw new ConfigurationException(error); - } + var indexName = config.GetValue("fullText:elastic:indexName"); - var indexName = config.GetValue("fullText:elastic:indexName"); + if (string.IsNullOrWhiteSpace(indexName)) + { + indexName = "squidex-index"; + } - if (string.IsNullOrWhiteSpace(indexName)) - { - indexName = "squidex-index"; - } + var openSearch = config.GetValue("fullText:elastic:openSearch"); - var openSearch = config.GetValue("fullText:elastic:openSearch"); + services.AddSingleton(c => + { + IElasticSearchClient elasticSearchClient; - services.AddSingleton(c => + if (openSearch) { - IElasticSearchClient elasticSearchClient; - - if (openSearch) - { - elasticSearchClient = new OpenSearchClient(configuration); - } - else - { - elasticSearchClient = new ElasticSearchClient(configuration); - } + elasticSearchClient = new OpenSearchClient(configuration); + } + else + { + elasticSearchClient = new ElasticSearchClient(configuration); + } - return new ElasticSearchTextIndex(elasticSearchClient, indexName, c.GetRequiredService()); - }); + return new ElasticSearchTextIndex(elasticSearchClient, indexName, c.GetRequiredService()); + }); - services.AddSingleton( - c => c.GetRequiredService()); + services.AddSingleton( + c => c.GetRequiredService()); - services.AddSingleton( - c => c.GetRequiredService()); - } + services.AddSingleton( + c => c.GetRequiredService()); } } } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/IElasticSearchClient.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/IElasticSearchClient.cs index 4d27540e15..94ce5e414f 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/IElasticSearchClient.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/IElasticSearchClient.cs @@ -5,17 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Extensions.Text.ElasticSearch +namespace Squidex.Extensions.Text.ElasticSearch; + +public interface IElasticSearchClient { - public interface IElasticSearchClient - { - Task CreateIndexAsync(string indexName, T request, - CancellationToken ct); + Task CreateIndexAsync(string indexName, T request, + CancellationToken ct); - Task BulkAsync(List requests, - CancellationToken ct); + Task BulkAsync(List requests, + CancellationToken ct); - Task> SearchAsync(string indexName, T request, - CancellationToken ct); - } + Task> SearchAsync(string indexName, T request, + CancellationToken ct); } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/OpenSearchClient.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/OpenSearchClient.cs index 9796e048e2..2b9da16541 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/OpenSearchClient.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/OpenSearchClient.cs @@ -7,72 +7,71 @@ using OpenSearch.Net; -namespace Squidex.Extensions.Text.ElasticSearch +namespace Squidex.Extensions.Text.ElasticSearch; + +public sealed class OpenSearchClient : IElasticSearchClient { - public sealed class OpenSearchClient : IElasticSearchClient + private readonly IOpenSearchLowLevelClient openSearch; + + public OpenSearchClient(string configurationString) { - private readonly IOpenSearchLowLevelClient openSearch; + var config = new ConnectionConfiguration(new Uri(configurationString)); - public OpenSearchClient(string configurationString) - { - var config = new ConnectionConfiguration(new Uri(configurationString)); + openSearch = new OpenSearchLowLevelClient(config); + } - openSearch = new OpenSearchLowLevelClient(config); - } + public async Task CreateIndexAsync(string indexName, T request, + CancellationToken ct) + { + var result = await openSearch.Indices.PutMappingAsync(indexName, CreatePost(request), ctx: ct); - public async Task CreateIndexAsync(string indexName, T request, - CancellationToken ct) + if (!result.Success) { - var result = await openSearch.Indices.PutMappingAsync(indexName, CreatePost(request), ctx: ct); - - if (!result.Success) - { - throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); - } + throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); } + } - public async Task BulkAsync(List requests, - CancellationToken ct) - { - var result = await openSearch.BulkAsync(CreatePost(requests), ctx: ct); + public async Task BulkAsync(List requests, + CancellationToken ct) + { + var result = await openSearch.BulkAsync(CreatePost(requests), ctx: ct); - if (!result.Success) - { - throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); - } + if (!result.Success) + { + throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); } + } - public async Task> SearchAsync(string indexName, T request, - CancellationToken ct) - { - var result = await openSearch.SearchAsync(indexName, CreatePost(request), ctx: ct); + public async Task> SearchAsync(string indexName, T request, + CancellationToken ct) + { + var result = await openSearch.SearchAsync(indexName, CreatePost(request), ctx: ct); - if (!result.Success) - { - throw result.OriginalException; - } + if (!result.Success) + { + throw result.OriginalException; + } - var hits = new List(); + var hits = new List(); - foreach (var item in result.Body.hits.hits) + foreach (var item in result.Body.hits.hits) + { + if (item != null) { - if (item != null) - { - hits.Add(item); - } + hits.Add(item); } - - return hits; } - private static PostData CreatePost(List requests) - { - return PostData.MultiJson(requests.OfType()); - } + return hits; + } - private static PostData CreatePost(T data) - { - return new SerializableData(data); - } + private static PostData CreatePost(List requests) + { + return PostData.MultiJson(requests.OfType()); + } + + private static PostData CreatePost(T data) + { + return new SerializableData(data); } } diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs index af9f1ae311..d28ac725d4 100644 --- a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs @@ -13,101 +13,100 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries; -namespace Squidex.Extensions.Validation +namespace Squidex.Extensions.Validation; + +internal sealed class CompositeUniqueValidator : IValidator { - internal sealed class CompositeUniqueValidator : IValidator + private readonly string tag; + private readonly IContentRepository contentRepository; + + public CompositeUniqueValidator(string tag, IContentRepository contentRepository) { - private readonly string tag; - private readonly IContentRepository contentRepository; + this.tag = tag; - public CompositeUniqueValidator(string tag, IContentRepository contentRepository) - { - this.tag = tag; + this.contentRepository = contentRepository; + } - this.contentRepository = contentRepository; + public void Validate(object value, ValidationContext context) + { + if (value is ContentData data) + { + context.Root.AddTask(async ct => await ValidateAsync(data, context)); } + } + + private async Task ValidateAsync(ContentData data, ValidationContext context) + { + var validateableFields = context.Root.Schema.Fields.Where(IsValidateableField); - public void Validate(object value, ValidationContext context) + var filters = new List>(); + + foreach (var field in validateableFields) { - if (value is ContentData data) + var fieldValue = TryGetValue(field, data); + + if (fieldValue != null) { - context.Root.AddTask(async ct => await ValidateAsync(data, context)); + filters.Add(ClrFilter.Eq($"data.{field.Name}.iv", fieldValue)); } } - private async Task ValidateAsync(ContentData data, ValidationContext context) + if (filters.Count > 0) { - var validateableFields = context.Root.Schema.Fields.Where(IsValidateableField); + var filter = ClrFilter.And(filters); - var filters = new List>(); + var found = await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter); - foreach (var field in validateableFields) + if (found.Any(x => x.Id != context.Root.ContentId)) { - var fieldValue = TryGetValue(field, data); - - if (fieldValue != null) - { - filters.Add(ClrFilter.Eq($"data.{field.Name}.iv", fieldValue)); - } + context.AddError(Enumerable.Empty(), "A content with the same values already exist."); } + } + } - if (filters.Count > 0) - { - var filter = ClrFilter.And(filters); - - var found = await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter); + private static ClrValue TryGetValue(IRootField field, ContentData data) + { + var value = JsonValue.Null; - if (found.Any(x => x.Id != context.Root.ContentId)) - { - context.AddError(Enumerable.Empty(), "A content with the same values already exist."); - } + if (data.TryGetValue(field.Name, out var fieldValue)) + { + if (fieldValue.TryGetValue(InvariantPartitioning.Key, out var temp) && temp != default) + { + value = temp; } } - private static ClrValue TryGetValue(IRootField field, ContentData data) + switch (field.RawProperties) { - var value = JsonValue.Null; - - if (data.TryGetValue(field.Name, out var fieldValue)) - { - if (fieldValue.TryGetValue(InvariantPartitioning.Key, out var temp) && temp != default) + case BooleanFieldProperties when value.Value is bool b: + return b; + case BooleanFieldProperties when value.Value == default: + return ClrValue.Null; + case NumberFieldProperties when value.Value is double n: + return n; + case NumberFieldProperties when value.Value == default: + return ClrValue.Null; + case StringFieldProperties when value.Value is string s: + return s; + case StringFieldProperties when value.Value == default: + return ClrValue.Null; + case ReferencesFieldProperties when value.Value is JsonArray a: + if (a.FirstOrDefault().Value is string first) { - value = temp; + return first; } - } - switch (field.RawProperties) - { - case BooleanFieldProperties when value.Value is bool b: - return b; - case BooleanFieldProperties when value.Value == default: - return ClrValue.Null; - case NumberFieldProperties when value.Value is double n: - return n; - case NumberFieldProperties when value.Value == default: - return ClrValue.Null; - case StringFieldProperties when value.Value is string s: - return s; - case StringFieldProperties when value.Value == default: - return ClrValue.Null; - case ReferencesFieldProperties when value.Value is JsonArray a: - if (a.FirstOrDefault().Value is string first) - { - return first; - } - - break; - } - - return null; + break; } - private bool IsValidateableField(IRootField field) - { - return - field.Partitioning == Partitioning.Invariant && - field.RawProperties.Tags?.Contains(tag) == true && - field.RawProperties is BooleanFieldProperties or NumberFieldProperties or ReferencesFieldProperties or StringFieldProperties; - } + return null; + } + + private bool IsValidateableField(IRootField field) + { + return + field.Partitioning == Partitioning.Invariant && + field.RawProperties.Tags?.Contains(tag) == true && + field.RawProperties is BooleanFieldProperties or NumberFieldProperties or ReferencesFieldProperties or StringFieldProperties; } } diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs index 7be4c1991f..16197370dc 100644 --- a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs @@ -8,39 +8,38 @@ using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Entities.Contents.Repositories; -namespace Squidex.Extensions.Validation +namespace Squidex.Extensions.Validation; + +public sealed class CompositeUniqueValidatorFactory : IValidatorsFactory { - public sealed class CompositeUniqueValidatorFactory : IValidatorsFactory + private const string Prefix = "unique:"; + private readonly IContentRepository contentRepository; + + public CompositeUniqueValidatorFactory(IContentRepository contentRepository) { - private const string Prefix = "unique:"; - private readonly IContentRepository contentRepository; + this.contentRepository = contentRepository; + } - public CompositeUniqueValidatorFactory(IContentRepository contentRepository) + public IEnumerable CreateContentValidators(ValidationContext context, ValidatorFactory createFieldValidator) + { + foreach (var validatorTag in ValidatorTags(context.Root.Schema.Properties.Tags)) { - this.contentRepository = contentRepository; + yield return new CompositeUniqueValidator(validatorTag, contentRepository); } + } - public IEnumerable CreateContentValidators(ValidationContext context, ValidatorFactory createFieldValidator) + private static IEnumerable ValidatorTags(IEnumerable tags) + { + if (tags == null) { - foreach (var validatorTag in ValidatorTags(context.Root.Schema.Properties.Tags)) - { - yield return new CompositeUniqueValidator(validatorTag, contentRepository); - } + yield break; } - private static IEnumerable ValidatorTags(IEnumerable tags) + foreach (var tag in tags) { - if (tags == null) - { - yield break; - } - - foreach (var tag in tags) + if (tag.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) && tag.Length > Prefix.Length) { - if (tag.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) && tag.Length > Prefix.Length) - { - yield return tag; - } + yield return tag; } } } diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs index b5b3762e53..905b1b6b75 100644 --- a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs @@ -10,13 +10,12 @@ using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure.Plugins; -namespace Squidex.Extensions.Validation +namespace Squidex.Extensions.Validation; + +public sealed class CompositeUniqueValidatorPlugin : IPlugin { - public sealed class CompositeUniqueValidatorPlugin : IPlugin + public void ConfigureServices(IServiceCollection services, IConfiguration config) { - public void ConfigureServices(IServiceCollection services, IConfiguration config) - { - services.AddSingleton(); - } + services.AddSingleton(); } } diff --git a/backend/i18n/translator/Squidex.Translator/Commands.cs b/backend/i18n/translator/Squidex.Translator/Commands.cs index 1d0c9fc06f..6dd4d1f6ac 100644 --- a/backend/i18n/translator/Squidex.Translator/Commands.cs +++ b/backend/i18n/translator/Squidex.Translator/Commands.cs @@ -13,175 +13,174 @@ #pragma warning disable CA1822 // Mark members as static -namespace Squidex.Translator +namespace Squidex.Translator; + +public class Commands { - public class Commands + [Command(Name = "info", Description = "Shows information about the translator.")] + public void Info() + { + var version = typeof(Commands).Assembly.GetName().Version; + + Console.WriteLine($"Squidex Translator Version v{version}"); + } + + [Command(Name = "translate", Description = "Translates different parts.")] + [SubCommand] + public class Translate { - [Command(Name = "info", Description = "Shows information about the translator.")] - public void Info() + [Command(Name = "check-backend", Description = "Check backend files.")] + public void CheckBackend(TranslateArguments arguments) { - var version = typeof(Commands).Assembly.GetName().Version; + var (folder, service) = Setup(arguments, "backend"); - Console.WriteLine($"Squidex Translator Version v{version}"); + new CheckBackend(folder, service).Run(); } - [Command(Name = "translate", Description = "Translates different parts.")] - [SubCommand] - public class Translate + [Command(Name = "check-frontend", Description = "Check frontend files.")] + public void CheckFrontend(TranslateArguments arguments) { - [Command(Name = "check-backend", Description = "Check backend files.")] - public void CheckBackend(TranslateArguments arguments) - { - var (folder, service) = Setup(arguments, "backend"); + var (folder, service) = Setup(arguments, "frontend"); - new CheckBackend(folder, service).Run(); - } + new CheckFrontend(folder, service).Run(); + } - [Command(Name = "check-frontend", Description = "Check frontend files.")] - public void CheckFrontend(TranslateArguments arguments) - { - var (folder, service) = Setup(arguments, "frontend"); + [Command(Name = "backend", Description = "Translate backend files.")] + public void Backend(TranslateArguments arguments) + { + var (folder, service) = Setup(arguments, "backend"); - new CheckFrontend(folder, service).Run(); - } + new TranslateBackend(folder, service).Run(); + } - [Command(Name = "backend", Description = "Translate backend files.")] - public void Backend(TranslateArguments arguments) - { - var (folder, service) = Setup(arguments, "backend"); + [Command(Name = "templates", Description = "Translate angular templates.")] + public void Templates(TranslateArguments arguments) + { + var (folder, service) = Setup(arguments, "frontend"); - new TranslateBackend(folder, service).Run(); - } + new TranslateTemplates(folder, service).Run(arguments.Report); + } - [Command(Name = "templates", Description = "Translate angular templates.")] - public void Templates(TranslateArguments arguments) - { - var (folder, service) = Setup(arguments, "frontend"); + [Command(Name = "typescript", Description = "Translate typescript files.")] + public void Typescript(TranslateArguments arguments) + { + var (folder, service) = Setup(arguments, "frontend"); - new TranslateTemplates(folder, service).Run(arguments.Report); - } + new TranslateTypescript(folder, service).Run(); + } - [Command(Name = "typescript", Description = "Translate typescript files.")] - public void Typescript(TranslateArguments arguments) - { - var (folder, service) = Setup(arguments, "frontend"); + [Command(Name = "gen-backend", Description = "Generate the backend translations.")] + public void GenerateBackend(TranslateArguments arguments) + { + var (folder, service) = Setup(arguments, "backend"); - new TranslateTypescript(folder, service).Run(); - } + new GenerateBackendResources(folder, service).Run(); + } - [Command(Name = "gen-backend", Description = "Generate the backend translations.")] - public void GenerateBackend(TranslateArguments arguments) - { - var (folder, service) = Setup(arguments, "backend"); + [Command(Name = "gen-frontend", Description = "Generate the frontend translations.")] + public void GenerateFrontend(TranslateArguments arguments) + { + var (folder, service) = Setup(arguments, "frontend"); - new GenerateBackendResources(folder, service).Run(); - } + new GenerateFrontendResources(folder, service).Run(); + } - [Command(Name = "gen-frontend", Description = "Generate the frontend translations.")] - public void GenerateFrontend(TranslateArguments arguments) - { - var (folder, service) = Setup(arguments, "frontend"); + [Command(Name = "clean-backend", Description = "Clean the backend translations.")] + public void CleanBackend(TranslateArguments arguments) + { + var (_, service) = Setup(arguments, "backend"); - new GenerateFrontendResources(folder, service).Run(); - } + Helper.CleanOtherLocales(service); - [Command(Name = "clean-backend", Description = "Clean the backend translations.")] - public void CleanBackend(TranslateArguments arguments) - { - var (_, service) = Setup(arguments, "backend"); + service.Save(); + } - Helper.CleanOtherLocales(service); + [Command(Name = "clean-frontend", Description = "Clean the frontend translations.")] + public void CleanFrontend(TranslateArguments arguments) + { + var (_, service) = Setup(arguments, "frontend"); - service.Save(); - } + Helper.CleanOtherLocales(service); - [Command(Name = "clean-frontend", Description = "Clean the frontend translations.")] - public void CleanFrontend(TranslateArguments arguments) - { - var (_, service) = Setup(arguments, "frontend"); + service.Save(); + } - Helper.CleanOtherLocales(service); + [Command(Name = "gen-keys", Description = "Generate the keys for translations.")] + public void GenerateBackendKeys(TranslateArguments arguments) + { + var (backendFolder, serviceBackend) = Setup(arguments, "backend"); - service.Save(); - } + new GenerateKeys(backendFolder, serviceBackend, "backend_keys.json").Run(); - [Command(Name = "gen-keys", Description = "Generate the keys for translations.")] - public void GenerateBackendKeys(TranslateArguments arguments) - { - var (backendFolder, serviceBackend) = Setup(arguments, "backend"); + var (frontendFolder, frontendService) = Setup(arguments, "frontend"); - new GenerateKeys(backendFolder, serviceBackend, "backend_keys.json").Run(); + new GenerateKeys(frontendFolder, frontendService, "frontend_keys.json").Run(); + } - var (frontendFolder, frontendService) = Setup(arguments, "frontend"); + [Command(Name = "migrate-backend", Description = "Migrate the backend files.")] + public void MigrateBackend(TranslateArguments arguments) + { + var (_, service) = Setup(arguments, "backend"); - new GenerateKeys(frontendFolder, frontendService, "frontend_keys.json").Run(); - } + service.Migrate(); + } - [Command(Name = "migrate-backend", Description = "Migrate the backend files.")] - public void MigrateBackend(TranslateArguments arguments) - { - var (_, service) = Setup(arguments, "backend"); + [Command(Name = "migrate-frontend", Description = "Migrate the frontend files.")] + public void MigrateFrontend(TranslateArguments arguments) + { + var (_, service) = Setup(arguments, "frontend"); - service.Migrate(); - } + service.Migrate(); + } - [Command(Name = "migrate-frontend", Description = "Migrate the frontend files.")] - public void MigrateFrontend(TranslateArguments arguments) + private static (DirectoryInfo, TranslationService) Setup(TranslateArguments arguments, string fileName) + { + if (!Directory.Exists(arguments.Folder)) { - var (_, service) = Setup(arguments, "frontend"); - - service.Migrate(); + throw new ArgumentException("Folder does not exist.", nameof(arguments)); } - private static (DirectoryInfo, TranslationService) Setup(TranslateArguments arguments, string fileName) - { - if (!Directory.Exists(arguments.Folder)) - { - throw new ArgumentException("Folder does not exist.", nameof(arguments)); - } - - var supportedLocales = new string[] { "en", "nl", "it", "zh" }; + var supportedLocales = new string[] { "en", "nl", "it", "zh" }; - var locales = supportedLocales; + var locales = supportedLocales; - if (arguments.Locales != null && arguments.Locales.Any()) - { - locales = supportedLocales.Intersect(arguments.Locales).ToArray(); - } + if (arguments.Locales != null && arguments.Locales.Any()) + { + locales = supportedLocales.Intersect(arguments.Locales).ToArray(); + } - if (locales.Length == 0) - { - locales = supportedLocales; - } + if (locales.Length == 0) + { + locales = supportedLocales; + } - var translationsDirectory = new DirectoryInfo(Path.Combine(arguments.Folder, "backend", "i18n")); - var translationsService = new TranslationService(translationsDirectory, fileName, locales, arguments.SingleWords); + var translationsDirectory = new DirectoryInfo(Path.Combine(arguments.Folder, "backend", "i18n")); + var translationsService = new TranslationService(translationsDirectory, fileName, locales, arguments.SingleWords); - return (new DirectoryInfo(arguments.Folder), translationsService); - } + return (new DirectoryInfo(arguments.Folder), translationsService); } + } - [Validator(typeof(Validator))] - public sealed class TranslateArguments : IArgumentModel - { - [Operand(Name = "folder", Description = "The squidex folder.")] - public string Folder { get; set; } + [Validator(typeof(Validator))] + public sealed class TranslateArguments : IArgumentModel + { + [Operand(Name = "folder", Description = "The squidex folder.")] + public string Folder { get; set; } - [Option(LongName = "single", ShortName = "s", Description = "Single words only.")] - public bool SingleWords { get; set; } + [Option(LongName = "single", ShortName = "s", Description = "Single words only.")] + public bool SingleWords { get; set; } - [Option(LongName = "report", ShortName = "r")] - public bool Report { get; set; } + [Option(LongName = "report", ShortName = "r")] + public bool Report { get; set; } - [Option(LongName = "locale", ShortName = "l")] - public IEnumerable Locales { get; set; } + [Option(LongName = "locale", ShortName = "l")] + public IEnumerable Locales { get; set; } - public sealed class Validator : AbstractValidator + public sealed class Validator : AbstractValidator + { + public Validator() { - public Validator() - { - RuleFor(x => x.Folder).NotEmpty(); - } + RuleFor(x => x.Folder).NotEmpty(); } } } diff --git a/backend/i18n/translator/Squidex.Translator/Extensions.cs b/backend/i18n/translator/Squidex.Translator/Extensions.cs index 79f7bb62ac..349bec6262 100644 --- a/backend/i18n/translator/Squidex.Translator/Extensions.cs +++ b/backend/i18n/translator/Squidex.Translator/Extensions.cs @@ -5,18 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator +namespace Squidex.Translator; + +public static class Extensions { - public static class Extensions + public static bool IsPotentialText(this string text) { - public static bool IsPotentialText(this string text) - { - return !string.IsNullOrWhiteSpace(text) && text.Any(c => char.IsLetter(c)); - } + return !string.IsNullOrWhiteSpace(text) && text.Any(c => char.IsLetter(c)); + } - public static bool IsPotentialMultiWordText(this string text) - { - return text.Contains(' ', StringComparison.Ordinal) && text.IsPotentialText(); - } + public static bool IsPotentialMultiWordText(this string text) + { + return text.Contains(' ', StringComparison.Ordinal) && text.IsPotentialText(); } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/Backend.cs b/backend/i18n/translator/Squidex.Translator/Processes/Backend.cs index 88ec17dd31..dd03ead38e 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/Backend.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/Backend.cs @@ -5,51 +5,50 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public static class Backend { - public static class Backend + public static DirectoryInfo GetFolder(DirectoryInfo folder) { - public static DirectoryInfo GetFolder(DirectoryInfo folder) - { - return new DirectoryInfo(Path.Combine(folder.FullName, "backend", "src")); - } + return new DirectoryInfo(Path.Combine(folder.FullName, "backend", "src")); + } - public static IEnumerable<(FileInfo, string)> GetFiles(DirectoryInfo folder) + public static IEnumerable<(FileInfo, string)> GetFiles(DirectoryInfo folder) + { + var files = + folder.GetFiles(@"*.cs", SearchOption.AllDirectories).Union( + folder.GetFiles(@"*.cshtml", SearchOption.AllDirectories)); + + foreach (var file in files) { - var files = - folder.GetFiles(@"*.cs", SearchOption.AllDirectories).Union( - folder.GetFiles(@"*.cshtml", SearchOption.AllDirectories)); + var relativeName = Helper.RelativeName(file, folder); - foreach (var file in files) + if (relativeName.Contains("/obj/", StringComparison.Ordinal) || + relativeName.Contains("/bin/", StringComparison.Ordinal)) { - var relativeName = Helper.RelativeName(file, folder); - - if (relativeName.Contains("/obj/", StringComparison.Ordinal) || - relativeName.Contains("/bin/", StringComparison.Ordinal)) - { - continue; - } - - yield return (file, relativeName); + continue; } + + yield return (file, relativeName); } + } + + public static IEnumerable<(FileInfo, string)> GetFilesCS(DirectoryInfo folder) + { + var files = folder.GetFiles(@"*AssetsController.cs", SearchOption.AllDirectories); - public static IEnumerable<(FileInfo, string)> GetFilesCS(DirectoryInfo folder) + foreach (var file in files) { - var files = folder.GetFiles(@"*AssetsController.cs", SearchOption.AllDirectories); + var relativeName = Helper.RelativeName(file, folder); - foreach (var file in files) + if (relativeName.Contains("/obj/", StringComparison.Ordinal) || + relativeName.Contains("/bin/", StringComparison.Ordinal)) { - var relativeName = Helper.RelativeName(file, folder); - - if (relativeName.Contains("/obj/", StringComparison.Ordinal) || - relativeName.Contains("/bin/", StringComparison.Ordinal)) - { - continue; - } - - yield return (file, relativeName); + continue; } + + yield return (file, relativeName); } } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/CheckBackend.cs b/backend/i18n/translator/Squidex.Translator/Processes/CheckBackend.cs index 32fddcf637..36a89ea916 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/CheckBackend.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/CheckBackend.cs @@ -8,54 +8,53 @@ using System.Text.RegularExpressions; using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public class CheckBackend { - public class CheckBackend + private readonly TranslationService service; + private readonly DirectoryInfo folder; + + public CheckBackend(DirectoryInfo folder, TranslationService service) { - private readonly TranslationService service; - private readonly DirectoryInfo folder; + this.folder = Backend.GetFolder(folder); - public CheckBackend(DirectoryInfo folder, TranslationService service) - { - this.folder = Backend.GetFolder(folder); + this.service = service; + } - this.service = service; - } + public void Run() + { + var all = new HashSet(); - public void Run() + foreach (var (file, relativeName) in Backend.GetFiles(folder)) { - var all = new HashSet(); + var content = File.ReadAllText(file.FullName); - foreach (var (file, relativeName) in Backend.GetFiles(folder)) - { - var content = File.ReadAllText(file.FullName); + var translations = new HashSet(); - var translations = new HashSet(); + void AddTranslations(string regex) + { + var matches = Regex.Matches(content, regex, RegexOptions.Singleline | RegexOptions.ExplicitCapture); - void AddTranslations(string regex) + foreach (Match match in matches) { - var matches = Regex.Matches(content, regex, RegexOptions.Singleline | RegexOptions.ExplicitCapture); - - foreach (Match match in matches) - { - var key = match.Groups["Key"].Value; + var key = match.Groups["Key"].Value; - translations.Add(key); + translations.Add(key); - all.Add(key); - } + all.Add(key); } - - AddTranslations("T\\.Get\\(\"(?[^\"]*)\""); - AddTranslations("\"(?history\\.[^\"]*)\""); - - Helper.CheckForFile(service, relativeName, translations); } - Helper.CheckUnused(service, all); - Helper.CheckOtherLocales(service); + AddTranslations("T\\.Get\\(\"(?[^\"]*)\""); + AddTranslations("\"(?history\\.[^\"]*)\""); - service.Save(); + Helper.CheckForFile(service, relativeName, translations); } + + Helper.CheckUnused(service, all); + Helper.CheckOtherLocales(service); + + service.Save(); } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/CheckFrontend.cs b/backend/i18n/translator/Squidex.Translator/Processes/CheckFrontend.cs index 6aae977fd6..85cf68a895 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/CheckFrontend.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/CheckFrontend.cs @@ -8,98 +8,97 @@ using System.Text.RegularExpressions; using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public class CheckFrontend { - public class CheckFrontend + private readonly TranslationService service; + private readonly DirectoryInfo folder; + + public CheckFrontend(DirectoryInfo folder, TranslationService service) { - private readonly TranslationService service; - private readonly DirectoryInfo folder; + this.folder = Frontend.GetFolder(folder); - public CheckFrontend(DirectoryInfo folder, TranslationService service) - { - this.folder = Frontend.GetFolder(folder); + this.service = service; + } - this.service = service; - } + public void Run() + { + var all = new HashSet(); - public void Run() + foreach (var (file, relativeName) in Frontend.GetTemplateFiles(folder)) { - var all = new HashSet(); + var translations = GetTranslationsInTemplate(file); - foreach (var (file, relativeName) in Frontend.GetTemplateFiles(folder)) + foreach (var translation in translations) { - var translations = GetTranslationsInTemplate(file); + all.Add(translation); + } - foreach (var translation in translations) - { - all.Add(translation); - } + Helper.CheckForFile(service, relativeName, translations); + } - Helper.CheckForFile(service, relativeName, translations); - } + foreach (var (file, relativeName) in Frontend.GetTypescriptFiles(folder)) + { + var translations = GetTranslationsInTypescript(file); - foreach (var (file, relativeName) in Frontend.GetTypescriptFiles(folder)) + foreach (var translation in translations) { - var translations = GetTranslationsInTypescript(file); + all.Add(translation); + } - foreach (var translation in translations) - { - all.Add(translation); - } + Helper.CheckForFile(service, relativeName, translations); + } - Helper.CheckForFile(service, relativeName, translations); - } + Helper.CheckUnused(service, all); + Helper.CheckOtherLocales(service); - Helper.CheckUnused(service, all); - Helper.CheckOtherLocales(service); + service.Save(); + } - service.Save(); - } + private static HashSet GetTranslationsInTemplate(FileInfo file) + { + var content = File.ReadAllText(file.FullName); - private static HashSet GetTranslationsInTemplate(FileInfo file) - { - var content = File.ReadAllText(file.FullName); + var translations = new HashSet(); - var translations = new HashSet(); + void AddTranslations(string regex) + { + var matches = Regex.Matches(content, regex, RegexOptions.Singleline | RegexOptions.ExplicitCapture); - void AddTranslations(string regex) + foreach (Match match in matches) { - var matches = Regex.Matches(content, regex, RegexOptions.Singleline | RegexOptions.ExplicitCapture); - - foreach (Match match in matches) - { - translations.Add(match.Groups["Key"].Value); - } + translations.Add(match.Groups["Key"].Value); } + } - AddTranslations("\"i18n\\:(?[^\"]+)\""); - AddTranslations("'i18n\\:(?[^\']+)'"); - AddTranslations("'(?[^\']+)' \\| sqxTranslate"); + AddTranslations("\"i18n\\:(?[^\"]+)\""); + AddTranslations("'i18n\\:(?[^\']+)'"); + AddTranslations("'(?[^\']+)' \\| sqxTranslate"); - return translations; - } + return translations; + } - private static HashSet GetTranslationsInTypescript(FileInfo file) - { - var content = File.ReadAllText(file.FullName); + private static HashSet GetTranslationsInTypescript(FileInfo file) + { + var content = File.ReadAllText(file.FullName); - var translations = new HashSet(); + var translations = new HashSet(); - void AddTranslations(string regex) - { - var matches = Regex.Matches(content, regex, RegexOptions.Singleline | RegexOptions.ExplicitCapture); + void AddTranslations(string regex) + { + var matches = Regex.Matches(content, regex, RegexOptions.Singleline | RegexOptions.ExplicitCapture); - foreach (Match match in matches) - { - translations.Add(match.Groups["Key"].Value); - } + foreach (Match match in matches) + { + translations.Add(match.Groups["Key"].Value); } + } - AddTranslations("'i18n\\:(?[^\']+)'"); - AddTranslations("localizer.get\\('(?[^\']+)'\\)"); - AddTranslations("localizer.getOrKey\\('(?[^\']+)'\\)"); + AddTranslations("'i18n\\:(?[^\']+)'"); + AddTranslations("localizer.get\\('(?[^\']+)'\\)"); + AddTranslations("localizer.getOrKey\\('(?[^\']+)'\\)"); - return translations; - } + return translations; } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/Frontend.cs b/backend/i18n/translator/Squidex.Translator/Processes/Frontend.cs index 5fa31512d4..b9d3d90c1f 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/Frontend.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/Frontend.cs @@ -5,38 +5,37 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public static class Frontend { - public static class Frontend + public static DirectoryInfo GetFolder(DirectoryInfo folder) { - public static DirectoryInfo GetFolder(DirectoryInfo folder) - { - return new DirectoryInfo(Path.Combine(folder.FullName, "frontend", "src", "app")); - } + return new DirectoryInfo(Path.Combine(folder.FullName, "frontend", "src", "app")); + } - public static IEnumerable<(FileInfo, string)> GetTypescriptFiles(DirectoryInfo folder) - { - var files = folder.GetFiles(@"*.ts", SearchOption.AllDirectories); + public static IEnumerable<(FileInfo, string)> GetTypescriptFiles(DirectoryInfo folder) + { + var files = folder.GetFiles(@"*.ts", SearchOption.AllDirectories); - foreach (var file in files) + foreach (var file in files) + { + if (file.Name.EndsWith(".spec.ts", StringComparison.OrdinalIgnoreCase)) { - if (file.Name.EndsWith(".spec.ts", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - yield return (file, Helper.RelativeName(file, folder)); + continue; } + + yield return (file, Helper.RelativeName(file, folder)); } + } - public static IEnumerable<(FileInfo, string)> GetTemplateFiles(DirectoryInfo folder) - { - var files = folder.GetFiles(@"*.html", SearchOption.AllDirectories); + public static IEnumerable<(FileInfo, string)> GetTemplateFiles(DirectoryInfo folder) + { + var files = folder.GetFiles(@"*.html", SearchOption.AllDirectories); - foreach (var file in files) - { - yield return (file, Helper.RelativeName(file, folder)); - } + foreach (var file in files) + { + yield return (file, Helper.RelativeName(file, folder)); } } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/GenerateBackendResources.cs b/backend/i18n/translator/Squidex.Translator/Processes/GenerateBackendResources.cs index 0c630aac13..1327c40dc2 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/GenerateBackendResources.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/GenerateBackendResources.cs @@ -9,60 +9,59 @@ using System.Text.RegularExpressions; using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public sealed class GenerateBackendResources { - public sealed class GenerateBackendResources + private readonly TranslationService service; + private readonly DirectoryInfo folder; + + public GenerateBackendResources(DirectoryInfo folder, TranslationService service) { - private readonly TranslationService service; - private readonly DirectoryInfo folder; + this.folder = new DirectoryInfo(Path.Combine(folder.FullName, "backend", "src", "Squidex.Shared")); + + this.service = service; + } - public GenerateBackendResources(DirectoryInfo folder, TranslationService service) + public void Run() + { + foreach (var locale in service.SupportedLocales) { - this.folder = new DirectoryInfo(Path.Combine(folder.FullName, "backend", "src", "Squidex.Shared")); + var name = locale == + service.MainLocale ? + $"Texts.resx" : + $"Texts.{locale}.resx"; - this.service = service; - } + var fullName = Path.Combine(folder.FullName, name); - public void Run() - { - foreach (var locale in service.SupportedLocales) + using (var writer = new ResXResourceWriter(fullName)) { - var name = locale == - service.MainLocale ? - $"Texts.resx" : - $"Texts.{locale}.resx"; + var texts = service.GetTextsWithFallback(locale); - var fullName = Path.Combine(folder.FullName, name); - - using (var writer = new ResXResourceWriter(fullName)) + foreach (var (key, value) in texts) { - var texts = service.GetTextsWithFallback(locale); + writer.AddResource(key, value); - foreach (var (key, value) in texts) + if (key.StartsWith("annotations_", StringComparison.OrdinalIgnoreCase)) { - writer.AddResource(key, value); - - if (key.StartsWith("annotations_", StringComparison.OrdinalIgnoreCase)) - { - var i = 0; + var i = 0; - var dotnetKey = $"dotnet_{key}"; - var dotnetValue = Regex.Replace(value, "{[^}]*}", m => $"{{{i++}}}"); + var dotnetKey = $"dotnet_{key}"; + var dotnetValue = Regex.Replace(value, "{[^}]*}", m => $"{{{i++}}}"); - writer.AddResource(dotnetKey, dotnetValue); - } + writer.AddResource(dotnetKey, dotnetValue); } } + } - var text = File.ReadAllText(fullName); - - text = text.Replace("System.Resources.NetStandard.ResXResourceReader, System.Resources.NetStandard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", StringComparison.Ordinal); - text = text.Replace("System.Resources.NetStandard.ResXResourceWriter, System.Resources.NetStandard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", StringComparison.Ordinal); + var text = File.ReadAllText(fullName); - File.WriteAllText(fullName, text); - } + text = text.Replace("System.Resources.NetStandard.ResXResourceReader, System.Resources.NetStandard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", StringComparison.Ordinal); + text = text.Replace("System.Resources.NetStandard.ResXResourceWriter, System.Resources.NetStandard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", StringComparison.Ordinal); - service.Save(); + File.WriteAllText(fullName, text); } + + service.Save(); } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/GenerateFrontendResources.cs b/backend/i18n/translator/Squidex.Translator/Processes/GenerateFrontendResources.cs index a6c9946130..c03c0bcf75 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/GenerateFrontendResources.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/GenerateFrontendResources.cs @@ -7,37 +7,36 @@ using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public sealed class GenerateFrontendResources { - public sealed class GenerateFrontendResources - { - private readonly TranslationService service; - private readonly DirectoryInfo folder; + private readonly TranslationService service; + private readonly DirectoryInfo folder; - public GenerateFrontendResources(DirectoryInfo folder, TranslationService service) - { - this.folder = new DirectoryInfo(Path.Combine(folder.FullName, "backend", "i18n")); + public GenerateFrontendResources(DirectoryInfo folder, TranslationService service) + { + this.folder = new DirectoryInfo(Path.Combine(folder.FullName, "backend", "i18n")); - this.service = service; - } + this.service = service; + } - public void Run() + public void Run() + { + foreach (var locale in service.SupportedLocales) { - foreach (var locale in service.SupportedLocales) - { - var fullName = Path.Combine(folder.FullName, $"frontend_{locale}.json"); - - if (!folder.Exists) - { - Directory.CreateDirectory(folder.FullName); - } - - var texts = service.GetTextsWithFallback(locale); + var fullName = Path.Combine(folder.FullName, $"frontend_{locale}.json"); - service.WriteTo(texts, fullName); + if (!folder.Exists) + { + Directory.CreateDirectory(folder.FullName); } - service.Save(); + var texts = service.GetTextsWithFallback(locale); + + service.WriteTo(texts, fullName); } + + service.Save(); } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/GenerateKeys.cs b/backend/i18n/translator/Squidex.Translator/Processes/GenerateKeys.cs index 3b49886f32..66650d7613 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/GenerateKeys.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/GenerateKeys.cs @@ -7,40 +7,39 @@ using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public sealed class GenerateKeys { - public sealed class GenerateKeys + private readonly TranslationService service; + private readonly string fileName; + private readonly DirectoryInfo folder; + + public GenerateKeys(DirectoryInfo folder, TranslationService service, string fileName) { - private readonly TranslationService service; - private readonly string fileName; - private readonly DirectoryInfo folder; + this.folder = folder; + this.service = service; + this.fileName = fileName; + } - public GenerateKeys(DirectoryInfo folder, TranslationService service, string fileName) - { - this.folder = folder; - this.service = service; - this.fileName = fileName; - } + public void Run() + { + var keys = new TranslatedTexts(); - public void Run() + foreach (var text in service.MainTranslations) { - var keys = new TranslatedTexts(); - - foreach (var text in service.MainTranslations) - { - keys.Add(text.Key, string.Empty); - } + keys.Add(text.Key, string.Empty); + } - var fullName = Path.Combine(folder.FullName, fileName); + var fullName = Path.Combine(folder.FullName, fileName); - if (!folder.Exists) - { - Directory.CreateDirectory(folder.FullName); - } + if (!folder.Exists) + { + Directory.CreateDirectory(folder.FullName); + } - service.WriteTo(keys, fullName); + service.WriteTo(keys, fullName); - service.Save(); - } + service.Save(); } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs b/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs index 1f8b1401fc..1d20e3bcd0 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs @@ -7,162 +7,161 @@ using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public static class Helper { - public static class Helper + public static string RelativeName(FileInfo file, DirectoryInfo folder) { - public static string RelativeName(FileInfo file, DirectoryInfo folder) - { - return file.FullName[folder.FullName.Length..].Replace("\\", "/", StringComparison.Ordinal); - } + return file.FullName[folder.FullName.Length..].Replace("\\", "/", StringComparison.Ordinal); + } - public static void CheckOtherLocales(TranslationService service) - { - var mainTranslations = service.MainTranslations; + public static void CheckOtherLocales(TranslationService service) + { + var mainTranslations = service.MainTranslations; - foreach (var (locale, texts) in service.Translations.Where(x => x.Key != service.MainLocale)) - { - Console.WriteLine(); - Console.WriteLine("----- CHECKING <{0}> -----", locale); + foreach (var (locale, texts) in service.Translations.Where(x => x.Key != service.MainLocale)) + { + Console.WriteLine(); + Console.WriteLine("----- CHECKING <{0}> -----", locale); - var notTranslated = mainTranslations.Keys.Except(texts.Keys).ToList(); - var notUsing = texts.Keys.Except(mainTranslations.Keys).ToList(); + var notTranslated = mainTranslations.Keys.Except(texts.Keys).ToList(); + var notUsing = texts.Keys.Except(mainTranslations.Keys).ToList(); - if (notTranslated.Count > 0 || notUsing.Count > 0) + if (notTranslated.Count > 0 || notUsing.Count > 0) + { + if (notTranslated.Count > 0) { - if (notTranslated.Count > 0) - { - Console.WriteLine(); - Console.WriteLine("Translations missing:"); - - foreach (var key in notTranslated.OrderBy(x => x)) - { - Console.Write(" * "); - Console.WriteLine(key); - } - } + Console.WriteLine(); + Console.WriteLine("Translations missing:"); - if (notUsing.Count > 0) + foreach (var key in notTranslated.OrderBy(x => x)) { - Console.WriteLine(); - Console.WriteLine("Translations not used:"); - - foreach (var key in notUsing.OrderBy(x => x)) - { - Console.Write(" * "); - Console.WriteLine(key); - } + Console.Write(" * "); + Console.WriteLine(key); } } - else + + if (notUsing.Count > 0) { - Console.WriteLine("> No errors found"); + Console.WriteLine(); + Console.WriteLine("Translations not used:"); + + foreach (var key in notUsing.OrderBy(x => x)) + { + Console.Write(" * "); + Console.WriteLine(key); + } } } + else + { + Console.WriteLine("> No errors found"); + } } + } + + public static void CleanOtherLocales(TranslationService service) + { + var mainTranslations = service.MainTranslations; - public static void CleanOtherLocales(TranslationService service) + foreach (var (locale, texts) in service.Translations.Where(x => x.Key != service.MainLocale)) { - var mainTranslations = service.MainTranslations; + Console.WriteLine(); + Console.WriteLine("----- CLEANING <{0}> -----", locale); - foreach (var (locale, texts) in service.Translations.Where(x => x.Key != service.MainLocale)) + var notUsed = texts.Keys.Except(mainTranslations.Keys).ToList(); + + if (notUsed.Count > 0) { - Console.WriteLine(); - Console.WriteLine("----- CLEANING <{0}> -----", locale); + foreach (var unused in notUsed) + { + texts.Remove(unused); + } - var notUsed = texts.Keys.Except(mainTranslations.Keys).ToList(); + Console.WriteLine("Cleaned {0} translations.", notUsed.Count); + } + else + { + Console.WriteLine("> No errors found"); + } + } + } - if (notUsed.Count > 0) - { - foreach (var unused in notUsed) - { - texts.Remove(unused); - } + public static void CheckUnused(TranslationService service, HashSet translations) + { + var notUsing = new SortedSet(); - Console.WriteLine("Cleaned {0} translations.", notUsed.Count); - } - else - { - Console.WriteLine("> No errors found"); - } + foreach (var key in service.MainTranslations.Keys) + { + if (!translations.Contains(key) && + !key.StartsWith("common.", StringComparison.OrdinalIgnoreCase) && + !key.StartsWith("dotnet_", StringComparison.OrdinalIgnoreCase) && + !key.StartsWith("validation.", StringComparison.OrdinalIgnoreCase) && + !key.StartsWith("rules.simulation.error", StringComparison.OrdinalIgnoreCase)) + { + notUsing.Add(key); } } - public static void CheckUnused(TranslationService service, HashSet translations) + if (notUsing.Count > 0) { - var notUsing = new SortedSet(); + Console.WriteLine("Translations not used:"); - foreach (var key in service.MainTranslations.Keys) + foreach (var key in notUsing) { - if (!translations.Contains(key) && - !key.StartsWith("common.", StringComparison.OrdinalIgnoreCase) && - !key.StartsWith("dotnet_", StringComparison.OrdinalIgnoreCase) && - !key.StartsWith("validation.", StringComparison.OrdinalIgnoreCase) && - !key.StartsWith("rules.simulation.error", StringComparison.OrdinalIgnoreCase)) - { - notUsing.Add(key); - } + Console.Write(" * "); + Console.WriteLine(key); } + } + } - if (notUsing.Count > 0) + public static void CheckForFile(TranslationService service, string relativeName, HashSet translations) + { + if (translations.Count > 0) + { + var prefixes = new HashSet(); + + foreach (var key in translations.ToList()) { - Console.WriteLine("Translations not used:"); + if (service.MainTranslations.ContainsKey(key)) + { + translations.Remove(key); + } - foreach (var key in notUsing) + var parts = key.Split("."); + + if (parts.Length > 1 && parts[0] != "common" && parts[0] != "validation") { - Console.Write(" * "); - Console.WriteLine(key); + prefixes.Add(parts[0]); } } - } - public static void CheckForFile(TranslationService service, string relativeName, HashSet translations) - { - if (translations.Count > 0) + if (HasInvalidPrefixes(prefixes) || translations.Count > 0) { - var prefixes = new HashSet(); + Console.WriteLine("Errors in file {0}.", relativeName); - foreach (var key in translations.ToList()) + if (HasInvalidPrefixes(prefixes)) { - if (service.MainTranslations.ContainsKey(key)) - { - translations.Remove(key); - } - - var parts = key.Split("."); - - if (parts.Length > 1 && parts[0] != "common" && parts[0] != "validation") - { - prefixes.Add(parts[0]); - } + Console.WriteLine(" > Multiple prefixes found: {0}", string.Join(",", prefixes)); } - if (HasInvalidPrefixes(prefixes) || translations.Count > 0) + if (translations.Count > 0) { - Console.WriteLine("Errors in file {0}.", relativeName); - - if (HasInvalidPrefixes(prefixes)) - { - Console.WriteLine(" > Multiple prefixes found: {0}", string.Join(",", prefixes)); - } - - if (translations.Count > 0) + foreach (var key in translations) { - foreach (var key in translations) - { - Console.Write(" * "); - Console.WriteLine(key); - } + Console.Write(" * "); + Console.WriteLine(key); } - - Console.WriteLine(); } + + Console.WriteLine(); } } + } - private static bool HasInvalidPrefixes(HashSet prefixes) - { - return prefixes.Count > 1; - } + private static bool HasInvalidPrefixes(HashSet prefixes) + { + return prefixes.Count > 1; } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/TranslateBackend.cs b/backend/i18n/translator/Squidex.Translator/Processes/TranslateBackend.cs index 027ad79f5d..45c26a0204 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/TranslateBackend.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/TranslateBackend.cs @@ -8,54 +8,53 @@ using System.Text.RegularExpressions; using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public class TranslateBackend { - public class TranslateBackend + private readonly TranslationService service; + private readonly DirectoryInfo folder; + + public TranslateBackend(DirectoryInfo folder, TranslationService service) { - private readonly TranslationService service; - private readonly DirectoryInfo folder; + this.folder = Backend.GetFolder(folder); + + this.service = service; + } - public TranslateBackend(DirectoryInfo folder, TranslationService service) + public void Run() + { + foreach (var (file, relativeName) in Backend.GetFilesCS(folder)) { - this.folder = Backend.GetFolder(folder); + var content = File.ReadAllText(file.FullName); - this.service = service; - } + var isReplaced = false; - public void Run() - { - foreach (var (file, relativeName) in Backend.GetFilesCS(folder)) + content = Regex.Replace(content, "\"[^\"]*\"", match => { - var content = File.ReadAllText(file.FullName); + var value = match.Value[1..^1]; - var isReplaced = false; + string result = null; - content = Regex.Replace(content, "\"[^\"]*\"", match => + if (value.IsPotentialMultiWordText()) { - var value = match.Value[1..^1]; - - string result = null; - - if (value.IsPotentialMultiWordText()) + service.Translate(relativeName, value, "Code", key => { - service.Translate(relativeName, value, "Code", key => - { - result = $"T.Get(\"{key}\")"; + result = $"T.Get(\"{key}\")"; - isReplaced = true; - }); - } + isReplaced = true; + }); + } - return result ?? $"\"{value}\""; - }); + return result ?? $"\"{value}\""; + }); - if (isReplaced) - { - Console.WriteLine("-----------------------------"); - Console.WriteLine("FILE {0} done", relativeName); + if (isReplaced) + { + Console.WriteLine("-----------------------------"); + Console.WriteLine("FILE {0} done", relativeName); - File.WriteAllText(file.FullName, content); - } + File.WriteAllText(file.FullName, content); } } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/TranslateTemplates.cs b/backend/i18n/translator/Squidex.Translator/Processes/TranslateTemplates.cs index 0d1c56c68f..e975443998 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/TranslateTemplates.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/TranslateTemplates.cs @@ -9,198 +9,197 @@ using HtmlAgilityPack; using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public class TranslateTemplates { - public class TranslateTemplates + private static readonly HashSet TagsToIgnore = new HashSet { - private static readonly HashSet TagsToIgnore = new HashSet - { - "code", - "script", - "sqx-code", - "style" - }; + "code", + "script", + "sqx-code", + "style" + }; - private static readonly HashSet AttributesToTranslate = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "title", // Tooltip - "placeholder", // Input placeholder - "confirmTitle", // Confirm Click - "confirmText", // Confirm Click - "message", // Title Component - }; - - private readonly TranslationService service; - private readonly DirectoryInfo folder; - private bool isReplaced; - private bool isSilent; - private int total; - - public TranslateTemplates(DirectoryInfo folder, TranslationService service) - { - this.folder = Frontend.GetFolder(folder); + private static readonly HashSet AttributesToTranslate = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "title", // Tooltip + "placeholder", // Input placeholder + "confirmTitle", // Confirm Click + "confirmText", // Confirm Click + "message", // Title Component + }; + + private readonly TranslationService service; + private readonly DirectoryInfo folder; + private bool isReplaced; + private bool isSilent; + private int total; + + public TranslateTemplates(DirectoryInfo folder, TranslationService service) + { + this.folder = Frontend.GetFolder(folder); - this.service = service; - } + this.service = service; + } + + public void Run(bool reportMissing) + { + isSilent = reportMissing; - public void Run(bool reportMissing) + foreach (var (file, relativeName) in Frontend.GetTemplateFiles(folder)) { - isSilent = reportMissing; + isReplaced = false; - foreach (var (file, relativeName) in Frontend.GetTemplateFiles(folder)) + // Keep the original casing, otherwise *ngIf is translated to ngif + var html = new HtmlDocument { - isReplaced = false; - - // Keep the original casing, otherwise *ngIf is translated to ngif - var html = new HtmlDocument - { - OptionOutputOriginalCase = true - }; + OptionOutputOriginalCase = true + }; - html.LoadHtml(File.ReadAllText(file.FullName)); + html.LoadHtml(File.ReadAllText(file.FullName)); - Traverse(relativeName, html.DocumentNode); + Traverse(relativeName, html.DocumentNode); - if (isReplaced && !reportMissing) - { - Console.WriteLine("-----------------------------"); - Console.WriteLine("FILE {0} done", relativeName); + if (isReplaced && !reportMissing) + { + Console.WriteLine("-----------------------------"); + Console.WriteLine("FILE {0} done", relativeName); - SaveHtml(file, html); - } + SaveHtml(file, html); } + } - if (reportMissing) - { - Console.WriteLine("TODO: {0}", total); - } + if (reportMissing) + { + Console.WriteLine("TODO: {0}", total); } + } - private void Traverse(string fileName, HtmlNode node) + private void Traverse(string fileName, HtmlNode node) + { + if (TagsToIgnore.Contains(node.Name)) { - if (TagsToIgnore.Contains(node.Name)) - { - return; - } + return; + } - if (node is HtmlTextNode textNode) - { - var text = textNode.Text; + if (node is HtmlTextNode textNode) + { + var text = textNode.Text; - // For strings like Next:
- var trimmed = text.Trim().Trim(':', '(', ')'); + // For strings like Next:
+ var trimmed = text.Trim().Trim(':', '(', ')'); - const string whitespace = " "; + const string whitespace = " "; - while (trimmed.StartsWith(whitespace, StringComparison.OrdinalIgnoreCase)) - { - trimmed = trimmed[whitespace.Length..]; - } + while (trimmed.StartsWith(whitespace, StringComparison.OrdinalIgnoreCase)) + { + trimmed = trimmed[whitespace.Length..]; + } - while (trimmed.EndsWith(whitespace, StringComparison.OrdinalIgnoreCase)) - { - trimmed = trimmed[..^whitespace.Length]; - } + while (trimmed.EndsWith(whitespace, StringComparison.OrdinalIgnoreCase)) + { + trimmed = trimmed[..^whitespace.Length]; + } - if (!string.IsNullOrWhiteSpace(trimmed) && !IsTranslated(trimmed) && !IsVariable(trimmed)) + if (!string.IsNullOrWhiteSpace(trimmed) && !IsTranslated(trimmed) && !IsVariable(trimmed)) + { + if (!string.IsNullOrWhiteSpace(trimmed)) { - if (!string.IsNullOrWhiteSpace(trimmed)) - { - // Extract prefix and suffix to keep our original indentation. - var originalIndex = text.IndexOf(trimmed, StringComparison.Ordinal); + // Extract prefix and suffix to keep our original indentation. + var originalIndex = text.IndexOf(trimmed, StringComparison.Ordinal); - var originalPrefix = text[..originalIndex]; - var originalSuffix = text[(originalIndex + trimmed.Length)..]; + var originalPrefix = text[..originalIndex]; + var originalSuffix = text[(originalIndex + trimmed.Length)..]; - var originText = $"text in {textNode.ParentNode.Name}"; + var originText = $"text in {textNode.ParentNode.Name}"; - service.Translate(fileName, trimmed, originText, key => + service.Translate(fileName, trimmed, originText, key => + { + if (isSilent) { - if (isSilent) - { - total++; - } - else - { - // Keep our original indentation. - textNode.Text = originalPrefix + $"{{{{ '{key}' | sqxTranslate }}}}" + originalSuffix; + total++; + } + else + { + // Keep our original indentation. + textNode.Text = originalPrefix + $"{{{{ '{key}' | sqxTranslate }}}}" + originalSuffix; - isReplaced = true; - } - }, isSilent); - } + isReplaced = true; + } + }, isSilent); } } - else + } + else + { + foreach (var attribute in node.Attributes.ToList()) { - foreach (var attribute in node.Attributes.ToList()) + if (AttributesToTranslate.Contains(attribute.Name) && !string.IsNullOrWhiteSpace(attribute.Value) && !IsPipe(attribute) && !IsTranslatedAttribute(attribute.Value)) { - if (AttributesToTranslate.Contains(attribute.Name) && !string.IsNullOrWhiteSpace(attribute.Value) && !IsPipe(attribute) && !IsTranslatedAttribute(attribute.Value)) - { - var originText = $"{attribute.Name} attribute"; + var originText = $"{attribute.Name} attribute"; - service.Translate(fileName, attribute.Value, originText, key => + service.Translate(fileName, attribute.Value, originText, key => + { + if (isSilent) + { + total++; + } + else { - if (isSilent) + if (attribute.Name.Contains('[', StringComparison.Ordinal)) { - total++; + node.SetAttributeValue(attribute.Name, $"{{{{ '{key}' | sqxTranslate }}}}"); } else { - if (attribute.Name.Contains('[', StringComparison.Ordinal)) - { - node.SetAttributeValue(attribute.Name, $"{{{{ '{key}' | sqxTranslate }}}}"); - } - else - { - node.SetAttributeValue(attribute.Name, $"i18n:{key}"); - } - - isReplaced = true; + node.SetAttributeValue(attribute.Name, $"i18n:{key}"); } - }, isSilent); - } - } - } - foreach (var child in node.ChildNodes) - { - Traverse(fileName, child); + isReplaced = true; + } + }, isSilent); + } } } - private static bool IsPipe(HtmlAttribute attribute) + foreach (var child in node.ChildNodes) { - return attribute.Value.Contains('{', StringComparison.Ordinal); + Traverse(fileName, child); } + } - private static bool IsTranslatedAttribute(string text) - { - return text.Contains("i18n:", StringComparison.Ordinal); - } + private static bool IsPipe(HtmlAttribute attribute) + { + return attribute.Value.Contains('{', StringComparison.Ordinal); + } - private static bool IsTranslated(string text) - { - return text.Contains("| sqxTranslate", StringComparison.Ordinal); - } + private static bool IsTranslatedAttribute(string text) + { + return text.Contains("i18n:", StringComparison.Ordinal); + } - private static bool IsVariable(string text) - { - return text.StartsWith("{{", StringComparison.Ordinal) && Regex.Matches(text, "\\}\\}").Count == 1; - } + private static bool IsTranslated(string text) + { + return text.Contains("| sqxTranslate", StringComparison.Ordinal); + } - private static void SaveHtml(FileInfo file, HtmlDocument html) - { - html.Save(file.FullName); + private static bool IsVariable(string text) + { + return text.StartsWith("{{", StringComparison.Ordinal) && Regex.Matches(text, "\\}\\}").Count == 1; + } + + private static void SaveHtml(FileInfo file, HtmlDocument html) + { + html.Save(file.FullName); - var text = File.ReadAllText(file.FullName); + var text = File.ReadAllText(file.FullName); - // Fix the attributes, because html agility packs converts attributes without value to attributes with empty string. - // For example - // becomes - text = Regex.Replace(text, " (?[^\\s]*)=\"\"", x => " " + x.Groups["Name"].Value); + // Fix the attributes, because html agility packs converts attributes without value to attributes with empty string. + // For example + // becomes + text = Regex.Replace(text, " (?[^\\s]*)=\"\"", x => " " + x.Groups["Name"].Value); - File.WriteAllText(file.FullName, text); - } + File.WriteAllText(file.FullName, text); } } diff --git a/backend/i18n/translator/Squidex.Translator/Processes/TranslateTypescript.cs b/backend/i18n/translator/Squidex.Translator/Processes/TranslateTypescript.cs index fddf0dae26..4948664ce5 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/TranslateTypescript.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/TranslateTypescript.cs @@ -8,54 +8,53 @@ using System.Text.RegularExpressions; using Squidex.Translator.State; -namespace Squidex.Translator.Processes +namespace Squidex.Translator.Processes; + +public class TranslateTypescript { - public class TranslateTypescript + private readonly TranslationService service; + private readonly DirectoryInfo folder; + + public TranslateTypescript(DirectoryInfo folder, TranslationService service) { - private readonly TranslationService service; - private readonly DirectoryInfo folder; + this.folder = Frontend.GetFolder(folder); + + this.service = service; + } - public TranslateTypescript(DirectoryInfo folder, TranslationService service) + public void Run() + { + foreach (var (file, relativeName) in Frontend.GetTypescriptFiles(folder)) { - this.folder = Frontend.GetFolder(folder); + var content = File.ReadAllText(file.FullName); - this.service = service; - } + var isReplaced = false; - public void Run() - { - foreach (var (file, relativeName) in Frontend.GetTypescriptFiles(folder)) + content = Regex.Replace(content, "'[^']*'", match => { - var content = File.ReadAllText(file.FullName); + var value = match.Value[1..^1]; - var isReplaced = false; + string result = null; - content = Regex.Replace(content, "'[^']*'", match => + if (value.IsPotentialMultiWordText()) { - var value = match.Value[1..^1]; - - string result = null; - - if (value.IsPotentialMultiWordText()) + service.Translate(relativeName, value, "Code", key => { - service.Translate(relativeName, value, "Code", key => - { - result = $"\'i18n:{key}\'"; + result = $"\'i18n:{key}\'"; - isReplaced = true; - }); - } + isReplaced = true; + }); + } - return result ?? $"'{value}'"; - }); + return result ?? $"'{value}'"; + }); - if (isReplaced) - { - Console.WriteLine("-----------------------------"); - Console.WriteLine("FILE {0} done", relativeName); + if (isReplaced) + { + Console.WriteLine("-----------------------------"); + Console.WriteLine("FILE {0} done", relativeName); - File.WriteAllText(file.FullName, content); - } + File.WriteAllText(file.FullName, content); } } } diff --git a/backend/i18n/translator/Squidex.Translator/Program.cs b/backend/i18n/translator/Squidex.Translator/Program.cs index dc6cf2a649..99e048aac0 100644 --- a/backend/i18n/translator/Squidex.Translator/Program.cs +++ b/backend/i18n/translator/Squidex.Translator/Program.cs @@ -8,25 +8,24 @@ using CommandDotNet; using CommandDotNet.FluentValidation; -namespace Squidex.Translator +namespace Squidex.Translator; + +public static class Program { - public static class Program + public static int Main(string[] args) { - public static int Main(string[] args) + try { - try - { - var appRunner = - new AppRunner() - .UseFluentValidation(true); + var appRunner = + new AppRunner() + .UseFluentValidation(true); - return appRunner.Run(args); - } - catch (Exception ex) - { - Console.WriteLine("ERROR: {0}", ex); - return -1; - } + return appRunner.Run(args); + } + catch (Exception ex) + { + Console.WriteLine("ERROR: {0}", ex); + return -1; } } } diff --git a/backend/i18n/translator/Squidex.Translator/State/Old/OldTranslatedText.cs b/backend/i18n/translator/Squidex.Translator/State/Old/OldTranslatedText.cs index 0031731878..b93f6ae6dc 100644 --- a/backend/i18n/translator/Squidex.Translator/State/Old/OldTranslatedText.cs +++ b/backend/i18n/translator/Squidex.Translator/State/Old/OldTranslatedText.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator.State.Old +namespace Squidex.Translator.State.Old; + +public class OldTranslatedText { - public class OldTranslatedText - { - public SortedDictionary Texts { get; set; } = new SortedDictionary(); + public SortedDictionary Texts { get; set; } = new SortedDictionary(); - public SortedSet Origins { get; set; } = new SortedSet(); - } + public SortedSet Origins { get; set; } = new SortedSet(); } diff --git a/backend/i18n/translator/Squidex.Translator/State/Old/OldTranslationState.cs b/backend/i18n/translator/Squidex.Translator/State/Old/OldTranslationState.cs index af9b9b4a84..c78e6ec2d7 100644 --- a/backend/i18n/translator/Squidex.Translator/State/Old/OldTranslationState.cs +++ b/backend/i18n/translator/Squidex.Translator/State/Old/OldTranslationState.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator.State.Old +namespace Squidex.Translator.State.Old; + +public class OldTranslationState { - public class OldTranslationState - { - public SortedDictionary Texts { get; set; } = new SortedDictionary(); + public SortedDictionary Texts { get; set; } = new SortedDictionary(); - public HashSet Ignores { get; set; } = new HashSet(); + public HashSet Ignores { get; set; } = new HashSet(); - public SortedDictionary> Todos { get; set; } = new SortedDictionary>(); - } + public SortedDictionary> Todos { get; set; } = new SortedDictionary>(); } diff --git a/backend/i18n/translator/Squidex.Translator/State/Old/TextOrigin.cs b/backend/i18n/translator/Squidex.Translator/State/Old/TextOrigin.cs index 64009827d2..173adf69e9 100644 --- a/backend/i18n/translator/Squidex.Translator/State/Old/TextOrigin.cs +++ b/backend/i18n/translator/Squidex.Translator/State/Old/TextOrigin.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator.State +namespace Squidex.Translator.State; + +public enum TextOrigin { - public enum TextOrigin - { - Frontend, - BackendInfrastructure, - BackendShared - } + Frontend, + BackendInfrastructure, + BackendShared } diff --git a/backend/i18n/translator/Squidex.Translator/State/TranslatedTexts.cs b/backend/i18n/translator/Squidex.Translator/State/TranslatedTexts.cs index dfb02e7f55..3f5597db53 100644 --- a/backend/i18n/translator/Squidex.Translator/State/TranslatedTexts.cs +++ b/backend/i18n/translator/Squidex.Translator/State/TranslatedTexts.cs @@ -5,17 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator.State +namespace Squidex.Translator.State; + +public sealed class TranslatedTexts : SortedDictionary { - public sealed class TranslatedTexts : SortedDictionary + public TranslatedTexts() { - public TranslatedTexts() - { - } + } - public TranslatedTexts(TranslatedTexts source) - : base(source) - { - } + public TranslatedTexts(TranslatedTexts source) + : base(source) + { } } diff --git a/backend/i18n/translator/Squidex.Translator/State/TranslationService.cs b/backend/i18n/translator/Squidex.Translator/State/TranslationService.cs index dd5d9c17da..ff19d861b5 100644 --- a/backend/i18n/translator/Squidex.Translator/State/TranslationService.cs +++ b/backend/i18n/translator/Squidex.Translator/State/TranslationService.cs @@ -11,319 +11,318 @@ using System.Text.Json.Serialization; using Squidex.Translator.State.Old; -namespace Squidex.Translator.State +namespace Squidex.Translator.State; + +public sealed class TranslationService { - public sealed class TranslationService + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions { - private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - AllowTrailingCommas = true, - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; - - private readonly Dictionary translations = new Dictionary(); - private readonly TranslationTodos translationsTodo; - private readonly TranslationsToIgnore translationToIgnore; - private readonly DirectoryInfo sourceDirectory; - private readonly string sourceFileName; - private readonly string[] supportedLocales; - private readonly bool onlySingleWords; - private string previousPrefix; - - public TranslatedTexts MainTranslations - { - get { return translations[MainLocale]; } - } - - public string MainLocale - { - get { return supportedLocales[0]; } - } + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + private readonly Dictionary translations = new Dictionary(); + private readonly TranslationTodos translationsTodo; + private readonly TranslationsToIgnore translationToIgnore; + private readonly DirectoryInfo sourceDirectory; + private readonly string sourceFileName; + private readonly string[] supportedLocales; + private readonly bool onlySingleWords; + private string previousPrefix; + + public TranslatedTexts MainTranslations + { + get { return translations[MainLocale]; } + } - public IEnumerable SupportedLocales - { - get { return supportedLocales; } - } + public string MainLocale + { + get { return supportedLocales[0]; } + } - public IEnumerable NonMainSupportedLocales - { - get { return supportedLocales.Skip(1); } - } + public IEnumerable SupportedLocales + { + get { return supportedLocales; } + } - public IReadOnlyDictionary Translations - { - get { return translations; } - } + public IEnumerable NonMainSupportedLocales + { + get { return supportedLocales.Skip(1); } + } - static TranslationService() - { - SerializerOptions.Converters.Add(new JsonStringEnumConverter()); - } + public IReadOnlyDictionary Translations + { + get { return translations; } + } - public TranslationService(DirectoryInfo sourceDirectory, string sourceFileName, string[] supportedLocales, bool onlySingleWords) - { - this.onlySingleWords = onlySingleWords; + static TranslationService() + { + SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + } - this.sourceDirectory = sourceDirectory; - this.sourceFileName = sourceFileName; + public TranslationService(DirectoryInfo sourceDirectory, string sourceFileName, string[] supportedLocales, bool onlySingleWords) + { + this.onlySingleWords = onlySingleWords; - this.supportedLocales = supportedLocales; + this.sourceDirectory = sourceDirectory; + this.sourceFileName = sourceFileName; - foreach (var locale in supportedLocales) - { - translations[locale] = Load($"_{locale}.json"); - } + this.supportedLocales = supportedLocales; - translationsTodo = Load("__todos.json"); - translationToIgnore = Load("__ignore.json"); + foreach (var locale in supportedLocales) + { + translations[locale] = Load($"_{locale}.json"); } - public TranslatedTexts GetTextsWithFallback(string locale) - { - var result = new TranslatedTexts(MainTranslations); + translationsTodo = Load("__todos.json"); + translationToIgnore = Load("__ignore.json"); + } + + public TranslatedTexts GetTextsWithFallback(string locale) + { + var result = new TranslatedTexts(MainTranslations); - if (translations.TryGetValue(locale, out var translated)) + if (translations.TryGetValue(locale, out var translated)) + { + foreach (var key in result.Keys.ToList()) { - foreach (var key in result.Keys.ToList()) + if (translated.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) { - if (translated.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) - { - result[key] = value; - } + result[key] = value; } } - - return result; } - private T Load(string name) where T : new() - { - var fullName = GetFullName(name); - - if (File.Exists(fullName)) - { - var json = File.ReadAllText(fullName); + return result; + } - return JsonSerializer.Deserialize(json, SerializerOptions); - } - else - { - return new T(); - } - } + private T Load(string name) where T : new() + { + var fullName = GetFullName(name); - private void Save(string name, T value) where T : new() + if (File.Exists(fullName)) { - var fullName = GetFullName(name); + var json = File.ReadAllText(fullName); - WriteTo(value, fullName); + return JsonSerializer.Deserialize(json, SerializerOptions); } - - private string GetFullName(string name) + else { - return Path.Combine(sourceDirectory.FullName, "source", $"{sourceFileName}{name}"); + return new T(); } + } - public void WriteTo(T value, string path) where T : new() - { - var json = JsonSerializer.Serialize(value, SerializerOptions); + private void Save(string name, T value) where T : new() + { + var fullName = GetFullName(name); - if (!sourceDirectory.Exists) - { - Directory.CreateDirectory(sourceDirectory.FullName); - } + WriteTo(value, fullName); + } - File.WriteAllText(path, json); - } + private string GetFullName(string name) + { + return Path.Combine(sourceDirectory.FullName, "source", $"{sourceFileName}{name}"); + } + + public void WriteTo(T value, string path) where T : new() + { + var json = JsonSerializer.Serialize(value, SerializerOptions); - public void Migrate() + if (!sourceDirectory.Exists) { - var oldState = Load(".json"); + Directory.CreateDirectory(sourceDirectory.FullName); + } - foreach (var (key, value) in oldState.Texts) - { - if (value.Texts.TryGetValue("en", out var text)) - { - MainTranslations[key] = text; - } - } + File.WriteAllText(path, json); + } + + public void Migrate() + { + var oldState = Load(".json"); - foreach (var (key, value) in oldState.Todos) + foreach (var (key, value) in oldState.Texts) + { + if (value.Texts.TryGetValue("en", out var text)) { - translationsTodo[key] = value; + MainTranslations[key] = text; } + } - Save(); + foreach (var (key, value) in oldState.Todos) + { + translationsTodo[key] = value; } - public void Save() + Save(); + } + + public void Save() + { + foreach (var (locale, texts) in translations) { - foreach (var (locale, texts) in translations) - { - Save($"_{locale}.json", texts); - } + Save($"_{locale}.json", texts); + } - Save("__todos.json", translationsTodo); - Save("__ignore.json", translationToIgnore); + Save("__todos.json", translationsTodo); + Save("__ignore.json", translationToIgnore); + } + + public void Translate(string fileName, string text, string originText, Action handler, bool silent = false) + { + if (onlySingleWords && text.Contains(' ', StringComparison.Ordinal)) + { + return; } - public void Translate(string fileName, string text, string originText, Action handler, bool silent = false) + if (!IsIgnored(fileName, text)) { - if (onlySingleWords && text.Contains(' ', StringComparison.Ordinal)) - { - return; - } + var (key, keyState) = MainTranslations.FirstOrDefault(x => x.Value == text); - if (!IsIgnored(fileName, text)) + if (string.IsNullOrWhiteSpace(key)) { - var (key, keyState) = MainTranslations.FirstOrDefault(x => x.Value == text); - - if (string.IsNullOrWhiteSpace(key)) + if (silent) { - if (silent) - { - handler("DUMMY"); - } - else + handler("DUMMY"); + } + else + { + Console.WriteLine(); + Console.WriteLine(">>> {0}", text); + Console.WriteLine("{1} in {0}", fileName, originText); + Console.WriteLine(); + + while (true) { - Console.WriteLine(); - Console.WriteLine(">>> {0}", text); - Console.WriteLine("{1} in {0}", fileName, originText); - Console.WriteLine(); + Console.WriteLine("Enter key or (USE with previous prefix), (TODO), (SKIP), (IGNORE), (IGNORE FILE)"); - while (true) - { - Console.WriteLine("Enter key or (USE with previous prefix), (TODO), (SKIP), (IGNORE), (IGNORE FILE)"); + key = Console.ReadLine(); - key = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(key)) + { + key = $"common.{text.ToLower(CultureInfo.InvariantCulture)}"; - if (string.IsNullOrWhiteSpace(key)) + if (translations.TryGetValue(key, out var existing)) { - key = $"common.{text.ToLower(CultureInfo.InvariantCulture)}"; + Console.WriteLine("Key is already in use with {0}", existing); + continue; + } + else + { + AddText(key, text); + + handler(key); + } + + break; + } + + if (key.Equals("s", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Skipped"); + } + else if (key.Equals("i", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Ignored"); + AddIgnore(fileName, text); + } + else if (key.Equals("f", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Ignored File"); + AddIgnore(fileName, "*"); + } + else if (key.Equals("t", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("ToDo"); + AddTodo(fileName, text); - if (translations.TryGetValue(key, out var existing)) + AddIgnore(fileName, text); + } + else + { + if (!key.Contains('.', StringComparison.Ordinal)) + { + if (previousPrefix != null) { - Console.WriteLine("Key is already in use with {0}", existing); - continue; + key = $"{previousPrefix}.{key}"; } else { - AddText(key, text); - - handler(key); + key = $"common.{key}"; } - - break; } - - if (key.Equals("s", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine("Skipped"); - } - else if (key.Equals("i", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine("Ignored"); - AddIgnore(fileName, text); - } - else if (key.Equals("f", StringComparison.OrdinalIgnoreCase)) + else { - Console.WriteLine("Ignored File"); - AddIgnore(fileName, "*"); + previousPrefix = string.Join('.', key.Split('.', StringSplitOptions.RemoveEmptyEntries).Skip(1)); } - else if (key.Equals("t", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine("ToDo"); - AddTodo(fileName, text); - AddIgnore(fileName, text); + var useOlder = key.EndsWith("?", StringComparison.OrdinalIgnoreCase); + var useNewer = key.EndsWith("!", StringComparison.OrdinalIgnoreCase); + + if (translations.TryGetValue(key, out var existing) && !useOlder && !useNewer) + { + Console.WriteLine("Key is already in use with '{0}'", existing); + continue; } else { - if (!key.Contains('.', StringComparison.Ordinal)) - { - if (previousPrefix != null) - { - key = $"{previousPrefix}.{key}"; - } - else - { - key = $"common.{key}"; - } - } - else - { - previousPrefix = string.Join('.', key.Split('.', StringSplitOptions.RemoveEmptyEntries).Skip(1)); - } - - var useOlder = key.EndsWith("?", StringComparison.OrdinalIgnoreCase); - var useNewer = key.EndsWith("!", StringComparison.OrdinalIgnoreCase); + key = key.TrimEnd('!', '?'); - if (translations.TryGetValue(key, out var existing) && !useOlder && !useNewer) + if (!useOlder) { - Console.WriteLine("Key is already in use with '{0}'", existing); - continue; + AddText(key, text); } - else - { - key = key.TrimEnd('!', '?'); - - if (!useOlder) - { - AddText(key, text); - } - handler(key); - } + handler(key); } - - break; } + + break; } } - else - { - handler(key); - } - - Save(); } - } + else + { + handler(key); + } - private bool IsIgnored(string name, string text) - { - return translationToIgnore.TryGetValue(name, out var ignores) && (ignores.Contains(text) || ignores.Contains("*")); + Save(); } + } - private void AddText(string key, string text) - { - MainTranslations[key] = text; - } + private bool IsIgnored(string name, string text) + { + return translationToIgnore.TryGetValue(name, out var ignores) && (ignores.Contains(text) || ignores.Contains("*")); + } - private void AddIgnore(string name, string text) - { - if (!translationToIgnore.TryGetValue(name, out var ignores)) - { - ignores = new SortedSet(); + private void AddText(string key, string text) + { + MainTranslations[key] = text; + } - translationToIgnore[name] = ignores; - } + private void AddIgnore(string name, string text) + { + if (!translationToIgnore.TryGetValue(name, out var ignores)) + { + ignores = new SortedSet(); - ignores.Add(text); + translationToIgnore[name] = ignores; } - private void AddTodo(string name, string text) - { - if (!translationsTodo.TryGetValue(name, out var todos)) - { - todos = new SortedSet(); + ignores.Add(text); + } - translationsTodo[name] = todos; - } + private void AddTodo(string name, string text) + { + if (!translationsTodo.TryGetValue(name, out var todos)) + { + todos = new SortedSet(); - todos.Add(text); + translationsTodo[name] = todos; } + + todos.Add(text); } } diff --git a/backend/i18n/translator/Squidex.Translator/State/TranslationTodos.cs b/backend/i18n/translator/Squidex.Translator/State/TranslationTodos.cs index 87c1272810..7f9754b7f3 100644 --- a/backend/i18n/translator/Squidex.Translator/State/TranslationTodos.cs +++ b/backend/i18n/translator/Squidex.Translator/State/TranslationTodos.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator.State +namespace Squidex.Translator.State; + +public sealed class TranslationTodos : SortedDictionary> { - public sealed class TranslationTodos : SortedDictionary> - { - } } diff --git a/backend/i18n/translator/Squidex.Translator/State/TranslationsToIgnore.cs b/backend/i18n/translator/Squidex.Translator/State/TranslationsToIgnore.cs index 2802911690..5e598a07df 100644 --- a/backend/i18n/translator/Squidex.Translator/State/TranslationsToIgnore.cs +++ b/backend/i18n/translator/Squidex.Translator/State/TranslationsToIgnore.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Translator.State +namespace Squidex.Translator.State; + +public sealed class TranslationsToIgnore : SortedDictionary> { - public sealed class TranslationsToIgnore : SortedDictionary> - { - } } diff --git a/backend/src/Migrations/MigrationPath.cs b/backend/src/Migrations/MigrationPath.cs index 705d4ebfb4..c8f3b8c595 100644 --- a/backend/src/Migrations/MigrationPath.cs +++ b/backend/src/Migrations/MigrationPath.cs @@ -11,114 +11,113 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Migrations; -namespace Migrations +namespace Migrations; + +public sealed class MigrationPath : IMigrationPath { - public sealed class MigrationPath : IMigrationPath + private const int CurrentVersion = 25; + private readonly IServiceProvider serviceProvider; + + public MigrationPath(IServiceProvider serviceProvider) { - private const int CurrentVersion = 25; - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public MigrationPath(IServiceProvider serviceProvider) + public (int Version, IEnumerable? Migrations) GetNext(int version) + { + if (version == CurrentVersion) { - this.serviceProvider = serviceProvider; + return (CurrentVersion, null); } - public (int Version, IEnumerable? Migrations) GetNext(int version) - { - if (version == CurrentVersion) - { - return (CurrentVersion, null); - } + var migrations = ResolveMigrators(version).NotNull().ToList(); - var migrations = ResolveMigrators(version).NotNull().ToList(); + return (CurrentVersion, migrations); + } - return (CurrentVersion, migrations); + private IEnumerable ResolveMigrators(int version) + { + // Version 06: Convert Event store. Must always be executed first. + if (version < 6) + { + yield return serviceProvider.GetRequiredService(); } - private IEnumerable ResolveMigrators(int version) + // Version 22: Integrate Domain Id. + if (version < 22) { - // Version 06: Convert Event store. Must always be executed first. - if (version < 6) - { - yield return serviceProvider.GetRequiredService(); - } + yield return serviceProvider.GetRequiredService(); + } - // Version 22: Integrate Domain Id. - if (version < 22) + // Version 07: Introduces AppId for backups. + else if (version < 7) + { + yield return serviceProvider.GetRequiredService(); + } + + // Version 05: Fixes the broken command architecture and requires a rebuild of all snapshots. + if (version < 5) + { + yield return serviceProvider.GetRequiredService(); + } + else + { + // Version 09: Grain indexes. + if (version < 9) { - yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetService(); } - // Version 07: Introduces AppId for backups. - else if (version < 7) + // Version 12: Introduce roles. + // Version 24: Improve a naming in the languages config. + // Version 26: Introduce full deletion. + if (version < 26) { - yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetRequiredService(); } - // Version 05: Fixes the broken command architecture and requires a rebuild of all snapshots. - if (version < 5) + // Version 18: Rebuild assets. + if (version < 18) { - yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetService(); + yield return serviceProvider.GetService(); } else { - // Version 09: Grain indexes. - if (version < 9) - { - yield return serviceProvider.GetService(); - } - - // Version 12: Introduce roles. - // Version 24: Improve a naming in the languages config. - // Version 26: Introduce full deletion. - if (version < 26) + // Version 20: Rename slug field. + if (version < 20) { - yield return serviceProvider.GetRequiredService(); - yield return serviceProvider.GetRequiredService(); - yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetService(); } - // Version 18: Rebuild assets. - if (version < 18) + // Version 22: Introduce domain id. + // Version 23: Fix parent id. + if (version < 23) { - yield return serviceProvider.GetService(); - yield return serviceProvider.GetService(); - } - else - { - // Version 20: Rename slug field. - if (version < 20) - { - yield return serviceProvider.GetService(); - } - - // Version 22: Introduce domain id. - // Version 23: Fix parent id. - if (version < 23) - { - yield return serviceProvider.GetRequiredService().ForAssets(); - } - } - - // Version 21: Introduce content drafts V2. - // Version 25: Convert content ids to names. - if (version < 25) - { - yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetRequiredService().ForAssets(); } + } - // Version 16: Introduce file name slugs for assets. - if (version < 16) - { - yield return serviceProvider.GetRequiredService(); - } + // Version 21: Introduce content drafts V2. + // Version 25: Convert content ids to names. + if (version < 25) + { + yield return serviceProvider.GetRequiredService(); } - // Version 13: Json refactoring - if (version < 13) + // Version 16: Introduce file name slugs for assets. + if (version < 16) { - yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetRequiredService(); } } + + // Version 13: Json refactoring + if (version < 13) + { + yield return serviceProvider.GetRequiredService(); + } } } diff --git a/backend/src/Migrations/Migrations/ClearRules.cs b/backend/src/Migrations/Migrations/ClearRules.cs index a3de3f5160..444d8e6515 100644 --- a/backend/src/Migrations/Migrations/ClearRules.cs +++ b/backend/src/Migrations/Migrations/ClearRules.cs @@ -9,21 +9,20 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.States; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class ClearRules : IMigration { - public sealed class ClearRules : IMigration - { - private readonly IStore store; + private readonly IStore store; - public ClearRules(IStore store) - { - this.store = store; - } + public ClearRules(IStore store) + { + this.store = store; + } - public Task UpdateAsync( - CancellationToken ct) - { - return store.ClearSnapshotsAsync(); - } + public Task UpdateAsync( + CancellationToken ct) + { + return store.ClearSnapshotsAsync(); } } diff --git a/backend/src/Migrations/Migrations/ClearSchemas.cs b/backend/src/Migrations/Migrations/ClearSchemas.cs index f5b2a4c276..553db32155 100644 --- a/backend/src/Migrations/Migrations/ClearSchemas.cs +++ b/backend/src/Migrations/Migrations/ClearSchemas.cs @@ -9,21 +9,20 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.States; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class ClearSchemas : IMigration { - public sealed class ClearSchemas : IMigration - { - private readonly IStore store; + private readonly IStore store; - public ClearSchemas(IStore store) - { - this.store = store; - } + public ClearSchemas(IStore store) + { + this.store = store; + } - public Task UpdateAsync( - CancellationToken ct) - { - return store.ClearSnapshotsAsync(); - } + public Task UpdateAsync( + CancellationToken ct) + { + return store.ClearSnapshotsAsync(); } } diff --git a/backend/src/Migrations/Migrations/ConvertEventStore.cs b/backend/src/Migrations/Migrations/ConvertEventStore.cs index 53a45bb83e..c595be1e75 100644 --- a/backend/src/Migrations/Migrations/ConvertEventStore.cs +++ b/backend/src/Migrations/Migrations/ConvertEventStore.cs @@ -11,56 +11,55 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class ConvertEventStore : MongoBase, IMigration { - public sealed class ConvertEventStore : MongoBase, IMigration - { - private readonly IEventStore eventStore; + private readonly IEventStore eventStore; - public ConvertEventStore(IEventStore eventStore) - { - this.eventStore = eventStore; - } + public ConvertEventStore(IEventStore 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) - { - // Do not resolve in constructor, because most of the time it is not executed anyway. - var collection = mongoEventStore.RawCollection; + // Do not resolve in constructor, because most of the time it is not executed anyway. + var collection = mongoEventStore.RawCollection; - var writes = new List>(); + var writes = new List>(); - async Task WriteAsync(WriteModel? model, bool force) + async Task WriteAsync(WriteModel? model, bool force) + { + if (model != null) { - if (model != null) - { - writes.Add(model); - } + writes.Add(model); + } - if (writes.Count == 1000 || (force && writes.Count > 0)) - { - await collection.BulkWriteAsync(writes, BulkUnordered, ct); - writes.Clear(); - } + if (writes.Count == 1000 || (force && writes.Count > 0)) + { + await collection.BulkWriteAsync(writes, BulkUnordered, ct); + writes.Clear(); } + } - await collection.Find(FindAll).ForEachAsync(async commit => + await collection.Find(FindAll).ForEachAsync(async commit => + { + foreach (BsonDocument @event in commit["Events"].AsBsonArray) { - foreach (BsonDocument @event in commit["Events"].AsBsonArray) - { - var meta = BsonDocument.Parse(@event["Metadata"].AsString); + var meta = BsonDocument.Parse(@event["Metadata"].AsString); - @event.Remove("EventId"); - @event["Metadata"] = meta; - } + @event.Remove("EventId"); + @event["Metadata"] = meta; + } - await WriteAsync(new ReplaceOneModel(Filter.Eq("_id", commit["_id"].AsString), commit), false); - }, ct); + await WriteAsync(new ReplaceOneModel(Filter.Eq("_id", commit["_id"].AsString), commit), false); + }, ct); - await WriteAsync(null, true); - } + await WriteAsync(null, true); } } } diff --git a/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs b/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs index 46a6cf7ff8..7c5f887651 100644 --- a/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs +++ b/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs @@ -12,80 +12,79 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class ConvertEventStoreAppId : MongoBase, IMigration { - public sealed class ConvertEventStoreAppId : MongoBase, IMigration - { - private readonly IEventStore eventStore; + private readonly IEventStore eventStore; - public ConvertEventStoreAppId(IEventStore eventStore) - { - this.eventStore = eventStore; - } + public ConvertEventStoreAppId(IEventStore 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) - { - // Do not resolve in constructor, because most of the time it is not executed anyway. - var collection = mongoEventStore.RawCollection; + // Do not resolve in constructor, because most of the time it is not executed anyway. + var collection = mongoEventStore.RawCollection; - var writes = new List>(); + var writes = new List>(); - async Task WriteAsync(WriteModel? model, bool force) + async Task WriteAsync(WriteModel? model, bool force) + { + if (model != null) { - if (model != null) - { - writes.Add(model); - } - - if (writes.Count == 1000 || (force && writes.Count > 0)) - { - await collection.BulkWriteAsync(writes, BulkUnordered, ct); - writes.Clear(); - } + writes.Add(model); } - await collection.Find(FindAll).ForEachAsync(async commit => + if (writes.Count == 1000 || (force && writes.Count > 0)) { - UpdateDefinition? update = null; + await collection.BulkWriteAsync(writes, BulkUnordered, ct); + writes.Clear(); + } + } - var index = 0; + await collection.Find(FindAll).ForEachAsync(async commit => + { + UpdateDefinition? update = null; + + var index = 0; + + foreach (BsonDocument @event in commit["Events"].AsBsonArray) + { + var data = BsonDocument.Parse(@event["Payload"].AsString); - foreach (BsonDocument @event in commit["Events"].AsBsonArray) + if (data.TryGetValue("appId", out var appIdValue)) { - var data = BsonDocument.Parse(@event["Payload"].AsString); + var appId = NamedId.Parse(appIdValue.AsString, Guid.TryParse).Id.ToString(); - if (data.TryGetValue("appId", out var appIdValue)) + var eventUpdate = Update.Set($"Events.{index}.Metadata.AppId", appId); + + if (update != null) { - var appId = NamedId.Parse(appIdValue.AsString, Guid.TryParse).Id.ToString(); - - var eventUpdate = Update.Set($"Events.{index}.Metadata.AppId", appId); - - if (update != null) - { - update = Update.Combine(update, eventUpdate); - } - else - { - update = eventUpdate; - } + update = Update.Combine(update, eventUpdate); + } + else + { + update = eventUpdate; } - - index++; } - if (update != null) - { - var write = new UpdateOneModel(Filter.Eq("_id", commit["_id"].AsString), update); + index++; + } - await WriteAsync(write, false); - } - }, ct); + if (update != null) + { + var write = new UpdateOneModel(Filter.Eq("_id", commit["_id"].AsString), update); - await WriteAsync(null, true); - } + await WriteAsync(write, false); + } + }, ct); + + await WriteAsync(null, true); } } } diff --git a/backend/src/Migrations/Migrations/CreateAssetSlugs.cs b/backend/src/Migrations/Migrations/CreateAssetSlugs.cs index 91469b9463..4ed6fe5498 100644 --- a/backend/src/Migrations/Migrations/CreateAssetSlugs.cs +++ b/backend/src/Migrations/Migrations/CreateAssetSlugs.cs @@ -10,28 +10,27 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.States; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class CreateAssetSlugs : IMigration { - public sealed class CreateAssetSlugs : IMigration - { - private readonly ISnapshotStore stateForAssets; + private readonly ISnapshotStore stateForAssets; - public CreateAssetSlugs(ISnapshotStore stateForAssets) - { - this.stateForAssets = stateForAssets; - } + public CreateAssetSlugs(ISnapshotStore stateForAssets) + { + this.stateForAssets = stateForAssets; + } - public async Task UpdateAsync( - CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) + { + await foreach (var (key, state, version, _) in stateForAssets.ReadAllAsync(ct)) { - await foreach (var (key, state, version, _) in stateForAssets.ReadAllAsync(ct)) - { - state.Slug = state.FileName.ToAssetSlug(); + state.Slug = state.FileName.ToAssetSlug(); - var job = new SnapshotWriteJob(key, state, version); + var job = new SnapshotWriteJob(key, state, version); - await stateForAssets.WriteAsync(job, ct); - } + await stateForAssets.WriteAsync(job, ct); } } } diff --git a/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs b/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs index 48efa47820..a9ed573175 100644 --- a/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs +++ b/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs @@ -14,159 +14,158 @@ using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Tasks; -namespace Migrations.Migrations.MongoDb +namespace Migrations.Migrations.MongoDb; + +public sealed class AddAppIdToEventStream : MongoBase, IMigration { - public sealed class AddAppIdToEventStream : MongoBase, IMigration + private readonly IMongoDatabase database; + + public AddAppIdToEventStream(IMongoDatabase database) { - private readonly IMongoDatabase database; + this.database = database; + } - public AddAppIdToEventStream(IMongoDatabase database) - { - this.database = database; - } + public async Task UpdateAsync( + CancellationToken ct) + { + const int SizeOfBatch = 1000; + const int SizeOfQueue = 20; - public async Task UpdateAsync( - CancellationToken ct) - { - const int SizeOfBatch = 1000; - const int SizeOfQueue = 20; + // Do not resolve in constructor, because most of the time it is not executed anyway. + var collectionV1 = database.GetCollection("Events"); + var collectionV2 = database.GetCollection("Events2"); - // Do not resolve in constructor, because most of the time it is not executed anyway. - var collectionV1 = database.GetCollection("Events"); - var collectionV2 = database.GetCollection("Events2"); + var batchBlock = new BatchBlock(SizeOfBatch, new GroupingDataflowBlockOptions + { + BoundedCapacity = SizeOfQueue * SizeOfBatch + }); - var batchBlock = new BatchBlock(SizeOfBatch, new GroupingDataflowBlockOptions + var actionBlock = new ActionBlock(async batch => + { + try { - BoundedCapacity = SizeOfQueue * SizeOfBatch - }); + var writes = new List>(); - var actionBlock = new ActionBlock(async batch => - { - try + foreach (var document in batch) { - var writes = new List>(); + var eventStream = document["EventStream"].AsString; - foreach (var document in batch) + if (TryGetAppId(document, out var appId)) { - var eventStream = document["EventStream"].AsString; - - if (TryGetAppId(document, out var appId)) + if (!eventStream.StartsWith("app-", StringComparison.OrdinalIgnoreCase)) { - if (!eventStream.StartsWith("app-", StringComparison.OrdinalIgnoreCase)) - { - var indexOfType = eventStream.IndexOf('-', StringComparison.Ordinal); - var indexOfId = indexOfType + 1; - - var indexOfOldId = eventStream.LastIndexOf("--", StringComparison.OrdinalIgnoreCase); + var indexOfType = eventStream.IndexOf('-', StringComparison.Ordinal); + var indexOfId = indexOfType + 1; - if (indexOfOldId > 0) - { - indexOfId = indexOfOldId + 2; - } + var indexOfOldId = eventStream.LastIndexOf("--", StringComparison.OrdinalIgnoreCase); - var domainType = eventStream[..indexOfType]; - var domainId = eventStream[indexOfId..]; - - var newDomainId = DomainId.Combine(DomainId.Create(appId), DomainId.Create(domainId)).ToString(); - var newStreamName = $"{domainType}-{newDomainId}"; + if (indexOfOldId > 0) + { + indexOfId = indexOfOldId + 2; + } - document["EventStream"] = newStreamName; + var domainType = eventStream[..indexOfType]; + var domainId = eventStream[indexOfId..]; - foreach (var @event in document["Events"].AsBsonArray) - { - var metadata = @event["Metadata"].AsBsonDocument; + var newDomainId = DomainId.Combine(DomainId.Create(appId), DomainId.Create(domainId)).ToString(); + var newStreamName = $"{domainType}-{newDomainId}"; - metadata["AggregateId"] = newDomainId; - } - } + document["EventStream"] = newStreamName; foreach (var @event in document["Events"].AsBsonArray) { var metadata = @event["Metadata"].AsBsonDocument; - metadata.Remove("AppId"); + metadata["AggregateId"] = newDomainId; } } - var filter = Builders.Filter.Eq("_id", document["_id"].AsString); - - writes.Add(new ReplaceOneModel(filter, document) + foreach (var @event in document["Events"].AsBsonArray) { - IsUpsert = true - }); + var metadata = @event["Metadata"].AsBsonDocument; + + metadata.Remove("AppId"); + } } - if (writes.Count > 0) + var filter = Builders.Filter.Eq("_id", document["_id"].AsString); + + writes.Add(new ReplaceOneModel(filter, document) { - await collectionV2.BulkWriteAsync(writes, BulkUnordered, ct); - } + IsUpsert = true + }); } - catch (OperationCanceledException ex) + + if (writes.Count > 0) { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); + await collectionV2.BulkWriteAsync(writes, BulkUnordered, ct); } - }, new ExecutionDataflowBlockOptions + } + catch (OperationCanceledException ex) { - MaxDegreeOfParallelism = Environment.ProcessorCount * 2, - MaxMessagesPerTask = 10, - BoundedCapacity = SizeOfQueue - }); + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); + } + }, new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = Environment.ProcessorCount * 2, + MaxMessagesPerTask = 10, + BoundedCapacity = SizeOfQueue + }); - batchBlock.BidirectionalLinkTo(actionBlock); + batchBlock.BidirectionalLinkTo(actionBlock); - await foreach (var commit in collectionV1.Find(FindAll).ToAsyncEnumerable(ct: ct)) + await foreach (var commit in collectionV1.Find(FindAll).ToAsyncEnumerable(ct: ct)) + { + if (!await batchBlock.SendAsync(commit, ct)) { - if (!await batchBlock.SendAsync(commit, ct)) - { - break; - } + break; } + } - batchBlock.Complete(); + batchBlock.Complete(); - await actionBlock.Completion; - } + await actionBlock.Completion; + } - private static bool TryGetAppId(BsonDocument document, [MaybeNullWhen(false)] out string appId) + private static bool TryGetAppId(BsonDocument document, [MaybeNullWhen(false)] out string appId) + { + const int guidLength = 36; + + foreach (var @event in document["Events"].AsBsonArray) { - const int guidLength = 36; + var metadata = @event["Metadata"].AsBsonDocument; - foreach (var @event in document["Events"].AsBsonArray) + if (metadata.TryGetValue("AppId", out var value)) { - var metadata = @event["Metadata"].AsBsonDocument; + appId = value.AsString; + return true; + } - if (metadata.TryGetValue("AppId", out var value)) - { - appId = value.AsString; - return true; - } + if (metadata.TryGetValue("AggregateId", out var aggregateId)) + { + var parts = aggregateId.AsString.Split("--"); - if (metadata.TryGetValue("AggregateId", out var aggregateId)) + if (parts.Length == 2) { - var parts = aggregateId.AsString.Split("--"); - - if (parts.Length == 2) - { - appId = parts[0]; - return true; - } + appId = parts[0]; + return true; } + } - var payload = @event["Payload"].AsString; + var payload = @event["Payload"].AsString; - var indexOfAppId = payload.IndexOf("appId\":\"", StringComparison.OrdinalIgnoreCase); + var indexOfAppId = payload.IndexOf("appId\":\"", StringComparison.OrdinalIgnoreCase); - if (indexOfAppId > 0) - { - appId = payload.Substring(indexOfAppId, guidLength); - return true; - } + if (indexOfAppId > 0) + { + appId = payload.Substring(indexOfAppId, guidLength); + return true; } + } - appId = null; + appId = null; - return false; - } + return false; } } diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs index d1b572f633..108636d2c4 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs @@ -13,166 +13,165 @@ using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Tasks; -namespace Migrations.Migrations.MongoDb +namespace Migrations.Migrations.MongoDb; + +public sealed class ConvertDocumentIds : MongoBase, IMigration { - public sealed class ConvertDocumentIds : MongoBase, IMigration - { - private readonly IMongoDatabase database; - private readonly IMongoDatabase databaseContent; - private Scope scope; + private readonly IMongoDatabase database; + private readonly IMongoDatabase databaseContent; + private Scope scope; - private enum Scope - { - None, - Assets, - Contents - } + private enum Scope + { + None, + Assets, + Contents + } - public ConvertDocumentIds(IMongoDatabase database, IMongoDatabase databaseContent) - { - this.database = database; - this.databaseContent = databaseContent; - } + public ConvertDocumentIds(IMongoDatabase database, IMongoDatabase databaseContent) + { + this.database = database; + this.databaseContent = databaseContent; + } - public override string ToString() - { - return $"{base.ToString()}({scope})"; - } + public override string ToString() + { + return $"{base.ToString()}({scope})"; + } - public ConvertDocumentIds ForContents() - { - scope = Scope.Contents; + public ConvertDocumentIds ForContents() + { + scope = Scope.Contents; - return this; - } + return this; + } - public ConvertDocumentIds ForAssets() - { - scope = Scope.Assets; + public ConvertDocumentIds ForAssets() + { + scope = Scope.Assets; - return this; - } + return this; + } - public async Task UpdateAsync( - CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) + { + switch (scope) { - switch (scope) - { - case Scope.Assets: - await RebuildAsync(database, ConvertParentId, "States_Assets", ct); - await RebuildAsync(database, ConvertParentId, "States_AssetFolders", ct); - break; - case Scope.Contents: - await RebuildAsync(databaseContent, null, "State_Contents_All", ct); - await RebuildAsync(databaseContent, null, "State_Contents_Published", ct); - break; - } + case Scope.Assets: + await RebuildAsync(database, ConvertParentId, "States_Assets", ct); + await RebuildAsync(database, ConvertParentId, "States_AssetFolders", ct); + break; + case Scope.Contents: + await RebuildAsync(databaseContent, null, "State_Contents_All", ct); + await RebuildAsync(databaseContent, null, "State_Contents_Published", ct); + break; } + } - private static async Task RebuildAsync(IMongoDatabase database, Action? extraAction, string collectionNameV1, - CancellationToken ct) - { - const int SizeOfBatch = 1000; - const int SizeOfQueue = 10; + private static async Task RebuildAsync(IMongoDatabase database, Action? extraAction, string collectionNameV1, + CancellationToken ct) + { + const int SizeOfBatch = 1000; + const int SizeOfQueue = 10; - string collectionNameV2; + string collectionNameV2; - collectionNameV2 = $"{collectionNameV1}2"; - collectionNameV2 = collectionNameV2.Replace("State_", "States_", StringComparison.Ordinal); + collectionNameV2 = $"{collectionNameV1}2"; + collectionNameV2 = collectionNameV2.Replace("State_", "States_", StringComparison.Ordinal); - // Do not resolve in constructor, because most of the time it is not executed anyway. - var collectionV1 = database.GetCollection(collectionNameV1); - var collectionV2 = database.GetCollection(collectionNameV2); + // Do not resolve in constructor, because most of the time it is not executed anyway. + var collectionV1 = database.GetCollection(collectionNameV1); + var collectionV2 = database.GetCollection(collectionNameV2); - if (!await collectionV1.AnyAsync(ct: ct)) - { - return; - } + if (!await collectionV1.AnyAsync(ct: ct)) + { + return; + } - await collectionV2.DeleteManyAsync(FindAll, ct); + await collectionV2.DeleteManyAsync(FindAll, ct); - var batchBlock = new BatchBlock(SizeOfBatch, new GroupingDataflowBlockOptions - { - BoundedCapacity = SizeOfQueue * SizeOfBatch - }); + var batchBlock = new BatchBlock(SizeOfBatch, new GroupingDataflowBlockOptions + { + BoundedCapacity = SizeOfQueue * SizeOfBatch + }); - var writeOptions = new BulkWriteOptions - { - IsOrdered = false - }; + var writeOptions = new BulkWriteOptions + { + IsOrdered = false + }; - var actionBlock = new ActionBlock(async batch => + var actionBlock = new ActionBlock(async batch => + { + try { - try - { - var writes = new List>(); - - foreach (var document in batch) - { - var appId = document["_ai"].AsString; + var writes = new List>(); - var documentIdOld = document["_id"].AsString; + foreach (var document in batch) + { + var appId = document["_ai"].AsString; - if (documentIdOld.Contains("--", StringComparison.OrdinalIgnoreCase)) - { - var index = documentIdOld.LastIndexOf("--", StringComparison.OrdinalIgnoreCase); + var documentIdOld = document["_id"].AsString; - documentIdOld = documentIdOld[(index + 2)..]; - } + if (documentIdOld.Contains("--", StringComparison.OrdinalIgnoreCase)) + { + var index = documentIdOld.LastIndexOf("--", StringComparison.OrdinalIgnoreCase); - var documentIdNew = DomainId.Combine(DomainId.Create(appId), DomainId.Create(documentIdOld)).ToString(); + documentIdOld = documentIdOld[(index + 2)..]; + } - document["id"] = documentIdOld; - document["_id"] = documentIdNew; + var documentIdNew = DomainId.Combine(DomainId.Create(appId), DomainId.Create(documentIdOld)).ToString(); - extraAction?.Invoke(document); + document["id"] = documentIdOld; + document["_id"] = documentIdNew; - var filter = Filter.Eq("_id", documentIdNew); + extraAction?.Invoke(document); - writes.Add(new ReplaceOneModel(filter, document) - { - IsUpsert = true - }); - } + var filter = Filter.Eq("_id", documentIdNew); - if (writes.Count > 0) + writes.Add(new ReplaceOneModel(filter, document) { - await collectionV2.BulkWriteAsync(writes, writeOptions, ct); - } + IsUpsert = true + }); } - catch (OperationCanceledException ex) + + if (writes.Count > 0) { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); + await collectionV2.BulkWriteAsync(writes, writeOptions, ct); } - }, new ExecutionDataflowBlockOptions + } + catch (OperationCanceledException ex) { - MaxDegreeOfParallelism = Environment.ProcessorCount * 2, - MaxMessagesPerTask = 1, - BoundedCapacity = SizeOfQueue - }); + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); + } + }, new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = Environment.ProcessorCount * 2, + MaxMessagesPerTask = 1, + BoundedCapacity = SizeOfQueue + }); - batchBlock.BidirectionalLinkTo(actionBlock); + batchBlock.BidirectionalLinkTo(actionBlock); - await foreach (var document in collectionV1.Find(FindAll).ToAsyncEnumerable(ct: ct)) + await foreach (var document in collectionV1.Find(FindAll).ToAsyncEnumerable(ct: ct)) + { + if (!await batchBlock.SendAsync(document, ct)) { - if (!await batchBlock.SendAsync(document, ct)) - { - break; - } + break; } + } - batchBlock.Complete(); + batchBlock.Complete(); - await actionBlock.Completion; - } + await actionBlock.Completion; + } - private static void ConvertParentId(BsonDocument document) + private static void ConvertParentId(BsonDocument document) + { + if (document.Contains("pi")) { - if (document.Contains("pi")) - { - document["pi"] = document["pi"].AsGuid.ToString(); - } + document["pi"] = document["pi"].AsGuid.ToString(); } } } diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs index 012321a48b..604b795a86 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs @@ -10,31 +10,30 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; -namespace Migrations.Migrations.MongoDb +namespace Migrations.Migrations.MongoDb; + +public sealed class ConvertOldSnapshotStores : MongoBase, IMigration { - public sealed class ConvertOldSnapshotStores : MongoBase, IMigration - { - private readonly IMongoDatabase database; + private readonly IMongoDatabase database; - public ConvertOldSnapshotStores(IMongoDatabase database) - { - this.database = database; - } + public ConvertOldSnapshotStores(IMongoDatabase database) + { + this.database = database; + } - public Task UpdateAsync( - CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) + { + // Do not resolve in constructor, because most of the time it is not executed anyway. + var collections = new[] { - // Do not resolve in constructor, because most of the time it is not executed anyway. - var collections = new[] - { - "States_Apps", - "States_Rules", - "States_Schemas" - }.Select(x => database.GetCollection(x)); + "States_Apps", + "States_Rules", + "States_Schemas" + }.Select(x => database.GetCollection(x)); - var update = Update.Rename("State", "Doc"); + var update = Update.Rename("State", "Doc"); - return Task.WhenAll(collections.Select(x => x.UpdateManyAsync(FindAll, update, cancellationToken: ct))); - } + return Task.WhenAll(collections.Select(x => x.UpdateManyAsync(FindAll, update, cancellationToken: ct))); } } diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs index 69c7f1a18e..b5891f3ef0 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs @@ -10,34 +10,33 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; -namespace Migrations.Migrations.MongoDb +namespace Migrations.Migrations.MongoDb; + +public sealed class ConvertRuleEventsJson : MongoBase, IMigration { - public sealed class ConvertRuleEventsJson : MongoBase, IMigration - { - private readonly IMongoCollection collection; + private readonly IMongoCollection collection; - public ConvertRuleEventsJson(IMongoDatabase database) - { - collection = database.GetCollection("RuleEvents"); - } + public ConvertRuleEventsJson(IMongoDatabase database) + { + collection = database.GetCollection("RuleEvents"); + } - public async Task UpdateAsync( - CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) + { + foreach (var document in collection.Find(FindAll).ToEnumerable(ct)) { - foreach (var document in collection.Find(FindAll).ToEnumerable(ct)) + try { - try - { - document["Job"]["actionData"] = document["Job"]["actionData"].ToBsonDocument().ToJson(); + document["Job"]["actionData"] = document["Job"]["actionData"].ToBsonDocument().ToJson(); - var filter = Filter.Eq("_id", document["_id"].ToString()); + var filter = Filter.Eq("_id", document["_id"].ToString()); - await collection.ReplaceOneAsync(filter, document, cancellationToken: ct); - } - catch - { - continue; - } + await collection.ReplaceOneAsync(filter, document, cancellationToken: ct); + } + catch + { + continue; } } } diff --git a/backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs b/backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs index 8020f72518..70ea81ec41 100644 --- a/backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs +++ b/backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs @@ -8,24 +8,23 @@ using MongoDB.Driver; using Squidex.Infrastructure.Migrations; -namespace Migrations.Migrations.MongoDb +namespace Migrations.Migrations.MongoDb; + +public sealed class DeleteContentCollections : IMigration { - public sealed class DeleteContentCollections : IMigration - { - private readonly IMongoDatabase database; + private readonly IMongoDatabase database; - public DeleteContentCollections(IMongoDatabase database) - { - this.database = database; - } + public DeleteContentCollections(IMongoDatabase database) + { + this.database = database; + } - public async Task UpdateAsync( - CancellationToken ct) - { - await database.DropCollectionAsync("States_Contents", ct); - await database.DropCollectionAsync("States_Contents_Archive", ct); - await database.DropCollectionAsync("State_Content_Draft", ct); - await database.DropCollectionAsync("State_Content_Published", ct); - } + public async Task UpdateAsync( + CancellationToken ct) + { + await database.DropCollectionAsync("States_Contents", ct); + await database.DropCollectionAsync("States_Contents_Archive", ct); + await database.DropCollectionAsync("State_Content_Draft", ct); + await database.DropCollectionAsync("State_Content_Published", ct); } } diff --git a/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs b/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs index 88d3990df5..a3bbc06b07 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs @@ -10,52 +10,51 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; -namespace Migrations.Migrations.MongoDb +namespace Migrations.Migrations.MongoDb; + +public sealed class RenameAssetMetadata : MongoBase, IMigration { - public sealed class RenameAssetMetadata : MongoBase, IMigration + private readonly IMongoDatabase database; + + public RenameAssetMetadata(IMongoDatabase database) + { + this.database = database; + } + + public async Task UpdateAsync( + CancellationToken ct) { - private readonly IMongoDatabase database; - - public RenameAssetMetadata(IMongoDatabase database) - { - this.database = database; - } - - public async Task UpdateAsync( - CancellationToken ct) - { - // Do not resolve in constructor, because most of the time it is not executed anyway. - var collection = database.GetCollection("States_Assets"); - - // Create metadata. - await collection.UpdateManyAsync(FindAll, - Update.Set("md", new BsonDocument()), - cancellationToken: ct); - - // Remove null pixel infos. - await collection.UpdateManyAsync(new BsonDocument("ph", BsonValue.Create(null)), - Update.Unset("ph").Unset("pw"), - cancellationToken: ct); - - // Set pixel metadata. - await collection.UpdateManyAsync(FindAll, - Update.Rename("ph", "md.pixelHeight").Rename("pw", "md.pixelWidth"), - cancellationToken: ct); - - // Set type to image. - await collection.UpdateManyAsync(new BsonDocument("im", true), - Update.Set("at", "Image"), - cancellationToken: ct); - - // Set type to unknown. - await collection.UpdateManyAsync(new BsonDocument("im", false), - Update.Set("at", "Unknown"), - cancellationToken: ct); - - // Remove IsImage. - await collection.UpdateManyAsync(FindAll, - Update.Unset("im"), - cancellationToken: ct); - } + // Do not resolve in constructor, because most of the time it is not executed anyway. + var collection = database.GetCollection("States_Assets"); + + // Create metadata. + await collection.UpdateManyAsync(FindAll, + Update.Set("md", new BsonDocument()), + cancellationToken: ct); + + // Remove null pixel infos. + await collection.UpdateManyAsync(new BsonDocument("ph", BsonValue.Create(null)), + Update.Unset("ph").Unset("pw"), + cancellationToken: ct); + + // Set pixel metadata. + await collection.UpdateManyAsync(FindAll, + Update.Rename("ph", "md.pixelHeight").Rename("pw", "md.pixelWidth"), + cancellationToken: ct); + + // Set type to image. + await collection.UpdateManyAsync(new BsonDocument("im", true), + Update.Set("at", "Image"), + cancellationToken: ct); + + // Set type to unknown. + await collection.UpdateManyAsync(new BsonDocument("im", false), + Update.Set("at", "Unknown"), + cancellationToken: ct); + + // Remove IsImage. + await collection.UpdateManyAsync(FindAll, + Update.Unset("im"), + cancellationToken: ct); } } diff --git a/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs b/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs index e62104194d..c872d959af 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs @@ -10,26 +10,25 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; -namespace Migrations.Migrations.MongoDb +namespace Migrations.Migrations.MongoDb; + +public sealed class RenameAssetSlugField : MongoBase, IMigration { - public sealed class RenameAssetSlugField : MongoBase, IMigration - { - private readonly IMongoDatabase database; + private readonly IMongoDatabase database; - public RenameAssetSlugField(IMongoDatabase database) - { - this.database = database; - } + public RenameAssetSlugField(IMongoDatabase database) + { + this.database = database; + } - public Task UpdateAsync( - CancellationToken ct) - { - // Do not resolve in constructor, because most of the time it is not executed anyway. - var collection = database.GetCollection("States_Assets"); + public Task UpdateAsync( + CancellationToken ct) + { + // Do not resolve in constructor, because most of the time it is not executed anyway. + var collection = database.GetCollection("States_Assets"); - var update = Builders.Update.Rename("FileNameSlug", "Slug"); + var update = Builders.Update.Rename("FileNameSlug", "Slug"); - return collection.UpdateManyAsync(FindAll, update, cancellationToken: ct); - } + return collection.UpdateManyAsync(FindAll, update, cancellationToken: ct); } } diff --git a/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs b/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs index 1588014593..766b09b0fa 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs @@ -10,34 +10,33 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; -namespace Migrations.Migrations.MongoDb +namespace Migrations.Migrations.MongoDb; + +public sealed class RestructureContentCollection : MongoBase, IMigration { - public sealed class RestructureContentCollection : MongoBase, IMigration + private readonly IMongoDatabase contentDatabase; + + public RestructureContentCollection(IMongoDatabase contentDatabase) { - private readonly IMongoDatabase contentDatabase; + this.contentDatabase = contentDatabase; + } - public RestructureContentCollection(IMongoDatabase contentDatabase) + public async Task UpdateAsync( + CancellationToken ct) + { + if (await contentDatabase.CollectionExistsAsync("State_Content_Draft", ct)) { - this.contentDatabase = contentDatabase; + await contentDatabase.DropCollectionAsync("State_Contents", ct); + await contentDatabase.DropCollectionAsync("State_Content_Published", ct); + + await contentDatabase.RenameCollectionAsync("State_Content_Draft", "State_Contents", cancellationToken: ct); } - public async Task UpdateAsync( - CancellationToken ct) + if (await contentDatabase.CollectionExistsAsync("State_Contents", ct)) { - if (await contentDatabase.CollectionExistsAsync("State_Content_Draft", ct)) - { - await contentDatabase.DropCollectionAsync("State_Contents", ct); - await contentDatabase.DropCollectionAsync("State_Content_Published", ct); - - await contentDatabase.RenameCollectionAsync("State_Content_Draft", "State_Contents", cancellationToken: ct); - } - - if (await contentDatabase.CollectionExistsAsync("State_Contents", ct)) - { - var collection = contentDatabase.GetCollection("State_Contents"); + var collection = contentDatabase.GetCollection("State_Contents"); - await collection.UpdateManyAsync(FindAll, Update.Unset("dt"), cancellationToken: ct); - } + await collection.UpdateManyAsync(FindAll, Update.Unset("dt"), cancellationToken: ct); } } } diff --git a/backend/src/Migrations/Migrations/RebuildApps.cs b/backend/src/Migrations/Migrations/RebuildApps.cs index 9b400886b4..30b71450df 100644 --- a/backend/src/Migrations/Migrations/RebuildApps.cs +++ b/backend/src/Migrations/Migrations/RebuildApps.cs @@ -9,24 +9,23 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class RebuildApps : IMigration { - public sealed class RebuildApps : IMigration - { - private readonly Rebuilder rebuilder; - private readonly RebuildOptions rebuildOptions; + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; - public RebuildApps(Rebuilder rebuilder, - IOptions rebuildOptions) - { - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - } + public RebuildApps(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } - public Task UpdateAsync( - CancellationToken ct) - { - return rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); - } + public Task UpdateAsync( + CancellationToken ct) + { + return rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildAssetFolders.cs b/backend/src/Migrations/Migrations/RebuildAssetFolders.cs index af76b54a66..54cae8d7e7 100644 --- a/backend/src/Migrations/Migrations/RebuildAssetFolders.cs +++ b/backend/src/Migrations/Migrations/RebuildAssetFolders.cs @@ -9,24 +9,23 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class RebuildAssetFolders : IMigration { - public sealed class RebuildAssetFolders : IMigration - { - private readonly Rebuilder rebuilder; - private readonly RebuildOptions rebuildOptions; + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; - public RebuildAssetFolders(Rebuilder rebuilder, - IOptions rebuildOptions) - { - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - } + public RebuildAssetFolders(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } - public Task UpdateAsync( - CancellationToken ct) - { - return rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct); - } + public Task UpdateAsync( + CancellationToken ct) + { + return rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildAssets.cs b/backend/src/Migrations/Migrations/RebuildAssets.cs index 1375f900ed..fb7dfc3d59 100644 --- a/backend/src/Migrations/Migrations/RebuildAssets.cs +++ b/backend/src/Migrations/Migrations/RebuildAssets.cs @@ -9,24 +9,23 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class RebuildAssets : IMigration { - public sealed class RebuildAssets : IMigration - { - private readonly Rebuilder rebuilder; - private readonly RebuildOptions rebuildOptions; + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; - public RebuildAssets(Rebuilder rebuilder, - IOptions rebuildOptions) - { - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - } + public RebuildAssets(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } - public Task UpdateAsync( - CancellationToken ct) - { - return rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct); - } + public Task UpdateAsync( + CancellationToken ct) + { + return rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildContents.cs b/backend/src/Migrations/Migrations/RebuildContents.cs index 62ef7b52a6..ed01bfccd0 100644 --- a/backend/src/Migrations/Migrations/RebuildContents.cs +++ b/backend/src/Migrations/Migrations/RebuildContents.cs @@ -9,24 +9,23 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class RebuildContents : IMigration { - public sealed class RebuildContents : IMigration - { - private readonly Rebuilder rebuilder; - private readonly RebuildOptions rebuildOptions; + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; - public RebuildContents(Rebuilder rebuilder, - IOptions rebuildOptions) - { - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - } + public RebuildContents(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } - public Task UpdateAsync( - CancellationToken ct) - { - return rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct); - } + public Task UpdateAsync( + CancellationToken ct) + { + return rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildRules.cs b/backend/src/Migrations/Migrations/RebuildRules.cs index 51f7c3f879..05c3ff143d 100644 --- a/backend/src/Migrations/Migrations/RebuildRules.cs +++ b/backend/src/Migrations/Migrations/RebuildRules.cs @@ -9,24 +9,23 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class RebuildRules : IMigration { - public sealed class RebuildRules : IMigration - { - private readonly Rebuilder rebuilder; - private readonly RebuildOptions rebuildOptions; + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; - public RebuildRules(Rebuilder rebuilder, - IOptions rebuildOptions) - { - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - } + public RebuildRules(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } - public Task UpdateAsync( - CancellationToken ct) - { - return rebuilder.RebuildRulesAsync(rebuildOptions.BatchSize, ct); - } + public Task UpdateAsync( + CancellationToken ct) + { + return rebuilder.RebuildRulesAsync(rebuildOptions.BatchSize, ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildSchemas.cs b/backend/src/Migrations/Migrations/RebuildSchemas.cs index 47447c624f..5566f1c4a7 100644 --- a/backend/src/Migrations/Migrations/RebuildSchemas.cs +++ b/backend/src/Migrations/Migrations/RebuildSchemas.cs @@ -9,24 +9,23 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class RebuildSchemas : IMigration { - public sealed class RebuildSchemas : IMigration - { - private readonly Rebuilder rebuilder; - private readonly RebuildOptions rebuildOptions; + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; - public RebuildSchemas(Rebuilder rebuilder, - IOptions rebuildOptions) - { - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - } + public RebuildSchemas(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } - public Task UpdateAsync( - CancellationToken ct) - { - return rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); - } + public Task UpdateAsync( + CancellationToken ct) + { + return rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildSnapshots.cs b/backend/src/Migrations/Migrations/RebuildSnapshots.cs index e14b69f6e5..cf684b103f 100644 --- a/backend/src/Migrations/Migrations/RebuildSnapshots.cs +++ b/backend/src/Migrations/Migrations/RebuildSnapshots.cs @@ -9,29 +9,28 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; -namespace Migrations.Migrations +namespace Migrations.Migrations; + +public sealed class RebuildSnapshots : IMigration { - public sealed class RebuildSnapshots : IMigration - { - private readonly Rebuilder rebuilder; - private readonly RebuildOptions rebuildOptions; + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; - public RebuildSnapshots(Rebuilder rebuilder, - IOptions rebuildOptions) - { - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; - } + public RebuildSnapshots(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } - public async Task UpdateAsync( - CancellationToken ct) - { - await rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildRulesAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct); - } + public async Task UpdateAsync( + CancellationToken ct) + { + await rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); + await rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); + await rebuilder.RebuildRulesAsync(rebuildOptions.BatchSize, ct); + await rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct); + await rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct); + await rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct); } } diff --git a/backend/src/Migrations/OldEvents/AppArchived.cs b/backend/src/Migrations/OldEvents/AppArchived.cs index f71fc057de..6c0f163345 100644 --- a/backend/src/Migrations/OldEvents/AppArchived.cs +++ b/backend/src/Migrations/OldEvents/AppArchived.cs @@ -11,14 +11,13 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AppArchived))] +public sealed class AppArchived : AppEvent, IMigrated { - [EventType(nameof(AppArchived))] - public sealed class AppArchived : AppEvent, IMigrated + public IEvent Migrate() { - public IEvent Migrate() - { - return SimpleMapper.Map(this, new AppDeleted()); - } + return SimpleMapper.Map(this, new AppDeleted()); } } diff --git a/backend/src/Migrations/OldEvents/AppClientChanged.cs b/backend/src/Migrations/OldEvents/AppClientChanged.cs index 33072a432e..803255d470 100644 --- a/backend/src/Migrations/OldEvents/AppClientChanged.cs +++ b/backend/src/Migrations/OldEvents/AppClientChanged.cs @@ -10,24 +10,23 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AppClientChanged))] +[Obsolete("New Event introduced")] +public sealed class AppClientChanged : AppEvent, IMigrated { - [EventType(nameof(AppClientChanged))] - [Obsolete("New Event introduced")] - public sealed class AppClientChanged : AppEvent, IMigrated - { - public string Id { get; set; } + public string Id { get; set; } - public bool IsReader { get; set; } + public bool IsReader { get; set; } - public IEvent Migrate() - { - var permission = - IsReader ? - AppClientPermission.Reader : - AppClientPermission.Editor; + public IEvent Migrate() + { + var permission = + IsReader ? + AppClientPermission.Reader : + AppClientPermission.Editor; - return SimpleMapper.Map(this, new AppClientUpdated { Permission = permission }); - } + return SimpleMapper.Map(this, new AppClientUpdated { Permission = permission }); } } diff --git a/backend/src/Migrations/OldEvents/AppClientPermission.cs b/backend/src/Migrations/OldEvents/AppClientPermission.cs index 2ce2cfc735..1332db787d 100644 --- a/backend/src/Migrations/OldEvents/AppClientPermission.cs +++ b/backend/src/Migrations/OldEvents/AppClientPermission.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[Obsolete("New Event introduced")] +public enum AppClientPermission { - [Obsolete("New Event introduced")] - public enum AppClientPermission - { - Developer, - Editor, - Reader - } + Developer, + Editor, + Reader } diff --git a/backend/src/Migrations/OldEvents/AppClientRenamed.cs b/backend/src/Migrations/OldEvents/AppClientRenamed.cs index 2c0d2cfe2b..eecc8f7b91 100644 --- a/backend/src/Migrations/OldEvents/AppClientRenamed.cs +++ b/backend/src/Migrations/OldEvents/AppClientRenamed.cs @@ -11,20 +11,19 @@ using Squidex.Infrastructure.Reflection; using AppClientUpdatedV2 = Squidex.Domain.Apps.Events.Apps.AppClientUpdated; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AppClientRenamed))] +public sealed class AppClientRenamed : AppEvent, IMigrated { - [EventType(nameof(AppClientRenamed))] - public sealed class AppClientRenamed : AppEvent, IMigrated - { - public string Id { get; set; } + public string Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public IEvent Migrate() - { - var result = SimpleMapper.Map(this, new AppClientUpdatedV2()); + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AppClientUpdatedV2()); - return result; - } + return result; } } diff --git a/backend/src/Migrations/OldEvents/AppClientUpdated.cs b/backend/src/Migrations/OldEvents/AppClientUpdated.cs index 6d600295f3..da14b637c8 100644 --- a/backend/src/Migrations/OldEvents/AppClientUpdated.cs +++ b/backend/src/Migrations/OldEvents/AppClientUpdated.cs @@ -12,34 +12,33 @@ using Squidex.Infrastructure.Reflection; using AppClientUpdatedV2 = Squidex.Domain.Apps.Events.Apps.AppClientUpdated; -namespace Migrations.OldEvents -{ - [EventType(nameof(AppClientUpdated))] - [Obsolete("New Event introduced")] - public sealed class AppClientUpdated : AppEvent, IMigrated - { - public string Id { get; set; } +namespace Migrations.OldEvents; - public AppClientPermission Permission { get; set; } +[EventType(nameof(AppClientUpdated))] +[Obsolete("New Event introduced")] +public sealed class AppClientUpdated : AppEvent, IMigrated +{ + public string Id { get; set; } - public IEvent Migrate() - { - var result = SimpleMapper.Map(this, new AppClientUpdatedV2()); + public AppClientPermission Permission { get; set; } - switch (Permission) - { - case AppClientPermission.Developer: - result.Role = Role.Developer; - break; - case AppClientPermission.Editor: - result.Role = Role.Editor; - break; - case AppClientPermission.Reader: - result.Role = Role.Reader; - break; - } + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AppClientUpdatedV2()); - return result; + switch (Permission) + { + case AppClientPermission.Developer: + result.Role = Role.Developer; + break; + case AppClientPermission.Editor: + result.Role = Role.Editor; + break; + case AppClientPermission.Reader: + result.Role = Role.Reader; + break; } + + return result; } } \ No newline at end of file diff --git a/backend/src/Migrations/OldEvents/AppContributorAssigned.cs b/backend/src/Migrations/OldEvents/AppContributorAssigned.cs index a3fb275793..c52d9f556a 100644 --- a/backend/src/Migrations/OldEvents/AppContributorAssigned.cs +++ b/backend/src/Migrations/OldEvents/AppContributorAssigned.cs @@ -12,34 +12,33 @@ using Squidex.Infrastructure.Reflection; using AppContributorAssignedV2 = Squidex.Domain.Apps.Events.Apps.AppContributorAssigned; -namespace Migrations.OldEvents -{ - [EventType(nameof(AppContributorAssigned))] - [Obsolete("New Event introduced")] - public sealed class AppContributorAssigned : AppEvent, IMigrated - { - public string ContributorId { get; set; } +namespace Migrations.OldEvents; - public AppContributorPermission Permission { get; set; } +[EventType(nameof(AppContributorAssigned))] +[Obsolete("New Event introduced")] +public sealed class AppContributorAssigned : AppEvent, IMigrated +{ + public string ContributorId { get; set; } - public IEvent Migrate() - { - var result = SimpleMapper.Map(this, new AppContributorAssignedV2()); + public AppContributorPermission Permission { get; set; } - switch (Permission) - { - case AppContributorPermission.Owner: - result.Role = Role.Owner; - break; - case AppContributorPermission.Developer: - result.Role = Role.Developer; - break; - case AppContributorPermission.Editor: - result.Role = Role.Editor; - break; - } + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AppContributorAssignedV2()); - return result; + switch (Permission) + { + case AppContributorPermission.Owner: + result.Role = Role.Owner; + break; + case AppContributorPermission.Developer: + result.Role = Role.Developer; + break; + case AppContributorPermission.Editor: + result.Role = Role.Editor; + break; } + + return result; } } diff --git a/backend/src/Migrations/OldEvents/AppContributorPermission.cs b/backend/src/Migrations/OldEvents/AppContributorPermission.cs index b41e073da4..748f0441db 100644 --- a/backend/src/Migrations/OldEvents/AppContributorPermission.cs +++ b/backend/src/Migrations/OldEvents/AppContributorPermission.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[Obsolete("New Event introduced")] +public enum AppContributorPermission { - [Obsolete("New Event introduced")] - public enum AppContributorPermission - { - Owner, - Developer, - Editor - } + Owner, + Developer, + Editor } diff --git a/backend/src/Migrations/OldEvents/AppPatternAdded.cs b/backend/src/Migrations/OldEvents/AppPatternAdded.cs index cdd19d986b..1559011f6d 100644 --- a/backend/src/Migrations/OldEvents/AppPatternAdded.cs +++ b/backend/src/Migrations/OldEvents/AppPatternAdded.cs @@ -15,39 +15,38 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AppPatternAdded))] +[Obsolete("New Event introduced")] +public sealed class AppPatternAdded : AppEvent, IMigratedStateEvent { - [EventType(nameof(AppPatternAdded))] - [Obsolete("New Event introduced")] - public sealed class AppPatternAdded : AppEvent, IMigratedStateEvent - { - public DomainId PatternId { get; set; } + public DomainId PatternId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string Pattern { get; set; } + public string Pattern { get; set; } - public string? Message { get; set; } + public string? Message { get; set; } - public IEvent Migrate(AppDomainObject.State state) + public IEvent Migrate(AppDomainObject.State state) + { + var newSettings = state.Settings with { - var newSettings = state.Settings with + Patterns = new List(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) { - Patterns = new List(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) + new Pattern(Name, Pattern) { - new Pattern(Name, Pattern) - { - Message = Message - } - }.ToReadonlyList() - }; - - var newEvent = new AppSettingsUpdated - { - Settings = newSettings - }; + Message = Message + } + }.ToReadonlyList() + }; + + var newEvent = new AppSettingsUpdated + { + Settings = newSettings + }; - return SimpleMapper.Map(this, newEvent); - } + return SimpleMapper.Map(this, newEvent); } } diff --git a/backend/src/Migrations/OldEvents/AppPatternDeleted.cs b/backend/src/Migrations/OldEvents/AppPatternDeleted.cs index a28f9ab8e1..fe87f91c37 100644 --- a/backend/src/Migrations/OldEvents/AppPatternDeleted.cs +++ b/backend/src/Migrations/OldEvents/AppPatternDeleted.cs @@ -13,22 +13,21 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AppPatternDeleted))] +[Obsolete("New Event introduced")] +public sealed class AppPatternDeleted : AppEvent, IMigratedStateEvent { - [EventType(nameof(AppPatternDeleted))] - [Obsolete("New Event introduced")] - public sealed class AppPatternDeleted : AppEvent, IMigratedStateEvent - { - public DomainId PatternId { get; set; } + public DomainId PatternId { get; set; } - public IEvent Migrate(AppDomainObject.State state) + public IEvent Migrate(AppDomainObject.State state) + { + var newEvent = new AppSettingsUpdated { - var newEvent = new AppSettingsUpdated - { - Settings = state.Settings - }; + Settings = state.Settings + }; - return SimpleMapper.Map(this, newEvent); - } + return SimpleMapper.Map(this, newEvent); } } diff --git a/backend/src/Migrations/OldEvents/AppPatternUpdated.cs b/backend/src/Migrations/OldEvents/AppPatternUpdated.cs index 44b8247b0b..523cc79cc8 100644 --- a/backend/src/Migrations/OldEvents/AppPatternUpdated.cs +++ b/backend/src/Migrations/OldEvents/AppPatternUpdated.cs @@ -15,40 +15,39 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AppPatternUpdated))] +[Obsolete("New Event introduced")] +public sealed class AppPatternUpdated : AppEvent, IMigratedStateEvent { - [EventType(nameof(AppPatternUpdated))] - [Obsolete("New Event introduced")] - public sealed class AppPatternUpdated : AppEvent, IMigratedStateEvent - { - public DomainId PatternId { get; set; } + public DomainId PatternId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string Pattern { get; set; } + public string Pattern { get; set; } - public string? Message { get; set; } + public string? Message { get; set; } - public IEvent Migrate(AppDomainObject.State state) + public IEvent Migrate(AppDomainObject.State state) + { + var newSettings = new AppSettings { - var newSettings = new AppSettings + Patterns = new List(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) { - Patterns = new List(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) + new Pattern(Name, Pattern) { - new Pattern(Name, Pattern) - { - Message = Message - } - }.ToReadonlyList(), - Editors = state.Settings.Editors - }; - - var newEvent = new AppSettingsUpdated - { - Settings = newSettings - }; + Message = Message + } + }.ToReadonlyList(), + Editors = state.Settings.Editors + }; + + var newEvent = new AppSettingsUpdated + { + Settings = newSettings + }; - return SimpleMapper.Map(this, newEvent); - } + return SimpleMapper.Map(this, newEvent); } } diff --git a/backend/src/Migrations/OldEvents/AppPlanChanged.cs b/backend/src/Migrations/OldEvents/AppPlanChanged.cs index 78d27d9ddf..69ba9ab953 100644 --- a/backend/src/Migrations/OldEvents/AppPlanChanged.cs +++ b/backend/src/Migrations/OldEvents/AppPlanChanged.cs @@ -12,24 +12,23 @@ using Squidex.Infrastructure.Reflection; using AppPlanChangedV2 = Squidex.Domain.Apps.Events.Apps.AppPlanChanged; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[TypeName("AppPlanChanged")] +[Obsolete("New Event introduced")] +public sealed class AppPlanChanged : AppEvent, IMigrated { - [TypeName("AppPlanChanged")] - [Obsolete("New Event introduced")] - public sealed class AppPlanChanged : AppEvent, IMigrated - { - public string PlanId { get; set; } + public string PlanId { get; set; } - public IEvent Migrate() + public IEvent Migrate() + { + if (!string.IsNullOrWhiteSpace(PlanId)) + { + return SimpleMapper.Map(this, new AppPlanChangedV2()); + } + else { - if (!string.IsNullOrWhiteSpace(PlanId)) - { - return SimpleMapper.Map(this, new AppPlanChangedV2()); - } - else - { - return SimpleMapper.Map(this, new AppPlanReset()); - } + return SimpleMapper.Map(this, new AppPlanReset()); } } } diff --git a/backend/src/Migrations/OldEvents/AppWorkflowConfigured.cs b/backend/src/Migrations/OldEvents/AppWorkflowConfigured.cs index 07aa06c770..9f8b7affff 100644 --- a/backend/src/Migrations/OldEvents/AppWorkflowConfigured.cs +++ b/backend/src/Migrations/OldEvents/AppWorkflowConfigured.cs @@ -12,17 +12,16 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AppWorkflowConfigured))] +[Obsolete("New Event introduced")] +public sealed class AppWorkflowConfigured : AppEvent, IMigrated { - [EventType(nameof(AppWorkflowConfigured))] - [Obsolete("New Event introduced")] - public sealed class AppWorkflowConfigured : AppEvent, IMigrated - { - public Workflow Workflow { get; set; } + public Workflow Workflow { get; set; } - public IEvent Migrate() - { - return SimpleMapper.Map(this, new AppWorkflowUpdated()); - } + public IEvent Migrate() + { + return SimpleMapper.Map(this, new AppWorkflowUpdated()); } } diff --git a/backend/src/Migrations/OldEvents/AssetCreated.cs b/backend/src/Migrations/OldEvents/AssetCreated.cs index 72f1d9933d..84b8c9379a 100644 --- a/backend/src/Migrations/OldEvents/AssetCreated.cs +++ b/backend/src/Migrations/OldEvents/AssetCreated.cs @@ -12,49 +12,48 @@ using Squidex.Infrastructure.Reflection; using AssetCreatedV2 = Squidex.Domain.Apps.Events.Assets.AssetCreated; -namespace Migrations.OldEvents -{ - [EventType(nameof(AssetCreated))] - [Obsolete("New Event introduced")] - public sealed class AssetCreated : AssetEvent, IMigrated - { - public Guid ParentId { get; set; } +namespace Migrations.OldEvents; - public string FileName { get; set; } +[EventType(nameof(AssetCreated))] +[Obsolete("New Event introduced")] +public sealed class AssetCreated : AssetEvent, IMigrated +{ + public Guid ParentId { get; set; } - public string FileHash { get; set; } + public string FileName { get; set; } - public string MimeType { get; set; } + public string FileHash { get; set; } - public string Slug { get; set; } + public string MimeType { get; set; } - public long FileVersion { get; set; } + public string Slug { get; set; } - public long FileSize { get; set; } + public long FileVersion { get; set; } - public bool IsImage { get; set; } + public long FileSize { get; set; } - public int? PixelWidth { get; set; } + public bool IsImage { get; set; } - public int? PixelHeight { get; set; } + public int? PixelWidth { get; set; } - public HashSet? Tags { get; set; } + public int? PixelHeight { get; set; } - public IEvent Migrate() - { - var result = SimpleMapper.Map(this, new AssetCreatedV2()); + public HashSet? Tags { get; set; } - result.Metadata = new AssetMetadata(); + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AssetCreatedV2()); - if (IsImage && PixelWidth != null && PixelHeight != null) - { - result.Type = AssetType.Image; + result.Metadata = new AssetMetadata(); - result.Metadata.SetPixelWidth(PixelWidth.Value); - result.Metadata.SetPixelHeight(PixelHeight.Value); - } + if (IsImage && PixelWidth != null && PixelHeight != null) + { + result.Type = AssetType.Image; - return result; + result.Metadata.SetPixelWidth(PixelWidth.Value); + result.Metadata.SetPixelHeight(PixelHeight.Value); } + + return result; } } diff --git a/backend/src/Migrations/OldEvents/AssetRenamed.cs b/backend/src/Migrations/OldEvents/AssetRenamed.cs index b02ec840ba..00fc0af79d 100644 --- a/backend/src/Migrations/OldEvents/AssetRenamed.cs +++ b/backend/src/Migrations/OldEvents/AssetRenamed.cs @@ -10,17 +10,16 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AssetRenamed))] +[Obsolete("New Event introduced")] +public sealed class AssetRenamed : AssetEvent, IMigrated { - [EventType(nameof(AssetRenamed))] - [Obsolete("New Event introduced")] - public sealed class AssetRenamed : AssetEvent, IMigrated - { - public string FileName { get; set; } + public string FileName { get; set; } - public IEvent Migrate() - { - return SimpleMapper.Map(this, new AssetAnnotated()); - } + public IEvent Migrate() + { + return SimpleMapper.Map(this, new AssetAnnotated()); } } diff --git a/backend/src/Migrations/OldEvents/AssetTagged.cs b/backend/src/Migrations/OldEvents/AssetTagged.cs index 64cf86d047..6d7b94f299 100644 --- a/backend/src/Migrations/OldEvents/AssetTagged.cs +++ b/backend/src/Migrations/OldEvents/AssetTagged.cs @@ -10,17 +10,16 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(AssetTagged))] +[Obsolete("New Event introduced")] +public sealed class AssetTagged : AssetEvent, IMigrated { - [EventType(nameof(AssetTagged))] - [Obsolete("New Event introduced")] - public sealed class AssetTagged : AssetEvent, IMigrated - { - public HashSet Tags { get; set; } + public HashSet Tags { get; set; } - public IEvent Migrate() - { - return SimpleMapper.Map(this, new AssetAnnotated()); - } + public IEvent Migrate() + { + return SimpleMapper.Map(this, new AssetAnnotated()); } } diff --git a/backend/src/Migrations/OldEvents/AssetUpdated.cs b/backend/src/Migrations/OldEvents/AssetUpdated.cs index 3e9dcf4709..41951cad12 100644 --- a/backend/src/Migrations/OldEvents/AssetUpdated.cs +++ b/backend/src/Migrations/OldEvents/AssetUpdated.cs @@ -12,40 +12,39 @@ using Squidex.Infrastructure.Reflection; using AssetUpdatedV2 = Squidex.Domain.Apps.Events.Assets.AssetUpdated; -namespace Migrations.OldEvents -{ - [TypeName("AssetUpdated")] - public sealed class AssetUpdated : AssetEvent, IMigrated - { - public string MimeType { get; set; } +namespace Migrations.OldEvents; - public string FileHash { get; set; } +[TypeName("AssetUpdated")] +public sealed class AssetUpdated : AssetEvent, IMigrated +{ + public string MimeType { get; set; } - public long FileSize { get; set; } + public string FileHash { get; set; } - public long FileVersion { get; set; } + public long FileSize { get; set; } - public bool IsImage { get; set; } + public long FileVersion { get; set; } - public int? PixelWidth { get; set; } + public bool IsImage { get; set; } - public int? PixelHeight { get; set; } + public int? PixelWidth { get; set; } - public IEvent Migrate() - { - var result = SimpleMapper.Map(this, new AssetUpdatedV2()); + public int? PixelHeight { get; set; } - result.Metadata = new AssetMetadata(); + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AssetUpdatedV2()); - if (IsImage && PixelWidth != null && PixelHeight != null) - { - result.Type = AssetType.Image; + result.Metadata = new AssetMetadata(); - result.Metadata.SetPixelWidth(PixelWidth.Value); - result.Metadata.SetPixelHeight(PixelHeight.Value); - } + if (IsImage && PixelWidth != null && PixelHeight != null) + { + result.Type = AssetType.Image; - return result; + result.Metadata.SetPixelWidth(PixelWidth.Value); + result.Metadata.SetPixelHeight(PixelHeight.Value); } + + return result; } } diff --git a/backend/src/Migrations/OldEvents/ContentArchived.cs b/backend/src/Migrations/OldEvents/ContentArchived.cs index 85b269abf0..b132bd2391 100644 --- a/backend/src/Migrations/OldEvents/ContentArchived.cs +++ b/backend/src/Migrations/OldEvents/ContentArchived.cs @@ -11,15 +11,14 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(ContentArchived))] +[Obsolete("New Event introduced")] +public sealed class ContentArchived : ContentEvent, IMigrated { - [EventType(nameof(ContentArchived))] - [Obsolete("New Event introduced")] - public sealed class ContentArchived : ContentEvent, IMigrated + public IEvent Migrate() { - public IEvent Migrate() - { - return SimpleMapper.Map(this, new ContentStatusChanged { Status = Status.Archived }); - } + return SimpleMapper.Map(this, new ContentStatusChanged { Status = Status.Archived }); } } diff --git a/backend/src/Migrations/OldEvents/ContentChangesDiscarded.cs b/backend/src/Migrations/OldEvents/ContentChangesDiscarded.cs index 951a8ebab2..d1e269485f 100644 --- a/backend/src/Migrations/OldEvents/ContentChangesDiscarded.cs +++ b/backend/src/Migrations/OldEvents/ContentChangesDiscarded.cs @@ -10,15 +10,14 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(ContentChangesDiscarded))] +[Obsolete("New Event introduced")] +public sealed class ContentChangesDiscarded : ContentEvent, IMigrated { - [EventType(nameof(ContentChangesDiscarded))] - [Obsolete("New Event introduced")] - public sealed class ContentChangesDiscarded : ContentEvent, IMigrated + public IEvent Migrate() { - public IEvent Migrate() - { - return SimpleMapper.Map(this, new ContentDraftDeleted()); - } + return SimpleMapper.Map(this, new ContentDraftDeleted()); } } diff --git a/backend/src/Migrations/OldEvents/ContentChangesPublished.cs b/backend/src/Migrations/OldEvents/ContentChangesPublished.cs index 0be5a5f7dc..46283496f1 100644 --- a/backend/src/Migrations/OldEvents/ContentChangesPublished.cs +++ b/backend/src/Migrations/OldEvents/ContentChangesPublished.cs @@ -12,19 +12,18 @@ using Squidex.Infrastructure.Reflection; using ContentStatusChangedV2 = Squidex.Domain.Apps.Events.Contents.ContentStatusChanged; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(ContentChangesPublished))] +[Obsolete("New Event introduced")] +public sealed class ContentChangesPublished : ContentEvent, IMigrated { - [EventType(nameof(ContentChangesPublished))] - [Obsolete("New Event introduced")] - public sealed class ContentChangesPublished : ContentEvent, IMigrated + public IEvent Migrate() { - public IEvent Migrate() + return SimpleMapper.Map(this, new ContentStatusChangedV2 { - return SimpleMapper.Map(this, new ContentStatusChangedV2 - { - Status = Status.Published, - Change = StatusChange.Published - }); - } + Status = Status.Published, + Change = StatusChange.Published + }); } } diff --git a/backend/src/Migrations/OldEvents/ContentCreated.cs b/backend/src/Migrations/OldEvents/ContentCreated.cs index ce07e9e3e1..52b45fb8c6 100644 --- a/backend/src/Migrations/OldEvents/ContentCreated.cs +++ b/backend/src/Migrations/OldEvents/ContentCreated.cs @@ -12,26 +12,25 @@ using Squidex.Infrastructure.Reflection; using ContentCreatedV2 = Squidex.Domain.Apps.Events.Contents.ContentCreated; -namespace Migrations.OldEvents -{ - [EventType(nameof(ContentCreated))] - [Obsolete("New Event introduced")] - public sealed class ContentCreated : ContentEvent, IMigrated - { - public Status Status { get; set; } +namespace Migrations.OldEvents; - public ContentData Data { get; set; } +[EventType(nameof(ContentCreated))] +[Obsolete("New Event introduced")] +public sealed class ContentCreated : ContentEvent, IMigrated +{ + public Status Status { get; set; } - public IEvent Migrate() - { - var migrated = SimpleMapper.Map(this, new ContentCreatedV2()); + public ContentData Data { get; set; } - if (migrated.Status == default) - { - migrated.Status = Status.Draft; - } + public IEvent Migrate() + { + var migrated = SimpleMapper.Map(this, new ContentCreatedV2()); - return migrated; + if (migrated.Status == default) + { + migrated.Status = Status.Draft; } + + return migrated; } } diff --git a/backend/src/Migrations/OldEvents/ContentPublished.cs b/backend/src/Migrations/OldEvents/ContentPublished.cs index 642f2dbdf4..9d809a21b4 100644 --- a/backend/src/Migrations/OldEvents/ContentPublished.cs +++ b/backend/src/Migrations/OldEvents/ContentPublished.cs @@ -11,15 +11,14 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(ContentPublished))] +[Obsolete("New Event introduced")] +public sealed class ContentPublished : ContentEvent, IMigrated { - [EventType(nameof(ContentPublished))] - [Obsolete("New Event introduced")] - public sealed class ContentPublished : ContentEvent, IMigrated + public IEvent Migrate() { - public IEvent Migrate() - { - return SimpleMapper.Map(this, new ContentStatusChanged { Status = Status.Published }); - } + return SimpleMapper.Map(this, new ContentStatusChanged { Status = Status.Published }); } } diff --git a/backend/src/Migrations/OldEvents/ContentRestored.cs b/backend/src/Migrations/OldEvents/ContentRestored.cs index 9bb64cd08d..52f2ac80bd 100644 --- a/backend/src/Migrations/OldEvents/ContentRestored.cs +++ b/backend/src/Migrations/OldEvents/ContentRestored.cs @@ -11,15 +11,14 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(ContentRestored))] +[Obsolete("New Event introduced")] +public sealed class ContentRestored : ContentEvent, IMigrated { - [EventType(nameof(ContentRestored))] - [Obsolete("New Event introduced")] - public sealed class ContentRestored : ContentEvent, IMigrated + public IEvent Migrate() { - public IEvent Migrate() - { - return SimpleMapper.Map(this, new ContentStatusChanged { Status = Status.Draft }); - } + return SimpleMapper.Map(this, new ContentStatusChanged { Status = Status.Draft }); } } diff --git a/backend/src/Migrations/OldEvents/ContentStatusChanged.cs b/backend/src/Migrations/OldEvents/ContentStatusChanged.cs index e84015541f..d20e5f121f 100644 --- a/backend/src/Migrations/OldEvents/ContentStatusChanged.cs +++ b/backend/src/Migrations/OldEvents/ContentStatusChanged.cs @@ -12,35 +12,34 @@ using Squidex.Infrastructure.Reflection; using ContentStatusChangedV2 = Squidex.Domain.Apps.Events.Contents.ContentStatusChanged; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(ContentStatusChanged))] +[Obsolete("New Event introduced")] +public sealed class ContentStatusChanged : ContentEvent, IMigrated { - [EventType(nameof(ContentStatusChanged))] - [Obsolete("New Event introduced")] - public sealed class ContentStatusChanged : ContentEvent, IMigrated + public string Change { get; set; } + + public Status Status { get; set; } + + public IEvent Migrate() { - public string Change { get; set; } + var migrated = SimpleMapper.Map(this, new ContentStatusChangedV2()); - public Status Status { get; set; } + if (migrated.Status == default) + { + migrated.Status = Status.Draft; + } - public IEvent Migrate() + if (Enum.TryParse(Change, out var result)) + { + migrated.Change = result; + } + else { - var migrated = SimpleMapper.Map(this, new ContentStatusChangedV2()); - - if (migrated.Status == default) - { - migrated.Status = Status.Draft; - } - - if (Enum.TryParse(Change, out var result)) - { - migrated.Change = result; - } - else - { - migrated.Change = StatusChange.Change; - } - - return migrated; + migrated.Change = StatusChange.Change; } + + return migrated; } } diff --git a/backend/src/Migrations/OldEvents/ContentUnpublished.cs b/backend/src/Migrations/OldEvents/ContentUnpublished.cs index aef733d762..c579de778c 100644 --- a/backend/src/Migrations/OldEvents/ContentUnpublished.cs +++ b/backend/src/Migrations/OldEvents/ContentUnpublished.cs @@ -11,15 +11,14 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(ContentUnpublished))] +[Obsolete("New Event introduced")] +public sealed class ContentUnpublished : ContentEvent, IMigrated { - [EventType(nameof(ContentUnpublished))] - [Obsolete("New Event introduced")] - public sealed class ContentUnpublished : ContentEvent, IMigrated + public IEvent Migrate() { - public IEvent Migrate() - { - return SimpleMapper.Map(this, new ContentStatusChanged { Status = Status.Draft }); - } + return SimpleMapper.Map(this, new ContentStatusChanged { Status = Status.Draft }); } } diff --git a/backend/src/Migrations/OldEvents/ContentUpdateProposed.cs b/backend/src/Migrations/OldEvents/ContentUpdateProposed.cs index 20ca42b675..b701702695 100644 --- a/backend/src/Migrations/OldEvents/ContentUpdateProposed.cs +++ b/backend/src/Migrations/OldEvents/ContentUpdateProposed.cs @@ -11,26 +11,25 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents -{ - [EventType(nameof(ContentUpdateProposed))] - [Obsolete("New Event introduced")] - public sealed class ContentUpdateProposed : ContentEvent, IMigrated - { - public ContentData Data { get; set; } +namespace Migrations.OldEvents; - public IEvent Migrate() - { - var migrated = SimpleMapper.Map(this, new ContentDraftCreated()); +[EventType(nameof(ContentUpdateProposed))] +[Obsolete("New Event introduced")] +public sealed class ContentUpdateProposed : ContentEvent, IMigrated +{ + public ContentData Data { get; set; } - migrated.MigratedData = Data; + public IEvent Migrate() + { + var migrated = SimpleMapper.Map(this, new ContentDraftCreated()); - if (migrated.Status == default) - { - migrated.Status = Status.Draft; - } + migrated.MigratedData = Data; - return migrated; + if (migrated.Status == default) + { + migrated.Status = Status.Draft; } + + return migrated; } } diff --git a/backend/src/Migrations/OldEvents/NoopConventEvent.cs b/backend/src/Migrations/OldEvents/NoopConventEvent.cs index 8396616326..98c0dc6c82 100644 --- a/backend/src/Migrations/OldEvents/NoopConventEvent.cs +++ b/backend/src/Migrations/OldEvents/NoopConventEvent.cs @@ -8,10 +8,9 @@ using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[TypeName(nameof(NoopConventEvent))] +public sealed class NoopConventEvent : ContentEvent { - [TypeName(nameof(NoopConventEvent))] - public sealed class NoopConventEvent : ContentEvent - { - } } diff --git a/backend/src/Migrations/OldEvents/NoopEvent.cs b/backend/src/Migrations/OldEvents/NoopEvent.cs index 1d46f59270..7ff163837c 100644 --- a/backend/src/Migrations/OldEvents/NoopEvent.cs +++ b/backend/src/Migrations/OldEvents/NoopEvent.cs @@ -8,10 +8,9 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[TypeName(nameof(NoopEvent))] +public sealed class NoopEvent : SquidexEvent { - [TypeName(nameof(NoopEvent))] - public sealed class NoopEvent : SquidexEvent - { - } } diff --git a/backend/src/Migrations/OldEvents/SchemaCreated.cs b/backend/src/Migrations/OldEvents/SchemaCreated.cs index ec8f4c8ecd..5133029c0f 100644 --- a/backend/src/Migrations/OldEvents/SchemaCreated.cs +++ b/backend/src/Migrations/OldEvents/SchemaCreated.cs @@ -14,70 +14,69 @@ using SchemaCreatedV2 = Squidex.Domain.Apps.Events.Schemas.SchemaCreated; using SchemaFields = System.Collections.Generic.List; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(SchemaCreated))] +[Obsolete("New Event introduced")] +public sealed class SchemaCreated : SchemaEvent, IMigrated { - [EventType(nameof(SchemaCreated))] - [Obsolete("New Event introduced")] - public sealed class SchemaCreated : SchemaEvent, IMigrated - { - public string Name { get; set; } + public string Name { get; set; } - public bool Singleton { get; set; } + public bool Singleton { get; set; } - public bool Publish { get; set; } + public bool Publish { get; set; } - public SchemaFields Fields { get; set; } + public SchemaFields Fields { get; set; } - public SchemaProperties Properties { get; set; } + public SchemaProperties Properties { get; set; } - public IEvent Migrate() - { - var schema = new Schema(Name, Properties, Singleton ? SchemaType.Singleton : SchemaType.Default); + public IEvent Migrate() + { + var schema = new Schema(Name, Properties, Singleton ? SchemaType.Singleton : SchemaType.Default); - if (Publish) - { - schema = schema.Publish(); - } + if (Publish) + { + schema = schema.Publish(); + } - var totalFields = 0; + var totalFields = 0; - if (Fields != null) + if (Fields != null) + { + foreach (var eventField in Fields) { - foreach (var eventField in Fields) - { - totalFields++; + totalFields++; - var partitioning = Partitioning.FromString(eventField.Partitioning); + var partitioning = Partitioning.FromString(eventField.Partitioning); - var field = - eventField.Properties.CreateRootField( - totalFields, - eventField.Name, partitioning, - eventField); + var field = + eventField.Properties.CreateRootField( + totalFields, + eventField.Name, partitioning, + eventField); - if (field is ArrayField arrayField && eventField.Nested?.Length > 0) + if (field is ArrayField arrayField && eventField.Nested?.Length > 0) + { + foreach (var nestedEventField in eventField.Nested) { - foreach (var nestedEventField in eventField.Nested) - { - totalFields++; - - var nestedField = - nestedEventField.Properties.CreateNestedField( - totalFields, - nestedEventField.Name, - nestedEventField); + totalFields++; - arrayField = arrayField.AddField(nestedField); - } + var nestedField = + nestedEventField.Properties.CreateNestedField( + totalFields, + nestedEventField.Name, + nestedEventField); - field = arrayField; + arrayField = arrayField.AddField(nestedField); } - schema = schema.AddField(field); + field = arrayField; } - } - return SimpleMapper.Map(this, new SchemaCreatedV2 { Schema = schema }); + schema = schema.AddField(field); + } } + + return SimpleMapper.Map(this, new SchemaCreatedV2 { Schema = schema }); } } diff --git a/backend/src/Migrations/OldEvents/ScriptsConfigured.cs b/backend/src/Migrations/OldEvents/ScriptsConfigured.cs index 0eab6b129e..3309286d90 100644 --- a/backend/src/Migrations/OldEvents/ScriptsConfigured.cs +++ b/backend/src/Migrations/OldEvents/ScriptsConfigured.cs @@ -12,52 +12,51 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(ScriptsConfigured))] +[Obsolete("New Event introduced")] +public sealed class ScriptsConfigured : SchemaEvent, IMigrated { - [EventType(nameof(ScriptsConfigured))] - [Obsolete("New Event introduced")] - public sealed class ScriptsConfigured : SchemaEvent, IMigrated + public string ScriptQuery { get; set; } + + public string ScriptCreate { get; set; } + + public string ScriptUpdate { get; set; } + + public string ScriptDelete { get; set; } + + public string ScriptChange { get; set; } + + public IEvent Migrate() { - public string ScriptQuery { get; set; } + var scripts = new SchemaScripts(); - public string ScriptCreate { get; set; } + if (!string.IsNullOrWhiteSpace(ScriptQuery)) + { + scripts = scripts with { Query = ScriptQuery }; + } - public string ScriptUpdate { get; set; } + if (!string.IsNullOrWhiteSpace(ScriptCreate)) + { + scripts = scripts with { Create = ScriptCreate }; + } - public string ScriptDelete { get; set; } + if (!string.IsNullOrWhiteSpace(ScriptUpdate)) + { + scripts = scripts with { Update = ScriptUpdate }; + } - public string ScriptChange { get; set; } + if (!string.IsNullOrWhiteSpace(ScriptDelete)) + { + scripts = scripts with { Delete = ScriptDelete }; + } - public IEvent Migrate() + if (!string.IsNullOrWhiteSpace(ScriptChange)) { - var scripts = new SchemaScripts(); - - if (!string.IsNullOrWhiteSpace(ScriptQuery)) - { - scripts = scripts with { Query = ScriptQuery }; - } - - if (!string.IsNullOrWhiteSpace(ScriptCreate)) - { - scripts = scripts with { Create = ScriptCreate }; - } - - if (!string.IsNullOrWhiteSpace(ScriptUpdate)) - { - scripts = scripts with { Update = ScriptUpdate }; - } - - if (!string.IsNullOrWhiteSpace(ScriptDelete)) - { - scripts = scripts with { Delete = ScriptDelete }; - } - - if (!string.IsNullOrWhiteSpace(ScriptChange)) - { - scripts = scripts with { Change = ScriptChange }; - } - - return SimpleMapper.Map(this, new SchemaScriptsConfigured { Scripts = scripts }); + scripts = scripts with { Change = ScriptChange }; } + + return SimpleMapper.Map(this, new SchemaScriptsConfigured { Scripts = scripts }); } } diff --git a/backend/src/Migrations/OldEvents/WebhookAdded.cs b/backend/src/Migrations/OldEvents/WebhookAdded.cs index 676c8c508d..72b64ac7d6 100644 --- a/backend/src/Migrations/OldEvents/WebhookAdded.cs +++ b/backend/src/Migrations/OldEvents/WebhookAdded.cs @@ -8,16 +8,15 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(WebhookAdded))] +[Obsolete("New Event introduced")] +public sealed class WebhookAdded : SchemaEvent { - [EventType(nameof(WebhookAdded))] - [Obsolete("New Event introduced")] - public sealed class WebhookAdded : SchemaEvent - { - public Guid Id { get; set; } + public Guid Id { get; set; } - public Uri Url { get; set; } + public Uri Url { get; set; } - public string SharedSecret { get; set; } - } + public string SharedSecret { get; set; } } diff --git a/backend/src/Migrations/OldEvents/WebhookDeleted.cs b/backend/src/Migrations/OldEvents/WebhookDeleted.cs index 9596a0a288..978910c203 100644 --- a/backend/src/Migrations/OldEvents/WebhookDeleted.cs +++ b/backend/src/Migrations/OldEvents/WebhookDeleted.cs @@ -8,12 +8,11 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Migrations.OldEvents +namespace Migrations.OldEvents; + +[EventType(nameof(WebhookDeleted))] +[Obsolete("New Event introduced")] +public sealed class WebhookDeleted : SchemaEvent { - [EventType(nameof(WebhookDeleted))] - [Obsolete("New Event introduced")] - public sealed class WebhookDeleted : SchemaEvent - { - public Guid Id { get; set; } - } + public Guid Id { get; set; } } diff --git a/backend/src/Migrations/OldTriggers/AssetChangedTrigger.cs b/backend/src/Migrations/OldTriggers/AssetChangedTrigger.cs index 4b0c096e0e..254438c7c9 100644 --- a/backend/src/Migrations/OldTriggers/AssetChangedTrigger.cs +++ b/backend/src/Migrations/OldTriggers/AssetChangedTrigger.cs @@ -11,60 +11,59 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldTriggers +namespace Migrations.OldTriggers; + +[TypeName(nameof(AssetChangedTrigger))] +public sealed record AssetChangedTrigger : RuleTrigger, IMigrated { - [TypeName(nameof(AssetChangedTrigger))] - public sealed record AssetChangedTrigger : RuleTrigger, IMigrated - { - public bool SendCreate { get; set; } + public bool SendCreate { get; set; } + + public bool SendUpdate { get; set; } - public bool SendUpdate { get; set; } + public bool SendRename { get; set; } - public bool SendRename { get; set; } + public bool SendDelete { get; set; } + + public override T Accept(IRuleTriggerVisitor visitor) + { + throw new NotSupportedException(); + } - public bool SendDelete { get; set; } + public RuleTrigger Migrate() + { + var conditions = new List(); - public override T Accept(IRuleTriggerVisitor visitor) + if (SendCreate) { - throw new NotSupportedException(); + conditions.Add($"event.type == '{EnrichedAssetEventType.Created}'"); } - public RuleTrigger Migrate() + if (SendUpdate) { - var conditions = new List(); - - if (SendCreate) - { - conditions.Add($"event.type == '{EnrichedAssetEventType.Created}'"); - } - - if (SendUpdate) - { - conditions.Add($"event.type == '{EnrichedAssetEventType.Updated}'"); - } - - if (SendRename) - { - conditions.Add($"event.type == '{EnrichedAssetEventType.Annotated}'"); - } + conditions.Add($"event.type == '{EnrichedAssetEventType.Updated}'"); + } - if (SendDelete) - { - conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'"); - } + if (SendRename) + { + conditions.Add($"event.type == '{EnrichedAssetEventType.Annotated}'"); + } - var condition = string.Empty; + if (SendDelete) + { + conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'"); + } - if (conditions.Count == 0) - { - condition = "false"; - } - else if (condition.Length < 4) - { - condition = string.Join(" || ", conditions); - } + var condition = string.Empty; - return new AssetChangedTriggerV2 { Condition = condition }; + if (conditions.Count == 0) + { + condition = "false"; + } + else if (condition.Length < 4) + { + condition = string.Join(" || ", conditions); } + + return new AssetChangedTriggerV2 { Condition = condition }; } } diff --git a/backend/src/Migrations/OldTriggers/ContentChangedTrigger.cs b/backend/src/Migrations/OldTriggers/ContentChangedTrigger.cs index 0acb250b51..9eacc08afa 100644 --- a/backend/src/Migrations/OldTriggers/ContentChangedTrigger.cs +++ b/backend/src/Migrations/OldTriggers/ContentChangedTrigger.cs @@ -11,25 +11,24 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Migrations.OldTriggers +namespace Migrations.OldTriggers; + +[TypeName(nameof(ContentChangedTrigger))] +public sealed record ContentChangedTrigger : RuleTrigger, IMigrated { - [TypeName(nameof(ContentChangedTrigger))] - public sealed record ContentChangedTrigger : RuleTrigger, IMigrated - { - public ReadonlyList Schemas { get; set; } + public ReadonlyList Schemas { get; set; } - public bool HandleAll { get; set; } + public bool HandleAll { get; set; } - public override T Accept(IRuleTriggerVisitor visitor) - { - throw new NotSupportedException(); - } + public override T Accept(IRuleTriggerVisitor visitor) + { + throw new NotSupportedException(); + } - public RuleTrigger Migrate() - { - var schemas = Schemas.Select(x => x.Migrate()).ToReadonlyList(); + public RuleTrigger Migrate() + { + var schemas = Schemas.Select(x => x.Migrate()).ToReadonlyList(); - return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas }; - } + return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas }; } } diff --git a/backend/src/Migrations/OldTriggers/ContentChangedTriggerSchema.cs b/backend/src/Migrations/OldTriggers/ContentChangedTriggerSchema.cs index cbb6014755..bb14788388 100644 --- a/backend/src/Migrations/OldTriggers/ContentChangedTriggerSchema.cs +++ b/backend/src/Migrations/OldTriggers/ContentChangedTriggerSchema.cs @@ -9,69 +9,68 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure; -namespace Migrations.OldTriggers +namespace Migrations.OldTriggers; + +public sealed class ContentChangedTriggerSchema { - public sealed class ContentChangedTriggerSchema - { - public Guid SchemaId { get; set; } + public Guid SchemaId { get; set; } + + public bool SendCreate { get; set; } + + public bool SendUpdate { get; set; } + + public bool SendDelete { get; set; } + + public bool SendPublish { get; set; } - public bool SendCreate { get; set; } + public bool SendUnpublish { get; set; } - public bool SendUpdate { get; set; } + public bool SendArchived { get; set; } + + public bool SendRestore { get; set; } + + public ContentChangedTriggerSchemaV2 Migrate() + { + var conditions = new List(); + + if (SendCreate) + { + conditions.Add($"event.type == '{EnrichedContentEventType.Created}'"); + } - public bool SendDelete { get; set; } + if (SendUpdate) + { + conditions.Add($"event.type == '{EnrichedContentEventType.Updated}'"); + } - public bool SendPublish { get; set; } + if (SendPublish) + { + conditions.Add($"event.type == '{EnrichedContentEventType.Published}'"); + } - public bool SendUnpublish { get; set; } + if (SendArchived) + { + conditions.Add($"event.status == 'Archived'"); + } - public bool SendArchived { get; set; } + if (SendDelete) + { + conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'"); + } - public bool SendRestore { get; set; } + var condition = string.Empty; - public ContentChangedTriggerSchemaV2 Migrate() + if (conditions.Count == 0 && condition.Length < 7) + { + condition = "false"; + } + else if (condition.Length < 7) { - var conditions = new List(); - - if (SendCreate) - { - conditions.Add($"event.type == '{EnrichedContentEventType.Created}'"); - } - - if (SendUpdate) - { - conditions.Add($"event.type == '{EnrichedContentEventType.Updated}'"); - } - - if (SendPublish) - { - conditions.Add($"event.type == '{EnrichedContentEventType.Published}'"); - } - - if (SendArchived) - { - conditions.Add($"event.status == 'Archived'"); - } - - if (SendDelete) - { - conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'"); - } - - var condition = string.Empty; - - if (conditions.Count == 0 && condition.Length < 7) - { - condition = "false"; - } - else if (condition.Length < 7) - { - condition = string.Join(" || ", conditions); - } - - var schemaId = DomainId.Create(SchemaId); - - return new ContentChangedTriggerSchemaV2 { SchemaId = schemaId, Condition = condition }; + condition = string.Join(" || ", conditions); } + + var schemaId = DomainId.Create(SchemaId); + + return new ContentChangedTriggerSchemaV2 { SchemaId = schemaId, Condition = condition }; } } diff --git a/backend/src/Migrations/RebuildOptions.cs b/backend/src/Migrations/RebuildOptions.cs index efa3138405..c43d09304c 100644 --- a/backend/src/Migrations/RebuildOptions.cs +++ b/backend/src/Migrations/RebuildOptions.cs @@ -5,29 +5,28 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Migrations +namespace Migrations; + +public sealed class RebuildOptions { - public sealed class RebuildOptions - { - public bool Apps { get; set; } + public bool Apps { get; set; } - public bool Assets { get; set; } + public bool Assets { get; set; } - public bool AssetFiles { get; set; } + public bool AssetFiles { get; set; } - public bool Contents { get; set; } + public bool Contents { get; set; } - public bool Indexes { get; set; } + public bool Indexes { get; set; } - public bool Rules { get; set; } + public bool Rules { get; set; } - public bool Schemas { get; set; } + public bool Schemas { get; set; } - public int BatchSize { get; set; } = 100; + public int BatchSize { get; set; } = 100; - public int CalculateBatchSize() - { - return Math.Max(10, Math.Min(1000, BatchSize)); - } + public int CalculateBatchSize() + { + return Math.Max(10, Math.Min(1000, BatchSize)); } } diff --git a/backend/src/Migrations/RebuildRunner.cs b/backend/src/Migrations/RebuildRunner.cs index c2764e37a1..86efb9fb13 100644 --- a/backend/src/Migrations/RebuildRunner.cs +++ b/backend/src/Migrations/RebuildRunner.cs @@ -9,59 +9,58 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure.Commands; -namespace Migrations +namespace Migrations; + +public sealed class RebuildRunner { - public sealed class RebuildRunner + private readonly RebuildFiles rebuildFiles; + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; + + public RebuildRunner( + IOptions rebuildOptions, + Rebuilder rebuilder, + RebuildFiles rebuildFiles) + { + this.rebuildFiles = rebuildFiles; + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } + + public async Task RunAsync( + CancellationToken ct) { - private readonly RebuildFiles rebuildFiles; - private readonly Rebuilder rebuilder; - private readonly RebuildOptions rebuildOptions; + var batchSize = rebuildOptions.CalculateBatchSize(); - public RebuildRunner( - IOptions rebuildOptions, - Rebuilder rebuilder, - RebuildFiles rebuildFiles) + if (rebuildOptions.Apps) { - this.rebuildFiles = rebuildFiles; - this.rebuilder = rebuilder; - this.rebuildOptions = rebuildOptions.Value; + await rebuilder.RebuildAppsAsync(batchSize, ct); } - public async Task RunAsync( - CancellationToken ct) + if (rebuildOptions.Schemas) { - var batchSize = rebuildOptions.CalculateBatchSize(); - - if (rebuildOptions.Apps) - { - await rebuilder.RebuildAppsAsync(batchSize, ct); - } - - if (rebuildOptions.Schemas) - { - await rebuilder.RebuildSchemasAsync(batchSize, ct); - } + await rebuilder.RebuildSchemasAsync(batchSize, ct); + } - if (rebuildOptions.Rules) - { - await rebuilder.RebuildRulesAsync(batchSize, ct); - } + if (rebuildOptions.Rules) + { + await rebuilder.RebuildRulesAsync(batchSize, ct); + } - if (rebuildOptions.Assets) - { - await rebuilder.RebuildAssetsAsync(batchSize, ct); - await rebuilder.RebuildAssetFoldersAsync(batchSize, ct); - } + if (rebuildOptions.Assets) + { + await rebuilder.RebuildAssetsAsync(batchSize, ct); + await rebuilder.RebuildAssetFoldersAsync(batchSize, ct); + } - if (rebuildOptions.AssetFiles) - { - await rebuildFiles.RepairAsync(ct); - } + if (rebuildOptions.AssetFiles) + { + await rebuildFiles.RepairAsync(ct); + } - if (rebuildOptions.Contents) - { - await rebuilder.RebuildContentAsync(batchSize, ct); - } + if (rebuildOptions.Contents) + { + await rebuilder.RebuildContentAsync(batchSize, ct); } } } diff --git a/backend/src/Migrations/RebuilderExtensions.cs b/backend/src/Migrations/RebuilderExtensions.cs index 19323e7857..1dc8462bd5 100644 --- a/backend/src/Migrations/RebuilderExtensions.cs +++ b/backend/src/Migrations/RebuilderExtensions.cs @@ -12,46 +12,45 @@ using Squidex.Domain.Apps.Entities.Schemas.DomainObject; using Squidex.Infrastructure.Commands; -namespace Migrations +namespace Migrations; + +public static class RebuilderExtensions { - public static class RebuilderExtensions + private const double AllowedErrorRate = 0.02; + + public static Task RebuildAppsAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) + { + return rebuilder.RebuildAsync("^app\\-", batchSize, AllowedErrorRate, ct); + } + + public static Task RebuildSchemasAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) + { + return rebuilder.RebuildAsync("^schema\\-", batchSize, AllowedErrorRate, ct); + } + + public static Task RebuildRulesAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) + { + return rebuilder.RebuildAsync("^rule\\-", batchSize, AllowedErrorRate, ct); + } + + public static Task RebuildAssetsAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) + { + return rebuilder.RebuildAsync("^asset\\-", batchSize, AllowedErrorRate, ct); + } + + public static Task RebuildAssetFoldersAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) + { + return rebuilder.RebuildAsync("^assetFolder\\-", batchSize, AllowedErrorRate, ct); + } + + public static Task RebuildContentAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) { - private const double AllowedErrorRate = 0.02; - - public static Task RebuildAppsAsync(this Rebuilder rebuilder, int batchSize, - CancellationToken ct = default) - { - return rebuilder.RebuildAsync("^app\\-", batchSize, AllowedErrorRate, ct); - } - - public static Task RebuildSchemasAsync(this Rebuilder rebuilder, int batchSize, - CancellationToken ct = default) - { - return rebuilder.RebuildAsync("^schema\\-", batchSize, AllowedErrorRate, ct); - } - - public static Task RebuildRulesAsync(this Rebuilder rebuilder, int batchSize, - CancellationToken ct = default) - { - return rebuilder.RebuildAsync("^rule\\-", batchSize, AllowedErrorRate, ct); - } - - public static Task RebuildAssetsAsync(this Rebuilder rebuilder, int batchSize, - CancellationToken ct = default) - { - return rebuilder.RebuildAsync("^asset\\-", batchSize, AllowedErrorRate, ct); - } - - public static Task RebuildAssetFoldersAsync(this Rebuilder rebuilder, int batchSize, - CancellationToken ct = default) - { - return rebuilder.RebuildAsync("^assetFolder\\-", batchSize, AllowedErrorRate, ct); - } - - public static Task RebuildContentAsync(this Rebuilder rebuilder, int batchSize, - CancellationToken ct = default) - { - return rebuilder.RebuildAsync("^content\\-", batchSize, AllowedErrorRate, ct); - } + return rebuilder.RebuildAsync("^content\\-", batchSize, AllowedErrorRate, ct); } } diff --git a/backend/src/Migrations/SquidexMigrations.cs b/backend/src/Migrations/SquidexMigrations.cs index e2016b421d..56dfcb6cea 100644 --- a/backend/src/Migrations/SquidexMigrations.cs +++ b/backend/src/Migrations/SquidexMigrations.cs @@ -9,10 +9,9 @@ #pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. -namespace Migrations +namespace Migrations; + +public sealed class SquidexMigrations { - public sealed class SquidexMigrations - { - public static readonly Assembly Assembly = typeof(SquidexMigrations).Assembly; - } + public static readonly Assembly Assembly = typeof(SquidexMigrations).Assembly; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs index 5ffc163bef..e277f0e502 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -9,20 +9,19 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed record AppClient(string Name, string Secret) { - public sealed record AppClient(string Name, string Secret) - { - public string Name { get; init; } = Guard.NotNullOrEmpty(Name); + public string Name { get; init; } = Guard.NotNullOrEmpty(Name); - public string Secret { get; } = Guard.NotNullOrEmpty(Secret); + public string Secret { get; } = Guard.NotNullOrEmpty(Secret); - public string Role { get; init; } = "Editor"; + public string Role { get; init; } = "Editor"; - public long ApiCallsLimit { get; init; } + public long ApiCallsLimit { get; init; } - public long ApiTrafficLimit { get; init; } + public long ApiTrafficLimit { get; init; } - public bool AllowAnonymous { get; init; } - } + public bool AllowAnonymous { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index 25d6e1cb78..177ee0f513 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -9,97 +9,96 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed class AppClients : ReadonlyDictionary { - public sealed class AppClients : ReadonlyDictionary + public static readonly AppClients Empty = new AppClients(); + + private AppClients() + { + } + + public AppClients(IDictionary inner) + : base(inner) + { + } + + [Pure] + public AppClients Revoke(string id) { - public static readonly AppClients Empty = new AppClients(); + Guard.NotNullOrEmpty(id); - private AppClients() + if (!this.TryRemove(id, out var updated)) { + return this; } - public AppClients(IDictionary inner) - : base(inner) + return new AppClients(updated); + } + + [Pure] + public AppClients Add(string id, string secret, string? role = null) + { + Guard.NotNullOrEmpty(id); + Guard.NotNullOrEmpty(secret); + + var newClient = new AppClient(id, secret) { - } + Role = role.Or(Role.Editor) + }; - [Pure] - public AppClients Revoke(string id) + if (!this.TryAdd(id, newClient, out var updated)) { - Guard.NotNullOrEmpty(id); + return this; + } - if (!this.TryRemove(id, out var updated)) - { - return this; - } + return new AppClients(updated); + } + + [Pure] + public AppClients Update(string id, string? name = null, string? role = null, + long? apiCallsLimit = null, + long? apiTrafficLimit = null, + bool? allowAnonymous = false) + { + Guard.NotNullOrEmpty(id); - return new AppClients(updated); + if (!TryGetValue(id, out var client)) + { + return this; } - [Pure] - public AppClients Add(string id, string secret, string? role = null) + var newClient = client with { - Guard.NotNullOrEmpty(id); - Guard.NotNullOrEmpty(secret); + AllowAnonymous = allowAnonymous ?? client.AllowAnonymous + }; - var newClient = new AppClient(id, secret) - { - Role = role.Or(Role.Editor) - }; + if (!string.IsNullOrWhiteSpace(name)) + { + newClient = newClient with { Name = name }; + } - if (!this.TryAdd(id, newClient, out var updated)) - { - return this; - } + if (!string.IsNullOrWhiteSpace(role)) + { + newClient = newClient with { Role = role }; + } + + if (apiCallsLimit >= 0) + { + newClient = newClient with { ApiCallsLimit = apiCallsLimit.Value }; + } - return new AppClients(updated); + if (apiTrafficLimit >= 0) + { + newClient = newClient with { ApiTrafficLimit = apiTrafficLimit.Value }; } - [Pure] - public AppClients Update(string id, string? name = null, string? role = null, - long? apiCallsLimit = null, - long? apiTrafficLimit = null, - bool? allowAnonymous = false) + if (!this.TrySet(id, newClient, out var updated)) { - Guard.NotNullOrEmpty(id); - - if (!TryGetValue(id, out var client)) - { - return this; - } - - var newClient = client with - { - AllowAnonymous = allowAnonymous ?? client.AllowAnonymous - }; - - if (!string.IsNullOrWhiteSpace(name)) - { - newClient = newClient with { Name = name }; - } - - if (!string.IsNullOrWhiteSpace(role)) - { - newClient = newClient with { Role = role }; - } - - if (apiCallsLimit >= 0) - { - newClient = newClient with { ApiCallsLimit = apiCallsLimit.Value }; - } - - if (apiTrafficLimit >= 0) - { - newClient = newClient with { ApiTrafficLimit = apiTrafficLimit.Value }; - } - - if (!this.TrySet(id, newClient, out var updated)) - { - return this; - } - - return new AppClients(updated); + return this; } + + return new AppClients(updated); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs index 40156590a4..0090f271bf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs @@ -9,12 +9,11 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed record AppImage(string MimeType, string? Etag = null) { - public sealed record AppImage(string MimeType, string? Etag = null) - { - public string MimeType { get; } = Guard.NotNullOrEmpty(MimeType); + public string MimeType { get; } = Guard.NotNullOrEmpty(MimeType); - public string Etag { get; } = Etag ?? RandomHash.Simple(); - } + public string Etag { get; } = Etag ?? RandomHash.Simple(); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppSettings.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppSettings.cs index 39bafc062f..8505af3a2d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppSettings.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppSettings.cs @@ -7,18 +7,17 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed record AppSettings { - public sealed record AppSettings - { - public static readonly AppSettings Empty = new AppSettings(); + public static readonly AppSettings Empty = new AppSettings(); - public ReadonlyList Patterns { get; init; } = ReadonlyList.Empty(); + public ReadonlyList Patterns { get; init; } = ReadonlyList.Empty(); - public ReadonlyList Editors { get; init; } = ReadonlyList.Empty(); + public ReadonlyList Editors { get; init; } = ReadonlyList.Empty(); - public bool HideScheduler { get; init; } + public bool HideScheduler { get; init; } - public bool HideDateTimeModeButton { get; init; } - } + public bool HideDateTimeModeButton { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs index 292cb3e0eb..d6211abf09 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed record Editor(string Name, string Url); -} +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed record Editor(string Name, string Url); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguageConfigSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguageConfigSurrogate.cs index eb17218c42..0112e26366 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguageConfigSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguageConfigSurrogate.cs @@ -8,31 +8,30 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Apps.Json +namespace Squidex.Domain.Apps.Core.Apps.Json; + +public sealed class LanguageConfigSurrogate : ISurrogate { - public sealed class LanguageConfigSurrogate : ISurrogate + public Language[]? Fallback { get; set; } + + public bool IsOptional { get; set; } + + public void FromSource(LanguageConfig source) { - public Language[]? Fallback { get; set; } + IsOptional = source.IsOptional; - public bool IsOptional { get; set; } + Fallback = source.Fallbacks.ToArray(); + } - public void FromSource(LanguageConfig source) + public LanguageConfig ToSource() + { + if (!IsOptional && (Fallback == null || Fallback.Length == 0)) { - IsOptional = source.IsOptional; - - Fallback = source.Fallbacks.ToArray(); + return LanguageConfig.Default; } - - public LanguageConfig ToSource() + else { - if (!IsOptional && (Fallback == null || Fallback.Length == 0)) - { - return LanguageConfig.Default; - } - else - { - return new LanguageConfig(IsOptional, ReadonlyList.Create(Fallback)); - } + return new LanguageConfig(IsOptional, ReadonlyList.Create(Fallback)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs index 8c5d869a47..eee3e28015 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs @@ -7,35 +7,34 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Apps.Json +namespace Squidex.Domain.Apps.Core.Apps.Json; + +public sealed class LanguagesConfigSurrogate : ISurrogate { - public sealed class LanguagesConfigSurrogate : ISurrogate - { - public Dictionary Languages { get; set; } + public Dictionary Languages { get; set; } - public string Master { get; set; } + public string Master { get; set; } - public void FromSource(LanguagesConfig source) + public void FromSource(LanguagesConfig source) + { + Languages = source.Languages.ToDictionary(x => x.Key, source => { - Languages = source.Languages.ToDictionary(x => x.Key, source => - { - var surrogate = new LanguageConfigSurrogate(); + var surrogate = new LanguageConfigSurrogate(); - surrogate.FromSource(source.Value); + surrogate.FromSource(source.Value); - return surrogate; - }); + return surrogate; + }); - Master = source.Master; - } + Master = source.Master; + } - public LanguagesConfig ToSource() - { - var languages = Languages.ToDictionary(x => x.Key, x => x.Value.ToSource()); + public LanguagesConfig ToSource() + { + var languages = Languages.ToDictionary(x => x.Key, x => x.Value.ToSource()); - var master = Master ?? languages.Keys.First(); + var master = Master ?? languages.Keys.First(); - return new LanguagesConfig(languages, master); - } + return new LanguagesConfig(languages, master); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs index 9a5b7a76b1..fe82412823 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs @@ -9,66 +9,65 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Security; -namespace Squidex.Domain.Apps.Core.Apps.Json +namespace Squidex.Domain.Apps.Core.Apps.Json; + +public sealed class RolesSurrogate : Dictionary, ISurrogate { - public sealed class RolesSurrogate : Dictionary, ISurrogate + public void FromSource(Roles source) { - public void FromSource(Roles source) + foreach (var customRole in source.Custom) { - foreach (var customRole in source.Custom) - { - var permissions = new JsonArray(); + var permissions = new JsonArray(); - foreach (var permission in customRole.Permissions) - { - permissions.Add(permission.Id); - } + foreach (var permission in customRole.Permissions) + { + permissions.Add(permission.Id); + } - var role = - new JsonObject() - .Add("permissions", permissions) - .Add("properties", customRole.Properties); + var role = + new JsonObject() + .Add("permissions", permissions) + .Add("properties", customRole.Properties); - Add(customRole.Name, role); - } + Add(customRole.Name, role); } + } - public Roles ToSource() + public Roles ToSource() + { + if (Count == 0) { - if (Count == 0) - { - return Roles.Empty; - } + return Roles.Empty; + } - return new Roles(this.ToDictionary(x => x.Key, x => - { - var (key, value) = x; + return new Roles(this.ToDictionary(x => x.Key, x => + { + var (key, value) = x; - var properties = new JsonObject(); - var permissions = PermissionSet.Empty; + var properties = new JsonObject(); + var permissions = PermissionSet.Empty; - if (value.Value is JsonArray a) + if (value.Value is JsonArray a) + { + if (a.Count > 0) { - if (a.Count > 0) - { - permissions = new PermissionSet(a.Select(x => x.Value).OfType()); - } + permissions = new PermissionSet(a.Select(x => x.Value).OfType()); } - else if (value.Value is JsonObject o) + } + else if (value.Value is JsonObject o) + { + if (o.TryGetValue("permissions", out var found) && found.Value is JsonArray permissionArray) { - if (o.TryGetValue("permissions", out var found) && found.Value is JsonArray permissionArray) - { - permissions = new PermissionSet(permissionArray.Select(x => x.Value).OfType()); - } + permissions = new PermissionSet(permissionArray.Select(x => x.Value).OfType()); + } - if (o.TryGetValue("properties", out found) && found.Value is JsonObject propertiesObject) - { - properties = propertiesObject; - } + if (o.TryGetValue("properties", out found) && found.Value is JsonObject propertiesObject) + { + properties = propertiesObject; } + } - return new Role(key, permissions, properties); - })); - } + return new Role(key, permissions, properties); + })); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs index afea555cf9..caef4b336e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs @@ -10,24 +10,23 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed record LanguageConfig(bool IsOptional = false, ReadonlyList? Fallbacks = null) { - public sealed record LanguageConfig(bool IsOptional = false, ReadonlyList? Fallbacks = null) - { - public static readonly LanguageConfig Default = new LanguageConfig(); + public static readonly LanguageConfig Default = new LanguageConfig(); - public ReadonlyList Fallbacks { get; } = Fallbacks ?? ReadonlyList.Empty(); + public ReadonlyList Fallbacks { get; } = Fallbacks ?? ReadonlyList.Empty(); - internal LanguageConfig Cleanup(string self, IReadOnlyDictionary allowed) + internal LanguageConfig Cleanup(string self, IReadOnlyDictionary allowed) + { + if (Fallbacks.Any(x => x.Iso2Code == self) || Fallbacks.Any(x => !allowed.ContainsKey(x))) { - if (Fallbacks.Any(x => x.Iso2Code == self) || Fallbacks.Any(x => !allowed.ContainsKey(x))) - { - var cleaned = Fallbacks.Where(x => x.Iso2Code != self && allowed.ContainsKey(x.Iso2Code)).ToReadonlyList(); + var cleaned = Fallbacks.Where(x => x.Iso2Code != self && allowed.ContainsKey(x.Iso2Code)).ToReadonlyList(); - return new LanguageConfig(IsOptional, cleaned); - } - - return this; + return new LanguageConfig(IsOptional, cleaned); } + + return this; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index 87de5d0038..b87b0cee60 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -9,200 +9,199 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Apps -{ - public sealed class LanguagesConfig : IFieldPartitioning - { - public static readonly LanguagesConfig English = new ( - new Dictionary - { - [Language.EN] = new LanguageConfig() - }, - Language.EN); +namespace Squidex.Domain.Apps.Core.Apps; - private readonly Dictionary languages; - private readonly string master; - - public string Master +public sealed class LanguagesConfig : IFieldPartitioning +{ + public static readonly LanguagesConfig English = new ( + new Dictionary { - get => master; - } + [Language.EN] = new LanguageConfig() + }, + Language.EN); - public IEnumerable AllKeys - { - get => languages.Keys; - } + private readonly Dictionary languages; + private readonly string master; - public IReadOnlyDictionary Languages - { - get => languages; - } + public string Master + { + get => master; + } - public LanguagesConfig(Dictionary languages, string master) - { - Guard.NotNull(languages); - Guard.NotNullOrEmpty(master); + public IEnumerable AllKeys + { + get => languages.Keys; + } - Cleanup(languages, ref master); + public IReadOnlyDictionary Languages + { + get => languages; + } - this.languages = languages; + public LanguagesConfig(Dictionary languages, string master) + { + Guard.NotNull(languages); + Guard.NotNullOrEmpty(master); - this.master = master; - } + Cleanup(languages, ref master); - [Pure] - public LanguagesConfig MakeMaster(Language language) - { - Guard.NotNull(language); + this.languages = languages; - return Build(languages, language); - } + this.master = master; + } - [Pure] - public LanguagesConfig Set(Language language, bool isOptional = false, params Language[]? fallbacks) - { - Guard.NotNull(language); + [Pure] + public LanguagesConfig MakeMaster(Language language) + { + Guard.NotNull(language); - var newLanguages = new Dictionary(languages) - { - [language] = new LanguageConfig(isOptional, ReadonlyList.Create(fallbacks)) - }; + return Build(languages, language); + } - return Build(newLanguages, master); - } + [Pure] + public LanguagesConfig Set(Language language, bool isOptional = false, params Language[]? fallbacks) + { + Guard.NotNull(language); - [Pure] - public LanguagesConfig Remove(Language language) + var newLanguages = new Dictionary(languages) { - Guard.NotNull(language); - - var newLanguages = new Dictionary(languages); + [language] = new LanguageConfig(isOptional, ReadonlyList.Create(fallbacks)) + }; - newLanguages.Remove(language); + return Build(newLanguages, master); + } - return Build(newLanguages, master); - } + [Pure] + public LanguagesConfig Remove(Language language) + { + Guard.NotNull(language); - private LanguagesConfig Build(Dictionary newLanguages, string newMaster) - { - if (newLanguages.Count == 0) - { - return this; - } + var newLanguages = new Dictionary(languages); - Cleanup(newLanguages, ref newMaster); + newLanguages.Remove(language); - if (EqualLanguages(newLanguages) && Equals(newMaster, master)) - { - return this; - } + return Build(newLanguages, master); + } - return new LanguagesConfig(newLanguages, newMaster); + private LanguagesConfig Build(Dictionary newLanguages, string newMaster) + { + if (newLanguages.Count == 0) + { + return this; } - private bool EqualLanguages(Dictionary newLanguages) + Cleanup(newLanguages, ref newMaster); + + if (EqualLanguages(newLanguages) && Equals(newMaster, master)) { - return newLanguages.EqualsDictionary(languages); + return this; } - private void Cleanup(Dictionary newLanguages, ref string newMaster) - { - if (!newLanguages.ContainsKey(newMaster)) - { - if (newLanguages.ContainsKey(master)) - { - newMaster = master; - } - else - { - newMaster = newLanguages.Keys.First(); - } - } + return new LanguagesConfig(newLanguages, newMaster); + } - var masterConfig = newLanguages[newMaster]; + private bool EqualLanguages(Dictionary newLanguages) + { + return newLanguages.EqualsDictionary(languages); + } - if (masterConfig.IsOptional || masterConfig.Fallbacks.Any()) + private void Cleanup(Dictionary newLanguages, ref string newMaster) + { + if (!newLanguages.ContainsKey(newMaster)) + { + if (newLanguages.ContainsKey(master)) { - newLanguages[newMaster] = LanguageConfig.Default; + newMaster = master; } - - foreach (var (key, config) in newLanguages.ToList()) + else { - newLanguages[key] = config.Cleanup(key, newLanguages); + newMaster = newLanguages.Keys.First(); } } - public PartitionResolver ToResolver() - { - return partitioning => - { - if (partitioning.Equals(Partitioning.Invariant)) - { - return InvariantPartitioning.Instance; - } + var masterConfig = newLanguages[newMaster]; - return this; - }; + if (masterConfig.IsOptional || masterConfig.Fallbacks.Any()) + { + newLanguages[newMaster] = LanguageConfig.Default; } - public bool IsMaster(string key) + foreach (var (key, config) in newLanguages.ToList()) { - return Equals(Master, key); + newLanguages[key] = config.Cleanup(key, newLanguages); } + } - public string? GetName(string key) + public PartitionResolver ToResolver() + { + return partitioning => { - if (key != null && languages.ContainsKey(key)) + if (partitioning.Equals(Partitioning.Invariant)) { - return Language.GetLanguage(key).EnglishName; + return InvariantPartitioning.Instance; } - return null; - } + return this; + }; + } - public bool IsOptional(string key) + public bool IsMaster(string key) + { + return Equals(Master, key); + } + + public string? GetName(string key) + { + if (key != null && languages.ContainsKey(key)) { - if (key != null && languages.TryGetValue(key, out var value)) - { - return value.IsOptional; - } + return Language.GetLanguage(key).EnglishName; + } + + return null; + } - return false; + public bool IsOptional(string key) + { + if (key != null && languages.TryGetValue(key, out var value)) + { + return value.IsOptional; } - public IEnumerable GetPriorities(string key) + return false; + } + + public IEnumerable GetPriorities(string key) + { + if (key != null) { - if (key != null) + if (Equals(Master, key)) { - if (Equals(Master, key)) + yield return key; + } + else if (languages.TryGetValue(key, out var config)) + { + yield return key; + + foreach (var fallback in config.Fallbacks) { - yield return key; + yield return fallback; } - else if (languages.TryGetValue(key, out var config)) - { - yield return key; - - foreach (var fallback in config.Fallbacks) - { - yield return fallback; - } - if (config.Fallbacks.All(x => x.Iso2Code != Master)) - { - yield return Master; - } + if (config.Fallbacks.All(x => x.Iso2Code != Master)) + { + yield return Master; } } } + } - public bool Contains(string key) - { - return key != null && languages.ContainsKey(key); - } + public bool Contains(string key) + { + return key != null && languages.ContainsKey(key); + } - public override string ToString() - { - return "language"; - } + public override string ToString() + { + return "language"; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs index 47c041ce46..fb0301238e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs @@ -7,10 +7,9 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed record Pattern(string Name, string Regex) { - public sealed record Pattern(string Name, string Regex) - { - public string? Message { get; init; } - } + public string? Message { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs index 2aaae7a341..b6abce29c0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs @@ -13,88 +13,87 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed record Role(string Name, PermissionSet? Permissions = null, JsonObject? Properties = null) { - public sealed record Role(string Name, PermissionSet? Permissions = null, JsonObject? Properties = null) + private static readonly HashSet ExtraPermissions = new HashSet { - private static readonly HashSet ExtraPermissions = new HashSet - { - PermissionIds.AppComments, - PermissionIds.AppContributorsRead, - PermissionIds.AppHistory, - PermissionIds.AppLanguagesRead, - PermissionIds.AppPing, - PermissionIds.AppRolesRead, - PermissionIds.AppSchemasRead, - PermissionIds.AppSearch, - PermissionIds.AppTranslate, - PermissionIds.AppUsage - }; - - public const string Editor = "Editor"; - public const string Developer = "Developer"; - public const string Owner = "Owner"; - public const string Reader = "Reader"; - - public string Name { get; } = Guard.NotNullOrEmpty(Name); - - public JsonObject Properties { get; } = Properties ?? new JsonObject(); - - public PermissionSet Permissions { get; } = Permissions ?? PermissionSet.Empty; - - public bool IsDefault - { - get => Roles.IsDefault(this); - } + PermissionIds.AppComments, + PermissionIds.AppContributorsRead, + PermissionIds.AppHistory, + PermissionIds.AppLanguagesRead, + PermissionIds.AppPing, + PermissionIds.AppRolesRead, + PermissionIds.AppSchemasRead, + PermissionIds.AppSearch, + PermissionIds.AppTranslate, + PermissionIds.AppUsage + }; + + public const string Editor = "Editor"; + public const string Developer = "Developer"; + public const string Owner = "Owner"; + public const string Reader = "Reader"; + + public string Name { get; } = Guard.NotNullOrEmpty(Name); + + public JsonObject Properties { get; } = Properties ?? new JsonObject(); + + public PermissionSet Permissions { get; } = Permissions ?? PermissionSet.Empty; + + public bool IsDefault + { + get => Roles.IsDefault(this); + } - public static Role WithPermissions(string name, params string[] permissions) - { - return new Role(name, new PermissionSet(permissions), new JsonObject()); - } + public static Role WithPermissions(string name, params string[] permissions) + { + return new Role(name, new PermissionSet(permissions), new JsonObject()); + } - public static Role WithProperties(string name, JsonObject properties) - { - return new Role(name, PermissionSet.Empty, properties); - } + public static Role WithProperties(string name, JsonObject properties) + { + return new Role(name, PermissionSet.Empty, properties); + } - [Pure] - public Role Update(PermissionSet? permissions, JsonObject? properties) - { - return new Role(Name, permissions ?? Permissions, properties ?? Properties); - } + [Pure] + public Role Update(PermissionSet? permissions, JsonObject? properties) + { + return new Role(Name, permissions ?? Permissions, properties ?? Properties); + } - public bool Equals(string name) - { - return name != null && name.Equals(Name, StringComparison.Ordinal); - } + public bool Equals(string name) + { + return name != null && name.Equals(Name, StringComparison.Ordinal); + } - public Role ForApp(string app, bool isFrontend = false) - { - Guard.NotNullOrEmpty(app); + public Role ForApp(string app, bool isFrontend = false) + { + Guard.NotNullOrEmpty(app); - var result = new HashSet(); + var result = new HashSet(); - if (Permissions.Any()) - { - var prefix = PermissionIds.ForApp(PermissionIds.App, app).Id; + if (Permissions.Any()) + { + var prefix = PermissionIds.ForApp(PermissionIds.App, app).Id; - foreach (var permission in Permissions) - { - result.Add(new Permission(string.Concat(prefix, ".", permission.Id))); - } + foreach (var permission in Permissions) + { + result.Add(new Permission(string.Concat(prefix, ".", permission.Id))); } + } - if (isFrontend) + if (isFrontend) + { + foreach (var extraPermissionId in ExtraPermissions) { - foreach (var extraPermissionId in ExtraPermissions) - { - var extraPermission = PermissionIds.ForApp(extraPermissionId, app); + var extraPermission = PermissionIds.ForApp(extraPermissionId, app); - result.Add(extraPermission); - } + result.Add(extraPermission); } - - return new Role(Name, new PermissionSet(result), Properties); } + + return new Role(Name, new PermissionSet(result), Properties); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs index e824ddf54f..bba1a2036f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -13,193 +13,192 @@ using Squidex.Infrastructure.Security; using Squidex.Shared; -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Apps; + +public sealed class Roles { - public sealed class Roles + private readonly ReadonlyDictionary inner; + + public static readonly IReadOnlyDictionary Defaults = new Dictionary + { + [Role.Owner] = + new Role(Role.Owner, + new PermissionSet( + WithoutPrefix(PermissionIds.App)), + new JsonObject()), + [Role.Reader] = + new Role(Role.Reader, + new PermissionSet( + WithoutPrefix(PermissionIds.AppAssetsRead), + WithoutPrefix(PermissionIds.AppContentsRead)), + new JsonObject() + .Add("ui.api.hide", true)), + [Role.Editor] = + new Role(Role.Editor, + new PermissionSet( + WithoutPrefix(PermissionIds.AppAssets), + WithoutPrefix(PermissionIds.AppContents), + WithoutPrefix(PermissionIds.AppRolesRead), + WithoutPrefix(PermissionIds.AppWorkflowsRead)), + new JsonObject() + .Add("ui.api.hide", true)), + [Role.Developer] = + new Role(Role.Developer, + new PermissionSet( + WithoutPrefix(PermissionIds.AppAssets), + WithoutPrefix(PermissionIds.AppContents), + WithoutPrefix(PermissionIds.AppRolesRead), + WithoutPrefix(PermissionIds.AppRules), + WithoutPrefix(PermissionIds.AppSchemas), + WithoutPrefix(PermissionIds.AppWorkflows)), + new JsonObject()) + }; + + public static readonly Roles Empty = new Roles(new ReadonlyDictionary()); + + public int CustomCount { - private readonly ReadonlyDictionary inner; + get => inner.Count; + } - public static readonly IReadOnlyDictionary Defaults = new Dictionary - { - [Role.Owner] = - new Role(Role.Owner, - new PermissionSet( - WithoutPrefix(PermissionIds.App)), - new JsonObject()), - [Role.Reader] = - new Role(Role.Reader, - new PermissionSet( - WithoutPrefix(PermissionIds.AppAssetsRead), - WithoutPrefix(PermissionIds.AppContentsRead)), - new JsonObject() - .Add("ui.api.hide", true)), - [Role.Editor] = - new Role(Role.Editor, - new PermissionSet( - WithoutPrefix(PermissionIds.AppAssets), - WithoutPrefix(PermissionIds.AppContents), - WithoutPrefix(PermissionIds.AppRolesRead), - WithoutPrefix(PermissionIds.AppWorkflowsRead)), - new JsonObject() - .Add("ui.api.hide", true)), - [Role.Developer] = - new Role(Role.Developer, - new PermissionSet( - WithoutPrefix(PermissionIds.AppAssets), - WithoutPrefix(PermissionIds.AppContents), - WithoutPrefix(PermissionIds.AppRolesRead), - WithoutPrefix(PermissionIds.AppRules), - WithoutPrefix(PermissionIds.AppSchemas), - WithoutPrefix(PermissionIds.AppWorkflows)), - new JsonObject()) - }; - - public static readonly Roles Empty = new Roles(new ReadonlyDictionary()); - - public int CustomCount - { - get => inner.Count; - } + public Role this[string name] + { + get => inner[name]; + } - public Role this[string name] - { - get => inner[name]; - } + public IEnumerable Custom + { + get => inner.Values; + } - public IEnumerable Custom - { - get => inner.Values; - } + public IEnumerable All + { + get => inner.Values.Union(Defaults.Values); + } - public IEnumerable All + private Roles(ReadonlyDictionary roles) + { + inner = roles; + } + + public Roles(Dictionary roles) + { + inner = new ReadonlyDictionary(Cleaned(roles)); + } + + [Pure] + public Roles Remove(string name) + { + if (!inner.TryRemove(name, out var updated)) { - get => inner.Values.Union(Defaults.Values); + return this; } - private Roles(ReadonlyDictionary roles) + return Create(new ReadonlyDictionary(updated)); + } + + [Pure] + public Roles Add(string name) + { + if (IsDefault(name)) { - inner = roles; + return this; } - public Roles(Dictionary roles) + var newRole = new Role(name, null, new JsonObject()); + + if (!inner.TryAdd(name, newRole, out var updated)) { - inner = new ReadonlyDictionary(Cleaned(roles)); + return this; } - [Pure] - public Roles Remove(string name) - { - if (!inner.TryRemove(name, out var updated)) - { - return this; - } + return Create(new ReadonlyDictionary(updated)); + } - return Create(new ReadonlyDictionary(updated)); - } + [Pure] + public Roles Update(string name, PermissionSet? permissions = null, JsonObject? properties = null) + { + Guard.NotNullOrEmpty(name); - [Pure] - public Roles Add(string name) + if (!inner.TryGetValue(name, out var role)) { - if (IsDefault(name)) - { - return this; - } - - var newRole = new Role(name, null, new JsonObject()); + return this; + } - if (!inner.TryAdd(name, newRole, out var updated)) - { - return this; - } + var newRole = role.Update(permissions, properties); - return Create(new ReadonlyDictionary(updated)); + if (!inner.TrySet(name, newRole, out var updated)) + { + return this; } - [Pure] - public Roles Update(string name, PermissionSet? permissions = null, JsonObject? properties = null) - { - Guard.NotNullOrEmpty(name); + return Create(new ReadonlyDictionary(updated)); + } - if (!inner.TryGetValue(name, out var role)) - { - return this; - } + public static bool IsDefault(string role) + { + return role != null && Defaults.ContainsKey(role); + } - var newRole = role.Update(permissions, properties); + public static bool IsDefault(Role role) + { + return role != null && Defaults.ContainsKey(role.Name); + } - if (!inner.TrySet(name, newRole, out var updated)) - { - return this; - } + public bool ContainsCustom(string name) + { + return inner.ContainsKey(name); + } - return Create(new ReadonlyDictionary(updated)); - } + public bool Contains(string name) + { + return inner.ContainsKey(name) || Defaults.ContainsKey(name); + } - public static bool IsDefault(string role) - { - return role != null && Defaults.ContainsKey(role); - } + public bool TryGet(string app, string name, bool isFrontend, [MaybeNullWhen(false)] out Role value) + { + Guard.NotNull(app); - public static bool IsDefault(Role role) - { - return role != null && Defaults.ContainsKey(role.Name); - } + value = null!; - public bool ContainsCustom(string name) + if (Defaults.TryGetValue(name, out var role)) { - return inner.ContainsKey(name); + value = role.ForApp(app, isFrontend && name != Role.Owner); } - - public bool Contains(string name) + else if (inner.TryGetValue(name, out role)) { - return inner.ContainsKey(name) || Defaults.ContainsKey(name); + value = role.ForApp(app, isFrontend); } - public bool TryGet(string app, string name, bool isFrontend, [MaybeNullWhen(false)] out Role value) - { - Guard.NotNull(app); - - value = null!; + return value != null; + } - if (Defaults.TryGetValue(name, out var role)) - { - value = role.ForApp(app, isFrontend && name != Role.Owner); - } - else if (inner.TryGetValue(name, out role)) - { - value = role.ForApp(app, isFrontend); - } + private static string WithoutPrefix(string permission) + { + permission = PermissionIds.ForApp(permission).Id; - return value != null; - } + var prefix = PermissionIds.ForApp(PermissionIds.App); - private static string WithoutPrefix(string permission) + if (permission.StartsWith(prefix.Id, StringComparison.OrdinalIgnoreCase)) { - permission = PermissionIds.ForApp(permission).Id; - - var prefix = PermissionIds.ForApp(PermissionIds.App); - - if (permission.StartsWith(prefix.Id, StringComparison.OrdinalIgnoreCase)) - { - permission = permission[prefix.Id.Length..]; - } - - if (permission.Length == 0) - { - return Permission.Any; - } - - return permission[1..]; + permission = permission[prefix.Id.Length..]; } - private static Dictionary Cleaned(Dictionary inner) + if (permission.Length == 0) { - return inner.Where(x => !Defaults.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value); + return Permission.Any; } - private Roles Create(ReadonlyDictionary newRoles) - { - return ReferenceEquals(inner, newRoles) ? this : new Roles(newRoles); - } + return permission[1..]; + } + + private static Dictionary Cleaned(Dictionary inner) + { + return inner.Where(x => !Defaults.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value); + } + + private Roles Create(ReadonlyDictionary newRoles) + { + return ReferenceEquals(inner, newRoles) ? this : new Roles(newRoles); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs index eb71933670..9fa72d9e02 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs @@ -8,165 +8,164 @@ using System.Diagnostics.CodeAnalysis; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Assets +namespace Squidex.Domain.Apps.Core.Assets; + +public sealed class AssetMetadata : Dictionary { - public sealed class AssetMetadata : Dictionary + private static readonly char[] PathSeparators = { '.', '[', ']' }; + + public const string FocusX = "focusX"; + public const string FocusY = "focusY"; + public const string PixelWidth = "pixelWidth"; + public const string PixelHeight = "pixelHeight"; + public const string VideoWidth = "videoWidth"; + public const string VideoHeight = "videoHeight"; + + public AssetMetadata SetFocusX(float value) { - private static readonly char[] PathSeparators = { '.', '[', ']' }; + this[FocusX] = (double)value; - public const string FocusX = "focusX"; - public const string FocusY = "focusY"; - public const string PixelWidth = "pixelWidth"; - public const string PixelHeight = "pixelHeight"; - public const string VideoWidth = "videoWidth"; - public const string VideoHeight = "videoHeight"; + return this; + } - public AssetMetadata SetFocusX(float value) - { - this[FocusX] = (double)value; + public AssetMetadata SetFocusY(float value) + { + this[FocusY] = (double)value; - return this; - } + return this; + } - public AssetMetadata SetFocusY(float value) - { - this[FocusY] = (double)value; + public AssetMetadata SetPixelWidth(int value) + { + this[PixelWidth] = (double)value; - return this; - } + return this; + } - public AssetMetadata SetPixelWidth(int value) - { - this[PixelWidth] = (double)value; + public AssetMetadata SetPixelHeight(int value) + { + this[PixelHeight] = (double)value; - return this; - } + return this; + } - public AssetMetadata SetPixelHeight(int value) - { - this[PixelHeight] = (double)value; + public AssetMetadata SetVideoWidth(int value) + { + this[VideoWidth] = (double)value; - return this; - } + return this; + } - public AssetMetadata SetVideoWidth(int value) - { - this[VideoWidth] = (double)value; + public AssetMetadata SetVideoHeight(int value) + { + this[VideoHeight] = (double)value; - return this; - } + return this; + } - public AssetMetadata SetVideoHeight(int value) - { - this[VideoHeight] = (double)value; + public float? GetFocusX() + { + return GetSingle(FocusX); + } - return this; - } + public float? GetFocusY() + { + return GetSingle(FocusY); + } - public float? GetFocusX() - { - return GetSingle(FocusX); - } + public int? GetPixelWidth() + { + return GetIn32(PixelWidth); + } - public float? GetFocusY() - { - return GetSingle(FocusY); - } + public int? GetPixelHeight() + { + return GetIn32(PixelHeight); + } - public int? GetPixelWidth() - { - return GetIn32(PixelWidth); - } + public int? GetVideoWidth() + { + return GetIn32(VideoWidth); + } - public int? GetPixelHeight() - { - return GetIn32(PixelHeight); - } + public int? GetVideoHeight() + { + return GetIn32(VideoHeight); + } - public int? GetVideoWidth() + public int? GetIn32(string name) + { + if (TryGetValue(name, out var value) && value.Value is double n) { - return GetIn32(VideoWidth); + return (int)n; } - public int? GetVideoHeight() - { - return GetIn32(VideoHeight); - } + return null; + } - public int? GetIn32(string name) + public float? GetSingle(string name) + { + if (TryGetValue(name, out var value) && value.Value is double n) { - if (TryGetValue(name, out var value) && value.Value is double n) - { - return (int)n; - } - - return null; + return (float)n; } - public float? GetSingle(string name) + return null; + } + + public bool TryGetNumber(string name, out double result) + { + if (TryGetValue(name, out var value) && value.Value is double n) { - if (TryGetValue(name, out var value) && value.Value is double n) - { - return (float)n; - } + result = n; - return null; + return true; } - public bool TryGetNumber(string name, out double result) - { - if (TryGetValue(name, out var value) && value.Value is double n) - { - result = n; + result = 0; - return true; - } + return false; + } - result = 0; + public bool TryGetString(string name, [MaybeNullWhen(false)] out string result) + { + if (TryGetValue(name, out var value) && value.Value is string s) + { + result = s; - return false; + return true; } - public bool TryGetString(string name, [MaybeNullWhen(false)] out string result) - { - if (TryGetValue(name, out var value) && value.Value is string s) - { - result = s; + result = null!; - return true; - } + return false; + } - result = null!; + public bool TryGetByPath(string? path, [MaybeNullWhen(false)] out object result) + { + return TryGetByPath(path?.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries), out result!); + } - return false; - } + public bool TryGetByPath(IEnumerable? path, [MaybeNullWhen(false)] out object result) + { + result = this; - public bool TryGetByPath(string? path, [MaybeNullWhen(false)] out object result) + if (path == null || !path.Any()) { - return TryGetByPath(path?.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries), out result!); + return false; } - public bool TryGetByPath(IEnumerable? path, [MaybeNullWhen(false)] out object result) - { - result = this; - - if (path == null || !path.Any()) - { - return false; - } + result = null!; - result = null!; - - if (!TryGetValue(path.First(), out var json)) - { - return false; - } + if (!TryGetValue(path.First(), out var json)) + { + return false; + } - json.TryGetByPath(path.Skip(1), out var temp); + json.TryGetByPath(path.Skip(1), out var temp); - result = temp!; + result = temp!; - return true; - } + return true; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetScripts.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetScripts.cs index 6317d30379..9d0bc3e070 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetScripts.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetScripts.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Assets +namespace Squidex.Domain.Apps.Core.Assets; + +public sealed record AssetScripts { - public sealed record AssetScripts - { - public static readonly AssetScripts Empty = new AssetScripts(); + public static readonly AssetScripts Empty = new AssetScripts(); - public string? Create { get; init; } + public string? Create { get; init; } - public string? Update { get; init; } + public string? Update { get; init; } - public string? Annotate { get; init; } + public string? Annotate { get; init; } - public string? Move { get; init; } + public string? Move { get; init; } - public string? Delete { get; init; } - } + public string? Delete { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs index 7acd36b8d7..ef4c6b33a6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Assets +namespace Squidex.Domain.Apps.Core.Assets; + +public enum AssetType { - public enum AssetType - { - Unknown, - Image, - Audio, - Video - } + Unknown, + Image, + Audio, + Video } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/AssignedPlan.cs b/backend/src/Squidex.Domain.Apps.Core.Model/AssignedPlan.cs index 36962a8d5a..3739787dbc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/AssignedPlan.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/AssignedPlan.cs @@ -9,12 +9,11 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +public sealed record AssignedPlan(RefToken Owner, string PlanId) { - public sealed record AssignedPlan(RefToken Owner, string PlanId) - { - public RefToken Owner { get; } = Guard.NotNull(Owner); + public RefToken Owner { get; } = Guard.NotNull(Owner); - public string PlanId { get; } = Guard.NotNullOrEmpty(PlanId); - } + public string PlanId { get; } = Guard.NotNullOrEmpty(PlanId); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs index f22d355e01..8c3a5224de 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs @@ -10,12 +10,11 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Comments +namespace Squidex.Domain.Apps.Core.Comments; + +public sealed record Comment(DomainId Id, Instant Time, RefToken User, string Text, Uri? Url = null) { - public sealed record Comment(DomainId Id, Instant Time, RefToken User, string Text, Uri? Url = null) - { - public RefToken User { get; } = Guard.NotNull(User); + public RefToken User { get; } = Guard.NotNull(User); - public string Text { get; } = Guard.NotNullOrEmpty(Text); - } + public string Text { get; } = Guard.NotNullOrEmpty(Text); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs index 4dbb3bb0be..4803ec4b31 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs @@ -12,40 +12,39 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed record Component(string Type, JsonObject Data, Schema Schema) { - public sealed record Component(string Type, JsonObject Data, Schema Schema) - { - public const string Discriminator = "schemaId"; + public const string Discriminator = "schemaId"; - public string Type { get; } = Guard.NotNullOrEmpty(Type); + public string Type { get; } = Guard.NotNullOrEmpty(Type); - public Schema Schema { get; } = Guard.NotNull(Schema); + public Schema Schema { get; } = Guard.NotNull(Schema); - public JsonObject Data { get; } = Guard.NotNull(Data); + public JsonObject Data { get; } = Guard.NotNull(Data); - public static bool IsValid(JsonValue value, [MaybeNullWhen(false)] out string discriminator) - { - discriminator = null!; + public static bool IsValid(JsonValue value, [MaybeNullWhen(false)] out string discriminator) + { + discriminator = null!; - if (value.Value is not JsonObject o) - { - return false; - } + if (value.Value is not JsonObject o) + { + return false; + } - if (!o.TryGetValue(Discriminator, out var found) || found.Value is not string s) - { - return false; - } + if (!o.TryGetValue(Discriminator, out var found) || found.Value is not string s) + { + return false; + } - if (string.IsNullOrWhiteSpace(s)) - { - return false; - } + if (string.IsNullOrWhiteSpace(s)) + { + return false; + } - discriminator = s; + discriminator = s; - return true; - } + return true; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs index 6eb5184524..8528da1ee3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs @@ -7,152 +7,151 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed class ContentData : Dictionary, IEquatable { - public sealed class ContentData : Dictionary, IEquatable + public ContentData() + : base(StringComparer.Ordinal) { - public ContentData() - : base(StringComparer.Ordinal) - { - } + } - public ContentData(ContentData source) - : base(source, StringComparer.Ordinal) - { - } + public ContentData(ContentData source) + : base(source, StringComparer.Ordinal) + { + } - public ContentData(int capacity) - : base(capacity, StringComparer.Ordinal) - { - } + public ContentData(int capacity) + : base(capacity, StringComparer.Ordinal) + { + } - public ContentData AddField(string name, ContentFieldData? data) - { - Guard.NotNullOrEmpty(name); + public ContentData AddField(string name, ContentFieldData? data) + { + Guard.NotNullOrEmpty(name); + + this[name] = data; - this[name] = data; + return this; + } + public ContentData UseSameFields(ContentData? other) + { + if (other == null || other.Count == 0) + { return this; } - public ContentData UseSameFields(ContentData? other) + foreach (var (fieldName, fieldData) in this.ToList()) { - if (other == null || other.Count == 0) + if (fieldData == null) { - return this; + continue; } - foreach (var (fieldName, fieldData) in this.ToList()) + if (!other.TryGetValue(fieldName, out var otherField) || otherField == null) { - if (fieldData == null) - { - continue; - } - - if (!other.TryGetValue(fieldName, out var otherField) || otherField == null) - { - continue; - } - - if (otherField.Equals(fieldData)) - { - this[fieldName] = otherField; - } - else - { - foreach (var (language, value) in fieldData.ToList()) - { - if (!otherField.TryGetValue(language, out var otherValue)) - { - continue; - } - - if (otherValue.Equals(value)) - { - fieldData[language] = otherValue; - } - } - } + continue; } - return this; - } - - private static ContentData MergeTo(ContentData target, params ContentData[] sources) - { - Guard.NotEmpty(sources); - - if (sources.Length == 1 || sources.Skip(1).All(x => ReferenceEquals(x, sources[0]))) + if (otherField.Equals(fieldData)) { - return sources[0]; + this[fieldName] = otherField; } - - foreach (var source in sources) + else { - foreach (var (fieldName, sourceFieldData) in source) + foreach (var (language, value) in fieldData.ToList()) { - if (sourceFieldData == null) + if (!otherField.TryGetValue(language, out var otherValue)) { continue; } - var targetFieldData = target.GetOrAdd(fieldName, _ => new ContentFieldData()); - - if (targetFieldData == null) + if (otherValue.Equals(value)) { - continue; - } - - foreach (var (partition, value) in sourceFieldData) - { - targetFieldData[partition] = value; + fieldData[language] = otherValue; } } } - - return target; } - public static ContentData Merge(params ContentData[] contents) - { - return MergeTo(new ContentData(), contents); - } + return this; + } - public ContentData MergeInto(ContentData target) - { - return Merge(target, this); - } + private static ContentData MergeTo(ContentData target, params ContentData[] sources) + { + Guard.NotEmpty(sources); - public override bool Equals(object? obj) + if (sources.Length == 1 || sources.Skip(1).All(x => ReferenceEquals(x, sources[0]))) { - return Equals(obj as ContentData); + return sources[0]; } - public bool Equals(ContentData? other) + foreach (var source in sources) { - return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); - } + foreach (var (fieldName, sourceFieldData) in source) + { + if (sourceFieldData == null) + { + continue; + } - public override int GetHashCode() - { - return this.DictionaryHashCode(); - } + var targetFieldData = target.GetOrAdd(fieldName, _ => new ContentFieldData()); - public override string ToString() - { - return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value}"))}}}"; + if (targetFieldData == null) + { + continue; + } + + foreach (var (partition, value) in sourceFieldData) + { + targetFieldData[partition] = value; + } + } } - public ContentData Clone() - { - var clone = new ContentData(Count); + return target; + } - foreach (var (key, value) in this) - { - clone[key] = value?.Clone()!; - } + public static ContentData Merge(params ContentData[] contents) + { + return MergeTo(new ContentData(), contents); + } + + public ContentData MergeInto(ContentData target) + { + return Merge(target, this); + } + + public override bool Equals(object? obj) + { + return Equals(obj as ContentData); + } + + public bool Equals(ContentData? other) + { + return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); + } - return clone; + public override string ToString() + { + return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value}"))}}}"; + } + + public ContentData Clone() + { + var clone = new ContentData(Count); + + foreach (var (key, value) in this) + { + clone[key] = value?.Clone()!; } + + return clone; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs index 080ed3f375..bd25be5967 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs @@ -9,86 +9,85 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed class ContentFieldData : Dictionary, IEquatable { - public sealed class ContentFieldData : Dictionary, IEquatable + public ContentFieldData() + : base(0, StringComparer.OrdinalIgnoreCase) { - public ContentFieldData() - : base(0, StringComparer.OrdinalIgnoreCase) - { - } + } - public ContentFieldData(int capacity) - : base(capacity, StringComparer.OrdinalIgnoreCase) - { - } + public ContentFieldData(int capacity) + : base(capacity, StringComparer.OrdinalIgnoreCase) + { + } - public ContentFieldData(ContentFieldData source) - : base(source.Count, StringComparer.OrdinalIgnoreCase) + public ContentFieldData(ContentFieldData source) + : base(source.Count, StringComparer.OrdinalIgnoreCase) + { + foreach (var (key, value) in source) { - foreach (var (key, value) in source) - { - this[key] = value; - } + this[key] = value; } + } - public bool TryGetNonNull(string key, [MaybeNullWhen(false)] out JsonValue result) - { - result = JsonValue.Null; - - if (TryGetValue(key, out var found) && found != default) - { - result = found; - return true; - } - - return false; - } + public bool TryGetNonNull(string key, [MaybeNullWhen(false)] out JsonValue result) + { + result = JsonValue.Null; - public ContentFieldData AddInvariant(JsonValue value) + if (TryGetValue(key, out var found) && found != default) { - this[InvariantPartitioning.Key] = value; - - return this; + result = found; + return true; } - public ContentFieldData AddLocalized(string key, JsonValue value) - { - this[key] = value; + return false; + } - return this; - } + public ContentFieldData AddInvariant(JsonValue value) + { + this[InvariantPartitioning.Key] = value; - public ContentFieldData Clone() - { - var clone = new ContentFieldData(Count); + return this; + } - foreach (var (key, value) in this) - { - clone[key] = value.Clone()!; - } + public ContentFieldData AddLocalized(string key, JsonValue value) + { + this[key] = value; - return clone; - } + return this; + } - public override bool Equals(object? obj) - { - return Equals(obj as ContentFieldData); - } + public ContentFieldData Clone() + { + var clone = new ContentFieldData(Count); - public bool Equals(ContentFieldData? other) + foreach (var (key, value) in this) { - return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); + clone[key] = value.Clone()!; } - public override int GetHashCode() - { - return this.DictionaryHashCode(); - } + return clone; + } - public override string ToString() - { - return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value}"))}}}"; - } + public override bool Equals(object? obj) + { + return Equals(obj as ContentFieldData); + } + + public bool Equals(ContentFieldData? other) + { + return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); + } + + public override string ToString() + { + return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value}"))}}}"; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentIdStatus.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentIdStatus.cs index 042a6132f7..cc93aa7094 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentIdStatus.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentIdStatus.cs @@ -9,9 +9,8 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public record struct ContentIdStatus(DomainId SchemaId, DomainId Id, Status Status) { - public record struct ContentIdStatus(DomainId SchemaId, DomainId Id, Status Status) - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/FlatContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/FlatContentData.cs index ed53a9704e..032379c055 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/FlatContentData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/FlatContentData.cs @@ -7,9 +7,8 @@ using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed class FlatContentData : Dictionary { - public sealed class FlatContentData : Dictionary - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonParseResult.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonParseResult.cs index bd31918870..dde9be4175 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonParseResult.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonParseResult.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public enum GeoJsonParseResult { - public enum GeoJsonParseResult - { - Success, - InvalidLatitude, - InvalidLongitude, - InvalidValue - } + Success, + InvalidLatitude, + InvalidLongitude, + InvalidValue } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs index 6935b8c939..026c34ccaf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs @@ -12,68 +12,67 @@ using Squidex.Infrastructure.ObjectPool; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public static class GeoJsonValue { - public static class GeoJsonValue + public static GeoJsonParseResult TryParse(JsonValue value, IJsonSerializer serializer, out Geometry? geoJSON) { - public static GeoJsonParseResult TryParse(JsonValue value, IJsonSerializer serializer, out Geometry? geoJSON) + Guard.NotNull(serializer); + Guard.NotNull(value); + + geoJSON = null; + + if (value.Value is JsonObject o) { - Guard.NotNull(serializer); - Guard.NotNull(value); + if (TryParseGeoJson(o, serializer, out geoJSON)) + { + return GeoJsonParseResult.Success; + } - geoJSON = null; + if (!o.TryGetValue("latitude", out var found) || found.Value is not double lat || !lat.IsBetween(-90, 90)) + { + return GeoJsonParseResult.InvalidLatitude; + } - if (value.Value is JsonObject o) + if (!o.TryGetValue("longitude", out found) || found.Value is not double lon || !lon.IsBetween(-180, 180)) { - if (TryParseGeoJson(o, serializer, out geoJSON)) - { - return GeoJsonParseResult.Success; - } + return GeoJsonParseResult.InvalidLongitude; + } - if (!o.TryGetValue("latitude", out var found) || found.Value is not double lat || !lat.IsBetween(-90, 90)) - { - return GeoJsonParseResult.InvalidLatitude; - } + geoJSON = new Point(new Coordinate(lon, lat)); - if (!o.TryGetValue("longitude", out found) || found.Value is not double lon || !lon.IsBetween(-180, 180)) - { - return GeoJsonParseResult.InvalidLongitude; - } + return GeoJsonParseResult.Success; + } - geoJSON = new Point(new Coordinate(lon, lat)); + return GeoJsonParseResult.InvalidValue; + } - return GeoJsonParseResult.Success; - } + private static bool TryParseGeoJson(JsonObject obj, IJsonSerializer serializer, out Geometry? geoJSON) + { + geoJSON = null; - return GeoJsonParseResult.InvalidValue; + if (!obj.TryGetValue("type", out var type) || type.Value is not string) + { + return false; } - private static bool TryParseGeoJson(JsonObject obj, IJsonSerializer serializer, out Geometry? geoJSON) + try { - geoJSON = null; - - if (!obj.TryGetValue("type", out var type) || type.Value is not string) + using (var stream = DefaultPools.MemoryStream.GetStream()) { - return false; - } + serializer.Serialize(obj, stream, true); - try - { - using (var stream = DefaultPools.MemoryStream.GetStream()) - { - serializer.Serialize(obj, stream, true); + stream.Position = 0; - stream.Position = 0; + geoJSON = serializer.Deserialize(stream, null, true); - geoJSON = serializer.Deserialize(stream, null, true); - - return true; - } - } - catch - { - return false; + return true; } } + catch + { + return false; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs index 132fd60f5b..cd33047a12 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs @@ -10,59 +10,58 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Contents.Json +namespace Squidex.Domain.Apps.Core.Contents.Json; + +public sealed class ContentFieldDataConverter : JsonConverter { - public sealed class ContentFieldDataConverter : JsonConverter + public override ContentFieldData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override ContentFieldData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var result = new ContentFieldData(); + var result = new ContentFieldData(); - while (reader.Read()) + while (reader.Read()) + { + switch (reader.TokenType) { - switch (reader.TokenType) - { - case JsonTokenType.PropertyName: - var propertyName = reader.GetString()!; + case JsonTokenType.PropertyName: + var propertyName = reader.GetString()!; - if (!reader.Read()) - { - throw new JsonException("Unexpected end when reading Object."); - } + if (!reader.Read()) + { + throw new JsonException("Unexpected end when reading Object."); + } - var value = JsonSerializer.Deserialize(ref reader, options)!; + var value = JsonSerializer.Deserialize(ref reader, options)!; - if (propertyName == InvariantPartitioning.Key) - { - propertyName = InvariantPartitioning.Key; - } - else if (Language.TryGetLanguage(propertyName, out var language)) - { - propertyName = language.Iso2Code; - } + if (propertyName == InvariantPartitioning.Key) + { + propertyName = InvariantPartitioning.Key; + } + else if (Language.TryGetLanguage(propertyName, out var language)) + { + propertyName = language.Iso2Code; + } - result[propertyName] = value; - break; - case JsonTokenType.EndObject: - return result; - } + result[propertyName] = value; + break; + case JsonTokenType.EndObject: + return result; } - - throw new JsonException("Unexpected end when reading Object."); } - public override void Write(Utf8JsonWriter writer, ContentFieldData value, JsonSerializerOptions options) - { - writer.WriteStartObject(); + throw new JsonException("Unexpected end when reading Object."); + } - foreach (var (key, jsonValue) in value) - { - writer.WritePropertyName(key); + public override void Write(Utf8JsonWriter writer, ContentFieldData value, JsonSerializerOptions options) + { + writer.WriteStartObject(); - JsonSerializer.Serialize(writer, jsonValue, options); - } + foreach (var (key, jsonValue) in value) + { + writer.WritePropertyName(key); - writer.WriteEndObject(); + JsonSerializer.Serialize(writer, jsonValue, options); } + + writer.WriteEndObject(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs index 0f3f6b4e9b..48c290238f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs @@ -10,51 +10,50 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Contents.Json +namespace Squidex.Domain.Apps.Core.Contents.Json; + +public sealed class WorkflowStepSurrogate : ISurrogate { - public sealed class WorkflowStepSurrogate : ISurrogate - { - public Dictionary Transitions { get; set; } + public Dictionary Transitions { get; set; } - [JsonPropertyName("noUpdateRules")] - public NoUpdate? NoUpdate { get; set; } + [JsonPropertyName("noUpdateRules")] + public NoUpdate? NoUpdate { get; set; } - [JsonPropertyName("noUpdate")] - public bool NoUpdateFlag { get; set; } + [JsonPropertyName("noUpdate")] + public bool NoUpdateFlag { get; set; } - public bool Validate { get; set; } + public bool Validate { get; set; } - public string? Color { get; set; } + public string? Color { get; set; } - public void FromSource(WorkflowStep source) + public void FromSource(WorkflowStep source) + { + SimpleMapper.Map(source, this); + + Transitions = source.Transitions.ToDictionary(x => x.Key, source => { - SimpleMapper.Map(source, this); + var surrogate = new WorkflowTransitionSurrogate(); - Transitions = source.Transitions.ToDictionary(x => x.Key, source => - { - var surrogate = new WorkflowTransitionSurrogate(); + surrogate.FromSource(source.Value); - surrogate.FromSource(source.Value); + return surrogate; + }); + } - return surrogate; - }); - } + public WorkflowStep ToSource() + { + var noUpdate = NoUpdate; - public WorkflowStep ToSource() + if (NoUpdateFlag) { - var noUpdate = NoUpdate; - - if (NoUpdateFlag) - { - noUpdate = NoUpdate.Always; - } + noUpdate = NoUpdate.Always; + } - var transitions = - Transitions?.ToReadonlyDictionary( - x => x.Key, - x => x.Value.ToSource()); + var transitions = + Transitions?.ToReadonlyDictionary( + x => x.Key, + x => x.Value.ToSource()); - return new WorkflowStep(transitions, Color, noUpdate, Validate); - } + return new WorkflowStep(transitions, Color, noUpdate, Validate); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionSurrogate.cs index da3a6d4df3..dc13455ef9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionSurrogate.cs @@ -7,33 +7,32 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Contents.Json -{ - public sealed class WorkflowTransitionSurrogate : ISurrogate - { - public string? Expression { get; set; } +namespace Squidex.Domain.Apps.Core.Contents.Json; - public string? Role { get; set; } +public sealed class WorkflowTransitionSurrogate : ISurrogate +{ + public string? Expression { get; set; } - public string[]? Roles { get; set; } + public string? Role { get; set; } - public void FromSource(WorkflowTransition source) - { - Roles = source.Roles?.ToArray(); + public string[]? Roles { get; set; } - Expression = source.Expression; - } + public void FromSource(WorkflowTransition source) + { + Roles = source.Roles?.ToArray(); - public WorkflowTransition ToSource() - { - var roles = Roles; + Expression = source.Expression; + } - if (!string.IsNullOrEmpty(Role)) - { - roles = new[] { Role }; - } + public WorkflowTransition ToSource() + { + var roles = Roles; - return WorkflowTransition.When(Expression, roles); + if (!string.IsNullOrEmpty(Role)) + { + roles = new[] { Role }; } + + return WorkflowTransition.When(Expression, roles); } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs index 2a94c1c52c..3205db652f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs @@ -7,25 +7,24 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed record NoUpdate : WorkflowCondition { - public sealed record NoUpdate : WorkflowCondition - { - public static readonly NoUpdate Always = new NoUpdate(); + public static readonly NoUpdate Always = new NoUpdate(); - public static NoUpdate When(string? expression, params string[]? roles) + public static NoUpdate When(string? expression, params string[]? roles) + { + if (roles?.Length > 0) { - if (roles?.Length > 0) - { - return new NoUpdate { Expression = expression, Roles = roles?.ToReadonlyList() }; - } - - if (!string.IsNullOrWhiteSpace(expression)) - { - return new NoUpdate { Expression = expression }; - } + return new NoUpdate { Expression = expression, Roles = roles?.ToReadonlyList() }; + } - return Always; + if (!string.IsNullOrWhiteSpace(expression)) + { + return new NoUpdate { Expression = expression }; } + + return Always; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs index 28430adf08..a0ed94a327 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -7,60 +7,59 @@ using System.ComponentModel; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +[TypeConverter(typeof(StatusTypeConverter))] +public readonly struct Status : IEquatable, IComparable { - [TypeConverter(typeof(StatusTypeConverter))] - public readonly struct Status : IEquatable, IComparable - { - public static readonly Status Archived = new Status("Archived"); - public static readonly Status Draft = new Status("Draft"); - public static readonly Status Published = new Status("Published"); + public static readonly Status Archived = new Status("Archived"); + public static readonly Status Draft = new Status("Draft"); + public static readonly Status Published = new Status("Published"); - private readonly string? name; + private readonly string? name; - public string Name - { - get => name ?? "Unknown"; - } + public string Name + { + get => name ?? "Unknown"; + } - public Status(string? name) - { - this.name = name; - } + public Status(string? name) + { + this.name = name; + } - public override bool Equals(object? obj) - { - return obj is Status status && Equals(status); - } + public override bool Equals(object? obj) + { + return obj is Status status && Equals(status); + } - public bool Equals(Status other) - { - return string.Equals(Name, other.Name, StringComparison.Ordinal); - } + public bool Equals(Status other) + { + return string.Equals(Name, other.Name, StringComparison.Ordinal); + } - public override int GetHashCode() - { - return Name.GetHashCode(StringComparison.Ordinal); - } + public override int GetHashCode() + { + return Name.GetHashCode(StringComparison.Ordinal); + } - public override string ToString() - { - return Name; - } + public override string ToString() + { + return Name; + } - public int CompareTo(Status other) - { - return string.Compare(Name, other.Name, StringComparison.Ordinal); - } + public int CompareTo(Status other) + { + return string.Compare(Name, other.Name, StringComparison.Ordinal); + } - public static bool operator ==(Status lhs, Status rhs) - { - return lhs.Equals(rhs); - } + public static bool operator ==(Status lhs, Status rhs) + { + return lhs.Equals(rhs); + } - public static bool operator !=(Status lhs, Status rhs) - { - return !lhs.Equals(rhs); - } + public static bool operator !=(Status lhs, Status rhs) + { + return !lhs.Equals(rhs); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs index 4a60b71670..0f778140d2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public enum StatusChange { - public enum StatusChange - { - Change, - Published, - Unpublished - } + Change, + Published, + Unpublished } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs index 8df9aea29c..94e40e1e61 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public static class StatusColors { - public static class StatusColors - { - public const string Archived = "#eb3142"; - public const string Draft = "#8091a5"; - public const string Published = "#4bb958"; - } + public const string Archived = "#eb3142"; + public const string Draft = "#8091a5"; + public const string Published = "#4bb958"; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs index 7568d32955..7e1c0915c7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs @@ -9,10 +9,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed record StatusInfo(Status Status, string Color) { - public sealed record StatusInfo(Status Status, string Color) - { - public string Color { get; } = Guard.NotNullOrEmpty(Color); - } + public string Color { get; } = Guard.NotNullOrEmpty(Color); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusTypeConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusTypeConverter.cs index 92a563d452..b489410445 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusTypeConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusTypeConverter.cs @@ -8,28 +8,27 @@ using System.ComponentModel; using System.Globalization; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed class StatusTypeConverter : TypeConverter { - public sealed class StatusTypeConverter : TypeConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - { - return sourceType == typeof(string); - } + return sourceType == typeof(string); + } - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) - { - return destinationType == typeof(string); - } + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string); + } - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - return new Status((string)value); - } + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + return new Status((string)value); + } - public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) - { - return value?.ToString()!; - } + public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) + { + return value?.ToString()!; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs index c3d821a053..b21c564393 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs @@ -10,59 +10,58 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public class TranslationStatus : Dictionary { - public class TranslationStatus : Dictionary + public TranslationStatus() { - public TranslationStatus() - { - } - - public TranslationStatus(int capacity) - : base(capacity) - { - } + } - public static TranslationStatus Create(ContentData data, Schema schema, LanguagesConfig languages) - { - Guard.NotNull(data); - Guard.NotNull(schema); - Guard.NotNull(languages); + public TranslationStatus(int capacity) + : base(capacity) + { + } - var result = new TranslationStatus(languages.Languages.Count); + public static TranslationStatus Create(ContentData data, Schema schema, LanguagesConfig languages) + { + Guard.NotNull(data); + Guard.NotNull(schema); + Guard.NotNull(languages); - var localizedFields = schema.Fields.Where(x => x.Partitioning == Partitioning.Language).ToList(); + var result = new TranslationStatus(languages.Languages.Count); - foreach (var language in languages.AllKeys) - { - var percent = 0; + var localizedFields = schema.Fields.Where(x => x.Partitioning == Partitioning.Language).ToList(); - foreach (var field in localizedFields) - { - if (IsValidValue(data.GetValueOrDefault(field.Name)?.GetValueOrDefault(language))) - { - percent++; - } - } + foreach (var language in languages.AllKeys) + { + var percent = 0; - if (localizedFields.Count > 0) - { - percent = (int)Math.Round(100 * (double)percent / localizedFields.Count); - } - else + foreach (var field in localizedFields) + { + if (IsValidValue(data.GetValueOrDefault(field.Name)?.GetValueOrDefault(language))) { - percent = 100; + percent++; } + } - result[language] = percent; + if (localizedFields.Count > 0) + { + percent = (int)Math.Round(100 * (double)percent / localizedFields.Count); + } + else + { + percent = 100; } - return result; + result[language] = percent; } - private static bool IsValidValue(JsonValue? value) - { - return value != null && value.Value.Type != JsonValueType.Null; - } + return result; + } + + private static bool IsValidValue(JsonValue? value) + { + return value != null && value.Value.Type != JsonValueType.Null; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs index 502c5a855d..e7c7a704f0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs @@ -11,97 +11,96 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed record Workflow(Status Initial, ReadonlyDictionary? Steps = null, ReadonlyList? SchemaIds = null, string? Name = null) { - public sealed record Workflow(Status Initial, ReadonlyDictionary? Steps = null, ReadonlyList? SchemaIds = null, string? Name = null) - { - private const string DefaultName = "Unnamed"; + private const string DefaultName = "Unnamed"; - public static readonly Workflow Default = CreateDefault(); - public static readonly Workflow Empty = new Workflow(default, null); + public static readonly Workflow Default = CreateDefault(); + public static readonly Workflow Empty = new Workflow(default, null); - public string Name { get; } = Name.Or(DefaultName); + public string Name { get; } = Name.Or(DefaultName); - public ReadonlyDictionary Steps { get; } = Steps ?? ReadonlyDictionary.Empty(); + public ReadonlyDictionary Steps { get; } = Steps ?? ReadonlyDictionary.Empty(); - public ReadonlyList SchemaIds { get; } = SchemaIds ?? ReadonlyList.Empty(); + public ReadonlyList SchemaIds { get; } = SchemaIds ?? ReadonlyList.Empty(); - public static Workflow CreateDefault(string? name = null) - { - return new Workflow( - Status.Draft, - new Dictionary - { - [Status.Archived] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = WorkflowTransition.Always - }.ToReadonlyDictionary(), - StatusColors.Archived, NoUpdate.Always), - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = WorkflowTransition.Always, - [Status.Published] = WorkflowTransition.Always - }.ToReadonlyDictionary(), - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = WorkflowTransition.Always, - [Status.Draft] = WorkflowTransition.Always - }.ToReadonlyDictionary(), - StatusColors.Published) - }.ToReadonlyDictionary(), null, name); - } + public static Workflow CreateDefault(string? name = null) + { + return new Workflow( + Status.Draft, + new Dictionary + { + [Status.Archived] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = WorkflowTransition.Always + }.ToReadonlyDictionary(), + StatusColors.Archived, NoUpdate.Always), + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = WorkflowTransition.Always, + [Status.Published] = WorkflowTransition.Always + }.ToReadonlyDictionary(), + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = WorkflowTransition.Always, + [Status.Draft] = WorkflowTransition.Always + }.ToReadonlyDictionary(), + StatusColors.Published) + }.ToReadonlyDictionary(), null, name); + } - public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status) + public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status) + { + if (TryGetStep(status, out var step)) { - if (TryGetStep(status, out var step)) + foreach (var (nextStatus, transition) in step.Transitions) { - foreach (var (nextStatus, transition) in step.Transitions) - { - yield return (nextStatus, Steps[nextStatus], transition); - } - } - else if (TryGetStep(Initial, out var initial)) - { - yield return (Initial, initial, WorkflowTransition.Always); + yield return (nextStatus, Steps[nextStatus], transition); } } - - public bool TryGetTransition(Status from, Status to, [MaybeNullWhen(false)] out WorkflowTransition transition) + else if (TryGetStep(Initial, out var initial)) { - transition = null!; + yield return (Initial, initial, WorkflowTransition.Always); + } + } - if (TryGetStep(from, out var step)) - { - if (step.Transitions.TryGetValue(to, out transition!)) - { - return true; - } - } - else if (to == Initial) - { - transition = WorkflowTransition.Always; + public bool TryGetTransition(Status from, Status to, [MaybeNullWhen(false)] out WorkflowTransition transition) + { + transition = null!; + if (TryGetStep(from, out var step)) + { + if (step.Transitions.TryGetValue(to, out transition!)) + { return true; } - - return false; } - - public bool TryGetStep(Status status, [MaybeNullWhen(false)] out WorkflowStep step) + else if (to == Initial) { - return Steps.TryGetValue(status, out step!); - } + transition = WorkflowTransition.Always; - public (Status Key, WorkflowStep) GetInitialStep() - { - return (Initial, Steps[Initial]); + return true; } + + return false; + } + + public bool TryGetStep(Status status, [MaybeNullWhen(false)] out WorkflowStep step) + { + return Steps.TryGetValue(status, out step!); + } + + public (Status Key, WorkflowStep) GetInitialStep() + { + return (Initial, Steps[Initial]); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs index e6117991a2..587e0798c8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs @@ -7,12 +7,11 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public abstract record WorkflowCondition { - public abstract record WorkflowCondition - { - public string? Expression { get; init; } + public string? Expression { get; init; } - public ReadonlyList? Roles { get; init; } - } + public ReadonlyList? Roles { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs index 6e798b4ebb..45a266c90d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs @@ -9,10 +9,9 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed record WorkflowStep(ReadonlyDictionary? Transitions = null, string? Color = null, NoUpdate? NoUpdate = null, bool Validate = false) { - public sealed record WorkflowStep(ReadonlyDictionary? Transitions = null, string? Color = null, NoUpdate? NoUpdate = null, bool Validate = false) - { - public ReadonlyDictionary Transitions { get; } = Transitions ?? ReadonlyDictionary.Empty(); - } + public ReadonlyDictionary Transitions { get; } = Transitions ?? ReadonlyDictionary.Empty(); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs index e163d49c5e..76f4fad6f6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs @@ -7,25 +7,24 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed record WorkflowTransition : WorkflowCondition { - public sealed record WorkflowTransition : WorkflowCondition - { - public static readonly WorkflowTransition Always = new WorkflowTransition(); + public static readonly WorkflowTransition Always = new WorkflowTransition(); - public static WorkflowTransition When(string? expression, params string[]? roles) + public static WorkflowTransition When(string? expression, params string[]? roles) + { + if (roles?.Length > 0) { - if (roles?.Length > 0) - { - return new WorkflowTransition { Expression = expression, Roles = roles?.ToReadonlyList() }; - } - - if (!string.IsNullOrWhiteSpace(expression)) - { - return new WorkflowTransition { Expression = expression }; - } + return new WorkflowTransition { Expression = expression, Roles = roles?.ToReadonlyList() }; + } - return Always; + if (!string.IsNullOrWhiteSpace(expression)) + { + return new WorkflowTransition { Expression = expression }; } + + return Always; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs index 3cc344178a..c509228995 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs @@ -9,92 +9,91 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Contents; + +public sealed class Workflows : ReadonlyDictionary { - public sealed class Workflows : ReadonlyDictionary + public static readonly Workflows Empty = new Workflows(); + + private Workflows() { - public static readonly Workflows Empty = new Workflows(); + } - private Workflows() - { - } + public Workflows(IDictionary inner) + : base(inner) + { + } - public Workflows(IDictionary inner) - : base(inner) + [Pure] + public Workflows Remove(DomainId id) + { + if (!this.TryRemove(id, out var updated)) { + return this; } - [Pure] - public Workflows Remove(DomainId id) - { - if (!this.TryRemove(id, out var updated)) - { - return this; - } + return new Workflows(updated); + } - return new Workflows(updated); - } + [Pure] + public Workflows Add(DomainId workflowId, string name) + { + Guard.NotNullOrEmpty(name); - [Pure] - public Workflows Add(DomainId workflowId, string name) + if (!this.TryAdd(workflowId, Workflow.CreateDefault(name), out var updated)) { - Guard.NotNullOrEmpty(name); - - if (!this.TryAdd(workflowId, Workflow.CreateDefault(name), out var updated)) - { - return this; - } - - return new Workflows(updated); + return this; } - [Pure] - public Workflows Set(Workflow workflow) - { - Guard.NotNull(workflow); - - if (!this.TrySet(default, workflow, out var updated)) - { - return this; - } + return new Workflows(updated); + } - return new Workflows(updated); - } + [Pure] + public Workflows Set(Workflow workflow) + { + Guard.NotNull(workflow); - [Pure] - public Workflows Set(DomainId id, Workflow workflow) + if (!this.TrySet(default, workflow, out var updated)) { - Guard.NotNull(workflow); + return this; + } - if (!this.TrySet(id, workflow, out var updated)) - { - return this; - } + return new Workflows(updated); + } - return new Workflows(updated); - } + [Pure] + public Workflows Set(DomainId id, Workflow workflow) + { + Guard.NotNull(workflow); - [Pure] - public Workflows Update(DomainId id, Workflow workflow) + if (!this.TrySet(id, workflow, out var updated)) { - Guard.NotNull(workflow); + return this; + } - if (id == DomainId.Empty) - { - return Set(workflow); - } + return new Workflows(updated); + } - if (!this.TryUpdate(id, workflow, out var updated)) - { - return this; - } + [Pure] + public Workflows Update(DomainId id, Workflow workflow) + { + Guard.NotNull(workflow); - return new Workflows(updated); + if (id == DomainId.Empty) + { + return Set(workflow); } - public Workflow GetFirst() + if (!this.TryUpdate(id, workflow, out var updated)) { - return Values.FirstOrDefault() ?? Workflow.Default; + return this; } + + return new Workflows(updated); + } + + public Workflow GetFirst() + { + return Values.FirstOrDefault() ?? Workflow.Default; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contributors.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contributors.cs index d03a3f3fd9..9f19ad92d4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contributors.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contributors.cs @@ -9,46 +9,45 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +public sealed class Contributors : ReadonlyDictionary { - public sealed class Contributors : ReadonlyDictionary + public static readonly Contributors Empty = new Contributors(); + + private Contributors() { - public static readonly Contributors Empty = new Contributors(); + } - private Contributors() - { - } + public Contributors(IDictionary inner) + : base(inner) + { + } - public Contributors(IDictionary inner) - : base(inner) - { - } + [Pure] + public Contributors Assign(string contributorId, string role) + { + Guard.NotNullOrEmpty(contributorId); + Guard.NotNullOrEmpty(role); - [Pure] - public Contributors Assign(string contributorId, string role) + if (!this.TrySet(contributorId, role, out var updated)) { - Guard.NotNullOrEmpty(contributorId); - Guard.NotNullOrEmpty(role); - - if (!this.TrySet(contributorId, role, out var updated)) - { - return this; - } - - return new Contributors(updated); + return this; } - [Pure] - public Contributors Remove(string contributorId) - { - Guard.NotNullOrEmpty(contributorId); + return new Contributors(updated); + } - if (!this.TryRemove(contributorId, out var updated)) - { - return this; - } + [Pure] + public Contributors Remove(string contributorId) + { + Guard.NotNullOrEmpty(contributorId); - return new Contributors(updated); + if (!this.TryRemove(contributorId, out var updated)) + { + return this; } + + return new Contributors(updated); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptionAttribute.cs b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptionAttribute.cs index 2b9311ba00..9303f63eb0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptionAttribute.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptionAttribute.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class FieldDescriptionAttribute : Attribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class FieldDescriptionAttribute : Attribute - { - public string Name { get; } + public string Name { get; } - public FieldDescriptionAttribute(string name) - { - Name = name; - } + public FieldDescriptionAttribute(string name) + { + Name = name; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs index 98963a6d9c..4909314682 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs @@ -8,992 +8,991 @@ // //------------------------------------------------------------------------------ -namespace Squidex.Domain.Apps.Core { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class FieldDescriptions { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal FieldDescriptions() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.FieldDescriptions", typeof(FieldDescriptions).Assembly); - resourceMan = temp; - } - return resourceMan; +namespace Squidex.Domain.Apps.Core; +using System; + + +/// +/// A strongly-typed resource class, for looking up localized strings, etc. +/// +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +public class FieldDescriptions { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal FieldDescriptions() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.FieldDescriptions", typeof(FieldDescriptions).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// - /// Looks up a localized string similar to The user or client that triggered the event or command.. - /// - public static string Actor { - get { - return ResourceManager.GetString("Actor", resourceCulture); - } + set { + resourceCulture = value; } - - /// - /// Looks up a localized string similar to The ID of this actor.. - /// - public static string ActorIdentifier { - get { - return ResourceManager.GetString("ActorIdentifier", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The user or client that triggered the event or command.. + /// + public static string Actor { + get { + return ResourceManager.GetString("Actor", resourceCulture); } - - /// - /// Looks up a localized string similar to The type of this actor.. - /// - public static string ActorType { - get { - return ResourceManager.GetString("ActorType", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The ID of this actor.. + /// + public static string ActorIdentifier { + get { + return ResourceManager.GetString("ActorIdentifier", resourceCulture); } - - /// - /// Looks up a localized string similar to The ID of the current app.. - /// - public static string AppId { - get { - return ResourceManager.GetString("AppId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The type of this actor.. + /// + public static string ActorType { + get { + return ResourceManager.GetString("ActorType", resourceCulture); } - - /// - /// Looks up a localized string similar to The name of the current app.. - /// - public static string AppName { - get { - return ResourceManager.GetString("AppName", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The ID of the current app.. + /// + public static string AppId { + get { + return ResourceManager.GetString("AppId", resourceCulture); } - - /// - /// Looks up a localized string similar to The asset.. - /// - public static string Asset { - get { - return ResourceManager.GetString("Asset", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The name of the current app.. + /// + public static string AppName { + get { + return ResourceManager.GetString("AppName", resourceCulture); } - - /// - /// Looks up a localized string similar to The hash of the file. Can be null for old files.. - /// - public static string AssetFileHash { - get { - return ResourceManager.GetString("AssetFileHash", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The asset.. + /// + public static string Asset { + get { + return ResourceManager.GetString("Asset", resourceCulture); } - - /// - /// Looks up a localized string similar to The file name of the asset.. - /// - public static string AssetFileName { - get { - return ResourceManager.GetString("AssetFileName", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The hash of the file. Can be null for old files.. + /// + public static string AssetFileHash { + get { + return ResourceManager.GetString("AssetFileHash", resourceCulture); } - - /// - /// Looks up a localized string similar to The size of the file in bytes.. - /// - public static string AssetFileSize { - get { - return ResourceManager.GetString("AssetFileSize", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The file name of the asset.. + /// + public static string AssetFileName { + get { + return ResourceManager.GetString("AssetFileName", resourceCulture); } - - /// - /// Looks up a localized string similar to The file type (file extension) of the asset.. - /// - public static string AssetFileType { - get { - return ResourceManager.GetString("AssetFileType", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The size of the file in bytes.. + /// + public static string AssetFileSize { + get { + return ResourceManager.GetString("AssetFileSize", resourceCulture); } - - /// - /// Looks up a localized string similar to The version of the file.. - /// - public static string AssetFileVersion { - get { - return ResourceManager.GetString("AssetFileVersion", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The file type (file extension) of the asset.. + /// + public static string AssetFileType { + get { + return ResourceManager.GetString("AssetFileType", resourceCulture); } - - /// - /// Looks up a localized string similar to Determines if the uploaded file is an image.. - /// - public static string AssetIsImage { - get { - return ResourceManager.GetString("AssetIsImage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The version of the file.. + /// + public static string AssetFileVersion { + get { + return ResourceManager.GetString("AssetFileVersion", resourceCulture); } - - /// - /// Looks up a localized string similar to True, when the asset is not public.. - /// - public static string AssetIsProtected { - get { - return ResourceManager.GetString("AssetIsProtected", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Determines if the uploaded file is an image.. + /// + public static string AssetIsImage { + get { + return ResourceManager.GetString("AssetIsImage", resourceCulture); } - - /// - /// Looks up a localized string similar to The asset metadata.. - /// - public static string AssetMetadata { - get { - return ResourceManager.GetString("AssetMetadata", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to True, when the asset is not public.. + /// + public static string AssetIsProtected { + get { + return ResourceManager.GetString("AssetIsProtected", resourceCulture); } - - /// - /// Looks up a localized string similar to The type of the image.. - /// - public static string AssetMetadataText { - get { - return ResourceManager.GetString("AssetMetadataText", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The asset metadata.. + /// + public static string AssetMetadata { + get { + return ResourceManager.GetString("AssetMetadata", resourceCulture); } - - /// - /// Looks up a localized string similar to The asset metadata with name 'name'.. - /// - public static string AssetMetadataValue { - get { - return ResourceManager.GetString("AssetMetadataValue", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The type of the image.. + /// + public static string AssetMetadataText { + get { + return ResourceManager.GetString("AssetMetadataText", resourceCulture); } - - /// - /// Looks up a localized string similar to The mime type.. - /// - public static string AssetMimeType { - get { - return ResourceManager.GetString("AssetMimeType", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The asset metadata with name 'name'.. + /// + public static string AssetMetadataValue { + get { + return ResourceManager.GetString("AssetMetadataValue", resourceCulture); } - - /// - /// Looks up a localized string similar to The ID of the parent folder. Empty for files without parent.. - /// - public static string AssetParentId { - get { - return ResourceManager.GetString("AssetParentId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The mime type.. + /// + public static string AssetMimeType { + get { + return ResourceManager.GetString("AssetMimeType", resourceCulture); } - - /// - /// Looks up a localized string similar to The full path in the folder hierarchy as array of folder infos.. - /// - public static string AssetParentPath { - get { - return ResourceManager.GetString("AssetParentPath", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The ID of the parent folder. Empty for files without parent.. + /// + public static string AssetParentId { + get { + return ResourceManager.GetString("AssetParentId", resourceCulture); } - - /// - /// Looks up a localized string similar to The height of the image in pixels if the asset is an image.. - /// - public static string AssetPixelHeight { - get { - return ResourceManager.GetString("AssetPixelHeight", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The full path in the folder hierarchy as array of folder infos.. + /// + public static string AssetParentPath { + get { + return ResourceManager.GetString("AssetParentPath", resourceCulture); } - - /// - /// Looks up a localized string similar to The width of the image in pixels if the asset is an image.. - /// - public static string AssetPixelWidth { - get { - return ResourceManager.GetString("AssetPixelWidth", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The height of the image in pixels if the asset is an image.. + /// + public static string AssetPixelHeight { + get { + return ResourceManager.GetString("AssetPixelHeight", resourceCulture); } - - /// - /// Looks up a localized string similar to The assets.. - /// - public static string AssetsItems { - get { - return ResourceManager.GetString("AssetsItems", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The width of the image in pixels if the asset is an image.. + /// + public static string AssetPixelWidth { + get { + return ResourceManager.GetString("AssetPixelWidth", resourceCulture); } - - /// - /// Looks up a localized string similar to The file name as slug.. - /// - public static string AssetSlug { - get { - return ResourceManager.GetString("AssetSlug", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The assets.. + /// + public static string AssetsItems { + get { + return ResourceManager.GetString("AssetsItems", resourceCulture); } - - /// - /// Looks up a localized string similar to The source URL of the asset.. - /// - public static string AssetSourceUrl { - get { - return ResourceManager.GetString("AssetSourceUrl", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The file name as slug.. + /// + public static string AssetSlug { + get { + return ResourceManager.GetString("AssetSlug", resourceCulture); } - - /// - /// Looks up a localized string similar to The total count of assets.. - /// - public static string AssetsTotal { - get { - return ResourceManager.GetString("AssetsTotal", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The source URL of the asset.. + /// + public static string AssetSourceUrl { + get { + return ResourceManager.GetString("AssetSourceUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to The asset tags.. - /// - public static string AssetTags { - get { - return ResourceManager.GetString("AssetTags", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The total count of assets.. + /// + public static string AssetsTotal { + get { + return ResourceManager.GetString("AssetsTotal", resourceCulture); } - - /// - /// Looks up a localized string similar to The thumbnail URL to the asset.. - /// - public static string AssetThumbnailUrl { - get { - return ResourceManager.GetString("AssetThumbnailUrl", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The asset tags.. + /// + public static string AssetTags { + get { + return ResourceManager.GetString("AssetTags", resourceCulture); } - - /// - /// Looks up a localized string similar to The type of the asset.. - /// - public static string AssetType { - get { - return ResourceManager.GetString("AssetType", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The thumbnail URL to the asset.. + /// + public static string AssetThumbnailUrl { + get { + return ResourceManager.GetString("AssetThumbnailUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to The URL to the asset.. - /// - public static string AssetUrl { - get { - return ResourceManager.GetString("AssetUrl", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The type of the asset.. + /// + public static string AssetType { + get { + return ResourceManager.GetString("AssetType", resourceCulture); } - - /// - /// Looks up a localized string similar to The executed command.. - /// - public static string Command { - get { - return ResourceManager.GetString("Command", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The URL to the asset.. + /// + public static string AssetUrl { + get { + return ResourceManager.GetString("AssetUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to The user when someone is mentioned with '@'.. - /// - public static string CommentMentionedUser { - get { - return ResourceManager.GetString("CommentMentionedUser", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The executed command.. + /// + public static string Command { + get { + return ResourceManager.GetString("Command", resourceCulture); } - - /// - /// Looks up a localized string similar to The text of the comment.. - /// - public static string CommentText { - get { - return ResourceManager.GetString("CommentText", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The user when someone is mentioned with '@'.. + /// + public static string CommentMentionedUser { + get { + return ResourceManager.GetString("CommentMentionedUser", resourceCulture); } - - /// - /// Looks up a localized string similar to The URL pointing to the source of the comment.. - /// - public static string CommentUrl { - get { - return ResourceManager.GetString("CommentUrl", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The text of the comment.. + /// + public static string CommentText { + get { + return ResourceManager.GetString("CommentText", resourceCulture); } - - /// - /// Looks up a localized string similar to {0} field ({1} component).. - /// - public static string ComponentField { - get { - return ResourceManager.GetString("ComponentField", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The URL pointing to the source of the comment.. + /// + public static string CommentUrl { + get { + return ResourceManager.GetString("CommentUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to The schema id to identity the component type.. - /// - public static string ComponentSchemaId { - get { - return ResourceManager.GetString("ComponentSchemaId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to {0} field ({1} component).. + /// + public static string ComponentField { + get { + return ResourceManager.GetString("ComponentField", resourceCulture); } - - /// - /// Looks up a localized string similar to The content.. - /// - public static string Content { - get { - return ResourceManager.GetString("Content", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The schema id to identity the component type.. + /// + public static string ComponentSchemaId { + get { + return ResourceManager.GetString("ComponentSchemaId", resourceCulture); } - - /// - /// Looks up a localized string similar to {0} nested field.. - /// - public static string ContentArrayField { - get { - return ResourceManager.GetString("ContentArrayField", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The content.. + /// + public static string Content { + get { + return ResourceManager.GetString("Content", resourceCulture); } - - /// - /// Looks up a localized string similar to The data of the content.. - /// - public static string ContentData { - get { - return ResourceManager.GetString("ContentData", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to {0} nested field.. + /// + public static string ContentArrayField { + get { + return ResourceManager.GetString("ContentArrayField", resourceCulture); } - - /// - /// Looks up a localized string similar to The previous data of the content.. - /// - public static string ContentDataOld { - get { - return ResourceManager.GetString("ContentDataOld", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The data of the content.. + /// + public static string ContentData { + get { + return ResourceManager.GetString("ContentData", resourceCulture); } - - /// - /// Looks up a localized string similar to {0} field.. - /// - public static string ContentField { - get { - return ResourceManager.GetString("ContentField", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The previous data of the content.. + /// + public static string ContentDataOld { + get { + return ResourceManager.GetString("ContentDataOld", resourceCulture); } - - /// - /// Looks up a localized string similar to The flat data of the content.. - /// - public static string ContentFlatData { - get { - return ResourceManager.GetString("ContentFlatData", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to {0} field.. + /// + public static string ContentField { + get { + return ResourceManager.GetString("ContentField", resourceCulture); } - - /// - /// Looks up a localized string similar to The new status of the content.. - /// - public static string ContentNewStatus { - get { - return ResourceManager.GetString("ContentNewStatus", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The flat data of the content.. + /// + public static string ContentFlatData { + get { + return ResourceManager.GetString("ContentFlatData", resourceCulture); } - - /// - /// Looks up a localized string similar to The new status color of the content.. - /// - public static string ContentNewStatusColor { - get { - return ResourceManager.GetString("ContentNewStatusColor", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The new status of the content.. + /// + public static string ContentNewStatus { + get { + return ResourceManager.GetString("ContentNewStatus", resourceCulture); } - - /// - /// Looks up a localized string similar to {0} field ({1}).. - /// - public static string ContentPartitionField { - get { - return ResourceManager.GetString("ContentPartitionField", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The new status color of the content.. + /// + public static string ContentNewStatusColor { + get { + return ResourceManager.GetString("ContentNewStatusColor", resourceCulture); } - - /// - /// Looks up a localized string similar to The data for the content.. - /// - public static string ContentRequestData { - get { - return ResourceManager.GetString("ContentRequestData", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to {0} field ({1}).. + /// + public static string ContentPartitionField { + get { + return ResourceManager.GetString("ContentPartitionField", resourceCulture); } - - /// - /// Looks up a localized string similar to The timestamp when the status should be changed.. - /// - public static string ContentRequestDueTime { - get { - return ResourceManager.GetString("ContentRequestDueTime", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The data for the content.. + /// + public static string ContentRequestData { + get { + return ResourceManager.GetString("ContentRequestData", resourceCulture); } - - /// - /// Looks up a localized string similar to The optional custom content ID.. - /// - public static string ContentRequestOptionalId { - get { - return ResourceManager.GetString("ContentRequestOptionalId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The timestamp when the status should be changed.. + /// + public static string ContentRequestDueTime { + get { + return ResourceManager.GetString("ContentRequestDueTime", resourceCulture); } - - /// - /// Looks up a localized string similar to The initial status.. - /// - public static string ContentRequestOptionalStatus { - get { - return ResourceManager.GetString("ContentRequestOptionalStatus", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The optional custom content ID.. + /// + public static string ContentRequestOptionalId { + get { + return ResourceManager.GetString("ContentRequestOptionalId", resourceCulture); } - - /// - /// Looks up a localized string similar to Makes the update as patch.. - /// - public static string ContentRequestPatch { - get { - return ResourceManager.GetString("ContentRequestPatch", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The initial status.. + /// + public static string ContentRequestOptionalStatus { + get { + return ResourceManager.GetString("ContentRequestOptionalStatus", resourceCulture); } - - /// - /// Looks up a localized string similar to Set to true to autopublish content on create.. - /// - public static string ContentRequestPublish { - get { - return ResourceManager.GetString("ContentRequestPublish", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Makes the update as patch.. + /// + public static string ContentRequestPatch { + get { + return ResourceManager.GetString("ContentRequestPatch", resourceCulture); } - - /// - /// Looks up a localized string similar to The status for the content.. - /// - public static string ContentRequestStatus { - get { - return ResourceManager.GetString("ContentRequestStatus", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Set to true to autopublish content on create.. + /// + public static string ContentRequestPublish { + get { + return ResourceManager.GetString("ContentRequestPublish", resourceCulture); } - - /// - /// Looks up a localized string similar to The display name of the schema.. - /// - public static string ContentSchemaDisplayName { - get { - return ResourceManager.GetString("ContentSchemaDisplayName", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The status for the content.. + /// + public static string ContentRequestStatus { + get { + return ResourceManager.GetString("ContentRequestStatus", resourceCulture); } - - /// - /// Looks up a localized string similar to The ID of the schema.. - /// - public static string ContentSchemaId { - get { - return ResourceManager.GetString("ContentSchemaId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The display name of the schema.. + /// + public static string ContentSchemaDisplayName { + get { + return ResourceManager.GetString("ContentSchemaDisplayName", resourceCulture); } - - /// - /// Looks up a localized string similar to The name of the schema.. - /// - public static string ContentSchemaName { - get { - return ResourceManager.GetString("ContentSchemaName", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The ID of the schema.. + /// + public static string ContentSchemaId { + get { + return ResourceManager.GetString("ContentSchemaId", resourceCulture); } - - /// - /// Looks up a localized string similar to The contents.. - /// - public static string ContentsItems { - get { - return ResourceManager.GetString("ContentsItems", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The name of the schema.. + /// + public static string ContentSchemaName { + get { + return ResourceManager.GetString("ContentSchemaName", resourceCulture); } - - /// - /// Looks up a localized string similar to The status of the content.. - /// - public static string ContentStatus { - get { - return ResourceManager.GetString("ContentStatus", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The contents.. + /// + public static string ContentsItems { + get { + return ResourceManager.GetString("ContentsItems", resourceCulture); } - - /// - /// Looks up a localized string similar to The status color of the content.. - /// - public static string ContentStatusColor { - get { - return ResourceManager.GetString("ContentStatusColor", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The status of the content.. + /// + public static string ContentStatus { + get { + return ResourceManager.GetString("ContentStatus", resourceCulture); } - - /// - /// Looks up a localized string similar to The previous status of the content.. - /// - public static string ContentStatusOld { - get { - return ResourceManager.GetString("ContentStatusOld", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The status color of the content.. + /// + public static string ContentStatusColor { + get { + return ResourceManager.GetString("ContentStatusColor", resourceCulture); } - - /// - /// Looks up a localized string similar to The total count of contents.. - /// - public static string ContentsTotal { - get { - return ResourceManager.GetString("ContentsTotal", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The previous status of the content.. + /// + public static string ContentStatusOld { + get { + return ResourceManager.GetString("ContentStatusOld", resourceCulture); } - - /// - /// Looks up a localized string similar to The URL to the content.. - /// - public static string ContentUrl { - get { - return ResourceManager.GetString("ContentUrl", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The total count of contents.. + /// + public static string ContentsTotal { + get { + return ResourceManager.GetString("ContentsTotal", resourceCulture); } - - /// - /// Looks up a localized string similar to Validates the content item.. - /// - public static string ContentValidate { - get { - return ResourceManager.GetString("ContentValidate", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The URL to the content.. + /// + public static string ContentUrl { + get { + return ResourceManager.GetString("ContentUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to The context object holding all values.. - /// - public static string Context { - get { - return ResourceManager.GetString("Context", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Validates the content item.. + /// + public static string ContentValidate { + get { + return ResourceManager.GetString("ContentValidate", resourceCulture); } - - /// - /// Looks up a localized string similar to The edit token.. - /// - public static string EditToken { - get { - return ResourceManager.GetString("EditToken", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The context object holding all values.. + /// + public static string Context { + get { + return ResourceManager.GetString("Context", resourceCulture); } - - /// - /// Looks up a localized string similar to The timestamp when the object was created.. - /// - public static string EntityCreated { - get { - return ResourceManager.GetString("EntityCreated", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The edit token.. + /// + public static string EditToken { + get { + return ResourceManager.GetString("EditToken", resourceCulture); } - - /// - /// Looks up a localized string similar to The user who created the object.. - /// - public static string EntityCreatedBy { - get { - return ResourceManager.GetString("EntityCreatedBy", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The timestamp when the object was created.. + /// + public static string EntityCreated { + get { + return ResourceManager.GetString("EntityCreated", resourceCulture); } - - /// - /// Looks up a localized string similar to The expected version.. - /// - public static string EntityExpectedVersion { - get { - return ResourceManager.GetString("EntityExpectedVersion", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The user who created the object.. + /// + public static string EntityCreatedBy { + get { + return ResourceManager.GetString("EntityCreatedBy", resourceCulture); } - - /// - /// Looks up a localized string similar to The ID of the object (usually GUID).. - /// - public static string EntityId { - get { - return ResourceManager.GetString("EntityId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The expected version.. + /// + public static string EntityExpectedVersion { + get { + return ResourceManager.GetString("EntityExpectedVersion", resourceCulture); } - - /// - /// Looks up a localized string similar to True when deleted.. - /// - public static string EntityIsDeleted { - get { - return ResourceManager.GetString("EntityIsDeleted", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The ID of the object (usually GUID).. + /// + public static string EntityId { + get { + return ResourceManager.GetString("EntityId", resourceCulture); } - - /// - /// Looks up a localized string similar to The timestamp when the object was updated the last time.. - /// - public static string EntityLastModified { - get { - return ResourceManager.GetString("EntityLastModified", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to True when deleted.. + /// + public static string EntityIsDeleted { + get { + return ResourceManager.GetString("EntityIsDeleted", resourceCulture); } - - /// - /// Looks up a localized string similar to The user who updated the object the last time.. - /// - public static string EntityLastModifiedBy { - get { - return ResourceManager.GetString("EntityLastModifiedBy", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The timestamp when the object was updated the last time.. + /// + public static string EntityLastModified { + get { + return ResourceManager.GetString("EntityLastModified", resourceCulture); } - - /// - /// Looks up a localized string similar to True when the entity should be deleted permanently.. - /// - public static string EntityRequestDeletePermanent { - get { - return ResourceManager.GetString("EntityRequestDeletePermanent", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The user who updated the object the last time.. + /// + public static string EntityLastModifiedBy { + get { + return ResourceManager.GetString("EntityLastModifiedBy", resourceCulture); } - - /// - /// Looks up a localized string similar to The version of the objec.. - /// - public static string EntityVersion { - get { - return ResourceManager.GetString("EntityVersion", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to True when the entity should be deleted permanently.. + /// + public static string EntityRequestDeletePermanent { + get { + return ResourceManager.GetString("EntityRequestDeletePermanent", resourceCulture); } - - /// - /// Looks up a localized string similar to The actual event.. - /// - public static string Event { - get { - return ResourceManager.GetString("Event", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The version of the objec.. + /// + public static string EntityVersion { + get { + return ResourceManager.GetString("EntityVersion", resourceCulture); } - - /// - /// Looks up a localized string similar to The name of the event.. - /// - public static string EventName { - get { - return ResourceManager.GetString("EventName", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The actual event.. + /// + public static string Event { + get { + return ResourceManager.GetString("Event", resourceCulture); } - - /// - /// Looks up a localized string similar to The event timestamp.. - /// - public static string EventTimestamp { - get { - return ResourceManager.GetString("EventTimestamp", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The name of the event.. + /// + public static string EventName { + get { + return ResourceManager.GetString("EventName", resourceCulture); } - - /// - /// Looks up a localized string similar to The type of the event, e.g. 'Created' or 'Updated'.. - /// - public static string EventType { - get { - return ResourceManager.GetString("EventType", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The event timestamp.. + /// + public static string EventTimestamp { + get { + return ResourceManager.GetString("EventTimestamp", resourceCulture); } - - /// - /// Looks up a localized string similar to The path to the json value.. - /// - public static string JsonPath { - get { - return ResourceManager.GetString("JsonPath", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The type of the event, e.g. 'Created' or 'Updated'.. + /// + public static string EventType { + get { + return ResourceManager.GetString("EventType", resourceCulture); } - - /// - /// Looks up a localized string similar to The ID part of this ID.. - /// - public static string NamedId { - get { - return ResourceManager.GetString("NamedId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The path to the json value.. + /// + public static string JsonPath { + get { + return ResourceManager.GetString("JsonPath", resourceCulture); } - - /// - /// Looks up a localized string similar to The name part of this ID.. - /// - public static string NamedName { - get { - return ResourceManager.GetString("NamedName", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The ID part of this ID.. + /// + public static string NamedId { + get { + return ResourceManager.GetString("NamedId", resourceCulture); } - - /// - /// Looks up a localized string similar to The current operation.. - /// - public static string Operation { - get { - return ResourceManager.GetString("Operation", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The name part of this ID.. + /// + public static string NamedName { + get { + return ResourceManager.GetString("NamedName", resourceCulture); } - - /// - /// Looks up a localized string similar to Optional OData filter.. - /// - public static string QueryFilter { - get { - return ResourceManager.GetString("QueryFilter", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The current operation.. + /// + public static string Operation { + get { + return ResourceManager.GetString("Operation", resourceCulture); } - - /// - /// Looks up a localized string similar to Comma separated list of object IDs. Overrides all other query parameters.. - /// - public static string QueryIds { - get { - return ResourceManager.GetString("QueryIds", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Optional OData filter.. + /// + public static string QueryFilter { + get { + return ResourceManager.GetString("QueryFilter", resourceCulture); } - - /// - /// Looks up a localized string similar to Optional OData order definition.. - /// - public static string QueryOrderBy { - get { - return ResourceManager.GetString("QueryOrderBy", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Comma separated list of object IDs. Overrides all other query parameters.. + /// + public static string QueryIds { + get { + return ResourceManager.GetString("QueryIds", resourceCulture); } - - /// - /// Looks up a localized string similar to JSON query as well formatted json string. Overrides all other query parameters, except 'ids'.. - /// - public static string QueryQ { - get { - return ResourceManager.GetString("QueryQ", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Optional OData order definition.. + /// + public static string QueryOrderBy { + get { + return ResourceManager.GetString("QueryOrderBy", resourceCulture); } - - /// - /// Looks up a localized string similar to Optional OData full text search.. - /// - public static string QuerySearch { - get { - return ResourceManager.GetString("QuerySearch", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to JSON query as well formatted json string. Overrides all other query parameters, except 'ids'.. + /// + public static string QueryQ { + get { + return ResourceManager.GetString("QueryQ", resourceCulture); } - - /// - /// Looks up a localized string similar to Optional number of items to skip.. - /// - public static string QuerySkip { - get { - return ResourceManager.GetString("QuerySkip", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Optional OData full text search.. + /// + public static string QuerySearch { + get { + return ResourceManager.GetString("QuerySearch", resourceCulture); } - - /// - /// Looks up a localized string similar to Optional number of items to take.. - /// - public static string QueryTop { - get { - return ResourceManager.GetString("QueryTop", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Optional number of items to skip.. + /// + public static string QuerySkip { + get { + return ResourceManager.GetString("QuerySkip", resourceCulture); } - - /// - /// Looks up a localized string similar to The optional version of the content to retrieve an older instance (not cached).. - /// - public static string QueryVersion { - get { - return ResourceManager.GetString("QueryVersion", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Optional number of items to take.. + /// + public static string QueryTop { + get { + return ResourceManager.GetString("QueryTop", resourceCulture); } - - /// - /// Looks up a localized string similar to The ID of the schema.. - /// - public static string SchemaId { - get { - return ResourceManager.GetString("SchemaId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The optional version of the content to retrieve an older instance (not cached).. + /// + public static string QueryVersion { + get { + return ResourceManager.GetString("QueryVersion", resourceCulture); } - - /// - /// Looks up a localized string similar to The referenced assets.. - /// - public static string StringFieldAssets { - get { - return ResourceManager.GetString("StringFieldAssets", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The ID of the schema.. + /// + public static string SchemaId { + get { + return ResourceManager.GetString("SchemaId", resourceCulture); } - - /// - /// Looks up a localized string similar to The referenced content items.. - /// - public static string StringFieldReferences { - get { - return ResourceManager.GetString("StringFieldReferences", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The referenced assets.. + /// + public static string StringFieldAssets { + get { + return ResourceManager.GetString("StringFieldAssets", resourceCulture); } - - /// - /// Looks up a localized string similar to The text of this field.. - /// - public static string StringFieldText { - get { - return ResourceManager.GetString("StringFieldText", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The referenced content items.. + /// + public static string StringFieldReferences { + get { + return ResourceManager.GetString("StringFieldReferences", resourceCulture); } - - /// - /// Looks up a localized string similar to The translation status.. - /// - public static string TranslationStatus { - get { - return ResourceManager.GetString("TranslationStatus", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The text of this field.. + /// + public static string StringFieldText { + get { + return ResourceManager.GetString("StringFieldText", resourceCulture); } - - /// - /// Looks up a localized string similar to The translation status ({0}).. - /// - public static string TranslationStatusLanguage { - get { - return ResourceManager.GetString("TranslationStatusLanguage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The translation status.. + /// + public static string TranslationStatus { + get { + return ResourceManager.GetString("TranslationStatus", resourceCulture); } - - /// - /// Looks up a localized string similar to The current number of calls.. - /// - public static string UsageCallsCurrent { - get { - return ResourceManager.GetString("UsageCallsCurrent", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The translation status ({0}).. + /// + public static string TranslationStatusLanguage { + get { + return ResourceManager.GetString("TranslationStatusLanguage", resourceCulture); } - - /// - /// Looks up a localized string similar to The configured usage limit.. - /// - public static string UsageCallsLimit { - get { - return ResourceManager.GetString("UsageCallsLimit", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The current number of calls.. + /// + public static string UsageCallsCurrent { + get { + return ResourceManager.GetString("UsageCallsCurrent", resourceCulture); } - - /// - /// Looks up a localized string similar to Information about the current user.. - /// - public static string User { - get { - return ResourceManager.GetString("User", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The configured usage limit.. + /// + public static string UsageCallsLimit { + get { + return ResourceManager.GetString("UsageCallsLimit", resourceCulture); } - - /// - /// Looks up a localized string similar to The additional properties of this user.. - /// - public static string UserClaims { - get { - return ResourceManager.GetString("UserClaims", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Information about the current user.. + /// + public static string User { + get { + return ResourceManager.GetString("User", resourceCulture); } - - /// - /// Looks up a localized string similar to The display name of this user.. - /// - public static string UserDisplayName { - get { - return ResourceManager.GetString("UserDisplayName", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The additional properties of this user.. + /// + public static string UserClaims { + get { + return ResourceManager.GetString("UserClaims", resourceCulture); } - - /// - /// Looks up a localized string similar to The email address ofthis user.. - /// - public static string UserEmail { - get { - return ResourceManager.GetString("UserEmail", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The display name of this user.. + /// + public static string UserDisplayName { + get { + return ResourceManager.GetString("UserDisplayName", resourceCulture); } - - /// - /// Looks up a localized string similar to The ID of this user.. - /// - public static string UserId { - get { - return ResourceManager.GetString("UserId", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The email address ofthis user.. + /// + public static string UserEmail { + get { + return ResourceManager.GetString("UserEmail", resourceCulture); } - - /// - /// Looks up a localized string similar to True when this user is a client, which is typically the case when the request is made from the API.. - /// - public static string UserIsClient { - get { - return ResourceManager.GetString("UserIsClient", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The ID of this user.. + /// + public static string UserId { + get { + return ResourceManager.GetString("UserId", resourceCulture); } - - /// - /// Looks up a localized string similar to True when this user is a user, which is typically the case when the request is made in the UI.. - /// - public static string UserIsUser { - get { - return ResourceManager.GetString("UserIsUser", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to True when this user is a client, which is typically the case when the request is made from the API.. + /// + public static string UserIsClient { + get { + return ResourceManager.GetString("UserIsClient", resourceCulture); } - - /// - /// Looks up a localized string similar to The list of additional properties that have the name 'name'.. - /// - public static string UsersClaimsValue { - get { - return ResourceManager.GetString("UsersClaimsValue", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to True when this user is a user, which is typically the case when the request is made in the UI.. + /// + public static string UserIsUser { + get { + return ResourceManager.GetString("UserIsUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The list of additional properties that have the name 'name'.. + /// + public static string UsersClaimsValue { + get { + return ResourceManager.GetString("UsersClaimsValue", resourceCulture); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs index 0d11f8f18f..ae102c1f37 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs @@ -5,22 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +public interface IFieldPartitioning { - public interface IFieldPartitioning - { - string Master { get; } + string Master { get; } - IEnumerable AllKeys { get; } + IEnumerable AllKeys { get; } - IEnumerable GetPriorities(string key); + IEnumerable GetPriorities(string key); - bool IsMaster(string key); + bool IsMaster(string key); - bool IsOptional(string key); + bool IsOptional(string key); - bool Contains(string key); + bool Contains(string key); - string? GetName(string key); - } + string? GetName(string key); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs index db42a8c960..0f53ab6d68 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs @@ -5,63 +5,62 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +public sealed class InvariantPartitioning : IFieldPartitioning { - public sealed class InvariantPartitioning : IFieldPartitioning + public static readonly InvariantPartitioning Instance = new InvariantPartitioning(); + public static readonly string Key = "iv"; + public static readonly string Name = "Invariant"; + public static readonly string Description = "invariant value"; + + public string Master { - public static readonly InvariantPartitioning Instance = new InvariantPartitioning(); - public static readonly string Key = "iv"; - public static readonly string Name = "Invariant"; - public static readonly string Description = "invariant value"; + get => Key; + } - public string Master - { - get => Key; - } + public IEnumerable AllKeys + { + get { yield return Key; } + } - public IEnumerable AllKeys + public string? GetName(string key) + { + if (Contains(key)) { - get { yield return Key; } + return Name; } - public string? GetName(string key) - { - if (Contains(key)) - { - return Name; - } - - return null; - } + return null; + } - public IEnumerable GetPriorities(string key) + public IEnumerable GetPriorities(string key) + { + if (Contains(key)) { - if (Contains(key)) - { - yield return Key; - } - - yield break; + yield return Key; } - public bool Contains(string key) - { - return Equals(Key, key); - } + yield break; + } - public bool IsMaster(string key) - { - return Contains(key); - } + public bool Contains(string key) + { + return Equals(Key, key); + } - public bool IsOptional(string key) - { - return false; - } + public bool IsMaster(string key) + { + return Contains(key); + } - public override string ToString() - { - return Description; - } + public bool IsOptional(string key) + { + return false; + } + + public override string ToString() + { + return Description; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs index 8bdf84d796..e2ff7599b4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs @@ -9,34 +9,33 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core -{ - public delegate IFieldPartitioning PartitionResolver(Partitioning key); +namespace Squidex.Domain.Apps.Core; - public sealed record Partitioning - { - public static readonly Partitioning Invariant = new Partitioning("invariant"); - public static readonly Partitioning Language = new Partitioning("language"); +public delegate IFieldPartitioning PartitionResolver(Partitioning key); - public string Key { get; } +public sealed record Partitioning +{ + public static readonly Partitioning Invariant = new Partitioning("invariant"); + public static readonly Partitioning Language = new Partitioning("language"); - public Partitioning(string key) - { - Guard.NotNullOrEmpty(key); + public string Key { get; } - Key = key; - } + public Partitioning(string key) + { + Guard.NotNullOrEmpty(key); - public override string ToString() - { - return Key; - } + Key = key; + } - public static Partitioning FromString(string? value) - { - var isLanguage = string.Equals(value, Language.Key, StringComparison.OrdinalIgnoreCase); + public override string ToString() + { + return Key; + } + + public static Partitioning FromString(string? value) + { + var isLanguage = string.Equals(value, Language.Key, StringComparison.OrdinalIgnoreCase); - return isLanguage ? Language : Invariant; - } + return isLanguage ? Language : Invariant; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs index a998642564..6e98ea1980 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +public static class PartitioningExtensions { - public static class PartitioningExtensions + private static readonly HashSet AllowedPartitions = new HashSet(StringComparer.OrdinalIgnoreCase) { - private static readonly HashSet AllowedPartitions = new HashSet(StringComparer.OrdinalIgnoreCase) - { - Partitioning.Language.Key, - Partitioning.Invariant.Key - }; + Partitioning.Language.Key, + Partitioning.Invariant.Key + }; - public static bool IsValidPartitioning(this string? value) - { - return value == null || AllowedPartitions.Contains(value); - } + public static bool IsValidPartitioning(this string? value) + { + return value == null || AllowedPartitions.Contains(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs index 55f424e7b3..a5f14368f1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs @@ -9,73 +9,72 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public sealed class EnrichedAssetEvent : EnrichedUserEventBase, IEnrichedEntityEvent { - public sealed class EnrichedAssetEvent : EnrichedUserEventBase, IEnrichedEntityEvent - { - [FieldDescription(nameof(FieldDescriptions.EventType))] - public EnrichedAssetEventType Type { get; set; } + [FieldDescription(nameof(FieldDescriptions.EventType))] + public EnrichedAssetEventType Type { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityId))] - public DomainId Id { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityId))] + public DomainId Id { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityCreated))] - public Instant Created { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityCreated))] + public Instant Created { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityLastModified))] - public Instant LastModified { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityLastModified))] + public Instant LastModified { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityCreatedBy))] - public RefToken CreatedBy { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityCreatedBy))] + public RefToken CreatedBy { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityLastModifiedBy))] - public RefToken LastModifiedBy { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityLastModifiedBy))] + public RefToken LastModifiedBy { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetParentId))] - public DomainId ParentId { get; } + [FieldDescription(nameof(FieldDescriptions.AssetParentId))] + public DomainId ParentId { get; } - [FieldDescription(nameof(FieldDescriptions.AssetMimeType))] - public string MimeType { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetMimeType))] + public string MimeType { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetFileName))] - public string FileName { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetFileName))] + public string FileName { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetFileHash))] - public string FileHash { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetFileHash))] + public string FileHash { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetSlug))] - public string Slug { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetSlug))] + public string Slug { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetFileVersion))] - public long FileVersion { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetFileVersion))] + public long FileVersion { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetFileSize))] - public long FileSize { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetFileSize))] + public long FileSize { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetIsProtected))] - public bool IsProtected { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetIsProtected))] + public bool IsProtected { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetPixelWidth))] - public int? PixelWidth { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetPixelWidth))] + public int? PixelWidth { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetPixelHeight))] - public int? PixelHeight { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetPixelHeight))] + public int? PixelHeight { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetType))] - public AssetType AssetType { get; set; } + [FieldDescription(nameof(FieldDescriptions.AssetType))] + public AssetType AssetType { get; set; } - [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] - public AssetMetadata Metadata { get; } + [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] + public AssetMetadata Metadata { get; } - [FieldDescription(nameof(FieldDescriptions.AssetIsImage))] - public bool IsImage - { - get => AssetType == AssetType.Image; - } + [FieldDescription(nameof(FieldDescriptions.AssetIsImage))] + public bool IsImage + { + get => AssetType == AssetType.Image; + } - public override long Partition - { - get => Id.GetHashCode(); - } + public override long Partition + { + get => Id.GetHashCode(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEventType.cs index f3e0d321d7..3b9fb0b6f1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEventType.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEventType.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public enum EnrichedAssetEventType { - public enum EnrichedAssetEventType - { - Created, - Deleted, - Annotated, - Updated - } + Created, + Deleted, + Annotated, + Updated } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs index 0e7abd25f9..c03ad8e537 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs @@ -11,33 +11,32 @@ #pragma warning disable CA1822 // Mark members as static #pragma warning disable SA1133 // Do not combine attributes -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public sealed class EnrichedCommentEvent : EnrichedUserEventBase { - public sealed class EnrichedCommentEvent : EnrichedUserEventBase + [FieldDescription(nameof(FieldDescriptions.CommentText))] + public string Text { get; set; } + + [FieldDescription(nameof(FieldDescriptions.CommentUrl))] + public Uri? Url { get; set; } + + [FieldDescription(nameof(FieldDescriptions.CommentMentionedUser)), JsonIgnore] + public IUser MentionedUser { get; set; } + + [JsonIgnore] + public override long Partition + { + get => MentionedUser?.Id.GetHashCode(StringComparison.Ordinal) ?? 0; + } + + public bool ShouldSerializeMentionedUser() + { + return false; + } + + public bool ShouldSerializePartition() { - [FieldDescription(nameof(FieldDescriptions.CommentText))] - public string Text { get; set; } - - [FieldDescription(nameof(FieldDescriptions.CommentUrl))] - public Uri? Url { get; set; } - - [FieldDescription(nameof(FieldDescriptions.CommentMentionedUser)), JsonIgnore] - public IUser MentionedUser { get; set; } - - [JsonIgnore] - public override long Partition - { - get => MentionedUser?.Id.GetHashCode(StringComparison.Ordinal) ?? 0; - } - - public bool ShouldSerializeMentionedUser() - { - return false; - } - - public bool ShouldSerializePartition() - { - return false; - } + return false; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs index 804517dbeb..8ed2fc7535 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs @@ -9,43 +9,42 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public sealed class EnrichedContentEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent { - public sealed class EnrichedContentEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent - { - [FieldDescription(nameof(FieldDescriptions.EventType))] - public EnrichedContentEventType Type { get; set; } + [FieldDescription(nameof(FieldDescriptions.EventType))] + public EnrichedContentEventType Type { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityId))] - public DomainId Id { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityId))] + public DomainId Id { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityCreated))] - public Instant Created { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityCreated))] + public Instant Created { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityLastModified))] - public Instant LastModified { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityLastModified))] + public Instant LastModified { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityCreatedBy))] - public RefToken CreatedBy { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityCreatedBy))] + public RefToken CreatedBy { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityLastModifiedBy))] - public RefToken LastModifiedBy { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityLastModifiedBy))] + public RefToken LastModifiedBy { get; set; } - [FieldDescription(nameof(FieldDescriptions.ContentData))] - public ContentData Data { get; set; } + [FieldDescription(nameof(FieldDescriptions.ContentData))] + public ContentData Data { get; set; } - [FieldDescription(nameof(FieldDescriptions.ContentDataOld))] - public ContentData? DataOld { get; set; } + [FieldDescription(nameof(FieldDescriptions.ContentDataOld))] + public ContentData? DataOld { get; set; } - [FieldDescription(nameof(FieldDescriptions.ContentStatus))] - public Status Status { get; set; } + [FieldDescription(nameof(FieldDescriptions.ContentStatus))] + public Status Status { get; set; } - [FieldDescription(nameof(FieldDescriptions.ContentNewStatus))] - public Status? NewStatus { get; set; } + [FieldDescription(nameof(FieldDescriptions.ContentNewStatus))] + public Status? NewStatus { get; set; } - public override long Partition - { - get => Id.GetHashCode(); - } + public override long Partition + { + get => Id.GetHashCode(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEventType.cs index ad6c54f96a..988fd85947 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEventType.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEventType.cs @@ -5,15 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public enum EnrichedContentEventType { - public enum EnrichedContentEventType - { - Created, - Deleted, - Published, - StatusChanged, - Updated, - Unpublished - } + Created, + Deleted, + Published, + StatusChanged, + Updated, + Unpublished } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedEvent.cs index ec5f8aeb5d..773d158d9c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedEvent.cs @@ -8,22 +8,21 @@ using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public abstract class EnrichedEvent { - public abstract class EnrichedEvent - { - [FieldDescription(nameof(FieldDescriptions.AppId))] - public NamedId AppId { get; set; } + [FieldDescription(nameof(FieldDescriptions.AppId))] + public NamedId AppId { get; set; } - [FieldDescription(nameof(FieldDescriptions.EventTimestamp))] - public Instant Timestamp { get; set; } + [FieldDescription(nameof(FieldDescriptions.EventTimestamp))] + public Instant Timestamp { get; set; } - [FieldDescription(nameof(FieldDescriptions.EventName))] - public string Name { get; set; } + [FieldDescription(nameof(FieldDescriptions.EventName))] + public string Name { get; set; } - [FieldDescription(nameof(FieldDescriptions.EntityVersion))] - public long Version { get; set; } + [FieldDescription(nameof(FieldDescriptions.EntityVersion))] + public long Version { get; set; } - public abstract long Partition { get; } - } + public abstract long Partition { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs index f9f4e924c4..2c72aa4755 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedManualEvent.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public sealed class EnrichedManualEvent : EnrichedUserEventBase { - public sealed class EnrichedManualEvent : EnrichedUserEventBase + public override long Partition { - public override long Partition - { - get => 0; - } + get => 0; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs index 7a20b0adba..a3d428cde5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs @@ -7,21 +7,20 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public sealed class EnrichedSchemaEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent { - public sealed class EnrichedSchemaEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent - { - [FieldDescription(nameof(FieldDescriptions.EventType))] - public EnrichedSchemaEventType Type { get; set; } + [FieldDescription(nameof(FieldDescriptions.EventType))] + public EnrichedSchemaEventType Type { get; set; } - public DomainId Id - { - get => SchemaId.Id; - } + public DomainId Id + { + get => SchemaId.Id; + } - public override long Partition - { - get => SchemaId?.GetHashCode() ?? 0; - } + public override long Partition + { + get => SchemaId?.GetHashCode() ?? 0; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventBase.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventBase.cs index 9bf33879f4..6c1059f0e1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventBase.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventBase.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public abstract class EnrichedSchemaEventBase : EnrichedUserEventBase { - public abstract class EnrichedSchemaEventBase : EnrichedUserEventBase - { - [FieldDescription(nameof(FieldDescriptions.EntityVersion))] - public NamedId SchemaId { get; set; } - } + [FieldDescription(nameof(FieldDescriptions.EntityVersion))] + public NamedId SchemaId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventType.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventType.cs index 7d6b3f5cf7..a0cbeaf56a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventType.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventType.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public enum EnrichedSchemaEventType { - public enum EnrichedSchemaEventType - { - Created, - Deleted, - Published, - Unpublished, - Updated - } + Created, + Deleted, + Published, + Unpublished, + Updated } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs index 15eaf96a0b..a31a0826c5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public sealed class EnrichedUsageExceededEvent : EnrichedEvent { - public sealed class EnrichedUsageExceededEvent : EnrichedEvent - { - [FieldDescription(nameof(FieldDescriptions.UsageCallsCurrent))] - public long CallsCurrent { get; set; } + [FieldDescription(nameof(FieldDescriptions.UsageCallsCurrent))] + public long CallsCurrent { get; set; } - [FieldDescription(nameof(FieldDescriptions.UsageCallsLimit))] - public long CallsLimit { get; set; } + [FieldDescription(nameof(FieldDescriptions.UsageCallsLimit))] + public long CallsLimit { get; set; } - public override long Partition - { - get => AppId?.GetHashCode() ?? 0; - } + public override long Partition + { + get => AppId?.GetHashCode() ?? 0; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs index 26652d1a8c..3b69e682e6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs @@ -12,19 +12,18 @@ #pragma warning disable CA1822 // Mark members as static #pragma warning disable SA1133 // Do not combine attributes -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public abstract class EnrichedUserEventBase : EnrichedEvent { - public abstract class EnrichedUserEventBase : EnrichedEvent - { - [FieldDescription(nameof(FieldDescriptions.Actor))] - public RefToken Actor { get; set; } + [FieldDescription(nameof(FieldDescriptions.Actor))] + public RefToken Actor { get; set; } - [FieldDescription(nameof(FieldDescriptions.User)), JsonIgnore] - public IUser? User { get; set; } + [FieldDescription(nameof(FieldDescriptions.User)), JsonIgnore] + public IUser? User { get; set; } - public bool ShouldSerializeUser() - { - return false; - } + public bool ShouldSerializeUser() + { + return false; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/IEnrichedEntityEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/IEnrichedEntityEvent.cs index 452aec1c6b..ea27f47f31 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/IEnrichedEntityEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/IEnrichedEntityEvent.cs @@ -7,9 +7,8 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents +namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +public interface IEnrichedEntityEvent : IWithId { - public interface IEnrichedEntityEvent : IWithId - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs index 82949b55d8..a70dc9f90b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs @@ -7,20 +7,19 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; -namespace Squidex.Domain.Apps.Core.Rules +namespace Squidex.Domain.Apps.Core.Rules; + +public interface IRuleTriggerVisitor { - public interface IRuleTriggerVisitor - { - T Visit(AssetChangedTriggerV2 trigger); + T Visit(AssetChangedTriggerV2 trigger); - T Visit(ContentChangedTriggerV2 trigger); + T Visit(ContentChangedTriggerV2 trigger); - T Visit(CommentTrigger trigger); + T Visit(CommentTrigger trigger); - T Visit(ManualTrigger trigger); + T Visit(ManualTrigger trigger); - T Visit(SchemaChangedTrigger trigger); + T Visit(SchemaChangedTrigger trigger); - T Visit(UsageTrigger trigger); - } + T Visit(UsageTrigger trigger); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleSorrgate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleSorrgate.cs index ce53f9306b..f454c912d0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleSorrgate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleSorrgate.cs @@ -9,45 +9,44 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Rules.Json -{ - public sealed class RuleSorrgate : ISurrogate - { - public RuleTrigger Trigger { get; set; } +namespace Squidex.Domain.Apps.Core.Rules.Json; - public RuleAction Action { get; set; } +public sealed class RuleSorrgate : ISurrogate +{ + public RuleTrigger Trigger { get; set; } - public bool IsEnabled { get; set; } + public RuleAction Action { get; set; } - public string Name { get; set; } + public bool IsEnabled { get; set; } - public void FromSource(Rule source) - { - SimpleMapper.Map(source, this); - } + public string Name { get; set; } - public Rule ToSource() - { - var trigger = Trigger; + public void FromSource(Rule source) + { + SimpleMapper.Map(source, this); + } - if (trigger is IMigrated migrated) - { - trigger = migrated.Migrate(); - } + public Rule ToSource() + { + var trigger = Trigger; - var rule = new Rule(trigger, Action); + if (trigger is IMigrated migrated) + { + trigger = migrated.Migrate(); + } - if (!IsEnabled) - { - rule = rule.Disable(); - } + var rule = new Rule(trigger, Action); - if (Name != null) - { - rule = rule.Rename(Name); - } + if (!IsEnabled) + { + rule = rule.Disable(); + } - return rule; + if (Name != null) + { + rule = rule.Rename(Name); } + + return rule; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs index 105784a1b0..42c7ee86ae 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -8,119 +8,118 @@ using System.Diagnostics.Contracts; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules +namespace Squidex.Domain.Apps.Core.Rules; + +public sealed class Rule { - public sealed class Rule - { - public string? Name { get; private set; } + public string? Name { get; private set; } + + public RuleTrigger Trigger { get; private set; } + + public RuleAction Action { get; private set; } - public RuleTrigger Trigger { get; private set; } + public bool IsEnabled { get; private set; } = true; - public RuleAction Action { get; private set; } + public Rule(RuleTrigger trigger, RuleAction action) + { + Guard.NotNull(trigger); + Guard.NotNull(action); + + Action = action; - public bool IsEnabled { get; private set; } = true; + Trigger = trigger; + } - public Rule(RuleTrigger trigger, RuleAction action) + [Pure] + public Rule Rename(string newName) + { + if (string.Equals(Name, newName, StringComparison.Ordinal)) { - Guard.NotNull(trigger); - Guard.NotNull(action); + return this; + } - Action = action; + return Clone(clone => + { + clone.Name = newName; + }); + } - Trigger = trigger; + [Pure] + public Rule Enable() + { + if (IsEnabled) + { + return this; } - [Pure] - public Rule Rename(string newName) + return Clone(clone => { - if (string.Equals(Name, newName, StringComparison.Ordinal)) - { - return this; - } - - return Clone(clone => - { - clone.Name = newName; - }); + clone.IsEnabled = true; + }); + } + + [Pure] + public Rule Disable() + { + if (!IsEnabled) + { + return this; } - [Pure] - public Rule Enable() + return Clone(clone => + { + clone.IsEnabled = false; + }); + } + + [Pure] + public Rule Update(RuleTrigger newTrigger) + { + Guard.NotNull(newTrigger); + + if (newTrigger.GetType() != Trigger.GetType()) { - if (IsEnabled) - { - return this; - } - - return Clone(clone => - { - clone.IsEnabled = true; - }); + ThrowHelper.ArgumentException("New trigger has another type.", nameof(newTrigger)); } - [Pure] - public Rule Disable() + if (Trigger.Equals(newTrigger)) { - if (!IsEnabled) - { - return this; - } - - return Clone(clone => - { - clone.IsEnabled = false; - }); + return this; } - [Pure] - public Rule Update(RuleTrigger newTrigger) + return Clone(clone => { - Guard.NotNull(newTrigger); - - if (newTrigger.GetType() != Trigger.GetType()) - { - ThrowHelper.ArgumentException("New trigger has another type.", nameof(newTrigger)); - } - - if (Trigger.Equals(newTrigger)) - { - return this; - } - - return Clone(clone => - { - clone.Trigger = newTrigger; - }); + clone.Trigger = newTrigger; + }); + } + + [Pure] + public Rule Update(RuleAction newAction) + { + Guard.NotNull(newAction); + + if (newAction.GetType() != Action.GetType()) + { + ThrowHelper.ArgumentException("New action has another type.", nameof(newAction)); } - [Pure] - public Rule Update(RuleAction newAction) + if (Action.Equals(newAction)) { - Guard.NotNull(newAction); - - if (newAction.GetType() != Action.GetType()) - { - ThrowHelper.ArgumentException("New action has another type.", nameof(newAction)); - } - - if (Action.Equals(newAction)) - { - return this; - } - - return Clone(clone => - { - clone.Action = newAction; - }); + return this; } - private Rule Clone(Action updater) + return Clone(clone => { - var clone = (Rule)MemberwiseClone(); + clone.Action = newAction; + }); + } - updater(clone); + private Rule Clone(Action updater) + { + var clone = (Rule)MemberwiseClone(); - return clone; - } + updater(clone); + + return clone; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs index 724fe4aca1..65a7839319 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs @@ -8,35 +8,34 @@ using System.ComponentModel.DataAnnotations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Core.Rules +namespace Squidex.Domain.Apps.Core.Rules; + +public abstract record RuleAction { - public abstract record RuleAction + public IEnumerable Validate() { - public IEnumerable Validate() - { - var context = new ValidationContext(this); - var errors = new List(); + var context = new ValidationContext(this); + var errors = new List(); - if (!Validator.TryValidateObject(this, context, errors, true)) + if (!Validator.TryValidateObject(this, context, errors, true)) + { + foreach (var error in errors) { - foreach (var error in errors) + if (!string.IsNullOrWhiteSpace(error.ErrorMessage)) { - if (!string.IsNullOrWhiteSpace(error.ErrorMessage)) - { - yield return new ValidationError(error.ErrorMessage, error.MemberNames.ToArray()); - } + yield return new ValidationError(error.ErrorMessage, error.MemberNames.ToArray()); } } - - foreach (var error in CustomValidate()) - { - yield return error; - } } - protected virtual IEnumerable CustomValidate() + foreach (var error in CustomValidate()) { - yield break; + yield return error; } } + + protected virtual IEnumerable CustomValidate() + { + yield break; + } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs index b0445455f7..30a3f1bae6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs @@ -8,28 +8,27 @@ using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules +namespace Squidex.Domain.Apps.Core.Rules; + +public sealed class RuleJob { - public sealed class RuleJob - { - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public DomainId AppId { get; set; } + public DomainId AppId { get; set; } - public DomainId RuleId { get; set; } + public DomainId RuleId { get; set; } - public string EventName { get; set; } + public string EventName { get; set; } - public string ActionName { get; set; } + public string ActionName { get; set; } - public string ActionData { get; set; } + public string ActionData { get; set; } - public string Description { get; set; } + public string Description { get; set; } - public long ExecutionPartition { get; set; } + public long ExecutionPartition { get; set; } - public Instant Created { get; set; } + public Instant Created { get; set; } - public Instant Expires { get; set; } - } + public Instant Expires { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs index 260d151720..f4234aae24 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Rules +namespace Squidex.Domain.Apps.Core.Rules; + +public abstract record RuleTrigger { - public abstract record RuleTrigger - { - public abstract T Accept(IRuleTriggerVisitor visitor); - } + public abstract T Accept(IRuleTriggerVisitor visitor); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs index d88ccb64bd..5b7183e2e5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs @@ -7,16 +7,15 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Rules.Triggers +namespace Squidex.Domain.Apps.Core.Rules.Triggers; + +[TypeName(nameof(AssetChangedTriggerV2))] +public sealed record AssetChangedTriggerV2 : RuleTrigger { - [TypeName(nameof(AssetChangedTriggerV2))] - public sealed record AssetChangedTriggerV2 : RuleTrigger - { - public string Condition { get; init; } + public string Condition { get; init; } - public override T Accept(IRuleTriggerVisitor visitor) - { - return visitor.Visit(this); - } + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CommentTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CommentTrigger.cs index 7f4908ed54..c1246884b3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CommentTrigger.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CommentTrigger.cs @@ -7,16 +7,15 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Rules.Triggers +namespace Squidex.Domain.Apps.Core.Rules.Triggers; + +[TypeName(nameof(CommentTrigger))] +public sealed record CommentTrigger : RuleTrigger { - [TypeName(nameof(CommentTrigger))] - public sealed record CommentTrigger : RuleTrigger - { - public string Condition { get; init; } + public string Condition { get; init; } - public override T Accept(IRuleTriggerVisitor visitor) - { - return visitor.Visit(this); - } + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs index bd904e3ba0..55e3452d45 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs @@ -7,12 +7,11 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Rules.Triggers +namespace Squidex.Domain.Apps.Core.Rules.Triggers; + +public sealed record ContentChangedTriggerSchemaV2 { - public sealed record ContentChangedTriggerSchemaV2 - { - public DomainId SchemaId { get; init; } + public DomainId SchemaId { get; init; } - public string? Condition { get; init; } - } + public string? Condition { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs index e3f522ce61..e46d31db68 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs @@ -8,18 +8,17 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Rules.Triggers +namespace Squidex.Domain.Apps.Core.Rules.Triggers; + +[TypeName(nameof(ContentChangedTriggerV2))] +public sealed record ContentChangedTriggerV2 : RuleTrigger { - [TypeName(nameof(ContentChangedTriggerV2))] - public sealed record ContentChangedTriggerV2 : RuleTrigger - { - public ReadonlyList? Schemas { get; init; } + public ReadonlyList? Schemas { get; init; } - public bool HandleAll { get; init; } + public bool HandleAll { get; init; } - public override T Accept(IRuleTriggerVisitor visitor) - { - return visitor.Visit(this); - } + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs index fbb3778b26..11848ee083 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Rules.Triggers +namespace Squidex.Domain.Apps.Core.Rules.Triggers; + +[TypeName(nameof(ManualTrigger))] +public sealed record ManualTrigger : RuleTrigger { - [TypeName(nameof(ManualTrigger))] - public sealed record ManualTrigger : RuleTrigger + public override T Accept(IRuleTriggerVisitor visitor) { - public override T Accept(IRuleTriggerVisitor visitor) - { - return visitor.Visit(this); - } + return visitor.Visit(this); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs index 99126af67a..72f73426a1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs @@ -7,16 +7,15 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Rules.Triggers +namespace Squidex.Domain.Apps.Core.Rules.Triggers; + +[TypeName(nameof(SchemaChangedTrigger))] +public sealed record SchemaChangedTrigger : RuleTrigger { - [TypeName(nameof(SchemaChangedTrigger))] - public sealed record SchemaChangedTrigger : RuleTrigger - { - public string Condition { get; init; } + public string Condition { get; init; } - public override T Accept(IRuleTriggerVisitor visitor) - { - return visitor.Visit(this); - } + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs index 6b30ca3e78..4f27f7b969 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs @@ -7,18 +7,17 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Rules.Triggers +namespace Squidex.Domain.Apps.Core.Rules.Triggers; + +[TypeName(nameof(UsageTrigger))] +public sealed record UsageTrigger : RuleTrigger { - [TypeName(nameof(UsageTrigger))] - public sealed record UsageTrigger : RuleTrigger - { - public int Limit { get; init; } + public int Limit { get; init; } - public int? NumDays { get; init; } + public int? NumDays { get; init; } - public override T Accept(IRuleTriggerVisitor visitor) - { - return visitor.Visit(this); - } + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs index 5ce58377cd..b8edb2566f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs @@ -7,70 +7,69 @@ using System.Diagnostics.Contracts; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed class ArrayField : RootField, IArrayField { - public sealed class ArrayField : RootField, IArrayField + public IReadOnlyList Fields { - public IReadOnlyList Fields - { - get => FieldCollection.Ordered; - } + get => FieldCollection.Ordered; + } - public IReadOnlyDictionary FieldsById - { - get => FieldCollection.ById; - } + public IReadOnlyDictionary FieldsById + { + get => FieldCollection.ById; + } - public IReadOnlyDictionary FieldsByName - { - get => FieldCollection.ByName; - } + public IReadOnlyDictionary FieldsByName + { + get => FieldCollection.ByName; + } - public FieldCollection FieldCollection { get; private set; } = FieldCollection.Empty; + public FieldCollection FieldCollection { get; private set; } = FieldCollection.Empty; - public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) - : base(id, name, partitioning, properties, settings) - { - FieldCollection = new FieldCollection(fields); - } + public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + : base(id, name, partitioning, properties, settings) + { + FieldCollection = new FieldCollection(fields); + } - [Pure] - public ArrayField DeleteField(long fieldId) - { - return Updatefields(f => f.Remove(fieldId)); - } + [Pure] + public ArrayField DeleteField(long fieldId) + { + return Updatefields(f => f.Remove(fieldId)); + } - [Pure] - public ArrayField ReorderFields(List ids) - { - return Updatefields(f => f.Reorder(ids)); - } + [Pure] + public ArrayField ReorderFields(List ids) + { + return Updatefields(f => f.Reorder(ids)); + } - [Pure] - public ArrayField AddField(NestedField field) - { - return Updatefields(f => f.Add(field)); - } + [Pure] + public ArrayField AddField(NestedField field) + { + return Updatefields(f => f.Add(field)); + } - [Pure] - public ArrayField UpdateField(long fieldId, Func updater) + [Pure] + public ArrayField UpdateField(long fieldId, Func updater) + { + return Updatefields(f => f.Update(fieldId, updater)); + } + + private ArrayField Updatefields(Func, FieldCollection> updater) + { + var newFields = updater(FieldCollection); + + if (ReferenceEquals(newFields, FieldCollection)) { - return Updatefields(f => f.Update(fieldId, updater)); + return this; } - private ArrayField Updatefields(Func, FieldCollection> updater) + return (ArrayField)Clone(clone => { - var newFields = updater(FieldCollection); - - if (ReferenceEquals(newFields, FieldCollection)) - { - return this; - } - - return (ArrayField)Clone(clone => - { - ((ArrayField)clone).FieldCollection = newFields; - }); - } + ((ArrayField)clone).FieldCollection = newFields; + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs index f27288f694..d5be61a81c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs @@ -7,34 +7,33 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record ArrayFieldProperties : FieldProperties { - public sealed record ArrayFieldProperties : FieldProperties - { - public int? MinItems { get; init; } + public int? MinItems { get; init; } - public int? MaxItems { get; init; } + public int? MaxItems { get; init; } - public ReadonlyList? UniqueFields { get; init; } + public ReadonlyList? UniqueFields { get; init; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IArrayField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IArrayField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Array(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Array(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - throw new NotSupportedException(); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + throw new NotSupportedException(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetPreviewMode.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetPreviewMode.cs index 738f891eaa..83120fb304 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetPreviewMode.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetPreviewMode.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum AssetPreviewMode { - public enum AssetPreviewMode - { - ImageAndFileName, - Image, - FileName - } + ImageAndFileName, + Image, + FileName } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs index 6a7af2ff88..1f3fbc9aff 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs @@ -8,76 +8,75 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record AssetsFieldProperties : FieldProperties { - public sealed record AssetsFieldProperties : FieldProperties - { - public AssetPreviewMode PreviewMode { get; init; } + public AssetPreviewMode PreviewMode { get; init; } - public LocalizedValue?> DefaultValues { get; init; } + public LocalizedValue?> DefaultValues { get; init; } - public ReadonlyList? DefaultValue { get; init; } + public ReadonlyList? DefaultValue { get; init; } - public string? FolderId { get; init; } + public string? FolderId { get; init; } - public int? MinItems { get; init; } + public int? MinItems { get; init; } - public int? MaxItems { get; init; } + public int? MaxItems { get; init; } - public int? MinWidth { get; init; } + public int? MinWidth { get; init; } - public int? MaxWidth { get; init; } + public int? MaxWidth { get; init; } - public int? MinHeight { get; init; } + public int? MinHeight { get; init; } - public int? MaxHeight { get; init; } + public int? MaxHeight { get; init; } - public int? MinSize { get; init; } + public int? MinSize { get; init; } - public int? MaxSize { get; init; } + public int? MaxSize { get; init; } - public int? AspectWidth { get; init; } + public int? AspectWidth { get; init; } - public int? AspectHeight { get; init; } + public int? AspectHeight { get; init; } - public AssetType? ExpectedType { get; set; } + public AssetType? ExpectedType { get; set; } - public bool AllowDuplicates { get; init; } + public bool AllowDuplicates { get; init; } - public bool ResolveFirst { get; init; } + public bool ResolveFirst { get; init; } - [Obsolete("Use 'AllowDuplicates' field now")] - public bool MustBeImage - { - init => ExpectedType = value ? AssetType.Image : ExpectedType; - } + [Obsolete("Use 'AllowDuplicates' field now")] + public bool MustBeImage + { + init => ExpectedType = value ? AssetType.Image : ExpectedType; + } - [Obsolete("Use 'ResolveFirst' field now")] - public bool ResolveImage - { - init => ResolveFirst = value; - } + [Obsolete("Use 'ResolveFirst' field now")] + public bool ResolveImage + { + init => ResolveFirst = value; + } - public ReadonlyList? AllowedExtensions { get; set; } + public ReadonlyList? AllowedExtensions { get; set; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Assets(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Assets(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.Assets(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Assets(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs index 25c1b46b7e..2400eb7889 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum BooleanFieldEditor { - public enum BooleanFieldEditor - { - Checkbox, - Toggle - } + Checkbox, + Toggle } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs index b8058c8244..95685c06c8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs @@ -5,36 +5,35 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record BooleanFieldProperties : FieldProperties { - public sealed record BooleanFieldProperties : FieldProperties - { - public LocalizedValue DefaultValues { get; init; } + public LocalizedValue DefaultValues { get; init; } - public bool? DefaultValue { get; init; } + public bool? DefaultValue { get; init; } - public bool InlineEditable { get; init; } + public bool InlineEditable { get; init; } - public BooleanFieldEditor Editor { get; init; } + public BooleanFieldEditor Editor { get; init; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Boolean(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Boolean(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.Boolean(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Boolean(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentFieldProperties.cs index 63f1a00f63..f459fb9112 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentFieldProperties.cs @@ -8,42 +8,41 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record ComponentFieldProperties : FieldProperties { - public sealed record ComponentFieldProperties : FieldProperties + public DomainId SchemaId { - public DomainId SchemaId + init { - init - { - SchemaIds = value != default ? ReadonlyList.Create(value) : null; - } - get - { - return SchemaIds?.FirstOrDefault() ?? default; - } + SchemaIds = value != default ? ReadonlyList.Create(value) : null; } - - public ReadonlyList? SchemaIds { get; init; } - - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + get { - return visitor.Visit(this, args); + return SchemaIds?.FirstOrDefault() ?? default; } + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public ReadonlyList? SchemaIds { get; init; } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Component(id, name, partitioning, this, settings); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.Component(id, name, this, settings); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Component(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Component(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs index 70064de985..4cbff35859 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs @@ -8,48 +8,47 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record ComponentsFieldProperties : FieldProperties { - public sealed record ComponentsFieldProperties : FieldProperties - { - public int? MinItems { get; init; } + public int? MinItems { get; init; } - public int? MaxItems { get; init; } + public int? MaxItems { get; init; } - public ReadonlyList? UniqueFields { get; init; } + public ReadonlyList? UniqueFields { get; init; } - public DomainId SchemaId + public DomainId SchemaId + { + init { - init - { - SchemaIds = value != default ? ReadonlyList.Create(value) : null; - } - get - { - return SchemaIds?.FirstOrDefault() ?? default; - } + SchemaIds = value != default ? ReadonlyList.Create(value) : null; } - - public ReadonlyList? SchemaIds { get; init; } - - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + get { - return visitor.Visit(this, args); + return SchemaIds?.FirstOrDefault() ?? default; } + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public ReadonlyList? SchemaIds { get; init; } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Components(id, name, partitioning, this, settings); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.Components(id, name, this, settings); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Components(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Components(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs index fe8dda7dae..e9f938e657 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum DateTimeCalculatedDefaultValue { - public enum DateTimeCalculatedDefaultValue - { - Now, - Today - } + Now, + Today } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs index d3281a92db..ecc9d7f500 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum DateTimeFieldEditor { - public enum DateTimeFieldEditor - { - Date, - DateTime - } + Date, + DateTime } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs index 06abc3f276..d9cd239ba9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs @@ -7,42 +7,41 @@ using NodaTime; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record DateTimeFieldProperties : FieldProperties { - public sealed record DateTimeFieldProperties : FieldProperties - { - public LocalizedValue DefaultValues { get; init; } + public LocalizedValue DefaultValues { get; init; } - public Instant? DefaultValue { get; init; } + public Instant? DefaultValue { get; init; } - public Instant? MaxValue { get; init; } + public Instant? MaxValue { get; init; } - public Instant? MinValue { get; init; } + public Instant? MinValue { get; init; } - public string? Format { get; set; } + public string? Format { get; set; } - public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; init; } + public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; init; } - public DateTimeFieldEditor Editor { get; init; } + public DateTimeFieldEditor Editor { get; init; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.DateTime(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.DateTime(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.DateTime(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.DateTime(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldBase.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldBase.cs index 9f52197e95..aa0af9e182 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldBase.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldBase.cs @@ -7,22 +7,21 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public abstract class FieldBase { - public abstract class FieldBase - { - public long Id { get; } + public long Id { get; } - public string Name { get; } + public string Name { get; } - protected FieldBase(long id, string name) - { - Guard.NotNullOrEmpty(name); - Guard.GreaterThan(id, 0); + protected FieldBase(long id, string name) + { + Guard.NotNullOrEmpty(name); + Guard.GreaterThan(id, 0); - Id = id; + Id = id; - Name = name; - } + Name = name; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs index 08a9efc2cc..b4ed096cbf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs @@ -8,152 +8,151 @@ using System.Diagnostics.Contracts; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed class FieldCollection where T : IField { - public sealed class FieldCollection where T : IField - { - public static readonly FieldCollection Empty = new FieldCollection(); + public static readonly FieldCollection Empty = new FieldCollection(); - private static readonly Dictionary EmptyById = new Dictionary(); - private static readonly Dictionary EmptyByString = new Dictionary(); + private static readonly Dictionary EmptyById = new Dictionary(); + private static readonly Dictionary EmptyByString = new Dictionary(); - private readonly T[] fieldsOrdered; - private Dictionary? fieldsById; - private Dictionary? fieldsByName; + private readonly T[] fieldsOrdered; + private Dictionary? fieldsById; + private Dictionary? fieldsByName; - public IReadOnlyList Ordered - { - get => fieldsOrdered; - } + public IReadOnlyList Ordered + { + get => fieldsOrdered; + } - public IReadOnlyDictionary ById + public IReadOnlyDictionary ById + { + get { - get + if (fieldsById == null) { - if (fieldsById == null) + if (fieldsOrdered.Length == 0) { - if (fieldsOrdered.Length == 0) - { - fieldsById = EmptyById; - } - else - { - fieldsById = fieldsOrdered.ToDictionary(x => x.Id); - } + fieldsById = EmptyById; + } + else + { + fieldsById = fieldsOrdered.ToDictionary(x => x.Id); } - - return fieldsById; } + + return fieldsById; } + } - public IReadOnlyDictionary ByName + public IReadOnlyDictionary ByName + { + get { - get + if (fieldsByName == null) { - if (fieldsByName == null) + if (fieldsOrdered.Length == 0) { - if (fieldsOrdered.Length == 0) - { - fieldsByName = EmptyByString; - } - else - { - fieldsByName = fieldsOrdered.ToDictionary(x => x.Name); - } + fieldsByName = EmptyByString; + } + else + { + fieldsByName = fieldsOrdered.ToDictionary(x => x.Name); } - - return fieldsByName; } - } - private FieldCollection() - { - fieldsOrdered = Array.Empty(); + return fieldsByName; } + } - public FieldCollection(T[] fields) - { - Guard.NotNull(fields); - - fieldsOrdered = fields; - } + private FieldCollection() + { + fieldsOrdered = Array.Empty(); + } - private FieldCollection(IEnumerable fields) - { - fieldsOrdered = fields.ToArray(); - } + public FieldCollection(T[] fields) + { + Guard.NotNull(fields); - [Pure] - public FieldCollection Remove(long fieldId) - { - if (!ById.TryGetValue(fieldId, out _)) - { - return this; - } + fieldsOrdered = fields; + } - return new FieldCollection(fieldsOrdered.Where(x => x.Id != fieldId)); - } + private FieldCollection(IEnumerable fields) + { + fieldsOrdered = fields.ToArray(); + } - [Pure] - public FieldCollection Reorder(List ids) + [Pure] + public FieldCollection Remove(long fieldId) + { + if (!ById.TryGetValue(fieldId, out _)) { - Guard.NotNull(ids); + return this; + } - if (ids.Count != fieldsOrdered.Length || ids.Any(x => !ById.ContainsKey(x))) - { - ThrowHelper.ArgumentException("Ids must cover all fields.", nameof(ids)); - } + return new FieldCollection(fieldsOrdered.Where(x => x.Id != fieldId)); + } - if (ids.SequenceEqual(fieldsOrdered.Select(x => x.Id))) - { - return this; - } + [Pure] + public FieldCollection Reorder(List ids) + { + Guard.NotNull(ids); - return new FieldCollection(fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id))); + if (ids.Count != fieldsOrdered.Length || ids.Any(x => !ById.ContainsKey(x))) + { + ThrowHelper.ArgumentException("Ids must cover all fields.", nameof(ids)); } - [Pure] - public FieldCollection Add(T field) + if (ids.SequenceEqual(fieldsOrdered.Select(x => x.Id))) { - Guard.NotNull(field); + return this; + } - if (ByName.ContainsKey(field.Name)) - { - ThrowHelper.ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field)); - } + return new FieldCollection(fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id))); + } - if (ById.ContainsKey(field.Id)) - { - ThrowHelper.ArgumentException($"A field with ID {field.Id} already exists.", nameof(field)); - } + [Pure] + public FieldCollection Add(T field) + { + Guard.NotNull(field); - return new FieldCollection(fieldsOrdered.Union(Enumerable.Repeat(field, 1))); + if (ByName.ContainsKey(field.Name)) + { + ThrowHelper.ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field)); } - [Pure] - public FieldCollection Update(long fieldId, Func updater) + if (ById.ContainsKey(field.Id)) { - Guard.NotNull(updater); + ThrowHelper.ArgumentException($"A field with ID {field.Id} already exists.", nameof(field)); + } - if (!ById.TryGetValue(fieldId, out var field)) - { - return this; - } + return new FieldCollection(fieldsOrdered.Union(Enumerable.Repeat(field, 1))); + } - var newField = updater(field); + [Pure] + public FieldCollection Update(long fieldId, Func updater) + { + Guard.NotNull(updater); - if (ReferenceEquals(newField, field)) - { - return this; - } + if (!ById.TryGetValue(fieldId, out var field)) + { + return this; + } - if (newField is null) - { - ThrowHelper.InvalidOperationException($"Field must be of type {typeof(T)}"); - return default!; - } + var newField = updater(field); + + if (ReferenceEquals(newField, field)) + { + return this; + } - return new FieldCollection(fieldsOrdered.Select(x => ReferenceEquals(x, field) ? newField : x)); + if (newField is null) + { + ThrowHelper.InvalidOperationException($"Field must be of type {typeof(T)}"); + return default!; } + + return new FieldCollection(fieldsOrdered.Select(x => ReferenceEquals(x, field) ? newField : x)); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs index 62d5b078a1..4ab0e88c0c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs @@ -8,182 +8,181 @@ using Squidex.Infrastructure; using NamedIdStatic = Squidex.Infrastructure.NamedId; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public static class FieldExtensions { - public static class FieldExtensions + public static NamedId NamedId(this IField field) { - public static NamedId NamedId(this IField field) - { - return NamedIdStatic.Of(field.Id, field.Name); - } + return NamedIdStatic.Of(field.Id, field.Name); + } - public static IEnumerable ForApi(this IEnumerable fields, bool withHidden = false) where T : IField - { - return fields.Where(x => IsForApi(x, withHidden)); - } + public static IEnumerable ForApi(this IEnumerable fields, bool withHidden = false) where T : IField + { + return fields.Where(x => IsForApi(x, withHidden)); + } - public static bool IsForApi(this T field, bool withHidden = false) where T : IField - { - return (withHidden || !field.IsHidden) && !field.RawProperties.IsUIProperty(); - } + public static bool IsForApi(this T field, bool withHidden = false) where T : IField + { + return (withHidden || !field.IsHidden) && !field.RawProperties.IsUIProperty(); + } - public static bool IsComponentLike(this T field) where T : IField - { - return field.RawProperties is ComponentFieldProperties or ComponentsFieldProperties; - } + public static bool IsComponentLike(this T field) where T : IField + { + return field.RawProperties is ComponentFieldProperties or ComponentsFieldProperties; + } - public static bool IsUI(this T field) where T : IField - { - return field.RawProperties is UIFieldProperties; - } + public static bool IsUI(this T field) where T : IField + { + return field.RawProperties is UIFieldProperties; + } - public static bool IsUIProperty(this T properties) where T : FieldProperties - { - return properties is UIFieldProperties; - } + public static bool IsUIProperty(this T properties) where T : FieldProperties + { + return properties is UIFieldProperties; + } - public static Schema ReorderFields(this Schema schema, List ids, long? parentId = null) + public static Schema ReorderFields(this Schema schema, List ids, long? parentId = null) + { + if (parentId != null) { - if (parentId != null) + return schema.UpdateField(parentId.Value, field => { - return schema.UpdateField(parentId.Value, field => + if (field is ArrayField arrayField) { - if (field is ArrayField arrayField) - { - return arrayField.ReorderFields(ids); - } + return arrayField.ReorderFields(ids); + } - return field; - }); - } - - return schema.ReorderFields(ids); + return field; + }); } - public static Schema DeleteField(this Schema schema, long fieldId, long? parentId = null) + return schema.ReorderFields(ids); + } + + public static Schema DeleteField(this Schema schema, long fieldId, long? parentId = null) + { + if (parentId != null) { - if (parentId != null) + return schema.UpdateField(parentId.Value, field => { - return schema.UpdateField(parentId.Value, field => + if (field is ArrayField arrayField) { - if (field is ArrayField arrayField) - { - return arrayField.DeleteField(fieldId); - } - - return field; - }); - } + return arrayField.DeleteField(fieldId); + } - return schema.DeleteField(fieldId); + return field; + }); } - public static Schema LockField(this Schema schema, long fieldId, long? parentId = null) + return schema.DeleteField(fieldId); + } + + public static Schema LockField(this Schema schema, long fieldId, long? parentId = null) + { + if (parentId != null) { - if (parentId != null) + return schema.UpdateField(parentId.Value, field => { - return schema.UpdateField(parentId.Value, field => + if (field is ArrayField arrayField) { - if (field is ArrayField arrayField) - { - return arrayField.UpdateField(fieldId, f => f.Lock()); - } - - return field; - }); - } + return arrayField.UpdateField(fieldId, f => f.Lock()); + } - return schema.UpdateField(fieldId, f => f.Lock()); + return field; + }); } - public static Schema HideField(this Schema schema, long fieldId, long? parentId = null) + return schema.UpdateField(fieldId, f => f.Lock()); + } + + public static Schema HideField(this Schema schema, long fieldId, long? parentId = null) + { + if (parentId != null) { - if (parentId != null) + return schema.UpdateField(parentId.Value, f => { - return schema.UpdateField(parentId.Value, f => + if (f is ArrayField arrayField) { - if (f is ArrayField arrayField) - { - return arrayField.UpdateField(fieldId, n => n.Hide()); - } + return arrayField.UpdateField(fieldId, n => n.Hide()); + } - return f; - }); - } - - return schema.UpdateField(fieldId, f => f.Hide()); + return f; + }); } - public static Schema ShowField(this Schema schema, long fieldId, long? parentId = null) + return schema.UpdateField(fieldId, f => f.Hide()); + } + + public static Schema ShowField(this Schema schema, long fieldId, long? parentId = null) + { + if (parentId != null) { - if (parentId != null) + return schema.UpdateField(parentId.Value, field => { - return schema.UpdateField(parentId.Value, field => + if (field is ArrayField arrayField) { - if (field is ArrayField arrayField) - { - return arrayField.UpdateField(fieldId, f => f.Show()); - } - - return field; - }); - } + return arrayField.UpdateField(fieldId, f => f.Show()); + } - return schema.UpdateField(fieldId, f => f.Show()); + return field; + }); } - public static Schema EnableField(this Schema schema, long fieldId, long? parentId = null) + return schema.UpdateField(fieldId, f => f.Show()); + } + + public static Schema EnableField(this Schema schema, long fieldId, long? parentId = null) + { + if (parentId != null) { - if (parentId != null) + return schema.UpdateField(parentId.Value, field => { - return schema.UpdateField(parentId.Value, field => + if (field is ArrayField arrayField) { - if (field is ArrayField arrayField) - { - return arrayField.UpdateField(fieldId, f => f.Enable()); - } - - return field; - }); - } + return arrayField.UpdateField(fieldId, f => f.Enable()); + } - return schema.UpdateField(fieldId, f => f.Enable()); + return field; + }); } - public static Schema DisableField(this Schema schema, long fieldId, long? parentId = null) + return schema.UpdateField(fieldId, f => f.Enable()); + } + + public static Schema DisableField(this Schema schema, long fieldId, long? parentId = null) + { + if (parentId != null) { - if (parentId != null) + return schema.UpdateField(parentId.Value, field => { - return schema.UpdateField(parentId.Value, field => + if (field is ArrayField arrayField) { - if (field is ArrayField arrayField) - { - return arrayField.UpdateField(fieldId, f => f.Disable()); - } + return arrayField.UpdateField(fieldId, f => f.Disable()); + } - return field; - }); - } - - return schema.UpdateField(fieldId, f => f.Disable()); + return field; + }); } - public static Schema UpdateField(this Schema schema, long fieldId, FieldProperties properties, long? parentId = null) + return schema.UpdateField(fieldId, f => f.Disable()); + } + + public static Schema UpdateField(this Schema schema, long fieldId, FieldProperties properties, long? parentId = null) + { + if (parentId != null) { - if (parentId != null) + return schema.UpdateField(parentId.Value, field => { - return schema.UpdateField(parentId.Value, field => + if (field is ArrayField arrayField) { - if (field is ArrayField arrayField) - { - return arrayField.UpdateField(fieldId, f => f.Update(properties)); - } - - return field; - }); - } + return arrayField.UpdateField(fieldId, f => f.Update(properties)); + } - return schema.UpdateField(fieldId, f => f.Update(properties)); + return field; + }); } + + return schema.UpdateField(fieldId, f => f.Update(properties)); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs index fc9335b7fd..31bed62888 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs @@ -7,42 +7,41 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed class FieldNames : ReadonlyList { - public sealed class FieldNames : ReadonlyList - { - public static readonly FieldNames Empty = new FieldNames(new List()); + public static readonly FieldNames Empty = new FieldNames(new List()); - public FieldNames() - { - } + public FieldNames() + { + } - public FieldNames(IList list) - : base(list) - { - } + public FieldNames(IList list) + : base(list) + { + } - public static FieldNames Create(params string[] names) - { - return new FieldNames(names.ToList()); - } + public static FieldNames Create(params string[] names) + { + return new FieldNames(names.ToList()); + } - public FieldNames Add(string field) - { - var list = this.ToList(); + public FieldNames Add(string field) + { + var list = this.ToList(); - list.Add(field); + list.Add(field); - return new FieldNames(list); - } + return new FieldNames(list); + } - public FieldNames Remove(string field) - { - var list = this.ToList(); + public FieldNames Remove(string field) + { + var list = this.ToList(); - list.Remove(field); + list.Remove(field); - return new FieldNames(list); - } + return new FieldNames(list); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs index 19d2744492..b85a3e9247 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -7,28 +7,27 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public abstract record FieldProperties : NamedElementPropertiesBase { - public abstract record FieldProperties : NamedElementPropertiesBase - { - public bool IsRequired { get; init; } + public bool IsRequired { get; init; } - public bool IsRequiredOnPublish { get; init; } + public bool IsRequiredOnPublish { get; init; } - public bool IsHalfWidth { get; init; } + public bool IsHalfWidth { get; init; } - public string? Placeholder { get; init; } + public string? Placeholder { get; init; } - public string? EditorUrl { get; init; } + public string? EditorUrl { get; init; } - public ReadonlyList? Tags { get; init; } + public ReadonlyList? Tags { get; init; } - public abstract T Accept(IFieldPropertiesVisitor visitor, TArgs args); + public abstract T Accept(IFieldPropertiesVisitor visitor, TArgs args); - public abstract T Accept(IFieldVisitor visitor, IField field, TArgs args); + public abstract T Accept(IFieldVisitor visitor, IField field, TArgs args); - public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null); + public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null); - public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null); - } + public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs index f64398fdc1..8278622bbe 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs @@ -7,40 +7,39 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record FieldRule { - public sealed record FieldRule - { - public FieldRuleAction Action { get; } + public FieldRuleAction Action { get; } - public string Field { get; } + public string Field { get; } - public string? Condition { get; init; } + public string? Condition { get; init; } - public FieldRule(FieldRuleAction action, string field) - { - Guard.Enum(action); - Guard.NotNullOrEmpty(field); + public FieldRule(FieldRuleAction action, string field) + { + Guard.Enum(action); + Guard.NotNullOrEmpty(field); - Action = action; + Action = action; - Field = field; - } + Field = field; + } - public static FieldRule Disable(string field, string? condition = null) + public static FieldRule Disable(string field, string? condition = null) + { + return new FieldRule(FieldRuleAction.Disable, field) { - return new FieldRule(FieldRuleAction.Disable, field) - { - Condition = condition - }; - } + Condition = condition + }; + } - public static FieldRule Hide(string field, string? condition = null) + public static FieldRule Hide(string field, string? condition = null) + { + return new FieldRule(FieldRuleAction.Hide, field) { - return new FieldRule(FieldRuleAction.Hide, field) - { - Condition = condition - }; - } + Condition = condition + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRuleAction.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRuleAction.cs index 8018ced32c..ea9d520b13 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRuleAction.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRuleAction.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum FieldRuleAction { - public enum FieldRuleAction - { - Disable, - Hide, - Require - } + Disable, + Hide, + Require } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs index 65b7963a4b..f6dcefcec5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs @@ -7,24 +7,23 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed class FieldRules : ReadonlyList { - public sealed class FieldRules : ReadonlyList - { - public static readonly FieldRules Empty = new FieldRules(new List()); + public static readonly FieldRules Empty = new FieldRules(new List()); - public FieldRules() - { - } + public FieldRules() + { + } - public FieldRules(IList list) - : base(list) - { - } + public FieldRules(IList list) + : base(list) + { + } - public static FieldRules Create(params FieldRule[] rules) - { - return new FieldRules(rules.ToArray()); - } + public static FieldRules Create(params FieldRule[] rules) + { + return new FieldRules(rules.ToArray()); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldTypeProvider.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldTypeProvider.cs index 00c6559f3a..0e7e3a2b63 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldTypeProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldTypeProvider.cs @@ -7,26 +7,25 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public class FieldTypeProvider : ITypeProvider { - public class FieldTypeProvider : ITypeProvider - { - private const string Suffix = "Properties"; - private const string SuffixOld = "FieldProperties"; + private const string Suffix = "Properties"; + private const string SuffixOld = "FieldProperties"; - public void Map(TypeNameRegistry typeNameRegistry) - { - var types = typeof(FieldTypeProvider).Assembly.GetTypes().Where(x => typeof(FieldProperties).IsAssignableFrom(x) && !x.IsAbstract); + public void Map(TypeNameRegistry typeNameRegistry) + { + var types = typeof(FieldTypeProvider).Assembly.GetTypes().Where(x => typeof(FieldProperties).IsAssignableFrom(x) && !x.IsAbstract); - var addedTypes = new HashSet(); + var addedTypes = new HashSet(); - foreach (var type in types) + foreach (var type in types) + { + if (addedTypes.Add(type)) { - if (addedTypes.Add(type)) - { - typeNameRegistry.Map(type, type.TypeName(false, Suffix)); - typeNameRegistry.MapObsolete(type, type.TypeName(false, SuffixOld)); - } + typeNameRegistry.Map(type, type.TypeName(false, Suffix)); + typeNameRegistry.MapObsolete(type, type.TypeName(false, SuffixOld)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs index 51b55095c7..3bed967a94 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs @@ -5,315 +5,314 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public static class Fields { - public static class Fields + public static ArrayField Array(long id, string name, Partitioning partitioning, + ArrayFieldProperties? properties = null, IFieldSettings? settings = null, params NestedField[] fields) { - public static ArrayField Array(long id, string name, Partitioning partitioning, - ArrayFieldProperties? properties = null, IFieldSettings? settings = null, params NestedField[] fields) - { - return new ArrayField(id, name, partitioning, fields, properties, settings); - } - - public static RootField Assets(long id, string name, Partitioning partitioning, - AssetsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } - - public static RootField Boolean(long id, string name, Partitioning partitioning, - BooleanFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + return new ArrayField(id, name, partitioning, fields, properties, settings); + } - public static RootField Component(long id, string name, Partitioning partitioning, - ComponentFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField Assets(long id, string name, Partitioning partitioning, + AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField Components(long id, string name, Partitioning partitioning, - ComponentsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField Boolean(long id, string name, Partitioning partitioning, + BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField DateTime(long id, string name, Partitioning partitioning, - DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField Component(long id, string name, Partitioning partitioning, + ComponentFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField Geolocation(long id, string name, Partitioning partitioning, - GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField Components(long id, string name, Partitioning partitioning, + ComponentsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField Json(long id, string name, Partitioning partitioning, - JsonFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField DateTime(long id, string name, Partitioning partitioning, + DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField Number(long id, string name, Partitioning partitioning, - NumberFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField Geolocation(long id, string name, Partitioning partitioning, + GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField References(long id, string name, Partitioning partitioning, - ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField Json(long id, string name, Partitioning partitioning, + JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField String(long id, string name, Partitioning partitioning, - StringFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField Number(long id, string name, Partitioning partitioning, + NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField Tags(long id, string name, Partitioning partitioning, - TagsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField References(long id, string name, Partitioning partitioning, + ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static RootField UI(long id, string name, Partitioning partitioning, - UIFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, properties, settings); - } + public static RootField String(long id, string name, Partitioning partitioning, + StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static NestedField Assets(long id, string name, - AssetsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static RootField Tags(long id, string name, Partitioning partitioning, + TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static NestedField Boolean(long id, string name, - BooleanFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static RootField UI(long id, string name, Partitioning partitioning, + UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } - public static NestedField Component(long id, string name, - ComponentFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField Assets(long id, string name, + AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField Components(long id, string name, - ComponentsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField Boolean(long id, string name, + BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField DateTime(long id, string name, - DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField Component(long id, string name, + ComponentFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField Geolocation(long id, string name, - GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField Components(long id, string name, + ComponentsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField Json(long id, string name, - JsonFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField DateTime(long id, string name, + DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField Number(long id, string name, - NumberFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField Geolocation(long id, string name, + GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField References(long id, string name, - ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField Json(long id, string name, + JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField String(long id, string name, - StringFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField Number(long id, string name, + NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField Tags(long id, string name, - TagsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField References(long id, string name, + ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static NestedField UI(long id, string name, - UIFieldProperties? properties = null, IFieldSettings? settings = null) - { - return new NestedField(id, name, properties, settings); - } + public static NestedField String(long id, string name, + StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, - Func? handler = null, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) - { - var field = Array(id, name, partitioning, properties, settings); + public static NestedField Tags(long id, string name, + TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - if (handler != null) - { - field = handler(field); - } + public static NestedField UI(long id, string name, + UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return new NestedField(id, name, properties, settings); + } - return schema.AddField(field); - } + public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, + Func? handler = null, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + { + var field = Array(id, name, partitioning, properties, settings); - public static Schema AddAssets(this Schema schema, long id, string name, Partitioning partitioning, - AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + if (handler != null) { - return schema.AddField(Assets(id, name, partitioning, properties, settings)); + field = handler(field); } - public static Schema AddBoolean(this Schema schema, long id, string name, Partitioning partitioning, - BooleanFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(Boolean(id, name, partitioning, properties, settings)); - } + return schema.AddField(field); + } - public static Schema AddComponent(this Schema schema, long id, string name, Partitioning partitioning, - ComponentFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(Component(id, name, partitioning, properties, settings)); - } + public static Schema AddAssets(this Schema schema, long id, string name, Partitioning partitioning, + AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Assets(id, name, partitioning, properties, settings)); + } - public static Schema AddComponents(this Schema schema, long id, string name, Partitioning partitioning, - ComponentsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(Components(id, name, partitioning, properties, settings)); - } + public static Schema AddBoolean(this Schema schema, long id, string name, Partitioning partitioning, + BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Boolean(id, name, partitioning, properties, settings)); + } - public static Schema AddDateTime(this Schema schema, long id, string name, Partitioning partitioning, - DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(DateTime(id, name, partitioning, properties, settings)); - } + public static Schema AddComponent(this Schema schema, long id, string name, Partitioning partitioning, + ComponentFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Component(id, name, partitioning, properties, settings)); + } - public static Schema AddGeolocation(this Schema schema, long id, string name, Partitioning partitioning, - GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(Geolocation(id, name, partitioning, properties, settings)); - } + public static Schema AddComponents(this Schema schema, long id, string name, Partitioning partitioning, + ComponentsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Components(id, name, partitioning, properties, settings)); + } - public static Schema AddJson(this Schema schema, long id, string name, Partitioning partitioning, - JsonFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(Json(id, name, partitioning, properties, settings)); - } + public static Schema AddDateTime(this Schema schema, long id, string name, Partitioning partitioning, + DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(DateTime(id, name, partitioning, properties, settings)); + } - public static Schema AddNumber(this Schema schema, long id, string name, Partitioning partitioning, - NumberFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(Number(id, name, partitioning, properties, settings)); - } + public static Schema AddGeolocation(this Schema schema, long id, string name, Partitioning partitioning, + GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Geolocation(id, name, partitioning, properties, settings)); + } - public static Schema AddReferences(this Schema schema, long id, string name, Partitioning partitioning, - ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(References(id, name, partitioning, properties, settings)); - } + public static Schema AddJson(this Schema schema, long id, string name, Partitioning partitioning, + JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Json(id, name, partitioning, properties, settings)); + } - public static Schema AddString(this Schema schema, long id, string name, Partitioning partitioning, - StringFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(String(id, name, partitioning, properties, settings)); - } + public static Schema AddNumber(this Schema schema, long id, string name, Partitioning partitioning, + NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Number(id, name, partitioning, properties, settings)); + } - public static Schema AddTags(this Schema schema, long id, string name, Partitioning partitioning, - TagsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(Tags(id, name, partitioning, properties, settings)); - } + public static Schema AddReferences(this Schema schema, long id, string name, Partitioning partitioning, + ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(References(id, name, partitioning, properties, settings)); + } - public static Schema AddUI(this Schema schema, long id, string name, Partitioning partitioning, - UIFieldProperties? properties = null, IFieldSettings? settings = null) - { - return schema.AddField(UI(id, name, partitioning, properties, settings)); - } + public static Schema AddString(this Schema schema, long id, string name, Partitioning partitioning, + StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(String(id, name, partitioning, properties, settings)); + } - public static ArrayField AddAssets(this ArrayField field, long id, string name, - AssetsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(Assets(id, name, properties, settings)); - } + public static Schema AddTags(this Schema schema, long id, string name, Partitioning partitioning, + TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(Tags(id, name, partitioning, properties, settings)); + } - public static ArrayField AddBoolean(this ArrayField field, long id, string name, - BooleanFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(Boolean(id, name, properties, settings)); - } + public static Schema AddUI(this Schema schema, long id, string name, Partitioning partitioning, + UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return schema.AddField(UI(id, name, partitioning, properties, settings)); + } - public static ArrayField AddComponent(this ArrayField field, long id, string name, - ComponentFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(Component(id, name, properties, settings)); - } + public static ArrayField AddAssets(this ArrayField field, long id, string name, + AssetsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Assets(id, name, properties, settings)); + } - public static ArrayField AddComponents(this ArrayField field, long id, string name, - ComponentsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(Components(id, name, properties, settings)); - } + public static ArrayField AddBoolean(this ArrayField field, long id, string name, + BooleanFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Boolean(id, name, properties, settings)); + } - public static ArrayField AddDateTime(this ArrayField field, long id, string name, - DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(DateTime(id, name, properties, settings)); - } + public static ArrayField AddComponent(this ArrayField field, long id, string name, + ComponentFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Component(id, name, properties, settings)); + } - public static ArrayField AddGeolocation(this ArrayField field, long id, string name, - GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(Geolocation(id, name, properties, settings)); - } + public static ArrayField AddComponents(this ArrayField field, long id, string name, + ComponentsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Components(id, name, properties, settings)); + } - public static ArrayField AddJson(this ArrayField field, long id, string name, - JsonFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(Json(id, name, properties, settings)); - } + public static ArrayField AddDateTime(this ArrayField field, long id, string name, + DateTimeFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(DateTime(id, name, properties, settings)); + } - public static ArrayField AddNumber(this ArrayField field, long id, string name, - NumberFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(Number(id, name, properties, settings)); - } + public static ArrayField AddGeolocation(this ArrayField field, long id, string name, + GeolocationFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Geolocation(id, name, properties, settings)); + } - public static ArrayField AddReferences(this ArrayField field, long id, string name, - ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(References(id, name, properties, settings)); - } + public static ArrayField AddJson(this ArrayField field, long id, string name, + JsonFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Json(id, name, properties, settings)); + } - public static ArrayField AddString(this ArrayField field, long id, string name, - StringFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(String(id, name, properties, settings)); - } + public static ArrayField AddNumber(this ArrayField field, long id, string name, + NumberFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Number(id, name, properties, settings)); + } - public static ArrayField AddTags(this ArrayField field, long id, string name, - TagsFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(Tags(id, name, properties, settings)); - } + public static ArrayField AddReferences(this ArrayField field, long id, string name, + ReferencesFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(References(id, name, properties, settings)); + } - public static ArrayField AddUI(this ArrayField field, long id, string name, - UIFieldProperties? properties = null, IFieldSettings? settings = null) - { - return field.AddField(UI(id, name, properties, settings)); - } + public static ArrayField AddString(this ArrayField field, long id, string name, + StringFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(String(id, name, properties, settings)); + } + + public static ArrayField AddTags(this ArrayField field, long id, string name, + TagsFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(Tags(id, name, properties, settings)); + } + + public static ArrayField AddUI(this ArrayField field, long id, string name, + UIFieldProperties? properties = null, IFieldSettings? settings = null) + { + return field.AddField(UI(id, name, properties, settings)); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs index 2909d10f41..ac9689f588 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum GeolocationFieldEditor { - public enum GeolocationFieldEditor - { - Map - } + Map } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs index f0cf4dd3aa..68997a0e8e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs @@ -5,30 +5,29 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record GeolocationFieldProperties : FieldProperties { - public sealed record GeolocationFieldProperties : FieldProperties - { - public GeolocationFieldEditor Editor { get; init; } + public GeolocationFieldEditor Editor { get; init; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Geolocation(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Geolocation(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.Geolocation(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Geolocation(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs index 4eb6351b58..1e0c5ad594 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public interface IArrayField : IField { - public interface IArrayField : IField - { - IReadOnlyList Fields { get; } + IReadOnlyList Fields { get; } - IReadOnlyDictionary FieldsById { get; } + IReadOnlyDictionary FieldsById { get; } - IReadOnlyDictionary FieldsByName { get; } + IReadOnlyDictionary FieldsByName { get; } - FieldCollection FieldCollection { get; } - } + FieldCollection FieldCollection { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs index 5428a52860..89c23fa05b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public interface IField : IFieldSettings { - public interface IField : IFieldSettings - { - long Id { get; } + long Id { get; } - string Name { get; } + string Name { get; } - FieldProperties RawProperties { get; } + FieldProperties RawProperties { get; } - T Accept(IFieldVisitor visitor, TArgs args); - } + T Accept(IFieldVisitor visitor, TArgs args); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs index 610fd2f236..0b63cc9c81 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs @@ -5,34 +5,33 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public interface IFieldPropertiesVisitor { - public interface IFieldPropertiesVisitor - { - T Visit(ArrayFieldProperties properties, TArgs args); + T Visit(ArrayFieldProperties properties, TArgs args); - T Visit(AssetsFieldProperties properties, TArgs args); + T Visit(AssetsFieldProperties properties, TArgs args); - T Visit(BooleanFieldProperties properties, TArgs args); + T Visit(BooleanFieldProperties properties, TArgs args); - T Visit(ComponentFieldProperties properties, TArgs args); + T Visit(ComponentFieldProperties properties, TArgs args); - T Visit(ComponentsFieldProperties properties, TArgs args); + T Visit(ComponentsFieldProperties properties, TArgs args); - T Visit(DateTimeFieldProperties properties, TArgs args); + T Visit(DateTimeFieldProperties properties, TArgs args); - T Visit(GeolocationFieldProperties properties, TArgs args); + T Visit(GeolocationFieldProperties properties, TArgs args); - T Visit(JsonFieldProperties properties, TArgs args); + T Visit(JsonFieldProperties properties, TArgs args); - T Visit(NumberFieldProperties properties, TArgs args); + T Visit(NumberFieldProperties properties, TArgs args); - T Visit(ReferencesFieldProperties properties, TArgs args); + T Visit(ReferencesFieldProperties properties, TArgs args); - T Visit(StringFieldProperties properties, TArgs args); + T Visit(StringFieldProperties properties, TArgs args); - T Visit(TagsFieldProperties properties, TArgs args); + T Visit(TagsFieldProperties properties, TArgs args); - T Visit(UIFieldProperties properties, TArgs args); - } + T Visit(UIFieldProperties properties, TArgs args); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs index 1bb69a70c4..c3661933cb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldSettings.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public interface IFieldSettings { - public interface IFieldSettings - { - bool IsLocked { get; } + bool IsLocked { get; } - bool IsDisabled { get; } + bool IsDisabled { get; } - bool IsHidden { get; } - } + bool IsHidden { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs index f581089a03..881330ed54 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs @@ -5,34 +5,33 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public interface IFieldVisitor { - public interface IFieldVisitor - { - T Visit(IArrayField field, TArgs args); + T Visit(IArrayField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); + T Visit(IField field, TArgs args); - T Visit(IField field, TArgs args); - } + T Visit(IField field, TArgs args); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs index 0430e72ec6..627c06c61e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public interface IField : IField { - public interface IField : IField - { - T Properties { get; } - } + T Properties { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs index 5bacd00ebd..bbecee4459 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public interface INestedField : IField { - public interface INestedField : IField - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs index 31d9cd05ff..4178a71a2b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public interface IRootField : IField { - public interface IRootField : IField - { - Partitioning Partitioning { get; } - } + Partitioning Partitioning { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/FieldSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/FieldSurrogate.cs index bda97b8b74..337b28f0e1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/FieldSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/FieldSurrogate.cs @@ -5,45 +5,44 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas.Json +namespace Squidex.Domain.Apps.Core.Schemas.Json; + +public sealed class FieldSurrogate : IFieldSettings { - public sealed class FieldSurrogate : IFieldSettings - { - public long Id { get; set; } + public long Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string Partitioning { get; set; } + public string Partitioning { get; set; } - public bool IsHidden { get; set; } + public bool IsHidden { get; set; } - public bool IsLocked { get; set; } + public bool IsLocked { get; set; } - public bool IsDisabled { get; set; } + public bool IsDisabled { get; set; } - public FieldProperties Properties { get; set; } + public FieldProperties Properties { get; set; } - public FieldSurrogate[]? Children { get; set; } + public FieldSurrogate[]? Children { get; set; } - public RootField ToField() + public RootField ToField() + { + var partitioning = Core.Partitioning.FromString(Partitioning); + + if (Properties is ArrayFieldProperties arrayProperties) { - var partitioning = Core.Partitioning.FromString(Partitioning); - - if (Properties is ArrayFieldProperties arrayProperties) - { - var nested = Children?.Select(n => n.ToNestedField()).ToArray() ?? Array.Empty(); - - return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this); - } - else - { - return Properties.CreateRootField(Id, Name, partitioning, this); - } - } + var nested = Children?.Select(n => n.ToNestedField()).ToArray() ?? Array.Empty(); - public NestedField ToNestedField() + return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this); + } + else { - return Properties.CreateNestedField(Id, Name, this); + return Properties.CreateRootField(Id, Name, partitioning, this); } } + + public NestedField ToNestedField() + { + return Properties.CreateNestedField(Id, Name, this); + } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaSurrogate.cs index 4f821ef0c8..860bdc644b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaSurrogate.cs @@ -9,118 +9,117 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.Schemas.Json +namespace Squidex.Domain.Apps.Core.Schemas.Json; + +public sealed class SchemaSurrogate : ISurrogate { - public sealed class SchemaSurrogate : ISurrogate - { - public string Name { get; set; } + public string Name { get; set; } - public string Category { get; set; } + public string Category { get; set; } - public bool IsPublished { get; set; } + public bool IsPublished { get; set; } - public SchemaType Type { get; set; } + public SchemaType Type { get; set; } - public SchemaProperties Properties { get; set; } + public SchemaProperties Properties { get; set; } - public SchemaScripts? Scripts { get; set; } + public SchemaScripts? Scripts { get; set; } - public FieldNames? FieldsInLists { get; set; } + public FieldNames? FieldsInLists { get; set; } - public FieldNames? FieldsInReferences { get; set; } + public FieldNames? FieldsInReferences { get; set; } - public FieldRules? FieldRules { get; set; } + public FieldRules? FieldRules { get; set; } - public FieldSurrogate[] Fields { get; set; } + public FieldSurrogate[] Fields { get; set; } - public ReadonlyDictionary? PreviewUrls { get; set; } + public ReadonlyDictionary? PreviewUrls { get; set; } - public bool IsSingleton + public bool IsSingleton + { + set { - set + if (value) { - if (value) - { - Type = SchemaType.Singleton; - } + Type = SchemaType.Singleton; } } + } - public void FromSource(Schema source) - { - SimpleMapper.Map(source, this); - - Fields = - source.Fields.Select(x => - new FieldSurrogate - { - Id = x.Id, - Name = x.Name, - Children = CreateChildren(x), - IsHidden = x.IsHidden, - IsLocked = x.IsLocked, - IsDisabled = x.IsDisabled, - Partitioning = x.Partitioning.Key, - Properties = x.RawProperties - }).ToArray(); - } + public void FromSource(Schema source) + { + SimpleMapper.Map(source, this); - private static FieldSurrogate[]? CreateChildren(IField field) - { - if (field is ArrayField arrayField) - { - return arrayField.Fields.Select(x => - new FieldSurrogate - { - Id = x.Id, - Name = x.Name, - IsHidden = x.IsHidden, - IsLocked = x.IsLocked, - IsDisabled = x.IsDisabled, - Properties = x.RawProperties - }).ToArray(); - } + Fields = + source.Fields.Select(x => + new FieldSurrogate + { + Id = x.Id, + Name = x.Name, + Children = CreateChildren(x), + IsHidden = x.IsHidden, + IsLocked = x.IsLocked, + IsDisabled = x.IsDisabled, + Partitioning = x.Partitioning.Key, + Properties = x.RawProperties + }).ToArray(); + } - return null; + private static FieldSurrogate[]? CreateChildren(IField field) + { + if (field is ArrayField arrayField) + { + return arrayField.Fields.Select(x => + new FieldSurrogate + { + Id = x.Id, + Name = x.Name, + IsHidden = x.IsHidden, + IsLocked = x.IsLocked, + IsDisabled = x.IsDisabled, + Properties = x.RawProperties + }).ToArray(); } - public Schema ToSource() - { - var fields = Fields?.Select(f => f.ToField()).ToArray() ?? Array.Empty(); + return null; + } - var schema = new Schema(Name, fields, Properties, IsPublished, Type); + public Schema ToSource() + { + var fields = Fields?.Select(f => f.ToField()).ToArray() ?? Array.Empty(); - if (!string.IsNullOrWhiteSpace(Category)) - { - schema = schema.ChangeCategory(Category); - } + var schema = new Schema(Name, fields, Properties, IsPublished, Type); - if (Scripts != null) - { - schema = schema.SetScripts(Scripts); - } + if (!string.IsNullOrWhiteSpace(Category)) + { + schema = schema.ChangeCategory(Category); + } - if (FieldsInLists?.Count > 0) - { - schema = schema.SetFieldsInLists(FieldsInLists); - } + if (Scripts != null) + { + schema = schema.SetScripts(Scripts); + } - if (FieldsInReferences?.Count > 0) - { - schema = schema.SetFieldsInReferences(FieldsInReferences); - } + if (FieldsInLists?.Count > 0) + { + schema = schema.SetFieldsInLists(FieldsInLists); + } - if (FieldRules?.Count > 0) - { - schema = schema.SetFieldRules(FieldRules); - } + if (FieldsInReferences?.Count > 0) + { + schema = schema.SetFieldsInReferences(FieldsInReferences); + } - if (PreviewUrls?.Count > 0) - { - schema = schema.SetPreviewUrls(PreviewUrls); - } + if (FieldRules?.Count > 0) + { + schema = schema.SetFieldRules(FieldRules); + } - return schema; + if (PreviewUrls?.Count > 0) + { + schema = schema.SetPreviewUrls(PreviewUrls); } + + return schema; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs index fc21b220d8..7d71d74383 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs @@ -5,30 +5,29 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record JsonFieldProperties : FieldProperties { - public sealed record JsonFieldProperties : FieldProperties - { - public string? GraphQLSchema { get; set; } + public string? GraphQLSchema { get; set; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Json(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Json(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.Json(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Json(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs index d56bdc7fec..126b4780d6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed class LocalizedValue : ReadonlyDictionary { - public sealed class LocalizedValue : ReadonlyDictionary + public LocalizedValue() { - public LocalizedValue() - { - } + } - public LocalizedValue(IDictionary inner) - : base(inner) - { - } + public LocalizedValue(IDictionary inner) + : base(inner) + { } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs index 073b6f5459..be22e538e7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public abstract record NamedElementPropertiesBase { - public abstract record NamedElementPropertiesBase - { - public string? Label { get; init; } + public string? Label { get; init; } - public string? Hints { get; init; } - } + public string? Hints { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs index f67d1cd77e..bb12b9b9c5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs @@ -7,110 +7,109 @@ using System.Diagnostics.Contracts; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public abstract class NestedField : FieldBase, INestedField { - public abstract class NestedField : FieldBase, INestedField - { - public bool IsLocked { get; private set; } + public bool IsLocked { get; private set; } - public bool IsHidden { get; private set; } + public bool IsHidden { get; private set; } - public bool IsDisabled { get; private set; } + public bool IsDisabled { get; private set; } - public abstract FieldProperties RawProperties { get; } + public abstract FieldProperties RawProperties { get; } - protected NestedField(long id, string name, IFieldSettings? settings = null) - : base(id, name) + protected NestedField(long id, string name, IFieldSettings? settings = null) + : base(id, name) + { + if (settings != null) { - if (settings != null) - { - IsLocked = settings.IsLocked; - IsHidden = settings.IsHidden; - IsDisabled = settings.IsDisabled; - } + IsLocked = settings.IsLocked; + IsHidden = settings.IsHidden; + IsDisabled = settings.IsDisabled; } + } - [Pure] - public NestedField Lock() + [Pure] + public NestedField Lock() + { + if (IsLocked) { - if (IsLocked) - { - return this; - } - - return Clone(clone => - { - clone.IsLocked = true; - }); + return this; } - [Pure] - public NestedField Hide() + return Clone(clone => { - if (IsHidden) - { - return this; - } - - return Clone(clone => - { - clone.IsHidden = true; - }); - } + clone.IsLocked = true; + }); + } - [Pure] - public NestedField Show() + [Pure] + public NestedField Hide() + { + if (IsHidden) { - if (!IsHidden) - { - return this; - } - - return Clone(clone => - { - clone.IsHidden = false; - }); + return this; } - [Pure] - public NestedField Disable() + return Clone(clone => { - if (IsDisabled) - { - return this; - } - - return Clone(clone => - { - clone.IsDisabled = true; - }); + clone.IsHidden = true; + }); + } + + [Pure] + public NestedField Show() + { + if (!IsHidden) + { + return this; } - [Pure] - public NestedField Enable() + return Clone(clone => { - if (!IsDisabled) - { - return this; - } - - return Clone(clone => - { - clone.IsDisabled = false; - }); + clone.IsHidden = false; + }); + } + + [Pure] + public NestedField Disable() + { + if (IsDisabled) + { + return this; } - public abstract T Accept(IFieldVisitor visitor, TArgs args); + return Clone(clone => + { + clone.IsDisabled = true; + }); + } - public abstract NestedField Update(FieldProperties newProperties); + [Pure] + public NestedField Enable() + { + if (!IsDisabled) + { + return this; + } - protected NestedField Clone(Action updater) + return Clone(clone => { - var clone = (NestedField)MemberwiseClone(); + clone.IsDisabled = false; + }); + } - updater(clone); + public abstract T Accept(IFieldVisitor visitor, TArgs args); - return clone; - } + public abstract NestedField Update(FieldProperties newProperties); + + protected NestedField Clone(Action updater) + { + var clone = (NestedField)MemberwiseClone(); + + updater(clone); + + return clone; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs index ddab3b06ac..e8bd96ff43 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs @@ -8,55 +8,54 @@ using System.Diagnostics.Contracts; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public class NestedField : NestedField, IField where T : FieldProperties, new() { - public class NestedField : NestedField, IField where T : FieldProperties, new() + public T Properties { get; private set; } + + public override FieldProperties RawProperties { - public T Properties { get; private set; } + get => Properties; + } - public override FieldProperties RawProperties - { - get => Properties; - } + public NestedField(long id, string name, T? properties = null, IFieldSettings? settings = null) + : base(id, name, settings) + { + Properties = properties ?? new T(); + } - public NestedField(long id, string name, T? properties = null, IFieldSettings? settings = null) - : base(id, name, settings) - { - Properties = properties ?? new T(); - } + [Pure] + public override NestedField Update(FieldProperties newProperties) + { + var typedProperties = ValidateProperties(newProperties); - [Pure] - public override NestedField Update(FieldProperties newProperties) + if (Properties.Equals(typedProperties)) { - var typedProperties = ValidateProperties(newProperties); - - if (Properties.Equals(typedProperties)) - { - return this; - } - - return Clone(clone => - { - ((NestedField)clone).Properties = typedProperties; - }); + return this; } - private static T ValidateProperties(FieldProperties newProperties) + return Clone(clone => { - Guard.NotNull(newProperties); - - if (newProperties is not T typedProperties) - { - ThrowHelper.ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); - return default!; - } + ((NestedField)clone).Properties = typedProperties; + }); + } - return typedProperties; - } + private static T ValidateProperties(FieldProperties newProperties) + { + Guard.NotNull(newProperties); - public override TResult Accept(IFieldVisitor visitor, TArgs args) + if (newProperties is not T typedProperties) { - return Properties.Accept(visitor, this, args); + ThrowHelper.ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); + return default!; } + + return typedProperties; + } + + public override TResult Accept(IFieldVisitor visitor, TArgs args) + { + return Properties.Accept(visitor, this, args); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs index ad177489ca..6d42e34568 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum NumberFieldEditor { - public enum NumberFieldEditor - { - Input, - Radio, - Dropdown, - Stars - } + Input, + Radio, + Dropdown, + Stars } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs index 4c793155e1..c13c27dfb0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs @@ -7,44 +7,43 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record NumberFieldProperties : FieldProperties { - public sealed record NumberFieldProperties : FieldProperties - { - public ReadonlyList? AllowedValues { get; init; } + public ReadonlyList? AllowedValues { get; init; } - public LocalizedValue DefaultValues { get; init; } + public LocalizedValue DefaultValues { get; init; } - public double? DefaultValue { get; init; } + public double? DefaultValue { get; init; } - public double? MaxValue { get; init; } + public double? MaxValue { get; init; } - public double? MinValue { get; init; } + public double? MinValue { get; init; } - public bool IsUnique { get; init; } + public bool IsUnique { get; init; } - public bool InlineEditable { get; init; } + public bool InlineEditable { get; init; } - public NumberFieldEditor Editor { get; init; } + public NumberFieldEditor Editor { get; init; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Number(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Number(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.Number(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Number(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs index 11fe50cd2c..f6715eb1fd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum ReferencesFieldEditor { - public enum ReferencesFieldEditor - { - List, - Dropdown, - Tags, - Checkboxes, - Input - } + List, + Dropdown, + Tags, + Checkboxes, + Input } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs index 38d2b72b83..d0f72af6ad 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -8,65 +8,64 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record ReferencesFieldProperties : FieldProperties { - public sealed record ReferencesFieldProperties : FieldProperties - { - public LocalizedValue?> DefaultValues { get; init; } + public LocalizedValue?> DefaultValues { get; init; } - public ReadonlyList? DefaultValue { get; init; } + public ReadonlyList? DefaultValue { get; init; } - public int? MinItems { get; init; } + public int? MinItems { get; init; } - public int? MaxItems { get; init; } + public int? MaxItems { get; init; } - public bool ResolveReference { get; init; } + public bool ResolveReference { get; init; } - public bool AllowDuplicates { get; init; } + public bool AllowDuplicates { get; init; } - public bool MustBePublished { get; init; } + public bool MustBePublished { get; init; } - public ReferencesFieldEditor Editor { get; init; } + public ReferencesFieldEditor Editor { get; init; } - public DomainId SchemaId + public DomainId SchemaId + { + init { - init + if (value != default) { - if (value != default) - { - SchemaIds = ReadonlyList.Create(value); - } - else - { - SchemaIds = null; - } + SchemaIds = ReadonlyList.Create(value); } - get + else { - return SchemaIds?.FirstOrDefault() ?? default; + SchemaIds = null; } } - - public ReadonlyList? SchemaIds { get; init; } - - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + get { - return visitor.Visit(this, args); + return SchemaIds?.FirstOrDefault() ?? default; } + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public ReadonlyList? SchemaIds { get; init; } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.References(id, name, partitioning, this, settings); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.References(id, name, this, settings); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.References(id, name, partitioning, this, settings); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.References(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs index cc74c4dd5c..09caeeb8ef 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs @@ -8,43 +8,42 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed class ResolvedComponents : ReadonlyDictionary { - public sealed class ResolvedComponents : ReadonlyDictionary + public static readonly ResolvedComponents Empty = new ResolvedComponents(); + + private ResolvedComponents() { - public static readonly ResolvedComponents Empty = new ResolvedComponents(); + } - private ResolvedComponents() - { - } + public ResolvedComponents(IDictionary inner) + : base(inner) + { + } - public ResolvedComponents(IDictionary inner) - : base(inner) - { - } + public ResolvedComponents Resolve(IEnumerable? schemaIds) + { + var result = (Dictionary?)null; - public ResolvedComponents Resolve(IEnumerable? schemaIds) + if (schemaIds != null) { - var result = (Dictionary?)null; - - if (schemaIds != null) + foreach (var schemaId in schemaIds) { - foreach (var schemaId in schemaIds) + if (TryGetValue(schemaId, out var schema)) { - if (TryGetValue(schemaId, out var schema)) - { - result ??= new Dictionary(); - result[schemaId] = schema; - } + result ??= new Dictionary(); + result[schemaId] = schema; } } + } - if (result == null) - { - return Empty; - } - - return new ResolvedComponents(result); + if (result == null) + { + return Empty; } + + return new ResolvedComponents(result); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs index f2b42a9cbc..ea6ca6ba7f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs @@ -8,116 +8,115 @@ using System.Diagnostics.Contracts; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public abstract class RootField : FieldBase, IRootField { - public abstract class RootField : FieldBase, IRootField - { - public Partitioning Partitioning { get; } + public Partitioning Partitioning { get; } - public bool IsLocked { get; private set; } + public bool IsLocked { get; private set; } - public bool IsHidden { get; private set; } + public bool IsHidden { get; private set; } - public bool IsDisabled { get; private set; } + public bool IsDisabled { get; private set; } - public abstract FieldProperties RawProperties { get; } + public abstract FieldProperties RawProperties { get; } - protected RootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - : base(id, name) - { - Guard.NotNull(partitioning); + protected RootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + : base(id, name) + { + Guard.NotNull(partitioning); - Partitioning = partitioning; + Partitioning = partitioning; - if (settings != null) - { - IsLocked = settings.IsLocked; - IsHidden = settings.IsHidden; - IsDisabled = settings.IsDisabled; - } + if (settings != null) + { + IsLocked = settings.IsLocked; + IsHidden = settings.IsHidden; + IsDisabled = settings.IsDisabled; } + } - [Pure] - public RootField Lock() + [Pure] + public RootField Lock() + { + if (IsLocked) { - if (IsLocked) - { - return this; - } - - return Clone(clone => - { - clone.IsLocked = true; - }); + return this; } - [Pure] - public RootField Hide() + return Clone(clone => { - if (IsHidden) - { - return this; - } - - return Clone(clone => - { - clone.IsHidden = true; - }); - } + clone.IsLocked = true; + }); + } - [Pure] - public RootField Show() + [Pure] + public RootField Hide() + { + if (IsHidden) { - if (!IsHidden) - { - return this; - } - - return Clone(clone => - { - clone.IsHidden = false; - }); + return this; } - [Pure] - public RootField Disable() + return Clone(clone => { - if (IsDisabled) - { - return this; - } - - return Clone(clone => - { - clone.IsDisabled = true; - }); + clone.IsHidden = true; + }); + } + + [Pure] + public RootField Show() + { + if (!IsHidden) + { + return this; } - [Pure] - public RootField Enable() + return Clone(clone => { - if (!IsDisabled) - { - return this; - } - - return Clone(clone => - { - clone.IsDisabled = false; - }); + clone.IsHidden = false; + }); + } + + [Pure] + public RootField Disable() + { + if (IsDisabled) + { + return this; } - public abstract T Accept(IFieldVisitor visitor, TArgs args); + return Clone(clone => + { + clone.IsDisabled = true; + }); + } - public abstract RootField Update(FieldProperties newProperties); + [Pure] + public RootField Enable() + { + if (!IsDisabled) + { + return this; + } - protected RootField Clone(Action updater) + return Clone(clone => { - var clone = (RootField)MemberwiseClone(); + clone.IsDisabled = false; + }); + } - updater(clone); + public abstract T Accept(IFieldVisitor visitor, TArgs args); - return clone; - } + public abstract RootField Update(FieldProperties newProperties); + + protected RootField Clone(Action updater) + { + var clone = (RootField)MemberwiseClone(); + + updater(clone); + + return clone; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs index 13f83326a7..68a49be0f9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs @@ -8,55 +8,54 @@ using System.Diagnostics.Contracts; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public class RootField : RootField, IField where T : FieldProperties, new() { - public class RootField : RootField, IField where T : FieldProperties, new() + public T Properties { get; private set; } + + public override FieldProperties RawProperties { - public T Properties { get; private set; } + get => Properties; + } - public override FieldProperties RawProperties - { - get => Properties; - } + public RootField(long id, string name, Partitioning partitioning, T? properties = null, IFieldSettings? settings = null) + : base(id, name, partitioning, settings) + { + Properties = properties ?? new T(); + } - public RootField(long id, string name, Partitioning partitioning, T? properties = null, IFieldSettings? settings = null) - : base(id, name, partitioning, settings) - { - Properties = properties ?? new T(); - } + [Pure] + public override RootField Update(FieldProperties newProperties) + { + var typedProperties = ValidateProperties(newProperties); - [Pure] - public override RootField Update(FieldProperties newProperties) + if (Properties.Equals(typedProperties)) { - var typedProperties = ValidateProperties(newProperties); - - if (Properties.Equals(typedProperties)) - { - return this; - } - - return Clone(clone => - { - ((RootField)clone).Properties = typedProperties; - }); + return this; } - private static T ValidateProperties(FieldProperties newProperties) + return Clone(clone => { - Guard.NotNull(newProperties); - - if (newProperties is not T typedProperties) - { - ThrowHelper.ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); - return default!; - } + ((RootField)clone).Properties = typedProperties; + }); + } - return typedProperties; - } + private static T ValidateProperties(FieldProperties newProperties) + { + Guard.NotNull(newProperties); - public override TResult Accept(IFieldVisitor visitor, TArgs args) + if (newProperties is not T typedProperties) { - return Properties.Accept(visitor, this, args); + ThrowHelper.ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); + return default!; } + + return typedProperties; + } + + public override TResult Accept(IFieldVisitor visitor, TArgs args) + { + return Properties.Accept(visitor, this, args); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs index 2f5aaf4b35..df63ea34ef 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -9,283 +9,282 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed class Schema { - public sealed class Schema - { - public SchemaType Type { get; } + public SchemaType Type { get; } - public string Name { get; } + public string Name { get; } - public string? Category { get; private set; } + public string? Category { get; private set; } - public bool IsPublished { get; private set; } + public bool IsPublished { get; private set; } - public FieldCollection FieldCollection { get; private set; } = FieldCollection.Empty; + public FieldCollection FieldCollection { get; private set; } = FieldCollection.Empty; - public FieldRules FieldRules { get; private set; } = FieldRules.Empty; + public FieldRules FieldRules { get; private set; } = FieldRules.Empty; - public FieldNames FieldsInLists { get; private set; } = FieldNames.Empty; + public FieldNames FieldsInLists { get; private set; } = FieldNames.Empty; - public FieldNames FieldsInReferences { get; private set; } = FieldNames.Empty; + public FieldNames FieldsInReferences { get; private set; } = FieldNames.Empty; - public SchemaScripts Scripts { get; private set; } = new SchemaScripts(); + public SchemaScripts Scripts { get; private set; } = new SchemaScripts(); - public SchemaProperties Properties { get; private set; } = new SchemaProperties(); + public SchemaProperties Properties { get; private set; } = new SchemaProperties(); - public ReadonlyDictionary PreviewUrls { get; private set; } = ReadonlyDictionary.Empty(); + public ReadonlyDictionary PreviewUrls { get; private set; } = ReadonlyDictionary.Empty(); - public IReadOnlyList Fields - { - get => FieldCollection.Ordered; - } + public IReadOnlyList Fields + { + get => FieldCollection.Ordered; + } - public IReadOnlyDictionary FieldsById - { - get => FieldCollection.ById; - } + public IReadOnlyDictionary FieldsById + { + get => FieldCollection.ById; + } - public IReadOnlyDictionary FieldsByName - { - get => FieldCollection.ByName; - } + public IReadOnlyDictionary FieldsByName + { + get => FieldCollection.ByName; + } - public Schema(string name, SchemaProperties? properties = null, SchemaType type = SchemaType.Default) + public Schema(string name, SchemaProperties? properties = null, SchemaType type = SchemaType.Default) + { + Guard.NotNullOrEmpty(name); + + Name = name; + + if (properties != null) { - Guard.NotNullOrEmpty(name); + Properties = properties; + } - Name = name; + Type = type; + } - if (properties != null) - { - Properties = properties; - } + public Schema(string name, RootField[] fields, SchemaProperties? properties, bool isPublished = false, SchemaType type = SchemaType.Default) + : this(name, properties, type) + { + Guard.NotNull(fields); - Type = type; - } + FieldCollection = new FieldCollection(fields); - public Schema(string name, RootField[] fields, SchemaProperties? properties, bool isPublished = false, SchemaType type = SchemaType.Default) - : this(name, properties, type) - { - Guard.NotNull(fields); + IsPublished = isPublished; + } - FieldCollection = new FieldCollection(fields); + [Pure] + public Schema Update(SchemaProperties? newProperties) + { + newProperties ??= new SchemaProperties(); - IsPublished = isPublished; + if (Properties.Equals(newProperties)) + { + return this; } - [Pure] - public Schema Update(SchemaProperties? newProperties) + return Clone(clone => { - newProperties ??= new SchemaProperties(); + clone.Properties = newProperties; + }); + } - if (Properties.Equals(newProperties)) - { - return this; - } + [Pure] + public Schema SetScripts(SchemaScripts? newScripts) + { + newScripts ??= new SchemaScripts(); - return Clone(clone => - { - clone.Properties = newProperties; - }); + if (Scripts.Equals(newScripts)) + { + return this; } - [Pure] - public Schema SetScripts(SchemaScripts? newScripts) + return Clone(clone => { - newScripts ??= new SchemaScripts(); + clone.Scripts = newScripts; + }); + } - if (Scripts.Equals(newScripts)) - { - return this; - } + [Pure] + public Schema SetFieldsInLists(FieldNames? names) + { + names ??= FieldNames.Empty; - return Clone(clone => - { - clone.Scripts = newScripts; - }); + if (FieldsInLists.SequenceEqual(names)) + { + return this; } - [Pure] - public Schema SetFieldsInLists(FieldNames? names) + return Clone(clone => { - names ??= FieldNames.Empty; + clone.FieldsInLists = names; + }); + } - if (FieldsInLists.SequenceEqual(names)) - { - return this; - } + [Pure] + public Schema SetFieldsInLists(params string[] names) + { + return SetFieldsInLists(new FieldNames(names)); + } - return Clone(clone => - { - clone.FieldsInLists = names; - }); - } + [Pure] + public Schema SetFieldsInReferences(FieldNames? names) + { + names ??= FieldNames.Empty; - [Pure] - public Schema SetFieldsInLists(params string[] names) + if (FieldsInReferences.SequenceEqual(names)) { - return SetFieldsInLists(new FieldNames(names)); + return this; } - [Pure] - public Schema SetFieldsInReferences(FieldNames? names) + return Clone(clone => { - names ??= FieldNames.Empty; + clone.FieldsInReferences = names; + }); + } - if (FieldsInReferences.SequenceEqual(names)) - { - return this; - } + [Pure] + public Schema SetFieldsInReferences(params string[] names) + { + return SetFieldsInReferences(new FieldNames(names)); + } - return Clone(clone => - { - clone.FieldsInReferences = names; - }); - } + [Pure] + public Schema SetFieldRules(FieldRules? rules) + { + rules ??= FieldRules.Empty; - [Pure] - public Schema SetFieldsInReferences(params string[] names) + if (FieldRules.Equals(rules)) { - return SetFieldsInReferences(new FieldNames(names)); + return this; } - [Pure] - public Schema SetFieldRules(FieldRules? rules) + return Clone(clone => { - rules ??= FieldRules.Empty; + clone.FieldRules = rules; + }); + } - if (FieldRules.Equals(rules)) - { - return this; - } + [Pure] + public Schema SetFieldRules(params FieldRule[] rules) + { + return SetFieldRules(new FieldRules(rules)); + } - return Clone(clone => - { - clone.FieldRules = rules; - }); + [Pure] + public Schema Publish() + { + if (IsPublished) + { + return this; } - [Pure] - public Schema SetFieldRules(params FieldRule[] rules) + return Clone(clone => { - return SetFieldRules(new FieldRules(rules)); - } + clone.IsPublished = true; + }); + } - [Pure] - public Schema Publish() + [Pure] + public Schema Unpublish() + { + if (!IsPublished) { - if (IsPublished) - { - return this; - } - - return Clone(clone => - { - clone.IsPublished = true; - }); + return this; } - [Pure] - public Schema Unpublish() + return Clone(clone => { - if (!IsPublished) - { - return this; - } - - return Clone(clone => - { - clone.IsPublished = false; - }); - } + clone.IsPublished = false; + }); + } - [Pure] - public Schema ChangeCategory(string? category) + [Pure] + public Schema ChangeCategory(string? category) + { + if (string.Equals(Category, category, StringComparison.Ordinal)) { - if (string.Equals(Category, category, StringComparison.Ordinal)) - { - return this; - } - - return Clone(clone => - { - clone.Category = category; - }); + return this; } - [Pure] - public Schema SetPreviewUrls(ReadonlyDictionary? previewUrls) + return Clone(clone => { - previewUrls ??= ReadonlyDictionary.Empty(); - - if (PreviewUrls.Equals(previewUrls)) - { - return this; - } + clone.Category = category; + }); + } - return Clone(clone => - { - clone.PreviewUrls = previewUrls; - }); - } + [Pure] + public Schema SetPreviewUrls(ReadonlyDictionary? previewUrls) + { + previewUrls ??= ReadonlyDictionary.Empty(); - [Pure] - public Schema DeleteField(long fieldId) + if (PreviewUrls.Equals(previewUrls)) { - if (!FieldsById.TryGetValue(fieldId, out var field)) - { - return this; - } - - return Clone(clone => - { - clone.FieldCollection = FieldCollection.Remove(fieldId); - clone.FieldsInLists = FieldsInLists.Remove(field.Name); - clone.FieldsInReferences = FieldsInReferences.Remove(field.Name); - }); + return this; } - [Pure] - public Schema ReorderFields(List ids) + return Clone(clone => { - return UpdateFields(f => f.Reorder(ids)); - } + clone.PreviewUrls = previewUrls; + }); + } - [Pure] - public Schema AddField(RootField field) + [Pure] + public Schema DeleteField(long fieldId) + { + if (!FieldsById.TryGetValue(fieldId, out var field)) { - return UpdateFields(f => f.Add(field)); + return this; } - [Pure] - public Schema UpdateField(long fieldId, Func updater) + return Clone(clone => { - return UpdateFields(f => f.Update(fieldId, updater)); - } + clone.FieldCollection = FieldCollection.Remove(fieldId); + clone.FieldsInLists = FieldsInLists.Remove(field.Name); + clone.FieldsInReferences = FieldsInReferences.Remove(field.Name); + }); + } - private Schema UpdateFields(Func, FieldCollection> updater) - { - var newFields = updater(FieldCollection); + [Pure] + public Schema ReorderFields(List ids) + { + return UpdateFields(f => f.Reorder(ids)); + } - if (ReferenceEquals(newFields, FieldCollection)) - { - return this; - } + [Pure] + public Schema AddField(RootField field) + { + return UpdateFields(f => f.Add(field)); + } - return Clone(clone => - { - clone.FieldCollection = newFields; - }); + [Pure] + public Schema UpdateField(long fieldId, Func updater) + { + return UpdateFields(f => f.Update(fieldId, updater)); + } + + private Schema UpdateFields(Func, FieldCollection> updater) + { + var newFields = updater(FieldCollection); + + if (ReferenceEquals(newFields, FieldCollection)) + { + return this; } - private Schema Clone(Action updater) + return Clone(clone => { - var clone = (Schema)MemberwiseClone(); + clone.FieldCollection = newFields; + }); + } + + private Schema Clone(Action updater) + { + var clone = (Schema)MemberwiseClone(); - updater(clone); + updater(clone); - return clone; - } + return clone; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs index cb3c7c2085..1bce682475 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs @@ -8,100 +8,99 @@ using Squidex.Infrastructure; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public static class SchemaExtensions { - public static class SchemaExtensions + public static long MaxId(this Schema schema) { - public static long MaxId(this Schema schema) - { - var id = 0L; + var id = 0L; - foreach (var field in schema.Fields) + foreach (var field in schema.Fields) + { + if (field is IArrayField arrayField) { - if (field is IArrayField arrayField) + foreach (var nestedField in arrayField.Fields) { - foreach (var nestedField in arrayField.Fields) - { - id = Math.Max(id, nestedField.Id); - } + id = Math.Max(id, nestedField.Id); } - - id = Math.Max(id, field.Id); } - return id; - } - - public static string TypeName(this IField field) - { - return field.Name.ToPascalCase(); + id = Math.Max(id, field.Id); } - public static string DisplayName(this IField field) - { - return field.RawProperties.Label.Or(field.TypeName()); - } + return id; + } - public static string TypeName(this Schema schema) - { - return schema.Name.ToPascalCase(); - } + public static string TypeName(this IField field) + { + return field.Name.ToPascalCase(); + } - public static string DisplayName(this Schema schema) - { - return schema.Properties.Label.Or(schema.TypeName()); - } + public static string DisplayName(this IField field) + { + return field.RawProperties.Label.Or(field.TypeName()); + } - public static string DisplayNameUnchanged(this Schema schema) - { - return schema.Properties.Label.Or(schema.Name); - } + public static string TypeName(this Schema schema) + { + return schema.Name.ToPascalCase(); + } - public static IEnumerable ReferenceFields(this Schema schema) - { - return schema.RootFields(schema.FieldsInReferences); - } + public static string DisplayName(this Schema schema) + { + return schema.Properties.Label.Or(schema.TypeName()); + } - public static IEnumerable ListFields(this Schema schema) - { - return schema.RootFields(schema.FieldsInLists); - } + public static string DisplayNameUnchanged(this Schema schema) + { + return schema.Properties.Label.Or(schema.Name); + } - public static IEnumerable RootFields(this Schema schema, FieldNames names) - { - var hasField = false; + public static IEnumerable ReferenceFields(this Schema schema) + { + return schema.RootFields(schema.FieldsInReferences); + } - foreach (var name in names) - { - if (schema.FieldsByName.TryGetValue(name, out var field)) - { - hasField = true; + public static IEnumerable ListFields(this Schema schema) + { + return schema.RootFields(schema.FieldsInLists); + } - yield return field; - } - } + public static IEnumerable RootFields(this Schema schema, FieldNames names) + { + var hasField = false; - if (!hasField) + foreach (var name in names) + { + if (schema.FieldsByName.TryGetValue(name, out var field)) { - var first = schema.Fields.FirstOrDefault(x => !x.IsUI()); + hasField = true; - if (first != null) - { - yield return first; - } + yield return field; } } - public static IEnumerable> ResolvingReferences(this Schema schema) + if (!hasField) { - return schema.Fields.OfType>() - .Where(x => x.Properties.ResolveReference && x.Properties.MaxItems == 1); - } + var first = schema.Fields.FirstOrDefault(x => !x.IsUI()); - public static IEnumerable> ResolvingAssets(this Schema schema) - { - return schema.Fields.OfType>() - .Where(x => x.Properties.ResolveFirst); + if (first != null) + { + yield return first; + } } } + + public static IEnumerable> ResolvingReferences(this Schema schema) + { + return schema.Fields.OfType>() + .Where(x => x.Properties.ResolveReference && x.Properties.MaxItems == 1); + } + + public static IEnumerable> ResolvingAssets(this Schema schema) + { + return schema.Fields.OfType>() + .Where(x => x.Properties.ResolveFirst); + } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs index a86cc8a0a3..1072e1e72f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs @@ -7,20 +7,19 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record SchemaProperties : NamedElementPropertiesBase { - public sealed record SchemaProperties : NamedElementPropertiesBase - { - public static readonly SchemaProperties Empty = new SchemaProperties(); + public static readonly SchemaProperties Empty = new SchemaProperties(); - public ReadonlyList? Tags { get; init; } + public ReadonlyList? Tags { get; init; } - public string? ContentsSidebarUrl { get; init; } + public string? ContentsSidebarUrl { get; init; } - public string? ContentSidebarUrl { get; init; } + public string? ContentSidebarUrl { get; init; } - public string? ContentEditorUrl { get; init; } + public string? ContentEditorUrl { get; init; } - public bool ValidateOnPublish { get; init; } - } + public bool ValidateOnPublish { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs index 4339965c6b..e803893b02 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record SchemaScripts { - public sealed record SchemaScripts - { - public string? Change { get; init; } + public string? Change { get; init; } - public string? Create { get; init; } + public string? Create { get; init; } - public string? Update { get; init; } + public string? Update { get; init; } - public string? Delete { get; init; } + public string? Delete { get; init; } - public string? Query { get; init; } + public string? Query { get; init; } - public string? QueryPre { get; init; } - } + public string? QueryPre { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaType.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaType.cs index 8a210bcbe9..b830ed8f69 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaType.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaType.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum SchemaType { - public enum SchemaType - { - Default, - Singleton, - Component - } + Default, + Singleton, + Component } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs index 20e76163a9..fb3b661a29 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum StringContentType { - public enum StringContentType - { - Unspecified, - Html, - Markdown - } + Unspecified, + Html, + Markdown } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs index 0a7a927d6c..edf20c28c0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum StringFieldEditor { - public enum StringFieldEditor - { - Input, - Color, - Markdown, - Dropdown, - Html, - Radio, - RichText, - Slug, - StockPhoto, - TextArea - } + Input, + Color, + Markdown, + Dropdown, + Html, + Radio, + RichText, + Slug, + StockPhoto, + TextArea } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs index a10b6f98ea..11448b2541 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -8,66 +8,65 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record StringFieldProperties : FieldProperties { - public sealed record StringFieldProperties : FieldProperties - { - public ReadonlyList? AllowedValues { get; init; } + public ReadonlyList? AllowedValues { get; init; } - public LocalizedValue DefaultValues { get; init; } + public LocalizedValue DefaultValues { get; init; } - public string? DefaultValue { get; init; } + public string? DefaultValue { get; init; } - public string? Pattern { get; init; } + public string? Pattern { get; init; } - public string? PatternMessage { get; init; } + public string? PatternMessage { get; init; } - public string? FolderId { get; init; } + public string? FolderId { get; init; } - public int? MinLength { get; init; } + public int? MinLength { get; init; } - public int? MaxLength { get; init; } + public int? MaxLength { get; init; } - public int? MinCharacters { get; init; } + public int? MinCharacters { get; init; } - public int? MaxCharacters { get; init; } + public int? MaxCharacters { get; init; } - public int? MinWords { get; init; } + public int? MinWords { get; init; } - public int? MaxWords { get; init; } + public int? MaxWords { get; init; } - public bool IsUnique { get; init; } + public bool IsUnique { get; init; } - public bool IsEmbeddable { get; init; } + public bool IsEmbeddable { get; init; } - public bool InlineEditable { get; init; } + public bool InlineEditable { get; init; } - public bool CreateEnum { get; init; } + public bool CreateEnum { get; init; } - public StringContentType ContentType { get; init; } + public StringContentType ContentType { get; init; } - public StringFieldEditor Editor { get; init; } + public StringFieldEditor Editor { get; init; } - public ReadonlyList? SchemaIds { get; init; } + public ReadonlyList? SchemaIds { get; init; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.String(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.String(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.String(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.String(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs index 0b7c4ed212..5a45582b21 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldEditor.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum TagsFieldEditor { - public enum TagsFieldEditor - { - Tags, - Checkboxes, - Dropdown - } + Tags, + Checkboxes, + Dropdown } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs index 6dd40eb261..39bbd5324b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldNormalization.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum TagsFieldNormalization { - public enum TagsFieldNormalization - { - None, - Schema - } + None, + Schema } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs index 5da7af4cbd..eaf335f5cc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs @@ -7,44 +7,43 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record TagsFieldProperties : FieldProperties { - public sealed record TagsFieldProperties : FieldProperties - { - public ReadonlyList? AllowedValues { get; init; } + public ReadonlyList? AllowedValues { get; init; } - public LocalizedValue?> DefaultValues { get; init; } + public LocalizedValue?> DefaultValues { get; init; } - public ReadonlyList? DefaultValue { get; init; } + public ReadonlyList? DefaultValue { get; init; } - public int? MinItems { get; init; } + public int? MinItems { get; init; } - public int? MaxItems { get; init; } + public int? MaxItems { get; init; } - public bool CreateEnum { get; init; } + public bool CreateEnum { get; init; } - public TagsFieldEditor Editor { get; init; } + public TagsFieldEditor Editor { get; init; } - public TagsFieldNormalization Normalization { get; init; } + public TagsFieldNormalization Normalization { get; init; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return Fields.Tags(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return Fields.Tags(id, name, partitioning, this, settings); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return Fields.Tags(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return Fields.Tags(id, name, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs index 076c087eae..3d3061927e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public enum UIFieldEditor { - public enum UIFieldEditor - { - Separator - } + Separator } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs index 21978aab42..a5773cad7c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs @@ -5,30 +5,29 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Schemas; + +public sealed record UIFieldProperties : FieldProperties { - public sealed record UIFieldProperties : FieldProperties - { - public UIFieldEditor Editor { get; init; } + public UIFieldEditor Editor { get; init; } - public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override T Accept(IFieldVisitor visitor, IField field, TArgs args) - { - return visitor.Visit((IField)field, args); - } + public override T Accept(IFieldVisitor visitor, IField field, TArgs args) + { + return visitor.Visit((IField)field, args); + } - public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) - { - return new NestedField(id, name, this, settings); - } + public override NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null) + { + return new NestedField(id, name, this, settings); + } - public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) - { - return new RootField(id, name, partitioning, this, settings); - } + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) + { + return new RootField(id, name, partitioning, this, settings); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs index c7978b96af..8ddc20f1f2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs @@ -9,10 +9,9 @@ #pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +public sealed class SquidexCoreModel { - public sealed class SquidexCoreModel - { - public static readonly Assembly Assembly = typeof(SquidexCoreModel).Assembly; - } + public static readonly Assembly Assembly = typeof(SquidexCoreModel).Assembly; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs index 2cdfb7f5df..ba0e0724ba 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs @@ -10,42 +10,41 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class AddSchemaNames : IContentItemConverter { - public sealed class AddSchemaNames : IContentItemConverter + private readonly ResolvedComponents components; + + public AddSchemaNames(ResolvedComponents components) { - private readonly ResolvedComponents components; + this.components = components; + } - public AddSchemaNames(ResolvedComponents components) + public JsonObject ConvertItem(IField field, JsonObject source) + { + if (field is IArrayField) { - this.components = components; + return source; } - public JsonObject ConvertItem(IField field, JsonObject source) + if (source.ContainsKey("schemaName")) { - if (field is IArrayField) - { - return source; - } - - if (source.ContainsKey("schemaName")) - { - return source; - } - - if (!Component.IsValid(source, out var discriminator)) - { - return source; - } + return source; + } - var id = DomainId.Create(discriminator); + if (!Component.IsValid(source, out var discriminator)) + { + return source; + } - if (components.TryGetValue(id, out var schema)) - { - source["schemaName"] = schema.Name; - } + var id = DomainId.Create(discriminator); - return source; + if (components.TryGetValue(id, out var schema)) + { + source["schemaName"] = schema.Name; } + + return source; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs index cd2a627dab..6555f0893f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs @@ -12,295 +12,294 @@ #pragma warning disable RECS0033 // Convert 'if' to '||' expression -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class ContentConverter { - public sealed class ContentConverter + private readonly List itemConverters = new List(); + private readonly List fieldAfterConverters = new List(); + private readonly List fieldConverters = new List(); + private readonly List valueConverters = new List(); + private readonly ResolvedComponents components; + private readonly Schema schema; + + public ContentConverter(ResolvedComponents components, Schema schema) + { + this.components = components; + this.schema = schema; + } + + public ContentConverter Add(IConverter converter) { - private readonly List itemConverters = new List(); - private readonly List fieldAfterConverters = new List(); - private readonly List fieldConverters = new List(); - private readonly List valueConverters = new List(); - private readonly ResolvedComponents components; - private readonly Schema schema; - - public ContentConverter(ResolvedComponents components, Schema schema) + if (converter is IContentItemConverter itemConverter) { - this.components = components; - this.schema = schema; + itemConverters.Add(itemConverter); } - public ContentConverter Add(IConverter converter) + if (converter is IContentFieldConverter fieldConverter) { - if (converter is IContentItemConverter itemConverter) - { - itemConverters.Add(itemConverter); - } - - if (converter is IContentFieldConverter fieldConverter) - { - fieldConverters.Add(fieldConverter); - } - - if (converter is IContentFieldAfterConverter fieldAfterConverter) - { - fieldAfterConverters.Add(fieldAfterConverter); - } - - if (converter is IContentValueConverter valueConverter) - { - valueConverters.Add(valueConverter); - } + fieldConverters.Add(fieldConverter); + } - return this; + if (converter is IContentFieldAfterConverter fieldAfterConverter) + { + fieldAfterConverters.Add(fieldAfterConverter); } - public ContentData Convert(ContentData content) + if (converter is IContentValueConverter valueConverter) { - Guard.NotNull(schema); + valueConverters.Add(valueConverter); + } + + return this; + } + + public ContentData Convert(ContentData content) + { + Guard.NotNull(schema); - // The conversion process assumes that we have ownership of the data and can manipulate it. - // Clones are only created to save allocations. - var result = new ContentData(content.Count); + // The conversion process assumes that we have ownership of the data and can manipulate it. + // Clones are only created to save allocations. + var result = new ContentData(content.Count); - foreach (var (fieldName, fieldData) in content) + foreach (var (fieldName, fieldData) in content) + { + if (fieldData == null || !schema.FieldsByName.TryGetValue(fieldName, out var field)) { - if (fieldData == null || !schema.FieldsByName.TryGetValue(fieldName, out var field)) - { - continue; - } + continue; + } - // Some conversions are faster to do upfront, e.g. to remove hidden fields. - var newData = ConvertField(field, fieldData); + // Some conversions are faster to do upfront, e.g. to remove hidden fields. + var newData = ConvertField(field, fieldData); - if (newData == null) - { - continue; - } + if (newData == null) + { + continue; + } - newData = ConvertValues(field, newData); + newData = ConvertValues(field, newData); - // Some conversions are faster to do later, e.g. fallback handling for languages. - newData = ConvertFieldAfter(field, newData); + // Some conversions are faster to do later, e.g. fallback handling for languages. + newData = ConvertFieldAfter(field, newData); - if (newData != null) - { - result.Add(field.Name, newData); - } + if (newData != null) + { + result.Add(field.Name, newData); } - - return result; } - private ContentFieldData? ConvertField(IRootField field, ContentFieldData? data) + return result; + } + + private ContentFieldData? ConvertField(IRootField field, ContentFieldData? data) + { + foreach (var converter in fieldConverters) { - foreach (var converter in fieldConverters) - { - data = converter.ConvertField(field, data!); + data = converter.ConvertField(field, data!); - if (data == null) - { - break; - } + if (data == null) + { + break; } - - return data; } - private ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData? data) + return data; + } + + private ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData? data) + { + foreach (var converter in fieldAfterConverters) { - foreach (var converter in fieldAfterConverters) - { - data = converter.ConvertFieldAfter(field, data!); + data = converter.ConvertFieldAfter(field, data!); - if (data == null) - { - break; - } + if (data == null) + { + break; } - - return data; } - private (bool Remove, JsonValue) ConvertByType(T field, JsonValue value, IField? parent) where T : IField + return data; + } + + private (bool Remove, JsonValue) ConvertByType(T field, JsonValue value, IField? parent) where T : IField + { + switch (field) { - switch (field) - { - case IArrayField arrayField: - return ConvertArray(arrayField, value); + case IArrayField arrayField: + return ConvertArray(arrayField, value); - case IField: - return ConvertComponent(value, field); + case IField: + return ConvertComponent(value, field); - case IField: - return ConvertComponents(value, field); + case IField: + return ConvertComponents(value, field); - default: - return ConvertValue(field, value, parent); - } + default: + return ConvertValue(field, value, parent); } + } - private (bool Remove, JsonValue) ConvertArray(IArrayField field, JsonValue value) + private (bool Remove, JsonValue) ConvertArray(IArrayField field, JsonValue value) + { + if (value.Value is not JsonArray array) { - if (value.Value is not JsonArray array) - { - return (true, default); - } - - for (int i = 0; i < array.Count; i++) - { - var oldValue = array[i]; - - var (removed, newValue) = ConvertArrayItem(field, oldValue); - - if (removed) - { - array.RemoveAt(i); - i--; - } - else if (!ReferenceEquals(newValue.Value, oldValue.Value)) - { - array[i] = newValue; - } - } - - return (false, array); + return (true, default); } - private (bool Remove, JsonValue) ConvertComponents(JsonValue value, IField parent) + for (int i = 0; i < array.Count; i++) { - if (value.Value is not JsonArray array) + var oldValue = array[i]; + + var (removed, newValue) = ConvertArrayItem(field, oldValue); + + if (removed) { - return (true, default); + array.RemoveAt(i); + i--; } - - for (int i = 0; i < array.Count; i++) + else if (!ReferenceEquals(newValue.Value, oldValue.Value)) { - var oldValue = array[i]; - - var (removed, newValue) = ConvertComponent(oldValue, parent); - - if (removed) - { - array.RemoveAt(i); - i--; - } - else if (!ReferenceEquals(newValue.Value, oldValue.Value)) - { - // Faster to check for reference equality than for deep equals. - array[i] = newValue; - } + array[i] = newValue; } + } - return (false, array); + return (false, array); + } + + private (bool Remove, JsonValue) ConvertComponents(JsonValue value, IField parent) + { + if (value.Value is not JsonArray array) + { + return (true, default); } - private (bool Remove, JsonValue) ConvertComponent(JsonValue value, IField parent) + for (int i = 0; i < array.Count; i++) { - if (value.Value is not JsonObject obj || !obj.TryGetValue(Component.Discriminator, out var discriminator)) + var oldValue = array[i]; + + var (removed, newValue) = ConvertComponent(oldValue, parent); + + if (removed) { - return (true, default); + array.RemoveAt(i); + i--; } - - if (!components.TryGetValue(DomainId.Create(discriminator.ToString()), out var component)) + else if (!ReferenceEquals(newValue.Value, oldValue.Value)) { - return (true, default); + // Faster to check for reference equality than for deep equals. + array[i] = newValue; } + } - return (false, ConvertNested(component.FieldCollection, obj, parent)); + return (false, array); + } + + private (bool Remove, JsonValue) ConvertComponent(JsonValue value, IField parent) + { + if (value.Value is not JsonObject obj || !obj.TryGetValue(Component.Discriminator, out var discriminator)) + { + return (true, default); } - private (bool Remove, JsonValue) ConvertArrayItem(IArrayField field, JsonValue value) + if (!components.TryGetValue(DomainId.Create(discriminator.ToString()), out var component)) { - if (value.Value is not JsonObject obj) - { - return (true, default); - } + return (true, default); + } + + return (false, ConvertNested(component.FieldCollection, obj, parent)); + } - return (false, ConvertNested(field.FieldCollection, obj, field)); + private (bool Remove, JsonValue) ConvertArrayItem(IArrayField field, JsonValue value) + { + if (value.Value is not JsonObject obj) + { + return (true, default); } - private ContentFieldData ConvertValues(IField field, ContentFieldData source) + return (false, ConvertNested(field.FieldCollection, obj, field)); + } + + private ContentFieldData ConvertValues(IField field, ContentFieldData source) + { + ContentFieldData? result = null; + + foreach (var (key, oldValue) in source) { - ContentFieldData? result = null; + var (removed, newData) = ConvertByType(field, oldValue, null); - foreach (var (key, oldValue) in source) + // Create a copy to avoid allocations if nothing has been changed. + if (removed) { - var (removed, newData) = ConvertByType(field, oldValue, null); - - // Create a copy to avoid allocations if nothing has been changed. - if (removed) - { - result ??= new ContentFieldData(source); - result.Remove(key); - } - else if (!ReferenceEquals(newData.Value, oldValue.Value)) - { - // Faster to check for reference equality than for deep equals. - result ??= new ContentFieldData(source); - result[key] = newData; - } + result ??= new ContentFieldData(source); + result.Remove(key); + } + else if (!ReferenceEquals(newData.Value, oldValue.Value)) + { + // Faster to check for reference equality than for deep equals. + result ??= new ContentFieldData(source); + result[key] = newData; } - - return result ?? source; } - private JsonValue ConvertNested(FieldCollection fields, JsonObject source, IField parent) where T : IField + return result ?? source; + } + + private JsonValue ConvertNested(FieldCollection fields, JsonObject source, IField parent) where T : IField + { + JsonObject? result = null; + + foreach (var (key, oldValue) in source) { - JsonObject? result = null; + var newValue = oldValue; + + var remove = false; - foreach (var (key, oldValue) in source) + if (fields.ByName.TryGetValue(key, out var field)) { - var newValue = oldValue; - - var remove = false; - - if (fields.ByName.TryGetValue(key, out var field)) - { - (remove, newValue) = ConvertByType(field, oldValue, parent); - } - else if (key != Component.Discriminator) - { - remove = true; - } - - // Create a copy to avoid allocations if nothing has been changed. - if (remove) - { - result ??= new JsonObject(source); - result.Remove(key); - } - else if (!ReferenceEquals(newValue.Value, oldValue.Value)) - { - // Faster to check for reference equality than for deep equals. - result ??= new JsonObject(source); - result[key] = newValue; - } + (remove, newValue) = ConvertByType(field, oldValue, parent); + } + else if (key != Component.Discriminator) + { + remove = true; } - result ??= source; - - foreach (var converter in itemConverters) + // Create a copy to avoid allocations if nothing has been changed. + if (remove) { - result = converter.ConvertItem(parent, result); + result ??= new JsonObject(source); + result.Remove(key); } + else if (!ReferenceEquals(newValue.Value, oldValue.Value)) + { + // Faster to check for reference equality than for deep equals. + result ??= new JsonObject(source); + result[key] = newValue; + } + } + + result ??= source; - return result ?? source; + foreach (var converter in itemConverters) + { + result = converter.ConvertItem(parent, result); } - private (bool Remove, JsonValue) ConvertValue(IField field, JsonValue value, IField? parent) + return result ?? source; + } + + private (bool Remove, JsonValue) ConvertValue(IField field, JsonValue value, IField? parent) + { + foreach (var converter in valueConverters) { - foreach (var converter in valueConverters) - { - // Use a tuple and not a nullable result to avoid boxing and allocations. - (var remove, value) = converter.ConvertValue(field, value, parent); + // Use a tuple and not a nullable result to avoid boxing and allocations. + (var remove, value) = converter.ConvertValue(field, value, parent); - if (remove) - { - return (true, default); - } + if (remove) + { + return (true, default); } - - return (false, value); } + + return (false, value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs index 2520abcb71..e9561f5601 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs @@ -8,85 +8,84 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public static class ContentConverterFlat { - public static class ContentConverterFlat + public static Dictionary ToFlatten(this ContentData content) { - public static Dictionary ToFlatten(this ContentData content) + var result = new Dictionary(); + + foreach (var (key, value) in content) { - var result = new Dictionary(); + var first = GetFirst(value); - foreach (var (key, value) in content) + if (first != null) { - var first = GetFirst(value); - - if (first != null) - { - result[key] = first; - } + result[key] = first; } - - return result; } - public static FlatContentData ToFlatten(this ContentData content, string fallback) - { - var result = new FlatContentData(); - - foreach (var (key, value) in content) - { - if (TryGetFirst(value, fallback, out var first)) - { - result[key] = first; - } - } + return result; + } - return result; - } + public static FlatContentData ToFlatten(this ContentData content, string fallback) + { + var result = new FlatContentData(); - private static object? GetFirst(ContentFieldData? fieldData) + foreach (var (key, value) in content) { - if (fieldData == null || fieldData.Count == 0) + if (TryGetFirst(value, fallback, out var first)) { - return null; + result[key] = first; } + } - if (fieldData.Count == 1) - { - return fieldData.Values.First(); - } + return result; + } - return fieldData; + private static object? GetFirst(ContentFieldData? fieldData) + { + if (fieldData == null || fieldData.Count == 0) + { + return null; } - private static bool TryGetFirst(ContentFieldData? fieldData, string fallback, out JsonValue result) + if (fieldData.Count == 1) { - result = JsonValue.Null; + return fieldData.Values.First(); + } - if (fieldData == null) - { - return false; - } + return fieldData; + } - if (fieldData.Count == 1) - { - result = fieldData.Values.First(); - return true; - } + private static bool TryGetFirst(ContentFieldData? fieldData, string fallback, out JsonValue result) + { + result = JsonValue.Null; - if (fieldData.TryGetValue(fallback, out var value)) - { - result = value; - return true; - } + if (fieldData == null) + { + return false; + } - if (fieldData.Count > 1) - { - result = fieldData.Values.First(); - return true; - } + if (fieldData.Count == 1) + { + result = fieldData.Values.First(); + return true; + } - return false; + if (fieldData.TryGetValue(fallback, out var value)) + { + result = value; + return true; } + + if (fieldData.Count > 1) + { + result = fieldData.Values.First(); + return true; + } + + return false; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs index b82cb8cb1e..1b1d99a581 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs @@ -11,55 +11,54 @@ using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class ExcludeChangedTypes : IContentFieldConverter, IContentValueConverter { - public sealed class ExcludeChangedTypes : IContentFieldConverter, IContentValueConverter - { - private readonly IJsonSerializer serializer; + private readonly IJsonSerializer serializer; - public ExcludeChangedTypes(IJsonSerializer serializer) - { - this.serializer = serializer; - } + public ExcludeChangedTypes(IJsonSerializer serializer) + { + this.serializer = serializer; + } - public ContentFieldData? ConvertField(IRootField field, ContentFieldData source) + public ContentFieldData? ConvertField(IRootField field, ContentFieldData source) + { + foreach (var (_, value) in source) { - foreach (var (_, value) in source) + if (value.Value == default) { - if (value.Value == default) - { - continue; - } - - if (IsChangedType(field, value)) - { - return null; - } + continue; } - return source; - } - - public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) - { - if (parent == null || source == default) + if (IsChangedType(field, value)) { - return (false, source); + return null; } + } - return (IsChangedType(field, source), source); + return source; + } + + public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) + { + if (parent == null || source == default) + { + return (false, source); } - private bool IsChangedType(IField field, JsonValue source) + return (IsChangedType(field, source), source); + } + + private bool IsChangedType(IField field, JsonValue source) + { + try { - try - { - return !JsonValueValidator.IsValid(field, source, serializer); - } - catch - { - return true; - } + return !JsonValueValidator.IsValid(field, source, serializer); + } + catch + { + return true; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeHidden.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeHidden.cs index 68a88aaff9..6667ec5b74 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeHidden.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeHidden.cs @@ -9,24 +9,23 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class ExcludeHidden : IContentFieldConverter, IContentValueConverter { - public sealed class ExcludeHidden : IContentFieldConverter, IContentValueConverter - { - public static readonly ExcludeHidden Instance = new ExcludeHidden(); + public static readonly ExcludeHidden Instance = new ExcludeHidden(); - private ExcludeHidden() - { - } + private ExcludeHidden() + { + } - public ContentFieldData? ConvertField(IRootField field, ContentFieldData source) - { - return field.IsForApi() ? source : null; - } + public ContentFieldData? ConvertField(IRootField field, ContentFieldData source) + { + return field.IsForApi() ? source : null; + } - public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) - { - return field.IsForApi() ? (false, source) : (true, default); - } + public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) + { + return field.IsForApi() ? (false, source) : (true, default); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IConverter.cs index 8dbc5476ff..f242c93105 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IConverter.cs @@ -11,29 +11,28 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public interface IConverter +{ +} + +public interface IContentFieldAfterConverter : IConverter +{ + ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData source); +} + +public interface IContentFieldConverter : IConverter +{ + ContentFieldData? ConvertField(IRootField field, ContentFieldData source); +} + +public interface IContentItemConverter : IConverter +{ + JsonObject ConvertItem(IField field, JsonObject source); +} + +public interface IContentValueConverter : IConverter { - public interface IConverter - { - } - - public interface IContentFieldAfterConverter : IConverter - { - ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData source); - } - - public interface IContentFieldConverter : IConverter - { - ContentFieldData? ConvertField(IRootField field, ContentFieldData source); - } - - public interface IContentItemConverter : IConverter - { - JsonObject ConvertItem(IField field, JsonObject source); - } - - public interface IContentValueConverter : IConverter - { - (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent); - } + (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveAssetUrls.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveAssetUrls.cs index e85a303a28..1079872ba5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveAssetUrls.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveAssetUrls.cs @@ -9,70 +9,69 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class ResolveAssetUrls : IContentValueConverter { - public sealed class ResolveAssetUrls : IContentValueConverter + private readonly NamedId appId; + private readonly IUrlGenerator urlGenerator; + private readonly Func shouldHandle; + + public ResolveAssetUrls(NamedId appId, IUrlGenerator urlGenerator, IReadOnlyCollection? fields) { - private readonly NamedId appId; - private readonly IUrlGenerator urlGenerator; - private readonly Func shouldHandle; + this.appId = appId; - public ResolveAssetUrls(NamedId appId, IUrlGenerator urlGenerator, IReadOnlyCollection? fields) + if (fields == null || fields.Count == 0) + { + shouldHandle = (field, parent) => false; + } + else if (fields.Contains("*")) { - this.appId = appId; + shouldHandle = (field, parent) => true; + } + else + { + var paths = fields.Select(x => x.Split('.')).ToList(); - if (fields == null || fields.Count == 0) + shouldHandle = (field, parent) => { - shouldHandle = (field, parent) => false; - } - else if (fields.Contains("*")) - { - shouldHandle = (field, parent) => true; - } - else - { - var paths = fields.Select(x => x.Split('.')).ToList(); - - shouldHandle = (field, parent) => + for (var i = 0; i < paths.Count; i++) { - for (var i = 0; i < paths.Count; i++) - { - var path = paths[i]; + var path = paths[i]; - if (parent != null) + if (parent != null) + { + if (path.Length == 2 && path[0] == parent.Name && path[1] == field.Name) { - if (path.Length == 2 && path[0] == parent.Name && path[1] == field.Name) - { - return true; - } + return true; } - else + } + else + { + if (path.Length == 1 && path[0] == field.Name) { - if (path.Length == 1 && path[0] == field.Name) - { - return true; - } + return true; } } + } - return false; - }; - } - - this.urlGenerator = urlGenerator; + return false; + }; } - public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) + this.urlGenerator = urlGenerator; + } + + public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) + { + if (field is IField && source.Value is JsonArray a && shouldHandle(field, parent)) { - if (field is IField && source.Value is JsonArray a && shouldHandle(field, parent)) + for (var i = 0; i < a.Count; i++) { - for (var i = 0; i < a.Count; i++) - { - a[i] = urlGenerator.AssetContent(appId, a[i].ToString()); - } + a[i] = urlGenerator.AssetContent(appId, a[i].ToString()); } - - return (false, source); } + + return (false, source); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveInvariant.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveInvariant.cs index d57289b7f6..bdbb6168c4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveInvariant.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveInvariant.cs @@ -9,50 +9,49 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class ResolveInvariant : IContentFieldAfterConverter { - public sealed class ResolveInvariant : IContentFieldAfterConverter + private readonly LanguagesConfig languages; + + public ResolveInvariant(LanguagesConfig languages) { - private readonly LanguagesConfig languages; + this.languages = languages; + } - public ResolveInvariant(LanguagesConfig languages) + public ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData source) + { + if (!field.Partitioning.Equals(Partitioning.Invariant)) { - this.languages = languages; + return source; } - public ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData source) + if (source.TryGetNonNull(InvariantPartitioning.Key, out _)) { - if (!field.Partitioning.Equals(Partitioning.Invariant)) - { - return source; - } - - if (source.TryGetNonNull(InvariantPartitioning.Key, out _)) - { - return source; - } - - if (source.TryGetNonNull(languages.Master, out var value)) - { - source.Clear(); - source[InvariantPartitioning.Key] = value; - - return source; - } + return source; + } - if (source.Count > 0) - { - var first = source.First().Value; + if (source.TryGetNonNull(languages.Master, out var value)) + { + source.Clear(); + source[InvariantPartitioning.Key] = value; - source.Clear(); - source[InvariantPartitioning.Key] = first; + return source; + } - return source; - } + if (source.Count > 0) + { + var first = source.First().Value; source.Clear(); + source[InvariantPartitioning.Key] = first; return source; } + + source.Clear(); + + return source; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveLanguages.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveLanguages.cs index fe842b8079..d30ca0b5d7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveLanguages.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveLanguages.cs @@ -10,91 +10,90 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class ResolveLanguages : IContentFieldAfterConverter { - public sealed class ResolveLanguages : IContentFieldAfterConverter + private readonly LanguagesConfig languages; + private readonly bool resolveFallback; + private readonly HashSet languageCodes; + + public ResolveLanguages(LanguagesConfig languages, bool resolveFallback = true, params Language[] filteredLanguages) { - private readonly LanguagesConfig languages; - private readonly bool resolveFallback; - private readonly HashSet languageCodes; + this.languages = languages; - public ResolveLanguages(LanguagesConfig languages, bool resolveFallback = true, params Language[] filteredLanguages) + if (filteredLanguages?.Length > 0) + { + languageCodes = languages.AllKeys.Intersect(filteredLanguages.Select(x => x.Iso2Code)).ToHashSet(); + } + else { - this.languages = languages; + languageCodes = languages.AllKeys.ToHashSet(); + } - if (filteredLanguages?.Length > 0) - { - languageCodes = languages.AllKeys.Intersect(filteredLanguages.Select(x => x.Iso2Code)).ToHashSet(); - } - else - { - languageCodes = languages.AllKeys.ToHashSet(); - } + if (languageCodes.Count == 0) + { + languageCodes.Add(languages.Master); + } - if (languageCodes.Count == 0) - { - languageCodes.Add(languages.Master); - } + this.resolveFallback = resolveFallback; + } - this.resolveFallback = resolveFallback; + public ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData source) + { + if (!field.Partitioning.Equals(Partitioning.Language)) + { + return source; } - public ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData source) + if (source.TryGetNonNull(InvariantPartitioning.Key, out var value)) { - if (!field.Partitioning.Equals(Partitioning.Language)) + source = new ContentFieldData { - return source; - } - - if (source.TryGetNonNull(InvariantPartitioning.Key, out var value)) - { - source = new ContentFieldData - { - [languages.Master] = value - }; - } + [languages.Master] = value + }; + } - if (resolveFallback) + if (resolveFallback) + { + foreach (var languageCode in languageCodes) { - foreach (var languageCode in languageCodes) + if (source.TryGetNonNull(languageCode, out _)) { - if (source.TryGetNonNull(languageCode, out _)) - { - continue; - } - - foreach (var fallback in languages.GetPriorities(languageCode)) - { - if (source.TryGetNonNull(fallback, out var fallbackValue)) - { - source[languageCode] = fallbackValue; - break; - } - } + continue; } - } - - while (true) - { - var isRemoved = false; - foreach (var (key, _) in source) + foreach (var fallback in languages.GetPriorities(languageCode)) { - if (!languageCodes.Contains(key)) + if (source.TryGetNonNull(fallback, out var fallbackValue)) { - source.Remove(key); - isRemoved = true; + source[languageCode] = fallbackValue; break; } } + } + } + + while (true) + { + var isRemoved = false; - if (!isRemoved) + foreach (var (key, _) in source) + { + if (!languageCodes.Contains(key)) { + source.Remove(key); + isRemoved = true; break; } } - return source; + if (!isRemoved) + { + break; + } } + + return source; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs index 4bcea3c5aa..fccf4a86ac 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs @@ -11,142 +11,141 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.ConvertContent +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class StringFormatter : IFieldPropertiesVisitor { - public sealed class StringFormatter : IFieldPropertiesVisitor + private static readonly StringFormatter Instance = new StringFormatter(); + + public record struct Args(JsonValue Value); + + private StringFormatter() { - private static readonly StringFormatter Instance = new StringFormatter(); + } - public record struct Args(JsonValue Value); + public static string Format(IField field, JsonValue value) + { + Guard.NotNull(field); - private StringFormatter() + if (value == default) { + return string.Empty; } - public static string Format(IField field, JsonValue value) - { - Guard.NotNull(field); + var args = new Args(value); - if (value == default) - { - return string.Empty; - } + return field.RawProperties.Accept(Instance, args); + } - var args = new Args(value); + public string Visit(ArrayFieldProperties properties, Args args) + { + return FormatArray(args.Value, "Item", "Items"); + } - return field.RawProperties.Accept(Instance, args); - } + public string Visit(AssetsFieldProperties properties, Args args) + { + return FormatArray(args.Value, "Asset", "Assets"); + } - public string Visit(ArrayFieldProperties properties, Args args) + public string Visit(BooleanFieldProperties properties, Args args) + { + if (Equals(args.Value.Value, true)) { - return FormatArray(args.Value, "Item", "Items"); + return "Yes"; } - - public string Visit(AssetsFieldProperties properties, Args args) + else { - return FormatArray(args.Value, "Asset", "Assets"); + return "No"; } + } - public string Visit(BooleanFieldProperties properties, Args args) - { - if (Equals(args.Value.Value, true)) - { - return "Yes"; - } - else - { - return "No"; - } - } + public string Visit(ComponentFieldProperties properties, Args args) + { + return "{ Component }"; + } - public string Visit(ComponentFieldProperties properties, Args args) - { - return "{ Component }"; - } + public string Visit(ComponentsFieldProperties properties, Args args) + { + return FormatArray(args.Value, "Component", "Components"); + } - public string Visit(ComponentsFieldProperties properties, Args args) - { - return FormatArray(args.Value, "Component", "Components"); - } + public string Visit(DateTimeFieldProperties properties, Args args) + { + return args.Value.ToString(); + } - public string Visit(DateTimeFieldProperties properties, Args args) + public string Visit(GeolocationFieldProperties properties, Args args) + { + if (args.Value.Value is JsonObject o && + o.TryGetValue("latitude", out var found) && found.Value is double lat && + o.TryGetValue("longitude", out found) && found.Value is double lon) { - return args.Value.ToString(); + return $"{lat}, {lon}"; } - - public string Visit(GeolocationFieldProperties properties, Args args) + else { - if (args.Value.Value is JsonObject o && - o.TryGetValue("latitude", out var found) && found.Value is double lat && - o.TryGetValue("longitude", out found) && found.Value is double lon) - { - return $"{lat}, {lon}"; - } - else - { - return string.Empty; - } + return string.Empty; } + } - public string Visit(JsonFieldProperties properties, Args args) + public string Visit(JsonFieldProperties properties, Args args) + { + return ""; + } + + public string Visit(NumberFieldProperties properties, Args args) + { + return args.Value.ToString(); + } + + public string Visit(ReferencesFieldProperties properties, Args args) + { + return FormatArray(args.Value, "Reference", "References"); + } + + public string Visit(StringFieldProperties properties, Args args) + { + if (properties.Editor == StringFieldEditor.StockPhoto) { - return ""; + return "[Photo]"; } - - public string Visit(NumberFieldProperties properties, Args args) + else { return args.Value.ToString(); } + } - public string Visit(ReferencesFieldProperties properties, Args args) + public string Visit(TagsFieldProperties properties, Args args) + { + if (args.Value.Value is JsonArray a) { - return FormatArray(args.Value, "Reference", "References"); + return string.Join(", ", a); } - - public string Visit(StringFieldProperties properties, Args args) + else { - if (properties.Editor == StringFieldEditor.StockPhoto) - { - return "[Photo]"; - } - else - { - return args.Value.ToString(); - } + return string.Empty; } + } - public string Visit(TagsFieldProperties properties, Args args) + public string Visit(UIFieldProperties properties, Args args) + { + return string.Empty; + } + + private static string FormatArray(JsonValue value, string singularName, string pluralName) + { + if (value.Value is JsonArray a) { - if (args.Value.Value is JsonArray a) + if (a.Count > 1) { - return string.Join(", ", a); + return $"{a.Count} {pluralName}"; } - else + else if (a.Count == 1) { - return string.Empty; + return $"1 {singularName}"; } } - public string Visit(UIFieldProperties properties, Args args) - { - return string.Empty; - } - - private static string FormatArray(JsonValue value, string singularName, string pluralName) - { - if (value.Value is JsonArray a) - { - if (a.Count > 1) - { - return $"{a.Count} {pluralName}"; - } - else if (a.Count == 1) - { - return $"1 {singularName}"; - } - } - - return $"0 {pluralName}"; - } + return $"0 {pluralName}"; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs index 669ae76b5c..741f758414 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs @@ -10,49 +10,48 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.DefaultValues +namespace Squidex.Domain.Apps.Core.DefaultValues; + +public static class DefaultValueExtensions { - public static class DefaultValueExtensions + public static void GenerateDefaultValues(this ContentData data, Schema schema, PartitionResolver partitionResolver) { - public static void GenerateDefaultValues(this ContentData data, Schema schema, PartitionResolver partitionResolver) + Guard.NotNull(schema); + Guard.NotNull(partitionResolver); + + foreach (var field in schema.Fields) { - Guard.NotNull(schema); - Guard.NotNull(partitionResolver); + var fieldData = data.GetOrCreate(field.Name, _ => new ContentFieldData()); - foreach (var field in schema.Fields) + if (fieldData != null) { - var fieldData = data.GetOrCreate(field.Name, _ => new ContentFieldData()); + var partitioning = partitionResolver(field.Partitioning); - if (fieldData != null) + foreach (var partitionKey in partitioning.AllKeys) { - var partitioning = partitionResolver(field.Partitioning); - - foreach (var partitionKey in partitioning.AllKeys) - { - Enrich(field, fieldData, partitionKey); - } + Enrich(field, fieldData, partitionKey); + } - if (fieldData.Count > 0) - { - data[field.Name] = fieldData; - } + if (fieldData.Count > 0) + { + data[field.Name] = fieldData; } } } + } - private static void Enrich(IField field, ContentFieldData fieldData, string partitionKey) - { - var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant(), partitionKey); + private static void Enrich(IField field, ContentFieldData fieldData, string partitionKey) + { + var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant(), partitionKey); - if (field.RawProperties.IsRequired || defaultValue == default) - { - return; - } + if (field.RawProperties.IsRequired || defaultValue == default) + { + return; + } - if (!fieldData.TryGetValue(partitionKey, out _)) - { - fieldData.AddLocalized(partitionKey, defaultValue); - } + if (!fieldData.TryGetValue(partitionKey, out _)) + { + fieldData.AddLocalized(partitionKey, defaultValue); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs index dfbad3f0b4..668bacc501 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs @@ -13,137 +13,136 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.DefaultValues +namespace Squidex.Domain.Apps.Core.DefaultValues; + +public sealed class DefaultValueFactory : IFieldPropertiesVisitor { - public sealed class DefaultValueFactory : IFieldPropertiesVisitor - { - private static readonly DefaultValueFactory Instance = new DefaultValueFactory(); + private static readonly DefaultValueFactory Instance = new DefaultValueFactory(); - public record struct Args(Instant Now, string Partition); + public record struct Args(Instant Now, string Partition); - private DefaultValueFactory() - { - } + private DefaultValueFactory() + { + } - public static JsonValue CreateDefaultValue(IField field, Instant now, string partition) - { - Guard.NotNull(field); - Guard.NotNull(partition); + public static JsonValue CreateDefaultValue(IField field, Instant now, string partition) + { + Guard.NotNull(field); + Guard.NotNull(partition); - var x = field.RawProperties.Accept(Instance, new Args(now, partition)); + var x = field.RawProperties.Accept(Instance, new Args(now, partition)); - return x; - } + return x; + } - public JsonValue Visit(ArrayFieldProperties properties, Args args) - { - return new JsonArray(); - } + public JsonValue Visit(ArrayFieldProperties properties, Args args) + { + return new JsonArray(); + } - public JsonValue Visit(AssetsFieldProperties properties, Args args) - { - var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); + public JsonValue Visit(AssetsFieldProperties properties, Args args) + { + var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); - return Array(value); - } + return Array(value); + } - public JsonValue Visit(BooleanFieldProperties properties, Args args) - { - var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); + public JsonValue Visit(BooleanFieldProperties properties, Args args) + { + var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); - return value ?? JsonValue.Null; - } + return value ?? JsonValue.Null; + } - public JsonValue Visit(ComponentFieldProperties properties, Args args) - { - return JsonValue.Null; - } + public JsonValue Visit(ComponentFieldProperties properties, Args args) + { + return JsonValue.Null; + } - public JsonValue Visit(ComponentsFieldProperties properties, Args args) - { - return new JsonArray(); - } + public JsonValue Visit(ComponentsFieldProperties properties, Args args) + { + return new JsonArray(); + } - public JsonValue Visit(GeolocationFieldProperties properties, Args args) - { - return JsonValue.Null; - } + public JsonValue Visit(GeolocationFieldProperties properties, Args args) + { + return JsonValue.Null; + } - public JsonValue Visit(JsonFieldProperties properties, Args args) - { - return JsonValue.Null; - } + public JsonValue Visit(JsonFieldProperties properties, Args args) + { + return JsonValue.Null; + } - public JsonValue Visit(NumberFieldProperties properties, Args args) - { - var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); + public JsonValue Visit(NumberFieldProperties properties, Args args) + { + var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); - return value ?? JsonValue.Null; - } + return value ?? JsonValue.Null; + } - public JsonValue Visit(ReferencesFieldProperties properties, Args args) - { - var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); + public JsonValue Visit(ReferencesFieldProperties properties, Args args) + { + var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); - return Array(value); - } + return Array(value); + } - public JsonValue Visit(StringFieldProperties properties, Args args) - { - var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); + public JsonValue Visit(StringFieldProperties properties, Args args) + { + var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); - return value; - } + return value; + } - public JsonValue Visit(TagsFieldProperties properties, Args args) - { - var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); + public JsonValue Visit(TagsFieldProperties properties, Args args) + { + var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); - return Array(value); - } + return Array(value); + } - public JsonValue Visit(UIFieldProperties properties, Args args) + public JsonValue Visit(UIFieldProperties properties, Args args) + { + return JsonValue.Null; + } + + public JsonValue Visit(DateTimeFieldProperties properties, Args args) + { + if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) { - return JsonValue.Null; + return JsonValue.Create(args.Now); } - public JsonValue Visit(DateTimeFieldProperties properties, Args args) + if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) { - if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) - { - return JsonValue.Create(args.Now); - } + return JsonValue.Create($"{args.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}T00:00:00Z"); + } - if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) - { - return JsonValue.Create($"{args.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}T00:00:00Z"); - } + var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); - var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition); + return value ?? JsonValue.Null; + } - return value ?? JsonValue.Null; + private static T GetDefaultValue(T value, LocalizedValue? values, string partition) + { + if (values != null && values.TryGetValue(partition, out var @default)) + { + return @default; } - private static T GetDefaultValue(T value, LocalizedValue? values, string partition) - { - if (values != null && values.TryGetValue(partition, out var @default)) - { - return @default; - } + return value; + } - return value; + private static JsonValue Array(IEnumerable? values) + { + if (values != null) + { + return JsonValue.Array(values); } - - private static JsonValue Array(IEnumerable? values) + else { - if (values != null) - { - return JsonValue.Array(values); - } - else - { - return new JsonArray(); - } + return new JsonArray(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs index 35588d0b42..6438904f26 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.EventSynchronization +namespace Squidex.Domain.Apps.Core.EventSynchronization; + +public sealed class SchemaSynchronizationOptions { - public sealed class SchemaSynchronizationOptions - { - public bool NoFieldDeletion { get; set; } + public bool NoFieldDeletion { get; set; } - public bool NoFieldRecreation { get; set; } - } + public bool NoFieldRecreation { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs index ee24f7a63f..946b0a629d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs @@ -10,209 +10,208 @@ using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.EventSynchronization +namespace Squidex.Domain.Apps.Core.EventSynchronization; + +public static class SchemaSynchronizer { - public static class SchemaSynchronizer + public static IEnumerable Synchronize(this Schema source, Schema? target, Func idGenerator, + SchemaSynchronizationOptions? options = null) { - public static IEnumerable Synchronize(this Schema source, Schema? target, Func idGenerator, - SchemaSynchronizationOptions? options = null) + Guard.NotNull(source); + Guard.NotNull(idGenerator); + + if (target == null) + { + yield return new SchemaDeleted(); + } + else { - Guard.NotNull(source); - Guard.NotNull(idGenerator); + options ??= new SchemaSynchronizationOptions(); - if (target == null) + if (!source.Properties.Equals(target.Properties)) { - yield return new SchemaDeleted(); + yield return new SchemaUpdated { Properties = target.Properties }; } - else + + if (!source.Category.StringEquals(target.Category)) { - options ??= new SchemaSynchronizationOptions(); + yield return new SchemaCategoryChanged { Name = target.Category }; + } - if (!source.Properties.Equals(target.Properties)) - { - yield return new SchemaUpdated { Properties = target.Properties }; - } + if (!source.Scripts.Equals(target.Scripts)) + { + yield return new SchemaScriptsConfigured { Scripts = target.Scripts }; + } - if (!source.Category.StringEquals(target.Category)) - { - yield return new SchemaCategoryChanged { Name = target.Category }; - } + if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) + { + yield return new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls }; + } - if (!source.Scripts.Equals(target.Scripts)) - { - yield return new SchemaScriptsConfigured { Scripts = target.Scripts }; - } + if (source.IsPublished != target.IsPublished) + { + yield return target.IsPublished ? + new SchemaPublished() : + new SchemaUnpublished(); + } - if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls)) - { - yield return new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls }; - } + var events = SyncFields(source.FieldCollection, target.FieldCollection, idGenerator, CanUpdateRoot, options); - if (source.IsPublished != target.IsPublished) - { - yield return target.IsPublished ? - new SchemaPublished() : - new SchemaUnpublished(); - } + foreach (var @event in events) + { + yield return @event; + } - var events = SyncFields(source.FieldCollection, target.FieldCollection, idGenerator, CanUpdateRoot, options); + if (!source.FieldsInLists.Equals(target.FieldsInLists)) + { + yield return new SchemaUIFieldsConfigured { FieldsInLists = target.FieldsInLists }; + } - foreach (var @event in events) - { - yield return @event; - } + if (!source.FieldsInReferences.Equals(target.FieldsInReferences)) + { + yield return new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences }; + } - if (!source.FieldsInLists.Equals(target.FieldsInLists)) - { - yield return new SchemaUIFieldsConfigured { FieldsInLists = target.FieldsInLists }; - } + if (!source.FieldRules.Equals(target.FieldRules)) + { + yield return new SchemaFieldRulesConfigured { FieldRules = target.FieldRules }; + } + } + } - if (!source.FieldsInReferences.Equals(target.FieldsInReferences)) - { - yield return new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences }; - } + private static IEnumerable SyncFields( + FieldCollection source, + FieldCollection target, + Func idGenerator, + Func canUpdate, + SchemaSynchronizationOptions options) where T : class, IField + { + var sourceIds = source.Ordered.Select(x => x.NamedId()).ToList(); - if (!source.FieldRules.Equals(target.FieldRules)) + if (!options.NoFieldDeletion) + { + foreach (var sourceField in source.Ordered) + { + if (!target.ByName.TryGetValue(sourceField.Name, out _)) { - yield return new SchemaFieldRulesConfigured { FieldRules = target.FieldRules }; + var id = sourceField.NamedId(); + + sourceIds.Remove(id); + + yield return new FieldDeleted { FieldId = id }; } } } - private static IEnumerable SyncFields( - FieldCollection source, - FieldCollection target, - Func idGenerator, - Func canUpdate, - SchemaSynchronizationOptions options) where T : class, IField + foreach (var targetField in target.Ordered) { - var sourceIds = source.Ordered.Select(x => x.NamedId()).ToList(); + NamedId? id = null; + + var canCreateField = true; - if (!options.NoFieldDeletion) + if (source.ByName.TryGetValue(targetField.Name, out var sourceField)) { - foreach (var sourceField in source.Ordered) + canCreateField = false; + + id = sourceField.NamedId(); + + if (canUpdate(sourceField, targetField)) { - if (!target.ByName.TryGetValue(sourceField.Name, out _)) + if (!sourceField.RawProperties.Equals(targetField.RawProperties as object)) { - var id = sourceField.NamedId(); + yield return new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }; + } + } + else if (!sourceField.IsLocked && !options.NoFieldRecreation) + { + canCreateField = true; - sourceIds.Remove(id); + sourceIds.Remove(id); - yield return new FieldDeleted { FieldId = id }; - } + yield return new FieldDeleted { FieldId = id }; } } - foreach (var targetField in target.Ordered) + if (canCreateField) { - NamedId? id = null; - - var canCreateField = true; + var partitioning = (string?)null; - if (source.ByName.TryGetValue(targetField.Name, out var sourceField)) + if (targetField is IRootField rootField) { - canCreateField = false; + partitioning = rootField.Partitioning.Key; + } - id = sourceField.NamedId(); + id = NamedId.Of(idGenerator(), targetField.Name); - if (canUpdate(sourceField, targetField)) - { - if (!sourceField.RawProperties.Equals(targetField.RawProperties as object)) - { - yield return new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }; - } - } - else if (!sourceField.IsLocked && !options.NoFieldRecreation) - { - canCreateField = true; + yield return new FieldAdded + { + Name = targetField.Name, + Partitioning = partitioning, + Properties = targetField.RawProperties, + FieldId = id + }; - sourceIds.Remove(id); + sourceIds.Add(id); + } - yield return new FieldDeleted { FieldId = id }; - } + if (id != null && (sourceField == null || CanUpdate(sourceField, targetField))) + { + if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) + { + yield return new FieldLocked { FieldId = id }; } - if (canCreateField) + if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) { - var partitioning = (string?)null; - - if (targetField is IRootField rootField) - { - partitioning = rootField.Partitioning.Key; - } - - id = NamedId.Of(idGenerator(), targetField.Name); - - yield return new FieldAdded - { - Name = targetField.Name, - Partitioning = partitioning, - Properties = targetField.RawProperties, - FieldId = id - }; - - sourceIds.Add(id); + yield return targetField.IsHidden ? + new FieldHidden { FieldId = id } : + new FieldShown { FieldId = id }; } - if (id != null && (sourceField == null || CanUpdate(sourceField, targetField))) + if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) { - if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked)) - { - yield return new FieldLocked { FieldId = id }; - } + yield return targetField.IsDisabled ? + new FieldDisabled { FieldId = id } : + new FieldEnabled { FieldId = id }; + } - if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden)) - { - yield return targetField.IsHidden ? - new FieldHidden { FieldId = id } : - new FieldShown { FieldId = id }; - } + if (sourceField is null or IArrayField && targetField is IArrayField targetArrayField) + { + var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.Empty; - if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled)) - { - yield return targetField.IsDisabled ? - new FieldDisabled { FieldId = id } : - new FieldEnabled { FieldId = id }; - } + var events = SyncFields(fields, targetArrayField.FieldCollection, idGenerator, CanUpdate, options); - if (sourceField is null or IArrayField && targetField is IArrayField targetArrayField) + foreach (var @event in events) { - var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.Empty; + @event.ParentFieldId = id; - var events = SyncFields(fields, targetArrayField.FieldCollection, idGenerator, CanUpdate, options); - - foreach (var @event in events) - { - @event.ParentFieldId = id; - - yield return @event; - } + yield return @event; } } } + } - if (sourceIds.Count > 1) - { - var sourceNames = sourceIds.Select(x => x.Name).ToHashSet(); - var targetNames = target.Ordered.Select(x => x.Name).ToHashSet(); + if (sourceIds.Count > 1) + { + var sourceNames = sourceIds.Select(x => x.Name).ToHashSet(); + var targetNames = target.Ordered.Select(x => x.Name).ToHashSet(); - if (sourceNames.SetEquals(targetNames) && !sourceNames.SequenceEqual(targetNames)) - { - var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToArray(); + if (sourceNames.SetEquals(targetNames) && !sourceNames.SequenceEqual(targetNames)) + { + var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToArray(); - yield return new SchemaFieldsReordered { FieldIds = fieldIds }; - } + yield return new SchemaFieldsReordered { FieldIds = fieldIds }; } } + } - private static bool CanUpdateRoot(IRootField source, IRootField target) - { - return CanUpdate(source, target) && source.Partitioning == target.Partitioning; - } + private static bool CanUpdateRoot(IRootField source, IRootField target) + { + return CanUpdate(source, target) && source.Partitioning == target.Partitioning; + } - private static bool CanUpdate(IField source, IField target) - { - return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties); - } + private static bool CanUpdate(IField source, IField target) + { + return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs index 0f25bdf04c..00174b0e0c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs @@ -5,23 +5,22 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.EventSynchronization +namespace Squidex.Domain.Apps.Core.EventSynchronization; + +public static class SyncHelpers { - public static class SyncHelpers + public static bool BoolEquals(this bool lhs, bool? rhs) { - public static bool BoolEquals(this bool lhs, bool? rhs) - { - return lhs == (rhs ?? false); - } + return lhs == (rhs ?? false); + } - public static bool StringEquals(this string? lhs, string? rhs) - { - return string.Equals(lhs ?? string.Empty, rhs ?? string.Empty, StringComparison.Ordinal); - } + public static bool StringEquals(this string? lhs, string? rhs) + { + return string.Equals(lhs ?? string.Empty, rhs ?? string.Empty, StringComparison.Ordinal); + } - public static bool TypeEquals(this object lhs, object rhs) - { - return lhs.GetType() == rhs.GetType(); - } + public static bool TypeEquals(this object lhs, object rhs) + { + return lhs.GetType() == rhs.GetType(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs index 0ff16f3a60..41e1ecfbb8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs @@ -11,140 +11,139 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds; + +public static class ContentReferencesExtensions { - public static class ContentReferencesExtensions + public static bool CanHaveReference(this ContentData source) { - public static bool CanHaveReference(this ContentData source) + if (source.Count == 0) { - if (source.Count == 0) + return false; + } + + static bool CanValueHaveReference(JsonValue value) + { + if (value.Value is JsonArray) { - return false; + return true; } - static bool CanValueHaveReference(JsonValue value) + if (value.Value is JsonObject o) { - if (value.Value is JsonArray) - { - return true; - } - - if (value.Value is JsonObject o) + foreach (var (_, nested) in o) { - foreach (var (_, nested) in o) + if (CanValueHaveReference(nested)) { - if (CanValueHaveReference(nested)) - { - return true; - } + return true; } } - - return false; } - foreach (var (_, field) in source) + return false; + } + + foreach (var (_, field) in source) + { + if (field != null) { - if (field != null) + foreach (var (_, value) in field) { - foreach (var (_, value) in field) + if (CanValueHaveReference(value)) { - if (CanValueHaveReference(value)) - { - return true; - } + return true; } } } - - return false; } - public static void AddReferencedIds(this ContentData source, Schema schema, HashSet result, - ResolvedComponents components, int take = int.MaxValue) - { - Guard.NotNull(schema); - Guard.NotNull(result); - Guard.NotNull(components); + return false; + } - ReferencesExtractor.Extract(schema.Fields, source, result, take, components); - } + public static void AddReferencedIds(this ContentData source, Schema schema, HashSet result, + ResolvedComponents components, int take = int.MaxValue) + { + Guard.NotNull(schema); + Guard.NotNull(result); + Guard.NotNull(components); - public static void AddReferencedIds(this ContentData source, IEnumerable fields, HashSet result, - ResolvedComponents components, int take = int.MaxValue) - { - Guard.NotNull(fields); - Guard.NotNull(result); - Guard.NotNull(components); + ReferencesExtractor.Extract(schema.Fields, source, result, take, components); + } - ReferencesExtractor.Extract(fields, source, result, take, components); - } + public static void AddReferencedIds(this ContentData source, IEnumerable fields, HashSet result, + ResolvedComponents components, int take = int.MaxValue) + { + Guard.NotNull(fields); + Guard.NotNull(result); + Guard.NotNull(components); - public static void AddReferencedIds(this JsonValue value, IField field, HashSet result, - ResolvedComponents components, int take = int.MaxValue) - { - Guard.NotNull(field); - Guard.NotNull(result); - Guard.NotNull(components); + ReferencesExtractor.Extract(fields, source, result, take, components); + } - ReferencesExtractor.Extract(field, value, result, take, components); - } + public static void AddReferencedIds(this JsonValue value, IField field, HashSet result, + ResolvedComponents components, int take = int.MaxValue) + { + Guard.NotNull(field); + Guard.NotNull(result); + Guard.NotNull(components); - public static HashSet GetReferencedIds(this IField field, JsonValue value, - ResolvedComponents components, int take = int.MaxValue) - { - var result = new HashSet(); + ReferencesExtractor.Extract(field, value, result, take, components); + } - AddReferencedIds(value, field, result, components, take); + public static HashSet GetReferencedIds(this IField field, JsonValue value, + ResolvedComponents components, int take = int.MaxValue) + { + var result = new HashSet(); - return result; - } + AddReferencedIds(value, field, result, components, take); - public static JsonObject FormatReferences(this ContentData data, Schema schema, IFieldPartitioning partitioning, string separator = ", ") - { - Guard.NotNull(schema); - Guard.NotNull(partitioning); + return result; + } - var result = new JsonObject(); + public static JsonObject FormatReferences(this ContentData data, Schema schema, IFieldPartitioning partitioning, string separator = ", ") + { + Guard.NotNull(schema); + Guard.NotNull(partitioning); - foreach (var partitionKey in partitioning.AllKeys) - { - result[partitionKey] = data.FormatReferenceFields(schema, partitionKey, separator); - } + var result = new JsonObject(); - return result; + foreach (var partitionKey in partitioning.AllKeys) + { + result[partitionKey] = data.FormatReferenceFields(schema, partitionKey, separator); } - private static string FormatReferenceFields(this ContentData data, Schema schema, string partitionKey, string separator) - { - Guard.NotNull(schema); + return result; + } - var sb = new StringBuilder(); + private static string FormatReferenceFields(this ContentData data, Schema schema, string partitionKey, string separator) + { + Guard.NotNull(schema); - void AddValue(object value) - { - sb.AppendIfNotEmpty(separator); - sb.Append(value); - } + var sb = new StringBuilder(); + + void AddValue(object value) + { + sb.AppendIfNotEmpty(separator); + sb.Append(value); + } - var referenceFields = schema.ReferenceFields(); + var referenceFields = schema.ReferenceFields(); - foreach (var referenceField in referenceFields) + foreach (var referenceField in referenceFields) + { + if (data.TryGetValue(referenceField.Name, out var fieldData) && fieldData != null) { - if (data.TryGetValue(referenceField.Name, out var fieldData) && fieldData != null) + if (fieldData.TryGetValue(partitionKey, out var value)) { - if (fieldData.TryGetValue(partitionKey, out var value)) - { - AddValue(value); - } - else if (fieldData.TryGetValue(InvariantPartitioning.Key, out var value2)) - { - AddValue(value2); - } + AddValue(value); + } + else if (fieldData.TryGetValue(InvariantPartitioning.Key, out var value2)) + { + AddValue(value2); } } - - return sb.ToString(); } + + return sb.ToString(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs index 5042e73a4c..66b6642030 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs @@ -11,119 +11,118 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds; + +internal sealed class ReferencesCleaner : IFieldVisitor { - internal sealed class ReferencesCleaner : IFieldVisitor - { - private static readonly ReferencesCleaner Instance = new ReferencesCleaner(); + private static readonly ReferencesCleaner Instance = new ReferencesCleaner(); - public record struct Args(JsonValue Value, ISet ValidIds); + public record struct Args(JsonValue Value, ISet ValidIds); - private ReferencesCleaner() - { - } + private ReferencesCleaner() + { + } - public static JsonValue Cleanup(IField field, JsonValue value, HashSet validIds) - { - var args = new Args(value, validIds); + public static JsonValue Cleanup(IField field, JsonValue value, HashSet validIds) + { + var args = new Args(value, validIds); - return field.Accept(Instance, args); - } + return field.Accept(Instance, args); + } - public JsonValue Visit(IField field, Args args) - { - return CleanIds(args); - } + public JsonValue Visit(IField field, Args args) + { + return CleanIds(args); + } - public JsonValue Visit(IField field, Args args) - { - return CleanIds(args); - } + public JsonValue Visit(IField field, Args args) + { + return CleanIds(args); + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IField field, Args args) + { + return args.Value; + } - public JsonValue Visit(IArrayField field, Args args) - { - return args.Value; - } + public JsonValue Visit(IArrayField field, Args args) + { + return args.Value; + } - private static JsonValue CleanIds(Args args) + private static JsonValue CleanIds(Args args) + { + if (args.Value.Value is JsonArray array) { - if (args.Value.Value is JsonArray array) - { - var result = args.Value.AsArray; + var result = args.Value.AsArray; - for (var i = 0; i < result.Count; i++) + for (var i = 0; i < result.Count; i++) + { + if (!IsValidReference(result[i], args)) { - if (!IsValidReference(result[i], args)) + if (ReferenceEquals(result, array)) { - if (ReferenceEquals(result, array)) - { - result = array; - } - - result.RemoveAt(i); - i--; + result = array; } - } - return result; + result.RemoveAt(i); + i--; + } } - return args.Value; + return result; } - private static bool IsValidReference(JsonValue item, Args args) - { - return item.Value is string s && args.ValidIds.Contains(DomainId.Create(s)); - } + return args.Value; + } + + private static bool IsValidReference(JsonValue item, Args args) + { + return item.Value is string s && args.ValidIds.Contains(DomainId.Create(s)); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs index 9f3740ce82..4d67475892 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs @@ -12,141 +12,140 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds; + +internal static class ReferencesExtractor { - internal static class ReferencesExtractor - { - public record struct Args(JsonValue Value, ISet Result, int Take, ResolvedComponents Components); + public record struct Args(JsonValue Value, ISet Result, int Take, ResolvedComponents Components); - public static void Extract(IField field, JsonValue value, HashSet result, int take, ResolvedComponents components) - { - var args = new Args(value, result, take, components); + public static void Extract(IField field, JsonValue value, HashSet result, int take, ResolvedComponents components) + { + var args = new Args(value, result, take, components); - ExtractCore(field, args); - } + ExtractCore(field, args); + } - public static void Extract(IEnumerable schema, ContentData data, HashSet result, int take, ResolvedComponents components) + public static void Extract(IEnumerable schema, ContentData data, HashSet result, int take, ResolvedComponents components) + { + foreach (var field in schema) { - foreach (var field in schema) - { - Extract(field, data, result, take, components); - } + Extract(field, data, result, take, components); } + } - public static void Extract(IField field, ContentData data, HashSet result, int take, ResolvedComponents components) + public static void Extract(IField field, ContentData data, HashSet result, int take, ResolvedComponents components) + { + if (CanHaveReferences(field.RawProperties) && data.TryGetValue(field.Name, out var fieldData) && fieldData != null) { - if (CanHaveReferences(field.RawProperties) && data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + foreach (var (_, value) in fieldData) { - foreach (var (_, value) in fieldData) - { - Extract(field, value, result, take, components); - } + Extract(field, value, result, take, components); } } + } - private static void ExtractCore(IField field, Args args) + private static void ExtractCore(IField field, Args args) + { + switch (field) { - switch (field) - { - case IField: - AddIds(ref args); - break; - case IField: - AddIds(ref args); - break; - case IField: - ExtractFromComponent(args.Value, args); - break; - case IField: - ExtractFromComponents(args); - break; - case IArrayField arrayField: - ExtractFromArray(arrayField, args); - break; - } + case IField: + AddIds(ref args); + break; + case IField: + AddIds(ref args); + break; + case IField: + ExtractFromComponent(args.Value, args); + break; + case IField: + ExtractFromComponents(args); + break; + case IArrayField arrayField: + ExtractFromArray(arrayField, args); + break; } + } - private static void ExtractFromArray(IArrayField field, Args args) + private static void ExtractFromArray(IArrayField field, Args args) + { + if (args.Value.Value is JsonArray a) { - if (args.Value.Value is JsonArray a) + foreach (var value in a) { - foreach (var value in a) - { - ExtractFromItem(field, value, args); - } + ExtractFromItem(field, value, args); } } + } - private static void ExtractFromComponents(Args args) + private static void ExtractFromComponents(Args args) + { + if (args.Value.Value is JsonArray a) { - if (args.Value.Value is JsonArray a) + foreach (var value in a) { - foreach (var value in a) - { - ExtractFromComponent(value, args); - } + ExtractFromComponent(value, args); } } + } - private static void ExtractFromItem(IArrayField field, JsonValue value, Args args) + private static void ExtractFromItem(IArrayField field, JsonValue value, Args args) + { + if (value.Value is JsonObject o) { - if (value.Value is JsonObject o) + foreach (var nestedField in field.Fields) { - foreach (var nestedField in field.Fields) + if (CanHaveReferences(nestedField.RawProperties) && o.TryGetValue(nestedField.Name, out var nested)) { - if (CanHaveReferences(nestedField.RawProperties) && o.TryGetValue(nestedField.Name, out var nested)) - { - ExtractCore(nestedField, args with { Value = nested }); - } + ExtractCore(nestedField, args with { Value = nested }); } } } + } - private static void ExtractFromComponent(JsonValue value, Args args) + private static void ExtractFromComponent(JsonValue value, Args args) + { + if (value.Value is JsonObject o) { - if (value.Value is JsonObject o) + if (o.TryGetValue(Component.Discriminator, out var found) && found.Value is string s) { - if (o.TryGetValue(Component.Discriminator, out var found) && found.Value is string s) - { - var id = DomainId.Create(s); + var id = DomainId.Create(s); - if (args.Components.TryGetValue(id, out var schema)) + if (args.Components.TryGetValue(id, out var schema)) + { + foreach (var componentField in schema.Fields) { - foreach (var componentField in schema.Fields) + if (CanHaveReferences(componentField.RawProperties) && o.TryGetValue(componentField.Name, out var nested)) { - if (CanHaveReferences(componentField.RawProperties) && o.TryGetValue(componentField.Name, out var nested)) - { - ExtractCore(componentField, args with { Value = nested }); - } + ExtractCore(componentField, args with { Value = nested }); } } } } } + } - private static bool CanHaveReferences(FieldProperties properties) - { - return properties is ArrayFieldProperties or ReferencesFieldProperties or AssetsFieldProperties or ComponentFieldProperties or ComponentsFieldProperties; - } + private static bool CanHaveReferences(FieldProperties properties) + { + return properties is ArrayFieldProperties or ReferencesFieldProperties or AssetsFieldProperties or ComponentFieldProperties or ComponentsFieldProperties; + } - private static void AddIds(ref Args args) - { - var added = 0; + private static void AddIds(ref Args args) + { + var added = 0; - if (args.Value.Value is JsonArray a) + if (args.Value.Value is JsonArray a) + { + foreach (var id in a) { - foreach (var id in a) + if (id.Value is string s) { - if (id.Value is string s) - { - args.Result.Add(DomainId.Create(s)); + args.Result.Add(DomainId.Create(s)); - added++; + added++; - if (added >= args.Take) - { - break; - } + if (added >= args.Take) + { + break; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/StringReferenceExtractor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/StringReferenceExtractor.cs index cc1ec4482f..7449db7ae1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/StringReferenceExtractor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/StringReferenceExtractor.cs @@ -8,98 +8,97 @@ using System.Text.RegularExpressions; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds; + +public sealed class StringReferenceExtractor { - public sealed class StringReferenceExtractor + private readonly List contentsPatterns = new List(); + private readonly List assetsPatterns = new List(); + + public StringReferenceExtractor(IUrlGenerator urlGenerator) { - private readonly List contentsPatterns = new List(); - private readonly List assetsPatterns = new List(); + AddAssetPattern(@"assets?:(?[a-z0-9\-_9]+)"); + AddAssetUrlPatterns(urlGenerator.AssetContentBase()); + AddAssetUrlPatterns(urlGenerator.AssetContentCDNBase()); - public StringReferenceExtractor(IUrlGenerator urlGenerator) - { - AddAssetPattern(@"assets?:(?[a-z0-9\-_9]+)"); - AddAssetUrlPatterns(urlGenerator.AssetContentBase()); - AddAssetUrlPatterns(urlGenerator.AssetContentCDNBase()); + AddContentPattern(@"contents?:(?[a-z0-9\-_9]+)"); + AddContentUrlPatterns(urlGenerator.ContentBase()); + AddContentUrlPatterns(urlGenerator.ContentCDNBase()); + } - AddContentPattern(@"contents?:(?[a-z0-9\-_9]+)"); - AddContentUrlPatterns(urlGenerator.ContentBase()); - AddContentUrlPatterns(urlGenerator.ContentCDNBase()); + private void AddContentUrlPatterns(string baseUrl) + { + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return; } - private void AddContentUrlPatterns(string baseUrl) + if (!baseUrl.EndsWith('/')) { - if (string.IsNullOrWhiteSpace(baseUrl)) - { - return; - } + baseUrl += "/"; + } - if (!baseUrl.EndsWith('/')) - { - baseUrl += "/"; - } + baseUrl = Regex.Escape(baseUrl); - baseUrl = Regex.Escape(baseUrl); + AddContentPattern(baseUrl + @"([^\/]+)\/([^\/]+)\/(?[a-z0-9\-_9]+)"); + } - AddContentPattern(baseUrl + @"([^\/]+)\/([^\/]+)\/(?[a-z0-9\-_9]+)"); + private void AddAssetUrlPatterns(string baseUrl) + { + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return; } - private void AddAssetUrlPatterns(string baseUrl) + if (!baseUrl.EndsWith('/')) { - if (string.IsNullOrWhiteSpace(baseUrl)) - { - return; - } + baseUrl += "/"; + } - if (!baseUrl.EndsWith('/')) - { - baseUrl += "/"; - } + baseUrl = Regex.Escape(baseUrl); - baseUrl = Regex.Escape(baseUrl); + AddAssetPattern(baseUrl + @"(?[a-z0-9\-_9]+)"); + AddAssetPattern(baseUrl + @"([^\/]+)\/(?[a-z0-9\-_9]+)"); + } - AddAssetPattern(baseUrl + @"(?[a-z0-9\-_9]+)"); - AddAssetPattern(baseUrl + @"([^\/]+)\/(?[a-z0-9\-_9]+)"); - } + private void AddAssetPattern(string pattern) + { + assetsPatterns.Add(new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)); + } - private void AddAssetPattern(string pattern) - { - assetsPatterns.Add(new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)); - } + private void AddContentPattern(string pattern) + { + contentsPatterns.Add(new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)); + } - private void AddContentPattern(string pattern) + public IEnumerable GetEmbeddedContentIds(string text) + { + if (string.IsNullOrWhiteSpace(text)) { - contentsPatterns.Add(new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)); + yield break; } - public IEnumerable GetEmbeddedContentIds(string text) + foreach (var pattern in contentsPatterns) { - if (string.IsNullOrWhiteSpace(text)) + foreach (Match match in pattern.Matches(text)) { - yield break; - } - - foreach (var pattern in contentsPatterns) - { - foreach (Match match in pattern.Matches(text)) - { - yield return DomainId.Create(match.Groups["Id"].Value); - } + yield return DomainId.Create(match.Groups["Id"].Value); } } + } - public IEnumerable GetEmbeddedAssetIds(string text) + public IEnumerable GetEmbeddedAssetIds(string text) + { + if (string.IsNullOrWhiteSpace(text)) { - if (string.IsNullOrWhiteSpace(text)) - { - yield break; - } + yield break; + } - foreach (var pattern in assetsPatterns) + foreach (var pattern in assetsPatterns) + { + foreach (Match match in pattern.Matches(text)) { - foreach (Match match in pattern.Matches(text)) - { - yield return DomainId.Create(match.Groups["Id"].Value); - } + yield return DomainId.Create(match.Groups["Id"].Value); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs index a40f4b1193..0911ee332e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs @@ -10,25 +10,24 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds; + +public sealed class ValueReferencesConverter : IContentValueConverter { - public sealed class ValueReferencesConverter : IContentValueConverter + private readonly HashSet? validIds; + + public ValueReferencesConverter(HashSet? validIds = null) { - private readonly HashSet? validIds; + this.validIds = validIds; + } - public ValueReferencesConverter(HashSet? validIds = null) + public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) + { + if (validIds == null || source == default) { - this.validIds = validIds; + return (false, source); } - public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) - { - if (validIds == null || source == default) - { - return (false, source); - } - - return (false, ReferencesCleaner.Cleanup(field, source, validIds)); - } + return (false, ReferencesCleaner.Cleanup(field, source, validIds)); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/AssetQueryModel.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/AssetQueryModel.cs index 30168643f8..046813cc72 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/AssetQueryModel.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/AssetQueryModel.cs @@ -8,94 +8,93 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Core.GenerateFilters +namespace Squidex.Domain.Apps.Core.GenerateFilters; + +public static class AssetQueryModel { - public static class AssetQueryModel + public static QueryModel Build() { - public static QueryModel Build() + var fields = new List { - var fields = new List - { - new FilterField(FilterSchema.String, "id") - { - Description = FieldDescriptions.EntityId - }, - new FilterField(FilterSchema.Boolean, "isDeleted") - { - Description = FieldDescriptions.EntityIsDeleted - }, - new FilterField(FilterSchema.DateTime, "created") - { - Description = FieldDescriptions.EntityCreated - }, - new FilterField(SharedSchemas.User, "createdBy") - { - Description = FieldDescriptions.EntityCreatedBy - }, - new FilterField(FilterSchema.DateTime, "lastModified") - { - Description = FieldDescriptions.EntityLastModified - }, - new FilterField(SharedSchemas.User, "lastModifiedBy") - { - Description = FieldDescriptions.EntityLastModifiedBy - }, - new FilterField(FilterSchema.String, "status") - { - Description = FieldDescriptions.ContentStatus - }, - new FilterField(FilterSchema.String, "version") - { - Description = FieldDescriptions.EntityVersion - }, - new FilterField(FilterSchema.String, "fileHash") - { - Description = FieldDescriptions.AssetFileHash - }, - new FilterField(FilterSchema.String, "fileName") - { - Description = FieldDescriptions.AssetFileName - }, - new FilterField(FilterSchema.Number, "fileSize") - { - Description = FieldDescriptions.AssetFileSize - }, - new FilterField(FilterSchema.Number, "fileVersion") - { - Description = FieldDescriptions.AssetFileVersion - }, - new FilterField(FilterSchema.Boolean, "isProtected") - { - Description = FieldDescriptions.AssetIsProtected - }, - new FilterField(FilterSchema.Any, "metadata") - { - Description = FieldDescriptions.AssetMetadata - }, - new FilterField(FilterSchema.String, "mimeType") - { - Description = FieldDescriptions.AssetMimeType - }, - new FilterField(FilterSchema.String, "slug") - { - Description = FieldDescriptions.AssetSlug - }, - new FilterField(FilterSchema.StringArray, "tags") - { - Description = FieldDescriptions.AssetTags - }, - new FilterField(FilterSchema.String, "type") - { - Description = FieldDescriptions.AssetType - } - }; - - var schema = new FilterSchema(FilterSchemaType.Object) + new FilterField(FilterSchema.String, "id") + { + Description = FieldDescriptions.EntityId + }, + new FilterField(FilterSchema.Boolean, "isDeleted") + { + Description = FieldDescriptions.EntityIsDeleted + }, + new FilterField(FilterSchema.DateTime, "created") + { + Description = FieldDescriptions.EntityCreated + }, + new FilterField(SharedSchemas.User, "createdBy") + { + Description = FieldDescriptions.EntityCreatedBy + }, + new FilterField(FilterSchema.DateTime, "lastModified") + { + Description = FieldDescriptions.EntityLastModified + }, + new FilterField(SharedSchemas.User, "lastModifiedBy") + { + Description = FieldDescriptions.EntityLastModifiedBy + }, + new FilterField(FilterSchema.String, "status") + { + Description = FieldDescriptions.ContentStatus + }, + new FilterField(FilterSchema.String, "version") + { + Description = FieldDescriptions.EntityVersion + }, + new FilterField(FilterSchema.String, "fileHash") + { + Description = FieldDescriptions.AssetFileHash + }, + new FilterField(FilterSchema.String, "fileName") { - Fields = fields.ToReadonlyList() - }; + Description = FieldDescriptions.AssetFileName + }, + new FilterField(FilterSchema.Number, "fileSize") + { + Description = FieldDescriptions.AssetFileSize + }, + new FilterField(FilterSchema.Number, "fileVersion") + { + Description = FieldDescriptions.AssetFileVersion + }, + new FilterField(FilterSchema.Boolean, "isProtected") + { + Description = FieldDescriptions.AssetIsProtected + }, + new FilterField(FilterSchema.Any, "metadata") + { + Description = FieldDescriptions.AssetMetadata + }, + new FilterField(FilterSchema.String, "mimeType") + { + Description = FieldDescriptions.AssetMimeType + }, + new FilterField(FilterSchema.String, "slug") + { + Description = FieldDescriptions.AssetSlug + }, + new FilterField(FilterSchema.StringArray, "tags") + { + Description = FieldDescriptions.AssetTags + }, + new FilterField(FilterSchema.String, "type") + { + Description = FieldDescriptions.AssetType + } + }; + + var schema = new FilterSchema(FilterSchemaType.Object) + { + Fields = fields.ToReadonlyList() + }; - return new QueryModel { Schema = schema }; - } + return new QueryModel { Schema = schema }; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/ContentQueryModel.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/ContentQueryModel.cs index e05831a2e0..154d6698a2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/ContentQueryModel.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/ContentQueryModel.cs @@ -10,90 +10,89 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Core.GenerateFilters +namespace Squidex.Domain.Apps.Core.GenerateFilters; + +public static class ContentQueryModel { - public static class ContentQueryModel + public static QueryModel Build(Schema? schema, PartitionResolver partitionResolver, ResolvedComponents components) { - public static QueryModel Build(Schema? schema, PartitionResolver partitionResolver, ResolvedComponents components) + var fields = new List { - var fields = new List + new FilterField(FilterSchema.String, "id") { - new FilterField(FilterSchema.String, "id") - { - Description = FieldDescriptions.EntityId - }, - new FilterField(FilterSchema.Boolean, "isDeleted") - { - Description = FieldDescriptions.EntityIsDeleted - }, - new FilterField(FilterSchema.DateTime, "created") - { - Description = FieldDescriptions.EntityCreated - }, - new FilterField(SharedSchemas.User, "createdBy") - { - Description = FieldDescriptions.EntityCreatedBy - }, - new FilterField(FilterSchema.DateTime, "lastModified") - { - Description = FieldDescriptions.EntityLastModified - }, - new FilterField(SharedSchemas.User, "lastModifiedBy") - { - Description = FieldDescriptions.EntityLastModifiedBy - }, - new FilterField(FilterSchema.Number, "version") - { - Description = FieldDescriptions.EntityVersion - }, - new FilterField(SharedSchemas.Status, "status") - { - Description = FieldDescriptions.ContentStatus - }, - new FilterField(SharedSchemas.Status, "newStatus", IsNullable: true) - { - Description = FieldDescriptions.ContentNewStatus - } - }; - - var translationStatusSchema = BuildTranslationStatus(partitionResolver); + Description = FieldDescriptions.EntityId + }, + new FilterField(FilterSchema.Boolean, "isDeleted") + { + Description = FieldDescriptions.EntityIsDeleted + }, + new FilterField(FilterSchema.DateTime, "created") + { + Description = FieldDescriptions.EntityCreated + }, + new FilterField(SharedSchemas.User, "createdBy") + { + Description = FieldDescriptions.EntityCreatedBy + }, + new FilterField(FilterSchema.DateTime, "lastModified") + { + Description = FieldDescriptions.EntityLastModified + }, + new FilterField(SharedSchemas.User, "lastModifiedBy") + { + Description = FieldDescriptions.EntityLastModifiedBy + }, + new FilterField(FilterSchema.Number, "version") + { + Description = FieldDescriptions.EntityVersion + }, + new FilterField(SharedSchemas.Status, "status") + { + Description = FieldDescriptions.ContentStatus + }, + new FilterField(SharedSchemas.Status, "newStatus", IsNullable: true) + { + Description = FieldDescriptions.ContentNewStatus + } + }; - fields.Add(new FilterField(translationStatusSchema, "translationStatus")); + var translationStatusSchema = BuildTranslationStatus(partitionResolver); - if (schema != null) - { - var dataSchema = schema.BuildDataSchema(partitionResolver, components); + fields.Add(new FilterField(translationStatusSchema, "translationStatus")); - fields.Add(new FilterField(dataSchema, "data") - { - Description = FieldDescriptions.ContentData - }); - } + if (schema != null) + { + var dataSchema = schema.BuildDataSchema(partitionResolver, components); - var filterSchema = new FilterSchema(FilterSchemaType.Object) + fields.Add(new FilterField(dataSchema, "data") { - Fields = fields.ToReadonlyList() - }; - - return new QueryModel { Schema = filterSchema }; + Description = FieldDescriptions.ContentData + }); } - private static FilterSchema BuildTranslationStatus(PartitionResolver partitionResolver) + var filterSchema = new FilterSchema(FilterSchemaType.Object) { - var fields = new List(); + Fields = fields.ToReadonlyList() + }; - foreach (var key in partitionResolver(Partitioning.Language).AllKeys) - { - fields.Add(new FilterField(FilterSchema.Number, key) - { - Description = string.Format(CultureInfo.InvariantCulture, FieldDescriptions.TranslationStatusLanguage, key) - }); - } + return new QueryModel { Schema = filterSchema }; + } - return new FilterSchema(FilterSchemaType.Object) + private static FilterSchema BuildTranslationStatus(PartitionResolver partitionResolver) + { + var fields = new List(); + + foreach (var key in partitionResolver(Partitioning.Language).AllKeys) + { + fields.Add(new FilterField(FilterSchema.Number, key) { - Fields = fields.ToReadonlyList() - }; + Description = string.Format(CultureInfo.InvariantCulture, FieldDescriptions.TranslationStatusLanguage, key) + }); } + + return new FilterSchema(FilterSchemaType.Object) + { + Fields = fields.ToReadonlyList() + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterExtensions.cs index 6cdb35c59c..b2f4e0a80c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterExtensions.cs @@ -11,76 +11,75 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Core.GenerateFilters +namespace Squidex.Domain.Apps.Core.GenerateFilters; + +public static class FilterExtensions { - public static class FilterExtensions + public static FilterSchema BuildDataSchema(this Schema schema, PartitionResolver partitionResolver, + ResolvedComponents components) { - public static FilterSchema BuildDataSchema(this Schema schema, PartitionResolver partitionResolver, - ResolvedComponents components) - { - Guard.NotNull(partitionResolver); - Guard.NotNull(components); + Guard.NotNull(partitionResolver); + Guard.NotNull(components); - var fields = new List(); + var fields = new List(); - var schemaName = schema.DisplayName(); + var schemaName = schema.DisplayName(); - foreach (var field in schema.Fields.ForApi(true)) + foreach (var field in schema.Fields.ForApi(true)) + { + var fieldSchema = FilterVisitor.BuildProperty(field, components); + + if (fieldSchema == null) { - var fieldSchema = FilterVisitor.BuildProperty(field, components); + continue; + } - if (fieldSchema == null) - { - continue; - } + var partitioning = partitionResolver(field.Partitioning); + var partitionFields = new List(); - var partitioning = partitionResolver(field.Partitioning); - var partitionFields = new List(); + foreach (var partitionKey in partitioning.AllKeys) + { + var partitionDescription = FieldPartitionDescription(field, partitioning.GetName(partitionKey) ?? partitionKey); - foreach (var partitionKey in partitioning.AllKeys) - { - var partitionDescription = FieldPartitionDescription(field, partitioning.GetName(partitionKey) ?? partitionKey); - - var partitionField = new FilterField( - fieldSchema, - partitionKey, - partitionDescription, - true); - - partitionFields.Add(partitionField); - } - - var filterable = new FilterField( - new FilterSchema(FilterSchemaType.Object) - { - Fields = partitionFields.ToReadonlyList() - }, - field.Name, - FieldDescription(schemaName, field)); - - fields.Add(filterable); + var partitionField = new FilterField( + fieldSchema, + partitionKey, + partitionDescription, + true); + + partitionFields.Add(partitionField); } - var dataSchema = new FilterSchema(FilterSchemaType.Object) - { - Fields = fields.ToReadonlyList() - }; + var filterable = new FilterField( + new FilterSchema(FilterSchemaType.Object) + { + Fields = partitionFields.ToReadonlyList() + }, + field.Name, + FieldDescription(schemaName, field)); - return dataSchema; + fields.Add(filterable); } - private static string FieldPartitionDescription(RootField field, string partition) + var dataSchema = new FilterSchema(FilterSchemaType.Object) { - var name = field.DisplayName(); + Fields = fields.ToReadonlyList() + }; - return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentPartitionField, name, partition); - } + return dataSchema; + } - private static string FieldDescription(string schemaName, RootField field) - { - var name = field.DisplayName(); + private static string FieldPartitionDescription(RootField field, string partition) + { + var name = field.DisplayName(); - return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentField, name, schemaName); - } + return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentPartitionField, name, partition); + } + + private static string FieldDescription(string schemaName, RootField field) + { + var name = field.DisplayName(); + + return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentField, name, schemaName); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterVisitor.cs index 163a4480d4..f647869bfa 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterVisitor.cs @@ -14,204 +14,203 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.GenerateFilters +namespace Squidex.Domain.Apps.Core.GenerateFilters; + +internal sealed class FilterVisitor : IFieldVisitor { - internal sealed class FilterVisitor : IFieldVisitor + private const int MaxDepth = 3; + private static readonly FilterVisitor Instance = new FilterVisitor(); + + public record struct Args(ResolvedComponents Components, int Level = 0); + + private FilterVisitor() { - private const int MaxDepth = 3; - private static readonly FilterVisitor Instance = new FilterVisitor(); + } - public record struct Args(ResolvedComponents Components, int Level = 0); + public static FilterSchema? BuildProperty(IField field, ResolvedComponents components) + { + var args = new Args(components); - private FilterVisitor() - { - } + return field.Accept(Instance, args); + } - public static FilterSchema? BuildProperty(IField field, ResolvedComponents components) + public FilterSchema? Visit(IArrayField field, Args args) + { + if (args.Level >= MaxDepth) { - var args = new Args(components); - - return field.Accept(Instance, args); + return null; } - public FilterSchema? Visit(IArrayField field, Args args) - { - if (args.Level >= MaxDepth) - { - return null; - } + var fields = new List(); - var fields = new List(); + var nestedArgs = args with { Level = args.Level + 1 }; - var nestedArgs = args with { Level = args.Level + 1 }; + foreach (var nestedField in field.Fields.ForApi(true)) + { + var nestedSchema = nestedField.Accept(this, nestedArgs); - foreach (var nestedField in field.Fields.ForApi(true)) + if (nestedSchema != null) { - var nestedSchema = nestedField.Accept(this, nestedArgs); + var filterableField = new FilterField( + nestedSchema, + nestedField.Name, + ArrayFieldDescription(nestedField), + true); - if (nestedSchema != null) - { - var filterableField = new FilterField( - nestedSchema, - nestedField.Name, - ArrayFieldDescription(nestedField), - true); - - fields.Add(filterableField); - } + fields.Add(filterableField); } - - return new FilterSchema(FilterSchemaType.ObjectArray) - { - Fields = fields.ToReadonlyList() - }; } - public FilterSchema? Visit(IField field, Args args) + return new FilterSchema(FilterSchemaType.ObjectArray) { - return FilterSchema.StringArray; - } + Fields = fields.ToReadonlyList() + }; + } - public FilterSchema? Visit(IField field, Args args) - { - return FilterSchema.Boolean; - } + public FilterSchema? Visit(IField field, Args args) + { + return FilterSchema.StringArray; + } - public FilterSchema? Visit(IField field, Args args) - { - return FilterSchema.GeoObject; - } + public FilterSchema? Visit(IField field, Args args) + { + return FilterSchema.Boolean; + } - public FilterSchema? Visit(IField field, Args args) - { - return FilterSchema.Any; - } + public FilterSchema? Visit(IField field, Args args) + { + return FilterSchema.GeoObject; + } - public FilterSchema? Visit(IField field, Args args) - { - return FilterSchema.Number; - } + public FilterSchema? Visit(IField field, Args args) + { + return FilterSchema.Any; + } - public FilterSchema? Visit(IField field, Args args) + public FilterSchema? Visit(IField field, Args args) + { + return FilterSchema.Number; + } + + public FilterSchema? Visit(IField field, Args args) + { + if (field.Properties.AllowedValues?.Count > 0) { - if (field.Properties.AllowedValues?.Count > 0) + return new FilterSchema(FilterSchemaType.String) { - return new FilterSchema(FilterSchemaType.String) + Extra = new { - Extra = new - { - options = field.Properties.AllowedValues - } - }; - } - - return FilterSchema.String; + options = field.Properties.AllowedValues + } + }; } - public FilterSchema? Visit(IField field, Args args) - { - return FilterSchema.StringArray; - } + return FilterSchema.String; + } + + public FilterSchema? Visit(IField field, Args args) + { + return FilterSchema.StringArray; + } + + public FilterSchema? Visit(IField field, Args args) + { + return null; + } - public FilterSchema? Visit(IField field, Args args) + public FilterSchema? Visit(IField field, Args args) + { + if (args.Level >= MaxDepth) { return null; } - public FilterSchema? Visit(IField field, Args args) + return new FilterSchema(FilterSchemaType.Object) { - if (args.Level >= MaxDepth) - { - return null; - } + Fields = BuildComponent(field.Properties.SchemaIds, args) + }; + } - return new FilterSchema(FilterSchemaType.Object) - { - Fields = BuildComponent(field.Properties.SchemaIds, args) - }; + public FilterSchema? Visit(IField field, Args args) + { + if (args.Level >= MaxDepth) + { + return null; } - public FilterSchema? Visit(IField field, Args args) + return new FilterSchema(FilterSchemaType.Object) { - if (args.Level >= MaxDepth) - { - return null; - } + Fields = BuildComponent(field.Properties.SchemaIds, args) + }; + } - return new FilterSchema(FilterSchemaType.Object) - { - Fields = BuildComponent(field.Properties.SchemaIds, args) - }; + public FilterSchema? Visit(IField field, Args args) + { + if (field.Properties.Editor == DateTimeFieldEditor.Date) + { + return SharedSchemas.Date; } - public FilterSchema? Visit(IField field, Args args) + return SharedSchemas.DateTime; + } + + public FilterSchema? Visit(IField field, Args args) + { + return new FilterSchema(FilterSchemaType.StringArray) { - if (field.Properties.Editor == DateTimeFieldEditor.Date) + Extra = new { - return SharedSchemas.Date; + schemaIds = field.Properties.SchemaIds } + }; + } - return SharedSchemas.DateTime; - } + private ReadonlyList BuildComponent(ReadonlyList? schemaIds, Args args) + { + var fields = new List(); - public FilterSchema? Visit(IField field, Args args) - { - return new FilterSchema(FilterSchemaType.StringArray) - { - Extra = new - { - schemaIds = field.Properties.SchemaIds - } - }; - } + var nestedArgs = args with { Level = args.Level + 1 }; - private ReadonlyList BuildComponent(ReadonlyList? schemaIds, Args args) + foreach (var (_, schema) in args.Components.Resolve(schemaIds)) { - var fields = new List(); - - var nestedArgs = args with { Level = args.Level + 1 }; + var componentName = schema.DisplayName(); - foreach (var (_, schema) in args.Components.Resolve(schemaIds)) + foreach (var field in schema.Fields.ForApi(true)) { - var componentName = schema.DisplayName(); + var fieldSchema = field.Accept(this, nestedArgs); - foreach (var field in schema.Fields.ForApi(true)) + if (fieldSchema != null) { - var fieldSchema = field.Accept(this, nestedArgs); - - if (fieldSchema != null) - { - var filterableField = new FilterField( - fieldSchema, - field.Name, - ComponentFieldDescription(componentName, field), - true); - - fields.Add(filterableField); - } - } + var filterableField = new FilterField( + fieldSchema, + field.Name, + ComponentFieldDescription(componentName, field), + true); - fields.Add(new FilterField(FilterSchema.String, Component.Discriminator) - { - Description = FieldDescriptions.ComponentSchemaId - }); + fields.Add(filterableField); + } } - return fields.ToReadonlyList(); + fields.Add(new FilterField(FilterSchema.String, Component.Discriminator) + { + Description = FieldDescriptions.ComponentSchemaId + }); } - private static string ArrayFieldDescription(IField field) - { - var name = field.DisplayName(); + return fields.ToReadonlyList(); + } - return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentArrayField, name); - } + private static string ArrayFieldDescription(IField field) + { + var name = field.DisplayName(); - private static string ComponentFieldDescription(string componentName, RootField field) - { - var name = field.DisplayName(); + return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ContentArrayField, name); + } - return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ComponentField, name, componentName); - } + private static string ComponentFieldDescription(string componentName, RootField field) + { + var name = field.DisplayName(); + + return string.Format(CultureInfo.InvariantCulture, FieldDescriptions.ComponentField, name, componentName); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/SharedSchemas.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/SharedSchemas.cs index 8ffecc92bf..5994aade27 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/SharedSchemas.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/SharedSchemas.cs @@ -7,40 +7,39 @@ using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Core.GenerateFilters +namespace Squidex.Domain.Apps.Core.GenerateFilters; + +internal static class SharedSchemas { - internal static class SharedSchemas + public static readonly FilterSchema Date = new FilterSchema(FilterSchemaType.DateTime) { - public static readonly FilterSchema Date = new FilterSchema(FilterSchemaType.DateTime) + Extra = new { - Extra = new - { - editor = "Date" - } - }; + editor = "Date" + } + }; - public static readonly FilterSchema DateTime = new FilterSchema(FilterSchemaType.DateTime) + public static readonly FilterSchema DateTime = new FilterSchema(FilterSchemaType.DateTime) + { + Extra = new { - Extra = new - { - editor = "DateTime" - } - }; + editor = "DateTime" + } + }; - public static readonly FilterSchema Status = new FilterSchema(FilterSchemaType.String) + public static readonly FilterSchema Status = new FilterSchema(FilterSchemaType.String) + { + Extra = new { - Extra = new - { - editor = "Status" - } - }; + editor = "Status" + } + }; - public static readonly FilterSchema User = new FilterSchema(FilterSchemaType.String) + public static readonly FilterSchema User = new FilterSchema(FilterSchemaType.String) + { + Extra = new { - Extra = new - { - editor = "User" - } - }; - } + editor = "User" + } + }; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentJsonSchema.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentJsonSchema.cs index 9e41129c22..e212aee7dd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentJsonSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentJsonSchema.cs @@ -7,48 +7,47 @@ using NJsonSchema; -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema; + +public static class ContentJsonSchema { - public static class ContentJsonSchema + public static JsonSchema Build(JsonSchema? dataSchema, bool extended = false, bool withDeleted = false) { - public static JsonSchema Build(JsonSchema? dataSchema, bool extended = false, bool withDeleted = false) + var jsonSchema = new JsonSchema { - var jsonSchema = new JsonSchema - { - AllowAdditionalProperties = false, - Properties = - { - ["id"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityId, true), - ["created"] = JsonTypeBuilder.DateTimeProperty(FieldDescriptions.EntityCreated, true), - ["createdBy"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityCreatedBy, true), - ["lastModified"] = JsonTypeBuilder.DateTimeProperty(FieldDescriptions.EntityLastModified, true), - ["lastModifiedBy"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityLastModifiedBy, true), - ["newStatus"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentNewStatus), - ["status"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentStatus, true) - }, - Type = JsonObjectType.Object - }; - - if (withDeleted) + AllowAdditionalProperties = false, + Properties = { - jsonSchema.Properties["isDeleted"] = JsonTypeBuilder.BooleanProperty(FieldDescriptions.EntityIsDeleted, false); - } + ["id"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityId, true), + ["created"] = JsonTypeBuilder.DateTimeProperty(FieldDescriptions.EntityCreated, true), + ["createdBy"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityCreatedBy, true), + ["lastModified"] = JsonTypeBuilder.DateTimeProperty(FieldDescriptions.EntityLastModified, true), + ["lastModifiedBy"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityLastModifiedBy, true), + ["newStatus"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentNewStatus), + ["status"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentStatus, true) + }, + Type = JsonObjectType.Object + }; - if (extended) - { - jsonSchema.Properties["newStatusColor"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentNewStatusColor, false); - jsonSchema.Properties["schemaId"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentSchemaId, true); - jsonSchema.Properties["schemaName"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentSchemaName, true); - jsonSchema.Properties["schemaDisplayName"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentSchemaDisplayName, true); - jsonSchema.Properties["statusColor"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentStatusColor, true); - } + if (withDeleted) + { + jsonSchema.Properties["isDeleted"] = JsonTypeBuilder.BooleanProperty(FieldDescriptions.EntityIsDeleted, false); + } - if (dataSchema != null) - { - jsonSchema.Properties["data"] = JsonTypeBuilder.ReferenceProperty(dataSchema, FieldDescriptions.ContentData, true); - } + if (extended) + { + jsonSchema.Properties["newStatusColor"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentNewStatusColor, false); + jsonSchema.Properties["schemaId"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentSchemaId, true); + jsonSchema.Properties["schemaName"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentSchemaName, true); + jsonSchema.Properties["schemaDisplayName"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentSchemaDisplayName, true); + jsonSchema.Properties["statusColor"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentStatusColor, true); + } - return jsonSchema; + if (dataSchema != null) + { + jsonSchema.Properties["data"] = JsonTypeBuilder.ReferenceProperty(dataSchema, FieldDescriptions.ContentData, true); } + + return jsonSchema; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs index f867f6446a..1f02f7e8fb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs @@ -12,164 +12,163 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema -{ - public delegate (JsonSchema Reference, JsonSchema? Actual) JsonTypeFactory(string name); +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema; + +public delegate (JsonSchema Reference, JsonSchema? Actual) JsonTypeFactory(string name); - public static class JsonSchemaExtensions +public static class JsonSchemaExtensions +{ + private static readonly JsonTypeFactory DefaultFactory = _ => { - private static readonly JsonTypeFactory DefaultFactory = _ => - { - var schema = JsonTypeBuilder.Object(); + var schema = JsonTypeBuilder.Object(); - return (schema, schema); - }; + return (schema, schema); + }; - public static JsonSchema BuildJsonSchemaFlat(this Schema schema, PartitionResolver partitionResolver, - ResolvedComponents components, - JsonTypeFactory? factory = null, - bool withHidden = false, - bool withComponents = false) - { - Guard.NotNull(partitionResolver); - Guard.NotNull(components); + public static JsonSchema BuildJsonSchemaFlat(this Schema schema, PartitionResolver partitionResolver, + ResolvedComponents components, + JsonTypeFactory? factory = null, + bool withHidden = false, + bool withComponents = false) + { + Guard.NotNull(partitionResolver); + Guard.NotNull(components); - factory ??= DefaultFactory; + factory ??= DefaultFactory; - var jsonSchema = JsonTypeBuilder.Object(); + var jsonSchema = JsonTypeBuilder.Object(); - foreach (var field in schema.Fields.ForApi(withHidden)) + foreach (var field in schema.Fields.ForApi(withHidden)) + { + var property = + JsonTypeVisitor.BuildProperty( + field, components, schema, + factory, + withHidden, + withComponents); + + // Property is null for UI fields. + if (property != null) { - var property = - JsonTypeVisitor.BuildProperty( - field, components, schema, - factory, - withHidden, - withComponents); - - // Property is null for UI fields. - if (property != null) - { - property.SetRequired(false); - property.SetDescription(field); + property.SetRequired(false); + property.SetDescription(field); - jsonSchema.Properties.Add(field.Name, property); - } + jsonSchema.Properties.Add(field.Name, property); } - - return jsonSchema; } - public static JsonSchema BuildJsonSchemaDynamic(this Schema schema, PartitionResolver partitionResolver, - ResolvedComponents components, - JsonTypeFactory? factory = null, - bool withHidden = false, - bool withComponents = false) - { - Guard.NotNull(partitionResolver); - Guard.NotNull(components); + return jsonSchema; + } + + public static JsonSchema BuildJsonSchemaDynamic(this Schema schema, PartitionResolver partitionResolver, + ResolvedComponents components, + JsonTypeFactory? factory = null, + bool withHidden = false, + bool withComponents = false) + { + Guard.NotNull(partitionResolver); + Guard.NotNull(components); - factory ??= DefaultFactory; + factory ??= DefaultFactory; - var jsonSchema = JsonTypeBuilder.Object(); + var jsonSchema = JsonTypeBuilder.Object(); - foreach (var field in schema.Fields.ForApi(withHidden)) + foreach (var field in schema.Fields.ForApi(withHidden)) + { + var property = + JsonTypeVisitor.BuildProperty( + field, components, schema, + factory, + withHidden, + withComponents); + + // Property is null for UI fields. + if (property != null) { - var property = - JsonTypeVisitor.BuildProperty( - field, components, schema, - factory, - withHidden, - withComponents); - - // Property is null for UI fields. - if (property != null) - { - var propertyObj = JsonTypeBuilder.ObjectProperty(property); + var propertyObj = JsonTypeBuilder.ObjectProperty(property); - // Property is not required because not all languages might be required. - propertyObj.SetRequired(false); - propertyObj.SetDescription(field); + // Property is not required because not all languages might be required. + propertyObj.SetRequired(false); + propertyObj.SetDescription(field); - jsonSchema.Properties.Add(field.Name, propertyObj); - } + jsonSchema.Properties.Add(field.Name, propertyObj); } - - return jsonSchema; } - public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, - ResolvedComponents components, - JsonTypeFactory? factory = null, - bool withHidden = false, - bool withComponents = false) - { - Guard.NotNull(partitionResolver); - Guard.NotNull(components); + return jsonSchema; + } - factory ??= DefaultFactory; + public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, + ResolvedComponents components, + JsonTypeFactory? factory = null, + bool withHidden = false, + bool withComponents = false) + { + Guard.NotNull(partitionResolver); + Guard.NotNull(components); - var jsonSchema = JsonTypeBuilder.Object(); + factory ??= DefaultFactory; - foreach (var field in schema.Fields.ForApi(withHidden)) - { - var typeName = $"{schema.TypeName()}{field.Name.ToPascalCase()}PropertyDto"; + var jsonSchema = JsonTypeBuilder.Object(); - // Create a reference to give it a nice name in code generation. - var (reference, actual) = factory(typeName); + foreach (var field in schema.Fields.ForApi(withHidden)) + { + var typeName = $"{schema.TypeName()}{field.Name.ToPascalCase()}PropertyDto"; - if (actual != null) - { - var partitioning = partitionResolver(field.Partitioning); + // Create a reference to give it a nice name in code generation. + var (reference, actual) = factory(typeName); + + if (actual != null) + { + var partitioning = partitionResolver(field.Partitioning); - foreach (var partitionKey in partitioning.AllKeys) + foreach (var partitionKey in partitioning.AllKeys) + { + var property = + JsonTypeVisitor.BuildProperty( + field, components, schema, + factory, + withHidden, + withComponents); + + // Property is null for UI fields. + if (property != null) { - var property = - JsonTypeVisitor.BuildProperty( - field, components, schema, - factory, - withHidden, - withComponents); - - // Property is null for UI fields. - if (property != null) - { - var isOptional = partitioning.IsOptional(partitionKey); - - var name = partitioning.GetName(partitionKey); - - // Required if property is required and language/partitioning is not optional. - property.SetRequired(field.RawProperties.IsRequired && !isOptional); - property.SetDescription(name); - - actual.Properties.Add(partitionKey, property); - } - } - } + var isOptional = partitioning.IsOptional(partitionKey); + + var name = partitioning.GetName(partitionKey); - var propertyReference = - JsonTypeBuilder.ReferenceProperty(reference) - .SetDescription(field) - .SetRequired(field.RawProperties.IsRequired); + // Required if property is required and language/partitioning is not optional. + property.SetRequired(field.RawProperties.IsRequired && !isOptional); + property.SetDescription(name); - jsonSchema.Properties.Add(field.Name, propertyReference); + actual.Properties.Add(partitionKey, property); + } + } } - return jsonSchema; + var propertyReference = + JsonTypeBuilder.ReferenceProperty(reference) + .SetDescription(field) + .SetRequired(field.RawProperties.IsRequired); + + jsonSchema.Properties.Add(field.Name, propertyReference); } - public static JsonSchemaProperty SetDescription(this JsonSchemaProperty jsonProperty, IField field) - { - if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) - { - jsonProperty.Description = $"{field.Name} ({field.RawProperties.Hints})"; - } - else - { - jsonProperty.Description = field.Name; - } + return jsonSchema; + } - return jsonProperty; + public static JsonSchemaProperty SetDescription(this JsonSchemaProperty jsonProperty, IField field) + { + if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) + { + jsonProperty.Description = $"{field.Name} ({field.RawProperties.Hints})"; } + else + { + jsonProperty.Description = field.Name; + } + + return jsonProperty; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs index 5b3f8bfd1c..e55e6b258b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs @@ -7,105 +7,104 @@ using NJsonSchema; -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema; + +public static class JsonTypeBuilder { - public static class JsonTypeBuilder + public static JsonSchema Object() + { + const JsonObjectType type = JsonObjectType.Object; + + return new JsonSchema { Type = type, AllowAdditionalProperties = false }; + } + + public static JsonSchema String() + { + const JsonObjectType type = JsonObjectType.String; + + return new JsonSchema { Type = type }; + } + + public static JsonSchemaProperty BooleanProperty(string? description = null, bool isRequired = false) + { + const JsonObjectType type = JsonObjectType.Boolean; + + return new JsonSchemaProperty { Type = type } + .SetDescription(description) + .SetRequired(isRequired); + } + + public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false) + { + const JsonObjectType type = JsonObjectType.String; + + return new JsonSchemaProperty { Type = type, Format = JsonFormatStrings.DateTime } + .SetDescription(description) + .SetRequired(isRequired); + } + + public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false) + { + const JsonObjectType type = JsonObjectType.Number; + + return new JsonSchemaProperty { Type = type } + .SetDescription(description) + .SetRequired(isRequired); + } + + public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false, string? format = null) + { + const JsonObjectType type = JsonObjectType.String; + + return new JsonSchemaProperty { Type = type, Format = format } + .SetDescription(description) + .SetRequired(isRequired); + } + + public static JsonSchemaProperty ObjectProperty(JsonSchema? value = null, string? description = null, bool isRequired = false) + { + const JsonObjectType type = JsonObjectType.Object; + + return new JsonSchemaProperty { Type = type, AdditionalPropertiesSchema = value } + .SetDescription(description) + .SetRequired(isRequired); + } + + public static JsonSchemaProperty ArrayProperty(JsonSchema item, string? description = null, bool isRequired = false) + { + const JsonObjectType type = JsonObjectType.Array; + + return new JsonSchemaProperty { Type = type, Item = item } + .SetDescription(description) + .SetRequired(isRequired); + } + + public static JsonSchemaProperty ReferenceProperty(JsonSchema reference, string? description = null, bool isRequired = false) { - public static JsonSchema Object() - { - const JsonObjectType type = JsonObjectType.Object; - - return new JsonSchema { Type = type, AllowAdditionalProperties = false }; - } - - public static JsonSchema String() - { - const JsonObjectType type = JsonObjectType.String; - - return new JsonSchema { Type = type }; - } - - public static JsonSchemaProperty BooleanProperty(string? description = null, bool isRequired = false) - { - const JsonObjectType type = JsonObjectType.Boolean; - - return new JsonSchemaProperty { Type = type } - .SetDescription(description) - .SetRequired(isRequired); - } - - public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false) - { - const JsonObjectType type = JsonObjectType.String; - - return new JsonSchemaProperty { Type = type, Format = JsonFormatStrings.DateTime } - .SetDescription(description) - .SetRequired(isRequired); - } - - public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false) - { - const JsonObjectType type = JsonObjectType.Number; - - return new JsonSchemaProperty { Type = type } - .SetDescription(description) - .SetRequired(isRequired); - } - - public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false, string? format = null) - { - const JsonObjectType type = JsonObjectType.String; - - return new JsonSchemaProperty { Type = type, Format = format } - .SetDescription(description) - .SetRequired(isRequired); - } - - public static JsonSchemaProperty ObjectProperty(JsonSchema? value = null, string? description = null, bool isRequired = false) - { - const JsonObjectType type = JsonObjectType.Object; - - return new JsonSchemaProperty { Type = type, AdditionalPropertiesSchema = value } - .SetDescription(description) - .SetRequired(isRequired); - } - - public static JsonSchemaProperty ArrayProperty(JsonSchema item, string? description = null, bool isRequired = false) - { - const JsonObjectType type = JsonObjectType.Array; - - return new JsonSchemaProperty { Type = type, Item = item } - .SetDescription(description) - .SetRequired(isRequired); - } - - public static JsonSchemaProperty ReferenceProperty(JsonSchema reference, string? description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Reference = reference } - .SetDescription(description) - .SetRequired(isRequired); - } - - public static JsonSchemaProperty JsonProperty(string? description = null, bool isRequired = false) - { - return new JsonSchemaProperty() - .SetDescription(description) - .SetRequired(isRequired); - } - - public static T SetDescription(this T property, string? description = null) where T : JsonSchemaProperty - { - property.Description = description; - - return property; - } - - public static T SetRequired(this T property, bool isRequired) where T : JsonSchemaProperty - { - property.IsRequired = isRequired; - property.IsNullableRaw = !isRequired; - - return property; - } + return new JsonSchemaProperty { Reference = reference } + .SetDescription(description) + .SetRequired(isRequired); + } + + public static JsonSchemaProperty JsonProperty(string? description = null, bool isRequired = false) + { + return new JsonSchemaProperty() + .SetDescription(description) + .SetRequired(isRequired); + } + + public static T SetDescription(this T property, string? description = null) where T : JsonSchemaProperty + { + property.Description = description; + + return property; + } + + public static T SetRequired(this T property, bool isRequired) where T : JsonSchemaProperty + { + property.IsRequired = isRequired; + property.IsNullableRaw = !isRequired; + + return property; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs index 0b6d519ba9..6bdd995187 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -16,296 +16,295 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema; + +internal sealed class JsonTypeVisitor : IFieldVisitor { - internal sealed class JsonTypeVisitor : IFieldVisitor + private const int MaxDepth = 5; + private static readonly JsonTypeVisitor Instance = new JsonTypeVisitor(); + + public record struct Args(ResolvedComponents Components, Schema Schema, + JsonTypeFactory Factory, + bool WithHidden, + bool WithComponents, + int Level = 0); + + private JsonTypeVisitor() { - private const int MaxDepth = 5; - private static readonly JsonTypeVisitor Instance = new JsonTypeVisitor(); + } - public record struct Args(ResolvedComponents Components, Schema Schema, - JsonTypeFactory Factory, - bool WithHidden, - bool WithComponents, - int Level = 0); + public static JsonSchemaProperty? BuildProperty(IField field, ResolvedComponents components, Schema schema, + JsonTypeFactory factory, + bool withHidden, + bool withComponents) + { + var args = new Args(components, schema, factory, withHidden, withComponents); - private JsonTypeVisitor() - { - } + return field.Accept(Instance, args); + } - public static JsonSchemaProperty? BuildProperty(IField field, ResolvedComponents components, Schema schema, - JsonTypeFactory factory, - bool withHidden, - bool withComponents) + private JsonSchemaProperty? Accept(Args args, NestedField nestedField) + { + if (args.Level > MaxDepth) { - var args = new Args(components, schema, factory, withHidden, withComponents); - - return field.Accept(Instance, args); + return null; } - private JsonSchemaProperty? Accept(Args args, NestedField nestedField) - { - if (args.Level > MaxDepth) - { - return null; - } + return nestedField.Accept(this, args); + } - return nestedField.Accept(this, args); - } + public JsonSchemaProperty? Visit(IArrayField field, Args args) + { + // Create a reference to give it a nice name in code generation. + var (reference, actual) = args.Factory($"{args.Schema.TypeName()}{field.Name.ToPascalCase()}ItemDto"); - public JsonSchemaProperty? Visit(IArrayField field, Args args) + if (actual != null) { - // Create a reference to give it a nice name in code generation. - var (reference, actual) = args.Factory($"{args.Schema.TypeName()}{field.Name.ToPascalCase()}ItemDto"); + var nestedArgs = args with { Level = args.Level + 1 }; - if (actual != null) + foreach (var nestedField in field.Fields.ForApi(args.WithHidden)) { - var nestedArgs = args with { Level = args.Level + 1 }; + var nestedProperty = Accept(nestedArgs, nestedField); - foreach (var nestedField in field.Fields.ForApi(args.WithHidden)) + if (nestedProperty != null) { - var nestedProperty = Accept(nestedArgs, nestedField); - - if (nestedProperty != null) - { - nestedProperty.Description = nestedField.RawProperties.Hints; - nestedProperty.SetRequired(nestedField.RawProperties.IsRequired); + nestedProperty.Description = nestedField.RawProperties.Hints; + nestedProperty.SetRequired(nestedField.RawProperties.IsRequired); - actual.Properties.Add(nestedField.Name, nestedProperty); - } + actual.Properties.Add(nestedField.Name, nestedProperty); } } - - return JsonTypeBuilder.ArrayProperty(reference); } - public JsonSchemaProperty? Visit(IField field, Args args) - { - var property = JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); - - property.Default = field.Properties.DefaultValue; + return JsonTypeBuilder.ArrayProperty(reference); + } - if (field.Properties.MinItems != null) - { - property.MinItems = field.Properties.MinItems.Value; - } + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); - if (field.Properties.MaxItems != null) - { - property.MaxItems = field.Properties.MaxItems.Value; - } + property.Default = field.Properties.DefaultValue; - return property; + if (field.Properties.MinItems != null) + { + property.MinItems = field.Properties.MinItems.Value; } - public JsonSchemaProperty? Visit(IField field, Args args) + if (field.Properties.MaxItems != null) { - var property = JsonTypeBuilder.BooleanProperty(); + property.MaxItems = field.Properties.MaxItems.Value; + } - property.Default = field.Properties.DefaultValue; + return property; + } - return property; - } + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.BooleanProperty(); - public JsonSchemaProperty? Visit(IField field, Args args) - { - var property = JsonTypeBuilder.ObjectProperty(); + property.Default = field.Properties.DefaultValue; - BuildComponent(property, field.Properties.SchemaIds, args); + return property; + } - return property; - } + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.ObjectProperty(); - public JsonSchemaProperty? Visit(IField field, Args args) - { - var itemSchema = JsonTypeBuilder.Object(); + BuildComponent(property, field.Properties.SchemaIds, args); - BuildComponent(itemSchema, field.Properties.SchemaIds, args); + return property; + } - var property = JsonTypeBuilder.ArrayProperty(itemSchema); + public JsonSchemaProperty? Visit(IField field, Args args) + { + var itemSchema = JsonTypeBuilder.Object(); - if (field.Properties.MinItems != null) - { - property.MinItems = field.Properties.MinItems.Value; - } + BuildComponent(itemSchema, field.Properties.SchemaIds, args); - if (field.Properties.MaxItems != null) - { - property.MaxItems = field.Properties.MaxItems.Value; - } + var property = JsonTypeBuilder.ArrayProperty(itemSchema); - return property; + if (field.Properties.MinItems != null) + { + property.MinItems = field.Properties.MinItems.Value; } - public JsonSchemaProperty? Visit(IField field, Args args) + if (field.Properties.MaxItems != null) { - var property = JsonTypeBuilder.DateTimeProperty(); + property.MaxItems = field.Properties.MaxItems.Value; + } - property.Default = field.Properties.DefaultValue?.ToString(); + return property; + } - return property; - } + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.DateTimeProperty(); - public JsonSchemaProperty? Visit(IField field, Args args) - { - var property = JsonTypeBuilder.ObjectProperty(); + property.Default = field.Properties.DefaultValue?.ToString(); - property.Format = GeoJson.Format; + return property; + } - return property; - } + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.ObjectProperty(); - public JsonSchemaProperty? Visit(IField field, Args args) - { - return JsonTypeBuilder.JsonProperty(); - } + property.Format = GeoJson.Format; - public JsonSchemaProperty? Visit(IField field, Args args) - { - var property = JsonTypeBuilder.NumberProperty(); + return property; + } - property.Default = field.Properties.DefaultValue; + public JsonSchemaProperty? Visit(IField field, Args args) + { + return JsonTypeBuilder.JsonProperty(); + } - if (field.Properties.MinValue != null) - { - property.Minimum = (decimal)field.Properties.MinValue.Value; - } + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.NumberProperty(); - if (field.Properties.MaxValue != null) - { - property.Maximum = (decimal)field.Properties.MaxValue.Value; - } + property.Default = field.Properties.DefaultValue; - return property; + if (field.Properties.MinValue != null) + { + property.Minimum = (decimal)field.Properties.MinValue.Value; } - public JsonSchemaProperty? Visit(IField field, Args args) + if (field.Properties.MaxValue != null) { - var property = JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); - - property.Default = field.Properties.DefaultValue; + property.Maximum = (decimal)field.Properties.MaxValue.Value; + } - if (field.Properties.MinItems != null) - { - property.MinItems = field.Properties.MinItems.Value; - } + return property; + } - if (field.Properties.MaxItems != null) - { - property.MaxItems = field.Properties.MaxItems.Value; - } + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); - property.ExtensionData = new Dictionary - { - ["schemaIds"] = field.Properties.SchemaIds ?? ReadonlyList.Empty() - }; + property.Default = field.Properties.DefaultValue; - property.UniqueItems = !field.Properties.AllowDuplicates; + if (field.Properties.MinItems != null) + { + property.MinItems = field.Properties.MinItems.Value; + } - return property; + if (field.Properties.MaxItems != null) + { + property.MaxItems = field.Properties.MaxItems.Value; } - public JsonSchemaProperty? Visit(IField field, Args args) + property.ExtensionData = new Dictionary { - var property = JsonTypeBuilder.StringProperty(); + ["schemaIds"] = field.Properties.SchemaIds ?? ReadonlyList.Empty() + }; - property.Default = field.Properties.DefaultValue; + property.UniqueItems = !field.Properties.AllowDuplicates; - property.MaxLength = field.Properties.MaxLength; - property.MinLength = field.Properties.MinLength; + return property; + } - property.Pattern = field.Properties.Pattern; + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.StringProperty(); - if (field.Properties.AllowedValues != null) - { - var names = property.EnumerationNames ??= new Collection(); + property.Default = field.Properties.DefaultValue; - foreach (var value in field.Properties.AllowedValues) - { - names.Add(value); - } - } + property.MaxLength = field.Properties.MaxLength; + property.MinLength = field.Properties.MinLength; - return property; - } + property.Pattern = field.Properties.Pattern; - public JsonSchemaProperty? Visit(IField field, Args args) + if (field.Properties.AllowedValues != null) { - var property = JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); - - property.Default = field.Properties.DefaultValue; + var names = property.EnumerationNames ??= new Collection(); - if (field.Properties.MinItems != null) + foreach (var value in field.Properties.AllowedValues) { - property.MinItems = field.Properties.MinItems.Value; + names.Add(value); } + } - if (field.Properties.MaxItems != null) - { - property.MaxItems = field.Properties.MaxItems.Value; - } + return property; + } - return property; + public JsonSchemaProperty? Visit(IField field, Args args) + { + var property = JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); + + property.Default = field.Properties.DefaultValue; + + if (field.Properties.MinItems != null) + { + property.MinItems = field.Properties.MinItems.Value; } - public JsonSchemaProperty? Visit(IField field, Args args) + if (field.Properties.MaxItems != null) { - return null; + property.MaxItems = field.Properties.MaxItems.Value; } - private static void BuildComponent(JsonSchema jsonSchema, ReadonlyList? schemaIds, Args args) + return property; + } + + public JsonSchemaProperty? Visit(IField field, Args args) + { + return null; + } + + private static void BuildComponent(JsonSchema jsonSchema, ReadonlyList? schemaIds, Args args) + { + if (args.WithComponents) { - if (args.WithComponents) + var discriminator = new OpenApiDiscriminator { - var discriminator = new OpenApiDiscriminator - { - PropertyName = Component.Discriminator - }; + PropertyName = Component.Discriminator + }; - foreach (var schema in args.Components.Resolve(schemaIds).Values) - { - // Create a reference to give it a nice name in code generation. - var (reference, actual) = args.Factory($"{schema.TypeName()}ComponentDto"); + foreach (var schema in args.Components.Resolve(schemaIds).Values) + { + // Create a reference to give it a nice name in code generation. + var (reference, actual) = args.Factory($"{schema.TypeName()}ComponentDto"); - if (actual != null) + if (actual != null) + { + foreach (var field in schema.Fields.ForApi(args.WithHidden)) { - foreach (var field in schema.Fields.ForApi(args.WithHidden)) + var property = + BuildProperty( + field, + args.Components, + schema, + args.Factory, + args.WithHidden, + args.WithComponents); + + if (property != null) { - var property = - BuildProperty( - field, - args.Components, - schema, - args.Factory, - args.WithHidden, - args.WithComponents); - - if (property != null) - { - property.SetRequired(field.RawProperties.IsRequired); - property.SetDescription(field); - - actual.Properties.Add(field.Name, property); - } + property.SetRequired(field.RawProperties.IsRequired); + property.SetDescription(field); + + actual.Properties.Add(field.Name, property); } } - - jsonSchema.OneOf.Add(reference); - - discriminator.Mapping[schema.Name] = reference; } - jsonSchema.DiscriminatorObject = discriminator; + jsonSchema.OneOf.Add(reference); - if (discriminator.Mapping.Count > 0) - { - jsonSchema.Properties.Add(Component.Discriminator, JsonTypeBuilder.StringProperty(isRequired: true)); - } + discriminator.Mapping[schema.Name] = reference; } - else + + jsonSchema.DiscriminatorObject = discriminator; + + if (discriminator.Mapping.Count > 0) { - jsonSchema.AllowAdditionalProperties = true; + jsonSchema.Properties.Add(Component.Discriminator, JsonTypeBuilder.StringProperty(isRequired: true)); } } + else + { + jsonSchema.AllowAdditionalProperties = true; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs index d61e239973..cad2fb5021 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Constants.cs @@ -7,12 +7,11 @@ using NodaTime; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public static class Constants { - public static class Constants - { - public static readonly Duration ExpirationTime = Duration.FromDays(30); + public static readonly Duration ExpirationTime = Duration.FromDays(30); - public static readonly Duration StaleTime = Duration.FromDays(2); - } + public static readonly Duration StaleTime = Duration.FromDays(2); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs index 72f39cf6d1..3340374666 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs @@ -8,16 +8,15 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class DependencyInjectionExtensions { - public static class DependencyInjectionExtensions + public static IServiceCollection AddRuleAction(this IServiceCollection services) where THandler : class, IRuleActionHandler where TAction : RuleAction { - public static IServiceCollection AddRuleAction(this IServiceCollection services) where THandler : class, IRuleActionHandler where TAction : RuleAction - { - services.AddSingleton(); - services.AddSingleton(new RuleActionRegistration(typeof(TAction))); + services.AddSingleton(); + services.AddSingleton(new RuleActionRegistration(typeof(TAction))); - return services; - } + return services; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EditorAttribute.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EditorAttribute.cs index e5cb10ad5c..348bf60be5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EditorAttribute.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EditorAttribute.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class EditorAttribute : Attribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class EditorAttribute : Attribute - { - public RuleFieldEditor Editor { get; } + public RuleFieldEditor Editor { get; } - public EditorAttribute(RuleFieldEditor editor) - { - Editor = editor; - } + public EditorAttribute(RuleFieldEditor editor) + { + Editor = editor; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs index b180dec434..e6e2728006 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs @@ -12,68 +12,67 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class EventEnricher : IEventEnricher { - public sealed class EventEnricher : IEventEnricher + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private readonly IMemoryCache userCache; + private readonly IUserResolver userResolver; + + public EventEnricher(IMemoryCache userCache, IUserResolver userResolver) { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private readonly IMemoryCache userCache; - private readonly IUserResolver userResolver; + this.userCache = userCache; + this.userResolver = userResolver; + } - public EventEnricher(IMemoryCache userCache, IUserResolver userResolver) + public async Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope? @event) + { + if (@event != null) { - this.userCache = userCache; - this.userResolver = userResolver; + enrichedEvent.Timestamp = @event.Headers.Timestamp(); + + enrichedEvent.AppId = @event.Payload.AppId; } - public async Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope? @event) + if (enrichedEvent is EnrichedUserEventBase userEvent) { - if (@event != null) + if (@event?.Payload is SquidexEvent squidexEvent) { - enrichedEvent.Timestamp = @event.Headers.Timestamp(); - - enrichedEvent.AppId = @event.Payload.AppId; + userEvent.Actor = squidexEvent.Actor; } - if (enrichedEvent is EnrichedUserEventBase userEvent) + if (userEvent.Actor != null) { - if (@event?.Payload is SquidexEvent squidexEvent) - { - userEvent.Actor = squidexEvent.Actor; - } - - if (userEvent.Actor != null) - { - userEvent.User = await FindUserAsync(userEvent.Actor); - } + userEvent.User = await FindUserAsync(userEvent.Actor); } } + } - private Task FindUserAsync(RefToken actor) + private Task FindUserAsync(RefToken actor) + { + var cacheKey = $"{typeof(EventEnricher)}_Users_{actor.Identifier}"; + + return userCache.GetOrCreateAsync(cacheKey, async x => { - var cacheKey = $"{typeof(EventEnricher)}_Users_{actor.Identifier}"; + x.AbsoluteExpirationRelativeToNow = CacheDuration; - return userCache.GetOrCreateAsync(cacheKey, async x => + IUser? user; + try { - x.AbsoluteExpirationRelativeToNow = CacheDuration; - - IUser? user; - try - { - user = await userResolver.FindByIdAsync(actor.Identifier); - } - catch - { - user = null; - } + user = await userResolver.FindByIdAsync(actor.Identifier); + } + catch + { + user = null; + } - if (user == null && actor.IsClient) - { - user = new ClientUser(actor); - } + if (user == null && actor.IsClient) + { + user = new ClientUser(actor); + } - return user; - }); - } + return user; + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs index a94886777b..c8d6d65244 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs @@ -10,51 +10,50 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class EventJsonSchemaGenerator { - public sealed class EventJsonSchemaGenerator - { - private readonly Lazy> schemas; - private readonly JsonSchemaGenerator schemaGenerator; + private readonly Lazy> schemas; + private readonly JsonSchemaGenerator schemaGenerator; - public IReadOnlyCollection AllTypes - { - get => schemas.Value.Keys; - } + public IReadOnlyCollection AllTypes + { + get => schemas.Value.Keys; + } - public EventJsonSchemaGenerator(JsonSchemaGenerator schemaGenerator) - { - this.schemaGenerator = schemaGenerator; + public EventJsonSchemaGenerator(JsonSchemaGenerator schemaGenerator) + { + this.schemaGenerator = schemaGenerator; - schemas = new Lazy>(GenerateSchemas); - } + schemas = new Lazy>(GenerateSchemas); + } - public JsonSchema? GetSchema(string typeName) - { - Guard.NotNull(typeName); + public JsonSchema? GetSchema(string typeName) + { + Guard.NotNull(typeName); - return schemas.Value.GetValueOrDefault(typeName); - } + return schemas.Value.GetValueOrDefault(typeName); + } - private Dictionary GenerateSchemas() - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + private Dictionary GenerateSchemas() + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - var baseType = typeof(EnrichedEvent); + var baseType = typeof(EnrichedEvent); - var assembly = baseType.Assembly; + var assembly = baseType.Assembly; - foreach (var type in assembly.GetTypes()) + foreach (var type in assembly.GetTypes()) + { + if (!type.IsAbstract && type.IsAssignableTo(baseType)) { - if (!type.IsAbstract && type.IsAssignableTo(baseType)) - { - var schema = schemaGenerator.Generate(type); + var schema = schemaGenerator.Generate(type); - result[type.Name] = schema!; - } + result[type.Name] = schema!; } - - return result; } + + return result; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs index 9569596dc0..a9fa812ddf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs @@ -12,139 +12,138 @@ using Squidex.Infrastructure; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.HandleRules.Extensions +namespace Squidex.Domain.Apps.Core.HandleRules.Extensions; + +public sealed class EventFluidExtensions : IFluidExtension { - public sealed class EventFluidExtensions : IFluidExtension + private readonly IUrlGenerator urlGenerator; + + public EventFluidExtensions(IUrlGenerator urlGenerator) { - private readonly IUrlGenerator urlGenerator; + this.urlGenerator = urlGenerator; + } - public EventFluidExtensions(IUrlGenerator urlGenerator) - { - this.urlGenerator = urlGenerator; - } + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + TemplateContext.GlobalFilters.AddFilter("contentUrl", ContentUrl); + TemplateContext.GlobalFilters.AddFilter("assetContentUrl", AssetContentUrl); + TemplateContext.GlobalFilters.AddFilter("assetContentAppUrl", AssetContentAppUrl); + TemplateContext.GlobalFilters.AddFilter("assetContentSlugUrl", AssetContentSlugUrl); + } - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - TemplateContext.GlobalFilters.AddFilter("contentUrl", ContentUrl); - TemplateContext.GlobalFilters.AddFilter("assetContentUrl", AssetContentUrl); - TemplateContext.GlobalFilters.AddFilter("assetContentAppUrl", AssetContentAppUrl); - TemplateContext.GlobalFilters.AddFilter("assetContentSlugUrl", AssetContentSlugUrl); - } + private FluidValue ContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var value = input.ToObjectValue(); - private FluidValue ContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + switch (value) { - var value = input.ToObjectValue(); - - switch (value) - { - case DomainId id: + case DomainId id: + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent) { - if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent) - { - var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, id); - - return new StringValue(result); - } - - break; - } - - case EnrichedContentEvent contentEvent: - { - var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); + var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, id); return new StringValue(result); } - } - return NilValue.Empty; - } + break; + } - private FluidValue AssetContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) - { - var value = input.ToObjectValue(); + case EnrichedContentEvent contentEvent: + { + var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); - switch (value) - { - case DomainId id: - { - if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) - { - var result = urlGenerator.AssetContent(assetEvent.AppId, id.ToString()); + return new StringValue(result); + } + } - return new StringValue(result); - } + return NilValue.Empty; + } - break; - } + private FluidValue AssetContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var value = input.ToObjectValue(); - case EnrichedAssetEvent assetEvent: + switch (value) + { + case DomainId id: + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) { - var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + var result = urlGenerator.AssetContent(assetEvent.AppId, id.ToString()); return new StringValue(result); } - } - return NilValue.Empty; - } + break; + } - private FluidValue AssetContentAppUrl(FluidValue input, FilterArguments arguments, TemplateContext context) - { - var value = input.ToObjectValue(); + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); - switch (value) - { - case DomainId id: - { - if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) - { - var result = urlGenerator.AssetContent(assetEvent.AppId, id.ToString()); + return new StringValue(result); + } + } - return new StringValue(result); - } + return NilValue.Empty; + } - break; - } + private FluidValue AssetContentAppUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var value = input.ToObjectValue(); - case EnrichedAssetEvent assetEvent: + switch (value) + { + case DomainId id: + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) { - var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + var result = urlGenerator.AssetContent(assetEvent.AppId, id.ToString()); return new StringValue(result); } - } - return NilValue.Empty; - } + break; + } - private FluidValue AssetContentSlugUrl(FluidValue input, FilterArguments arguments, TemplateContext context) - { - var value = input.ToObjectValue(); + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); - switch (value) - { - case string s: - { - if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) - { - var result = urlGenerator.AssetContent(assetEvent.AppId, s.Slugify()); + return new StringValue(result); + } + } - return new StringValue(result); - } + return NilValue.Empty; + } - break; - } + private FluidValue AssetContentSlugUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var value = input.ToObjectValue(); - case EnrichedAssetEvent assetEvent: + switch (value) + { + case string s: + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) { - var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + var result = urlGenerator.AssetContent(assetEvent.AppId, s.Slugify()); return new StringValue(result); } - } - return NilValue.Empty; + break; + } + + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + + return new StringValue(result); + } } + + return NilValue.Empty; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs index 266a6941c4..2059919152 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs @@ -11,93 +11,92 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.HandleRules.Extensions +namespace Squidex.Domain.Apps.Core.HandleRules.Extensions; + +public sealed class EventJintExtension : IJintExtension, IScriptDescriptor { - public sealed class EventJintExtension : IJintExtension, IScriptDescriptor - { - private delegate JsValue EventDelegate(); - private readonly IUrlGenerator urlGenerator; + private delegate JsValue EventDelegate(); + private readonly IUrlGenerator urlGenerator; - public EventJintExtension(IUrlGenerator urlGenerator) - { - this.urlGenerator = urlGenerator; - } + public EventJintExtension(IUrlGenerator urlGenerator) + { + this.urlGenerator = urlGenerator; + } - public void Extend(ScriptExecutionContext context) + public void Extend(ScriptExecutionContext context) + { + context.Engine.SetValue("contentAction", new EventDelegate(() => { - context.Engine.SetValue("contentAction", new EventDelegate(() => + if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent) { - if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent) - { - return contentEvent.Status.ToString(); - } + return contentEvent.Status.ToString(); + } - return JsValue.Null; - })); + return JsValue.Null; + })); - context.Engine.SetValue("contentUrl", new EventDelegate(() => + context.Engine.SetValue("contentUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent) { - if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent) - { - return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); - } + return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); + } - return JsValue.Null; - })); + return JsValue.Null; + })); - context.Engine.SetValue("assetContentUrl", new EventDelegate(() => + context.Engine.SetValue("assetContentUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) { - if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) - { - return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); - } + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + } - return JsValue.Null; - })); + return JsValue.Null; + })); - context.Engine.SetValue("assetContentAppUrl", new EventDelegate(() => + context.Engine.SetValue("assetContentAppUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) { - if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) - { - return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); - } + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + } - return JsValue.Null; - })); + return JsValue.Null; + })); - context.Engine.SetValue("assetContentSlugUrl", new EventDelegate(() => + context.Engine.SetValue("assetContentSlugUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) { - if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) - { - return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); - } + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + } - return JsValue.Null; - })); - } + return JsValue.Null; + })); + } - public void Describe(AddDescription describe, ScriptScope scope) + public void Describe(AddDescription describe, ScriptScope scope) + { + if (scope.HasFlag(ScriptScope.ContentTrigger)) { - if (scope.HasFlag(ScriptScope.ContentTrigger)) - { - describe(JsonType.Function, "contentAction", - Resources.ScriptingContentAction); + describe(JsonType.Function, "contentAction", + Resources.ScriptingContentAction); - describe(JsonType.Function, "contentUrl", - Resources.ScriptingContentUrl); - } + describe(JsonType.Function, "contentUrl", + Resources.ScriptingContentUrl); + } - if (scope.HasFlag(ScriptScope.AssetTrigger)) - { - describe(JsonType.Function, "assetContentUrl", - Resources.ScriptingAssetContentUrl); + if (scope.HasFlag(ScriptScope.AssetTrigger)) + { + describe(JsonType.Function, "assetContentUrl", + Resources.ScriptingAssetContentUrl); - describe(JsonType.Function, "assetContentAppUrl", - Resources.ScriptingAssetContentAppUrl); + describe(JsonType.Function, "assetContentAppUrl", + Resources.ScriptingAssetContentAppUrl); - describe(JsonType.Function, "assetContentSlugUrl", - Resources.ScriptingAssetContentSlugUrl); - } + describe(JsonType.Function, "assetContentSlugUrl", + Resources.ScriptingAssetContentSlugUrl); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs index 48ab9474c0..ada85ef0d0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class FormattableAttribute : Attribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class FormattableAttribute : Attribute - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs index 32c5b9ce55..3a1b1af9c4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs @@ -9,10 +9,9 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public interface IEventEnricher { - public interface IEventEnricher - { - Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope? @event); - } + Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope? @event); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs index 7386b925a8..5057f7b55a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs @@ -8,17 +8,16 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public interface IRuleActionHandler { - public interface IRuleActionHandler - { - Type ActionType { get; } + Type ActionType { get; } - Type DataType { get; } + Type DataType { get; } - Task<(string Description, object Data)> CreateJobAsync(EnrichedEvent @event, RuleAction action); + Task<(string Description, object Data)> CreateJobAsync(EnrichedEvent @event, RuleAction action); - Task ExecuteJobAsync(object data, - CancellationToken ct = default); - } + Task ExecuteJobAsync(object data, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleEventFormatter.cs index c897483cb3..78f6175b1f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleEventFormatter.cs @@ -7,18 +7,17 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public interface IRuleEventFormatter { - public interface IRuleEventFormatter + (bool Match, string?, int ReplacedLength) Format(EnrichedEvent @event, string text) { - (bool Match, string?, int ReplacedLength) Format(EnrichedEvent @event, string text) - { - return default; - } + return default; + } - (bool Match, ValueTask) Format(EnrichedEvent @event, object value, string[] path) - { - return default; - } + (bool Match, ValueTask) Format(EnrichedEvent @event, object value, string[] path) + { + return default; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs index c11c348b0b..b9bebb7578 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs @@ -8,21 +8,20 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public interface IRuleService { - public interface IRuleService - { - bool CanCreateSnapshotEvents(RuleContext context); + bool CanCreateSnapshotEvents(RuleContext context); - string GetName(AppEvent @event); + string GetName(AppEvent @event); - IAsyncEnumerable CreateSnapshotJobsAsync(RuleContext context, - CancellationToken ct = default); + IAsyncEnumerable CreateSnapshotJobsAsync(RuleContext context, + CancellationToken ct = default); - IAsyncEnumerable CreateJobsAsync(Envelope @event, RuleContext context, - CancellationToken ct = default); + IAsyncEnumerable CreateJobsAsync(Envelope @event, RuleContext context, + CancellationToken ct = default); - Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job, - CancellationToken ct = default); - } + Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs index bff3ef8a46..8197ff814d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -9,41 +9,40 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public interface IRuleTriggerHandler { - public interface IRuleTriggerHandler + Type TriggerType { get; } + + bool CanCreateSnapshotEvents + { + get => false; + } + + IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context, + CancellationToken ct) + { + return AsyncEnumerable.Empty(); + } + + IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context, + CancellationToken ct); + + string? GetName(AppEvent @event) + { + return null; + } + + bool Trigger(Envelope @event, RuleContext context) { - Type TriggerType { get; } - - bool CanCreateSnapshotEvents - { - get => false; - } - - IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context, - CancellationToken ct) - { - return AsyncEnumerable.Empty(); - } - - IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context, - CancellationToken ct); - - string? GetName(AppEvent @event) - { - return null; - } - - bool Trigger(Envelope @event, RuleContext context) - { - return true; - } - - bool Trigger(EnrichedEvent @event, RuleContext context) - { - return true; - } - - bool Handles(AppEvent @event); + return true; } + + bool Trigger(EnrichedEvent @event, RuleContext context) + { + return true; + } + + bool Handles(AppEvent @event); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs index ed7cbb23a3..9ba556c1ff 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs @@ -8,72 +8,71 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed record JobResult { - public sealed record JobResult + public static readonly JobResult ConditionDoesNotMatch = new JobResult { - public static readonly JobResult ConditionDoesNotMatch = new JobResult - { - SkipReason = SkipReason.ConditionDoesNotMatch - }; + SkipReason = SkipReason.ConditionDoesNotMatch + }; - public static readonly JobResult ConditionPrecheckDoesNotMatch = new JobResult - { - SkipReason = SkipReason.ConditionPrecheckDoesNotMatch - }; + public static readonly JobResult ConditionPrecheckDoesNotMatch = new JobResult + { + SkipReason = SkipReason.ConditionPrecheckDoesNotMatch + }; - public static readonly JobResult Disabled = new JobResult - { - SkipReason = SkipReason.Disabled - }; + public static readonly JobResult Disabled = new JobResult + { + SkipReason = SkipReason.Disabled + }; - public static readonly JobResult WrongEvent = new JobResult - { - SkipReason = SkipReason.WrongEvent - }; + public static readonly JobResult WrongEvent = new JobResult + { + SkipReason = SkipReason.WrongEvent + }; - public static readonly JobResult FromRule = new JobResult - { - SkipReason = SkipReason.FromRule - }; + public static readonly JobResult FromRule = new JobResult + { + SkipReason = SkipReason.FromRule + }; - public static readonly JobResult NoAction = new JobResult - { - SkipReason = SkipReason.NoAction - }; + public static readonly JobResult NoAction = new JobResult + { + SkipReason = SkipReason.NoAction + }; - public static readonly JobResult NoTrigger = new JobResult - { - SkipReason = SkipReason.NoTrigger - }; + public static readonly JobResult NoTrigger = new JobResult + { + SkipReason = SkipReason.NoTrigger + }; - public static readonly JobResult TooOld = new JobResult - { - SkipReason = SkipReason.TooOld - }; + public static readonly JobResult TooOld = new JobResult + { + SkipReason = SkipReason.TooOld + }; - public static readonly JobResult WrongEventForTrigger = new JobResult - { - SkipReason = SkipReason.WrongEventForTrigger - }; + public static readonly JobResult WrongEventForTrigger = new JobResult + { + SkipReason = SkipReason.WrongEventForTrigger + }; - public RuleJob? Job { get; init; } + public RuleJob? Job { get; init; } - public EnrichedEvent? EnrichedEvent { get; init; } + public EnrichedEvent? EnrichedEvent { get; init; } - public Exception? EnrichmentError { get; init; } + public Exception? EnrichmentError { get; init; } - public SkipReason SkipReason { get; init; } + public SkipReason SkipReason { get; init; } - public static JobResult Failed(Exception exception, EnrichedEvent? enrichedEvent = null, RuleJob? job = null) + public static JobResult Failed(Exception exception, EnrichedEvent? enrichedEvent = null, RuleJob? job = null) + { + return new JobResult { - return new JobResult - { - Job = job, - EnrichedEvent = enrichedEvent, - EnrichmentError = exception, - SkipReason = SkipReason.Failed - }; - } + Job = job, + EnrichedEvent = enrichedEvent, + EnrichmentError = exception, + SkipReason = SkipReason.Failed + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs index d62ff07d04..9c5b8ac8d3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs @@ -10,206 +10,205 @@ using Squidex.Shared.Identity; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class PredefinedPatternsFormatter : IRuleEventFormatter { - public sealed class PredefinedPatternsFormatter : IRuleEventFormatter + private readonly List<(string Pattern, Func Replacer)> patterns = new List<(string Pattern, Func Replacer)>(); + private readonly IUrlGenerator urlGenerator; + + public PredefinedPatternsFormatter(IUrlGenerator urlGenerator) { - private readonly List<(string Pattern, Func Replacer)> patterns = new List<(string Pattern, Func Replacer)>(); - private readonly IUrlGenerator urlGenerator; + this.urlGenerator = urlGenerator; + + AddPattern("APP_ID", AppId); + AddPattern("APP_NAME", AppName); + AddPattern("ASSET_CONTENT_URL", AssetContentUrl); + AddPattern("ASSET_CONTENT_APP_URL", AssetContentAppUrl); + AddPattern("ASSET_CONTENT_SLUG_URL", AssetContentSlugUrl); + AddPattern("CONTENT_ACTION", ContentAction); + AddPattern("CONTENT_URL", ContentUrl); + AddPattern("MENTIONED_ID", MentionedId); + AddPattern("MENTIONED_NAME", MentionedName); + AddPattern("MENTIONED_EMAIL", MentionedEmail); + AddPattern("SCHEMA_ID", SchemaId); + AddPattern("SCHEMA_NAME", SchemaName); + AddPattern("TIMESTAMP_DATETIME", TimestampTime); + AddPattern("TIMESTAMP_DATE", TimestampDate); + AddPattern("USER_ID", UserId); + AddPattern("USER_NAME", UserName); + AddPattern("USER_EMAIL", UserEmail); + } - public PredefinedPatternsFormatter(IUrlGenerator urlGenerator) - { - this.urlGenerator = urlGenerator; - - AddPattern("APP_ID", AppId); - AddPattern("APP_NAME", AppName); - AddPattern("ASSET_CONTENT_URL", AssetContentUrl); - AddPattern("ASSET_CONTENT_APP_URL", AssetContentAppUrl); - AddPattern("ASSET_CONTENT_SLUG_URL", AssetContentSlugUrl); - AddPattern("CONTENT_ACTION", ContentAction); - AddPattern("CONTENT_URL", ContentUrl); - AddPattern("MENTIONED_ID", MentionedId); - AddPattern("MENTIONED_NAME", MentionedName); - AddPattern("MENTIONED_EMAIL", MentionedEmail); - AddPattern("SCHEMA_ID", SchemaId); - AddPattern("SCHEMA_NAME", SchemaName); - AddPattern("TIMESTAMP_DATETIME", TimestampTime); - AddPattern("TIMESTAMP_DATE", TimestampDate); - AddPattern("USER_ID", UserId); - AddPattern("USER_NAME", UserName); - AddPattern("USER_EMAIL", UserEmail); - } + private void AddPattern(string placeholder, Func generator) + { + patterns.Add((placeholder, generator)); + } - private void AddPattern(string placeholder, Func generator) + public (bool Match, string?, int ReplacedLength) Format(EnrichedEvent @event, string text) + { + for (var j = 0; j < patterns.Count; j++) { - patterns.Add((placeholder, generator)); - } + var (pattern, replacer) = patterns[j]; - public (bool Match, string?, int ReplacedLength) Format(EnrichedEvent @event, string text) - { - for (var j = 0; j < patterns.Count; j++) + if (text.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) { - var (pattern, replacer) = patterns[j]; - - if (text.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) - { - var result = replacer(@event); + var result = replacer(@event); - return (true, result, pattern.Length); - } + return (true, result, pattern.Length); } - - return default; } - private static string TimestampDate(EnrichedEvent @event) - { - return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture); - } + return default; + } - private static string TimestampTime(EnrichedEvent @event) - { - return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture); - } + private static string TimestampDate(EnrichedEvent @event) + { + return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture); + } - private static string AppId(EnrichedEvent @event) - { - return @event.AppId.Id.ToString(); - } + private static string TimestampTime(EnrichedEvent @event) + { + return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture); + } - private static string AppName(EnrichedEvent @event) - { - return @event.AppId.Name; - } + private static string AppId(EnrichedEvent @event) + { + return @event.AppId.Id.ToString(); + } - private static string? SchemaId(EnrichedEvent @event) - { - if (@event is EnrichedSchemaEventBase schemaEvent) - { - return schemaEvent.SchemaId.Id.ToString(); - } + private static string AppName(EnrichedEvent @event) + { + return @event.AppId.Name; + } - return null; + private static string? SchemaId(EnrichedEvent @event) + { + if (@event is EnrichedSchemaEventBase schemaEvent) + { + return schemaEvent.SchemaId.Id.ToString(); } - private static string? SchemaName(EnrichedEvent @event) - { - if (@event is EnrichedSchemaEventBase schemaEvent) - { - return schemaEvent.SchemaId.Name; - } + return null; + } - return null; + private static string? SchemaName(EnrichedEvent @event) + { + if (@event is EnrichedSchemaEventBase schemaEvent) + { + return schemaEvent.SchemaId.Name; } - private static string? ContentAction(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return contentEvent.Type.ToString(); - } + return null; + } - return null; + private static string? ContentAction(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return contentEvent.Type.ToString(); } - private string? AssetContentUrl(EnrichedEvent @event) - { - if (@event is EnrichedAssetEvent assetEvent) - { - return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); - } + return null; + } - return null; + private string? AssetContentUrl(EnrichedEvent @event) + { + if (@event is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); } - private string? AssetContentAppUrl(EnrichedEvent @event) - { - if (@event is EnrichedAssetEvent assetEvent) - { - return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); - } + return null; + } - return null; + private string? AssetContentAppUrl(EnrichedEvent @event) + { + if (@event is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); } - private string? AssetContentSlugUrl(EnrichedEvent @event) - { - if (@event is EnrichedAssetEvent assetEvent) - { - return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); - } + return null; + } - return null; + private string? AssetContentSlugUrl(EnrichedEvent @event) + { + if (@event is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); } - private string? ContentUrl(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); - } + return null; + } - return null; + private string? ContentUrl(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); } - private static string? UserName(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Claims.DisplayName(); - } + return null; + } - return null; + private static string? UserName(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Claims.DisplayName(); } - private static string? UserId(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Id; - } + return null; + } - return null; + private static string? UserId(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Id; } - private static string? UserEmail(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Email; - } + return null; + } - return null; + private static string? UserEmail(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Email; } - private static string? MentionedName(EnrichedEvent @event) - { - if (@event is EnrichedCommentEvent commentEvent) - { - return commentEvent.MentionedUser.Claims.DisplayName(); - } + return null; + } - return null; + private static string? MentionedName(EnrichedEvent @event) + { + if (@event is EnrichedCommentEvent commentEvent) + { + return commentEvent.MentionedUser.Claims.DisplayName(); } - private static string? MentionedId(EnrichedEvent @event) - { - if (@event is EnrichedCommentEvent commentEvent) - { - return commentEvent.MentionedUser.Id; - } + return null; + } - return null; + private static string? MentionedId(EnrichedEvent @event) + { + if (@event is EnrichedCommentEvent commentEvent) + { + return commentEvent.MentionedUser.Id; } - private static string? MentionedEmail(EnrichedEvent @event) - { - if (@event is EnrichedCommentEvent commentEvent) - { - return commentEvent.MentionedUser.Email; - } + return null; + } - return null; + private static string? MentionedEmail(EnrichedEvent @event) + { + if (@event is EnrichedCommentEvent commentEvent) + { + return commentEvent.MentionedUser.Email; } + + return null; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs index a2bb672b04..a57b761f2f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs @@ -8,96 +8,95 @@ using System.Globalization; using System.Text; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class Result { - public sealed class Result - { - public Exception? Exception { get; private init; } + public Exception? Exception { get; private init; } - public string? Dump { get; private set; } + public string? Dump { get; private set; } - public RuleResult Status { get; private set; } + public RuleResult Status { get; private set; } - public void Enrich(TimeSpan elapsed) - { - var dumpBuilder = new StringBuilder(); - - if (!string.IsNullOrWhiteSpace(Dump)) - { - dumpBuilder.AppendLine(Dump); - } - - if (Status == RuleResult.Timeout) - { - dumpBuilder.AppendLine(); - dumpBuilder.AppendLine("Action timed out."); - } - - if (Exception != null) - { - dumpBuilder.AppendLine(); - dumpBuilder.Append("Error: "); - dumpBuilder.AppendLine(Exception.Message); - } - - dumpBuilder.AppendLine(); - dumpBuilder.AppendFormat(CultureInfo.InvariantCulture, "Elapsed {0}.", elapsed); - dumpBuilder.AppendLine(); - - Dump = dumpBuilder.ToString(); - } + public void Enrich(TimeSpan elapsed) + { + var dumpBuilder = new StringBuilder(); - public static Result Ignored() + if (!string.IsNullOrWhiteSpace(Dump)) { - return Success("Ignored"); + dumpBuilder.AppendLine(Dump); } - public static Result Complete() + if (Status == RuleResult.Timeout) { - return Success("Completed"); + dumpBuilder.AppendLine(); + dumpBuilder.AppendLine("Action timed out."); } - public static Result Create(string? dump, RuleResult result) + if (Exception != null) { - return new Result { Dump = dump, Status = result }; + dumpBuilder.AppendLine(); + dumpBuilder.Append("Error: "); + dumpBuilder.AppendLine(Exception.Message); } - public static Result Success(string? dump) + dumpBuilder.AppendLine(); + dumpBuilder.AppendFormat(CultureInfo.InvariantCulture, "Elapsed {0}.", elapsed); + dumpBuilder.AppendLine(); + + Dump = dumpBuilder.ToString(); + } + + public static Result Ignored() + { + return Success("Ignored"); + } + + public static Result Complete() + { + return Success("Completed"); + } + + public static Result Create(string? dump, RuleResult result) + { + return new Result { Dump = dump, Status = result }; + } + + public static Result Success(string? dump) + { + return new Result { Dump = dump, Status = RuleResult.Success }; + } + + public static Result Failed(Exception? ex) + { + return Failed(ex, ex?.Message); + } + + public static Result SuccessOrFailed(Exception? ex, string? dump) + { + if (ex != null) { - return new Result { Dump = dump, Status = RuleResult.Success }; + return Failed(ex, dump); } - - public static Result Failed(Exception? ex) + else { - return Failed(ex, ex?.Message); + return Success(dump); } + } - public static Result SuccessOrFailed(Exception? ex, string? dump) + public static Result Failed(Exception? ex, string? dump) + { + var result = new Result { Exception = ex, Dump = dump ?? ex?.Message }; + + if (ex is OperationCanceledException or TimeoutException) { - if (ex != null) - { - return Failed(ex, dump); - } - else - { - return Success(dump); - } + result.Status = RuleResult.Timeout; } - - public static Result Failed(Exception? ex, string? dump) + else { - var result = new Result { Exception = ex, Dump = dump ?? ex?.Message }; - - if (ex is OperationCanceledException or TimeoutException) - { - result.Status = RuleResult.Timeout; - } - else - { - result.Status = RuleResult.Failed; - } - - return result; + result.Status = RuleResult.Failed; } + + return result; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs index 9ebc0500e4..58d45897d6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs @@ -5,21 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class RuleActionAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class RuleActionAttribute : Attribute - { - public string Title { get; set; } + public string Title { get; set; } - public string ReadMore { get; set; } + public string ReadMore { get; set; } - public string IconImage { get; set; } + public string IconImage { get; set; } - public string IconColor { get; set; } + public string IconColor { get; set; } - public string Display { get; set; } + public string Display { get; set; } - public string Description { get; set; } - } + public string Description { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs index 638ae4e4c3..4406013cfd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs @@ -5,24 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class RuleActionDefinition { - public sealed class RuleActionDefinition - { - public Type Type { get; set; } + public Type Type { get; set; } - public string Title { get; set; } + public string Title { get; set; } - public string ReadMore { get; set; } + public string ReadMore { get; set; } - public string IconImage { get; set; } + public string IconImage { get; set; } - public string IconColor { get; set; } + public string IconColor { get; set; } - public string Display { get; set; } + public string Display { get; set; } - public string Description { get; set; } + public string Description { get; set; } - public List Properties { get; } = new List(); - } + public List Properties { get; } = new List(); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs index 1360451894..7e9fdc2362 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -10,75 +10,74 @@ #pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public abstract class RuleActionHandler : IRuleActionHandler where TAction : RuleAction { - public abstract class RuleActionHandler : IRuleActionHandler where TAction : RuleAction + private readonly RuleEventFormatter formatter; + + Type IRuleActionHandler.ActionType + { + get => typeof(TAction); + } + + Type IRuleActionHandler.DataType + { + get => typeof(TData); + } + + protected RuleActionHandler(RuleEventFormatter formatter) + { + this.formatter = formatter; + } + + protected virtual string ToJson(T @event) where T : notnull + { + return formatter.ToPayload(@event); + } + + protected virtual string ToEnvelopeJson(EnrichedEvent @event) + { + return formatter.ToEnvelope(@event); + } + + protected ValueTask FormatAsync(Uri uri, EnrichedEvent @event) + { + return formatter.FormatAsync(uri.ToString(), @event); + } + + protected ValueTask FormatAsync(string text, EnrichedEvent @event) + { + return formatter.FormatAsync(text, @event); + } + + async Task<(string Description, object Data)> IRuleActionHandler.CreateJobAsync(EnrichedEvent @event, RuleAction action) + { + var (description, data) = await CreateJobAsync(@event, (TAction)action); + + return (description, data!); + } + + async Task IRuleActionHandler.ExecuteJobAsync(object data, + CancellationToken ct) + { + var typedData = (TData)data; + + return await ExecuteJobAsync(typedData, ct); + } + + protected virtual Task<(string Description, TData Data)> CreateJobAsync(EnrichedEvent @event, TAction action) { - private readonly RuleEventFormatter formatter; - - Type IRuleActionHandler.ActionType - { - get => typeof(TAction); - } - - Type IRuleActionHandler.DataType - { - get => typeof(TData); - } - - protected RuleActionHandler(RuleEventFormatter formatter) - { - this.formatter = formatter; - } - - protected virtual string ToJson(T @event) where T : notnull - { - return formatter.ToPayload(@event); - } - - protected virtual string ToEnvelopeJson(EnrichedEvent @event) - { - return formatter.ToEnvelope(@event); - } - - protected ValueTask FormatAsync(Uri uri, EnrichedEvent @event) - { - return formatter.FormatAsync(uri.ToString(), @event); - } - - protected ValueTask FormatAsync(string text, EnrichedEvent @event) - { - return formatter.FormatAsync(text, @event); - } - - async Task<(string Description, object Data)> IRuleActionHandler.CreateJobAsync(EnrichedEvent @event, RuleAction action) - { - var (description, data) = await CreateJobAsync(@event, (TAction)action); - - return (description, data!); - } - - async Task IRuleActionHandler.ExecuteJobAsync(object data, - CancellationToken ct) - { - var typedData = (TData)data; - - return await ExecuteJobAsync(typedData, ct); - } - - 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) - { - throw new NotImplementedException(); - } + } - protected abstract Task ExecuteJobAsync(TData job, - CancellationToken ct = default); + protected virtual (string Description, TData Data) CreateJob(EnrichedEvent @event, TAction action) + { + throw new NotImplementedException(); } + + protected abstract Task ExecuteJobAsync(TData job, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs index e360d603bb..3a75e22a6a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs @@ -5,22 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class RuleActionProperty { - public sealed class RuleActionProperty - { - public RuleFieldEditor Editor { get; set; } + public RuleFieldEditor Editor { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string Display { get; set; } + public string Display { get; set; } - public string? Description { get; set; } + public string? Description { get; set; } - public string[]? Options { get; set; } + public string[]? Options { get; set; } - public bool IsFormattable { get; set; } + public bool IsFormattable { get; set; } - public bool IsRequired { get; set; } - } + public bool IsRequired { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs index d78769e6cc..f50596272c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class RuleActionRegistration { - public sealed class RuleActionRegistration - { - public Type ActionType { get; } + public Type ActionType { get; } - internal RuleActionRegistration(Type actionType) - { - Guard.NotNull(actionType); + internal RuleActionRegistration(Type actionType) + { + Guard.NotNull(actionType); - ActionType = actionType; - } + ActionType = actionType; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs index 7499e760ae..6afb52d966 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs @@ -8,18 +8,17 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public struct RuleContext { - public struct RuleContext - { - public NamedId AppId { get; init; } + public NamedId AppId { get; init; } - public DomainId RuleId { get; init; } + public DomainId RuleId { get; init; } - public Rule Rule { get; init; } + public Rule Rule { get; init; } - public bool IncludeSkipped { get; init; } + public bool IncludeSkipped { get; init; } - public bool IncludeStale { get; init; } - } + public bool IncludeStale { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index fc969406a4..4734f35547 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -21,376 +21,375 @@ using Squidex.Text; using ValueTaskSupplement; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public class RuleEventFormatter { - public class RuleEventFormatter + private const string GlobalFallback = "null"; + private static readonly Regex RegexPatternOld = new Regex(@"^(?(?[^_]*)_(?[^\s]*))", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly Regex RegexPatternNew = new Regex(@"^\{(?(?[\w]+)_(?[\w\.\-]+))[\s]*(\|[\s]*(?[^\?}]+))?(\?[\s]*(?[^\}\s]+))?[\s]*\}", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private readonly IJsonSerializer serializer; + private readonly IEnumerable formatters; + private readonly ITemplateEngine templateEngine; + private readonly IScriptEngine scriptEngine; + + private struct TextPart { - private const string GlobalFallback = "null"; - private static readonly Regex RegexPatternOld = new Regex(@"^(?(?[^_]*)_(?[^\s]*))", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - private static readonly Regex RegexPatternNew = new Regex(@"^\{(?(?[\w]+)_(?[\w\.\-]+))[\s]*(\|[\s]*(?[^\?}]+))?(\?[\s]*(?[^\}\s]+))?[\s]*\}", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - private readonly IJsonSerializer serializer; - private readonly IEnumerable formatters; - private readonly ITemplateEngine templateEngine; - private readonly IScriptEngine scriptEngine; - - private struct TextPart - { - public bool IsText; + public bool IsText; - public int TextLength; + public int TextLength; - public int TextOffset; + public int TextOffset; - public string VarFallback; + public string VarFallback; - public string VarTransform; + public string VarTransform; - public ValueTask Var; + public ValueTask Var; - public static TextPart Text(int offset, int length) - { - var result = default(TextPart); + public static TextPart Text(int offset, int length) + { + var result = default(TextPart); - result.TextOffset = offset; - result.TextLength = length; - result.IsText = true; + result.TextOffset = offset; + result.TextLength = length; + result.IsText = true; - return result; - } + return result; + } - public static TextPart Variable(ValueTask replacement, string fallback, string transform) - { - var result = default(TextPart); + public static TextPart Variable(ValueTask replacement, string fallback, string transform) + { + var result = default(TextPart); - result.Var = replacement; - result.VarFallback = fallback; - result.VarTransform = transform; + result.Var = replacement; + result.VarFallback = fallback; + result.VarTransform = transform; - return result; - } + return result; } + } - public RuleEventFormatter(IJsonSerializer serializer, IEnumerable formatters, ITemplateEngine templateEngine, IScriptEngine scriptEngine) - { - this.serializer = serializer; - this.formatters = formatters; - this.templateEngine = templateEngine; - this.scriptEngine = scriptEngine; - } + public RuleEventFormatter(IJsonSerializer serializer, IEnumerable formatters, ITemplateEngine templateEngine, IScriptEngine scriptEngine) + { + this.serializer = serializer; + this.formatters = formatters; + this.templateEngine = templateEngine; + this.scriptEngine = scriptEngine; + } - public virtual string ToPayload(T @event) where T : notnull - { - // Just serialize the payload. - return serializer.Serialize((object)@event, true); - } + public virtual string ToPayload(T @event) where T : notnull + { + // Just serialize the payload. + return serializer.Serialize((object)@event, true); + } - public virtual string ToEnvelope(EnrichedEvent @event) - { - // Use the overloard with object to serialize a concrete type. - return ToEnvelope(@event.Name, @event, @event.Timestamp); - } + public virtual string ToEnvelope(EnrichedEvent @event) + { + // Use the overloard with object to serialize a concrete type. + return ToEnvelope(@event.Name, @event, @event.Timestamp); + } - public virtual string ToEnvelope(string type, object payload, Instant timestamp) + public virtual string ToEnvelope(string type, object payload, Instant timestamp) + { + // Provide this overload with object to serialize the derived type and not the static type. + return serializer.Serialize(new { type, payload, timestamp }, true); + } + + public async ValueTask FormatAsync(string text, EnrichedEvent @event) + { + if (string.IsNullOrWhiteSpace(text)) { - // Provide this overload with object to serialize the derived type and not the static type. - return serializer.Serialize(new { type, payload, timestamp }, true); + return text; } - public async ValueTask FormatAsync(string text, EnrichedEvent @event) + if (TryGetTemplate(text.Trim(), out var template)) { - if (string.IsNullOrWhiteSpace(text)) + var vars = new TemplateVars { - return text; - } - - if (TryGetTemplate(text.Trim(), out var template)) - { - var vars = new TemplateVars - { - ["event"] = @event - }; + ["event"] = @event + }; - return await templateEngine.RenderAsync(template, vars); - } + return await templateEngine.RenderAsync(template, vars); + } - if (TryGetScript(text.Trim(), out var script)) + if (TryGetScript(text.Trim(), out var script)) + { + // Script vars are just wrappers over dictionaries for better performance. + var vars = new EventScriptVars { - // Script vars are just wrappers over dictionaries for better performance. - var vars = new EventScriptVars - { - Event = @event, - AppId = @event.AppId.Id, - AppName = @event.AppId.Name, - User = Admin() - }; + Event = @event, + AppId = @event.AppId.Id, + AppName = @event.AppId.Name, + User = Admin() + }; - var result = (await scriptEngine.ExecuteAsync(vars, script)).ToString(); + var result = (await scriptEngine.ExecuteAsync(vars, script)).ToString(); - if (result == "undefined") - { - return GlobalFallback; - } - - return result; - } - - var parts = BuildParts(text, @event); - - if (parts.Any(x => !x.Var.IsCompleted)) + if (result == "undefined") { - await ValueTaskEx.WhenAll(parts.Select(x => x.Var)); + return GlobalFallback; } - return CombineParts(text, parts); + return result; } - private static ClaimsPrincipal Admin() + var parts = BuildParts(text, @event); + + if (parts.Any(x => !x.Var.IsCompleted)) { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + await ValueTaskEx.WhenAll(parts.Select(x => x.Var)); + } - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.All)); + return CombineParts(text, parts); + } - return claimsPrincipal; - } + private static ClaimsPrincipal Admin() + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - private static string CombineParts(string text, List parts) - { - var span = text.AsSpan(); + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.All)); - var sb = new StringBuilder(); + return claimsPrincipal; + } - foreach (var part in parts) - { - if (!part.IsText) - { - var result = part.Var.Result; + private static string CombineParts(string text, List parts) + { + var span = text.AsSpan(); - result = TransformText(result, part.VarTransform); + var sb = new StringBuilder(); - if (result == null) - { - result = part.VarFallback; - } + foreach (var part in parts) + { + if (!part.IsText) + { + var result = part.Var.Result; - if (string.IsNullOrEmpty(result)) - { - result = GlobalFallback; - } + result = TransformText(result, part.VarTransform); - sb.Append(result); + if (result == null) + { + result = part.VarFallback; } - else + + if (string.IsNullOrEmpty(result)) { - sb.Append(span.Slice(part.TextOffset, part.TextLength)); + result = GlobalFallback; } - } - return sb.ToString(); + sb.Append(result); + } + else + { + sb.Append(span.Slice(part.TextOffset, part.TextLength)); + } } - private List BuildParts(string text, EnrichedEvent @event) - { - var parts = new List(); + return sb.ToString(); + } - var span = text.AsSpan(); + private List BuildParts(string text, EnrichedEvent @event) + { + var parts = new List(); - var currentOffset = 0; + var span = text.AsSpan(); - for (var i = 0; i < text.Length; i++) - { - var c = text[i]; + var currentOffset = 0; - if (c == '$') - { - parts.Add(TextPart.Text(currentOffset, i - currentOffset)); + for (var i = 0; i < text.Length; i++) + { + var c = text[i]; - var (length, part) = GetReplacement(span[(i + 1)..].ToString(), @event); + if (c == '$') + { + parts.Add(TextPart.Text(currentOffset, i - currentOffset)); - if (length > 0) - { - parts.Add(part); + var (length, part) = GetReplacement(span[(i + 1)..].ToString(), @event); - i += length + 1; - } + if (length > 0) + { + parts.Add(part); - currentOffset = i; + i += length + 1; } + + currentOffset = i; } + } - parts.Add(TextPart.Text(currentOffset, text.Length - currentOffset)); + parts.Add(TextPart.Text(currentOffset, text.Length - currentOffset)); - return parts; - } + return parts; + } + + private (int Length, TextPart Part) GetReplacement(string test, EnrichedEvent @event) + { + var (isNewRegex, match) = Match(test); - private (int Length, TextPart Part) GetReplacement(string test, EnrichedEvent @event) + if (match.Success) { - var (isNewRegex, match) = Match(test); + var (length, replacement) = ResolveOldPatterns(match, isNewRegex, @event); - if (match.Success) + if (length == 0) { - var (length, replacement) = ResolveOldPatterns(match, isNewRegex, @event); - - if (length == 0) - { - (length, replacement) = ResolveFromPath(match, @event); - } - - return (length, TextPart.Variable(replacement, match.Groups["Fallback"].Value, match.Groups["Transform"].Value)); + (length, replacement) = ResolveFromPath(match, @event); } - return default; + return (length, TextPart.Variable(replacement, match.Groups["Fallback"].Value, match.Groups["Transform"].Value)); } - private static (bool IsNew, Match) Match(string test) - { - var match = RegexPatternNew.Match(test); + return default; + } - if (match.Success) - { - return (true, match); - } + private static (bool IsNew, Match) Match(string test) + { + var match = RegexPatternNew.Match(test); - return (false, RegexPatternOld.Match(test)); + if (match.Success) + { + return (true, match); } - private (int Length, ValueTask Result) ResolveOldPatterns(Match match, bool isNewRegex, EnrichedEvent @event) + return (false, RegexPatternOld.Match(test)); + } + + private (int Length, ValueTask Result) ResolveOldPatterns(Match match, bool isNewRegex, EnrichedEvent @event) + { + var fullPath = match.Groups["FullPath"].Value; + + foreach (var formatter in formatters) { - var fullPath = match.Groups["FullPath"].Value; + var (replaced, result, replacedLength) = formatter.Format(@event, fullPath); - foreach (var formatter in formatters) + if (replaced) { - var (replaced, result, replacedLength) = formatter.Format(@event, fullPath); - - if (replaced) + if (isNewRegex) { - if (isNewRegex) - { - replacedLength = match.Length; - } - - return (replacedLength, new ValueTask(result)); + replacedLength = match.Length; } - } - return default; + return (replacedLength, new ValueTask(result)); + } } - private static string? TransformText(string? text, string? transform) + return default; + } + + private static string? TransformText(string? text, string? transform) + { + if (text != null && !string.IsNullOrWhiteSpace(transform)) { - if (text != null && !string.IsNullOrWhiteSpace(transform)) - { - var transformations = transform.Split("|", StringSplitOptions.RemoveEmptyEntries); + var transformations = transform.Split("|", StringSplitOptions.RemoveEmptyEntries); - foreach (var transformation in transformations) + foreach (var transformation in transformations) + { + switch (transformation.Trim().ToLowerInvariant()) { - switch (transformation.Trim().ToLowerInvariant()) - { - case "lower": - text = text.ToLowerInvariant(); - break; - case "upper": - text = text.ToUpperInvariant(); - break; - case "escape": - text = text.JsonEscape(); - break; - case "slugify": - text = text.Slugify(); - break; - case "trim": - text = text.Trim(); - break; - case "timestamp": + case "lower": + text = text.ToLowerInvariant(); + break; + case "upper": + text = text.ToUpperInvariant(); + break; + case "escape": + text = text.JsonEscape(); + break; + case "slugify": + text = text.Slugify(); + break; + case "trim": + text = text.Trim(); + break; + case "timestamp": + { + var instant = InstantPattern.ExtendedIso.Parse(text); + + if (instant.Success) { - var instant = InstantPattern.ExtendedIso.Parse(text); - - if (instant.Success) - { - text = instant.Value.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture); - } - - break; + text = instant.Value.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture); } - case "timestamp_sec": - { - var instant = InstantPattern.ExtendedIso.Parse(text); + break; + } - if (instant.Success) - { - text = instant.Value.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); - } + case "timestamp_sec": + { + var instant = InstantPattern.ExtendedIso.Parse(text); - break; + if (instant.Success) + { + text = instant.Value.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); } - } + + break; + } } } - - return text; } - private (int Length, ValueTask Result) ResolveFromPath(Match match, EnrichedEvent @event) - { - var path = match.Groups["Path"].Value.Split('.', StringSplitOptions.RemoveEmptyEntries); + return text; + } - var (result, remaining) = RuleVariable.GetValue(@event, path); + private (int Length, ValueTask Result) ResolveFromPath(Match match, EnrichedEvent @event) + { + var path = match.Groups["Path"].Value.Split('.', StringSplitOptions.RemoveEmptyEntries); + + var (result, remaining) = RuleVariable.GetValue(@event, path); - if (remaining.Length > 0 && result != null) + if (remaining.Length > 0 && result != null) + { + foreach (var formatter in formatters) { - foreach (var formatter in formatters) - { - var (replaced, result2) = formatter.Format(@event, result, remaining); + var (replaced, result2) = formatter.Format(@event, result, remaining); - if (replaced) - { - return (match.Length, result2); - } + if (replaced) + { + return (match.Length, result2); } } - else if (remaining.Length == 0) - { - return (match.Length, new ValueTask(result?.ToString())); - } - - return (match.Length, default); } - - private static bool TryGetScript(string text, out string script) + else if (remaining.Length == 0) { - const string ScriptSuffix = ")"; - const string ScriptPrefix = "Script("; + return (match.Length, new ValueTask(result?.ToString())); + } - script = null!; + return (match.Length, default); + } - const StringComparison comparer = StringComparison.OrdinalIgnoreCase; + private static bool TryGetScript(string text, out string script) + { + const string ScriptSuffix = ")"; + const string ScriptPrefix = "Script("; - if (text.StartsWith(ScriptPrefix, comparer) && text.EndsWith(ScriptSuffix, comparer)) - { - script = text.Substring(ScriptPrefix.Length, text.Length - ScriptPrefix.Length - ScriptSuffix.Length); - return true; - } + script = null!; - return false; - } + const StringComparison comparer = StringComparison.OrdinalIgnoreCase; - private static bool TryGetTemplate(string text, out string script) + if (text.StartsWith(ScriptPrefix, comparer) && text.EndsWith(ScriptSuffix, comparer)) { - const string TemplateSuffix = ")"; - const string TemplatePrefix = "Liquid("; + script = text.Substring(ScriptPrefix.Length, text.Length - ScriptPrefix.Length - ScriptSuffix.Length); + return true; + } - script = null!; + return false; + } - const StringComparison comparer = StringComparison.OrdinalIgnoreCase; + private static bool TryGetTemplate(string text, out string script) + { + const string TemplateSuffix = ")"; + const string TemplatePrefix = "Liquid("; - if (text.StartsWith(TemplatePrefix, comparer) && text.EndsWith(TemplateSuffix, comparer)) - { - script = text.Substring(TemplatePrefix.Length, text.Length - TemplatePrefix.Length - TemplateSuffix.Length); - return true; - } + script = null!; + + const StringComparison comparer = StringComparison.OrdinalIgnoreCase; - return false; + if (text.StartsWith(TemplatePrefix, comparer) && text.EndsWith(TemplateSuffix, comparer)) + { + script = text.Substring(TemplatePrefix.Length, text.Length - TemplatePrefix.Length - TemplateSuffix.Length); + return true; } + + return false; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleFieldEditor.cs index 92f38840e4..8ceedb60c7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleFieldEditor.cs @@ -5,18 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public enum RuleFieldEditor { - public enum RuleFieldEditor - { - Checkbox, - Dropdown, - Email, - Javascript, - Number, - Password, - Text, - TextArea, - Url - } + Checkbox, + Dropdown, + Email, + Javascript, + Number, + Password, + Text, + TextArea, + Url } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs index 15211f0acd..f7716b1515 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleOptions.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class RuleOptions { - public sealed class RuleOptions - { - public int ExecutionTimeoutInSeconds { get; set; } = 3; + public int ExecutionTimeoutInSeconds { get; set; } = 3; - public TimeSpan RulesCacheDuration { get; set; } = TimeSpan.FromSeconds(10); - } + public TimeSpan RulesCacheDuration { get; set; } = TimeSpan.FromSeconds(10); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs index beab551d28..ffe6c439b6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public enum RuleResult { - public enum RuleResult - { - Pending, - Success, - Failed, - Timeout - } + Pending, + Success, + Failed, + Timeout } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 49f2d33f5c..42801fcea8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -18,375 +18,374 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Tasks; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class RuleService : IRuleService { - public sealed class RuleService : IRuleService + private readonly Dictionary ruleActionHandlers; + private readonly Dictionary ruleTriggerHandlers; + private readonly TypeNameRegistry typeNameRegistry; + private readonly RuleOptions ruleOptions; + private readonly IEventEnricher eventEnricher; + private readonly IJsonSerializer serializer; + private readonly ILogger log; + + public IClock Clock { get; set; } = SystemClock.Instance; + + public RuleService( + IOptions ruleOptions, + IEnumerable ruleTriggerHandlers, + IEnumerable ruleActionHandlers, + IEventEnricher eventEnricher, + IJsonSerializer serializer, + ILogger log, + TypeNameRegistry typeNameRegistry) + { + this.typeNameRegistry = typeNameRegistry; + this.eventEnricher = eventEnricher; + this.ruleOptions = ruleOptions.Value; + this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); + this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); + this.serializer = serializer; + this.log = log; + } + + public bool CanCreateSnapshotEvents(RuleContext context) { - private readonly Dictionary ruleActionHandlers; - private readonly Dictionary ruleTriggerHandlers; - private readonly TypeNameRegistry typeNameRegistry; - private readonly RuleOptions ruleOptions; - private readonly IEventEnricher eventEnricher; - private readonly IJsonSerializer serializer; - private readonly ILogger log; - - public IClock Clock { get; set; } = SystemClock.Instance; - - public RuleService( - IOptions ruleOptions, - IEnumerable ruleTriggerHandlers, - IEnumerable ruleActionHandlers, - IEventEnricher eventEnricher, - IJsonSerializer serializer, - ILogger log, - TypeNameRegistry typeNameRegistry) + Guard.NotNull(context.Rule, nameof(context.Rule)); + + if (!ruleTriggerHandlers.TryGetValue(context.Rule.Trigger.GetType(), out var triggerHandler)) { - this.typeNameRegistry = typeNameRegistry; - this.eventEnricher = eventEnricher; - this.ruleOptions = ruleOptions.Value; - this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); - this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); - this.serializer = serializer; - this.log = log; + return false; } - public bool CanCreateSnapshotEvents(RuleContext context) + return triggerHandler.CanCreateSnapshotEvents; + } + + public async IAsyncEnumerable CreateSnapshotJobsAsync(RuleContext context, + [EnumeratorCancellation] CancellationToken ct = default) + { + Guard.NotNull(context.Rule, nameof(context.Rule)); + + var rule = context.Rule; + + if (!rule.IsEnabled && !context.IncludeSkipped) { - Guard.NotNull(context.Rule, nameof(context.Rule)); + yield break; + } - if (!ruleTriggerHandlers.TryGetValue(context.Rule.Trigger.GetType(), out var triggerHandler)) - { - return false; - } + if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) + { + yield break; + } - return triggerHandler.CanCreateSnapshotEvents; + if (!ruleActionHandlers.TryGetValue(rule.Action.GetType(), out var actionHandler)) + { + yield break; } - public async IAsyncEnumerable CreateSnapshotJobsAsync(RuleContext context, - [EnumeratorCancellation] CancellationToken ct = default) + if (!triggerHandler.CanCreateSnapshotEvents) { - Guard.NotNull(context.Rule, nameof(context.Rule)); + yield break; + } - var rule = context.Rule; + var now = Clock.GetCurrentInstant(); - if (!rule.IsEnabled && !context.IncludeSkipped) + await foreach (var enrichedEvent in triggerHandler.CreateSnapshotEventsAsync(context, ct)) + { + JobResult? job; + try { - yield break; - } + await eventEnricher.EnrichAsync(enrichedEvent, null); - if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) - { - yield break; - } + if (!triggerHandler.Trigger(enrichedEvent, context)) + { + continue; + } - if (!ruleActionHandlers.TryGetValue(rule.Action.GetType(), out var actionHandler)) - { - yield break; + job = await CreateJobAsync(actionHandler, enrichedEvent, context, now); } - - if (!triggerHandler.CanCreateSnapshotEvents) + catch (Exception ex) { - yield break; + job = JobResult.Failed(ex); } - var now = Clock.GetCurrentInstant(); + yield return job; + } + } - await foreach (var enrichedEvent in triggerHandler.CreateSnapshotEventsAsync(context, ct)) - { - JobResult? job; - try - { - await eventEnricher.EnrichAsync(enrichedEvent, null); + public async IAsyncEnumerable CreateJobsAsync(Envelope @event, RuleContext context, + [EnumeratorCancellation] CancellationToken ct = default) + { + Guard.NotNull(@event, nameof(@event)); - if (!triggerHandler.Trigger(enrichedEvent, context)) - { - continue; - } + var jobs = new List(); - job = await CreateJobAsync(actionHandler, enrichedEvent, context, now); - } - catch (Exception ex) - { - job = JobResult.Failed(ex); - } + await AddJobsAsync(jobs, @event, context, ct); - yield return job; + foreach (var job in jobs) + { + if (ct.IsCancellationRequested) + { + break; } + + yield return job; } + } - public async IAsyncEnumerable CreateJobsAsync(Envelope @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct = default) + private async Task AddJobsAsync(List jobs, Envelope @event, RuleContext context, + CancellationToken ct) + { + try { - Guard.NotNull(@event, nameof(@event)); + var skipReason = SkipReason.None; - var jobs = new List(); - - await AddJobsAsync(jobs, @event, context, ct); + var rule = context.Rule; - foreach (var job in jobs) + if (!rule.IsEnabled) { - if (ct.IsCancellationRequested) + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) { - break; + skipReason |= SkipReason.Disabled; + } + else + { + jobs.Add(JobResult.Disabled); + return; } - - yield return job; } - } - private async Task AddJobsAsync(List jobs, Envelope @event, RuleContext context, - CancellationToken ct) - { - try + if (@event.Payload is not AppEvent) { - var skipReason = SkipReason.None; + jobs.Add(JobResult.WrongEvent); + return; + } - var rule = context.Rule; + var typed = @event.To(); - if (!rule.IsEnabled) + if (typed.Payload.FromRule) + { + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) { - // For the simulation we want to proceed as much as possible. - if (context.IncludeSkipped) - { - skipReason |= SkipReason.Disabled; - } - else - { - jobs.Add(JobResult.Disabled); - return; - } + skipReason |= SkipReason.FromRule; } - - if (@event.Payload is not AppEvent) + else { - jobs.Add(JobResult.WrongEvent); + jobs.Add(JobResult.FromRule); return; } + } - var typed = @event.To(); + var actionType = rule.Action.GetType(); - if (typed.Payload.FromRule) - { - // For the simulation we want to proceed as much as possible. - if (context.IncludeSkipped) - { - skipReason |= SkipReason.FromRule; - } - else - { - jobs.Add(JobResult.FromRule); - return; - } - } + if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) + { + jobs.Add(JobResult.NoTrigger); + return; + } + + if (!triggerHandler.Handles(typed.Payload)) + { + jobs.Add(JobResult.WrongEventForTrigger); + return; + } + + if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) + { + jobs.Add(JobResult.NoAction); + return; + } - var actionType = rule.Action.GetType(); + var now = Clock.GetCurrentInstant(); - if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) + var eventTime = + @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? + @event.Headers.Timestamp() : + now; + + if (!context.IncludeStale && eventTime.Plus(Constants.StaleTime) < now) + { + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) { - jobs.Add(JobResult.NoTrigger); - return; + skipReason |= SkipReason.TooOld; } - - if (!triggerHandler.Handles(typed.Payload)) + else { - jobs.Add(JobResult.WrongEventForTrigger); + jobs.Add(JobResult.TooOld); return; } + } - if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) + if (!triggerHandler.Trigger(typed, context)) + { + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) { - jobs.Add(JobResult.NoAction); - return; + skipReason |= SkipReason.ConditionPrecheckDoesNotMatch; } - - var now = Clock.GetCurrentInstant(); - - var eventTime = - @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? - @event.Headers.Timestamp() : - now; - - if (!context.IncludeStale && eventTime.Plus(Constants.StaleTime) < now) + else { - // For the simulation we want to proceed as much as possible. - if (context.IncludeSkipped) - { - skipReason |= SkipReason.TooOld; - } - else - { - jobs.Add(JobResult.TooOld); - return; - } + jobs.Add(JobResult.ConditionPrecheckDoesNotMatch); + return; } + } - if (!triggerHandler.Trigger(typed, context)) + await foreach (var enrichedEvent in triggerHandler.CreateEnrichedEventsAsync(typed, context, ct)) + { + if (string.IsNullOrWhiteSpace(enrichedEvent.Name)) { - // For the simulation we want to proceed as much as possible. - if (context.IncludeSkipped) - { - skipReason |= SkipReason.ConditionPrecheckDoesNotMatch; - } - else - { - jobs.Add(JobResult.ConditionPrecheckDoesNotMatch); - return; - } + enrichedEvent.Name = GetName(typed.Payload); } - await foreach (var enrichedEvent in triggerHandler.CreateEnrichedEventsAsync(typed, context, ct)) + try { - if (string.IsNullOrWhiteSpace(enrichedEvent.Name)) - { - enrichedEvent.Name = GetName(typed.Payload); - } + await eventEnricher.EnrichAsync(enrichedEvent, typed); - try + if (!triggerHandler.Trigger(enrichedEvent, context)) { - await eventEnricher.EnrichAsync(enrichedEvent, typed); - - if (!triggerHandler.Trigger(enrichedEvent, context)) + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) { - // For the simulation we want to proceed as much as possible. - if (context.IncludeSkipped) - { - skipReason |= SkipReason.ConditionDoesNotMatch; - } - else - { - jobs.Add(JobResult.ConditionDoesNotMatch); - return; - } + skipReason |= SkipReason.ConditionDoesNotMatch; } - - var job = await CreateJobAsync(actionHandler, enrichedEvent, context, now); - - // If the conditions matchs, we can skip creating a new object and save a few allocation.s - if (skipReason != SkipReason.None) + else { - job = job with { SkipReason = skipReason }; + jobs.Add(JobResult.ConditionDoesNotMatch); + return; } + } + + var job = await CreateJobAsync(actionHandler, enrichedEvent, context, now); - jobs.Add(job); + // If the conditions matchs, we can skip creating a new object and save a few allocation.s + if (skipReason != SkipReason.None) + { + job = job with { SkipReason = skipReason }; } - catch (Exception ex) + + jobs.Add(job); + } + catch (Exception ex) + { + if (jobs.Count == 0) { - if (jobs.Count == 0) + jobs.Add(new JobResult { - jobs.Add(new JobResult - { - EnrichedEvent = enrichedEvent, - EnrichmentError = ex, - SkipReason = SkipReason.Failed - }); - } - - log.LogError(ex, "Failed to create rule jobs from event."); + EnrichedEvent = enrichedEvent, + EnrichmentError = ex, + SkipReason = SkipReason.Failed + }); } - } - } - catch (Exception ex) - { - jobs.Add(JobResult.Failed(ex)); - log.LogError(ex, "Failed to create rule job."); + log.LogError(ex, "Failed to create rule jobs from event."); + } } } - - private async Task CreateJobAsync(IRuleActionHandler actionHandler, EnrichedEvent enrichedEvent, RuleContext context, Instant now) + catch (Exception ex) { - var actionName = typeNameRegistry.GetName(context.Rule.Action.GetType()); + jobs.Add(JobResult.Failed(ex)); - var expires = now.Plus(Constants.ExpirationTime); + log.LogError(ex, "Failed to create rule job."); + } + } - var job = new RuleJob - { - Id = DomainId.NewGuid(), - ActionData = string.Empty, - ActionName = actionName, - AppId = enrichedEvent.AppId.Id, - Created = now, - EventName = enrichedEvent.Name, - ExecutionPartition = enrichedEvent.Partition, - Expires = expires, - RuleId = context.RuleId - }; + private async Task CreateJobAsync(IRuleActionHandler actionHandler, EnrichedEvent enrichedEvent, RuleContext context, Instant now) + { + var actionName = typeNameRegistry.GetName(context.Rule.Action.GetType()); - try - { - var (description, data) = await actionHandler.CreateJobAsync(enrichedEvent, context.Rule.Action); + var expires = now.Plus(Constants.ExpirationTime); - var json = serializer.Serialize(data); + var job = new RuleJob + { + Id = DomainId.NewGuid(), + ActionData = string.Empty, + ActionName = actionName, + AppId = enrichedEvent.AppId.Id, + Created = now, + EventName = enrichedEvent.Name, + ExecutionPartition = enrichedEvent.Partition, + Expires = expires, + RuleId = context.RuleId + }; + + try + { + var (description, data) = await actionHandler.CreateJobAsync(enrichedEvent, context.Rule.Action); - job.ActionData = json; - job.ActionName = actionName; - job.Description = description; + var json = serializer.Serialize(data); - return new JobResult { Job = job, EnrichedEvent = enrichedEvent }; - } - catch (Exception ex) - { - job.Description = "Failed to create job"; + job.ActionData = json; + job.ActionName = actionName; + job.Description = description; - return JobResult.Failed(ex, enrichedEvent, job); - } + return new JobResult { Job = job, EnrichedEvent = enrichedEvent }; } + catch (Exception ex) + { + job.Description = "Failed to create job"; + + return JobResult.Failed(ex, enrichedEvent, job); + } + } - public string GetName(AppEvent @event) + public string GetName(AppEvent @event) + { + foreach (var (_, handler) in ruleTriggerHandlers) { - foreach (var (_, handler) in ruleTriggerHandlers) + if (handler.Handles(@event)) { - if (handler.Handles(@event)) - { - var name = handler.GetName(@event); + var name = handler.GetName(@event); - if (!string.IsNullOrWhiteSpace(name)) - { - return name; - } + if (!string.IsNullOrWhiteSpace(name)) + { + return name; } } - - return @event.GetType().Name; } - public async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job, - CancellationToken ct = default) - { - var actionWatch = ValueStopwatch.StartNew(); + return @event.GetType().Name; + } - Result result; + public async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job, + CancellationToken ct = default) + { + var actionWatch = ValueStopwatch.StartNew(); - try - { - var actionType = typeNameRegistry.GetType(actionName); - var actionHandler = ruleActionHandlers[actionType]; + Result result; - var deserialized = serializer.Deserialize(job, actionHandler.DataType); + try + { + var actionType = typeNameRegistry.GetType(actionName); + var actionHandler = ruleActionHandlers[actionType]; - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a timeout after a configured time span. - combined.CancelAfter(GetTimeoutInMs()); + var deserialized = serializer.Deserialize(job, actionHandler.DataType); - result = await actionHandler.ExecuteJobAsync(deserialized, combined.Token).WithCancellation(combined.Token); - } - } - catch (Exception ex) + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - result = Result.Failed(ex); + // Enforce a timeout after a configured time span. + combined.CancelAfter(GetTimeoutInMs()); + + result = await actionHandler.ExecuteJobAsync(deserialized, combined.Token).WithCancellation(combined.Token); } + } + catch (Exception ex) + { + result = Result.Failed(ex); + } - var elapsed = TimeSpan.FromMilliseconds(actionWatch.Stop()); + var elapsed = TimeSpan.FromMilliseconds(actionWatch.Stop()); - result.Enrich(elapsed); + result.Enrich(elapsed); - return (result, elapsed); - } + return (result, elapsed); + } - private int GetTimeoutInMs() - { - return ruleOptions.ExecutionTimeoutInSeconds * 1000; - } + private int GetTimeoutInMs() + { + return ruleOptions.ExecutionTimeoutInSeconds * 1000; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs index b3339b39d0..538968ef1c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs @@ -14,226 +14,225 @@ #pragma warning disable RECS0033 // Convert 'if' to '||' expression -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public sealed class RuleTypeProvider : ITypeProvider { - public sealed class RuleTypeProvider : ITypeProvider - { - private const string ActionSuffix = "Action"; - private const string ActionSuffixV2 = "ActionV2"; - private readonly Dictionary actionTypes = new Dictionary(); + private const string ActionSuffix = "Action"; + private const string ActionSuffixV2 = "ActionV2"; + private readonly Dictionary actionTypes = new Dictionary(); - public IReadOnlyDictionary Actions - { - get => actionTypes; - } + public IReadOnlyDictionary Actions + { + get => actionTypes; + } - public RuleTypeProvider(IEnumerable? registrations = null) + public RuleTypeProvider(IEnumerable? registrations = null) + { + if (registrations != null) { - if (registrations != null) + foreach (var registration in registrations) { - foreach (var registration in registrations) - { - Add(registration.ActionType); - } + Add(registration.ActionType); } } + } + + public void Add() where T : RuleAction + { + Add(typeof(T)); + } - public void Add() where T : RuleAction + private void Add(Type actionType) + { + var metadata = actionType.GetCustomAttribute(); + + if (metadata == null) { - Add(typeof(T)); + return; } - private void Add(Type actionType) - { - var metadata = actionType.GetCustomAttribute(); + var name = GetActionName(actionType); - if (metadata == null) + var definition = + new RuleActionDefinition { - return; - } + Type = actionType, + Title = metadata.Title, + Display = metadata.Display, + Description = metadata.Description, + IconColor = metadata.IconColor, + IconImage = metadata.IconImage, + ReadMore = metadata.ReadMore + }; + + foreach (var property in actionType.GetProperties()) + { + if (property.CanRead && property.CanWrite) + { + var actionProperty = new RuleActionProperty + { + Name = property.Name.ToCamelCase() + }; - var name = GetActionName(actionType); + var display = property.GetCustomAttribute(); - var definition = - new RuleActionDefinition + if (!string.IsNullOrWhiteSpace(display?.Name)) { - Type = actionType, - Title = metadata.Title, - Display = metadata.Display, - Description = metadata.Description, - IconColor = metadata.IconColor, - IconImage = metadata.IconImage, - ReadMore = metadata.ReadMore - }; + actionProperty.Display = display.Name; + } + else + { + actionProperty.Display = property.Name; + } - foreach (var property in actionType.GetProperties()) - { - if (property.CanRead && property.CanWrite) + if (!string.IsNullOrWhiteSpace(display?.Description)) { - var actionProperty = new RuleActionProperty - { - Name = property.Name.ToCamelCase() - }; + actionProperty.Description = display.Description; + } - var display = property.GetCustomAttribute(); + var type = GetType(property); - if (!string.IsNullOrWhiteSpace(display?.Name)) - { - actionProperty.Display = display.Name; - } - else + if (!IsNullable(property.PropertyType)) + { + if (GetDataAttribute(property) != null) { - actionProperty.Display = property.Name; + actionProperty.IsRequired = true; } - if (!string.IsNullOrWhiteSpace(display?.Description)) + if (type.IsValueType && !IsBoolean(type) && !type.IsEnum) { - actionProperty.Description = display.Description; + actionProperty.IsRequired = true; } + } - var type = GetType(property); + if (property.GetCustomAttribute() != null) + { + actionProperty.IsFormattable = true; + } - if (!IsNullable(property.PropertyType)) - { - if (GetDataAttribute(property) != null) - { - actionProperty.IsRequired = true; - } - - if (type.IsValueType && !IsBoolean(type) && !type.IsEnum) - { - actionProperty.IsRequired = true; - } - } + if (type.IsEnum) + { + var values = Enum.GetNames(type); - if (property.GetCustomAttribute() != null) - { - actionProperty.IsFormattable = true; - } + actionProperty.Options = values; + actionProperty.Editor = RuleFieldEditor.Dropdown; + } + else if (IsBoolean(type)) + { + actionProperty.Editor = RuleFieldEditor.Checkbox; + } + else if (IsNumericType(type)) + { + actionProperty.Editor = RuleFieldEditor.Number; + } + else + { + actionProperty.Editor = GetEditor(property); + } - if (type.IsEnum) - { - var values = Enum.GetNames(type); + definition.Properties.Add(actionProperty); + } + } - actionProperty.Options = values; - actionProperty.Editor = RuleFieldEditor.Dropdown; - } - else if (IsBoolean(type)) - { - actionProperty.Editor = RuleFieldEditor.Checkbox; - } - else if (IsNumericType(type)) - { - actionProperty.Editor = RuleFieldEditor.Number; - } - else - { - actionProperty.Editor = GetEditor(property); - } + actionTypes[name] = definition; + } - definition.Properties.Add(actionProperty); - } - } + private static T? GetDataAttribute(PropertyInfo property) where T : ValidationAttribute + { + var result = property.GetCustomAttribute(); - actionTypes[name] = definition; - } + result?.IsValid(null); - private static T? GetDataAttribute(PropertyInfo property) where T : ValidationAttribute - { - var result = property.GetCustomAttribute(); + return result; + } - result?.IsValid(null); + private static RuleFieldEditor GetEditor(PropertyInfo property) + { + return property.GetCustomAttribute()?.Editor ?? RuleFieldEditor.Text; + } - return result; - } + private static bool IsNullable(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } - private static RuleFieldEditor GetEditor(PropertyInfo property) + private static bool IsBoolean(Type type) + { + switch (Type.GetTypeCode(type)) { - return property.GetCustomAttribute()?.Editor ?? RuleFieldEditor.Text; + case TypeCode.Boolean: + return true; + default: + return false; } + } - private static bool IsNullable(Type type) + private static bool IsNumericType(Type type) + { + switch (Type.GetTypeCode(type)) { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + return true; + default: + return false; } + } - private static bool IsBoolean(Type type) - { - switch (Type.GetTypeCode(type)) - { - case TypeCode.Boolean: - return true; - default: - return false; - } - } + private static Type GetType(PropertyInfo property) + { + var type = property.PropertyType; - private static bool IsNumericType(Type type) + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { - switch (Type.GetTypeCode(type)) - { - case TypeCode.Byte: - case TypeCode.SByte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Single: - return true; - default: - return false; - } + type = type.GetGenericArguments()[0]; } - private static Type GetType(PropertyInfo property) - { - var type = property.PropertyType; + return type; + } - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - type = type.GetGenericArguments()[0]; - } + private static string GetActionName(Type type) + { + return type.TypeName(false, ActionSuffix, ActionSuffixV2); + } - return type; + public void Map(TypeNameRegistry typeNameRegistry) + { + foreach (var (_, actionType) in actionTypes) + { + typeNameRegistry.Map(actionType.Type, actionType.Type.Name); } - private static string GetActionName(Type type) + var addedTypes = new HashSet(); + + static IEnumerable FindTypes(Type baseType) { - return type.TypeName(false, ActionSuffix, ActionSuffixV2); + return baseType.Assembly.GetTypes().Where(x => baseType.IsAssignableFrom(x) && !x.IsAbstract); } - public void Map(TypeNameRegistry typeNameRegistry) + foreach (var type in FindTypes(typeof(EnrichedEvent))) { - foreach (var (_, actionType) in actionTypes) + if (addedTypes.Add(type)) { - typeNameRegistry.Map(actionType.Type, actionType.Type.Name); - } - - var addedTypes = new HashSet(); - - static IEnumerable FindTypes(Type baseType) - { - return baseType.Assembly.GetTypes().Where(x => baseType.IsAssignableFrom(x) && !x.IsAbstract); - } - - foreach (var type in FindTypes(typeof(EnrichedEvent))) - { - if (addedTypes.Add(type)) - { - typeNameRegistry.Map(type, type.Name); - } + typeNameRegistry.Map(type, type.Name); } + } - foreach (var type in FindTypes(typeof(RuleTrigger))) + foreach (var type in FindTypes(typeof(RuleTrigger))) + { + if (addedTypes.Add(type)) { - if (addedTypes.Add(type)) - { - typeNameRegistry.Map(type, type.Name); - } + typeNameRegistry.Map(type, type.Name); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleVariable.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleVariable.cs index 942cfbc37c..06e0ccae60 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleVariable.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleVariable.cs @@ -11,89 +11,88 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +public static class RuleVariable { - public static class RuleVariable + public static (object? Result, string[] Remaining) GetValue(object @event, string[] path) { - public static (object? Result, string[] Remaining) GetValue(object @event, string[] path) - { - object? current = @event; + object? current = @event; - var i = 0; + var i = 0; - for (; i < path.Length; i++) - { - var segment = path[i]; + for (; i < path.Length; i++) + { + var segment = path[i]; - if (current is ContentData data) + if (current is ContentData data) + { + if (!data.TryGetValue(segment, out var temp) || temp == null) { - if (!data.TryGetValue(segment, out var temp) || temp == null) - { - break; - } - - current = temp; + break; } - else if (current is ContentFieldData field) - { - if (!field.TryGetValue(segment, out var temp)) - { - break; - } - current = temp; - } - else if (current is JsonValue json) + current = temp; + } + else if (current is ContentFieldData field) + { + if (!field.TryGetValue(segment, out var temp)) { - if (!json.TryGetValue(segment, out var temp) || temp == JsonValue.Null) - { - break; - } - - current = temp; + break; } - else if (current != null) - { - if (current is IUser user) - { - var type = segment; - if (string.Equals(type, "Name", StringComparison.OrdinalIgnoreCase)) - { - type = SquidexClaimTypes.DisplayName; - } + current = temp; + } + else if (current is JsonValue json) + { + if (!json.TryGetValue(segment, out var temp) || temp == JsonValue.Null) + { + break; + } - var claim = user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); + current = temp; + } + else if (current != null) + { + if (current is IUser user) + { + var type = segment; - if (claim != null) - { - current = claim.Value; - continue; - } + if (string.Equals(type, "Name", StringComparison.OrdinalIgnoreCase)) + { + type = SquidexClaimTypes.DisplayName; } - const BindingFlags bindingFlags = - BindingFlags.FlattenHierarchy | - BindingFlags.Public | - BindingFlags.Instance; + var claim = user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); - var properties = current.GetType().GetProperties(bindingFlags); - var property = properties.FirstOrDefault(x => x.CanRead && string.Equals(x.Name, segment, StringComparison.OrdinalIgnoreCase)); - - if (property == null) + if (claim != null) { - break; + current = claim.Value; + continue; } - - current = property.GetValue(current); } - else + + const BindingFlags bindingFlags = + BindingFlags.FlattenHierarchy | + BindingFlags.Public | + BindingFlags.Instance; + + var properties = current.GetType().GetProperties(bindingFlags); + var property = properties.FirstOrDefault(x => x.CanRead && string.Equals(x.Name, segment, StringComparison.OrdinalIgnoreCase)); + + if (property == null) { break; } - } - return (current, path.Skip(i).ToArray()); + current = property.GetValue(current); + } + else + { + break; + } } + + return (current, path.Skip(i).ToArray()); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs index d1b6641311..98b7d1b892 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs @@ -5,21 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.HandleRules +namespace Squidex.Domain.Apps.Core.HandleRules; + +[Flags] +public enum SkipReason { - [Flags] - public enum SkipReason - { - None = 0, - ConditionDoesNotMatch = 1 << 0, - ConditionPrecheckDoesNotMatch = 1 << 1, - Disabled = 1 << 2, - Failed = 1 << 3, - FromRule = 1 << 4, - NoAction = 1 << 5, - NoTrigger = 1 << 6, - TooOld = 1 << 7, - WrongEvent = 1 << 8, - WrongEventForTrigger = 1 << 9 - } + None = 0, + ConditionDoesNotMatch = 1 << 0, + ConditionPrecheckDoesNotMatch = 1 << 1, + Disabled = 1 << 2, + Failed = 1 << 3, + FromRule = 1 << 4, + NoAction = 1 << 5, + NoTrigger = 1 << 6, + TooOld = 1 << 7, + WrongEvent = 1 << 8, + WrongEventForTrigger = 1 << 9 } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs index 72c0967645..d7be6f71bc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs @@ -8,58 +8,57 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +public interface IUrlGenerator { - public interface IUrlGenerator - { - string? AssetSource(NamedId appId, DomainId assetId, long fileVersion); + string? AssetSource(NamedId appId, DomainId assetId, long fileVersion); - string? AssetThumbnail(NamedId appId, string idOrSlug, AssetType assetType); + string? AssetThumbnail(NamedId appId, string idOrSlug, AssetType assetType); - string AssetsUI(NamedId appId, string? @ref = null); + string AssetsUI(NamedId appId, string? @ref = null); - string AssetContentCDNBase(); + string AssetContentCDNBase(); - string AssetContent(NamedId appId, string idOrSlug); + string AssetContent(NamedId appId, string idOrSlug); - string AssetContentBase(); + string AssetContentBase(); - string AssetContentBase(string appName); + string AssetContentBase(string appName); - string BackupsUI(NamedId appId); + string BackupsUI(NamedId appId); - string ClientsUI(NamedId appId); + string ClientsUI(NamedId appId); - string ContentCDNBase(); + string ContentCDNBase(); - string ContentBase(); + string ContentBase(); - string ContentsUI(NamedId appId, NamedId schemaId); + string ContentsUI(NamedId appId, NamedId schemaId); - string ContentUI(NamedId appId, NamedId schemaId, DomainId contentId); + string ContentUI(NamedId appId, NamedId schemaId, DomainId contentId); - string ContributorsUI(NamedId appId); + string ContributorsUI(NamedId appId); - string DashboardUI(NamedId appId); + string DashboardUI(NamedId appId); - string LanguagesUI(NamedId appId); + string LanguagesUI(NamedId appId); - string PatternsUI(NamedId appId); + string PatternsUI(NamedId appId); - string PlansUI(NamedId appId); + string PlansUI(NamedId appId); - string RolesUI(NamedId appId); + string RolesUI(NamedId appId); - string RulesUI(NamedId appId); + string RulesUI(NamedId appId); - string SchemasUI(NamedId appId); + string SchemasUI(NamedId appId); - string SchemaUI(NamedId appId, NamedId schemaId); + string SchemaUI(NamedId appId, NamedId schemaId); - string WorkflowsUI(NamedId appId); + string WorkflowsUI(NamedId appId); - string Root(); + string Root(); - string UI(); - } + string UI(); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs index 19db21a604..1c507d92ad 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs @@ -8,299 +8,298 @@ // //------------------------------------------------------------------------------ -namespace Squidex.Domain.Apps.Core.Properties { - using System; +namespace Squidex.Domain.Apps.Core.Properties; +using System; + + +/// +/// A strongly-typed resource class, for looking up localized strings, etc. +/// +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } /// - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Returns the cached ResourceManager instance used by this class. /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// - /// Looks up a localized string similar to The download URL to the asset.. - /// - internal static string ScriptingAssetContentAppUrl { - get { - return ResourceManager.GetString("ScriptingAssetContentAppUrl", resourceCulture); - } + set { + resourceCulture = value; } - - /// - /// Looks up a localized string similar to The download URL to the asset using the file slug instead of the ID.. - /// - internal static string ScriptingAssetContentSlugUrl { - get { - return ResourceManager.GetString("ScriptingAssetContentSlugUrl", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The download URL to the asset.. + /// + internal static string ScriptingAssetContentAppUrl { + get { + return ResourceManager.GetString("ScriptingAssetContentAppUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to The download URL to the asset without the app name (deprecated).. - /// - internal static string ScriptingAssetContentUrl { - get { - return ResourceManager.GetString("ScriptingAssetContentUrl", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The download URL to the asset using the file slug instead of the ID.. + /// + internal static string ScriptingAssetContentSlugUrl { + get { + return ResourceManager.GetString("ScriptingAssetContentSlugUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text.. - /// - internal static string ScriptingCharacterCount { - get { - return ResourceManager.GetString("ScriptingCharacterCount", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The download URL to the asset without the app name (deprecated).. + /// + internal static string ScriptingAssetContentUrl { + get { + return ResourceManager.GetString("ScriptingAssetContentUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to Completes the script when an async method is used.. - /// - internal static string ScriptingComplete { - get { - return ResourceManager.GetString("ScriptingComplete", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text.. + /// + internal static string ScriptingCharacterCount { + get { + return ResourceManager.GetString("ScriptingCharacterCount", resourceCulture); } - - /// - /// Looks up a localized string similar to The status of the content.. - /// - internal static string ScriptingContentAction { - get { - return ResourceManager.GetString("ScriptingContentAction", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Completes the script when an async method is used.. + /// + internal static string ScriptingComplete { + get { + return ResourceManager.GetString("ScriptingComplete", resourceCulture); } - - /// - /// Looks up a localized string similar to The URL to the content in the UI.. - /// - internal static string ScriptingContentUrl { - get { - return ResourceManager.GetString("ScriptingContentUrl", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The status of the content.. + /// + internal static string ScriptingContentAction { + get { + return ResourceManager.GetString("ScriptingContentAction", resourceCulture); } - - /// - /// Looks up a localized string similar to Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingDeleteJson { - get { - return ResourceManager.GetString("ScriptingDeleteJson", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The URL to the content in the UI.. + /// + internal static string ScriptingContentUrl { + get { + return ResourceManager.GetString("ScriptingContentUrl", resourceCulture); } - - /// - /// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 400 (BadRequest).. - /// - internal static string ScriptingDisallow { - get { - return ResourceManager.GetString("ScriptingDisallow", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingDeleteJson { + get { + return ResourceManager.GetString("ScriptingDeleteJson", resourceCulture); } - - /// - /// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern.. - /// - internal static string ScriptingFormatDate { - get { - return ResourceManager.GetString("ScriptingFormatDate", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 400 (BadRequest).. + /// + internal static string ScriptingDisallow { + get { + return ResourceManager.GetString("ScriptingDisallow", resourceCulture); } - - /// - /// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern.. - /// - internal static string ScriptingFormatTime { - get { - return ResourceManager.GetString("ScriptingFormatTime", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern.. + /// + internal static string ScriptingFormatDate { + get { + return ResourceManager.GetString("ScriptingFormatDate", resourceCulture); } - - /// - /// Looks up a localized string similar to Makes a GET request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingGetJSON { - get { - return ResourceManager.GetString("ScriptingGetJSON", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern.. + /// + internal static string ScriptingFormatTime { + get { + return ResourceManager.GetString("ScriptingFormatTime", resourceCulture); } - - /// - /// Looks up a localized string similar to Generates a guid.. - /// - internal static string ScriptingGuid { - get { - return ResourceManager.GetString("ScriptingGuid", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Makes a GET request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingGetJSON { + get { + return ResourceManager.GetString("ScriptingGetJSON", resourceCulture); } - - /// - /// Looks up a localized string similar to Converts a HTML string to plain text.. - /// - internal static string ScriptingHtml2Text { - get { - return ResourceManager.GetString("ScriptingHtml2Text", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Generates a guid.. + /// + internal static string ScriptingGuid { + get { + return ResourceManager.GetString("ScriptingGuid", resourceCulture); } - - /// - /// Looks up a localized string similar to Converts a markdown string to plain text.. - /// - internal static string ScriptingMarkdown2Text { - get { - return ResourceManager.GetString("ScriptingMarkdown2Text", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Converts a HTML string to plain text.. + /// + internal static string ScriptingHtml2Text { + get { + return ResourceManager.GetString("ScriptingHtml2Text", resourceCulture); } - - /// - /// Looks up a localized string similar to Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important.. - /// - internal static string ScriptingMD5 { - get { - return ResourceManager.GetString("ScriptingMD5", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Converts a markdown string to plain text.. + /// + internal static string ScriptingMarkdown2Text { + get { + return ResourceManager.GetString("ScriptingMarkdown2Text", resourceCulture); } - - /// - /// Looks up a localized string similar to Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingPatchJson { - get { - return ResourceManager.GetString("ScriptingPatchJson", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important.. + /// + internal static string ScriptingMD5 { + get { + return ResourceManager.GetString("ScriptingMD5", resourceCulture); } - - /// - /// Looks up a localized string similar to Makes a POST request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingPostJSON { - get { - return ResourceManager.GetString("ScriptingPostJSON", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingPatchJson { + get { + return ResourceManager.GetString("ScriptingPatchJson", resourceCulture); } - - /// - /// Looks up a localized string similar to Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingPutJson { - get { - return ResourceManager.GetString("ScriptingPutJson", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Makes a POST request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingPostJSON { + get { + return ResourceManager.GetString("ScriptingPostJSON", resourceCulture); } - - /// - /// Looks up a localized string similar to Tell Squidex to reject the current operation and to return a 403 (Forbidden).. - /// - internal static string ScriptingReject { - get { - return ResourceManager.GetString("ScriptingReject", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingPutJson { + get { + return ResourceManager.GetString("ScriptingPutJson", resourceCulture); } - - /// - /// Looks up a localized string similar to Tell Squidex that you have modified the data and that the change should be applied.. - /// - internal static string ScriptingReplace { - get { - return ResourceManager.GetString("ScriptingReplace", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Tell Squidex to reject the current operation and to return a 403 (Forbidden).. + /// + internal static string ScriptingReject { + get { + return ResourceManager.GetString("ScriptingReject", resourceCulture); } - - /// - /// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords.. - /// - internal static string ScriptingSHA256 { - get { - return ResourceManager.GetString("ScriptingSHA256", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Tell Squidex that you have modified the data and that the change should be applied.. + /// + internal static string ScriptingReplace { + get { + return ResourceManager.GetString("ScriptingReplace", resourceCulture); } - - /// - /// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords.. - /// - internal static string ScriptingSHA512 { - get { - return ResourceManager.GetString("ScriptingSHA512", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords.. + /// + internal static string ScriptingSHA256 { + get { + return ResourceManager.GetString("ScriptingSHA256", resourceCulture); } - - /// - /// Looks up a localized string similar to Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.. - /// - internal static string ScriptingSlugify { - get { - return ResourceManager.GetString("ScriptingSlugify", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords.. + /// + internal static string ScriptingSHA512 { + get { + return ResourceManager.GetString("ScriptingSHA512", resourceCulture); } - - /// - /// Looks up a localized string similar to Converts a text to camelCase.. - /// - internal static string ScriptingToCamelCase { - get { - return ResourceManager.GetString("ScriptingToCamelCase", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.. + /// + internal static string ScriptingSlugify { + get { + return ResourceManager.GetString("ScriptingSlugify", resourceCulture); } - - /// - /// Looks up a localized string similar to Converts a text to PascalCase. - /// - internal static string ScriptingToPascalCase { - get { - return ResourceManager.GetString("ScriptingToPascalCase", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Converts a text to camelCase.. + /// + internal static string ScriptingToCamelCase { + get { + return ResourceManager.GetString("ScriptingToCamelCase", resourceCulture); } - - /// - /// Looks up a localized string similar to Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.. - /// - internal static string ScriptingWordCount { - get { - return ResourceManager.GetString("ScriptingWordCount", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to Converts a text to PascalCase. + /// + internal static string ScriptingToPascalCase { + get { + return ResourceManager.GetString("ScriptingToPascalCase", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.. + /// + internal static string ScriptingWordCount { + get { + return ResourceManager.GetString("ScriptingWordCount", resourceCulture); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs index b50b53b72c..7405a9a14f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs @@ -9,74 +9,73 @@ using Squidex.Domain.Apps.Core.Scripting.Internal; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class AssetCommandScriptVars : ScriptVars { - public sealed class AssetCommandScriptVars : ScriptVars + [FieldDescription(nameof(FieldDescriptions.AssetParentId))] + public DomainId ParentId { - [FieldDescription(nameof(FieldDescriptions.AssetParentId))] - public DomainId ParentId - { - set => SetValue(value); - } + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetFileHash))] - public string? FileHash - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetFileHash))] + public string? FileHash + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetFileName))] - public string? FileName - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetFileName))] + public string? FileName + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetSlug))] - public string? FileSlug - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetSlug))] + public string? FileSlug + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetMimeType))] - public string? MimeType - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetMimeType))] + public string? MimeType + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetParentPath))] - public Array? ParentPath - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetParentPath))] + public Array? ParentPath + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] - public AssetMetadata? Metadata - { - set => SetValue(value != null ? new AssetMetadataWrapper(value) : null); - } + [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] + public AssetMetadata? Metadata + { + set => SetValue(value != null ? new AssetMetadataWrapper(value) : null); + } - [FieldDescription(nameof(FieldDescriptions.AssetTags))] - public HashSet? Tags - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetTags))] + public HashSet? Tags + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetFileSize))] - public long FileSize - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetFileSize))] + public long FileSize + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetIsProtected))] - public bool? IsProtected - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetIsProtected))] + public bool? IsProtected + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.EntityRequestDeletePermanent))] - public bool? Permanent - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.EntityRequestDeletePermanent))] + public bool? Permanent + { + set => SetValue(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs index de6c452231..b70516eca9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs @@ -10,74 +10,73 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class AssetEntityScriptVars : ScriptVars { - public sealed class AssetEntityScriptVars : ScriptVars + [FieldDescription(nameof(FieldDescriptions.AssetParentId))] + public DomainId ParentId { - [FieldDescription(nameof(FieldDescriptions.AssetParentId))] - public DomainId ParentId - { - set => SetValue(value); - } + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetFileHash))] - public string? FileHash - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetFileHash))] + public string? FileHash + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetFileName))] - public string? FileName - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetFileName))] + public string? FileName + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetSlug))] - public string? FileSlug - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetSlug))] + public string? FileSlug + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetMimeType))] - public string? MimeType - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetMimeType))] + public string? MimeType + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetParentPath))] - public Array? ParentPath - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetParentPath))] + public Array? ParentPath + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] - public AssetMetadata? Metadata - { - set => SetValue(value != null ? new ReadOnlyDictionary(value) : null); - } + [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] + public AssetMetadata? Metadata + { + set => SetValue(value != null ? new ReadOnlyDictionary(value) : null); + } - [FieldDescription(nameof(FieldDescriptions.AssetTags))] - public HashSet? Tags - { - set => SetValue(value != null ? new ReadOnlyCollection(value.ToList()) : null); - } + [FieldDescription(nameof(FieldDescriptions.AssetTags))] + public HashSet? Tags + { + set => SetValue(value != null ? new ReadOnlyCollection(value.ToList()) : null); + } - [FieldDescription(nameof(FieldDescriptions.AssetFileSize))] - public long FileSize - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetFileSize))] + public long FileSize + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetFileVersion))] - public long FileVersion - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetFileVersion))] + public long FileVersion + { + set => SetValue(value); + } - [FieldDescription(nameof(FieldDescriptions.AssetIsProtected))] - public bool? IsProtected - { - set => SetValue(value); - } + [FieldDescription(nameof(FieldDescriptions.AssetIsProtected))] + public bool? IsProtected + { + set => SetValue(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs index 0f716d23f5..beaf238ff1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs @@ -8,50 +8,49 @@ using System.Security.Claims; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class AssetScriptVars : ScriptVars { - public sealed class AssetScriptVars : ScriptVars + [FieldDescription(nameof(FieldDescriptions.AppId))] + public DomainId AppId + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.EntityId))] + public DomainId AssetId + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.AppName))] + public string AppName + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.Operation))] + public string Operation + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.Command))] + public AssetCommandScriptVars Command + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.Asset))] + public AssetEntityScriptVars Asset + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.User))] + public ClaimsPrincipal? User { - [FieldDescription(nameof(FieldDescriptions.AppId))] - public DomainId AppId - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.EntityId))] - public DomainId AssetId - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.AppName))] - public string AppName - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.Operation))] - public string Operation - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.Command))] - public AssetCommandScriptVars Command - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.Asset))] - public AssetEntityScriptVars Asset - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.User))] - public ClaimsPrincipal? User - { - set => SetValue(value); - } + set => SetValue(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs index a616affd51..3d967ecafc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs @@ -9,99 +9,98 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class ContentScriptVars : DataScriptVars { - public sealed class ContentScriptVars : DataScriptVars + [FieldDescription(nameof(FieldDescriptions.ContentValidate))] + public Action Validate + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.AppId))] + public DomainId AppId + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.SchemaId))] + public DomainId SchemaId + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.EntityId))] + public DomainId ContentId + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.AppName))] + public string AppName + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.ContentSchemaName))] + public string SchemaName + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.Operation))] + public string Operation + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.EntityRequestDeletePermanent))] + public bool Permanent + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.User))] + public ClaimsPrincipal? User + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.ContentStatus))] + public Status Status + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.ContentStatusOld))] + public Status StatusOld + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.ContentStatusOld))] + public Status OldStatus + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.ContentData))] + public ContentData? DataOld + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.ContentDataOld))] + public ContentData? OldData + { + set => SetValue(value); + } + + [FieldDescription(nameof(FieldDescriptions.ContentData))] + public override ContentData? Data { - [FieldDescription(nameof(FieldDescriptions.ContentValidate))] - public Action Validate - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.AppId))] - public DomainId AppId - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.SchemaId))] - public DomainId SchemaId - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.EntityId))] - public DomainId ContentId - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.AppName))] - public string AppName - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.ContentSchemaName))] - public string SchemaName - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.Operation))] - public string Operation - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.EntityRequestDeletePermanent))] - public bool Permanent - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.User))] - public ClaimsPrincipal? User - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.ContentStatus))] - public Status Status - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.ContentStatusOld))] - public Status StatusOld - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.ContentStatusOld))] - public Status OldStatus - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.ContentData))] - public ContentData? DataOld - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.ContentDataOld))] - public ContentData? OldData - { - set => SetValue(value); - } - - [FieldDescription(nameof(FieldDescriptions.ContentData))] - public override ContentData? Data - { - get => GetValue(); - set => SetValue(value); - } + get => GetValue(); + set => SetValue(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs index fc3eb86f18..e2e68bab48 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs @@ -13,145 +13,144 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper; + +public sealed class ContentDataObject : ObjectInstance { - public sealed class ContentDataObject : ObjectInstance - { - private readonly ContentData contentData; - private HashSet fieldsToDelete; - private Dictionary fieldProperties; - private bool isChanged; + private readonly ContentData contentData; + private HashSet fieldsToDelete; + private Dictionary fieldProperties; + private bool isChanged; - public override bool Extensible => true; + public override bool Extensible => true; - public ContentDataObject(Engine engine, ContentData contentData) - : base(engine) - { - this.contentData = contentData; - } + public ContentDataObject(Engine engine, ContentData contentData) + : base(engine) + { + this.contentData = contentData; + } - public void MarkChanged() - { - isChanged = true; - } + public void MarkChanged() + { + isChanged = true; + } - public bool TryUpdate(out ContentData result) - { - result = contentData; + public bool TryUpdate(out ContentData result) + { + result = contentData; - if (isChanged) + if (isChanged) + { + if (fieldsToDelete != null) { - if (fieldsToDelete != null) + foreach (var field in fieldsToDelete) { - foreach (var field in fieldsToDelete) - { - contentData.Remove(field); - } + contentData.Remove(field); } + } - if (fieldProperties != null) + if (fieldProperties != null) + { + foreach (var (key, propertyDescriptor) in fieldProperties) { - foreach (var (key, propertyDescriptor) in fieldProperties) - { - var value = (ContentDataProperty)propertyDescriptor; + var value = (ContentDataProperty)propertyDescriptor; - if (value.ContentField != null && value.ContentField.TryUpdate(out var fieldData)) - { - contentData[key] = fieldData; - } + if (value.ContentField != null && value.ContentField.TryUpdate(out var fieldData)) + { + contentData[key] = fieldData; } } } - - return isChanged; } - public override void RemoveOwnProperty(JsValue property) - { - var propertyName = property.AsString(); + return isChanged; + } - fieldsToDelete ??= new HashSet(); - fieldsToDelete.Add(propertyName); + public override void RemoveOwnProperty(JsValue property) + { + var propertyName = property.AsString(); - fieldProperties?.Remove(propertyName); + fieldsToDelete ??= new HashSet(); + fieldsToDelete.Add(propertyName); - MarkChanged(); - } + fieldProperties?.Remove(propertyName); - public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc) - { - EnsurePropertiesInitialized(); + MarkChanged(); + } - var propertyName = property.AsString(); + public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc) + { + EnsurePropertiesInitialized(); - if (!fieldProperties.ContainsKey(propertyName)) - { - fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; - } + var propertyName = property.AsString(); - return true; + if (!fieldProperties.ContainsKey(propertyName)) + { + fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; } - public override bool Set(JsValue property, JsValue value, JsValue receiver) - { - EnsurePropertiesInitialized(); + return true; + } - var propertyName = property.AsString(); + public override bool Set(JsValue property, JsValue value, JsValue receiver) + { + EnsurePropertiesInitialized(); - fieldProperties.GetOrAdd(propertyName, (k, c) => new ContentDataProperty(c), this).Value = value; + var propertyName = property.AsString(); - return true; - } + fieldProperties.GetOrAdd(propertyName, (k, c) => new ContentDataProperty(c), this).Value = value; - public override PropertyDescriptor GetOwnProperty(JsValue property) - { - EnsurePropertiesInitialized(); + return true; + } - var propertyName = property.AsString(); + public override PropertyDescriptor GetOwnProperty(JsValue property) + { + EnsurePropertiesInitialized(); - if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase)) - { - return PropertyDescriptor.Undefined; - } + var propertyName = property.AsString(); - return fieldProperties.GetOrAdd(propertyName, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false)), this); + if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase)) + { + return PropertyDescriptor.Undefined; } - public override IEnumerable> GetOwnProperties() - { - EnsurePropertiesInitialized(); + return fieldProperties.GetOrAdd(propertyName, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false)), this); + } - return fieldProperties.Select(x => new KeyValuePair(x.Key, x.Value)); - } + public override IEnumerable> GetOwnProperties() + { + EnsurePropertiesInitialized(); - public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol) - { - EnsurePropertiesInitialized(); + return fieldProperties.Select(x => new KeyValuePair(x.Key, x.Value)); + } - return fieldProperties.Keys.Select(x => (JsValue)x).ToList(); - } + public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol) + { + EnsurePropertiesInitialized(); + + return fieldProperties.Keys.Select(x => (JsValue)x).ToList(); + } - private void EnsurePropertiesInitialized() + private void EnsurePropertiesInitialized() + { + if (fieldProperties == null) { - if (fieldProperties == null) - { - fieldProperties = new Dictionary(contentData.Count); + fieldProperties = new Dictionary(contentData.Count); - foreach (var (key, value) in contentData) - { - fieldProperties.Add(key, new ContentDataProperty(this, new ContentFieldObject(this, value, false))); - } + foreach (var (key, value) in contentData) + { + fieldProperties.Add(key, new ContentDataProperty(this, new ContentFieldObject(this, value, false))); } } + } - public override object ToObject() + public override object ToObject() + { + if (TryUpdate(out var result)) { - if (TryUpdate(out var result)) - { - return result; - } - - return contentData; + return result; } + + return contentData; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs index a8199232f3..ae6a140c30 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs @@ -10,57 +10,56 @@ using Jint.Runtime; using Squidex.Domain.Apps.Core.Contents; -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper; + +public sealed class ContentDataProperty : CustomProperty { - public sealed class ContentDataProperty : CustomProperty - { - private readonly ContentDataObject contentData; - private ContentFieldObject? contentField; - private JsValue value; + private readonly ContentDataObject contentData; + private ContentFieldObject? contentField; + private JsValue value; - protected override JsValue CustomValue + protected override JsValue CustomValue + { + get { - get - { - return value; - } - set + return value; + } + set + { + if (!Equals(this.value, value)) { - if (!Equals(this.value, value)) + if (value == null || !value.IsObject()) { - if (value == null || !value.IsObject()) - { - throw new JavaScriptException("You can only assign objects to content data."); - } - - var obj = value.AsObject(); + throw new JavaScriptException("You can only assign objects to content data."); + } - contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); + var obj = value.AsObject(); - foreach (var (key, propertyDescriptor) in obj.GetOwnProperties()) - { - contentField.Set(key, propertyDescriptor.Value); - } + contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); - this.value = contentField; + foreach (var (key, propertyDescriptor) in obj.GetOwnProperties()) + { + contentField.Set(key, propertyDescriptor.Value); } + + this.value = contentField; } } + } - public ContentFieldObject? ContentField - { - get => contentField; - } + public ContentFieldObject? ContentField + { + get => contentField; + } - public ContentDataProperty(ContentDataObject contentData, ContentFieldObject? contentField = null) - { - this.contentData = contentData; - this.contentField = contentField; + public ContentDataProperty(ContentDataObject contentData, ContentFieldObject? contentField = null) + { + this.contentData = contentData; + this.contentField = contentField; - if (contentField != null) - { - value = contentField; - } + if (contentField != null) + { + value = contentField; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs index 3717e48432..576d3d7e0a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs @@ -13,163 +13,162 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper; + +public sealed class ContentFieldObject : ObjectInstance { - public sealed class ContentFieldObject : ObjectInstance - { - private readonly ContentDataObject contentData; - private readonly ContentFieldData? fieldData; - private HashSet? valuesToDelete; - private Dictionary valueProperties; - private bool isChanged; + private readonly ContentDataObject contentData; + private readonly ContentFieldData? fieldData; + private HashSet? valuesToDelete; + private Dictionary valueProperties; + private bool isChanged; - public ContentFieldData? FieldData - { - get => fieldData; - } + public ContentFieldData? FieldData + { + get => fieldData; + } - public override bool Extensible => true; + public override bool Extensible => true; - public ContentFieldObject(ContentDataObject contentData, ContentFieldData? fieldData, bool isNew) - : base(contentData.Engine) - { - this.contentData = contentData; + public ContentFieldObject(ContentDataObject contentData, ContentFieldData? fieldData, bool isNew) + : base(contentData.Engine) + { + this.contentData = contentData; - this.fieldData = fieldData; + this.fieldData = fieldData; - if (isNew) - { - MarkChanged(); - } + if (isNew) + { + MarkChanged(); } + } - public void MarkChanged() - { - isChanged = true; + public void MarkChanged() + { + isChanged = true; - contentData.MarkChanged(); - } + contentData.MarkChanged(); + } - public bool TryUpdate(out ContentFieldData? result) - { - result = fieldData; + public bool TryUpdate(out ContentFieldData? result) + { + result = fieldData; - if (isChanged && fieldData != null) + if (isChanged && fieldData != null) + { + if (valuesToDelete != null) { - if (valuesToDelete != null) + foreach (var field in valuesToDelete) { - foreach (var field in valuesToDelete) - { - fieldData.Remove(field); - } + fieldData.Remove(field); } + } - if (valueProperties != null) + if (valueProperties != null) + { + foreach (var (key, propertyDescriptor) in valueProperties) { - foreach (var (key, propertyDescriptor) in valueProperties) - { - var value = (ContentFieldProperty)propertyDescriptor; + var value = (ContentFieldProperty)propertyDescriptor; - if (value.IsChanged) - { - fieldData[key] = value.ContentValue; - } + if (value.IsChanged) + { + fieldData[key] = value.ContentValue; } } } - - return isChanged; } - public override void RemoveOwnProperty(JsValue property) - { - var propertyName = property.AsString(); + return isChanged; + } - valuesToDelete ??= new HashSet(); - valuesToDelete.Add(propertyName); + public override void RemoveOwnProperty(JsValue property) + { + var propertyName = property.AsString(); - valueProperties?.Remove(propertyName); + valuesToDelete ??= new HashSet(); + valuesToDelete.Add(propertyName); - MarkChanged(); - } + valueProperties?.Remove(propertyName); - public override bool Set(JsValue property, JsValue value, JsValue receiver) - { - EnsurePropertiesInitialized(); + MarkChanged(); + } - var propertyName = property.AsString(); + public override bool Set(JsValue property, JsValue value, JsValue receiver) + { + EnsurePropertiesInitialized(); - valueProperties.GetOrAdd(propertyName, _ => new ContentFieldProperty(this)).Value = value; + var propertyName = property.AsString(); - return true; - } + valueProperties.GetOrAdd(propertyName, _ => new ContentFieldProperty(this)).Value = value; - public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc) - { - EnsurePropertiesInitialized(); + return true; + } - var propertyName = property.AsString(); + public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc) + { + EnsurePropertiesInitialized(); - if (!valueProperties.ContainsKey(propertyName)) - { - valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; - } + var propertyName = property.AsString(); - return true; + if (!valueProperties.ContainsKey(propertyName)) + { + valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; } - public override PropertyDescriptor GetOwnProperty(JsValue property) - { - EnsurePropertiesInitialized(); + return true; + } - var propertyName = property.AsString(); + public override PropertyDescriptor GetOwnProperty(JsValue property) + { + EnsurePropertiesInitialized(); - if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase)) - { - return PropertyDescriptor.Undefined; - } + var propertyName = property.AsString(); - return valueProperties?.GetValueOrDefault(propertyName) ?? PropertyDescriptor.Undefined; + if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase)) + { + return PropertyDescriptor.Undefined; } - public override IEnumerable> GetOwnProperties() - { - EnsurePropertiesInitialized(); + return valueProperties?.GetValueOrDefault(propertyName) ?? PropertyDescriptor.Undefined; + } - return valueProperties.Select(x => new KeyValuePair(x.Key, x.Value)); - } + public override IEnumerable> GetOwnProperties() + { + EnsurePropertiesInitialized(); - public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol) - { - EnsurePropertiesInitialized(); + return valueProperties.Select(x => new KeyValuePair(x.Key, x.Value)); + } - return valueProperties.Keys.Select(x => (JsValue)x).ToList(); - } + public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol) + { + EnsurePropertiesInitialized(); - private void EnsurePropertiesInitialized() + return valueProperties.Keys.Select(x => (JsValue)x).ToList(); + } + + private void EnsurePropertiesInitialized() + { + if (valueProperties == null) { - if (valueProperties == null) - { - valueProperties = new Dictionary(fieldData?.Count ?? 0); + valueProperties = new Dictionary(fieldData?.Count ?? 0); - if (fieldData != null) + if (fieldData != null) + { + foreach (var (key, value) in fieldData) { - foreach (var (key, value) in fieldData) - { - valueProperties.Add(key, new ContentFieldProperty(this, value)); - } + valueProperties.Add(key, new ContentFieldProperty(this, value)); } } } + } - public override object ToObject() + public override object ToObject() + { + if (TryUpdate(out var result)) { - if (TryUpdate(out var result)) - { - return result!; - } - - return fieldData!; + return result!; } + + return fieldData!; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs index a2c5752619..cb56270853 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs @@ -9,60 +9,59 @@ using Jint.Native; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper; + +public sealed class ContentFieldProperty : CustomProperty { - public sealed class ContentFieldProperty : CustomProperty - { - private readonly ContentFieldObject contentField; - private JsonValue contentValue; - private JsValue? value; - private bool isChanged; + private readonly ContentFieldObject contentField; + private JsonValue contentValue; + private JsValue? value; + private bool isChanged; - [DebuggerHidden] - protected override JsValue? CustomValue + [DebuggerHidden] + protected override JsValue? CustomValue + { + get { - get + if (value == null) { - if (value == null) + if (contentValue != default) { - if (contentValue != default) - { - value = JsonMapper.Map(contentValue, contentField.Engine); - } + value = JsonMapper.Map(contentValue, contentField.Engine); } - - return value; } - set - { - var newContentValue = JsonMapper.Map(value); - if (!Equals(contentValue, newContentValue)) - { - this.value = value; + return value; + } + set + { + var newContentValue = JsonMapper.Map(value); + + if (!Equals(contentValue, newContentValue)) + { + this.value = value; - contentValue = newContentValue; - contentField.MarkChanged(); + contentValue = newContentValue; + contentField.MarkChanged(); - isChanged = true; - } + isChanged = true; } } + } - public JsonValue ContentValue - { - get => contentValue; - } + public JsonValue ContentValue + { + get => contentValue; + } - public bool IsChanged - { - get => isChanged; - } + public bool IsChanged + { + get => isChanged; + } - public ContentFieldProperty(ContentFieldObject contentField, JsonValue contentValue = default) - { - this.contentField = contentField; - this.contentValue = contentValue; - } + public ContentFieldProperty(ContentFieldObject contentField, JsonValue contentValue = default) + { + this.contentField = contentField; + this.contentValue = contentValue; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs index 087002eca9..bdea0778e6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs @@ -7,18 +7,17 @@ using Jint.Runtime.Descriptors; -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper; + +public abstract class CustomProperty : PropertyDescriptor { - public abstract class CustomProperty : PropertyDescriptor + protected CustomProperty() + : base(PropertyFlag.CustomJsValue) { - protected CustomProperty() - : base(PropertyFlag.CustomJsValue) - { - Enumerable = true; + Enumerable = true; - Writable = true; + Writable = true; - Configurable = true; - } + Configurable = true; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs index 156b9c37a4..741518a77d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -12,130 +12,129 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper; + +public static class JsonMapper { - public static class JsonMapper + public static JsValue Map(JsonValue value, Engine engine) { - public static JsValue Map(JsonValue value, Engine engine) + switch (value.Value) { - switch (value.Value) - { - case null: - return JsValue.Null; - case bool b: - return new JsBoolean(b); - case double n: - return new JsNumber(n); - case string s: - return new JsString(s); - case JsonObject o: - return FromObject(o, engine); - case JsonArray a: - return FromArray(a, engine); - } - - ThrowInvalidType(nameof(value)); - return JsValue.Null; + case null: + return JsValue.Null; + case bool b: + return new JsBoolean(b); + case double n: + return new JsNumber(n); + case string s: + return new JsString(s); + case JsonObject o: + return FromObject(o, engine); + case JsonArray a: + return FromArray(a, engine); } - private static JsValue FromArray(JsonArray arr, Engine engine) - { - var target = new JsValue[arr.Count]; + ThrowInvalidType(nameof(value)); + return JsValue.Null; + } - for (var i = 0; i < arr.Count; i++) - { - target[i] = Map(arr[i], engine); - } + private static JsValue FromArray(JsonArray arr, Engine engine) + { + var target = new JsValue[arr.Count]; - return engine.Realm.Intrinsics.Array.Construct(target); + for (var i = 0; i < arr.Count; i++) + { + target[i] = Map(arr[i], engine); } - private static JsValue FromObject(JsonObject obj, Engine engine) + return engine.Realm.Intrinsics.Array.Construct(target); + } + + private static JsValue FromObject(JsonObject obj, Engine engine) + { + var target = new ObjectInstance(engine); + + foreach (var (key, value) in obj) { - var target = new ObjectInstance(engine); + target.FastAddProperty(key, Map(value, engine), true, true, true); + } - foreach (var (key, value) in obj) - { - target.FastAddProperty(key, Map(value, engine), true, true, true); - } + return target; + } - return target; + public static JsonValue Map(JsValue? value) + { + if (value == null || value.IsNull() || value.IsUndefined()) + { + return default; } - public static JsonValue Map(JsValue? value) + if (value.IsString()) { - if (value == null || value.IsNull() || value.IsUndefined()) - { - return default; - } + return value.AsString(); + } - if (value.IsString()) - { - return value.AsString(); - } + if (value.IsBoolean()) + { + return value.AsBoolean(); + } - if (value.IsBoolean()) - { - return value.AsBoolean(); - } + if (value.IsDate()) + { + return value.AsDate().ToString(); + } - if (value.IsDate()) - { - return value.AsDate().ToString(); - } + if (value.IsRegExp()) + { + return value.AsRegExp().Value?.ToString(); + } - if (value.IsRegExp()) - { - return value.AsRegExp().Value?.ToString(); - } + if (value.IsNumber()) + { + var number = value.AsNumber(); - if (value.IsNumber()) + if (double.IsNaN(number) || double.IsPositiveInfinity(number) || double.IsNegativeInfinity(number)) { - var number = value.AsNumber(); - - if (double.IsNaN(number) || double.IsPositiveInfinity(number) || double.IsNegativeInfinity(number)) - { - return 0; - } - - return number; + return 0; } - if (value.IsArray()) - { - var arr = value.AsArray(); + return number; + } - var result = new JsonArray((int)arr.Length); + if (value.IsArray()) + { + var arr = value.AsArray(); - for (var i = 0; i < arr.Length; i++) - { - result.Add(Map(arr.Get(i.ToString(CultureInfo.InvariantCulture)))); - } + var result = new JsonArray((int)arr.Length); - return result; + for (var i = 0; i < arr.Length; i++) + { + result.Add(Map(arr.Get(i.ToString(CultureInfo.InvariantCulture)))); } - if (value.IsObject()) - { - var obj = value.AsObject(); + return result; + } - var result = new JsonObject((int)obj.Length); + if (value.IsObject()) + { + var obj = value.AsObject(); - foreach (var (key, propertyDescriptor) in obj.GetOwnProperties()) - { - result[key.AsString()] = Map(propertyDescriptor.Value); - } + var result = new JsonObject((int)obj.Length); - return result; + foreach (var (key, propertyDescriptor) in obj.GetOwnProperties()) + { + result[key.AsString()] = Map(propertyDescriptor.Value); } - ThrowInvalidType(nameof(value)); - return default; + return result; } - private static void ThrowInvalidType(string argument) - { - ThrowHelper.ArgumentException("Invalid json type.", argument); - } + ThrowInvalidType(nameof(value)); + return default; + } + + private static void ThrowInvalidType(string argument) + { + ThrowHelper.ArgumentException("Invalid json type.", argument); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs index 1f25f23a7b..d2a409ae08 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs @@ -7,14 +7,13 @@ using Squidex.Domain.Apps.Core.Contents; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public class DataScriptVars : ScriptVars { - public class DataScriptVars : ScriptVars + public virtual ContentData? Data { - public virtual ContentData? Data - { - get => GetValue(); - set => SetValue(value); - } + get => GetValue(); + set => SetValue(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs index de126dff3e..d3484a905d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs @@ -16,52 +16,51 @@ using Squidex.Infrastructure; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class DefaultConverter : IObjectConverter { - public sealed class DefaultConverter : IObjectConverter + public static readonly DefaultConverter Instance = new DefaultConverter(); + + private DefaultConverter() + { + } + + public bool TryConvert(Engine engine, object value, [MaybeNullWhen(false)] out JsValue result) { - public static readonly DefaultConverter Instance = new DefaultConverter(); + result = null!; - private DefaultConverter() + if (value is Enum) { + result = value.ToString(); + return true; } - public bool TryConvert(Engine engine, object value, [MaybeNullWhen(false)] out JsValue result) + switch (value) { - result = null!; - - if (value is Enum) - { - result = value.ToString(); + case IUser user: + result = JintUser.Create(engine, user); + return true; + case ClaimsPrincipal principal: + result = JintUser.Create(engine, principal); + return true; + case DomainId domainId: + result = domainId.ToString(); + return true; + case Guid guid: + result = guid.ToString(); + return true; + case Instant instant: + result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); + return true; + case Status status: + result = status.ToString(); + return true; + case ContentData content: + result = new ContentDataObject(engine, content); return true; - } - - switch (value) - { - case IUser user: - result = JintUser.Create(engine, user); - return true; - case ClaimsPrincipal principal: - result = JintUser.Create(engine, principal); - return true; - case DomainId domainId: - result = domainId.ToString(); - return true; - case Guid guid: - result = guid.ToString(); - return true; - case Instant instant: - result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); - return true; - case Status status: - result = status.ToString(); - return true; - case ContentData content: - result = new ContentDataObject(engine, content); - return true; - } - - return false; } + + return false; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs index b131eccaea..75cbb210e0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs @@ -9,28 +9,27 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class EventScriptVars : ScriptVars { - public sealed class EventScriptVars : ScriptVars + public DomainId AppId { - public DomainId AppId - { - set => SetValue(value); - } + set => SetValue(value); + } - public string AppName - { - set => SetValue(value); - } + public string AppName + { + set => SetValue(value); + } - public ClaimsPrincipal User - { - set => SetValue(value); - } + public ClaimsPrincipal User + { + set => SetValue(value); + } - public EnrichedEvent Event - { - set => SetValue(value); - } + public EnrichedEvent Event + { + set => SetValue(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeJintExtension.cs index 9ad8b30a88..d5843a5611 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeJintExtension.cs @@ -10,35 +10,34 @@ using Jint.Native; using Squidex.Domain.Apps.Core.Properties; -namespace Squidex.Domain.Apps.Core.Scripting.Extensions +namespace Squidex.Domain.Apps.Core.Scripting.Extensions; + +public sealed class DateTimeJintExtension : IJintExtension, IScriptDescriptor { - public sealed class DateTimeJintExtension : IJintExtension, IScriptDescriptor + private readonly Func formatDate = (date, format) => { - private readonly Func formatDate = (date, format) => + try { - try - { - return date.ToString(format, CultureInfo.InvariantCulture); - } - catch - { - return JsValue.Undefined; - } - }; - - public void Extend(Engine engine) + return date.ToString(format, CultureInfo.InvariantCulture); + } + catch { - engine.SetValue("formatTime", formatDate); - engine.SetValue("formatDate", formatDate); + return JsValue.Undefined; } + }; - public void Describe(AddDescription describe, ScriptScope scope) - { - describe(JsonType.Function, "formatDate(data, pattern)", - Resources.ScriptingFormatDate); + public void Extend(Engine engine) + { + engine.SetValue("formatTime", formatDate); + engine.SetValue("formatDate", formatDate); + } - describe(JsonType.Function, "formatTime(text)", - Resources.ScriptingFormatTime); - } + public void Describe(AddDescription describe, ScriptScope scope) + { + describe(JsonType.Function, "formatDate(data, pattern)", + Resources.ScriptingFormatDate); + + describe(JsonType.Function, "formatTime(text)", + Resources.ScriptingFormatTime); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs index 207996e929..9650f41668 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs @@ -12,151 +12,150 @@ using Jint.Runtime; using Squidex.Domain.Apps.Core.Properties; -namespace Squidex.Domain.Apps.Core.Scripting.Extensions +namespace Squidex.Domain.Apps.Core.Scripting.Extensions; + +public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor { - public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor + private delegate void HttpJson(string url, Action callback, JsValue? headers = null); + private delegate void HttpJsonWithBody(string url, JsValue post, Action callback, JsValue? headers = null); + private readonly IHttpClientFactory httpClientFactory; + + public HttpJintExtension(IHttpClientFactory httpClientFactory) { - private delegate void HttpJson(string url, Action callback, JsValue? headers = null); - private delegate void HttpJsonWithBody(string url, JsValue post, Action callback, JsValue? headers = null); - private readonly IHttpClientFactory httpClientFactory; + this.httpClientFactory = httpClientFactory; + } - public HttpJintExtension(IHttpClientFactory httpClientFactory) - { - this.httpClientFactory = httpClientFactory; - } + public void ExtendAsync(ScriptExecutionContext context) + { + AddBodyMethod(context, HttpMethod.Patch, "patchJSON"); + AddBodyMethod(context, HttpMethod.Post, "postJSON"); + AddBodyMethod(context, HttpMethod.Put, "putJSON"); + AddMethod(context, HttpMethod.Delete, "deleteJSON"); + AddMethod(context, HttpMethod.Get, "getJSON"); + } - public void ExtendAsync(ScriptExecutionContext context) + public void Describe(AddDescription describe, ScriptScope scope) + { + if (!scope.HasFlag(ScriptScope.Async)) { - AddBodyMethod(context, HttpMethod.Patch, "patchJSON"); - AddBodyMethod(context, HttpMethod.Post, "postJSON"); - AddBodyMethod(context, HttpMethod.Put, "putJSON"); - AddMethod(context, HttpMethod.Delete, "deleteJSON"); - AddMethod(context, HttpMethod.Get, "getJSON"); + return; } - public void Describe(AddDescription describe, ScriptScope scope) - { - if (!scope.HasFlag(ScriptScope.Async)) - { - return; - } + describe(JsonType.Function, "getJSON(url, callback, headers?)", + Resources.ScriptingGetJSON); - describe(JsonType.Function, "getJSON(url, callback, headers?)", - Resources.ScriptingGetJSON); + describe(JsonType.Function, "postJSON(url, body, callback, headers?)", + Resources.ScriptingPostJSON); - describe(JsonType.Function, "postJSON(url, body, callback, headers?)", - Resources.ScriptingPostJSON); + describe(JsonType.Function, "putJSON(url, body, callback, headers?)", + Resources.ScriptingPutJson); - describe(JsonType.Function, "putJSON(url, body, callback, headers?)", - Resources.ScriptingPutJson); + describe(JsonType.Function, "patchJSON(url, body, callback, headers?)", + Resources.ScriptingPatchJson); - describe(JsonType.Function, "patchJSON(url, body, callback, headers?)", - Resources.ScriptingPatchJson); - - describe(JsonType.Function, "deleteJSON(url, body, callback, headers?)", - Resources.ScriptingDeleteJson); - } + describe(JsonType.Function, "deleteJSON(url, body, callback, headers?)", + Resources.ScriptingDeleteJson); + } - private void AddMethod(ScriptExecutionContext context, HttpMethod method, string name) + private void AddMethod(ScriptExecutionContext context, HttpMethod method, string name) + { + var action = new HttpJson((url, callback, headers) => { - var action = new HttpJson((url, callback, headers) => - { - Request(context, method, url, null, callback, headers); - }); + Request(context, method, url, null, callback, headers); + }); - context.Engine.SetValue(name, action); - } + context.Engine.SetValue(name, action); + } - private void AddBodyMethod(ScriptExecutionContext context, HttpMethod method, string name) + private void AddBodyMethod(ScriptExecutionContext context, HttpMethod method, string name) + { + var action = new HttpJsonWithBody((url, body, callback, headers) => { - var action = new HttpJsonWithBody((url, body, callback, headers) => - { - Request(context, method, url, body, callback, headers); - }); + Request(context, method, url, body, callback, headers); + }); - context.Engine.SetValue(name, action); - } + context.Engine.SetValue(name, action); + } - private void Request(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action callback, JsValue? headers) + private void Request(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action callback, JsValue? headers) + { + context.Schedule(async (scheduler, ct) => { - context.Schedule(async (scheduler, ct) => + if (callback == null) { - if (callback == null) - { - throw new JavaScriptException("Callback cannot be null."); - } + throw new JavaScriptException("Callback cannot be null."); + } - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - { - throw new JavaScriptException("URL is not valid."); - } + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + throw new JavaScriptException("URL is not valid."); + } - using (var httpClient = httpClientFactory.CreateClient()) + using (var httpClient = httpClientFactory.CreateClient()) + { + using (var request = CreateRequest(context, method, uri, body, headers)) { - using (var request = CreateRequest(context, method, uri, body, headers)) + using (var response = await httpClient.SendAsync(request, ct)) { - using (var response = await httpClient.SendAsync(request, ct)) - { - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - var responseObject = await ParseResponseasync(context, response, ct); + var responseObject = await ParseResponseasync(context, response, ct); - scheduler.Run(callback, responseObject); - } + scheduler.Run(callback, responseObject); } } - }); - } + } + }); + } - private static HttpRequestMessage CreateRequest(ScriptExecutionContext context, HttpMethod method, Uri uri, JsValue? body, JsValue? headers) - { - var request = new HttpRequestMessage(method, uri); + private static HttpRequestMessage CreateRequest(ScriptExecutionContext context, HttpMethod method, Uri uri, JsValue? body, JsValue? headers) + { + var request = new HttpRequestMessage(method, uri); - if (body != null) - { - var serializer = new JsonSerializer(context.Engine); + if (body != null) + { + var serializer = new JsonSerializer(context.Engine); - var json = serializer.Serialize(body, JsValue.Undefined, JsValue.Undefined)?.ToString(); + var json = serializer.Serialize(body, JsValue.Undefined, JsValue.Undefined)?.ToString(); - if (json != null) - { - request.Content = new StringContent(json, Encoding.UTF8, "text/json"); - } + if (json != null) + { + request.Content = new StringContent(json, Encoding.UTF8, "text/json"); } + } - if (headers != null && headers.Type == Types.Object) - { - var obj = headers.AsObject(); + if (headers != null && headers.Type == Types.Object) + { + var obj = headers.AsObject(); - foreach (var (key, property) in obj.GetOwnProperties()) - { - var value = TypeConverter.ToString(property.Value); + foreach (var (key, property) in obj.GetOwnProperties()) + { + var value = TypeConverter.ToString(property.Value); - var keyString = key.AsString(); + var keyString = key.AsString(); - if (!string.IsNullOrWhiteSpace(keyString)) - { - request.Headers.TryAddWithoutValidation(keyString, value ?? string.Empty); - } + if (!string.IsNullOrWhiteSpace(keyString)) + { + request.Headers.TryAddWithoutValidation(keyString, value ?? string.Empty); } } - - return request; } - private static async Task ParseResponseasync(ScriptExecutionContext context, HttpResponseMessage response, - CancellationToken ct) - { - var responseString = await response.Content.ReadAsStringAsync(ct); + return request; + } - ct.ThrowIfCancellationRequested(); + private static async Task ParseResponseasync(ScriptExecutionContext context, HttpResponseMessage response, + CancellationToken ct) + { + var responseString = await response.Content.ReadAsStringAsync(ct); - var jsonParser = new JsonParser(context.Engine); - var jsonValue = jsonParser.Parse(responseString); + ct.ThrowIfCancellationRequested(); - ct.ThrowIfCancellationRequested(); + var jsonParser = new JsonParser(context.Engine); + var jsonValue = jsonParser.Parse(responseString); - return jsonValue; - } + ct.ThrowIfCancellationRequested(); + + return jsonValue; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs index 3851587ee0..a2ab8d60e2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs @@ -11,156 +11,155 @@ using Squidex.Infrastructure; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.Scripting.Extensions +namespace Squidex.Domain.Apps.Core.Scripting.Extensions; + +public sealed class StringJintExtension : IJintExtension, IScriptDescriptor { - public sealed class StringJintExtension : IJintExtension, IScriptDescriptor + private delegate JsValue StringSlugifyDelegate(string text, bool single = false); + + private readonly Func sha256 = text => + { + try + { + return text.ToSha256(); + } + catch + { + return JsValue.Undefined; + } + }; + + private readonly Func sha512 = text => + { + try + { + return text.ToSha512(); + } + catch + { + return JsValue.Undefined; + } + }; + + private readonly Func md5 = text => + { + try + { + return text.ToMD5(); + } + catch + { + return JsValue.Undefined; + } + }; + + private readonly StringSlugifyDelegate slugify = (text, single) => + { + try + { + return text.Slugify(null, single); + } + catch + { + return JsValue.Undefined; + } + }; + + private readonly Func toCamelCase = text => + { + try + { + return text.ToCamelCase(); + } + catch + { + return JsValue.Undefined; + } + }; + + private readonly Func toPascalCase = text => + { + try + { + return text.ToPascalCase(); + } + catch + { + return JsValue.Undefined; + } + }; + + private readonly Func html2Text = text => { - private delegate JsValue StringSlugifyDelegate(string text, bool single = false); - - private readonly Func sha256 = text => - { - try - { - return text.ToSha256(); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly Func sha512 = text => - { - try - { - return text.ToSha512(); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly Func md5 = text => - { - try - { - return text.ToMD5(); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly StringSlugifyDelegate slugify = (text, single) => - { - try - { - return text.Slugify(null, single); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly Func toCamelCase = text => - { - try - { - return text.ToCamelCase(); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly Func toPascalCase = text => - { - try - { - return text.ToPascalCase(); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly Func html2Text = text => - { - try - { - return text.Html2Text(); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly Func markdown2Text = text => - { - try - { - return text.Markdown2Text(); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly Func guid = () => - { - return Guid.NewGuid().ToString(); - }; - - public Func Html2Text => html2Text; - - public void Extend(Engine engine) - { - engine.SetValue("guid", guid); - engine.SetValue("html2Text", Html2Text); - engine.SetValue("markdown2Text", markdown2Text); - engine.SetValue("md5", md5); - engine.SetValue("sha256", sha256); - engine.SetValue("sha512", sha512); - engine.SetValue("slugify", slugify); - engine.SetValue("toCamelCase", toCamelCase); - engine.SetValue("toPascalCase", toPascalCase); - } - - public void Describe(AddDescription describe, ScriptScope scope) - { - describe(JsonType.Function, "html2Text(text)", - Resources.ScriptingHtml2Text); - - describe(JsonType.Function, "markdown2Text(text)", - Resources.ScriptingMarkdown2Text); - - describe(JsonType.Function, "toCamelCase(text)", - Resources.ScriptingToCamelCase); - - describe(JsonType.Function, "toPascalCase(text)", - Resources.ScriptingToPascalCase); - - describe(JsonType.Function, "md5(text)", - Resources.ScriptingMD5); - - describe(JsonType.Function, "sha256(text)", - Resources.ScriptingSHA256); + try + { + return text.Html2Text(); + } + catch + { + return JsValue.Undefined; + } + }; - describe(JsonType.Function, "sha512(text)", - Resources.ScriptingSHA512); - - describe(JsonType.Function, "slugify(text)", - Resources.ScriptingSlugify); - - describe(JsonType.Function, "guid()", - Resources.ScriptingGuid); + private readonly Func markdown2Text = text => + { + try + { + return text.Markdown2Text(); + } + catch + { + return JsValue.Undefined; } + }; + + private readonly Func guid = () => + { + return Guid.NewGuid().ToString(); + }; + + public Func Html2Text => html2Text; + + public void Extend(Engine engine) + { + engine.SetValue("guid", guid); + engine.SetValue("html2Text", Html2Text); + engine.SetValue("markdown2Text", markdown2Text); + engine.SetValue("md5", md5); + engine.SetValue("sha256", sha256); + engine.SetValue("sha512", sha512); + engine.SetValue("slugify", slugify); + engine.SetValue("toCamelCase", toCamelCase); + engine.SetValue("toPascalCase", toPascalCase); + } + + public void Describe(AddDescription describe, ScriptScope scope) + { + describe(JsonType.Function, "html2Text(text)", + Resources.ScriptingHtml2Text); + + describe(JsonType.Function, "markdown2Text(text)", + Resources.ScriptingMarkdown2Text); + + describe(JsonType.Function, "toCamelCase(text)", + Resources.ScriptingToCamelCase); + + describe(JsonType.Function, "toPascalCase(text)", + Resources.ScriptingToPascalCase); + + describe(JsonType.Function, "md5(text)", + Resources.ScriptingMD5); + + describe(JsonType.Function, "sha256(text)", + Resources.ScriptingSHA256); + + describe(JsonType.Function, "sha512(text)", + Resources.ScriptingSHA512); + + describe(JsonType.Function, "slugify(text)", + Resources.ScriptingSlugify); + + describe(JsonType.Function, "guid()", + Resources.ScriptingGuid); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs index 071af3f4ee..d4ed0735c6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs @@ -10,47 +10,46 @@ using Squidex.Domain.Apps.Core.Properties; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.Scripting.Extensions +namespace Squidex.Domain.Apps.Core.Scripting.Extensions; + +public sealed class StringWordsJintExtension : IJintExtension, IScriptDescriptor { - public sealed class StringWordsJintExtension : IJintExtension, IScriptDescriptor + private readonly Func wordCount = text => { - private readonly Func wordCount = text => + try { - try - { - return text.WordCount(); - } - catch - { - return JsValue.Undefined; - } - }; - - private readonly Func characterCount = text => + return text.WordCount(); + } + catch { - try - { - return text.CharacterCount(); - } - catch - { - return JsValue.Undefined; - } - }; + return JsValue.Undefined; + } + }; - public void Extend(Engine engine) + private readonly Func characterCount = text => + { + try { - engine.SetValue("wordCount", wordCount); - engine.SetValue("characterCount", characterCount); + return text.CharacterCount(); } - - public void Describe(AddDescription describe, ScriptScope scope) + catch { - describe(JsonType.Function, "wordCount(text)", - Resources.ScriptingWordCount); - - describe(JsonType.Function, "characterCount(text)", - Resources.ScriptingCharacterCount); + return JsValue.Undefined; } + }; + + public void Extend(Engine engine) + { + engine.SetValue("wordCount", wordCount); + engine.SetValue("characterCount", characterCount); + } + + public void Describe(AddDescription describe, ScriptScope scope) + { + describe(JsonType.Function, "wordCount(text)", + Resources.ScriptingWordCount); + + describe(JsonType.Function, "characterCount(text)", + Resources.ScriptingCharacterCount); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs index cffe00e124..0f0f6d391b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs @@ -7,20 +7,19 @@ using Jint; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public interface IJintExtension { - public interface IJintExtension + void Extend(Engine engine) { - void Extend(Engine engine) - { - } + } - void Extend(ScriptExecutionContext context) - { - } + void Extend(ScriptExecutionContext context) + { + } - void ExtendAsync(ScriptExecutionContext context) - { - } + void ExtendAsync(ScriptExecutionContext context) + { } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptDescriptor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptDescriptor.cs index d2827573a3..fdf2889e78 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptDescriptor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptDescriptor.cs @@ -7,12 +7,11 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core.Scripting -{ - public delegate void AddDescription(JsonType type, string name, string description); +namespace Squidex.Domain.Apps.Core.Scripting; + +public delegate void AddDescription(JsonType type, string name, string description); - public interface IScriptDescriptor - { - void Describe(AddDescription describe, ScriptScope scope); - } +public interface IScriptDescriptor +{ + void Describe(AddDescription describe, ScriptScope scope); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs index a745eb9e60..629b49885d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs @@ -8,28 +8,27 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public interface IScriptEngine { - public interface IScriptEngine - { - Task ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default, - CancellationToken ct = default); + Task ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default, + CancellationToken ct = default); - Task TransformAsync(DataScriptVars vars, string script, ScriptOptions options = default, - CancellationToken ct = default); + Task TransformAsync(DataScriptVars vars, string script, ScriptOptions options = default, + CancellationToken ct = default); - JsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default); + JsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default); - bool Evaluate(ScriptVars vars, string script, ScriptOptions options = default) + bool Evaluate(ScriptVars vars, string script, ScriptOptions options = default) + { + try + { + return Execute(vars, script, options).Equals(true); + } + catch { - try - { - return Execute(vars, script, options).Equals(true); - } - catch - { - return false; - } + return false; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs index b5b34e1694..d25e9b93b8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs @@ -10,116 +10,115 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Scripting.Internal +namespace Squidex.Domain.Apps.Core.Scripting.Internal; + +internal sealed class AssetMetadataWrapper : IDictionary { - internal sealed class AssetMetadataWrapper : IDictionary + private readonly AssetMetadata metadata; + + public int Count { - private readonly AssetMetadata metadata; + get => metadata.Count; + } - public int Count - { - get => metadata.Count; - } + public ICollection Keys + { + get => metadata.Keys; + } - public ICollection Keys - { - get => metadata.Keys; - } + public ICollection Values + { + get => metadata.Values.Cast().ToList(); + } - public ICollection Values - { - get => metadata.Values.Cast().ToList(); - } + public object? this[string key] + { + get => metadata[key]; + set => metadata[key] = JsonValue.Create(value); + } - public object? this[string key] - { - get => metadata[key]; - set => metadata[key] = JsonValue.Create(value); - } + public bool IsReadOnly + { + get => false; + } - public bool IsReadOnly - { - get => false; - } + public AssetMetadataWrapper(AssetMetadata metadata) + { + this.metadata = metadata; + } - public AssetMetadataWrapper(AssetMetadata metadata) + public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) + { + if (metadata.TryGetValue(key, out var temp)) { - this.metadata = metadata; + value = temp; + return true; } - - public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) + else { - if (metadata.TryGetValue(key, out var temp)) - { - value = temp; - return true; - } - else - { - value = null; - return false; - } + value = null; + return false; } + } - public void Add(string key, object? value) - { - metadata.Add(key, JsonValue.Create(value)); - } + public void Add(string key, object? value) + { + metadata.Add(key, JsonValue.Create(value)); + } - public void Add(KeyValuePair item) - { - Add(item.Key, item.Value); - } + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } - public bool Remove(string key) - { - return metadata.Remove(key); - } + public bool Remove(string key) + { + return metadata.Remove(key); + } - public bool Remove(KeyValuePair item) - { - return false; - } + public bool Remove(KeyValuePair item) + { + return false; + } - public void Clear() - { - metadata.Clear(); - } + public void Clear() + { + metadata.Clear(); + } - public bool Contains(KeyValuePair item) - { - return false; - } + public bool Contains(KeyValuePair item) + { + return false; + } - public bool ContainsKey(string key) - { - return metadata.ContainsKey(key); - } + public bool ContainsKey(string key) + { + return metadata.ContainsKey(key); + } - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - var i = arrayIndex; + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + var i = arrayIndex; - foreach (var item in metadata) + foreach (var item in metadata) + { + if (i >= array.Length) { - if (i >= array.Length) - { - break; - } - - array[i] = new KeyValuePair(item.Key, item.Value); - i++; + break; } - } - public IEnumerator> GetEnumerator() - { - return metadata.Select(x => new KeyValuePair(x.Key, x.Value)).GetEnumerator(); + array[i] = new KeyValuePair(item.Key, item.Value); + i++; } + } - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)metadata).GetEnumerator(); - } + public IEnumerator> GetEnumerator() + { + return metadata.Select(x => new KeyValuePair(x.Key, x.Value)).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)metadata).GetEnumerator(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs index fb32e6cacc..c4d6bf8b91 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs @@ -9,35 +9,34 @@ using Esprima.Ast; using Microsoft.Extensions.Caching.Memory; -namespace Squidex.Domain.Apps.Core.Scripting.Internal +namespace Squidex.Domain.Apps.Core.Scripting.Internal; + +internal sealed class Parser { - internal sealed class Parser + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private static readonly ParserOptions DefaultParserOptions = new ParserOptions { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private static readonly ParserOptions DefaultParserOptions = new ParserOptions - { - AdaptRegexp = true, Tolerant = true - }; + AdaptRegexp = true, Tolerant = true + }; - private readonly IMemoryCache cache; + private readonly IMemoryCache cache; - public Parser(IMemoryCache cache) - { - this.cache = cache; - } + public Parser(IMemoryCache cache) + { + this.cache = cache; + } - public Script Parse(string script) - { - var cacheKey = $"{typeof(Parser)}_Script_{script}"; + public Script Parse(string script) + { + var cacheKey = $"{typeof(Parser)}_Script_{script}"; - return cache.GetOrCreate(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; + return cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; - var parser = new JavaScriptParser(script, DefaultParserOptions); + var parser = new JavaScriptParser(script, DefaultParserOptions); - return parser.ParseScript(); - }); - } + return parser.ParseScript(); + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintExtensions.cs index 1a039c9f36..f0838d2d64 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintExtensions.cs @@ -9,30 +9,29 @@ using Jint.Native; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public static class JintExtensions { - public static class JintExtensions + public static List ToIds(this JsValue? value) { - public static List ToIds(this JsValue? value) - { - var ids = new List(); + var ids = new List(); - if (value?.IsString() == true) - { - ids.Add(DomainId.Create(value.ToString())); - } - else if (value?.IsArray() == true) + if (value?.IsString() == true) + { + ids.Add(DomainId.Create(value.ToString())); + } + else if (value?.IsArray() == true) + { + foreach (var item in value.AsArray()) { - foreach (var item in value.AsArray()) + if (item.IsString()) { - if (item.IsString()) - { - ids.Add(DomainId.Create(item.ToString())); - } + ids.Add(DomainId.Create(item.ToString())); } } - - return ids; } + + return ids; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index 954e184032..77ae989e4d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -21,198 +21,197 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor { - public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor + private readonly IJintExtension[] extensions; + private readonly Parser parser; + private readonly TimeSpan timeoutScript; + private readonly TimeSpan timeoutExecution; + + public JintScriptEngine(IMemoryCache cache, IOptions options, IEnumerable? extensions = null) { - private readonly IJintExtension[] extensions; - private readonly Parser parser; - private readonly TimeSpan timeoutScript; - private readonly TimeSpan timeoutExecution; + parser = new Parser(cache); - public JintScriptEngine(IMemoryCache cache, IOptions options, IEnumerable? extensions = null) - { - parser = new Parser(cache); + timeoutScript = options.Value.TimeoutScript; + timeoutExecution = options.Value.TimeoutExecution; - timeoutScript = options.Value.TimeoutScript; - timeoutExecution = options.Value.TimeoutExecution; + this.extensions = extensions?.ToArray() ?? Array.Empty(); + } - this.extensions = extensions?.ToArray() ?? Array.Empty(); - } + public async Task ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default, + CancellationToken ct = default) + { + Guard.NotNull(vars); + Guard.NotNullOrEmpty(script); - public async Task ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default, - CancellationToken ct = default) + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - Guard.NotNull(vars); - Guard.NotNullOrEmpty(script); + // Enforce a timeout after a configured time span. + combined.CancelAfter(timeoutExecution); - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a timeout after a configured time span. - combined.CancelAfter(timeoutExecution); - - var context = - CreateEngine(options, combined.Token) - .Extend(vars, options) - .Extend(extensions) - .ExtendAsync(extensions); + var context = + CreateEngine(options, combined.Token) + .Extend(vars, options) + .Extend(extensions) + .ExtendAsync(extensions); - context.Engine.SetValue("complete", new Action(value => - { - context.Complete(JsonMapper.Map(value)); - })); + context.Engine.SetValue("complete", new Action(value => + { + context.Complete(JsonMapper.Map(value)); + })); - var result = Execute(context.Engine, script); + var result = Execute(context.Engine, script); - return await context.CompleteAsync() ?? JsonMapper.Map(result); - } + return await context.CompleteAsync() ?? JsonMapper.Map(result); } + } - public async Task TransformAsync(DataScriptVars vars, string script, ScriptOptions options = default, - CancellationToken ct = default) + public async Task TransformAsync(DataScriptVars vars, string script, ScriptOptions options = default, + CancellationToken ct = default) + { + Guard.NotNull(vars); + Guard.NotNullOrEmpty(script); + + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - Guard.NotNull(vars); - Guard.NotNullOrEmpty(script); + // Enforce a timeout after a configured time span. + combined.CancelAfter(timeoutExecution); - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a timeout after a configured time span. - combined.CancelAfter(timeoutExecution); + var context = + CreateEngine(options, combined.Token) + .Extend(vars, options) + .Extend(extensions) + .ExtendAsync(extensions); - var context = - CreateEngine(options, combined.Token) - .Extend(vars, options) - .Extend(extensions) - .ExtendAsync(extensions); + context.Engine.SetValue("complete", new Action(_ => + { + context.Complete(vars.Data!); + })); - context.Engine.SetValue("complete", new Action(_ => - { - context.Complete(vars.Data!); - })); + context.Engine.SetValue("replace", new Action(() => + { + var dataInstance = context.Engine.GetValue("ctx").AsObject().Get("data"); - context.Engine.SetValue("replace", new Action(() => + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) { - var dataInstance = context.Engine.GetValue("ctx").AsObject().Get("data"); - - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + if (!context.IsCompleted && data.TryUpdate(out var modified)) { - if (!context.IsCompleted && data.TryUpdate(out var modified)) - { - context.Complete(modified); - } + context.Complete(modified); } - })); + } + })); - Execute(context.Engine, script); + Execute(context.Engine, script); - return await context.CompleteAsync() ?? vars.Data!; - } + return await context.CompleteAsync() ?? vars.Data!; } + } - public JsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default) - { - Guard.NotNull(vars); - Guard.NotNullOrEmpty(script); + public JsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default) + { + Guard.NotNull(vars); + Guard.NotNullOrEmpty(script); - var context = - CreateEngine(options, default) - .Extend(vars, options) - .Extend(extensions); + var context = + CreateEngine(options, default) + .Extend(vars, options) + .Extend(extensions); - var result = Execute(context.Engine, script); + var result = Execute(context.Engine, script); - return JsonMapper.Map(result); - } + return JsonMapper.Map(result); + } - private ScriptExecutionContext CreateEngine(ScriptOptions options, CancellationToken ct) + private ScriptExecutionContext CreateEngine(ScriptOptions options, CancellationToken ct) + { + if (Debugger.IsAttached) { - if (Debugger.IsAttached) - { - ct = default; - } - - var engine = new Engine(engineOptions => - { - engineOptions.AddObjectConverter(DefaultConverter.Instance); - engineOptions.SetReferencesResolver(NullPropagation.Instance); - engineOptions.Strict(); + ct = default; + } - if (!Debugger.IsAttached) - { - engineOptions.TimeoutInterval(timeoutScript); - engineOptions.CancellationToken(ct); - } - }); + var engine = new Engine(engineOptions => + { + engineOptions.AddObjectConverter(DefaultConverter.Instance); + engineOptions.SetReferencesResolver(NullPropagation.Instance); + engineOptions.Strict(); - if (options.CanDisallow) + if (!Debugger.IsAttached) { - engine.AddDisallow(); + engineOptions.TimeoutInterval(timeoutScript); + engineOptions.CancellationToken(ct); } + }); - if (options.CanReject) - { - engine.AddReject(); - } + if (options.CanDisallow) + { + engine.AddDisallow(); + } - foreach (var extension in extensions) - { - extension.Extend(engine); - } + if (options.CanReject) + { + engine.AddReject(); + } - return new ScriptExecutionContext(engine, ct); + foreach (var extension in extensions) + { + extension.Extend(engine); } - private JsValue Execute(Engine engine, string script) + return new ScriptExecutionContext(engine, ct); + } + + private JsValue Execute(Engine engine, string script) + { + try { - try - { - var program = parser.Parse(script); + var program = parser.Parse(script); - return engine.Evaluate(program); - } - catch (ArgumentException ex) - { - throw new ValidationException(T.Get("common.jsParseError", new { error = ex.Message })); - } - catch (JavaScriptException ex) - { - throw new ValidationException(T.Get("common.jsError", new { message = ex.Message })); - } - catch (ParserException ex) - { - throw new ValidationException(T.Get("common.jsError", new { message = ex.Message })); - } - catch (DomainException) - { - throw; - } - catch (Exception ex) - { - throw new ValidationException(T.Get("common.jsError", new { message = ex.GetType().Name }), ex); - } + return engine.Evaluate(program); + } + catch (ArgumentException ex) + { + throw new ValidationException(T.Get("common.jsParseError", new { error = ex.Message })); + } + catch (JavaScriptException ex) + { + throw new ValidationException(T.Get("common.jsError", new { message = ex.Message })); + } + catch (ParserException ex) + { + throw new ValidationException(T.Get("common.jsError", new { message = ex.Message })); } + catch (DomainException) + { + throw; + } + catch (Exception ex) + { + throw new ValidationException(T.Get("common.jsError", new { message = ex.GetType().Name }), ex); + } + } - public void Describe(AddDescription describe, ScriptScope scope) + public void Describe(AddDescription describe, ScriptScope scope) + { + if (scope.HasFlag(ScriptScope.ContentTrigger) || scope.HasFlag(ScriptScope.AssetTrigger)) { - if (scope.HasFlag(ScriptScope.ContentTrigger) || scope.HasFlag(ScriptScope.AssetTrigger)) - { - return; - } + return; + } - if (scope.HasFlag(ScriptScope.Transform) || scope.HasFlag(ScriptScope.ContentScript)) - { - describe(JsonType.Function, "replace()", - Resources.ScriptingReplace); - } + if (scope.HasFlag(ScriptScope.Transform) || scope.HasFlag(ScriptScope.ContentScript)) + { + describe(JsonType.Function, "replace()", + Resources.ScriptingReplace); + } - describe(JsonType.Function, "disallow(reason)", - Resources.ScriptingDisallow); + describe(JsonType.Function, "disallow(reason)", + Resources.ScriptingDisallow); - describe(JsonType.Function, "reject(reason)", - Resources.ScriptingReject); + describe(JsonType.Function, "reject(reason)", + Resources.ScriptingReject); - describe(JsonType.Function, "complete()", - Resources.ScriptingComplete); - } + describe(JsonType.Function, "complete()", + Resources.ScriptingComplete); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptOptions.cs index cf778749a3..2580da9cc7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptOptions.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class JintScriptOptions { - public sealed class JintScriptOptions - { - public TimeSpan TimeoutScript { get; set; } = TimeSpan.FromMilliseconds(200); + public TimeSpan TimeoutScript { get; set; } = TimeSpan.FromMilliseconds(200); - public TimeSpan TimeoutExecution { get; set; } = TimeSpan.FromMilliseconds(4000); - } + public TimeSpan TimeoutExecution { get; set; } = TimeSpan.FromMilliseconds(4000); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs index a41f476a4b..d9e53ee4b3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs @@ -13,57 +13,56 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public static class JintUser { - public static class JintUser + private static readonly char[] ClaimSeparators = { '/', '.', ':' }; + + public static JsValue Create(Engine engine, IUser user) { - private static readonly char[] ClaimSeparators = { '/', '.', ':' }; + var isClient = user.Claims.Any(x => x.Type == OpenIdClaims.ClientId); - public static JsValue Create(Engine engine, IUser user) - { - var isClient = user.Claims.Any(x => x.Type == OpenIdClaims.ClientId); + return CreateUser( + engine, + user.Id, + isClient, + user.Email, + user.Claims.DisplayName(), + user.Claims); + } - return CreateUser( - engine, - user.Id, - isClient, - user.Email, - user.Claims.DisplayName(), - user.Claims); - } + public static JsValue Create(Engine engine, ClaimsPrincipal principal) + { + var token = principal.Token(); - public static JsValue Create(Engine engine, ClaimsPrincipal principal) - { - var token = principal.Token(); + return CreateUser( + engine, + token?.Identifier ?? string.Empty, + token?.Type != RefTokenType.Subject, + principal.OpenIdEmail()!, + principal.Claims.DisplayName(), + principal.Claims); + } - return CreateUser( - engine, - token?.Identifier ?? string.Empty, - token?.Type != RefTokenType.Subject, - principal.OpenIdEmail()!, - principal.Claims.DisplayName(), - principal.Claims); - } + private static JsValue CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable allClaims) + { + var claims = + allClaims.GroupBy(x => x.Type.Split(ClaimSeparators)[^1]) + .ToDictionary( + x => x.Key, + x => x.Select(y => y.Value).ToArray()); - private static JsValue CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable allClaims) + var result = new Dictionary { - var claims = - allClaims.GroupBy(x => x.Type.Split(ClaimSeparators)[^1]) - .ToDictionary( - x => x.Key, - x => x.Select(y => y.Value).ToArray()); - - var result = new Dictionary - { - ["id"] = id, - ["email"] = email, - ["isClient"] = isClient, - ["isUser"] = !isClient, - ["name"] = name, - ["claims"] = claims - }; + ["id"] = id, + ["email"] = email, + ["isClient"] = isClient, + ["isUser"] = !isClient, + ["name"] = name, + ["claims"] = claims + }; - return JsValue.FromObject(engine, result); - } + return JsValue.FromObject(engine, result); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JsonType.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JsonType.cs index d39d4936f9..3ca6777cd5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JsonType.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JsonType.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public enum JsonType { - public enum JsonType - { - Any, - Array, - Boolean, - Function, - Number, - Object, - String - } + Any, + Array, + Boolean, + Function, + Number, + Object, + String } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs index 7aff9fd396..5820d00cdd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs @@ -10,34 +10,33 @@ using Jint.Runtime.Interop; using Jint.Runtime.References; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class NullPropagation : IReferenceResolver { - public sealed class NullPropagation : IReferenceResolver - { - public static readonly NullPropagation Instance = new NullPropagation(); + public static readonly NullPropagation Instance = new NullPropagation(); - public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value) - { - value = reference.GetBase(); + public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value) + { + value = reference.GetBase(); - return true; - } + return true; + } - public bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value) - { - return value.IsNull() || value.IsUndefined(); - } + public bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value) + { + return value.IsNull() || value.IsUndefined(); + } - public bool TryGetCallable(Engine engine, object reference, out JsValue value) - { - value = new ClrFunctionInstance(engine, "anonymous", (thisObj, _) => thisObj); + public bool TryGetCallable(Engine engine, object reference, out JsValue value) + { + value = new ClrFunctionInstance(engine, "anonymous", (thisObj, _) => thisObj); - return true; - } + return true; + } - public bool CheckCoercible(JsValue value) - { - return true; - } + public bool CheckCoercible(JsValue value) + { + return true; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs index e1f0c97591..796c5c21ea 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs @@ -8,28 +8,27 @@ using System.Diagnostics.CodeAnalysis; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public class ScriptContext : Dictionary { - public class ScriptContext : Dictionary + public ScriptContext() + : base(StringComparer.OrdinalIgnoreCase) { - public ScriptContext() - : base(StringComparer.OrdinalIgnoreCase) - { - } - - public bool TryGetValue(string key, [MaybeNullWhen(false)] out T value) - { - Guard.NotNull(key); + } - value = default!; + public bool TryGetValue(string key, [MaybeNullWhen(false)] out T value) + { + Guard.NotNull(key); - if (TryGetValue(key, out var temp) && temp is T typed) - { - value = typed; - return true; - } + value = default!; - return false; + if (TryGetValue(key, out var temp) && temp is T typed) + { + value = typed; + return true; } + + return false; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs index a1c3b10cea..53b685ade9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs @@ -9,166 +9,165 @@ using Squidex.Infrastructure.Tasks; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.Scripting -{ - public abstract class ScriptExecutionContext : ScriptContext - { - public Engine Engine { get; } +namespace Squidex.Domain.Apps.Core.Scripting; - protected ScriptExecutionContext(Engine engine) - { - Engine = engine; - } +public abstract class ScriptExecutionContext : ScriptContext +{ + public Engine Engine { get; } - public abstract void Schedule(Func action); + protected ScriptExecutionContext(Engine engine) + { + Engine = engine; } + public abstract void Schedule(Func action); +} + #pragma warning disable MA0048 // File name must match type name - public interface IScheduler +public interface IScheduler #pragma warning restore MA0048 // File name must match type name - { - void Run(Action? action); +{ + void Run(Action? action); - void Run(Action? action, T argument); - } + void Run(Action? action, T argument); +} - public sealed class ScriptExecutionContext : ScriptExecutionContext, IScheduler +public sealed class ScriptExecutionContext : ScriptExecutionContext, IScheduler +{ + private readonly TaskCompletionSource tcs = new TaskCompletionSource(); + private readonly CancellationToken cancellationToken; + private int pendingTasks; + + public bool IsCompleted { - private readonly TaskCompletionSource tcs = new TaskCompletionSource(); - private readonly CancellationToken cancellationToken; - private int pendingTasks; + get => tcs.Task.Status is TaskStatus.RanToCompletion or TaskStatus.Faulted; + } - public bool IsCompleted - { - get => tcs.Task.Status is TaskStatus.RanToCompletion or TaskStatus.Faulted; - } + internal ScriptExecutionContext(Engine engine, CancellationToken cancellationToken) + : base(engine) + { + this.cancellationToken = cancellationToken; + } - internal ScriptExecutionContext(Engine engine, CancellationToken cancellationToken) - : base(engine) + public Task CompleteAsync() + { + if (pendingTasks <= 0) { - this.cancellationToken = cancellationToken; + tcs.TrySetResult(default); } - public Task CompleteAsync() - { - if (pendingTasks <= 0) - { - tcs.TrySetResult(default); - } + return tcs.Task.WithCancellation(cancellationToken); + } - return tcs.Task.WithCancellation(cancellationToken); - } + public void Complete(T value) + { + tcs.TrySetResult(value); + } - public void Complete(T value) + public override void Schedule(Func action) + { + if (IsCompleted) { - tcs.TrySetResult(value); + return; } - public override void Schedule(Func action) + async Task ScheduleAsync() { - if (IsCompleted) + try { - return; - } + Interlocked.Increment(ref pendingTasks); - async Task ScheduleAsync() - { - try - { - Interlocked.Increment(ref pendingTasks); + await action(this, cancellationToken); - await action(this, cancellationToken); - - if (Interlocked.Decrement(ref pendingTasks) <= 0) - { - tcs.TrySetResult(default); - } - } - catch (Exception ex) + if (Interlocked.Decrement(ref pendingTasks) <= 0) { - tcs.TrySetException(ex); + tcs.TrySetResult(default); } } - - ScheduleAsync().Forget(); + catch (Exception ex) + { + tcs.TrySetException(ex); + } } - public ScriptExecutionContext ExtendAsync(IEnumerable extensions) + ScheduleAsync().Forget(); + } + + public ScriptExecutionContext ExtendAsync(IEnumerable extensions) + { + foreach (var extension in extensions) { - foreach (var extension in extensions) - { - extension.ExtendAsync(this); - } + extension.ExtendAsync(this); + } - return this; + return this; + } + + public ScriptExecutionContext Extend(IEnumerable extensions) + { + foreach (var extension in extensions) + { + extension.Extend(this); } - public ScriptExecutionContext Extend(IEnumerable extensions) + return this; + } + + public ScriptExecutionContext Extend(ScriptVars vars, ScriptOptions options) + { + var engine = Engine; + + if (options.AsContext) { - foreach (var extension in extensions) + var contextInstance = new WritableContext(engine, vars); + + foreach (var (key, value) in vars.Where(x => x.Value != null)) { - extension.Extend(this); + this[key.ToCamelCase()] = value; } - return this; + engine.SetValue("ctx", contextInstance); + engine.SetValue("context", contextInstance); } - - public ScriptExecutionContext Extend(ScriptVars vars, ScriptOptions options) + else { - var engine = Engine; - - if (options.AsContext) + foreach (var (key, value) in vars) { - var contextInstance = new WritableContext(engine, vars); - - foreach (var (key, value) in vars.Where(x => x.Value != null)) - { - this[key.ToCamelCase()] = value; - } + var property = key.ToCamelCase(); - engine.SetValue("ctx", contextInstance); - engine.SetValue("context", contextInstance); - } - else - { - foreach (var (key, value) in vars) + if (value != null) { - var property = key.ToCamelCase(); + engine.SetValue(property, value); - if (value != null) - { - engine.SetValue(property, value); - - this[property] = value; - } + this[property] = value; } } + } - engine.SetValue("async", true); + engine.SetValue("async", true); - return this; - } + return this; + } - void IScheduler.Run(Action? action) + void IScheduler.Run(Action? action) + { + if (IsCompleted || action == null) { - if (IsCompleted || action == null) - { - return; - } - - Engine.ResetConstraints(); - action(); + return; } - void IScheduler.Run(Action? action, TArg argument) - { - if (IsCompleted || action == null) - { - return; - } + Engine.ResetConstraints(); + action(); + } - Engine.ResetConstraints(); - action(argument); + void IScheduler.Run(Action? action, TArg argument) + { + if (IsCompleted || action == null) + { + return; } + + Engine.ResetConstraints(); + action(argument); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOperations.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOperations.cs index a7a320416e..82436cf293 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOperations.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOperations.cs @@ -10,38 +10,37 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +internal static class ScriptOperations { - internal static class ScriptOperations - { - private delegate void MessageDelegate(string? message); + private delegate void MessageDelegate(string? message); - private static readonly MessageDelegate Disallow = message => - { - message = !string.IsNullOrWhiteSpace(message) ? message : T.Get("common.jsNotAllowed"); + private static readonly MessageDelegate Disallow = message => + { + message = !string.IsNullOrWhiteSpace(message) ? message : T.Get("common.jsNotAllowed"); - throw new DomainForbiddenException(message); - }; + throw new DomainForbiddenException(message); + }; - private static readonly MessageDelegate Reject = message => - { - message = !string.IsNullOrWhiteSpace(message) ? message : T.Get("common.jsRejected"); + private static readonly MessageDelegate Reject = message => + { + message = !string.IsNullOrWhiteSpace(message) ? message : T.Get("common.jsRejected"); - throw new ValidationException(message); - }; + throw new ValidationException(message); + }; - public static Engine AddDisallow(this Engine engine) - { - engine.SetValue("disallow", Disallow); + public static Engine AddDisallow(this Engine engine) + { + engine.SetValue("disallow", Disallow); - return engine; - } + return engine; + } - public static Engine AddReject(this Engine engine) - { - engine.SetValue("reject", Reject); + public static Engine AddReject(this Engine engine) + { + engine.SetValue("reject", Reject); - return engine; - } + return engine; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs index 52c8174cff..58cb1c91b7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public struct ScriptOptions { - public struct ScriptOptions - { - public bool CanReject { get; set; } + public bool CanReject { get; set; } - public bool CanDisallow { get; set; } + public bool CanDisallow { get; set; } - public bool AsContext { get; set; } + public bool AsContext { get; set; } - public override readonly string ToString() - { - return $"CanReject={CanReject}, CanDisallow={CanDisallow}, AsContext={AsContext}"; - } + public override readonly string ToString() + { + return $"CanReject={CanReject}, CanDisallow={CanDisallow}, AsContext={AsContext}"; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptScope.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptScope.cs index 3048853506..adf7dc0717 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptScope.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptScope.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +[Flags] +public enum ScriptScope { - [Flags] - public enum ScriptScope - { - None = 0, - AssetScript = 1, - AssetTrigger = 2, - Async = 4, - CommentTrigger = 8, - ContentScript = 16, - ContentTrigger = 32, - SchemaTrigger = 128, - Transform = 256, - UsageTrigger = 512 - } + None = 0, + AssetScript = 1, + AssetTrigger = 2, + Async = 4, + CommentTrigger = 8, + ContentScript = 16, + ContentTrigger = 32, + SchemaTrigger = 128, + Transform = 256, + UsageTrigger = 512 } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs index 964f553f77..1aeac926e7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs @@ -7,26 +7,25 @@ using System.Runtime.CompilerServices; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public class ScriptVars : ScriptContext { - public class ScriptVars : ScriptContext + public void SetValue(object? value, [CallerMemberName] string? key = null) { - public void SetValue(object? value, [CallerMemberName] string? key = null) + if (key != null) { - if (key != null) - { - this[key] = value; - } + this[key] = value; } + } - public T GetValue([CallerMemberName] string? key = null) + public T GetValue([CallerMemberName] string? key = null) + { + if (key != null && TryGetValue(key, out var temp) && temp is T result) { - if (key != null && TryGetValue(key, out var temp) && temp is T result) - { - return result; - } - - return default!; + return result; } + + return default!; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs index f58b79a656..eabf3cffff 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs @@ -21,452 +21,451 @@ using Squidex.Shared.Users; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed class ScriptingCompleter { - public sealed class ScriptingCompleter + private readonly IEnumerable descriptors; + private static readonly FilterSchema DynamicData = new FilterSchema(FilterSchemaType.Object) { - private readonly IEnumerable descriptors; - private static readonly FilterSchema DynamicData = new FilterSchema(FilterSchemaType.Object) - { - Fields = ReadonlyList.Create( - new FilterField(new FilterSchema(FilterSchemaType.Object), "my-field"), - new FilterField(FilterSchema.String, "my-field.iv")) - }; + Fields = ReadonlyList.Create( + new FilterField(new FilterSchema(FilterSchemaType.Object), "my-field"), + new FilterField(FilterSchema.String, "my-field.iv")) + }; - public ScriptingCompleter(IEnumerable descriptors) + public ScriptingCompleter(IEnumerable descriptors) + { + this.descriptors = descriptors; + } + + public IReadOnlyList Trigger(string type) + { + Guard.NotNull(type); + + switch (type) { - this.descriptors = descriptors; + case "AssetChanged": + return AssetTrigger(); + case "Comment": + return CommentTrigger(); + case "ContentChanged": + return ContentTrigger(DynamicData); + case "SchemaChanged": + return SchemaTrigger(); + case "Usage": + return UsageTrigger(); + default: + return new List(); } + } - public IReadOnlyList Trigger(string type) - { - Guard.NotNull(type); + public IReadOnlyList ContentScript(FilterSchema dataSchema) + { + Guard.NotNull(dataSchema); - switch (type) - { - case "AssetChanged": - return AssetTrigger(); - case "Comment": - return CommentTrigger(); - case "ContentChanged": - return ContentTrigger(DynamicData); - case "SchemaChanged": - return SchemaTrigger(); - case "Usage": - return UsageTrigger(); - default: - return new List(); - } - } + return new Process(descriptors, dataSchema.Flatten()).ContentScript(); + } - public IReadOnlyList ContentScript(FilterSchema dataSchema) - { - Guard.NotNull(dataSchema); + public IReadOnlyList ContentTrigger(FilterSchema dataSchema) + { + Guard.NotNull(dataSchema); - return new Process(descriptors, dataSchema.Flatten()).ContentScript(); - } + return new Process(descriptors, dataSchema.Flatten()).ContentTrigger(); + } - public IReadOnlyList ContentTrigger(FilterSchema dataSchema) - { - Guard.NotNull(dataSchema); + public IReadOnlyList AssetScript() + { + return new Process(descriptors).AssetScript(); + } - return new Process(descriptors, dataSchema.Flatten()).ContentTrigger(); - } + public IReadOnlyList AssetTrigger() + { + return new Process(descriptors).AssetTrigger(); + } - public IReadOnlyList AssetScript() - { - return new Process(descriptors).AssetScript(); - } + public IReadOnlyList CommentTrigger() + { + return new Process(descriptors).CommentTrigger(); + } - public IReadOnlyList AssetTrigger() - { - return new Process(descriptors).AssetTrigger(); - } + public IReadOnlyList SchemaTrigger() + { + return new Process(descriptors).SchemaTrigger(); + } - public IReadOnlyList CommentTrigger() - { - return new Process(descriptors).CommentTrigger(); - } + public IReadOnlyList UsageTrigger() + { + return new Process(descriptors).UsageTrigger(); + } - public IReadOnlyList SchemaTrigger() + private sealed class Process + { + private static readonly Regex PropertyRegex = new Regex(@"^(?!\d)[\w$]+$", RegexOptions.Compiled); + private readonly Stack prefixes = new Stack(); + private readonly Dictionary result = new Dictionary(); + private readonly IEnumerable descriptors; + private readonly FilterSchema? dataSchema; + + public Process(IEnumerable descriptors, FilterSchema? dataSchema = null) { - return new Process(descriptors).SchemaTrigger(); + this.descriptors = descriptors; + this.dataSchema = dataSchema; } - public IReadOnlyList UsageTrigger() + private IReadOnlyList Build() { - return new Process(descriptors).UsageTrigger(); + return result.Values.OrderBy(x => x.Path).ToList(); } - private sealed class Process + public IReadOnlyList SchemaTrigger() { - private static readonly Regex PropertyRegex = new Regex(@"^(?!\d)[\w$]+$", RegexOptions.Compiled); - private readonly Stack prefixes = new Stack(); - private readonly Dictionary result = new Dictionary(); - private readonly IEnumerable descriptors; - private readonly FilterSchema? dataSchema; - - public Process(IEnumerable descriptors, FilterSchema? dataSchema = null) - { - this.descriptors = descriptors; - this.dataSchema = dataSchema; - } + AddHelpers(ScriptScope.SchemaTrigger | ScriptScope.Async); - private IReadOnlyList Build() + AddObject("event", FieldDescriptions.Event, () => { - return result.Values.OrderBy(x => x.Path).ToList(); - } - - public IReadOnlyList SchemaTrigger() - { - AddHelpers(ScriptScope.SchemaTrigger | ScriptScope.Async); + AddType(typeof(EnrichedSchemaEvent)); + }); - AddObject("event", FieldDescriptions.Event, () => - { - AddType(typeof(EnrichedSchemaEvent)); - }); + return Build(); + } - return Build(); - } + public IReadOnlyList CommentTrigger() + { + AddHelpers(ScriptScope.CommentTrigger | ScriptScope.Async); - public IReadOnlyList CommentTrigger() + AddObject("event", FieldDescriptions.Event, () => { - AddHelpers(ScriptScope.CommentTrigger | ScriptScope.Async); + AddType(typeof(EnrichedCommentEvent)); + }); - AddObject("event", FieldDescriptions.Event, () => - { - AddType(typeof(EnrichedCommentEvent)); - }); + return Build(); + } - return Build(); - } + public IReadOnlyList UsageTrigger() + { + AddHelpers(ScriptScope.UsageTrigger | ScriptScope.Async); - public IReadOnlyList UsageTrigger() + AddObject("event", FieldDescriptions.Event, () => { - AddHelpers(ScriptScope.UsageTrigger | ScriptScope.Async); + AddType(typeof(EnrichedUsageExceededEvent)); + }); - AddObject("event", FieldDescriptions.Event, () => - { - AddType(typeof(EnrichedUsageExceededEvent)); - }); + return Build(); + } - return Build(); - } + public IReadOnlyList ContentScript() + { + var scope = ScriptScope.ContentScript | ScriptScope.Transform | ScriptScope.Async; - public IReadOnlyList ContentScript() + AddHelpers(scope); + + AddObject("ctx", FieldDescriptions.Context, () => { - var scope = ScriptScope.ContentScript | ScriptScope.Transform | ScriptScope.Async; + AddType(typeof(ContentScriptVars)); + }); - AddHelpers(scope); + return Build(); + } - AddObject("ctx", FieldDescriptions.Context, () => - { - AddType(typeof(ContentScriptVars)); - }); + public IReadOnlyList ContentTrigger() + { + var scope = ScriptScope.ContentTrigger | ScriptScope.Async; - return Build(); - } + AddHelpers(scope); - public IReadOnlyList ContentTrigger() + AddObject("event", FieldDescriptions.Event, () => { - var scope = ScriptScope.ContentTrigger | ScriptScope.Async; - - AddHelpers(scope); + AddType(typeof(EnrichedContentEvent)); + }); - AddObject("event", FieldDescriptions.Event, () => - { - AddType(typeof(EnrichedContentEvent)); - }); + return Build(); + } - return Build(); - } + public IReadOnlyList AssetTrigger() + { + AddHelpers(ScriptScope.AssetTrigger | ScriptScope.Async); - public IReadOnlyList AssetTrigger() + AddObject("event", FieldDescriptions.Event, () => { - AddHelpers(ScriptScope.AssetTrigger | ScriptScope.Async); + AddType(typeof(EnrichedAssetEvent)); + }); - AddObject("event", FieldDescriptions.Event, () => - { - AddType(typeof(EnrichedAssetEvent)); - }); + return Build(); + } - return Build(); - } + public IReadOnlyList AssetScript() + { + AddHelpers(ScriptScope.AssetScript | ScriptScope.Async); - public IReadOnlyList AssetScript() + AddObject("ctx", FieldDescriptions.Event, () => { - AddHelpers(ScriptScope.AssetScript | ScriptScope.Async); + AddType(typeof(AssetScriptVars)); + }); - AddObject("ctx", FieldDescriptions.Event, () => - { - AddType(typeof(AssetScriptVars)); - }); + return Build(); + } - return Build(); + private void AddHelpers(ScriptScope scope) + { + foreach (var descriptor in descriptors) + { + descriptor.Describe(Add, scope); } + } - private void AddHelpers(ScriptScope scope) + private void AddType(Type type) + { + foreach (var (name, description, propertyTypeOrNullable) in GetFields(type)) { - foreach (var descriptor in descriptors) + var propertyType = Nullable.GetUnderlyingType(propertyTypeOrNullable) ?? propertyTypeOrNullable; + + if (propertyType.IsEnum || + propertyType == typeof(string) || + propertyType == typeof(DomainId) || + propertyType == typeof(Instant) || + propertyType == typeof(Status)) { - descriptor.Describe(Add, scope); + AddString(name, description); } - } - - private void AddType(Type type) - { - foreach (var (name, description, propertyTypeOrNullable) in GetFields(type)) + else if (propertyType == typeof(int) || propertyType == typeof(long)) { - var propertyType = Nullable.GetUnderlyingType(propertyTypeOrNullable) ?? propertyTypeOrNullable; - - if (propertyType.IsEnum || - propertyType == typeof(string) || - propertyType == typeof(DomainId) || - propertyType == typeof(Instant) || - propertyType == typeof(Status)) - { - AddString(name, description); - } - else if (propertyType == typeof(int) || propertyType == typeof(long)) - { - AddNumber(name, description); - } - else if (propertyType == typeof(bool)) - { - AddBoolean(name, description); - } - else if (typeof(MulticastDelegate).IsAssignableFrom(propertyType.BaseType)) - { - AddFunction(name, description); - } - else if (propertyType == typeof(AssetMetadata)) + AddNumber(name, description); + } + else if (propertyType == typeof(bool)) + { + AddBoolean(name, description); + } + else if (typeof(MulticastDelegate).IsAssignableFrom(propertyType.BaseType)) + { + AddFunction(name, description); + } + else if (propertyType == typeof(AssetMetadata)) + { + AddObject(name, description, () => { - AddObject(name, description, () => - { - AddString("my-name", - FieldDescriptions.AssetMetadataValue); - }); - } - else if (propertyType == typeof(NamedId)) + AddString("my-name", + FieldDescriptions.AssetMetadataValue); + }); + } + else if (propertyType == typeof(NamedId)) + { + AddObject(name, description, () => { - AddObject(name, description, () => - { - AddString("id", - FieldDescriptions.NamedId); + AddString("id", + FieldDescriptions.NamedId); - AddString("name", - FieldDescriptions.NamedName); - }); - } - else if (propertyType == typeof(RefToken)) + AddString("name", + FieldDescriptions.NamedName); + }); + } + else if (propertyType == typeof(RefToken)) + { + AddObject(name, description, () => { - AddObject(name, description, () => - { - AddString("identifier", - FieldDescriptions.ActorIdentifier); + AddString("identifier", + FieldDescriptions.ActorIdentifier); - AddString("type", - FieldDescriptions.ActorType); - }); - } - else if (propertyType == typeof(IUser) || propertyType == typeof(ClaimsPrincipal)) + AddString("type", + FieldDescriptions.ActorType); + }); + } + else if (propertyType == typeof(IUser) || propertyType == typeof(ClaimsPrincipal)) + { + AddObject(name, description, () => { - AddObject(name, description, () => - { - AddString("id", - FieldDescriptions.UserId); + AddString("id", + FieldDescriptions.UserId); - AddString("email", - FieldDescriptions.UserEmail); + AddString("email", + FieldDescriptions.UserEmail); - AddBoolean("isClient", - FieldDescriptions.UserIsClient); + AddBoolean("isClient", + FieldDescriptions.UserIsClient); - AddBoolean("isUser", - FieldDescriptions.UserIsUser); + AddBoolean("isUser", + FieldDescriptions.UserIsUser); - AddObject("claims", FieldDescriptions.UserClaims, () => - { - AddArray("name", - FieldDescriptions.UsersClaimsValue); - }); - }); - } - else if (propertyType == typeof(ContentData) && dataSchema != null) - { - AddObject(name, description, () => + AddObject("claims", FieldDescriptions.UserClaims, () => { - AddData(); + AddArray("name", + FieldDescriptions.UsersClaimsValue); }); - } - else if (GetFields(propertyType).Any()) - { - AddObject(name, description, () => - { - AddType(propertyType); - }); - } - else if (propertyType.GetInterfaces().Contains(typeof(IEnumerable))) + }); + } + else if (propertyType == typeof(ContentData) && dataSchema != null) + { + AddObject(name, description, () => { - AddArray(name, description); - } + AddData(); + }); } - } - - private static IEnumerable<(string Name, string Description, Type Type)> GetFields(Type type) - { - foreach (var property in type.GetPublicProperties()) + else if (GetFields(propertyType).Any()) { - var descriptionKey = property.GetCustomAttribute()?.Name; - - if (descriptionKey == null) + AddObject(name, description, () => { - continue; - } - - var description = FieldDescriptions.ResourceManager.GetString(descriptionKey, CultureInfo.InvariantCulture)!; - - yield return (property.Name.ToCamelCase(), description, property.PropertyType); + AddType(propertyType); + }); + } + else if (propertyType.GetInterfaces().Contains(typeof(IEnumerable))) + { + AddArray(name, description); } } + } - private void AddData() + private static IEnumerable<(string Name, string Description, Type Type)> GetFields(Type type) + { + foreach (var property in type.GetPublicProperties()) { - if (dataSchema?.Fields == null) - { - return; - } + var descriptionKey = property.GetCustomAttribute()?.Name; - foreach (var field in dataSchema.Fields) + if (descriptionKey == null) { - switch (field.Schema.Type) - { - case FilterSchemaType.Any: - AddAny(field.Path, field.Description); - break; - case FilterSchemaType.Boolean: - AddBoolean(field.Path, field.Description); - break; - case FilterSchemaType.DateTime: - AddString(field.Path, field.Description); - break; - case FilterSchemaType.GeoObject: - AddObject(field.Path, field.Description); - break; - case FilterSchemaType.Guid: - AddString(field.Path, field.Description); - break; - case FilterSchemaType.Number: - AddNumber(field.Path, field.Description); - break; - case FilterSchemaType.Object: - AddObject(field.Path, field.Description); - break; - case FilterSchemaType.ObjectArray: - AddArray(field.Path, field.Description); - break; - case FilterSchemaType.String: - AddString(field.Path, field.Description); - break; - case FilterSchemaType.StringArray: - AddArray(field.Path, field.Description); - break; - } + continue; } - } - private void AddAny(string? name, string? description) - { - Add(JsonType.Any, name, description); - } + var description = FieldDescriptions.ResourceManager.GetString(descriptionKey, CultureInfo.InvariantCulture)!; - private void AddArray(string? name, string? description) - { - Add(JsonType.Array, name, description); + yield return (property.Name.ToCamelCase(), description, property.PropertyType); } + } - private void AddBoolean(string? name, string? description) + private void AddData() + { + if (dataSchema?.Fields == null) { - Add(JsonType.Boolean, name, description); + return; } - private void AddNumber(string? name, string? description) + foreach (var field in dataSchema.Fields) { - Add(JsonType.Number, name, description); + switch (field.Schema.Type) + { + case FilterSchemaType.Any: + AddAny(field.Path, field.Description); + break; + case FilterSchemaType.Boolean: + AddBoolean(field.Path, field.Description); + break; + case FilterSchemaType.DateTime: + AddString(field.Path, field.Description); + break; + case FilterSchemaType.GeoObject: + AddObject(field.Path, field.Description); + break; + case FilterSchemaType.Guid: + AddString(field.Path, field.Description); + break; + case FilterSchemaType.Number: + AddNumber(field.Path, field.Description); + break; + case FilterSchemaType.Object: + AddObject(field.Path, field.Description); + break; + case FilterSchemaType.ObjectArray: + AddArray(field.Path, field.Description); + break; + case FilterSchemaType.String: + AddString(field.Path, field.Description); + break; + case FilterSchemaType.StringArray: + AddArray(field.Path, field.Description); + break; + } } + } - private void AddFunction(string? name, string? description) - { - Add(JsonType.Function, name, description); - } + private void AddAny(string? name, string? description) + { + Add(JsonType.Any, name, description); + } - private void AddObject(string? name, string? description) - { - Add(JsonType.Object, name, description); - } + private void AddArray(string? name, string? description) + { + Add(JsonType.Array, name, description); + } - private void AddString(string? name, string? description) - { - Add(JsonType.String, name, description); - } + private void AddBoolean(string? name, string? description) + { + Add(JsonType.Boolean, name, description); + } - private void Add(JsonType type, string? name, string? description) - { - var parts = name?.Split('.') ?? Array.Empty(); + private void AddNumber(string? name, string? description) + { + Add(JsonType.Number, name, description); + } - foreach (var part in parts) - { - PushPrefix(part); - } + private void AddFunction(string? name, string? description) + { + Add(JsonType.Function, name, description); + } - if (prefixes.Count == 0) - { - return; - } + private void AddObject(string? name, string? description) + { + Add(JsonType.Object, name, description); + } - var path = string.Concat(prefixes.Reverse()); + private void AddString(string? name, string? description) + { + Add(JsonType.String, name, description); + } - result[path] = new ScriptingValue(path, type, description); + private void Add(JsonType type, string? name, string? description) + { + var parts = name?.Split('.') ?? Array.Empty(); - for (int i = 0; i < parts.Length; i++) - { - prefixes.Pop(); - } + foreach (var part in parts) + { + PushPrefix(part); } - private void AddObject(string name, string description, Action inner) + if (prefixes.Count == 0) { - Add(JsonType.Object, name, description); + return; + } - var parts = name.Split('.'); + var path = string.Concat(prefixes.Reverse()); - foreach (var part in parts) - { - PushPrefix(part); - } + result[path] = new ScriptingValue(path, type, description); - inner(); + for (int i = 0; i < parts.Length; i++) + { + prefixes.Pop(); + } + } - for (int i = 0; i < parts.Length; i++) - { - prefixes.Pop(); - } + private void AddObject(string name, string description, Action inner) + { + Add(JsonType.Object, name, description); + + var parts = name.Split('.'); + + foreach (var part in parts) + { + PushPrefix(part); } - private void PushPrefix(string name) + inner(); + + for (int i = 0; i < parts.Length; i++) { - if (prefixes.Count == 0) - { - prefixes.Push(name); - } - else if (PropertyRegex.IsMatch(name)) - { - prefixes.Push($".{name}"); - } - else - { - prefixes.Push($"['{name}']"); - } + prefixes.Pop(); + } + } + + private void PushPrefix(string name) + { + if (prefixes.Count == 0) + { + prefixes.Push(name); + } + else if (PropertyRegex.IsMatch(name)) + { + prefixes.Push($".{name}"); + } + else + { + prefixes.Push($"['{name}']"); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingValue.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingValue.cs index b78d50217f..bf5c5492ff 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingValue.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingValue.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.Scripting -{ - public sealed record ScriptingValue(string Path, JsonType Type, string? Description); -} +namespace Squidex.Domain.Apps.Core.Scripting; + +public sealed record ScriptingValue(string Path, JsonType Type, string? Description); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs index fe842af7ee..d177f19e45 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs @@ -10,35 +10,34 @@ using Jint.Native.Object; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Scripting; + +internal sealed class WritableContext : ObjectInstance { - internal sealed class WritableContext : ObjectInstance + private readonly ScriptVars vars; + + public WritableContext(Engine engine, ScriptVars vars) + : base(engine) { - private readonly ScriptVars vars; + this.vars = vars; - public WritableContext(Engine engine, ScriptVars vars) - : base(engine) + foreach (var (key, value) in vars) { - this.vars = vars; + var property = key.ToCamelCase(); - foreach (var (key, value) in vars) + if (value != null) { - var property = key.ToCamelCase(); - - if (value != null) - { - FastAddProperty(property, FromObject(engine, value), true, true, true); - } + FastAddProperty(property, FromObject(engine, value), true, true, true); } } + } - public override bool Set(JsValue property, JsValue value, JsValue receiver) - { - var propertyName = property.AsString(); + public override bool Set(JsValue property, JsValue value, JsValue receiver) + { + var propertyName = property.AsString(); - vars[propertyName] = value.ToObject(); + vars[propertyName] = value.ToObject(); - return base.Set(property, value, receiver); - } + return base.Set(property, value, receiver); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs index 9e56704ead..ff3d66b418 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs @@ -7,10 +7,9 @@ using System.Reflection; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core; + +public static class SquidexCoreOperations { - public static class SquidexCoreOperations - { - public static readonly Assembly Assembly = typeof(SquidexCoreOperations).Assembly; - } + public static readonly Assembly Assembly = typeof(SquidexCoreOperations).Assembly; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs index 74476816ab..838094e17f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs @@ -9,14 +9,13 @@ using Squidex.Infrastructure.Security; using Squidex.Messaging.Subscriptions; -namespace Squidex.Domain.Apps.Core.Subscriptions +namespace Squidex.Domain.Apps.Core.Subscriptions; + +public abstract class AppSubscription : ISubscription { - public abstract class AppSubscription : ISubscription - { - public DomainId AppId { get; set; } + public DomainId AppId { get; set; } - public PermissionSet Permissions { get; set; } + public PermissionSet Permissions { get; set; } - public abstract ValueTask ShouldHandle(object message); - } + public abstract ValueTask ShouldHandle(object message); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs index d13e7c0ac1..bdd31a1f60 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs @@ -9,67 +9,66 @@ using Squidex.Domain.Apps.Events.Assets; using Squidex.Shared; -namespace Squidex.Domain.Apps.Core.Subscriptions +namespace Squidex.Domain.Apps.Core.Subscriptions; + +public sealed class AssetSubscription : AppSubscription { - public sealed class AssetSubscription : AppSubscription - { - public EnrichedAssetEventType? Type { get; set; } + public EnrichedAssetEventType? Type { get; set; } - public override ValueTask ShouldHandle(object message) - { - return new ValueTask(ShouldHandleCore(message)); - } + public override ValueTask ShouldHandle(object message) + { + return new ValueTask(ShouldHandleCore(message)); + } - private bool ShouldHandleCore(object message) + private bool ShouldHandleCore(object message) + { + switch (message) { - switch (message) - { - case EnrichedAssetEvent enrichedAssetEvent: - return ShouldHandle(enrichedAssetEvent); - case AssetEvent assetEvent: - return ShouldHandle(assetEvent); - default: - return false; - } + case EnrichedAssetEvent enrichedAssetEvent: + return ShouldHandle(enrichedAssetEvent); + case AssetEvent assetEvent: + return ShouldHandle(assetEvent); + default: + return false; } + } - private bool ShouldHandle(EnrichedAssetEvent @event) - { - return CheckType(@event) && CheckPermission(@event.AppId.Name); - } + private bool ShouldHandle(EnrichedAssetEvent @event) + { + return CheckType(@event) && CheckPermission(@event.AppId.Name); + } - private bool ShouldHandle(AssetEvent @event) - { - return CheckType(@event) && CheckPermission(@event.AppId.Name); - } + private bool ShouldHandle(AssetEvent @event) + { + return CheckType(@event) && CheckPermission(@event.AppId.Name); + } - private bool CheckType(EnrichedAssetEvent @event) - { - return Type == null || Type.Value == @event.Type; - } + private bool CheckType(EnrichedAssetEvent @event) + { + return Type == null || Type.Value == @event.Type; + } - private bool CheckType(AssetEvent @event) + private bool CheckType(AssetEvent @event) + { + switch (Type) { - switch (Type) - { - case EnrichedAssetEventType.Created: - return @event is AssetCreated; - case EnrichedAssetEventType.Deleted: - return @event is AssetDeleted; - case EnrichedAssetEventType.Annotated: - return @event is AssetAnnotated; - case EnrichedAssetEventType.Updated: - return @event is AssetUpdated; - default: - return true; - } + case EnrichedAssetEventType.Created: + return @event is AssetCreated; + case EnrichedAssetEventType.Deleted: + return @event is AssetDeleted; + case EnrichedAssetEventType.Annotated: + return @event is AssetAnnotated; + case EnrichedAssetEventType.Updated: + return @event is AssetUpdated; + default: + return true; } + } - private bool CheckPermission(string appName) - { - var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appName); + private bool CheckPermission(string appName) + { + var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appName); - return Permissions.Includes(permission); - } + return Permissions.Includes(permission); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs index 0eef9fb213..052527d70c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs @@ -9,82 +9,81 @@ using Squidex.Domain.Apps.Events.Contents; using Squidex.Shared; -namespace Squidex.Domain.Apps.Core.Subscriptions +namespace Squidex.Domain.Apps.Core.Subscriptions; + +public sealed class ContentSubscription : AppSubscription { - public sealed class ContentSubscription : AppSubscription - { - public string? SchemaName { get; set; } + public string? SchemaName { get; set; } - public EnrichedContentEventType? Type { get; set; } + public EnrichedContentEventType? Type { get; set; } - public override ValueTask ShouldHandle(object message) - { - return new ValueTask(ShouldHandleCore(message)); - } + public override ValueTask ShouldHandle(object message) + { + return new ValueTask(ShouldHandleCore(message)); + } - private bool ShouldHandleCore(object message) + private bool ShouldHandleCore(object message) + { + switch (message) { - switch (message) - { - case EnrichedContentEvent enrichedContentEvent: - return ShouldHandle(enrichedContentEvent); - case ContentEvent contentEvent: - return ShouldHandle(contentEvent); - default: - return false; - } + case EnrichedContentEvent enrichedContentEvent: + return ShouldHandle(enrichedContentEvent); + case ContentEvent contentEvent: + return ShouldHandle(contentEvent); + default: + return false; } + } - private bool ShouldHandle(EnrichedContentEvent @event) - { - var schemaName = @event.SchemaId.Name; + private bool ShouldHandle(EnrichedContentEvent @event) + { + var schemaName = @event.SchemaId.Name; - return CheckSchema(schemaName) && CheckType(@event) && CheckPermission(@event.AppId.Name, schemaName); - } + return CheckSchema(schemaName) && CheckType(@event) && CheckPermission(@event.AppId.Name, schemaName); + } - private bool ShouldHandle(ContentEvent @event) - { - var schemaName = @event.SchemaId.Name; + private bool ShouldHandle(ContentEvent @event) + { + var schemaName = @event.SchemaId.Name; - return CheckSchema(schemaName) && CheckType(@event) && CheckPermission(@event.AppId.Name, schemaName); - } + return CheckSchema(schemaName) && CheckType(@event) && CheckPermission(@event.AppId.Name, schemaName); + } - private bool CheckSchema(string schemaName) - { - return string.IsNullOrWhiteSpace(SchemaName) || schemaName == SchemaName; - } + private bool CheckSchema(string schemaName) + { + return string.IsNullOrWhiteSpace(SchemaName) || schemaName == SchemaName; + } - private bool CheckType(EnrichedContentEvent @event) - { - return Type == null || Type.Value == @event.Type; - } + private bool CheckType(EnrichedContentEvent @event) + { + return Type == null || Type.Value == @event.Type; + } - private bool CheckType(ContentEvent @event) + private bool CheckType(ContentEvent @event) + { + switch (Type) { - switch (Type) - { - case EnrichedContentEventType.Created: - return @event is ContentCreated; - case EnrichedContentEventType.Deleted: - return @event is ContentDeleted; - case EnrichedContentEventType.Published: - return @event is ContentStatusChanged { Change: Contents.StatusChange.Published }; - case EnrichedContentEventType.Unpublished: - return @event is ContentStatusChanged { Change: Contents.StatusChange.Unpublished }; - case EnrichedContentEventType.StatusChanged: - return @event is ContentStatusChanged { Change: Contents.StatusChange.Change }; - case EnrichedContentEventType.Updated: - return @event is ContentUpdated; - default: - return true; - } + case EnrichedContentEventType.Created: + return @event is ContentCreated; + case EnrichedContentEventType.Deleted: + return @event is ContentDeleted; + case EnrichedContentEventType.Published: + return @event is ContentStatusChanged { Change: Contents.StatusChange.Published }; + case EnrichedContentEventType.Unpublished: + return @event is ContentStatusChanged { Change: Contents.StatusChange.Unpublished }; + case EnrichedContentEventType.StatusChanged: + return @event is ContentStatusChanged { Change: Contents.StatusChange.Change }; + case EnrichedContentEventType.Updated: + return @event is ContentUpdated; + default: + return true; } + } - private bool CheckPermission(string appName, string schemaName) - { - var permission = PermissionIds.ForApp(PermissionIds.AppContentsRead, appName, schemaName); + private bool CheckPermission(string appName, string schemaName) + { + var permission = PermissionIds.ForApp(PermissionIds.AppContentsRead, appName, schemaName); - return Permissions.Allows(permission); - } + return Permissions.Allows(permission); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs index b9efecf773..e75f5acefd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs @@ -9,79 +9,78 @@ using Squidex.Infrastructure; using Squidex.Messaging.Subscriptions; -namespace Squidex.Domain.Apps.Core.Subscriptions +namespace Squidex.Domain.Apps.Core.Subscriptions; + +public sealed class EventMessageEvaluator : IMessageEvaluator { - public sealed class EventMessageEvaluator : IMessageEvaluator + private readonly Dictionary> subscriptions = new Dictionary>(); + private readonly ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim(); + + public async ValueTask> GetSubscriptionsAsync(object message) { - private readonly Dictionary> subscriptions = new Dictionary>(); - private readonly ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim(); + if (message is not AppEvent appEvent) + { + return Enumerable.Empty(); + } - public async ValueTask> GetSubscriptionsAsync(object message) + readerWriterLock.EnterReadLock(); + try { - if (message is not AppEvent appEvent) - { - return Enumerable.Empty(); - } + List? result = null; - readerWriterLock.EnterReadLock(); - try + if (subscriptions.TryGetValue(appEvent.AppId.Id, out var appSubscriptions)) { - List? result = null; - - if (subscriptions.TryGetValue(appEvent.AppId.Id, out var appSubscriptions)) + foreach (var (id, subscription) in appSubscriptions) { - foreach (var (id, subscription) in appSubscriptions) + if (await subscription.ShouldHandle(appEvent)) { - if (await subscription.ShouldHandle(appEvent)) - { - result ??= new List(); - result.Add(id); - } + result ??= new List(); + result.Add(id); } } - - return result ?? Enumerable.Empty(); - } - finally - { - readerWriterLock.ExitReadLock(); } + + return result ?? Enumerable.Empty(); + } + finally + { + readerWriterLock.ExitReadLock(); } + } - public void SubscriptionAdded(Guid id, ISubscription subscription) + public void SubscriptionAdded(Guid id, ISubscription subscription) + { + if (subscription is not AppSubscription appSubscription) { - if (subscription is not AppSubscription appSubscription) - { - return; - } + return; + } - readerWriterLock.EnterWriteLock(); - try - { - subscriptions.GetOrAddNew(appSubscription.AppId)[id] = appSubscription; - } - finally - { - readerWriterLock.ExitWriteLock(); - } + readerWriterLock.EnterWriteLock(); + try + { + subscriptions.GetOrAddNew(appSubscription.AppId)[id] = appSubscription; } + finally + { + readerWriterLock.ExitWriteLock(); + } + } - public void SubscriptionRemoved(Guid id, ISubscription subscription) + public void SubscriptionRemoved(Guid id, ISubscription subscription) + { + if (subscription is not AppSubscription appSubscription) { - if (subscription is not AppSubscription appSubscription) - { - return; - } + return; + } - readerWriterLock.EnterWriteLock(); - try - { - subscriptions.GetOrAddDefault(appSubscription.AppId)?.Remove(id); - } - finally - { - readerWriterLock.ExitWriteLock(); - } + readerWriterLock.EnterWriteLock(); + try + { + subscriptions.GetOrAddDefault(appSubscription.AppId)?.Remove(id); + } + finally + { + readerWriterLock.ExitWriteLock(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs index fe6ceca7cb..0afc04bbab 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs @@ -9,41 +9,40 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Messaging.Subscriptions; -namespace Squidex.Domain.Apps.Core.Subscriptions +namespace Squidex.Domain.Apps.Core.Subscriptions; + +public sealed class EventMessageWrapper : IPayloadWrapper { - public sealed class EventMessageWrapper : IPayloadWrapper - { - private readonly IEnumerable subscriptionEventCreators; + private readonly IEnumerable subscriptionEventCreators; - public Envelope Event { get; } + public Envelope Event { get; } - object IPayloadWrapper.Message => Event.Payload; + object IPayloadWrapper.Message => Event.Payload; - public EventMessageWrapper(Envelope @event, IEnumerable subscriptionEventCreators) - { - Event = @event; + public EventMessageWrapper(Envelope @event, IEnumerable subscriptionEventCreators) + { + Event = @event; - this.subscriptionEventCreators = subscriptionEventCreators; - } + this.subscriptionEventCreators = subscriptionEventCreators; + } - public async ValueTask CreatePayloadAsync() + public async ValueTask CreatePayloadAsync() + { + foreach (var creator in subscriptionEventCreators) { - foreach (var creator in subscriptionEventCreators) + if (!creator.Handles(Event.Payload)) { - if (!creator.Handles(Event.Payload)) - { - continue; - } + continue; + } - var result = await creator.CreateEnrichedEventsAsync(Event, default); + var result = await creator.CreateEnrichedEventsAsync(Event, default); - if (result != null) - { - return result; - } + if (result != null) + { + return result; } - - return null!; } + + return null!; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ISubscriptionEventCreator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ISubscriptionEventCreator.cs index e8f0a8d7b5..db8673ee08 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ISubscriptionEventCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ISubscriptionEventCreator.cs @@ -9,13 +9,12 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Core.Subscriptions +namespace Squidex.Domain.Apps.Core.Subscriptions; + +public interface ISubscriptionEventCreator { - public interface ISubscriptionEventCreator - { - bool Handles(AppEvent @event); + bool Handles(AppEvent @event); - ValueTask CreateEnrichedEventsAsync(Envelope @event, - CancellationToken ct); - } + ValueTask CreateEnrichedEventsAsync(Envelope @event, + CancellationToken ct); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs index c5f2b458c8..fa2db26d59 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs @@ -9,54 +9,53 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Messaging.Subscriptions; -namespace Squidex.Domain.Apps.Core.Subscriptions +namespace Squidex.Domain.Apps.Core.Subscriptions; + +public sealed class SubscriptionPublisher : IEventConsumer { - public sealed class SubscriptionPublisher : IEventConsumer + private readonly ISubscriptionService subscriptionService; + private readonly IEnumerable subscriptionEventCreators; + + public string Name { - private readonly ISubscriptionService subscriptionService; - private readonly IEnumerable subscriptionEventCreators; + get => "Subscriptions"; + } - public string Name - { - get => "Subscriptions"; - } + public string EventsFilter + { + get => "^(content-|asset-)"; + } - public string EventsFilter - { - get => "^(content-|asset-)"; - } + public bool StartLatest + { + get => true; + } - public bool StartLatest - { - get => true; - } + public bool CanClear + { + get => false; + } - public bool CanClear - { - get => false; - } + public SubscriptionPublisher(ISubscriptionService subscriptionService, IEnumerable subscriptionEventCreators) + { + this.subscriptionService = subscriptionService; + this.subscriptionEventCreators = subscriptionEventCreators; + } - public SubscriptionPublisher(ISubscriptionService subscriptionService, IEnumerable subscriptionEventCreators) - { - this.subscriptionService = subscriptionService; - this.subscriptionEventCreators = subscriptionEventCreators; - } + public bool Handles(StoredEvent @event) + { + return subscriptionService.HasSubscriptions; + } - public bool Handles(StoredEvent @event) + public Task On(Envelope @event) + { + if (@event.Payload is not AppEvent) { - return subscriptionService.HasSubscriptions; + return Task.CompletedTask; } - public Task On(Envelope @event) - { - if (@event.Payload is not AppEvent) - { - return Task.CompletedTask; - } - - var wrapper = new EventMessageWrapper(@event.To(), subscriptionEventCreators); + var wrapper = new EventMessageWrapper(@event.To(), subscriptionEventCreators); - return subscriptionService.PublishAsync(wrapper); - } + return subscriptionService.PublishAsync(wrapper); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs index 5a6a93049f..0570d221b7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs @@ -7,35 +7,34 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Tags +namespace Squidex.Domain.Apps.Core.Tags; + +public interface ITagService { - public interface ITagService - { - Task> GetTagIdsAsync(DomainId id, string group, HashSet names, - CancellationToken ct = default); + Task> GetTagIdsAsync(DomainId id, string group, HashSet names, + CancellationToken ct = default); - Task> GetTagNamesAsync(DomainId id, string group, HashSet ids, - CancellationToken ct = default); + Task> GetTagNamesAsync(DomainId id, string group, HashSet ids, + CancellationToken ct = default); - Task UpdateAsync(DomainId id, string group, Dictionary updates, - CancellationToken ct = default); + Task UpdateAsync(DomainId id, string group, Dictionary updates, + CancellationToken ct = default); - Task GetTagsAsync(DomainId id, string group, - CancellationToken ct = default); + Task GetTagsAsync(DomainId id, string group, + CancellationToken ct = default); - Task GetExportableTagsAsync(DomainId id, string group, - CancellationToken ct = default); + Task GetExportableTagsAsync(DomainId id, string group, + CancellationToken ct = default); - Task RenameTagAsync(DomainId id, string group, string name, string newName, - CancellationToken ct = default); + Task RenameTagAsync(DomainId id, string group, string name, string newName, + CancellationToken ct = default); - Task RebuildTagsAsync(DomainId id, string group, TagsExport export, - CancellationToken ct = default); + Task RebuildTagsAsync(DomainId id, string group, TagsExport export, + CancellationToken ct = default); - Task ClearAsync(DomainId id, string group, - CancellationToken ct = default); + Task ClearAsync(DomainId id, string group, + CancellationToken ct = default); - Task ClearAsync( - CancellationToken ct = default); - } + Task ClearAsync( + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs index ff47a46c11..bea32a3c6c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Tags +namespace Squidex.Domain.Apps.Core.Tags; + +public sealed record Tag { - public sealed record Tag - { - public string Name { get; set; } + public string Name { get; set; } - public int Count { get; set; } - } + public int Count { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs index 6a62b0a073..4e08e730b3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs @@ -7,15 +7,14 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Tags +namespace Squidex.Domain.Apps.Core.Tags; + +public static class TagGroups { - public static class TagGroups - { - public const string Assets = "Assets"; + public const string Assets = "Assets"; - public static string Schemas(DomainId schemaId) - { - return $"Schemas_{schemaId}"; - } + public static string Schemas(DomainId schemaId) + { + return $"Schemas_{schemaId}"; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs index b7e0c81b22..06de568d10 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Tags +namespace Squidex.Domain.Apps.Core.Tags; + +public class TagsExport { - public class TagsExport - { - public Dictionary Tags { get; set; } = new Dictionary(); + public Dictionary Tags { get; set; } = new Dictionary(); - public Dictionary Alias { get; set; } = new Dictionary(); - } + public Dictionary Alias { get; set; } = new Dictionary(); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs index ea849e469c..55aa600b4b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Tags +namespace Squidex.Domain.Apps.Core.Tags; + +public sealed class TagsSet : Dictionary { - public sealed class TagsSet : Dictionary - { - public long Version { get; set; } + public long Version { get; set; } - public TagsSet() - { - } + public TagsSet() + { + } - public TagsSet(IDictionary tags, long version) - : base(tags) - { - Version = version; - } + public TagsSet(IDictionary tags, long version) + : base(tags) + { + Version = version; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs index 2fc4865df4..6985da240d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs @@ -11,57 +11,56 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Templates.Extensions +namespace Squidex.Domain.Apps.Core.Templates.Extensions; + +public sealed class ContentFluidExtension : IFluidExtension { - public sealed class ContentFluidExtension : IFluidExtension + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) { - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - FluidValue.SetTypeMapping(x => new ObjectValue(x)); - FluidValue.SetTypeMapping(x => new ObjectValue(x)); - FluidValue.SetTypeMapping(x => new ObjectValue(x)); - FluidValue.SetTypeMapping(x => new JsonArrayFluidValue(x)); + FluidValue.SetTypeMapping(x => new ObjectValue(x)); + FluidValue.SetTypeMapping(x => new ObjectValue(x)); + FluidValue.SetTypeMapping(x => new ObjectValue(x)); + FluidValue.SetTypeMapping(x => new JsonArrayFluidValue(x)); - FluidValue.SetTypeMapping(source => + FluidValue.SetTypeMapping(source => + { + switch (source.Value) { - switch (source.Value) - { - case null: - return FluidValue.Create(null); - case bool b: - return FluidValue.Create(b); - case double n: - return FluidValue.Create(n); - case string s: - return FluidValue.Create(s); - case JsonObject o: - return new ObjectValue(o); - case JsonArray a: - return new JsonArrayFluidValue(a); - } + case null: + return FluidValue.Create(null); + case bool b: + return FluidValue.Create(b); + case double n: + return FluidValue.Create(n); + case string s: + return FluidValue.Create(s); + case JsonObject o: + return new ObjectValue(o); + case JsonArray a: + return new JsonArrayFluidValue(a); + } - ThrowHelper.InvalidOperationException(); - return default!; - }); + ThrowHelper.InvalidOperationException(); + return default!; + }); - memberAccessStrategy.Register((value, name) => + memberAccessStrategy.Register((value, name) => + { + if (value.Value is JsonObject o) { - if (value.Value is JsonObject o) - { - return o.GetValueOrDefault(name); - } + return o.GetValueOrDefault(name); + } - return null; - }); + return null; + }); - memberAccessStrategy.Register( - (value, name) => value.GetValueOrDefault(name)); + memberAccessStrategy.Register( + (value, name) => value.GetValueOrDefault(name)); - memberAccessStrategy.Register( - (value, name) => value.GetValueOrDefault(name).Value); + memberAccessStrategy.Register( + (value, name) => value.GetValueOrDefault(name).Value); - memberAccessStrategy.Register( - (value, name) => value.GetValueOrDefault(name).Value); - } + memberAccessStrategy.Register( + (value, name) => value.GetValueOrDefault(name).Value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs index 9902e70d8f..6c04dd29f1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs @@ -11,84 +11,83 @@ using NodaTime; using NodaTime.Text; -namespace Squidex.Domain.Apps.Core.Templates.Extensions +namespace Squidex.Domain.Apps.Core.Templates.Extensions; + +public class DateTimeFluidExtension : IFluidExtension { - public class DateTimeFluidExtension : IFluidExtension + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) { - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - TemplateContext.GlobalFilters.AddFilter("format_date", FormatDate); + TemplateContext.GlobalFilters.AddFilter("format_date", FormatDate); - TemplateContext.GlobalFilters.AddFilter("timestamp", FormatTimestamp); - TemplateContext.GlobalFilters.AddFilter("timestamp_sec", FormatTimestampSec); - } + TemplateContext.GlobalFilters.AddFilter("timestamp", FormatTimestamp); + TemplateContext.GlobalFilters.AddFilter("timestamp_sec", FormatTimestampSec); + } - public static FluidValue FormatTimestamp(FluidValue input, FilterArguments arguments, TemplateContext context) - { - return FormatDate(input, x => FluidValue.Create(x.ToUnixTimeMilliseconds())); - } + public static FluidValue FormatTimestamp(FluidValue input, FilterArguments arguments, TemplateContext context) + { + return FormatDate(input, x => FluidValue.Create(x.ToUnixTimeMilliseconds())); + } - public static FluidValue FormatTimestampSec(FluidValue input, FilterArguments arguments, TemplateContext context) - { - return FormatDate(input, x => FluidValue.Create(x.ToUnixTimeMilliseconds() / 1000)); - } + public static FluidValue FormatTimestampSec(FluidValue input, FilterArguments arguments, TemplateContext context) + { + return FormatDate(input, x => FluidValue.Create(x.ToUnixTimeMilliseconds() / 1000)); + } - public static FluidValue FormatDate(FluidValue input, FilterArguments arguments, TemplateContext context) + public static FluidValue FormatDate(FluidValue input, FilterArguments arguments, TemplateContext context) + { + if (arguments.Count == 1) { - if (arguments.Count == 1) - { - return FormatDate(input, x => Format(arguments, x)); - } - - return input; + return FormatDate(input, x => Format(arguments, x)); } - private static FluidValue FormatDate(FluidValue input, Func formatter) - { - switch (input) - { - case DateTimeValue dateTime: - { - var value = (DateTimeOffset)dateTime.ToObjectValue(); + return input; + } - return formatter(value); - } + private static FluidValue FormatDate(FluidValue input, Func formatter) + { + switch (input) + { + case DateTimeValue dateTime: + { + var value = (DateTimeOffset)dateTime.ToObjectValue(); - case StringValue stringValue: - { - var value = stringValue.ToStringValue(); + return formatter(value); + } - var instant = InstantPattern.ExtendedIso.Parse(value); + case StringValue stringValue: + { + var value = stringValue.ToStringValue(); - if (instant.Success) - { - return formatter(instant.Value.ToDateTimeOffset()); - } + var instant = InstantPattern.ExtendedIso.Parse(value); - break; + if (instant.Success) + { + return formatter(instant.Value.ToDateTimeOffset()); } - case ObjectValue objectValue: - { - var value = objectValue.ToObjectValue(); + break; + } - if (value is Instant instant) - { - return formatter(instant.ToDateTimeOffset()); - } + case ObjectValue objectValue: + { + var value = objectValue.ToObjectValue(); - break; + if (value is Instant instant) + { + return formatter(instant.ToDateTimeOffset()); } - } - return input; + break; + } } - private static FluidValue Format(FilterArguments arguments, DateTimeOffset value) - { - var formatted = value.ToString(arguments.At(0).ToStringValue(), CultureInfo.InvariantCulture); + return input; + } - return new StringValue(formatted); - } + private static FluidValue Format(FilterArguments arguments, DateTimeOffset value) + { + var formatted = value.ToString(arguments.At(0).ToStringValue(), CultureInfo.InvariantCulture); + + return new StringValue(formatted); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/JsonArrayFluidValue.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/JsonArrayFluidValue.cs index 6984fa1661..d58b3dabe6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/JsonArrayFluidValue.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/JsonArrayFluidValue.cs @@ -11,94 +11,93 @@ using Fluid.Values; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Core.Templates.Extensions +namespace Squidex.Domain.Apps.Core.Templates.Extensions; + +public sealed class JsonArrayFluidValue : FluidValue { - public sealed class JsonArrayFluidValue : FluidValue - { - private readonly JsonArray value; + private readonly JsonArray value; - public override FluidValues Type { get; } = FluidValues.Array; + public override FluidValues Type { get; } = FluidValues.Array; - public JsonArrayFluidValue(JsonArray value) - { - this.value = value; - } + public JsonArrayFluidValue(JsonArray value) + { + this.value = value; + } - public override bool Equals(FluidValue other) - { - return other is JsonArrayFluidValue array && array.value.Equals(value); - } + public override bool Equals(FluidValue other) + { + return other is JsonArrayFluidValue array && array.value.Equals(value); + } - public override bool ToBooleanValue() - { - return true; - } + public override bool ToBooleanValue() + { + return true; + } - public override decimal ToNumberValue() - { - return 0; - } + public override decimal ToNumberValue() + { + return 0; + } - public override object ToObjectValue() - { - return new ObjectValue(value); - } + public override object ToObjectValue() + { + return new ObjectValue(value); + } - public override string ToStringValue() - { - return value.ToString()!; - } + public override string ToStringValue() + { + return value.ToString()!; + } - protected override FluidValue GetValue(string name, TemplateContext context) + protected override FluidValue GetValue(string name, TemplateContext context) + { + switch (name) { - switch (name) - { - case "size": - return NumberValue.Create(value.Count); - - case "first": - if (value.Count > 0) - { - return Create(value[0]); - } + case "size": + return NumberValue.Create(value.Count); - break; + case "first": + if (value.Count > 0) + { + return Create(value[0]); + } - case "last": - if (value.Count > 0) - { - return Create(value[^1]); - } + break; - break; - } + case "last": + if (value.Count > 0) + { + return Create(value[^1]); + } - return NilValue.Instance; + break; } - protected override FluidValue GetIndex(FluidValue index, TemplateContext context) - { - var i = (int)index.ToNumberValue(); - - if (i >= 0 && i < value.Count) - { - return Create(value[i]); - } + return NilValue.Instance; + } - return NilValue.Instance; - } + protected override FluidValue GetIndex(FluidValue index, TemplateContext context) + { + var i = (int)index.ToNumberValue(); - public override IEnumerable Enumerate() + if (i >= 0 && i < value.Count) { - foreach (var item in value) - { - yield return Create(item); - } + return Create(value[i]); } - public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + return NilValue.Instance; + } + + public override IEnumerable Enumerate() + { + foreach (var item in value) { - writer.Write(value); + yield return Create(item); } } + + public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + writer.Write(value); + } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs index e6cb0f0b63..3193aab0b5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs @@ -10,61 +10,60 @@ using Squidex.Infrastructure; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.Templates.Extensions +namespace Squidex.Domain.Apps.Core.Templates.Extensions; + +public sealed class StringFluidExtension : IFluidExtension { - public sealed class StringFluidExtension : IFluidExtension + private static readonly FilterDelegate Slugify = (input, arguments, context) => { - private static readonly FilterDelegate Slugify = (input, arguments, context) => + if (input is StringValue value) { - if (input is StringValue value) - { - var result = value.ToStringValue().Slugify(); + var result = value.ToStringValue().Slugify(); - return FluidValue.Create(result); - } + return FluidValue.Create(result); + } - return input; - }; + return input; + }; - private static readonly FilterDelegate Escape = (input, arguments, context) => - { - return FluidValue.Create(input.ToStringValue().JsonEscape()); - }; + private static readonly FilterDelegate Escape = (input, arguments, context) => + { + return FluidValue.Create(input.ToStringValue().JsonEscape()); + }; - private static readonly FilterDelegate Markdown2Text = (input, arguments, context) => - { - return FluidValue.Create(input.ToStringValue().Markdown2Text()); - }; + private static readonly FilterDelegate Markdown2Text = (input, arguments, context) => + { + return FluidValue.Create(input.ToStringValue().Markdown2Text()); + }; - private static readonly FilterDelegate Html2Text = (input, arguments, context) => - { - return FluidValue.Create(input.ToStringValue().Html2Text()); - }; + private static readonly FilterDelegate Html2Text = (input, arguments, context) => + { + return FluidValue.Create(input.ToStringValue().Html2Text()); + }; - private static readonly FilterDelegate Trim = (input, arguments, context) => - { - return FluidValue.Create(input.ToStringValue().Trim()); - }; + private static readonly FilterDelegate Trim = (input, arguments, context) => + { + return FluidValue.Create(input.ToStringValue().Trim()); + }; - private static readonly FilterDelegate MD5 = (input, arguments, context) => - { - return FluidValue.Create(input.ToStringValue().ToMD5()); - }; + private static readonly FilterDelegate MD5 = (input, arguments, context) => + { + return FluidValue.Create(input.ToStringValue().ToMD5()); + }; - private static readonly FilterDelegate Sha256 = (input, arguments, context) => - { - return FluidValue.Create(input.ToStringValue().ToSha256()); - }; + private static readonly FilterDelegate Sha256 = (input, arguments, context) => + { + return FluidValue.Create(input.ToStringValue().ToSha256()); + }; - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - TemplateContext.GlobalFilters.AddFilter("escape", Escape); - TemplateContext.GlobalFilters.AddFilter("html2text", Html2Text); - TemplateContext.GlobalFilters.AddFilter("markdown2text", Markdown2Text); - TemplateContext.GlobalFilters.AddFilter("md5", MD5); - TemplateContext.GlobalFilters.AddFilter("sha256", Sha256); - TemplateContext.GlobalFilters.AddFilter("slugify", Slugify); - TemplateContext.GlobalFilters.AddFilter("trim", Trim); - } + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + TemplateContext.GlobalFilters.AddFilter("escape", Escape); + TemplateContext.GlobalFilters.AddFilter("html2text", Html2Text); + TemplateContext.GlobalFilters.AddFilter("markdown2text", Markdown2Text); + TemplateContext.GlobalFilters.AddFilter("md5", MD5); + TemplateContext.GlobalFilters.AddFilter("sha256", Sha256); + TemplateContext.GlobalFilters.AddFilter("slugify", Slugify); + TemplateContext.GlobalFilters.AddFilter("trim", Trim); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringWordsFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringWordsFluidExtension.cs index 75f5f235ad..6a3f6c0487 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringWordsFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringWordsFluidExtension.cs @@ -9,24 +9,23 @@ using Fluid.Values; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.Templates.Extensions +namespace Squidex.Domain.Apps.Core.Templates.Extensions; + +public class StringWordsFluidExtension : IFluidExtension { - public class StringWordsFluidExtension : IFluidExtension + private static readonly FilterDelegate WordCount = (input, arguments, context) => { - private static readonly FilterDelegate WordCount = (input, arguments, context) => - { - return FluidValue.Create(input.ToStringValue().WordCount()); - }; + return FluidValue.Create(input.ToStringValue().WordCount()); + }; - private static readonly FilterDelegate CharacterCount = (input, arguments, context) => - { - return FluidValue.Create(input.ToStringValue().CharacterCount()); - }; + private static readonly FilterDelegate CharacterCount = (input, arguments, context) => + { + return FluidValue.Create(input.ToStringValue().CharacterCount()); + }; - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - TemplateContext.GlobalFilters.AddFilter("word_count", WordCount); - TemplateContext.GlobalFilters.AddFilter("character_count", CharacterCount); - } + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + TemplateContext.GlobalFilters.AddFilter("word_count", WordCount); + TemplateContext.GlobalFilters.AddFilter("character_count", CharacterCount); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs index 417ae09ae1..edf7eb7af8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs @@ -10,35 +10,34 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Core.Templates.Extensions +namespace Squidex.Domain.Apps.Core.Templates.Extensions; + +public sealed class UserFluidExtension : IFluidExtension { - public sealed class UserFluidExtension : IFluidExtension + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) { - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + memberAccessStrategy.Register((user, name) => { - memberAccessStrategy.Register((user, name) => + switch (name) { - switch (name) - { - case "id": - return new StringValue(user.Id); - case "email": - return new StringValue(user.Email); - case "name": - return new StringValue(user.Claims.DisplayName()); - default: - { - var claim = user.Claims.FirstOrDefault(x => string.Equals(name, x.Type, StringComparison.OrdinalIgnoreCase)); - - if (claim != null) - { - return new StringValue(claim.Value); - } + case "id": + return new StringValue(user.Id); + case "email": + return new StringValue(user.Email); + case "name": + return new StringValue(user.Claims.DisplayName()); + default: + { + var claim = user.Claims.FirstOrDefault(x => string.Equals(name, x.Type, StringComparison.OrdinalIgnoreCase)); - return NilValue.Instance; + if (claim != null) + { + return new StringValue(claim.Value); } - } - }); - } + + return NilValue.Instance; + } + } + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/FluidTemplateEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/FluidTemplateEngine.cs index 9b92ff5d3f..b5951b4ac7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/FluidTemplateEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/FluidTemplateEngine.cs @@ -9,82 +9,81 @@ using Fluid.Values; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.Templates +namespace Squidex.Domain.Apps.Core.Templates; + +public sealed class FluidTemplateEngine : ITemplateEngine { - public sealed class FluidTemplateEngine : ITemplateEngine - { - private readonly IEnumerable extensions; + private readonly IEnumerable extensions; - private sealed class SquidexTemplate : BaseFluidTemplate + private sealed class SquidexTemplate : BaseFluidTemplate + { + public static void Setup(IEnumerable extensions) { - public static void Setup(IEnumerable extensions) + foreach (var extension in extensions) { - foreach (var extension in extensions) - { - extension.RegisterLanguageExtensions(Factory); - } + extension.RegisterLanguageExtensions(Factory); } + } - public static void SetupTypes(IEnumerable extensions) - { - var globalTypes = TemplateContext.GlobalMemberAccessStrategy; - - globalTypes.MemberNameStrategy = MemberNameStrategies.CamelCase; - - foreach (var extension in extensions) - { - extension.RegisterGlobalTypes(globalTypes); - } + public static void SetupTypes(IEnumerable extensions) + { + var globalTypes = TemplateContext.GlobalMemberAccessStrategy; - foreach (var type in SquidexCoreModel.Assembly.GetTypes().Where(x => x.IsEnum)) - { - FluidValue.SetTypeMapping(type, x => new StringValue(x.ToString())); - } + globalTypes.MemberNameStrategy = MemberNameStrategies.CamelCase; - FluidValue.SetTypeMapping(x => new StringValue(x.ToString().ToLowerInvariant())); + foreach (var extension in extensions) + { + extension.RegisterGlobalTypes(globalTypes); + } - globalTypes.Register>(); - globalTypes.Register>(); - globalTypes.Register>(); - globalTypes.Register>(); - globalTypes.Register(); + foreach (var type in SquidexCoreModel.Assembly.GetTypes().Where(x => x.IsEnum)) + { + FluidValue.SetTypeMapping(type, x => new StringValue(x.ToString())); } - } - public FluidTemplateEngine(IEnumerable extensions) - { - this.extensions = extensions; + FluidValue.SetTypeMapping(x => new StringValue(x.ToString().ToLowerInvariant())); - SquidexTemplate.Setup(extensions); - SquidexTemplate.SetupTypes(extensions); + globalTypes.Register>(); + globalTypes.Register>(); + globalTypes.Register>(); + globalTypes.Register>(); + globalTypes.Register(); } + } - public async Task RenderAsync(string template, TemplateVars variables) - { - Guard.NotNull(variables); + public FluidTemplateEngine(IEnumerable extensions) + { + this.extensions = extensions; - if (SquidexTemplate.TryParse(template, out var parsed, out var errors)) - { - var context = new TemplateContext(); + SquidexTemplate.Setup(extensions); + SquidexTemplate.SetupTypes(extensions); + } - foreach (var extension in extensions) - { - extension.BeforeRun(context); - } + public async Task RenderAsync(string template, TemplateVars variables) + { + Guard.NotNull(variables); - foreach (var (key, value) in variables) - { - context.MemberAccessStrategy.Register(value.GetType()); + if (SquidexTemplate.TryParse(template, out var parsed, out var errors)) + { + var context = new TemplateContext(); - context.SetValue(key, value); - } + foreach (var extension in extensions) + { + extension.BeforeRun(context); + } - var result = await parsed.RenderAsync(context); + foreach (var (key, value) in variables) + { + context.MemberAccessStrategy.Register(value.GetType()); - return result; + context.SetValue(key, value); } - throw new TemplateParseException(template, errors); + var result = await parsed.RenderAsync(context); + + return result; } + + throw new TemplateParseException(template, errors); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/IFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/IFluidExtension.cs index 70b911664d..7811a1346c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/IFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/IFluidExtension.cs @@ -7,20 +7,19 @@ using Fluid; -namespace Squidex.Domain.Apps.Core.Templates +namespace Squidex.Domain.Apps.Core.Templates; + +public interface IFluidExtension { - public interface IFluidExtension + void RegisterLanguageExtensions(FluidParserFactory factory) { - void RegisterLanguageExtensions(FluidParserFactory factory) - { - } + } - void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - } + void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + } - void BeforeRun(TemplateContext templateContext) - { - } + void BeforeRun(TemplateContext templateContext) + { } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/ITemplateEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/ITemplateEngine.cs index e2c6c805b0..ee204d190f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/ITemplateEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/ITemplateEngine.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Templates +namespace Squidex.Domain.Apps.Core.Templates; + +public interface ITemplateEngine { - public interface ITemplateEngine - { - Task RenderAsync(string template, TemplateVars variables); - } + Task RenderAsync(string template, TemplateVars variables); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateParseException.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateParseException.cs index 26041f2924..a6a147a051 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateParseException.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateParseException.cs @@ -8,47 +8,46 @@ using System.Runtime.Serialization; using System.Text; -namespace Squidex.Domain.Apps.Core.Templates +namespace Squidex.Domain.Apps.Core.Templates; + +[Serializable] +public class TemplateParseException : Exception { - [Serializable] - public class TemplateParseException : Exception - { - public IReadOnlyList Errors { get; } + public IReadOnlyList Errors { get; } - public TemplateParseException(string template, IEnumerable errors, Exception? inner = null) - : base(BuildErrorMessage(errors, template), inner) - { - Errors = errors.ToList(); - } + public TemplateParseException(string template, IEnumerable errors, Exception? inner = null) + : base(BuildErrorMessage(errors, template), inner) + { + Errors = errors.ToList(); + } - protected TemplateParseException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Errors = (info.GetValue(nameof(Errors), typeof(List)) as List) ?? new List(); - } + protected TemplateParseException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Errors = (info.GetValue(nameof(Errors), typeof(List)) as List) ?? new List(); + } - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Errors), Errors.ToList()); - } + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Errors), Errors.ToList()); + } - private static string BuildErrorMessage(IEnumerable errors, string template) - { - var sb = new StringBuilder(); + private static string BuildErrorMessage(IEnumerable errors, string template) + { + var sb = new StringBuilder(); - sb.AppendLine("Failed to parse template"); + sb.AppendLine("Failed to parse template"); - foreach (var error in errors) - { - sb.Append(" * "); - sb.AppendLine(error); - } + foreach (var error in errors) + { + sb.Append(" * "); + sb.AppendLine(error); + } - sb.AppendLine(); - sb.AppendLine("Template:"); - sb.AppendLine(template); + sb.AppendLine(); + sb.AppendLine("Template:"); + sb.AppendLine(template); - return sb.ToString(); - } + return sb.ToString(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateVars.cs index 1510e72bb8..bdd86e21a3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/TemplateVars.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.Templates +namespace Squidex.Domain.Apps.Core.Templates; + +public sealed class TemplateVars : Dictionary { - public sealed class TemplateVars : Dictionary - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index 1fc087241e..078703f64c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -12,135 +12,134 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public sealed class ContentValidator { - public sealed class ContentValidator + private readonly PartitionResolver partitionResolver; + private readonly ValidationContext context; + private readonly IEnumerable factories; + + public IEnumerable Errors { - private readonly PartitionResolver partitionResolver; - private readonly ValidationContext context; - private readonly IEnumerable factories; + get => context.Root.Errors; + } - public IEnumerable Errors - { - get => context.Root.Errors; - } + public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, + IEnumerable factories) + { + Guard.NotNull(context); + Guard.NotNull(factories); + Guard.NotNull(partitionResolver); - public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, - IEnumerable factories) - { - Guard.NotNull(context); - Guard.NotNull(factories); - Guard.NotNull(partitionResolver); + this.context = context; + this.factories = factories; + this.partitionResolver = partitionResolver; + } - this.context = context; - this.factories = factories; - this.partitionResolver = partitionResolver; - } + public ValueTask ValidateInputPartialAsync(ContentData data) + { + Guard.NotNull(data); - public ValueTask ValidateInputPartialAsync(ContentData data) - { - Guard.NotNull(data); + ValidateInputCore(data, true); - ValidateInputCore(data, true); + return context.Root.CompleteAsync(); + } - return context.Root.CompleteAsync(); - } + public ValueTask ValidateInputAsync(ContentData data) + { + Guard.NotNull(data); - public ValueTask ValidateInputAsync(ContentData data) - { - Guard.NotNull(data); + ValidateInputCore(data, false); - ValidateInputCore(data, false); + return context.Root.CompleteAsync(); + } - return context.Root.CompleteAsync(); - } + public ValueTask ValidateInputAndContentAsync(ContentData data) + { + Guard.NotNull(data); - public ValueTask ValidateInputAndContentAsync(ContentData data) - { - Guard.NotNull(data); + ValidateInputCore(data, false); + ValidateContentCore(data); - ValidateInputCore(data, false); - ValidateContentCore(data); + return context.Root.CompleteAsync(); + } - return context.Root.CompleteAsync(); - } + public ValueTask ValidateContentAsync(ContentData data) + { + Guard.NotNull(data); - public ValueTask ValidateContentAsync(ContentData data) - { - Guard.NotNull(data); + ValidateContentCore(data); - ValidateContentCore(data); + return context.Root.CompleteAsync(); + } - return context.Root.CompleteAsync(); - } + private void ValidateInputCore(ContentData data, bool partial) + { + CreateSchemaValidator(partial).Validate(data, context); + } - private void ValidateInputCore(ContentData data, bool partial) - { - CreateSchemaValidator(partial).Validate(data, context); - } + private void ValidateContentCore(ContentData data) + { + CreatecSchemaValidator().Validate(data, context); + } - private void ValidateContentCore(ContentData data) - { - CreatecSchemaValidator().Validate(data, context); - } + private IValidator CreatecSchemaValidator() + { + return new AggregateValidator(CreateContentValidators()); + } - private IValidator CreatecSchemaValidator() + private IValidator CreateSchemaValidator(bool isPartial) + { + var fieldValidators = new Dictionary(context.Root.Schema.Fields.Count); + + foreach (var field in context.Root.Schema.Fields) { - return new AggregateValidator(CreateContentValidators()); + fieldValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); } - private IValidator CreateSchemaValidator(bool isPartial) - { - var fieldValidators = new Dictionary(context.Root.Schema.Fields.Count); + return new ObjectValidator(fieldValidators, isPartial, "field"); + } - foreach (var field in context.Root.Schema.Fields) - { - fieldValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); - } + private IValidator CreateFieldValidator(IRootField field, bool isPartial) + { + var valueValidator = CreateValueValidator(field); - return new ObjectValidator(fieldValidators, isPartial, "field"); - } + var partitioning = partitionResolver(field.Partitioning); + var partitioningValidators = new Dictionary(); - private IValidator CreateFieldValidator(IRootField field, bool isPartial) + foreach (var partitionKey in partitioning.AllKeys) { - var valueValidator = CreateValueValidator(field); - - var partitioning = partitionResolver(field.Partitioning); - var partitioningValidators = new Dictionary(); - - foreach (var partitionKey in partitioning.AllKeys) - { - var optional = partitioning.IsOptional(partitionKey); + var optional = partitioning.IsOptional(partitionKey); - partitioningValidators[partitionKey] = (optional, valueValidator); - } + partitioningValidators[partitionKey] = (optional, valueValidator); + } - var typeName = partitioning.ToString()!; + var typeName = partitioning.ToString()!; - return new AggregateValidator( - CreateFieldValidators(field) - .Union(Enumerable.Repeat( - new ObjectValidator(partitioningValidators, isPartial, typeName), 1))); - } + return new AggregateValidator( + CreateFieldValidators(field) + .Union(Enumerable.Repeat( + new ObjectValidator(partitioningValidators, isPartial, typeName), 1))); + } - private IValidator CreateValueValidator(IField field) - { - return new FieldValidator(new AggregateValidator(CreateValueValidators(field)), field); - } + private IValidator CreateValueValidator(IField field) + { + return new FieldValidator(new AggregateValidator(CreateValueValidators(field)), field); + } - private IEnumerable CreateContentValidators() - { - return factories.SelectMany(x => x.CreateContentValidators(context, CreateValueValidator)); - } + private IEnumerable CreateContentValidators() + { + return factories.SelectMany(x => x.CreateContentValidators(context, CreateValueValidator)); + } - private IEnumerable CreateValueValidators(IField field) - { - return factories.SelectMany(x => x.CreateValueValidators(context, field, CreateValueValidator)); - } + private IEnumerable CreateValueValidators(IField field) + { + return factories.SelectMany(x => x.CreateValueValidators(context, field, CreateValueValidator)); + } - private IEnumerable CreateFieldValidators(IField field) - { - return factories.SelectMany(x => x.CreateFieldValidators(context, field, CreateValueValidator)); - } + private IEnumerable CreateFieldValidators(IField field) + { + return factories.SelectMany(x => x.CreateFieldValidators(context, field, CreateValueValidator)); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs index a01a1a1dee..58215f4482 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -13,272 +13,271 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor, DefaultFieldValueValidatorsFactory.Args> { - internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor, DefaultFieldValueValidatorsFactory.Args> + private static readonly DefaultFieldValueValidatorsFactory Instance = new DefaultFieldValueValidatorsFactory(); + + public record struct Args(ValidationContext Context, ValidatorFactory Factory); + + private DefaultFieldValueValidatorsFactory() { - private static readonly DefaultFieldValueValidatorsFactory Instance = new DefaultFieldValueValidatorsFactory(); + } - public record struct Args(ValidationContext Context, ValidatorFactory Factory); + public static IEnumerable CreateValidators(ValidationContext context, IField field, ValidatorFactory factory) + { + var args = new Args(context, factory); - private DefaultFieldValueValidatorsFactory() - { - } + return field.Accept(Instance, args); + } - public static IEnumerable CreateValidators(ValidationContext context, IField field, ValidatorFactory factory) - { - var args = new Args(context, factory); + public IEnumerable Visit(IArrayField field, Args args) + { + var properties = field.Properties; - return field.Accept(Instance, args); + var isRequired = IsRequired(properties, args.Context); + + if (isRequired || properties.MinItems != null || properties.MaxItems != null) + { + yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } - public IEnumerable Visit(IArrayField field, Args args) + if (properties.UniqueFields?.Count > 0) { - var properties = field.Properties; + yield return new UniqueObjectValuesValidator(properties.UniqueFields); + } - var isRequired = IsRequired(properties, args.Context); + var nestedValidators = new Dictionary(field.Fields.Count); - if (isRequired || properties.MinItems != null || properties.MaxItems != null) - { - yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); - } + foreach (var nestedField in field.Fields) + { + nestedValidators[nestedField.Name] = (false, args.Factory(nestedField)); + } - if (properties.UniqueFields?.Count > 0) - { - yield return new UniqueObjectValuesValidator(properties.UniqueFields); - } + yield return new CollectionItemValidator(new ObjectValidator(nestedValidators, false, "field")); + } - var nestedValidators = new Dictionary(field.Fields.Count); + public IEnumerable Visit(IField field, Args args) + { + yield break; + } - foreach (var nestedField in field.Fields) - { - nestedValidators[nestedField.Name] = (false, args.Factory(nestedField)); - } + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - yield return new CollectionItemValidator(new ObjectValidator(nestedValidators, false, "field")); - } + var isRequired = IsRequired(properties, args.Context); - public IEnumerable Visit(IField field, Args args) + if (isRequired) { - yield break; + yield return new RequiredValidator(); } + } - public IEnumerable Visit(IField field, Args args) - { - var properties = field.Properties; + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - var isRequired = IsRequired(properties, args.Context); + var isRequired = IsRequired(properties, args.Context); - if (isRequired) - { - yield return new RequiredValidator(); - } + if (isRequired) + { + yield return new RequiredValidator(); } - public IEnumerable Visit(IField field, Args args) - { - var properties = field.Properties; + yield return ComponentValidator(args.Factory); + } - var isRequired = IsRequired(properties, args.Context); + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - if (isRequired) - { - yield return new RequiredValidator(); - } + var isRequired = IsRequired(properties, args.Context); - yield return ComponentValidator(args.Factory); + if (isRequired || properties.MinItems != null || properties.MaxItems != null) + { + yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } - public IEnumerable Visit(IField field, Args args) + if (properties.UniqueFields?.Count > 0) { - var properties = field.Properties; + yield return new UniqueObjectValuesValidator(properties.UniqueFields); + } - var isRequired = IsRequired(properties, args.Context); + yield return new CollectionItemValidator(ComponentValidator(args.Factory)); + } - if (isRequired || properties.MinItems != null || properties.MaxItems != null) - { - yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); - } + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - if (properties.UniqueFields?.Count > 0) - { - yield return new UniqueObjectValuesValidator(properties.UniqueFields); - } + var isRequired = IsRequired(properties, args.Context); - yield return new CollectionItemValidator(ComponentValidator(args.Factory)); + if (isRequired) + { + yield return new RequiredValidator(); } - public IEnumerable Visit(IField field, Args args) + if (properties.MinValue != null || properties.MaxValue != null) { - var properties = field.Properties; + yield return new RangeValidator(properties.MinValue, properties.MaxValue); + } + } - var isRequired = IsRequired(properties, args.Context); + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - if (isRequired) - { - yield return new RequiredValidator(); - } + var isRequired = IsRequired(properties, args.Context); - if (properties.MinValue != null || properties.MaxValue != null) - { - yield return new RangeValidator(properties.MinValue, properties.MaxValue); - } + if (isRequired) + { + yield return new RequiredValidator(); } + } - public IEnumerable Visit(IField field, Args args) - { - var properties = field.Properties; + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - var isRequired = IsRequired(properties, args.Context); + var isRequired = IsRequired(properties, args.Context); - if (isRequired) - { - yield return new RequiredValidator(); - } + if (isRequired) + { + yield return new RequiredValidator(); } + } - public IEnumerable Visit(IField field, Args args) - { - var properties = field.Properties; + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - var isRequired = IsRequired(properties, args.Context); + var isRequired = IsRequired(properties, args.Context); - if (isRequired) - { - yield return new RequiredValidator(); - } + if (isRequired) + { + yield return new RequiredValidator(); } - public IEnumerable Visit(IField field, Args args) + if (properties.MinValue != null || properties.MaxValue != null) { - var properties = field.Properties; + yield return new RangeValidator(properties.MinValue, properties.MaxValue); + } - var isRequired = IsRequired(properties, args.Context); + if (properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(properties.AllowedValues); + } + } - if (isRequired) - { - yield return new RequiredValidator(); - } + public IEnumerable Visit(IField field, Args args) + { + yield break; + } - if (properties.MinValue != null || properties.MaxValue != null) - { - yield return new RangeValidator(properties.MinValue, properties.MaxValue); - } + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - if (properties.AllowedValues != null) - { - yield return new AllowedValuesValidator(properties.AllowedValues); - } - } + var isRequired = IsRequired(properties, args.Context); - public IEnumerable Visit(IField field, Args args) + if (isRequired) { - yield break; + yield return new RequiredStringValidator(true); } - public IEnumerable Visit(IField field, Args args) + if (properties.MinLength != null || properties.MaxLength != null) { - var properties = field.Properties; - - var isRequired = IsRequired(properties, args.Context); - - if (isRequired) - { - yield return new RequiredStringValidator(true); - } - - if (properties.MinLength != null || properties.MaxLength != null) - { - yield return new StringLengthValidator(properties.MinLength, properties.MaxLength); - } + yield return new StringLengthValidator(properties.MinLength, properties.MaxLength); + } - if (properties.MinCharacters != null || - properties.MaxCharacters != null || - properties.MinWords != null || - properties.MaxWords != null) - { - Func? transform = null; - - switch (properties.ContentType) - { - case StringContentType.Markdown: - transform = MarkdownExtensions.Markdown2Text; - break; - case StringContentType.Html: - transform = HtmlExtensions.Html2Text; - break; - } - - yield return new StringTextValidator(transform, - properties.MinCharacters, - properties.MaxCharacters, - properties.MinWords, - properties.MaxWords); - } + if (properties.MinCharacters != null || + properties.MaxCharacters != null || + properties.MinWords != null || + properties.MaxWords != null) + { + Func? transform = null; - if (!string.IsNullOrWhiteSpace(properties.Pattern)) + switch (properties.ContentType) { - yield return new PatternValidator(properties.Pattern, properties.PatternMessage); + case StringContentType.Markdown: + transform = MarkdownExtensions.Markdown2Text; + break; + case StringContentType.Html: + transform = HtmlExtensions.Html2Text; + break; } - if (properties.AllowedValues != null) - { - yield return new AllowedValuesValidator(properties.AllowedValues); - } + yield return new StringTextValidator(transform, + properties.MinCharacters, + properties.MaxCharacters, + properties.MinWords, + properties.MaxWords); } - public IEnumerable Visit(IField field, Args args) + if (!string.IsNullOrWhiteSpace(properties.Pattern)) { - var properties = field.Properties; + yield return new PatternValidator(properties.Pattern, properties.PatternMessage); + } - var isRequired = IsRequired(properties, args.Context); + if (properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(properties.AllowedValues); + } + } - if (isRequired || properties.MinItems != null || properties.MaxItems != null) - { - yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); - } + public IEnumerable Visit(IField field, Args args) + { + var properties = field.Properties; - if (properties.AllowedValues != null) - { - yield return new CollectionItemValidator(new AllowedValuesValidator(properties.AllowedValues)); - } + var isRequired = IsRequired(properties, args.Context); - yield return new CollectionItemValidator(new RequiredStringValidator(true)); + if (isRequired || properties.MinItems != null || properties.MaxItems != null) + { + yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } - public IEnumerable Visit(IField field, Args args) + if (properties.AllowedValues != null) { - if (field is INestedField) - { - yield return NoValueValidator.Instance; - } + yield return new CollectionItemValidator(new AllowedValuesValidator(properties.AllowedValues)); } - private static bool IsRequired(FieldProperties properties, ValidationContext context) + yield return new CollectionItemValidator(new RequiredStringValidator(true)); + } + + public IEnumerable Visit(IField field, Args args) + { + if (field is INestedField) { - var isRequired = properties.IsRequired; + yield return NoValueValidator.Instance; + } + } - if (context.Action == ValidationAction.Publish) - { - isRequired = isRequired || properties.IsRequiredOnPublish; - } + private static bool IsRequired(FieldProperties properties, ValidationContext context) + { + var isRequired = properties.IsRequired; - return isRequired; + if (context.Action == ValidationAction.Publish) + { + isRequired = isRequired || properties.IsRequiredOnPublish; } - private static IValidator ComponentValidator(ValidatorFactory factory) + return isRequired; + } + + private static IValidator ComponentValidator(ValidatorFactory factory) + { + return new ComponentValidator(schema => { - return new ComponentValidator(schema => - { - var nestedValidators = new Dictionary(schema.Fields.Count); + var nestedValidators = new Dictionary(schema.Fields.Count); - foreach (var nestedField in schema.Fields) - { - nestedValidators[nestedField.Name] = (false, factory(nestedField)); - } + foreach (var nestedField in schema.Fields) + { + nestedValidators[nestedField.Name] = (false, factory(nestedField)); + } - return new ObjectValidator(nestedValidators, false, "field"); - }); - } + return new ObjectValidator(nestedValidators, false, "field"); + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs index c00e03e048..75f318a3d5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs @@ -8,21 +8,20 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent.Validators; -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public sealed class DefaultValidatorsFactory : IValidatorsFactory { - public sealed class DefaultValidatorsFactory : IValidatorsFactory + public IEnumerable CreateFieldValidators(ValidationContext context, IField field, ValidatorFactory factory) { - public IEnumerable CreateFieldValidators(ValidationContext context, IField field, ValidatorFactory factory) + if (field is IField) { - if (field is IField) - { - yield return NoValueValidator.Instance; - } + yield return NoValueValidator.Instance; } + } - public IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory factory) - { - return DefaultFieldValueValidatorsFactory.CreateValidators(context, field, factory); - } + public IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory factory) + { + return DefaultFieldValueValidatorsFactory.CreateValidators(context, field, factory); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs index 6cc7943ebe..0895b26808 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs @@ -8,24 +8,23 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public interface IAssetInfo { - public interface IAssetInfo - { - DomainId AssetId { get; } + DomainId AssetId { get; } - long FileSize { get; } + long FileSize { get; } - string FileName { get; } + string FileName { get; } - string FileHash { get; } + string FileHash { get; } - string MimeType { get; } + string MimeType { get; } - string Slug { get; } + string Slug { get; } - AssetMetadata Metadata { get; } + AssetMetadata Metadata { get; } - AssetType Type { get; } - } + AssetType Type { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs index dd84aefdaa..1bc0abec54 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public interface IValidator { - public interface IValidator - { - void Validate(object? value, ValidationContext context); - } + void Validate(object? value, ValidationContext context); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs index 9a2c9747b3..296de831f1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs @@ -9,25 +9,24 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public delegate IValidator ValidatorFactory(IField field); +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public delegate IValidator ValidatorFactory(IField field); - public interface IValidatorsFactory +public interface IValidatorsFactory +{ + IEnumerable CreateFieldValidators(ValidationContext context, IField field, ValidatorFactory factory) { - IEnumerable CreateFieldValidators(ValidationContext context, IField field, ValidatorFactory factory) - { - yield break; - } + yield break; + } - IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory factory) - { - yield break; - } + IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory factory) + { + yield break; + } - IEnumerable CreateContentValidators(ValidationContext context, ValidatorFactory factory) - { - yield break; - } + IEnumerable CreateContentValidators(ValidationContext context, ValidatorFactory factory) + { + yield break; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonError.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonError.cs index e4e5a300fb..eab5636783 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonError.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonError.cs @@ -5,15 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public sealed class JsonError { - public sealed class JsonError - { - public string Error { get; } + public string Error { get; } - public JsonError(string error) - { - Error = error; - } + public JsonError(string error) + { + Error = error; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index 7d56898912..19bb0adfde 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -16,279 +16,278 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonError? Error), JsonValueConverter.Args> { - public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonError? Error), JsonValueConverter.Args> + private static readonly JsonValueConverter Instance = new JsonValueConverter(); + + public record struct Args(JsonValue Value, IJsonSerializer Serializer, ResolvedComponents Components); + + private JsonValueConverter() { - private static readonly JsonValueConverter Instance = new JsonValueConverter(); + } - public record struct Args(JsonValue Value, IJsonSerializer Serializer, ResolvedComponents Components); + public static (object? Result, JsonError? Error) ConvertValue(IField field, JsonValue value, IJsonSerializer serializer, + ResolvedComponents components) + { + Guard.NotNull(field); + Guard.NotNull(value); - private JsonValueConverter() - { - } + var args = new Args(value, serializer, components); - public static (object? Result, JsonError? Error) ConvertValue(IField field, JsonValue value, IJsonSerializer serializer, - ResolvedComponents components) - { - Guard.NotNull(field); - Guard.NotNull(value); + return field.Accept(Instance, args); + } - var args = new Args(value, serializer, components); + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + return (args.Value, null); + } - return field.Accept(Instance, args); - } + public (object? Result, JsonError? Error) Visit(IArrayField field, Args args) + { + return ConvertToObjectList(args.Value); + } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - return (args.Value, null); - } + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + return ConvertToIdList(args.Value); + } - public (object? Result, JsonError? Error) Visit(IArrayField field, Args args) - { - return ConvertToObjectList(args.Value); - } + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + return ConvertToComponent(args.Value, args.Components, field.Properties.SchemaIds); + } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - return ConvertToIdList(args.Value); - } + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + return ConvertToComponentList(args.Value, args.Components, field.Properties.SchemaIds); + } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - return ConvertToComponent(args.Value, args.Components, field.Properties.SchemaIds); - } + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + return ConvertToIdList(args.Value); + } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - return ConvertToComponentList(args.Value, args.Components, field.Properties.SchemaIds); - } + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + return ConvertToStringList(args.Value); + } - public (object? Result, JsonError? Error) Visit(IField field, Args args) + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + if (args.Value.Value is bool b) { - return ConvertToIdList(args.Value); + return (b, null); } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - return ConvertToStringList(args.Value); - } + return (null, new JsonError(T.Get("contents.invalidBoolean"))); + } - public (object? Result, JsonError? Error) Visit(IField field, Args args) + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + if (args.Value.Value is double d) { - if (args.Value.Value is bool b) - { - return (b, null); - } - - return (null, new JsonError(T.Get("contents.invalidBoolean"))); + return (d, null); } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - if (args.Value.Value is double d) - { - return (d, null); - } + return (null, new JsonError(T.Get("contents.invalidNumber"))); + } - return (null, new JsonError(T.Get("contents.invalidNumber"))); + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + if (args.Value.Value is string s) + { + return (s, null); } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - if (args.Value.Value is string s) - { - return (s, null); - } + return (null, new JsonError(T.Get("contents.invalidString"))); + } - return (null, new JsonError(T.Get("contents.invalidString"))); - } + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + return (args.Value, null); + } - public (object? Result, JsonError? Error) Visit(IField field, Args args) + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + if (args.Value.Value is string s) { - return (args.Value, null); - } + var parseResult = InstantPattern.ExtendedIso.Parse(s); - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - if (args.Value.Value is string s) + if (!parseResult.Success) { - var parseResult = InstantPattern.ExtendedIso.Parse(s); - - if (!parseResult.Success) - { - return (null, new JsonError(parseResult.Exception.Message)); - } - - return (parseResult.Value, null); + return (null, new JsonError(parseResult.Exception.Message)); } - return (null, new JsonError(T.Get("contents.invalidString"))); + return (parseResult.Value, null); } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - var result = GeoJsonValue.TryParse(args.Value, args.Serializer, out var value); + return (null, new JsonError(T.Get("contents.invalidString"))); + } - switch (result) - { - case GeoJsonParseResult.InvalidLatitude: - return (null, new JsonError(T.Get("contents.invalidGeolocationLatitude"))); - case GeoJsonParseResult.InvalidLongitude: - return (null, new JsonError(T.Get("contents.invalidGeolocationLongitude"))); - case GeoJsonParseResult.InvalidValue: - return (null, new JsonError(T.Get("contents.invalidGeolocation"))); - default: - return (value, null); - } + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + var result = GeoJsonValue.TryParse(args.Value, args.Serializer, out var value); + + switch (result) + { + case GeoJsonParseResult.InvalidLatitude: + return (null, new JsonError(T.Get("contents.invalidGeolocationLatitude"))); + case GeoJsonParseResult.InvalidLongitude: + return (null, new JsonError(T.Get("contents.invalidGeolocationLongitude"))); + case GeoJsonParseResult.InvalidValue: + return (null, new JsonError(T.Get("contents.invalidGeolocation"))); + default: + return (value, null); } + } - private static (object? Result, JsonError? Error) ConvertToIdList(JsonValue value) + private static (object? Result, JsonError? Error) ConvertToIdList(JsonValue value) + { + if (value.Value is JsonArray a) { - if (value.Value is JsonArray a) - { - var result = new List(a.Count); + var result = new List(a.Count); - foreach (var item in a) + foreach (var item in a) + { + if (item.Value is string s) { - if (item.Value is string s) + if (!string.IsNullOrWhiteSpace(s)) { - if (!string.IsNullOrWhiteSpace(s)) - { - result.Add(DomainId.Create(s)); - continue; - } + result.Add(DomainId.Create(s)); + continue; } - - return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); } - return (result, null); + return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); } - return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); + return (result, null); } - private static (object? Result, JsonError? Error) ConvertToStringList(JsonValue value) + return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); + } + + private static (object? Result, JsonError? Error) ConvertToStringList(JsonValue value) + { + if (value.Value is JsonArray a) { - if (value.Value is JsonArray a) - { - var result = new List(a.Count); + var result = new List(a.Count); - foreach (var item in a) + foreach (var item in a) + { + if (item.Value is string s) { - if (item.Value is string s) + if (!string.IsNullOrWhiteSpace(s)) { - if (!string.IsNullOrWhiteSpace(s)) - { - result.Add(s); - continue; - } + result.Add(s); + continue; } - - return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); } - return (result, null); + return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); } - return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); + return (result, null); } - private static (object? Result, JsonError? Error) ConvertToObjectList(JsonValue value) + return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); + } + + private static (object? Result, JsonError? Error) ConvertToObjectList(JsonValue value) + { + if (value.Value is JsonArray a) { - if (value.Value is JsonArray a) - { - var result = new List(a.Count); + var result = new List(a.Count); - foreach (var item in a) + foreach (var item in a) + { + if (item.Value is JsonObject o) { - if (item.Value is JsonObject o) - { - result.Add(o); - continue; - } - - return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); + result.Add(o); + continue; } - return (result, null); + return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); } - return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); + return (result, null); } - private static (object? Result, JsonError? Error) ConvertToComponentList(JsonValue value, - ResolvedComponents components, ReadonlyList? allowedIds) + return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); + } + + private static (object? Result, JsonError? Error) ConvertToComponentList(JsonValue value, + ResolvedComponents components, ReadonlyList? allowedIds) + { + if (value.Value is JsonArray a) { - if (value.Value is JsonArray a) + var result = new List(a.Count); + + foreach (var item in a) { - var result = new List(a.Count); + var (component, error) = ConvertToComponent(item, components, allowedIds); - foreach (var item in a) + if (error != null) { - var (component, error) = ConvertToComponent(item, components, allowedIds); - - if (error != null) - { - return (null, error); - } - - if (component != null) - { - result.Add(component); - } + return (null, error); } - return (result, null); + if (component != null) + { + result.Add(component); + } } - return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); + return (result, null); } - private static (Component? Result, JsonError? Error) ConvertToComponent(JsonValue value, - ResolvedComponents components, ReadonlyList? allowedIds) + return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); + } + + private static (Component? Result, JsonError? Error) ConvertToComponent(JsonValue value, + ResolvedComponents components, ReadonlyList? allowedIds) + { + if (value.Value is not JsonObject o) { - if (value.Value is not JsonObject o) - { - return (null, new JsonError(T.Get("contents.invalidComponentNoObject"))); - } + return (null, new JsonError(T.Get("contents.invalidComponentNoObject"))); + } - var id = DomainId.Empty; + var id = DomainId.Empty; - if (o.TryGetValue("schemaName", out var found) && found.Value is string schemaName) - { - id = components.FirstOrDefault(x => x.Value.Name == schemaName).Key; + if (o.TryGetValue("schemaName", out var found) && found.Value is string schemaName) + { + id = components.FirstOrDefault(x => x.Value.Name == schemaName).Key; - o.Remove("schemaName"); - o[Component.Discriminator] = id; - } - else if (o.TryGetValue(Component.Discriminator, out found) && found.Value is string discriminator) - { - id = DomainId.Create(discriminator); - } - else if (allowedIds?.Count == 1) - { - id = allowedIds[0]; + o.Remove("schemaName"); + o[Component.Discriminator] = id; + } + else if (o.TryGetValue(Component.Discriminator, out found) && found.Value is string discriminator) + { + id = DomainId.Create(discriminator); + } + else if (allowedIds?.Count == 1) + { + id = allowedIds[0]; - o[Component.Discriminator] = id; - } + o[Component.Discriminator] = id; + } - if (id == default) - { - return (null, new JsonError(T.Get("contents.invalidComponentNoType"))); - } + if (id == default) + { + return (null, new JsonError(T.Get("contents.invalidComponentNoType"))); + } - if (allowedIds?.Contains(id) == false || !components.TryGetValue(id, out var schema)) - { - return (null, new JsonError(T.Get("contents.invalidComponentUnknownSchema"))); - } + if (allowedIds?.Contains(id) == false || !components.TryGetValue(id, out var schema)) + { + return (null, new JsonError(T.Get("contents.invalidComponentUnknownSchema"))); + } - var data = new JsonObject(o); + var data = new JsonObject(o); - data.Remove(Component.Discriminator); + data.Remove(Component.Discriminator); - return (new Component(id.ToString(), data, schema), null); - } + return (new Component(id.ToString(), data, schema), null); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs index b219a6b77a..efbde081f6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs @@ -14,159 +14,158 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public sealed class JsonValueValidator : IFieldVisitor { - public sealed class JsonValueValidator : IFieldVisitor + private static readonly JsonValueValidator Instance = new JsonValueValidator(); + + public record struct Args(JsonValue Value, IJsonSerializer Serializer); + + private JsonValueValidator() { - private static readonly JsonValueValidator Instance = new JsonValueValidator(); + } - public record struct Args(JsonValue Value, IJsonSerializer Serializer); + public static bool IsValid(IField field, JsonValue value, IJsonSerializer serializer) + { + Guard.NotNull(field); + Guard.NotNull(value); - private JsonValueValidator() - { - } + var args = new Args(value, serializer); - public static bool IsValid(IField field, JsonValue value, IJsonSerializer serializer) - { - Guard.NotNull(field); - Guard.NotNull(value); + return field.Accept(Instance, args); + } - var args = new Args(value, serializer); + public bool Visit(IArrayField field, Args args) + { + return IsValidObjectList(args.Value); + } - return field.Accept(Instance, args); - } + public bool Visit(IField field, Args args) + { + return IsValidStringList(args.Value); + } - public bool Visit(IArrayField field, Args args) - { - return IsValidObjectList(args.Value); - } + public bool Visit(IField field, Args args) + { + return args.Value.Value is bool; + } - public bool Visit(IField field, Args args) - { - return IsValidStringList(args.Value); - } + public bool Visit(IField field, Args args) + { + return IsValidComponent(args.Value); + } - public bool Visit(IField field, Args args) - { - return args.Value.Value is bool; - } + public bool Visit(IField field, Args args) + { + return IsValidComponentList(args.Value); + } - public bool Visit(IField field, Args args) + public bool Visit(IField field, Args args) + { + if (args.Value.Value is string s) { - return IsValidComponent(args.Value); - } + var parseResult = InstantPattern.ExtendedIso.Parse(s); - public bool Visit(IField field, Args args) - { - return IsValidComponentList(args.Value); + return parseResult.Success; } - public bool Visit(IField field, Args args) - { - if (args.Value.Value is string s) - { - var parseResult = InstantPattern.ExtendedIso.Parse(s); - - return parseResult.Success; - } + return false; + } - return false; - } + public bool Visit(IField field, Args args) + { + var result = GeoJsonValue.TryParse(args.Value, args.Serializer, out _); - public bool Visit(IField field, Args args) - { - var result = GeoJsonValue.TryParse(args.Value, args.Serializer, out _); + return result == GeoJsonParseResult.Success; + } - return result == GeoJsonParseResult.Success; - } + public bool Visit(IField field, Args args) + { + return true; + } - public bool Visit(IField field, Args args) - { - return true; - } + public bool Visit(IField field, Args args) + { + return args.Value.Value is double; + } - public bool Visit(IField field, Args args) - { - return args.Value.Value is double; - } + public bool Visit(IField field, Args args) + { + return IsValidStringList(args.Value); + } - public bool Visit(IField field, Args args) - { - return IsValidStringList(args.Value); - } + public bool Visit(IField field, Args args) + { + return args.Value.Value is string; + } - public bool Visit(IField field, Args args) - { - return args.Value.Value is string; - } + public bool Visit(IField field, Args args) + { + return IsValidStringList(args.Value); + } - public bool Visit(IField field, Args args) - { - return IsValidStringList(args.Value); - } + public bool Visit(IField field, Args args) + { + return true; + } - public bool Visit(IField field, Args args) + private static bool IsValidStringList(JsonValue value) + { + if (value.Value is not JsonArray a) { - return true; + return false; } - private static bool IsValidStringList(JsonValue value) + foreach (var item in a) { - if (value.Value is not JsonArray a) + if (item.Value is not string) { return false; } + } - foreach (var item in a) - { - if (item.Value is not string) - { - return false; - } - } + return true; + } - return true; + private static bool IsValidObjectList(JsonValue value) + { + if (value.Value is not JsonArray a) + { + return false; } - private static bool IsValidObjectList(JsonValue value) + foreach (var item in a) { - if (value.Value is not JsonArray a) + if (item.Value is not JsonObject) { return false; } + } - foreach (var item in a) - { - if (item.Value is not JsonObject) - { - return false; - } - } + return true; + } - return true; + private static bool IsValidComponentList(JsonValue value) + { + if (value.Value is not JsonArray a) + { + return false; } - private static bool IsValidComponentList(JsonValue value) + foreach (var item in a) { - if (value.Value is not JsonArray a) + if (!IsValidComponent(item)) { return false; } - - foreach (var item in a) - { - if (!IsValidComponent(item)) - { - return false; - } - } - - return true; } - private static bool IsValidComponent(JsonValue value) - { - return Component.IsValid(value, out _); - } + return true; + } + + private static bool IsValidComponent(JsonValue value) + { + return Component.IsValid(value, out _); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs index e42fd59259..f3c4bd047d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs @@ -7,36 +7,35 @@ using System.Text; -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public static class ObjectPath { - public static class ObjectPath + public static string ToPathString(this IEnumerable path) { - public static string ToPathString(this IEnumerable path) - { - var sb = new StringBuilder(); + var sb = new StringBuilder(); - var index = 0; + var index = 0; - foreach (var property in path) + foreach (var property in path) + { + if (index == 0) { - if (index == 0) - { - sb.Append(property); - } - else + sb.Append(property); + } + else + { + if (property[0] != '[') { - if (property[0] != '[') - { - sb.Append('.'); - } - - sb.Append(property); + sb.Append('.'); } - index++; + sb.Append(property); } - return sb.ToString(); + index++; } + + return sb.ToString(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs index e2c222e1d2..508356a692 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs @@ -12,68 +12,67 @@ using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public sealed class RootContext { - public sealed class RootContext - { - private readonly ConcurrentBag errors = new ConcurrentBag(); - private readonly Scheduler scheduler = new Scheduler(); + private readonly ConcurrentBag errors = new ConcurrentBag(); + private readonly Scheduler scheduler = new Scheduler(); - public IJsonSerializer Serializer { get; } + public IJsonSerializer Serializer { get; } - public DomainId ContentId { get; } + public DomainId ContentId { get; } - public NamedId AppId { get; } + public NamedId AppId { get; } - public NamedId SchemaId { get; } + public NamedId SchemaId { get; } - public Schema Schema { get; } + public Schema Schema { get; } - public ResolvedComponents Components { get; } + public ResolvedComponents Components { get; } - public IEnumerable Errors - { - get => errors; - } + public IEnumerable Errors + { + get => errors; + } - public RootContext( - IJsonSerializer serializer, - NamedId appId, - NamedId schemaId, - Schema schema, - DomainId contentId, - ResolvedComponents components) - { - AppId = appId; - Components = components; - ContentId = contentId; - Serializer = serializer; - Schema = schema; - SchemaId = schemaId; - } + public RootContext( + IJsonSerializer serializer, + NamedId appId, + NamedId schemaId, + Schema schema, + DomainId contentId, + ResolvedComponents components) + { + AppId = appId; + Components = components; + ContentId = contentId; + Serializer = serializer; + Schema = schema; + SchemaId = schemaId; + } - public void AddError(IEnumerable path, string message) - { - errors.Add(new ValidationError(message, path.ToPathString())); - } + public void AddError(IEnumerable path, string message) + { + errors.Add(new ValidationError(message, path.ToPathString())); + } - public void AddTask(SchedulerTask task) - { - scheduler.Schedule(task); - } + public void AddTask(SchedulerTask task) + { + scheduler.Schedule(task); + } - public void ThrowOnErrors() + public void ThrowOnErrors() + { + if (!errors.IsEmpty) { - if (!errors.IsEmpty) - { - throw new ValidationException(errors.ToList()); - } + throw new ValidationException(errors.ToList()); } + } - public ValueTask CompleteAsync( - CancellationToken ct = default) - { - return scheduler.CompleteAsync(ct); - } + public ValueTask CompleteAsync( + CancellationToken ct = default) + { + return scheduler.CompleteAsync(ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs index 0962a5e4b6..da56aa20c1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public static class Undefined { - public static class Undefined - { - public static readonly object Value = new object(); + public static readonly object Value = new object(); - public static bool IsUndefined(this object? other) - { - return ReferenceEquals(other, Value); - } + public static bool IsUndefined(this object? other) + { + return ReferenceEquals(other, Value); + } - public static bool IsNullOrUndefined(this object? other) - { - return other == null || other.IsUndefined(); - } + public static bool IsNullOrUndefined(this object? other) + { + return other == null || other.IsUndefined(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs index 6172bf391e..8dcaac7daf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public enum ValidationAction { - public enum ValidationAction - { - Upsert, - Publish - } + Upsert, + Publish } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index 0570048593..498aa4d8c1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -9,71 +9,70 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public sealed record ValidationContext(RootContext Root) { - public sealed record ValidationContext(RootContext Root) - { - public ImmutableQueue Path { get; init; } = ImmutableQueue.Empty; + public ImmutableQueue Path { get; init; } = ImmutableQueue.Empty; - public bool IsOptional { get; init; } + public bool IsOptional { get; init; } - public ValidationMode Mode { get; init; } + public ValidationMode Mode { get; init; } - public ValidationAction Action { get; init; } + public ValidationAction Action { get; init; } - public void AddError(IEnumerable path, string message) - { - Root.AddError(path, message); - } + public void AddError(IEnumerable path, string message) + { + Root.AddError(path, message); + } - public ValidationContext Optimized(bool optimized = true) - { - return WithMode(optimized ? ValidationMode.Optimized : ValidationMode.Default); - } + public ValidationContext Optimized(bool optimized = true) + { + return WithMode(optimized ? ValidationMode.Optimized : ValidationMode.Default); + } - public ValidationContext AsPublishing(bool publish = true) - { - return WithAction(publish ? ValidationAction.Publish : ValidationAction.Upsert); - } + public ValidationContext AsPublishing(bool publish = true) + { + return WithAction(publish ? ValidationAction.Publish : ValidationAction.Upsert); + } - public ValidationContext Optional(bool isOptional = true) + public ValidationContext Optional(bool isOptional = true) + { + if (IsOptional == isOptional) { - if (IsOptional == isOptional) - { - return this; - } - - return this with { IsOptional = isOptional }; + return this; } - public ValidationContext WithAction(ValidationAction action) - { - if (Action == action) - { - return this; - } - - return this with { Action = action }; - } + return this with { IsOptional = isOptional }; + } - public ValidationContext WithMode(ValidationMode mode) + public ValidationContext WithAction(ValidationAction action) + { + if (Action == action) { - if (Mode == mode) - { - return this; - } - - return this with { Mode = mode }; + return this; } - public ValidationContext Nested(string property) - { - return this with { Path = Path.Enqueue(property) }; - } + return this with { Action = action }; + } - public ValidationContext Nested(string property, bool isOptional) + public ValidationContext WithMode(ValidationMode mode) + { + if (Mode == mode) { - return this with { Path = Path.Enqueue(property), IsOptional = isOptional }; + return this; } + + return this with { Mode = mode }; + } + + public ValidationContext Nested(string property) + { + return this with { Path = Path.Enqueue(property) }; + } + + public ValidationContext Nested(string property, bool isOptional) + { + return this with { Path = Path.Enqueue(property), IsOptional = isOptional }; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs index 1230e45dd0..b41bfc8db3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Core.ValidateContent +namespace Squidex.Domain.Apps.Core.ValidateContent; + +public enum ValidationMode { - public enum ValidationMode - { - Default, - Optimized - } + Default, + Optimized } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs index d6e51a6b3c..7eec9f0e02 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs @@ -7,35 +7,34 @@ using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class AggregateValidator : IValidator { - public sealed class AggregateValidator : IValidator + private readonly IValidator[]? validators; + + public AggregateValidator(IEnumerable? validators) { - private readonly IValidator[]? validators; + this.validators = validators?.ToArray(); + } - public AggregateValidator(IEnumerable? validators) + public void Validate(object? value, ValidationContext context) + { + if (validators == null || validators.Length == 0) { - this.validators = validators?.ToArray(); + return; } - public void Validate(object? value, ValidationContext context) + try { - if (validators == null || validators.Length == 0) + foreach (var validator in validators) { - return; - } - - try - { - foreach (var validator in validators) - { - validator.Validate(value, context); - } - } - catch - { - context.AddError(context.Path, T.Get("contents.validation.error")); + validator.Validate(value, context); } } + catch + { + context.AddError(context.Path, T.Get("contents.validation.error")); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs index f92565b9c1..0ea3e4de2e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs @@ -8,30 +8,29 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class AllowedValuesValidator : IValidator { - public sealed class AllowedValuesValidator : IValidator - { - private readonly IEnumerable allowedValues; + private readonly IEnumerable allowedValues; - public AllowedValuesValidator(params TValue[] allowedValues) - : this((IEnumerable)allowedValues) - { - } + public AllowedValuesValidator(params TValue[] allowedValues) + : this((IEnumerable)allowedValues) + { + } - public AllowedValuesValidator(IEnumerable allowedValues) - { - Guard.NotNull(allowedValues); + public AllowedValuesValidator(IEnumerable allowedValues) + { + Guard.NotNull(allowedValues); - this.allowedValues = allowedValues; - } + this.allowedValues = allowedValues; + } - public void Validate(object? value, ValidationContext context) + public void Validate(object? value, ValidationContext context) + { + if (value is TValue typedValue && !allowedValues.Contains(typedValue)) { - if (value is TValue typedValue && !allowedValues.Contains(typedValue)) - { - context.AddError(context.Path, T.Get("contents.validation.notAllowed")); - } + context.AddError(context.Path, T.Get("contents.validation.notAllowed")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index 650a0f683a..9676895b7d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -13,171 +13,170 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public delegate Task> CheckAssets(IEnumerable ids); +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; - public sealed class AssetsValidator : IValidator - { - private readonly AssetsFieldProperties properties; - private readonly CollectionValidator? collectionValidator; - private readonly UniqueValuesValidator? uniqueValidator; - private readonly CheckAssets checkAssets; +public delegate Task> CheckAssets(IEnumerable ids); - public AssetsValidator(bool isRequired, AssetsFieldProperties properties, CheckAssets checkAssets) - { - Guard.NotNull(properties); - Guard.NotNull(checkAssets); - - this.properties = properties; +public sealed class AssetsValidator : IValidator +{ + private readonly AssetsFieldProperties properties; + private readonly CollectionValidator? collectionValidator; + private readonly UniqueValuesValidator? uniqueValidator; + private readonly CheckAssets checkAssets; - if (isRequired || properties.MinItems != null || properties.MaxItems != null) - { - collectionValidator = new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); - } + public AssetsValidator(bool isRequired, AssetsFieldProperties properties, CheckAssets checkAssets) + { + Guard.NotNull(properties); + Guard.NotNull(checkAssets); - if (!properties.AllowDuplicates) - { - uniqueValidator = new UniqueValuesValidator(); - } + this.properties = properties; - this.checkAssets = checkAssets; + if (isRequired || properties.MinItems != null || properties.MaxItems != null) + { + collectionValidator = new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } - public void Validate(object? value, ValidationContext context) + if (!properties.AllowDuplicates) { - context.Root.AddTask(ct => ValidateCoreAsync(value, context)); + uniqueValidator = new UniqueValuesValidator(); } - private async Task ValidateCoreAsync(object? value, ValidationContext context) + this.checkAssets = checkAssets; + } + + public void Validate(object? value, ValidationContext context) + { + context.Root.AddTask(ct => ValidateCoreAsync(value, context)); + } + + private async Task ValidateCoreAsync(object? value, ValidationContext context) + { + var foundIds = new List(); + + if (value is ICollection { Count: > 0 } assetIds) { - var foundIds = new List(); + var assets = await checkAssets(assetIds); + var index = 1; - if (value is ICollection { Count: > 0 } assetIds) + foreach (var assetId in assetIds) { - var assets = await checkAssets(assetIds); - var index = 1; + var assetPath = context.Path.Enqueue($"[{index}]"); + var assetItem = assets.FirstOrDefault(x => x.AssetId == assetId); - foreach (var assetId in assetIds) + if (assetItem == null) { - var assetPath = context.Path.Enqueue($"[{index}]"); - var assetItem = assets.FirstOrDefault(x => x.AssetId == assetId); - - if (assetItem == null) + if (context.Action == ValidationAction.Upsert) { - if (context.Action == ValidationAction.Upsert) - { - context.AddError(assetPath, T.Get("contents.validation.assetNotFound", new { id = assetId })); - } - - continue; + context.AddError(assetPath, T.Get("contents.validation.assetNotFound", new { id = assetId })); } - foundIds.Add(assetItem.AssetId); + continue; + } - ValidateCommon(assetItem, assetPath, context); - ValidateType(assetItem, assetPath, context); + foundIds.Add(assetItem.AssetId); - if (assetItem.Type == AssetType.Image) - { - var w = assetItem.Metadata.GetPixelWidth(); - var h = assetItem.Metadata.GetPixelHeight(); + ValidateCommon(assetItem, assetPath, context); + ValidateType(assetItem, assetPath, context); - if (w != null && h != null) - { - ValidateDimensions(w.Value, h.Value, assetPath, context); - } - } - else if (assetItem.Type == AssetType.Video) - { - var w = assetItem.Metadata.GetVideoWidth(); - var h = assetItem.Metadata.GetVideoHeight(); + if (assetItem.Type == AssetType.Image) + { + var w = assetItem.Metadata.GetPixelWidth(); + var h = assetItem.Metadata.GetPixelHeight(); - if (w != null && h != null) - { - ValidateDimensions(w.Value, h.Value, assetPath, context); - } + if (w != null && h != null) + { + ValidateDimensions(w.Value, h.Value, assetPath, context); } + } + else if (assetItem.Type == AssetType.Video) + { + var w = assetItem.Metadata.GetVideoWidth(); + var h = assetItem.Metadata.GetVideoHeight(); - index++; + if (w != null && h != null) + { + ValidateDimensions(w.Value, h.Value, assetPath, context); + } } - } - if (collectionValidator != null) - { - collectionValidator.Validate(foundIds, context); + index++; } + } - if (uniqueValidator != null) - { - uniqueValidator.Validate(foundIds, context); - } + if (collectionValidator != null) + { + collectionValidator.Validate(foundIds, context); } - private void ValidateCommon(IAssetInfo asset, ImmutableQueue path, ValidationContext context) + if (uniqueValidator != null) { - if (properties.MinSize != null && asset.FileSize < properties.MinSize) - { - var min = properties.MinSize.Value.ToReadableSize(); + uniqueValidator.Validate(foundIds, context); + } + } - context.AddError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min })); - } + private void ValidateCommon(IAssetInfo asset, ImmutableQueue path, ValidationContext context) + { + if (properties.MinSize != null && asset.FileSize < properties.MinSize) + { + var min = properties.MinSize.Value.ToReadableSize(); - if (properties.MaxSize != null && asset.FileSize > properties.MaxSize) - { - var max = properties.MaxSize.Value.ToReadableSize(); + context.AddError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min })); + } - context.AddError(path, T.Get("contents.validation.maximumSize", new { size = asset.FileSize.ToReadableSize(), max })); - } + if (properties.MaxSize != null && asset.FileSize > properties.MaxSize) + { + var max = properties.MaxSize.Value.ToReadableSize(); - if (properties.AllowedExtensions is { Count: > 0 } && !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) - { - context.AddError(path, T.Get("contents.validation.extension")); - } + context.AddError(path, T.Get("contents.validation.maximumSize", new { size = asset.FileSize.ToReadableSize(), max })); } - private void ValidateType(IAssetInfo asset, ImmutableQueue path, ValidationContext context) + if (properties.AllowedExtensions is { Count: > 0 } && !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) { - var type = asset.MimeType == "image/svg+xml" ? AssetType.Image : asset.Type; + context.AddError(path, T.Get("contents.validation.extension")); + } + } - if (properties.ExpectedType != null && properties.ExpectedType != type) - { - context.AddError(path, T.Get("contents.validation.assetType", new { type = properties.ExpectedType })); - } + private void ValidateType(IAssetInfo asset, ImmutableQueue path, ValidationContext context) + { + var type = asset.MimeType == "image/svg+xml" ? AssetType.Image : asset.Type; + + if (properties.ExpectedType != null && properties.ExpectedType != type) + { + context.AddError(path, T.Get("contents.validation.assetType", new { type = properties.ExpectedType })); } + } + + private void ValidateDimensions(int w, int h, ImmutableQueue path, ValidationContext context) + { + var actualRatio = (double)w / h; - private void ValidateDimensions(int w, int h, ImmutableQueue path, ValidationContext context) + if (properties.MinWidth != null && w < properties.MinWidth) { - var actualRatio = (double)w / h; + context.AddError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); + } - if (properties.MinWidth != null && w < properties.MinWidth) - { - context.AddError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); - } + if (properties.MaxWidth != null && w > properties.MaxWidth) + { + context.AddError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); + } - if (properties.MaxWidth != null && w > properties.MaxWidth) - { - context.AddError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); - } + if (properties.MinHeight != null && h < properties.MinHeight) + { + context.AddError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); + } - if (properties.MinHeight != null && h < properties.MinHeight) - { - context.AddError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); - } + if (properties.MaxHeight != null && h > properties.MaxHeight) + { + context.AddError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); + } - if (properties.MaxHeight != null && h > properties.MaxHeight) - { - context.AddError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); - } + if (properties.AspectHeight != null && properties.AspectWidth != null) + { + var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; - if (properties.AspectHeight != null && properties.AspectWidth != null) + if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) { - var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; - - if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) - { - context.AddError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight })); - } + context.AddError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs index 3c32fc89ad..77cbdf430c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs @@ -8,32 +8,31 @@ using System.Collections; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class CollectionItemValidator : IValidator { - public sealed class CollectionItemValidator : IValidator - { - private readonly IValidator itemValidator; + private readonly IValidator itemValidator; - public CollectionItemValidator(IValidator itemValidator) - { - Guard.NotNull(itemValidator); + public CollectionItemValidator(IValidator itemValidator) + { + Guard.NotNull(itemValidator); - this.itemValidator = itemValidator; - } + this.itemValidator = itemValidator; + } - public void Validate(object? value, ValidationContext context) + public void Validate(object? value, ValidationContext context) + { + if (value is ICollection { Count: > 0 } items) { - if (value is ICollection { Count: > 0 } items) - { - var itemIndex = 1; + var itemIndex = 1; - foreach (var item in items) - { - var itemContext = context.Nested($"[{itemIndex}]"); + foreach (var item in items) + { + var itemContext = context.Nested($"[{itemIndex}]"); - itemValidator.Validate(item, itemContext); - itemIndex++; - } + itemValidator.Validate(item, itemContext); + itemIndex++; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs index cde6561722..793fd9902a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -9,60 +9,59 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class CollectionValidator : IValidator { - public sealed class CollectionValidator : IValidator + private readonly bool isRequired; + private readonly int? minItems; + private readonly int? maxItems; + + public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) { - private readonly bool isRequired; - private readonly int? minItems; - private readonly int? maxItems; + if (minItems > maxItems) + { + ThrowHelper.ArgumentException("Min length must be greater than max length.", nameof(minItems)); + } - public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) + this.isRequired = isRequired; + this.minItems = minItems; + this.maxItems = maxItems; + } + + public void Validate(object? value, ValidationContext context) + { + if (value is not ICollection items || items.Count == 0) { - if (minItems > maxItems) + if (isRequired && !context.IsOptional) { - ThrowHelper.ArgumentException("Min length must be greater than max length.", nameof(minItems)); + context.AddError(context.Path, T.Get("contents.validation.required")); } - this.isRequired = isRequired; - this.minItems = minItems; - this.maxItems = maxItems; + return; } - public void Validate(object? value, ValidationContext context) + if (minItems != null && maxItems != null) { - if (value is not ICollection items || items.Count == 0) + if (minItems == maxItems && minItems != items.Count) { - if (isRequired && !context.IsOptional) - { - context.AddError(context.Path, T.Get("contents.validation.required")); - } - - return; + context.AddError(context.Path, T.Get("contents.validation.itemCount", new { count = minItems })); } - - if (minItems != null && maxItems != null) + else if (items.Count < minItems || items.Count > maxItems) { - if (minItems == maxItems && minItems != items.Count) - { - context.AddError(context.Path, T.Get("contents.validation.itemCount", new { count = minItems })); - } - else if (items.Count < minItems || items.Count > maxItems) - { - context.AddError(context.Path, T.Get("contents.validation.itemCountBetween", new { min = minItems, max = maxItems })); - } + context.AddError(context.Path, T.Get("contents.validation.itemCountBetween", new { min = minItems, max = maxItems })); } - else + } + else + { + if (minItems != null && items.Count < minItems) { - if (minItems != null && items.Count < minItems) - { - context.AddError(context.Path, T.Get("contents.validation.minItems", new { min = minItems })); - } + context.AddError(context.Path, T.Get("contents.validation.minItems", new { min = minItems })); + } - if (maxItems != null && items.Count > maxItems) - { - context.AddError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems })); - } + if (maxItems != null && items.Count > maxItems) + { + context.AddError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs index 891a410eb8..7f2d5c7a29 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs @@ -8,25 +8,24 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class ComponentValidator : IValidator { - public sealed class ComponentValidator : IValidator - { - private readonly Func validatorFactory; + private readonly Func validatorFactory; - public ComponentValidator(Func validatorFactory) - { - this.validatorFactory = validatorFactory; - } + public ComponentValidator(Func validatorFactory) + { + this.validatorFactory = validatorFactory; + } - public void Validate(object? value, ValidationContext context) + public void Validate(object? value, ValidationContext context) + { + if (value is Component component) { - if (value is Component component) - { - var validator = validatorFactory(component.Schema); + var validator = validatorFactory(component.Schema); - validator?.Validate(component.Data, context); - } + validator?.Validate(component.Data, context); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs index 374c3c04d7..61519017c2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -10,60 +10,59 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class FieldValidator : IValidator { - public sealed class FieldValidator : IValidator + private readonly IValidator validator; + private readonly IField field; + + public FieldValidator(IValidator validator, IField field) { - private readonly IValidator validator; - private readonly IField field; + Guard.NotNull(field); + Guard.NotNull(validator); - public FieldValidator(IValidator validator, IField field) - { - Guard.NotNull(field); - Guard.NotNull(validator); + this.field = field; + this.validator = validator; + } - this.field = field; - this.validator = validator; - } + public void Validate(object? value, ValidationContext context) + { + var typedValue = value; - public void Validate(object? value, ValidationContext context) + try { - var typedValue = value; - - try + if (value is JsonValue jsonValue) { - if (value is JsonValue jsonValue) + if (jsonValue == default) { - if (jsonValue == default) + typedValue = null; + } + else + { + typedValue = jsonValue.Value; + + var (json, error) = JsonValueConverter.ConvertValue(field, jsonValue, + context.Root.Serializer, + context.Root.Components); + + if (error != null) { - typedValue = null; + context.AddError(context.Path, error.Error); } else { - typedValue = jsonValue.Value; - - var (json, error) = JsonValueConverter.ConvertValue(field, jsonValue, - context.Root.Serializer, - context.Root.Components); - - if (error != null) - { - context.AddError(context.Path, error.Error); - } - else - { - typedValue = json; - } + typedValue = json; } } } - catch - { - context.AddError(context.Path, T.Get("contents.validation.invalid")); - return; - } - - validator.Validate(typedValue, context); } + catch + { + context.AddError(context.Path, T.Get("contents.validation.invalid")); + return; + } + + validator.Validate(typedValue, context); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs index a20f4cd31c..6eb3b1f810 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs @@ -7,22 +7,21 @@ using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class NoValueValidator : IValidator { - public sealed class NoValueValidator : IValidator - { - public static readonly NoValueValidator Instance = new NoValueValidator(); + public static readonly NoValueValidator Instance = new NoValueValidator(); - private NoValueValidator() - { - } + private NoValueValidator() + { + } - public void Validate(object? value, ValidationContext context) + public void Validate(object? value, ValidationContext context) + { + if (!value.IsUndefined()) { - if (!value.IsUndefined()) - { - context.AddError(context.Path, T.Get("contents.validation.mustBeEmpty")); - } + context.AddError(context.Path, T.Get("contents.validation.mustBeEmpty")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs index a96f576e6d..cb2173e2b2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs @@ -7,61 +7,60 @@ using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class ObjectValidator : IValidator { - public sealed class ObjectValidator : IValidator + private static readonly IReadOnlyDictionary DefaultValue = new Dictionary(); + private readonly IDictionary fields; + private readonly bool isPartial; + private readonly string fieldType; + + public ObjectValidator(IDictionary fields, bool isPartial, string fieldType) { - private static readonly IReadOnlyDictionary DefaultValue = new Dictionary(); - private readonly IDictionary fields; - private readonly bool isPartial; - private readonly string fieldType; + this.fields = fields; + this.fieldType = fieldType; + this.isPartial = isPartial; + } - public ObjectValidator(IDictionary fields, bool isPartial, string fieldType) + public void Validate(object? value, ValidationContext context) + { + if (value.IsNullOrUndefined()) { - this.fields = fields; - this.fieldType = fieldType; - this.isPartial = isPartial; + value = DefaultValue; } - public void Validate(object? value, ValidationContext context) + if (value is IReadOnlyDictionary values) { - if (value.IsNullOrUndefined()) + foreach (var fieldData in values) { - value = DefaultValue; + var name = fieldData.Key; + + if (!fields.ContainsKey(name)) + { + context.AddError(context.Path.Enqueue(name), T.Get("contents.validation.unknownField", new { fieldType })); + } } - if (value is IReadOnlyDictionary values) + foreach (var (name, field) in fields) { - foreach (var fieldData in values) - { - var name = fieldData.Key; + var fieldValue = Undefined.Value; - if (!fields.ContainsKey(name)) + if (!values.TryGetValue(name, out var nestedValue)) + { + if (isPartial) { - context.AddError(context.Path.Enqueue(name), T.Get("contents.validation.unknownField", new { fieldType })); + return; } } - - foreach (var (name, field) in fields) + else { - var fieldValue = Undefined.Value; - - if (!values.TryGetValue(name, out var nestedValue)) - { - if (isPartial) - { - return; - } - } - else - { - fieldValue = nestedValue!; - } + fieldValue = nestedValue!; + } - var fieldContext = context.Nested(name, field.IsOptional); + var fieldContext = context.Nested(name, field.IsOptional); - field.Validator.Validate(fieldValue, fieldContext); - } + field.Validator.Validate(fieldValue, fieldContext); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs index 9bfb1e9b65..b6493067c0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs @@ -9,54 +9,53 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public class PatternValidator : IValidator { - public class PatternValidator : IValidator - { - private static readonly TimeSpan Timeout = TimeSpan.FromMilliseconds(20); - private readonly Regex regex; - private readonly string? errorMessage; + private static readonly TimeSpan Timeout = TimeSpan.FromMilliseconds(20); + private readonly Regex regex; + private readonly string? errorMessage; - public PatternValidator(string pattern, string? errorMessage = null, bool capture = false) - { - Guard.NotNullOrEmpty(pattern); + public PatternValidator(string pattern, string? errorMessage = null, bool capture = false) + { + Guard.NotNullOrEmpty(pattern); - this.errorMessage = errorMessage; + this.errorMessage = errorMessage; - var options = RegexOptions.None; + var options = RegexOptions.None; - if (!capture) - { - options |= RegexOptions.ExplicitCapture; - } - - regex = new Regex($"^{pattern}$", options, Timeout); + if (!capture) + { + options |= RegexOptions.ExplicitCapture; } - public void Validate(object? value, ValidationContext context) + regex = new Regex($"^{pattern}$", options, Timeout); + } + + public void Validate(object? value, ValidationContext context) + { + if (value is string stringValue) { - if (value is string stringValue) + if (!string.IsNullOrEmpty(stringValue)) { - if (!string.IsNullOrEmpty(stringValue)) + try { - try + if (!regex.IsMatch(stringValue)) { - if (!regex.IsMatch(stringValue)) + if (string.IsNullOrWhiteSpace(errorMessage)) { - if (string.IsNullOrWhiteSpace(errorMessage)) - { - context.AddError(context.Path, T.Get("contents.validation.pattern")); - } - else - { - context.AddError(context.Path, errorMessage); - } + context.AddError(context.Path, T.Get("contents.validation.pattern")); + } + else + { + context.AddError(context.Path, errorMessage); } } - catch - { - context.AddError(context.Path, T.Get("contents.validation.regexTooSlow")); - } + } + catch + { + context.AddError(context.Path, T.Get("contents.validation.regexTooSlow")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs index 4f16f9d240..5f6a9fa193 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs @@ -8,50 +8,49 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class RangeValidator : IValidator where TValue : struct, IComparable { - public sealed class RangeValidator : IValidator where TValue : struct, IComparable - { - private readonly TValue? min; - private readonly TValue? max; + private readonly TValue? min; + private readonly TValue? max; - public RangeValidator(TValue? min, TValue? max) + public RangeValidator(TValue? min, TValue? max) + { + if (min != null && max != null && min.Value.CompareTo(max.Value) > 0) { - if (min != null && max != null && min.Value.CompareTo(max.Value) > 0) - { - ThrowHelper.ArgumentException("Min value must be greater than max value.", nameof(min)); - } - - this.min = min; - this.max = max; + ThrowHelper.ArgumentException("Min value must be greater than max value.", nameof(min)); } - public void Validate(object? value, ValidationContext context) + this.min = min; + this.max = max; + } + + public void Validate(object? value, ValidationContext context) + { + if (value is TValue typedValue) { - if (value is TValue typedValue) + if (min != null && max != null) { - if (min != null && max != null) + if (Equals(min, max) && Equals(min.Value, max.Value)) { - if (Equals(min, max) && Equals(min.Value, max.Value)) - { - context.AddError(context.Path, T.Get("contents.validation.exactValue", new { value = max.Value })); - } - else if (typedValue.CompareTo(min.Value) < 0 || typedValue.CompareTo(max.Value) > 0) - { - context.AddError(context.Path, T.Get("contents.validation.between", new { min, max })); - } + context.AddError(context.Path, T.Get("contents.validation.exactValue", new { value = max.Value })); } - else + else if (typedValue.CompareTo(min.Value) < 0 || typedValue.CompareTo(max.Value) > 0) { - if (min != null && typedValue.CompareTo(min.Value) < 0) - { - context.AddError(context.Path, T.Get("contents.validation.min", new { min })); - } + context.AddError(context.Path, T.Get("contents.validation.between", new { min, max })); + } + } + else + { + if (min != null && typedValue.CompareTo(min.Value) < 0) + { + context.AddError(context.Path, T.Get("contents.validation.min", new { min })); + } - if (max != null && typedValue.CompareTo(max.Value) > 0) - { - context.AddError(context.Path, T.Get("contents.validation.max", new { max })); - } + if (max != null && typedValue.CompareTo(max.Value) > 0) + { + context.AddError(context.Path, T.Get("contents.validation.max", new { max })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index 3380e11e1f..984900a735 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -12,99 +12,98 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public delegate Task> CheckContentsByIds(HashSet ids); +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; - public sealed class ReferencesValidator : IValidator - { - private readonly ReferencesFieldProperties properties; - private readonly CollectionValidator? collectionValidator; - private readonly UniqueValuesValidator? uniqueValidator; - private readonly CheckContentsByIds checkReferences; +public delegate Task> CheckContentsByIds(HashSet ids); - public ReferencesValidator(bool isRequired, ReferencesFieldProperties properties, CheckContentsByIds checkReferences) - { - Guard.NotNull(properties); - Guard.NotNull(checkReferences); - - this.properties = properties; +public sealed class ReferencesValidator : IValidator +{ + private readonly ReferencesFieldProperties properties; + private readonly CollectionValidator? collectionValidator; + private readonly UniqueValuesValidator? uniqueValidator; + private readonly CheckContentsByIds checkReferences; - if (isRequired || properties.MinItems != null || properties.MaxItems != null) - { - collectionValidator = new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); - } + public ReferencesValidator(bool isRequired, ReferencesFieldProperties properties, CheckContentsByIds checkReferences) + { + Guard.NotNull(properties); + Guard.NotNull(checkReferences); - if (!properties.AllowDuplicates) - { - uniqueValidator = new UniqueValuesValidator(); - } + this.properties = properties; - this.checkReferences = checkReferences; + if (isRequired || properties.MinItems != null || properties.MaxItems != null) + { + collectionValidator = new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } - public void Validate(object? value, ValidationContext context) + if (!properties.AllowDuplicates) { - context.Root.AddTask(ct => ValidateCoreAsync(value, context)); + uniqueValidator = new UniqueValuesValidator(); } - private async Task ValidateCoreAsync(object? value, ValidationContext context) + this.checkReferences = checkReferences; + } + + public void Validate(object? value, ValidationContext context) + { + context.Root.AddTask(ct => ValidateCoreAsync(value, context)); + } + + private async Task ValidateCoreAsync(object? value, ValidationContext context) + { + var foundIds = new List(); + + if (value is ICollection { Count: > 0 } contentIds) { - var foundIds = new List(); + var references = await checkReferences(contentIds.ToHashSet()); + var referenceIndex = 1; - if (value is ICollection { Count: > 0 } contentIds) + foreach (var id in contentIds) { - var references = await checkReferences(contentIds.ToHashSet()); - var referenceIndex = 1; - - foreach (var id in contentIds) - { - var path = context.Path.Enqueue($"[{referenceIndex}]"); + var path = context.Path.Enqueue($"[{referenceIndex}]"); - var (schemaId, _, status) = references.FirstOrDefault(x => x.Id == id); + var (schemaId, _, status) = references.FirstOrDefault(x => x.Id == id); - if (schemaId == DomainId.Empty) + if (schemaId == DomainId.Empty) + { + if (context.Action == ValidationAction.Upsert) { - if (context.Action == ValidationAction.Upsert) - { - context.AddError(path, T.Get("contents.validation.referenceNotFound", new { id })); - } - - continue; + context.AddError(path, T.Get("contents.validation.referenceNotFound", new { id })); } - var isValid = true; + continue; + } - if (properties.SchemaIds?.Any() == true && !properties.SchemaIds.Contains(schemaId)) - { - if (context.Action == ValidationAction.Upsert) - { - context.AddError(path, T.Get("contents.validation.referenceToInvalidSchema", new { id })); - } + var isValid = true; - isValid = false; + if (properties.SchemaIds?.Any() == true && !properties.SchemaIds.Contains(schemaId)) + { + if (context.Action == ValidationAction.Upsert) + { + context.AddError(path, T.Get("contents.validation.referenceToInvalidSchema", new { id })); } - isValid &= !properties.MustBePublished || status == Status.Published; + isValid = false; + } - if (isValid) - { - foundIds.Add(id); - } + isValid &= !properties.MustBePublished || status == Status.Published; - referenceIndex++; + if (isValid) + { + foundIds.Add(id); } - } - if (collectionValidator != null) - { - collectionValidator.Validate(foundIds, context); + referenceIndex++; } + } - if (uniqueValidator != null) - { - uniqueValidator.Validate(foundIds, context); - } + if (collectionValidator != null) + { + collectionValidator.Validate(foundIds, context); + } + + if (uniqueValidator != null) + { + uniqueValidator.Validate(foundIds, context); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs index 39c827550c..eaaa1bf997 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs @@ -7,33 +7,32 @@ using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public class RequiredStringValidator : IValidator { - public class RequiredStringValidator : IValidator + private readonly bool validateEmptyStrings; + + public RequiredStringValidator(bool validateEmptyStrings = false) { - private readonly bool validateEmptyStrings; + this.validateEmptyStrings = validateEmptyStrings; + } - public RequiredStringValidator(bool validateEmptyStrings = false) + public void Validate(object? value, ValidationContext context) + { + if (context.IsOptional) { - this.validateEmptyStrings = validateEmptyStrings; + return; } - public void Validate(object? value, ValidationContext context) + if (value.IsNullOrUndefined() || IsEmptyString(value)) { - if (context.IsOptional) - { - return; - } - - if (value.IsNullOrUndefined() || IsEmptyString(value)) - { - context.AddError(context.Path, T.Get("contents.validation.required")); - } + context.AddError(context.Path, T.Get("contents.validation.required")); } + } - private bool IsEmptyString(object? value) - { - return value is string typed && validateEmptyStrings && string.IsNullOrWhiteSpace(typed); - } + private bool IsEmptyString(object? value) + { + return value is string typed && validateEmptyStrings && string.IsNullOrWhiteSpace(typed); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs index 2f7a2dc4ae..a585dff10d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs @@ -7,16 +7,15 @@ using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public class RequiredValidator : IValidator { - public class RequiredValidator : IValidator + public void Validate(object? value, ValidationContext context) { - public void Validate(object? value, ValidationContext context) + if (value.IsNullOrUndefined() && !context.IsOptional) { - if (value.IsNullOrUndefined() && !context.IsOptional) - { - context.AddError(context.Path, T.Get("contents.validation.required")); - } + context.AddError(context.Path, T.Get("contents.validation.required")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs index 36e94a0ce7..38ee990f63 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs @@ -8,50 +8,49 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public class StringLengthValidator : IValidator { - public class StringLengthValidator : IValidator - { - private readonly int? minLength; - private readonly int? maxLength; + private readonly int? minLength; + private readonly int? maxLength; - public StringLengthValidator(int? minLength = null, int? maxLength = null) + public StringLengthValidator(int? minLength = null, int? maxLength = null) + { + if (minLength > maxLength) { - if (minLength > maxLength) - { - ThrowHelper.ArgumentException("Min length must be greater than max length.", nameof(minLength)); - } - - this.minLength = minLength; - this.maxLength = maxLength; + ThrowHelper.ArgumentException("Min length must be greater than max length.", nameof(minLength)); } - public void Validate(object? value, ValidationContext context) + this.minLength = minLength; + this.maxLength = maxLength; + } + + public void Validate(object? value, ValidationContext context) + { + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) { - if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + if (minLength != null && maxLength != null) { - if (minLength != null && maxLength != null) + if (minLength == maxLength && minLength != stringValue.Length) { - if (minLength == maxLength && minLength != stringValue.Length) - { - context.AddError(context.Path, T.Get("contents.validation.characterCount", new { count = minLength })); - } - else if (stringValue.Length < minLength || stringValue.Length > maxLength) - { - context.AddError(context.Path, T.Get("contents.validation.charactersBetween", new { min = minLength, max = maxLength })); - } + context.AddError(context.Path, T.Get("contents.validation.characterCount", new { count = minLength })); } - else + else if (stringValue.Length < minLength || stringValue.Length > maxLength) { - if (minLength != null && stringValue.Length < minLength) - { - context.AddError(context.Path, T.Get("contents.validation.minLength", new { min = minLength })); - } + context.AddError(context.Path, T.Get("contents.validation.charactersBetween", new { min = minLength, max = maxLength })); + } + } + else + { + if (minLength != null && stringValue.Length < minLength) + { + context.AddError(context.Path, T.Get("contents.validation.minLength", new { min = minLength })); + } - if (maxLength != null && stringValue.Length > maxLength) - { - context.AddError(context.Path, T.Get("contents.validation.maxLength", new { max = maxLength })); - } + if (maxLength != null && stringValue.Length > maxLength) + { + context.AddError(context.Path, T.Get("contents.validation.maxLength", new { max = maxLength })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs index 9bc90e2ebe..78253a60cb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs @@ -9,103 +9,102 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class StringTextValidator : IValidator { - public sealed class StringTextValidator : IValidator + private readonly Func? transform; + private readonly int? minCharacters; + private readonly int? maxCharacters; + private readonly int? minWords; + private readonly int? maxWords; + + public StringTextValidator(Func? transform = null, + int? minCharacters = null, + int? maxCharacters = null, + int? minWords = null, + int? maxWords = null) { - private readonly Func? transform; - private readonly int? minCharacters; - private readonly int? maxCharacters; - private readonly int? minWords; - private readonly int? maxWords; + if (minCharacters > maxCharacters) + { + ThrowHelper.ArgumentException("Min characters must be greater than max characters.", nameof(minCharacters)); + } - public StringTextValidator(Func? transform = null, - int? minCharacters = null, - int? maxCharacters = null, - int? minWords = null, - int? maxWords = null) + if (minWords > maxWords) { - if (minCharacters > maxCharacters) - { - ThrowHelper.ArgumentException("Min characters must be greater than max characters.", nameof(minCharacters)); - } + ThrowHelper.ArgumentException("Min words must be greater than max words.", nameof(minWords)); + } + + this.transform = transform; + this.minCharacters = minCharacters; + this.maxCharacters = maxCharacters; + this.minWords = minWords; + this.maxWords = maxWords; + } - if (minWords > maxWords) + public void Validate(object? value, ValidationContext context) + { + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + if (transform != null) { - ThrowHelper.ArgumentException("Min words must be greater than max words.", nameof(minWords)); + stringValue = transform(stringValue); } - this.transform = transform; - this.minCharacters = minCharacters; - this.maxCharacters = maxCharacters; - this.minWords = minWords; - this.maxWords = maxWords; - } - - public void Validate(object? value, ValidationContext context) - { - if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + if (minWords != null || maxWords != null) { - if (transform != null) - { - stringValue = transform(stringValue); - } + var words = stringValue.WordCount(); - if (minWords != null || maxWords != null) + if (minWords != null && maxWords != null) { - var words = stringValue.WordCount(); - - if (minWords != null && maxWords != null) + if (minWords == maxWords && minWords != words) { - if (minWords == maxWords && minWords != words) - { - context.AddError(context.Path, T.Get("contents.validation.wordCount", new { count = minWords })); - } - else if (words < minWords || words > maxWords) - { - context.AddError(context.Path, T.Get("contents.validation.wordsBetween", new { min = minWords, max = maxWords })); - } + context.AddError(context.Path, T.Get("contents.validation.wordCount", new { count = minWords })); } - else + else if (words < minWords || words > maxWords) { - if (words < minWords) - { - context.AddError(context.Path, T.Get("contents.validation.minWords", new { min = minWords })); - } + context.AddError(context.Path, T.Get("contents.validation.wordsBetween", new { min = minWords, max = maxWords })); + } + } + else + { + if (words < minWords) + { + context.AddError(context.Path, T.Get("contents.validation.minWords", new { min = minWords })); + } - if (words > maxWords) - { - context.AddError(context.Path, T.Get("contents.validation.maxWords", new { max = maxWords })); - } + if (words > maxWords) + { + context.AddError(context.Path, T.Get("contents.validation.maxWords", new { max = maxWords })); } } + } - if (minCharacters != null || maxCharacters != null) - { - var characters = stringValue.CharacterCount(); + if (minCharacters != null || maxCharacters != null) + { + var characters = stringValue.CharacterCount(); - if (minCharacters != null && maxCharacters != null) + if (minCharacters != null && maxCharacters != null) + { + if (minCharacters == maxCharacters && minCharacters != characters) + { + context.AddError(context.Path, T.Get("contents.validation.normalCharacterCount", new { count = minCharacters })); + } + else if (characters < minCharacters || characters > maxCharacters) { - if (minCharacters == maxCharacters && minCharacters != characters) - { - context.AddError(context.Path, T.Get("contents.validation.normalCharacterCount", new { count = minCharacters })); - } - else if (characters < minCharacters || characters > maxCharacters) - { - context.AddError(context.Path, T.Get("contents.validation.normalCharactersBetween", new { min = minCharacters, max = maxCharacters })); - } + context.AddError(context.Path, T.Get("contents.validation.normalCharactersBetween", new { min = minCharacters, max = maxCharacters })); } - else + } + else + { + if (characters < minCharacters) { - if (characters < minCharacters) - { - context.AddError(context.Path, T.Get("contents.validation.minNormalCharacters", new { min = minCharacters })); - } + context.AddError(context.Path, T.Get("contents.validation.minNormalCharacters", new { min = minCharacters })); + } - if (characters > maxCharacters) - { - context.AddError(context.Path, T.Get("contents.validation.maxCharacters", new { max = maxCharacters })); - } + if (characters > maxCharacters) + { + context.AddError(context.Path, T.Get("contents.validation.maxCharacters", new { max = maxCharacters })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs index 9f3b258c9c..e6eb465eec 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs @@ -9,44 +9,43 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class UniqueObjectValuesValidator : IValidator { - public sealed class UniqueObjectValuesValidator : IValidator + private readonly IEnumerable fields; + + public UniqueObjectValuesValidator(IEnumerable fields) { - private readonly IEnumerable fields; + this.fields = fields; + } - public UniqueObjectValuesValidator(IEnumerable fields) + public void Validate(object? value, ValidationContext context) + { + if (value is IEnumerable objects && objects.Count() > 1) { - this.fields = fields; + Validate(objects, context); } - - public void Validate(object? value, ValidationContext context) + else if (value is IEnumerable components && components.Count() > 1) { - if (value is IEnumerable objects && objects.Count() > 1) - { - Validate(objects, context); - } - else if (value is IEnumerable components && components.Count() > 1) - { - Validate(components.Select(x => x.Data), context); - } + Validate(components.Select(x => x.Data), context); } + } - private void Validate(IEnumerable items, ValidationContext context) + private void Validate(IEnumerable items, ValidationContext context) + { + var duplicates = new HashSet(10); + + foreach (var field in fields) { - var duplicates = new HashSet(10); + duplicates.Clear(); - foreach (var field in fields) + foreach (var item in items) { - duplicates.Clear(); - - foreach (var item in items) + if (item.TryGetValue(field, out var fieldValue) && !duplicates.Add(fieldValue)) { - if (item.TryGetValue(field, out var fieldValue) && !duplicates.Add(fieldValue)) - { - context.AddError(context.Path, T.Get("contents.validation.uniqueObjectValues", new { field })); - break; - } + context.AddError(context.Path, T.Get("contents.validation.uniqueObjectValues", new { field })); + break; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs index 44004d893c..2e299b8a85 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs @@ -11,56 +11,55 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public delegate Task> CheckUniqueness(FilterNode filter); + +public sealed class UniqueValidator : IValidator { - public delegate Task> CheckUniqueness(FilterNode filter); + private readonly CheckUniqueness checkUniqueness; - public sealed class UniqueValidator : IValidator + public UniqueValidator(CheckUniqueness checkUniqueness) { - private readonly CheckUniqueness checkUniqueness; + this.checkUniqueness = checkUniqueness; + } - public UniqueValidator(CheckUniqueness checkUniqueness) - { - this.checkUniqueness = checkUniqueness; - } + public void Validate(object? value, ValidationContext context) + { + var count = context.Path.Count(); - public void Validate(object? value, ValidationContext context) + if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) { - var count = context.Path.Count(); + FilterNode? filter = null; - if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) + if (value is string s) { - FilterNode? filter = null; - - if (value is string s) - { - filter = ClrFilter.Eq(Path(context), s); - } - else if (value is double d) - { - filter = ClrFilter.Eq(Path(context), d); - } - - if (filter != null) - { - context.Root.AddTask(ct => ValidateCoreAsync(context, filter)); - } + filter = ClrFilter.Eq(Path(context), s); + } + else if (value is double d) + { + filter = ClrFilter.Eq(Path(context), d); } - } - - private async Task ValidateCoreAsync(ValidationContext context, FilterNode filter) - { - var found = await checkUniqueness(filter); - if (found.Any(x => x.Id != context.Root.ContentId)) + if (filter != null) { - context.AddError(context.Path, T.Get("contents.validation.unique")); + context.Root.AddTask(ct => ValidateCoreAsync(context, filter)); } } + } - private static List Path(ValidationContext context) + private async Task ValidateCoreAsync(ValidationContext context, FilterNode filter) + { + var found = await checkUniqueness(filter); + + if (found.Any(x => x.Id != context.Root.ContentId)) { - return Enumerable.Repeat("Data", 1).Union(context.Path).ToList(); + context.AddError(context.Path, T.Get("contents.validation.unique")); } } + + private static List Path(ValidationContext context) + { + return Enumerable.Repeat("Data", 1).Union(context.Path).ToList(); + } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs index a8d498083f..b1ecdfa7c7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs @@ -7,20 +7,19 @@ using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; + +public sealed class UniqueValuesValidator : IValidator { - public sealed class UniqueValuesValidator : IValidator + public void Validate(object? value, ValidationContext context) { - public void Validate(object? value, ValidationContext context) + if (value is IEnumerable items && items.Any()) { - if (value is IEnumerable items && items.Any()) - { - var itemsArray = items.ToArray(); + var itemsArray = items.ToArray(); - if (itemsArray.Length != itemsArray.Distinct().Count()) - { - context.AddError(context.Path, T.Get("contents.validation.duplicates")); - } + if (itemsArray.Length != itemsArray.Distinct().Count()) + { + context.AddError(context.Path, T.Get("contents.validation.duplicates")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs index e1680a824b..fcbcb0eec5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/AdaptIdVisitor.cs @@ -11,71 +11,70 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.MongoDb +namespace Squidex.Domain.Apps.Entities.MongoDb; + +internal sealed class AdaptIdVisitor : TransformVisitor { - internal sealed class AdaptIdVisitor : TransformVisitor + private static readonly AdaptIdVisitor Instance = new AdaptIdVisitor(); + + public record struct Args(DomainId AppId); + + private AdaptIdVisitor() { - private static readonly AdaptIdVisitor Instance = new AdaptIdVisitor(); + } - public record struct Args(DomainId AppId); + public static FilterNode? AdaptFilter(FilterNode filter, DomainId appId) + { + var args = new Args(appId); - private AdaptIdVisitor() - { - } + return filter.Accept(Instance, args); + } - public static FilterNode? AdaptFilter(FilterNode filter, DomainId appId) - { - var args = new Args(appId); + public override FilterNode Visit(CompareFilter nodeIn, Args args) + { + var (path, _, value) = nodeIn; - return filter.Accept(Instance, args); - } + var clrValue = value.Value; - public override FilterNode Visit(CompareFilter nodeIn, Args args) + if (string.Equals(path[0], "id", StringComparison.OrdinalIgnoreCase)) { - var (path, _, value) = nodeIn; + path = "_id"; - var clrValue = value.Value; - - if (string.Equals(path[0], "id", StringComparison.OrdinalIgnoreCase)) + if (clrValue is List idList) { - path = "_id"; - - if (clrValue is List idList) - { - value = idList.Select(x => DomainId.Combine(args.AppId, DomainId.Create(x)).ToString()).ToList(); - } - else if (clrValue is string id) - { - value = DomainId.Combine(args.AppId, DomainId.Create(id)).ToString(); - } - else if (clrValue is List guidIdList) - { - value = guidIdList.Select(x => DomainId.Combine(args.AppId, DomainId.Create(x)).ToString()).ToList(); - } - else if (clrValue is Guid guidId) - { - value = DomainId.Combine(args.AppId, DomainId.Create(guidId)).ToString(); - } + value = idList.Select(x => DomainId.Combine(args.AppId, DomainId.Create(x)).ToString()).ToList(); } - else + else if (clrValue is string id) { - if (clrValue is List guidList) - { - value = guidList.Select(x => x.ToString()).ToList(); - } - else if (clrValue is Guid guid) - { - value = guid.ToString(); - } - else if (clrValue is Instant && - !string.Equals(path[0], "mt", StringComparison.OrdinalIgnoreCase) && - !string.Equals(path[0], "ct", StringComparison.OrdinalIgnoreCase)) - { - value = clrValue.ToString(); - } + value = DomainId.Combine(args.AppId, DomainId.Create(id)).ToString(); + } + else if (clrValue is List guidIdList) + { + value = guidIdList.Select(x => DomainId.Combine(args.AppId, DomainId.Create(x)).ToString()).ToList(); + } + else if (clrValue is Guid guidId) + { + value = DomainId.Combine(args.AppId, DomainId.Create(guidId)).ToString(); } - - return nodeIn with { Path = path, Value = value }; } + else + { + if (clrValue is List guidList) + { + value = guidList.Select(x => x.ToString()).ToList(); + } + else if (clrValue is Guid guid) + { + value = guid.ToString(); + } + else if (clrValue is Instant && + !string.Equals(path[0], "mt", StringComparison.OrdinalIgnoreCase) && + !string.Equals(path[0], "ct", StringComparison.OrdinalIgnoreCase)) + { + value = clrValue.ToString(); + } + } + + return nodeIn with { Path = path, Value = value }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs index 87f62551f7..d0068d0715 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs @@ -11,45 +11,44 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Apps +namespace Squidex.Domain.Apps.Entities.MongoDb.Apps; + +public sealed class MongoAppEntity : MongoState { - public sealed class MongoAppEntity : MongoState - { - [BsonRequired] - [BsonElement("_an")] - public string IndexedName { get; set; } + [BsonRequired] + [BsonElement("_an")] + public string IndexedName { get; set; } - [BsonRequired] - [BsonElement("_ui")] - public string[] IndexedUserIds { get; set; } + [BsonRequired] + [BsonElement("_ui")] + public string[] IndexedUserIds { get; set; } - [BsonIgnoreIfDefault] - [BsonElement("_ti")] - public DomainId? IndexedTeamId { get; set; } + [BsonIgnoreIfDefault] + [BsonElement("_ti")] + public DomainId? IndexedTeamId { get; set; } - [BsonRequired] - [BsonElement("_dl")] - public bool IndexedDeleted { get; set; } + [BsonRequired] + [BsonElement("_dl")] + public bool IndexedDeleted { get; set; } - [BsonIgnoreIfDefault] - [BsonElement("_ct")] - public Instant IndexedCreated { get; set; } + [BsonIgnoreIfDefault] + [BsonElement("_ct")] + public Instant IndexedCreated { get; set; } - public override void Prepare() + public override void Prepare() + { + var users = new HashSet { - var users = new HashSet - { - Document.CreatedBy.Identifier - }; - - users.AddRange(Document.Contributors.Keys); - users.AddRange(Document.Clients.Keys); - - IndexedUserIds = users.ToArray(); - IndexedCreated = Document.Created; - IndexedDeleted = Document.IsDeleted; - IndexedTeamId = Document.TeamId; - IndexedName = Document.Name; - } + Document.CreatedBy.Identifier + }; + + users.AddRange(Document.Contributors.Keys); + users.AddRange(Document.Clients.Keys); + + IndexedUserIds = users.ToArray(); + IndexedCreated = Document.Created; + IndexedDeleted = Document.IsDeleted; + IndexedTeamId = Document.TeamId; + IndexedName = Document.Name; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index c9d95089c2..5908e3359c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -12,101 +12,100 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Apps +namespace Squidex.Domain.Apps.Entities.MongoDb.Apps; + +public sealed class MongoAppRepository : MongoSnapshotStoreBase, IAppRepository, IDeleter { - public sealed class MongoAppRepository : MongoSnapshotStoreBase, IAppRepository, IDeleter + public MongoAppRepository(IMongoDatabase database) + : base(database) { - public MongoAppRepository(IMongoDatabase database) - : base(database) - { - } + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedName)), - new CreateIndexModel( - Index - .Ascending(x => x.IndexedUserIds)), - new CreateIndexModel( - Index - .Ascending(x => x.IndexedTeamId)) - }, ct); - } + new CreateIndexModel( + Index + .Ascending(x => x.IndexedName)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedUserIds)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedTeamId)) + }, ct); + } - Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return Collection.DeleteManyAsync(Filter.Eq(x => x.DocumentId, app.Id), ct); - } + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return Collection.DeleteManyAsync(Filter.Eq(x => x.DocumentId, app.Id), ct); + } - public async Task> QueryAllAsync(string contributorId, IEnumerable names, - CancellationToken ct = default) + public async Task> QueryAllAsync(string contributorId, IEnumerable names, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAllAsync")) { - using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAllAsync")) - { - var entities = - await Collection.Find(x => (x.IndexedUserIds.Contains(contributorId) || names.Contains(x.IndexedName)) && !x.IndexedDeleted) - .ToListAsync(ct); - - return RemoveDuplcateNames(entities); - } - } + var entities = + await Collection.Find(x => (x.IndexedUserIds.Contains(contributorId) || names.Contains(x.IndexedName)) && !x.IndexedDeleted) + .ToListAsync(ct); - public async Task> QueryAllAsync(DomainId teamId, - CancellationToken ct = default) - { - using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAllAsync")) - { - var entities = - await Collection.Find(x => x.IndexedTeamId == teamId).SortBy(x => x.IndexedCreated) - .ToListAsync(ct); - - return RemoveDuplcateNames(entities); - } + return RemoveDuplcateNames(entities); } + } - public async Task FindAsync(DomainId id, - CancellationToken ct = default) + public async Task> QueryAllAsync(DomainId teamId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAllAsync")) { - using (Telemetry.Activities.StartActivity("MongoAppRepository/FindAsync")) - { - var entity = - await Collection.Find(x => x.DocumentId == id && !x.IndexedDeleted) - .FirstOrDefaultAsync(ct); - - return entity?.Document; - } + var entities = + await Collection.Find(x => x.IndexedTeamId == teamId).SortBy(x => x.IndexedCreated) + .ToListAsync(ct); + + return RemoveDuplcateNames(entities); } + } - public async Task FindAsync(string name, - CancellationToken ct = default) + public async Task FindAsync(DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/FindAsync")) { - using (Telemetry.Activities.StartActivity("MongoAppRepository/FindAsyncByName")) - { - var entity = - await Collection.Find(x => x.IndexedName == name && !x.IndexedDeleted).SortByDescending(x => x.IndexedCreated) - .FirstOrDefaultAsync(ct); - - return entity?.Document; - } + var entity = + await Collection.Find(x => x.DocumentId == id && !x.IndexedDeleted) + .FirstOrDefaultAsync(ct); + + return entity?.Document; } + } - private static List RemoveDuplcateNames(List entities) + public async Task FindAsync(string name, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/FindAsyncByName")) { - var byName = new Dictionary(); + var entity = + await Collection.Find(x => x.IndexedName == name && !x.IndexedDeleted).SortByDescending(x => x.IndexedCreated) + .FirstOrDefaultAsync(ct); - // Remove duplicate names, the latest wins. - foreach (var entity in entities.OrderBy(x => x.IndexedCreated)) - { - byName[entity.IndexedName] = entity.Document; - } + return entity?.Document; + } + } - return byName.Values.ToList(); + private static List RemoveDuplcateNames(List entities) + { + var byName = new Dictionary(); + + // Remove duplicate names, the latest wins. + foreach (var entity in entities.OrderBy(x => x.IndexedCreated)) + { + byName[entity.IndexedName] = entity.Document; } + + return byName.Values.ToList(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 715e89059f..2fbf2c496d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -15,121 +15,120 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets; + +public sealed class MongoAssetEntity : IAssetEntity, IVersionedEntity { - public sealed class MongoAssetEntity : IAssetEntity, IVersionedEntity + [BsonId] + [BsonElement("_id")] + public DomainId DocumentId { get; set; } + + [BsonRequired] + [BsonElement("_ai")] + public DomainId IndexedAppId { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("id")] + public DomainId Id { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("pi")] + public DomainId ParentId { get; set; } + + [BsonRequired] + [BsonElement("ai")] + public NamedId AppId { get; set; } + + [BsonRequired] + [BsonElement("ct")] + public Instant Created { get; set; } + + [BsonRequired] + [BsonElement("mt")] + public Instant LastModified { get; set; } + + [BsonRequired] + [BsonElement("mm")] + public string MimeType { get; set; } + + [BsonRequired] + [BsonElement("fn")] + public string FileName { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("fh")] + public string FileHash { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("sl")] + public string Slug { get; set; } + + [BsonRequired] + [BsonElement("fs")] + public long FileSize { get; set; } + + [BsonRequired] + [BsonElement("fv")] + public long FileVersion { get; set; } + + [BsonRequired] + [BsonElement("vs")] + public long Version { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("ts")] + public long TotalSize { get; set; } + + [BsonRequired] + [BsonElement("at")] + public AssetType Type { get; set; } + + [BsonRequired] + [BsonElement("cb")] + public RefToken CreatedBy { get; set; } + + [BsonRequired] + [BsonElement("mb")] + public RefToken LastModifiedBy { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("td")] + public HashSet Tags { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("pt")] + public bool IsProtected { get; set; } + + [BsonRequired] + [BsonElement("dl")] + public bool IsDeleted { get; set; } + + [BsonRequired] + [BsonElement("md")] + public AssetMetadata Metadata { get; set; } + + public DomainId AssetId { - [BsonId] - [BsonElement("_id")] - public DomainId DocumentId { get; set; } - - [BsonRequired] - [BsonElement("_ai")] - public DomainId IndexedAppId { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("id")] - public DomainId Id { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("pi")] - public DomainId ParentId { get; set; } - - [BsonRequired] - [BsonElement("ai")] - public NamedId AppId { get; set; } - - [BsonRequired] - [BsonElement("ct")] - public Instant Created { get; set; } - - [BsonRequired] - [BsonElement("mt")] - public Instant LastModified { get; set; } - - [BsonRequired] - [BsonElement("mm")] - public string MimeType { get; set; } - - [BsonRequired] - [BsonElement("fn")] - public string FileName { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("fh")] - public string FileHash { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("sl")] - public string Slug { get; set; } - - [BsonRequired] - [BsonElement("fs")] - public long FileSize { get; set; } - - [BsonRequired] - [BsonElement("fv")] - public long FileVersion { get; set; } - - [BsonRequired] - [BsonElement("vs")] - public long Version { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("ts")] - public long TotalSize { get; set; } - - [BsonRequired] - [BsonElement("at")] - public AssetType Type { get; set; } - - [BsonRequired] - [BsonElement("cb")] - public RefToken CreatedBy { get; set; } - - [BsonRequired] - [BsonElement("mb")] - public RefToken LastModifiedBy { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("td")] - public HashSet Tags { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("pt")] - public bool IsProtected { get; set; } - - [BsonRequired] - [BsonElement("dl")] - public bool IsDeleted { get; set; } - - [BsonRequired] - [BsonElement("md")] - public AssetMetadata Metadata { get; set; } - - public DomainId AssetId - { - get => Id; - } - - public DomainId UniqueId - { - get => DocumentId; - } - - public AssetDomainObject.State ToState() - { - return SimpleMapper.Map(this, new AssetDomainObject.State()); - } + get => Id; + } - public static MongoAssetEntity Create(SnapshotWriteJob job) - { - var entity = SimpleMapper.Map(job.Value, new MongoAssetEntity()); + public DomainId UniqueId + { + get => DocumentId; + } + + public AssetDomainObject.State ToState() + { + return SimpleMapper.Map(this, new AssetDomainObject.State()); + } + + public static MongoAssetEntity Create(SnapshotWriteJob job) + { + var entity = SimpleMapper.Map(job.Value, new MongoAssetEntity()); - entity.DocumentId = job.Key; - entity.IndexedAppId = job.Value.AppId.Id; + entity.DocumentId = job.Key; + entity.IndexedAppId = job.Value.AppId.Id; - return entity; - } + return entity; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs index eb6ae6d496..1df4a7bffb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs @@ -14,76 +14,75 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets; + +public sealed class MongoAssetFolderEntity : IAssetFolderEntity, IVersionedEntity { - public sealed class MongoAssetFolderEntity : IAssetFolderEntity, IVersionedEntity - { - [BsonId] - [BsonElement("_id")] - public DomainId DocumentId { get; set; } + [BsonId] + [BsonElement("_id")] + public DomainId DocumentId { get; set; } - [BsonRequired] - [BsonElement("_ai")] - public DomainId IndexedAppId { get; set; } + [BsonRequired] + [BsonElement("_ai")] + public DomainId IndexedAppId { get; set; } - [BsonRequired] - [BsonElement("id")] - public DomainId Id { get; set; } + [BsonRequired] + [BsonElement("id")] + public DomainId Id { get; set; } - [BsonRequired] - [BsonElement("pi")] - public DomainId ParentId { get; set; } + [BsonRequired] + [BsonElement("pi")] + public DomainId ParentId { get; set; } - [BsonRequired] - [BsonElement("ai")] - public NamedId AppId { get; set; } + [BsonRequired] + [BsonElement("ai")] + public NamedId AppId { get; set; } - [BsonRequired] - [BsonElement("ct")] - public Instant Created { get; set; } + [BsonRequired] + [BsonElement("ct")] + public Instant Created { get; set; } - [BsonRequired] - [BsonElement("mt")] - public Instant LastModified { get; set; } + [BsonRequired] + [BsonElement("mt")] + public Instant LastModified { get; set; } - [BsonRequired] - [BsonElement("fn")] - public string FolderName { get; set; } + [BsonRequired] + [BsonElement("fn")] + public string FolderName { get; set; } - [BsonRequired] - [BsonElement("vs")] - public long Version { get; set; } + [BsonRequired] + [BsonElement("vs")] + public long Version { get; set; } - [BsonRequired] - [BsonElement("cb")] - public RefToken CreatedBy { get; set; } + [BsonRequired] + [BsonElement("cb")] + public RefToken CreatedBy { get; set; } - [BsonRequired] - [BsonElement("mb")] - public RefToken LastModifiedBy { get; set; } + [BsonRequired] + [BsonElement("mb")] + public RefToken LastModifiedBy { get; set; } - [BsonRequired] - [BsonElement("dl")] - public bool IsDeleted { get; set; } + [BsonRequired] + [BsonElement("dl")] + public bool IsDeleted { get; set; } - public DomainId UniqueId - { - get => DocumentId; - } + public DomainId UniqueId + { + get => DocumentId; + } - public AssetFolderDomainObject.State ToState() - { - return SimpleMapper.Map(this, new AssetFolderDomainObject.State()); - } + public AssetFolderDomainObject.State ToState() + { + return SimpleMapper.Map(this, new AssetFolderDomainObject.State()); + } - public static MongoAssetFolderEntity Create(SnapshotWriteJob job) - { - var entity = SimpleMapper.Map(job.Value, new MongoAssetFolderEntity()); + public static MongoAssetFolderEntity Create(SnapshotWriteJob job) + { + var entity = SimpleMapper.Map(job.Value, new MongoAssetFolderEntity()); - entity.DocumentId = job.Key; - entity.IndexedAppId = job.Value.AppId.Id; + entity.DocumentId = job.Key; + entity.IndexedAppId = job.Value.AppId.Id; - return entity; - } + return entity; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs index 02ccb6d342..d5c191baba 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs @@ -11,104 +11,103 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets; + +public sealed partial class MongoAssetFolderRepository : MongoRepositoryBase, IAssetFolderRepository { - public sealed partial class MongoAssetFolderRepository : MongoRepositoryBase, IAssetFolderRepository + public MongoAssetFolderRepository(IMongoDatabase database) + : base(database) { - public MongoAssetFolderRepository(IMongoDatabase database) - : base(database) - { - } + } - protected override string CollectionName() - { - return "States_AssetFolders2"; - } + protected override string CollectionName() + { + return "States_AssetFolders2"; + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.ParentId) - .Ascending(x => x.IsDeleted)) - }, ct); - } + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.ParentId) + .Ascending(x => x.IsDeleted)) + }, ct); + } - public async Task> QueryAsync(DomainId appId, DomainId parentId, - CancellationToken ct = default) + public async Task> QueryAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/QueryAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/QueryAsync")) - { - var filter = BuildFilter(appId, parentId); + var filter = BuildFilter(appId, parentId); - var assetFolderEntities = - await Collection.Find(filter).SortBy(x => x.FolderName) - .ToListAsync(ct); + var assetFolderEntities = + await Collection.Find(filter).SortBy(x => x.FolderName) + .ToListAsync(ct); - return ResultList.Create(assetFolderEntities.Count, assetFolderEntities); - } + return ResultList.Create(assetFolderEntities.Count, assetFolderEntities); } + } - public async Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, - CancellationToken ct = default) + public async Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/QueryChildIdsAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/QueryChildIdsAsync")) - { - var filter = BuildFilter(appId, parentId); + var filter = BuildFilter(appId, parentId); - var assetFolderEntities = - await Collection.Find(filter).Only(x => x.Id) - .ToListAsync(ct); + var assetFolderEntities = + await Collection.Find(filter).Only(x => x.Id) + .ToListAsync(ct); - var field = Field.Of(x => nameof(x.Id)); + var field = Field.Of(x => nameof(x.Id)); - return assetFolderEntities.Select(x => DomainId.Create(x[field].AsString)).ToList(); - } + return assetFolderEntities.Select(x => DomainId.Create(x[field].AsString)).ToList(); } + } - public async Task FindAssetFolderAsync(DomainId appId, DomainId id, - CancellationToken ct = default) + public async Task FindAssetFolderAsync(DomainId appId, DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/FindAssetFolderAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/FindAssetFolderAsync")) - { - var documentId = DomainId.Combine(appId, id); + var documentId = DomainId.Combine(appId, id); - var assetFolderEntity = - await Collection.Find(x => x.DocumentId == documentId && !x.IsDeleted) - .FirstOrDefaultAsync(ct); + var assetFolderEntity = + await Collection.Find(x => x.DocumentId == documentId && !x.IsDeleted) + .FirstOrDefaultAsync(ct); - return assetFolderEntity; - } + return assetFolderEntity; } + } - private static FilterDefinition BuildFilter(DomainId appId, DomainId? parentId) + private static FilterDefinition BuildFilter(DomainId appId, DomainId? parentId) + { + var filters = new List> { - var filters = new List> - { - Filter.Eq(x => x.IndexedAppId, appId), - Filter.Eq(x => x.IsDeleted, false) - }; + Filter.Eq(x => x.IndexedAppId, appId), + Filter.Eq(x => x.IsDeleted, false) + }; - if (parentId != null) + if (parentId != null) + { + if (parentId == DomainId.Empty) { - if (parentId == DomainId.Empty) - { - filters.Add( - Filter.Or( - Filter.Exists(x => x.ParentId, false), - Filter.Eq(x => x.ParentId, DomainId.Empty))); - } - else - { - filters.Add(Filter.Eq(x => x.ParentId, parentId.Value)); - } + filters.Add( + Filter.Or( + Filter.Exists(x => x.ParentId, false), + Filter.Eq(x => x.ParentId, DomainId.Empty))); + } + else + { + filters.Add(Filter.Eq(x => x.ParentId, parentId.Value)); } - - return Filter.And(filters); } + + return Filter.And(filters); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs index 0cae620c0a..d3af6212a5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs @@ -14,81 +14,80 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets; + +public sealed partial class MongoAssetFolderRepository : ISnapshotStore, IDeleter { - public sealed partial class MongoAssetFolderRepository : ISnapshotStore, IDeleter + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) { - Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); - } + return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); + } - IAsyncEnumerable> ISnapshotStore.ReadAllAsync( - CancellationToken ct) - { - return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct) - .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version, true)); - } + IAsyncEnumerable> ISnapshotStore.ReadAllAsync( + CancellationToken ct) + { + return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct) + .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version, true)); + } - async Task> ISnapshotStore.ReadAsync(DomainId key, - CancellationToken ct) + async Task> ISnapshotStore.ReadAsync(DomainId key, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/ReadAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/ReadAsync")) - { - var existing = - await Collection.Find(x => x.DocumentId == key) - .FirstOrDefaultAsync(ct); + var existing = + await Collection.Find(x => x.DocumentId == key) + .FirstOrDefaultAsync(ct); - if (existing != null) - { - return new SnapshotResult(existing.DocumentId, existing.ToState(), existing.Version); - } - - return new SnapshotResult(default, null!, EtagVersion.Empty); + if (existing != null) + { + return new SnapshotResult(existing.DocumentId, existing.ToState(), existing.Version); } + + return new SnapshotResult(default, null!, EtagVersion.Empty); } + } - async Task ISnapshotStore.WriteAsync(SnapshotWriteJob job, - CancellationToken ct) + async Task ISnapshotStore.WriteAsync(SnapshotWriteJob job, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteAsync")) - { - var entityJob = job.As(MongoAssetFolderEntity.Create(job)); + var entityJob = job.As(MongoAssetFolderEntity.Create(job)); - await Collection.UpsertVersionedAsync(entityJob, ct); - } + await Collection.UpsertVersionedAsync(entityJob, ct); } + } - async Task ISnapshotStore.WriteManyAsync(IEnumerable> jobs, - CancellationToken ct) + async Task ISnapshotStore.WriteManyAsync(IEnumerable> jobs, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteManyAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteManyAsync")) - { - var updates = jobs.Select(MongoAssetFolderEntity.Create).Select(x => - new ReplaceOneModel( - Filter.Eq(y => y.DocumentId, x.DocumentId), - x) - { - IsUpsert = true - }).ToList(); - - if (updates.Count == 0) + var updates = jobs.Select(MongoAssetFolderEntity.Create).Select(x => + new ReplaceOneModel( + Filter.Eq(y => y.DocumentId, x.DocumentId), + x) { - return; - } + IsUpsert = true + }).ToList(); - await Collection.BulkWriteAsync(updates, BulkUnordered, ct); + if (updates.Count == 0) + { + return; } + + await Collection.BulkWriteAsync(updates, BulkUnordered, ct); } + } - async Task ISnapshotStore.RemoveAsync(DomainId key, - CancellationToken ct) + async Task ISnapshotStore.RemoveAsync(DomainId key, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/RemoveAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/RemoveAsync")) - { - await Collection.DeleteOneAsync(x => x.DocumentId == key, ct); - } + await Collection.DeleteOneAsync(x => x.DocumentId == key, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index d71b7f7feb..7e8e2ee307 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -15,253 +15,252 @@ using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets; + +public sealed partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository { - public sealed partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository + private readonly MongoCountCollection countCollection; + + public MongoAssetRepository(IMongoDatabase database) + : base(database) { - private readonly MongoCountCollection countCollection; + countCollection = new MongoCountCollection(database, CollectionName()); + } - public MongoAssetRepository(IMongoDatabase database) - : base(database) - { - countCollection = new MongoCountCollection(database, CollectionName()); - } + public IMongoCollection GetInternalCollection() + { + return Collection; + } - public IMongoCollection GetInternalCollection() - { - return Collection; - } + protected override string CollectionName() + { + return "States_Assets2"; + } - protected override string CollectionName() + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - return "States_Assets2"; - } + new CreateIndexModel( + Index + .Descending(x => x.LastModified) + .Ascending(x => x.Id) + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.ParentId) + .Ascending(x => x.Tags)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.Slug)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.FileHash)), + new CreateIndexModel( + Index + .Ascending(x => x.Id) + .Ascending(x => x.IsDeleted)) + }, ct); + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Descending(x => x.LastModified) - .Ascending(x => x.Id) - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.ParentId) - .Ascending(x => x.Tags)), - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.Slug)), - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.FileHash)), - new CreateIndexModel( - Index - .Ascending(x => x.Id) - .Ascending(x => x.IsDeleted)) - }, ct); - } + public async IAsyncEnumerable StreamAll(DomainId appId, + [EnumeratorCancellation] CancellationToken ct = default) + { + var find = Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted); - public async IAsyncEnumerable StreamAll(DomainId appId, - [EnumeratorCancellation] CancellationToken ct = default) + using (var cursor = await find.ToCursorAsync(ct)) { - var find = Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted); - - using (var cursor = await find.ToCursorAsync(ct)) + while (await cursor.MoveNextAsync(ct)) { - while (await cursor.MoveNextAsync(ct)) + foreach (var entity in cursor.Current) { - foreach (var entity in cursor.Current) - { - yield return entity; - } + yield return entity; } } } + } - public async Task> QueryAsync(DomainId appId, DomainId? parentId, Q q, - CancellationToken ct = default) + public async Task> QueryAsync(DomainId appId, DomainId? parentId, Q q, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryAsync")) + try { - try + if (q.Ids is { Count: > 0 }) { - if (q.Ids is { Count: > 0 }) - { - var filter = BuildFilter(appId, q.Ids.ToHashSet()); + var filter = BuildFilter(appId, q.Ids.ToHashSet()); - var assetEntities = - await Collection.Find(filter) - .SortByDescending(x => x.LastModified).ThenBy(x => x.Id) - .QueryLimit(q.Query) - .QuerySkip(q.Query) - .ToListRandomAsync(Collection, q.Query.Random, ct); - long assetTotal = assetEntities.Count; + var assetEntities = + await Collection.Find(filter) + .SortByDescending(x => x.LastModified).ThenBy(x => x.Id) + .QueryLimit(q.Query) + .QuerySkip(q.Query) + .ToListRandomAsync(Collection, q.Query.Random, ct); + long assetTotal = assetEntities.Count; - if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) + if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) + { + if (q.NoTotal) { - if (q.NoTotal) - { - assetTotal = -1; - } - else - { - assetTotal = await Collection.Find(filter).CountDocumentsAsync(ct); - } + assetTotal = -1; + } + else + { + assetTotal = await Collection.Find(filter).CountDocumentsAsync(ct); } - - return ResultList.Create(assetTotal, assetEntities.OfType()); } - else - { - var query = q.Query.AdjustToModel(appId); - // Default means that no other filters are applied and we only query by app. - var (filter, isDefault) = query.BuildFilter(appId, parentId); + return ResultList.Create(assetTotal, assetEntities.OfType()); + } + else + { + var query = q.Query.AdjustToModel(appId); + + // Default means that no other filters are applied and we only query by app. + var (filter, isDefault) = query.BuildFilter(appId, parentId); - var assetEntities = - await Collection.Find(filter) - .QueryLimit(query) - .QuerySkip(query) - .QuerySort(query) - .ToListRandomAsync(Collection, query.Random, ct); - long assetTotal = assetEntities.Count; + var assetEntities = + await Collection.Find(filter) + .QueryLimit(query) + .QuerySkip(query) + .QuerySort(query) + .ToListRandomAsync(Collection, query.Random, ct); + long assetTotal = assetEntities.Count; + + if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) + { + var isDefaultQuery = q.Query.Filter == null; - if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) + if (q.NoTotal || (q.NoSlowTotal && !isDefaultQuery)) { - var isDefaultQuery = q.Query.Filter == null; - - if (q.NoTotal || (q.NoSlowTotal && !isDefaultQuery)) - { - assetTotal = -1; - } - else if (isDefaultQuery) - { - // Cache total count by app and asset folder because no other filters are applied (aka default). - var totalKey = $"{appId}_{parentId}"; - - assetTotal = await countCollection.GetOrAddAsync(totalKey, ct => Collection.Find(filter).CountDocumentsAsync(ct), ct); - } - else - { - assetTotal = await Collection.Find(filter).CountDocumentsAsync(ct); - } + assetTotal = -1; } + else if (isDefaultQuery) + { + // Cache total count by app and asset folder because no other filters are applied (aka default). + var totalKey = $"{appId}_{parentId}"; - return ResultList.Create(assetTotal, assetEntities); + assetTotal = await countCollection.GetOrAddAsync(totalKey, ct => Collection.Find(filter).CountDocumentsAsync(ct), ct); + } + else + { + assetTotal = await Collection.Find(filter).CountDocumentsAsync(ct); + } } + + return ResultList.Create(assetTotal, assetEntities); } - catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) - { - throw new DomainException(T.Get("common.resultTooLarge")); - } + } + catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) + { + throw new DomainException(T.Get("common.resultTooLarge")); } } + } - public async Task> QueryIdsAsync(DomainId appId, HashSet ids, - CancellationToken ct = default) + public async Task> QueryIdsAsync(DomainId appId, HashSet ids, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryIdsAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryIdsAsync")) - { - var assetEntities = - await Collection.Find(BuildFilter(appId, ids)).Only(x => x.Id) - .ToListAsync(ct); + var assetEntities = + await Collection.Find(BuildFilter(appId, ids)).Only(x => x.Id) + .ToListAsync(ct); - var field = Field.Of(x => nameof(x.Id)); + var field = Field.Of(x => nameof(x.Id)); - return assetEntities.Select(x => DomainId.Create(x[field].AsString)).ToList(); - } + return assetEntities.Select(x => DomainId.Create(x[field].AsString)).ToList(); } + } - public async Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, - CancellationToken ct = default) + public async Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryChildIdsAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryChildIdsAsync")) - { - var assetEntities = - await Collection.Find(BuildFilter(appId, parentId)).Only(x => x.Id) - .ToListAsync(ct); + var assetEntities = + await Collection.Find(BuildFilter(appId, parentId)).Only(x => x.Id) + .ToListAsync(ct); - var field = Field.Of(x => nameof(x.Id)); + var field = Field.Of(x => nameof(x.Id)); - return assetEntities.Select(x => DomainId.Create(x[field].AsString)).ToList(); - } + return assetEntities.Select(x => DomainId.Create(x[field].AsString)).ToList(); } + } - public async Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, - CancellationToken ct = default) + public async Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetByHashAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetByHashAsync")) - { - var assetEntity = - await Collection.Find(x => x.IndexedAppId == appId && x.FileHash == hash && !x.IsDeleted && x.FileSize == fileSize && x.FileName == fileName) - .FirstOrDefaultAsync(ct); + var assetEntity = + await Collection.Find(x => x.IndexedAppId == appId && x.FileHash == hash && !x.IsDeleted && x.FileSize == fileSize && x.FileName == fileName) + .FirstOrDefaultAsync(ct); - return assetEntity; - } + return assetEntity; } + } - public async Task FindAssetBySlugAsync(DomainId appId, string slug, - CancellationToken ct = default) + public async Task FindAssetBySlugAsync(DomainId appId, string slug, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetBySlugAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetBySlugAsync")) - { - var assetEntity = - await Collection.Find(x => x.IndexedAppId == appId && x.Slug == slug && !x.IsDeleted) - .FirstOrDefaultAsync(ct); + var assetEntity = + await Collection.Find(x => x.IndexedAppId == appId && x.Slug == slug && !x.IsDeleted) + .FirstOrDefaultAsync(ct); - return assetEntity; - } + return assetEntity; } + } - public async Task FindAssetAsync(DomainId appId, DomainId id, - CancellationToken ct = default) + public async Task FindAssetAsync(DomainId appId, DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetAsync")) - { - var documentId = DomainId.Combine(appId, id); + var documentId = DomainId.Combine(appId, id); - var assetEntity = - await Collection.Find(x => x.DocumentId == documentId && !x.IsDeleted) - .FirstOrDefaultAsync(ct); + var assetEntity = + await Collection.Find(x => x.DocumentId == documentId && !x.IsDeleted) + .FirstOrDefaultAsync(ct); - return assetEntity; - } + return assetEntity; } + } - public async Task FindAssetAsync(DomainId id, - CancellationToken ct = default) + public async Task FindAssetAsync(DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetAsync")) - { - var assetEntity = - await Collection.Find(x => x.Id == id && !x.IsDeleted) - .FirstOrDefaultAsync(ct); + var assetEntity = + await Collection.Find(x => x.Id == id && !x.IsDeleted) + .FirstOrDefaultAsync(ct); - return assetEntity; - } + return assetEntity; } + } - private static FilterDefinition BuildFilter(DomainId appId, HashSet ids) - { - var documentIds = ids.Select(x => DomainId.Combine(appId, x)); + private static FilterDefinition BuildFilter(DomainId appId, HashSet ids) + { + var documentIds = ids.Select(x => DomainId.Combine(appId, x)); - return Filter.And( - Filter.In(x => x.DocumentId, documentIds), - Filter.Ne(x => x.IsDeleted, true)); - } + return Filter.And( + Filter.In(x => x.DocumentId, documentIds), + Filter.Ne(x => x.IsDeleted, true)); + } - private static FilterDefinition BuildFilter(DomainId appId, DomainId parentId) - { - return Filter.And( - Filter.Gt(x => x.LastModified, default), - Filter.Gt(x => x.Id, DomainId.Create(string.Empty)), - Filter.Eq(x => x.IndexedAppId, appId), - Filter.Ne(x => x.IsDeleted, true), - Filter.Eq(x => x.ParentId, parentId)); - } + private static FilterDefinition BuildFilter(DomainId appId, DomainId parentId) + { + return Filter.And( + Filter.Gt(x => x.LastModified, default), + Filter.Gt(x => x.Id, DomainId.Create(string.Empty)), + Filter.Eq(x => x.IndexedAppId, appId), + Filter.Ne(x => x.IsDeleted, true), + Filter.Eq(x => x.ParentId, parentId)); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index 83f84279f4..a5809d3a14 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -14,81 +14,80 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets; + +public sealed partial class MongoAssetRepository : ISnapshotStore, IDeleter { - public sealed partial class MongoAssetRepository : ISnapshotStore, IDeleter + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) { - Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); - } + return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); + } - IAsyncEnumerable> ISnapshotStore.ReadAllAsync( - CancellationToken ct) - { - return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct) - .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version)); - } + IAsyncEnumerable> ISnapshotStore.ReadAllAsync( + CancellationToken ct) + { + return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct) + .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version)); + } - async Task> ISnapshotStore.ReadAsync(DomainId key, - CancellationToken ct) + async Task> ISnapshotStore.ReadAsync(DomainId key, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/ReadAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/ReadAsync")) - { - var existing = - await Collection.Find(x => x.DocumentId == key) - .FirstOrDefaultAsync(ct); + var existing = + await Collection.Find(x => x.DocumentId == key) + .FirstOrDefaultAsync(ct); - if (existing != null) - { - return new SnapshotResult(existing.DocumentId, existing.ToState(), existing.Version); - } - - return new SnapshotResult(default, null!, EtagVersion.Empty); + if (existing != null) + { + return new SnapshotResult(existing.DocumentId, existing.ToState(), existing.Version); } + + return new SnapshotResult(default, null!, EtagVersion.Empty); } + } - async Task ISnapshotStore.WriteAsync(SnapshotWriteJob job, - CancellationToken ct) + async Task ISnapshotStore.WriteAsync(SnapshotWriteJob job, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteAsync")) - { - var entityJob = job.As(MongoAssetEntity.Create(job)); + var entityJob = job.As(MongoAssetEntity.Create(job)); - await Collection.UpsertVersionedAsync(entityJob, ct); - } + await Collection.UpsertVersionedAsync(entityJob, ct); } + } - async Task ISnapshotStore.WriteManyAsync(IEnumerable> jobs, - CancellationToken ct) + async Task ISnapshotStore.WriteManyAsync(IEnumerable> jobs, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteManyAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteManyAsync")) - { - var updates = jobs.Select(MongoAssetEntity.Create).Select(x => - new ReplaceOneModel( - Filter.Eq(y => y.DocumentId, x.DocumentId), - x) - { - IsUpsert = true - }).ToList(); - - if (updates.Count == 0) + var updates = jobs.Select(MongoAssetEntity.Create).Select(x => + new ReplaceOneModel( + Filter.Eq(y => y.DocumentId, x.DocumentId), + x) { - return; - } + IsUpsert = true + }).ToList(); - await Collection.BulkWriteAsync(updates, BulkUnordered, ct); + if (updates.Count == 0) + { + return; } + + await Collection.BulkWriteAsync(updates, BulkUnordered, ct); } + } - async Task ISnapshotStore.RemoveAsync(DomainId key, - CancellationToken ct) + async Task ISnapshotStore.RemoveAsync(DomainId key, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoAssetRepository/RemoveAsync")) { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/RemoveAsync")) - { - await Collection.DeleteOneAsync(x => x.DocumentId == key, null, ct); - } + await Collection.DeleteOneAsync(x => x.DocumentId == key, null, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index f506ca20be..659f1aa6ff 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -10,84 +10,83 @@ using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; + +public static class FindExtensions { - public static class FindExtensions + private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + + public static ClrQuery AdjustToModel(this ClrQuery query, DomainId appId) { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + if (query.Filter != null) + { + query.Filter = FirstPascalPathConverter.Transform(query.Filter); + } - public static ClrQuery AdjustToModel(this ClrQuery query, DomainId appId) + if (query.Filter != null) { - if (query.Filter != null) - { - query.Filter = FirstPascalPathConverter.Transform(query.Filter); - } + query.Filter = AdaptIdVisitor.AdaptFilter(query.Filter, appId); + } - if (query.Filter != null) - { - query.Filter = AdaptIdVisitor.AdaptFilter(query.Filter, appId); - } + if (query.Sort != null) + { + query.Sort = query.Sort.Select(x => new SortNode(x.Path.ToFirstPascalCase(), x.Order)).ToList(); + } - if (query.Sort != null) - { - query.Sort = query.Sort.Select(x => new SortNode(x.Path.ToFirstPascalCase(), x.Order)).ToList(); - } + return query; + } - return query; - } + public static (FilterDefinition, bool) BuildFilter(this ClrQuery query, DomainId appId, DomainId? parentId) + { + var filters = new List> + { + Filter.Ne(x => x.LastModified, default), + Filter.Ne(x => x.Id, default), + Filter.Eq(x => x.IndexedAppId, appId) + }; + + var isDefault = false; - public static (FilterDefinition, bool) BuildFilter(this ClrQuery query, DomainId appId, DomainId? parentId) + if (!query.HasFilterField("IsDeleted")) { - var filters = new List> - { - Filter.Ne(x => x.LastModified, default), - Filter.Ne(x => x.Id, default), - Filter.Eq(x => x.IndexedAppId, appId) - }; + filters.Add(Filter.Eq(x => x.IsDeleted, false)); - var isDefault = false; + isDefault = true; + } - if (!query.HasFilterField("IsDeleted")) + if (parentId != null) + { + if (parentId == DomainId.Empty) { - filters.Add(Filter.Eq(x => x.IsDeleted, false)); - - isDefault = true; + filters.Add( + Filter.Or( + Filter.Exists(x => x.ParentId, false), + Filter.Eq(x => x.ParentId, DomainId.Empty))); } - - if (parentId != null) + else { - if (parentId == DomainId.Empty) - { - filters.Add( - Filter.Or( - Filter.Exists(x => x.ParentId, false), - Filter.Eq(x => x.ParentId, DomainId.Empty))); - } - else - { - filters.Add(Filter.Eq(x => x.ParentId, parentId.Value)); - } - - isDefault = false; + filters.Add(Filter.Eq(x => x.ParentId, parentId.Value)); } - var (filter, last) = query.BuildFilter(false); + isDefault = false; + } - if (filter != null) + var (filter, last) = query.BuildFilter(false); + + if (filter != null) + { + isDefault = false; + + if (last) { - isDefault = false; - - if (last) - { - filters.Add(filter); - } - else - { - filters.Insert(0, filter); - } + filters.Add(filter); + } + else + { + filters.Insert(0, filter); } - - return (Filter.And(filters), isDefault); } + + return (Filter.And(filters), isDefault); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs index 2fb1c90ef8..99b9ca2f67 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs @@ -8,24 +8,23 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; + +public sealed class FirstPascalPathConverter : TransformVisitor { - public sealed class FirstPascalPathConverter : TransformVisitor - { - private static readonly FirstPascalPathConverter Instance = new FirstPascalPathConverter(); + private static readonly FirstPascalPathConverter Instance = new FirstPascalPathConverter(); - private FirstPascalPathConverter() - { - } + private FirstPascalPathConverter() + { + } - public static FilterNode? Transform(FilterNode node) - { - return node.Accept(Instance, None.Value); - } + public static FilterNode? Transform(FilterNode node) + { + return node.Accept(Instance, None.Value); + } - public override FilterNode? Visit(CompareFilter nodeIn, None args) - { - return nodeIn with { Path = nodeIn.Path.ToFirstPascalCase() }; - } + public override FilterNode? Visit(CompareFilter nodeIn, None args) + { + return nodeIn with { Path = nodeIn.Path.ToFirstPascalCase() }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs index 891e36db68..96ff823bb6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs @@ -8,17 +8,16 @@ using Squidex.Infrastructure.Queries; using Squidex.Text; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; + +public static class FirstPascalPathExtension { - public static class FirstPascalPathExtension + public static PropertyPath ToFirstPascalCase(this PropertyPath path) { - public static PropertyPath ToFirstPascalCase(this PropertyPath path) - { - var result = path.ToList(); + var result = path.ToList(); - result[0] = result[0].ToPascalCase(); + result[0] = result[0].ToPascalCase(); - return new PropertyPath(result); - } + return new PropertyPath(result); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index e98f228156..8bf3c7f794 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -20,290 +20,289 @@ #pragma warning disable IDE0060 // Remove unused parameter -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents; + +public sealed class MongoContentCollection : MongoRepositoryBase { - public sealed class MongoContentCollection : MongoRepositoryBase + private readonly QueryAsStream queryAsStream; + private readonly QueryById queryBdId; + private readonly QueryByIds queryByIds; + private readonly QueryByQuery queryByQuery; + private readonly QueryInDedicatedCollection? queryInDedicatedCollection; + private readonly QueryReferences queryReferences; + private readonly QueryReferrers queryReferrers; + private readonly QueryScheduled queryScheduled; + private readonly ReadPreference readPreference; + private readonly string name; + + public MongoContentCollection(string name, IMongoDatabase database, ReadPreference readPreference, + bool dedicatedCollections) + : base(database) { - private readonly QueryAsStream queryAsStream; - private readonly QueryById queryBdId; - private readonly QueryByIds queryByIds; - private readonly QueryByQuery queryByQuery; - private readonly QueryInDedicatedCollection? queryInDedicatedCollection; - private readonly QueryReferences queryReferences; - private readonly QueryReferrers queryReferrers; - private readonly QueryScheduled queryScheduled; - private readonly ReadPreference readPreference; - private readonly string name; - - public MongoContentCollection(string name, IMongoDatabase database, ReadPreference readPreference, - bool dedicatedCollections) - : base(database) - { - this.name = name; - - queryAsStream = new QueryAsStream(); - queryBdId = new QueryById(); - queryByIds = new QueryByIds(); - queryReferences = new QueryReferences(queryByIds); - queryReferrers = new QueryReferrers(); - queryScheduled = new QueryScheduled(); - queryByQuery = new QueryByQuery(new MongoCountCollection(database, name)); + this.name = name; - if (dedicatedCollections) - { - queryInDedicatedCollection = - new QueryInDedicatedCollection( - database.Client, - database.DatabaseNamespace.DatabaseName, - name); - } + queryAsStream = new QueryAsStream(); + queryBdId = new QueryById(); + queryByIds = new QueryByIds(); + queryReferences = new QueryReferences(queryByIds); + queryReferrers = new QueryReferrers(); + queryScheduled = new QueryScheduled(); + queryByQuery = new QueryByQuery(new MongoCountCollection(database, name)); - this.readPreference = readPreference; - } - - protected override string CollectionName() + if (dedicatedCollections) { - return name; + queryInDedicatedCollection = + new QueryInDedicatedCollection( + database.Client, + database.DatabaseNamespace.DatabaseName, + name); } - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings - { - ReadPreference = readPreference - }; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) - { - var operations = new OperationBase[] - { - queryAsStream, - queryBdId, - queryByIds, - queryByQuery, - queryReferences, - queryReferrers, - queryScheduled - }; - - foreach (var operation in operations) - { - operation.Setup(collection); - } + this.readPreference = readPreference; + } - return collection.Indexes.CreateManyAsync(operations.SelectMany(x => x.CreateIndexes()), ct); - } + protected override string CollectionName() + { + return name; + } - public Task ResetScheduledAsync(DomainId documentId, - CancellationToken ct) + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { - return Collection.UpdateOneAsync(x => x.DocumentId == documentId, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt), cancellationToken: ct); - } + ReadPreference = readPreference + }; + } - public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + var operations = new OperationBase[] { - return queryAsStream.StreamAll(appId, schemaIds, ct); - } - - public IAsyncEnumerable QueryScheduledWithoutDataAsync(Instant now, - CancellationToken ct) + queryAsStream, + queryBdId, + queryByIds, + queryByQuery, + queryReferences, + queryReferrers, + queryScheduled + }; + + foreach (var operation in operations) { - return queryScheduled.QueryAsync(now, ct); + operation.Setup(collection); } - public async Task DeleteAppAsync(DomainId appId, - CancellationToken ct) + return collection.Indexes.CreateManyAsync(operations.SelectMany(x => x.CreateIndexes()), ct); + } + + public Task ResetScheduledAsync(DomainId documentId, + CancellationToken ct) + { + return Collection.UpdateOneAsync(x => x.DocumentId == documentId, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt), cancellationToken: ct); + } + + public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, + CancellationToken ct) + { + return queryAsStream.StreamAll(appId, schemaIds, ct); + } + + public IAsyncEnumerable QueryScheduledWithoutDataAsync(Instant now, + CancellationToken ct) + { + return queryScheduled.QueryAsync(now, ct); + } + + public async Task DeleteAppAsync(DomainId appId, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentCollection/DeleteAppAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentCollection/DeleteAppAsync")) - { - await Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, appId), ct); - } + await Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, appId), ct); } + } - public async Task> QueryAsync(IAppEntity app, List schemas, Q q, - CancellationToken ct) + public async Task> QueryAsync(IAppEntity app, List schemas, Q q, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryAsync")) + try { - try + if (q.Ids is { Count: > 0 } && schemas.Count > 0) { - if (q.Ids is { Count: > 0 } && schemas.Count > 0) - { - return await queryByIds.QueryAsync(app, schemas, q, ct); - } - - if (q.ScheduledFrom != null && q.ScheduledTo != null && schemas.Count > 0) - { - return await queryScheduled.QueryAsync(app, schemas, q, ct); - } - - if (q.Referencing != default && schemas.Count > 0) - { - return await queryReferences.QueryAsync(app, schemas, q, ct); - } - - if (q.Reference != default && schemas.Count > 0) - { - return await queryByQuery.QueryAsync(app, schemas, q, ct); - } + return await queryByIds.QueryAsync(app, schemas, q, ct); + } - return ResultList.Empty(); + if (q.ScheduledFrom != null && q.ScheduledTo != null && schemas.Count > 0) + { + return await queryScheduled.QueryAsync(app, schemas, q, ct); } - catch (MongoCommandException ex) when (ex.Code == 96) + + if (q.Referencing != default && schemas.Count > 0) { - throw new DomainException(T.Get("common.resultTooLarge")); + return await queryReferences.QueryAsync(app, schemas, q, ct); } - catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) + + if (q.Reference != default && schemas.Count > 0) { - throw new DomainException(T.Get("common.resultTooLarge")); + return await queryByQuery.QueryAsync(app, schemas, q, ct); } + + return ResultList.Empty(); + } + catch (MongoCommandException ex) when (ex.Code == 96) + { + throw new DomainException(T.Get("common.resultTooLarge")); + } + catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) + { + throw new DomainException(T.Get("common.resultTooLarge")); } } + } - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, - CancellationToken ct) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryAsync")) + try { - try + if (q.Ids is { Count: > 0 }) { - if (q.Ids is { Count: > 0 }) - { - return await queryByIds.QueryAsync(app, new List { schema }, q, ct); - } + return await queryByIds.QueryAsync(app, new List { schema }, q, ct); + } - if (q.ScheduledFrom != null && q.ScheduledTo != null) - { - return await queryScheduled.QueryAsync(app, new List { schema }, q, ct); - } + if (q.ScheduledFrom != null && q.ScheduledTo != null) + { + return await queryScheduled.QueryAsync(app, new List { schema }, q, ct); + } - if (q.Referencing == default) + if (q.Referencing == default) + { + if (queryInDedicatedCollection != null) { - if (queryInDedicatedCollection != null) - { - return await queryInDedicatedCollection.QueryAsync(schema, q, ct); - } - - return await queryByQuery.QueryAsync(schema, q, ct); + return await queryInDedicatedCollection.QueryAsync(schema, q, ct); } - return ResultList.Empty(); - } - catch (MongoCommandException ex) when (ex.Code == 96) - { - throw new DomainException(T.Get("common.resultTooLarge")); + return await queryByQuery.QueryAsync(schema, q, ct); } - catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) - { - throw new DomainException(T.Get("common.resultTooLarge")); - } - } - } - public async Task FindContentAsync(ISchemaEntity schema, DomainId id, - CancellationToken ct) - { - using (Telemetry.Activities.StartActivity("MongoContentCollection/FindContentAsync")) + return ResultList.Empty(); + } + catch (MongoCommandException ex) when (ex.Code == 96) { - return await queryBdId.QueryAsync(schema, id, ct); + throw new DomainException(T.Get("common.resultTooLarge")); } - } - - public async Task> QueryIdsAsync(DomainId appId, HashSet ids, - CancellationToken ct) - { - using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryIdsAsync")) + catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) { - return await queryByIds.QueryIdsAsync(appId, ids, ct); + throw new DomainException(T.Get("common.resultTooLarge")); } } + } - public async Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, - CancellationToken ct) + public async Task FindContentAsync(ISchemaEntity schema, DomainId id, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentCollection/FindContentAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryIdsAsync")) - { - return await queryByQuery.QueryIdsAsync(appId, schemaId, filterNode, ct); - } + return await queryBdId.QueryAsync(schema, id, ct); } + } - public async Task HasReferrersAsync(DomainId appId, DomainId contentId, - CancellationToken ct) + public async Task> QueryIdsAsync(DomainId appId, HashSet ids, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryIdsAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentCollection/HasReferrersAsync")) - { - return await queryReferrers.CheckExistsAsync(appId, contentId, ct); - } + return await queryByIds.QueryIdsAsync(appId, ids, ct); } + } - public Task FindAsync(DomainId documentId, - CancellationToken ct = default) + public async Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryIdsAsync")) { - return Collection.Find(x => x.DocumentId == documentId).FirstOrDefaultAsync(ct); + return await queryByQuery.QueryIdsAsync(appId, schemaId, filterNode, ct); } + } - public IAsyncEnumerable StreamAll( - CancellationToken ct) + public async Task HasReferrersAsync(DomainId appId, DomainId contentId, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentCollection/HasReferrersAsync")) { - return Collection.Find(FindAll).ToAsyncEnumerable(ct); + return await queryReferrers.CheckExistsAsync(appId, contentId, ct); } + } - public async Task UpsertAsync(SnapshotWriteJob job, - CancellationToken ct = default) - { - if (queryInDedicatedCollection != null) - { - await queryInDedicatedCollection.UpsertAsync(job, ct); - } + public Task FindAsync(DomainId documentId, + CancellationToken ct = default) + { + return Collection.Find(x => x.DocumentId == documentId).FirstOrDefaultAsync(ct); + } - await Collection.ReplaceOneAsync(Filter.Eq(x => x.DocumentId, job.Key), job.Value, UpsertReplace, ct); - } + public IAsyncEnumerable StreamAll( + CancellationToken ct) + { + return Collection.Find(FindAll).ToAsyncEnumerable(ct); + } - public async Task UpsertVersionedAsync(IClientSessionHandle session, SnapshotWriteJob job, - CancellationToken ct = default) + public async Task UpsertAsync(SnapshotWriteJob job, + CancellationToken ct = default) + { + if (queryInDedicatedCollection != null) { - if (queryInDedicatedCollection != null) - { - await queryInDedicatedCollection.UpsertVersionedAsync(session, job, ct); - } - - await Collection.UpsertVersionedAsync(session, job, ct); + await queryInDedicatedCollection.UpsertAsync(job, ct); } - public async Task RemoveAsync(DomainId key, - CancellationToken ct = default) - { - var previous = await Collection.FindOneAndDeleteAsync(x => x.DocumentId == key, null, ct); + await Collection.ReplaceOneAsync(Filter.Eq(x => x.DocumentId, job.Key), job.Value, UpsertReplace, ct); + } - if (queryInDedicatedCollection != null && previous != null) - { - await queryInDedicatedCollection.RemoveAsync(previous, ct); - } + public async Task UpsertVersionedAsync(IClientSessionHandle session, SnapshotWriteJob job, + CancellationToken ct = default) + { + if (queryInDedicatedCollection != null) + { + await queryInDedicatedCollection.UpsertVersionedAsync(session, job, ct); } - public async Task RemoveAsync(IClientSessionHandle session, DomainId key, - CancellationToken ct = default) - { - var previous = await Collection.FindOneAndDeleteAsync(session, x => x.DocumentId == key, null, ct); + await Collection.UpsertVersionedAsync(session, job, ct); + } - if (queryInDedicatedCollection != null && previous != null) - { - await queryInDedicatedCollection.RemoveAsync(session, previous, ct); - } + public async Task RemoveAsync(DomainId key, + CancellationToken ct = default) + { + var previous = await Collection.FindOneAndDeleteAsync(x => x.DocumentId == key, null, ct); + + if (queryInDedicatedCollection != null && previous != null) + { + await queryInDedicatedCollection.RemoveAsync(previous, ct); } + } + + public async Task RemoveAsync(IClientSessionHandle session, DomainId key, + CancellationToken ct = default) + { + var previous = await Collection.FindOneAndDeleteAsync(session, x => x.DocumentId == key, null, ct); - public async Task AddCollectionsAsync(MongoContentEntity entity, Action, MongoContentEntity> add, - CancellationToken ct) + if (queryInDedicatedCollection != null && previous != null) { - if (queryInDedicatedCollection != null) - { - add(await queryInDedicatedCollection.GetCollectionAsync(entity.AppId.Id, entity.SchemaId.Id), entity); - } + await queryInDedicatedCollection.RemoveAsync(session, previous, ct); + } + } - add(Collection, entity); + public async Task AddCollectionsAsync(MongoContentEntity entity, Action, MongoContentEntity> add, + CancellationToken ct) + { + if (queryInDedicatedCollection != null) + { + add(await queryInDedicatedCollection.GetCollectionAsync(entity.AppId.Id, entity.SchemaId.Id), entity); } + + add(Collection, entity); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index a0b79ffa63..070d7088df 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -16,167 +16,166 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents; + +public sealed class MongoContentEntity : IContentEntity, IVersionedEntity { - public sealed class MongoContentEntity : IContentEntity, IVersionedEntity - { - [BsonId] - [BsonElement("_id")] - public DomainId DocumentId { get; set; } + [BsonId] + [BsonElement("_id")] + public DomainId DocumentId { get; set; } + + [BsonRequired] + [BsonElement("_ai")] + public DomainId IndexedAppId { get; set; } + + [BsonRequired] + [BsonElement("_si")] + public DomainId IndexedSchemaId { get; set; } - [BsonRequired] - [BsonElement("_ai")] - public DomainId IndexedAppId { get; set; } + [BsonRequired] + [BsonElement("ai")] + public NamedId AppId { get; set; } - [BsonRequired] - [BsonElement("_si")] - public DomainId IndexedSchemaId { get; set; } + [BsonRequired] + [BsonElement("si")] + public NamedId SchemaId { get; set; } - [BsonRequired] - [BsonElement("ai")] - public NamedId AppId { get; set; } + [BsonRequired] + [BsonElement("rf")] + public HashSet? ReferencedIds { get; set; } - [BsonRequired] - [BsonElement("si")] - public NamedId SchemaId { get; set; } + [BsonRequired] + [BsonElement("id")] + public DomainId Id { get; set; } - [BsonRequired] - [BsonElement("rf")] - public HashSet? ReferencedIds { get; set; } + [BsonRequired] + [BsonElement("ss")] + public Status Status { get; set; } - [BsonRequired] - [BsonElement("id")] - public DomainId Id { get; set; } + [BsonIgnoreIfNull] + [BsonElement("ns")] + public Status? NewStatus { get; set; } - [BsonRequired] - [BsonElement("ss")] - public Status Status { get; set; } + [BsonIgnoreIfNull] + [BsonElement("do")] + public ContentData Data { get; set; } - [BsonIgnoreIfNull] - [BsonElement("ns")] - public Status? NewStatus { get; set; } + [BsonIgnoreIfNull] + [BsonElement("dd")] + public ContentData? DraftData { get; set; } - [BsonIgnoreIfNull] - [BsonElement("do")] - public ContentData Data { get; set; } + [BsonIgnoreIfNull] + [BsonElement("sa")] + public Instant? ScheduledAt { get; set; } - [BsonIgnoreIfNull] - [BsonElement("dd")] - public ContentData? DraftData { get; set; } + [BsonRequired] + [BsonElement("ct")] + public Instant Created { get; set; } - [BsonIgnoreIfNull] - [BsonElement("sa")] - public Instant? ScheduledAt { get; set; } + [BsonRequired] + [BsonElement("mt")] + public Instant LastModified { get; set; } - [BsonRequired] - [BsonElement("ct")] - public Instant Created { get; set; } + [BsonRequired] + [BsonElement("vs")] + public long Version { get; set; } - [BsonRequired] - [BsonElement("mt")] - public Instant LastModified { get; set; } + [BsonIgnoreIfDefault] + [BsonElement("dl")] + public bool IsDeleted { get; set; } - [BsonRequired] - [BsonElement("vs")] - public long Version { get; set; } + [BsonIgnoreIfDefault] + [BsonElement("is")] + public bool IsSnapshot { get; set; } - [BsonIgnoreIfDefault] - [BsonElement("dl")] - public bool IsDeleted { get; set; } + [BsonIgnoreIfDefault] + [BsonElement("sj")] + public ScheduleJob? ScheduleJob { get; set; } - [BsonIgnoreIfDefault] - [BsonElement("is")] - public bool IsSnapshot { get; set; } + [BsonRequired] + [BsonElement("cb")] + public RefToken CreatedBy { get; set; } - [BsonIgnoreIfDefault] - [BsonElement("sj")] - public ScheduleJob? ScheduleJob { get; set; } + [BsonRequired] + [BsonElement("mb")] + public RefToken LastModifiedBy { get; set; } - [BsonRequired] - [BsonElement("cb")] - public RefToken CreatedBy { get; set; } + [BsonIgnoreIfNull] + [BsonElement("ts")] + public TranslationStatus? TranslationStatus { get; set; } - [BsonRequired] - [BsonElement("mb")] - public RefToken LastModifiedBy { get; set; } + public DomainId UniqueId + { + get => DocumentId; + } - [BsonIgnoreIfNull] - [BsonElement("ts")] - public TranslationStatus? TranslationStatus { get; set; } + public ContentDomainObject.State ToState() + { + var state = SimpleMapper.Map(this, new ContentDomainObject.State()); - public DomainId UniqueId + if (DraftData != null && NewStatus.HasValue) { - get => DocumentId; + state.NewVersion = new ContentVersion(NewStatus.Value, Data); + state.CurrentVersion = new ContentVersion(Status, DraftData); } - - public ContentDomainObject.State ToState() + else { - var state = SimpleMapper.Map(this, new ContentDomainObject.State()); - - if (DraftData != null && NewStatus.HasValue) - { - state.NewVersion = new ContentVersion(NewStatus.Value, Data); - state.CurrentVersion = new ContentVersion(Status, DraftData); - } - else - { - state.NewVersion = null; - state.CurrentVersion = new ContentVersion(Status, Data); - } - - return state; + state.NewVersion = null; + state.CurrentVersion = new ContentVersion(Status, Data); } - public static async Task CreatePublishedAsync(SnapshotWriteJob job, IAppProvider appProvider) - { - var entity = await CreateContentAsync(job.Value.CurrentVersion.Data, job, appProvider); + return state; + } - entity.ScheduledAt = null; - entity.ScheduleJob = null; - entity.NewStatus = null; + public static async Task CreatePublishedAsync(SnapshotWriteJob job, IAppProvider appProvider) + { + var entity = await CreateContentAsync(job.Value.CurrentVersion.Data, job, appProvider); - return entity; - } + entity.ScheduledAt = null; + entity.ScheduleJob = null; + entity.NewStatus = null; - public static async Task CreateCompleteAsync(SnapshotWriteJob job, IAppProvider appProvider) - { - var entity = await CreateContentAsync(job.Value.Data, job, appProvider); + return entity; + } - entity.ScheduledAt = job.Value.ScheduleJob?.DueTime; - entity.ScheduleJob = job.Value.ScheduleJob; - entity.NewStatus = job.Value.NewStatus; - entity.DraftData = job.Value.NewVersion != null ? job.Value.CurrentVersion.Data : null; - entity.IsSnapshot = true; + public static async Task CreateCompleteAsync(SnapshotWriteJob job, IAppProvider appProvider) + { + var entity = await CreateContentAsync(job.Value.Data, job, appProvider); - return entity; - } + entity.ScheduledAt = job.Value.ScheduleJob?.DueTime; + entity.ScheduleJob = job.Value.ScheduleJob; + entity.NewStatus = job.Value.NewStatus; + entity.DraftData = job.Value.NewVersion != null ? job.Value.CurrentVersion.Data : null; + entity.IsSnapshot = true; - private static async Task CreateContentAsync(ContentData data, SnapshotWriteJob job, IAppProvider appProvider) - { - var entity = SimpleMapper.Map(job.Value, new MongoContentEntity()); + return entity; + } - entity.Data = data; - entity.DocumentId = job.Value.UniqueId; - entity.IndexedAppId = job.Value.AppId.Id; - entity.IndexedSchemaId = job.Value.SchemaId.Id; - entity.ReferencedIds ??= new HashSet(); - entity.Version = job.NewVersion; + private static async Task CreateContentAsync(ContentData data, SnapshotWriteJob job, IAppProvider appProvider) + { + var entity = SimpleMapper.Map(job.Value, new MongoContentEntity()); - var (app, schema) = await appProvider.GetAppWithSchemaAsync(job.Value.AppId.Id, job.Value.SchemaId.Id, true); + entity.Data = data; + entity.DocumentId = job.Value.UniqueId; + entity.IndexedAppId = job.Value.AppId.Id; + entity.IndexedSchemaId = job.Value.SchemaId.Id; + entity.ReferencedIds ??= new HashSet(); + entity.Version = job.NewVersion; - if (schema?.SchemaDef != null && app != null) - { - if (data.CanHaveReference()) - { - var components = await appProvider.GetComponentsAsync(schema); + var (app, schema) = await appProvider.GetAppWithSchemaAsync(job.Value.AppId.Id, job.Value.SchemaId.Id, true); - entity.Data.AddReferencedIds(schema.SchemaDef, entity.ReferencedIds, components); - } + if (schema?.SchemaDef != null && app != null) + { + if (data.CanHaveReference()) + { + var components = await appProvider.GetComponentsAsync(schema); - entity.TranslationStatus = TranslationStatus.Create(data, schema.SchemaDef, app.Languages); + entity.Data.AddReferencedIds(schema.SchemaDef, entity.ReferencedIds, components); } - return entity; + entity.TranslationStatus = TranslationStatus.Create(data, schema.SchemaDef, app.Languages); } + + return entity; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index a2c43b8b12..bfee84bfe4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -20,141 +20,140 @@ using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents; + +public partial class MongoContentRepository : MongoBase, IContentRepository, IInitializable { - public partial class MongoContentRepository : MongoBase, IContentRepository, IInitializable + private readonly MongoContentCollection collectionComplete; + private readonly MongoContentCollection collectionPublished; + private readonly ContentOptions options; + private readonly IMongoDatabase database; + private readonly IAppProvider appProvider; + + public bool CanUseTransactions { get; private set; } + + static MongoContentRepository() { - private readonly MongoContentCollection collectionComplete; - private readonly MongoContentCollection collectionPublished; - private readonly ContentOptions options; - private readonly IMongoDatabase database; - private readonly IAppProvider appProvider; + BsonEscapedDictionarySerializer.Register(); + BsonEscapedDictionarySerializer.Register(); + BsonEscapedDictionarySerializer.Register(); + BsonStringSerializer.Register(); + } - public bool CanUseTransactions { get; private set; } + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, + IOptions options) + { + this.appProvider = appProvider; + this.database = database; + this.options = options.Value; - static MongoContentRepository() - { - BsonEscapedDictionarySerializer.Register(); - BsonEscapedDictionarySerializer.Register(); - BsonEscapedDictionarySerializer.Register(); - BsonStringSerializer.Register(); - } + collectionComplete = + new MongoContentCollection("States_Contents_All3", database, + ReadPreference.Primary, options.Value.OptimizeForSelfHosting); - public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, - IOptions options) - { - this.appProvider = appProvider; - this.database = database; - this.options = options.Value; + collectionPublished = + new MongoContentCollection("States_Contents_Published3", database, + ReadPreference.Secondary, options.Value.OptimizeForSelfHosting); + } - collectionComplete = - new MongoContentCollection("States_Contents_All3", database, - ReadPreference.Primary, options.Value.OptimizeForSelfHosting); + public async Task InitializeAsync( + CancellationToken ct) + { + await collectionComplete.InitializeAsync(ct); + await collectionPublished.InitializeAsync(ct); - collectionPublished = - new MongoContentCollection("States_Contents_Published3", database, - ReadPreference.Secondary, options.Value.OptimizeForSelfHosting); - } + var clusterVersion = await database.GetMajorVersionAsync(ct); + var clusteredAsReplica = database.Client.Cluster.Description.Type == ClusterType.ReplicaSet; - public async Task InitializeAsync( - CancellationToken ct) - { - await collectionComplete.InitializeAsync(ct); - await collectionPublished.InitializeAsync(ct); + CanUseTransactions = clusteredAsReplica && clusterVersion >= 4 && options.UseTransactions; + } - var clusterVersion = await database.GetMajorVersionAsync(ct); - var clusteredAsReplica = database.Client.Cluster.Description.Type == ClusterType.ReplicaSet; + public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, + CancellationToken ct = default) + { + return collectionComplete.StreamAll(appId, schemaIds, ct); + } - CanUseTransactions = clusteredAsReplica && clusterVersion >= 4 && options.UseTransactions; - } + public IAsyncEnumerable QueryScheduledWithoutDataAsync(Instant now, + CancellationToken ct = default) + { + return collectionComplete.QueryScheduledWithoutDataAsync(now, ct); + } - public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, - CancellationToken ct = default) + public Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope, + CancellationToken ct = default) + { + if (scope == SearchScope.All) { - return collectionComplete.StreamAll(appId, schemaIds, ct); + return collectionComplete.QueryAsync(app, schemas, q, ct); } - - public IAsyncEnumerable QueryScheduledWithoutDataAsync(Instant now, - CancellationToken ct = default) + else { - return collectionComplete.QueryScheduledWithoutDataAsync(now, ct); + return collectionPublished.QueryAsync(app, schemas, q, ct); } + } - public Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope, - CancellationToken ct = default) + public Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope, + CancellationToken ct = default) + { + if (scope == SearchScope.All) { - if (scope == SearchScope.All) - { - return collectionComplete.QueryAsync(app, schemas, q, ct); - } - else - { - return collectionPublished.QueryAsync(app, schemas, q, ct); - } + return collectionComplete.QueryAsync(app, schema, q, ct); } - - public Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope, - CancellationToken ct = default) + else { - if (scope == SearchScope.All) - { - return collectionComplete.QueryAsync(app, schema, q, ct); - } - else - { - return collectionPublished.QueryAsync(app, schema, q, ct); - } + return collectionPublished.QueryAsync(app, schema, q, ct); } + } - public Task FindContentAsync(IAppEntity app, ISchemaEntity schema, DomainId id, SearchScope scope, - CancellationToken ct = default) + public Task FindContentAsync(IAppEntity app, ISchemaEntity schema, DomainId id, SearchScope scope, + CancellationToken ct = default) + { + if (scope == SearchScope.All) { - if (scope == SearchScope.All) - { - return collectionComplete.FindContentAsync(schema, id, ct); - } - else - { - return collectionPublished.FindContentAsync(schema, id, ct); - } + return collectionComplete.FindContentAsync(schema, id, ct); } - - public Task> QueryIdsAsync(DomainId appId, HashSet ids, SearchScope scope, - CancellationToken ct = default) + else { - if (scope == SearchScope.All) - { - return collectionComplete.QueryIdsAsync(appId, ids, ct); - } - else - { - return collectionPublished.QueryIdsAsync(appId, ids, ct); - } + return collectionPublished.FindContentAsync(schema, id, ct); } + } - public Task HasReferrersAsync(DomainId appId, DomainId contentId, SearchScope scope, - CancellationToken ct = default) + public Task> QueryIdsAsync(DomainId appId, HashSet ids, SearchScope scope, + CancellationToken ct = default) + { + if (scope == SearchScope.All) { - if (scope == SearchScope.All) - { - return collectionComplete.HasReferrersAsync(appId, contentId, ct); - } - else - { - return collectionPublished.HasReferrersAsync(appId, contentId, ct); - } + return collectionComplete.QueryIdsAsync(appId, ids, ct); } - - public Task ResetScheduledAsync(DomainId documentId, - CancellationToken ct = default) + else { - return collectionComplete.ResetScheduledAsync(documentId, ct); + return collectionPublished.QueryIdsAsync(appId, ids, ct); } + } - public Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, - CancellationToken ct = default) + public Task HasReferrersAsync(DomainId appId, DomainId contentId, SearchScope scope, + CancellationToken ct = default) + { + if (scope == SearchScope.All) + { + return collectionComplete.HasReferrersAsync(appId, contentId, ct); + } + else { - return collectionComplete.QueryIdsAsync(appId, schemaId, filterNode, ct); + return collectionPublished.HasReferrersAsync(appId, contentId, ct); } } + + public Task ResetScheduledAsync(DomainId documentId, + CancellationToken ct = default) + { + return collectionComplete.ResetScheduledAsync(documentId, ct); + } + + public Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, + CancellationToken ct = default) + { + return collectionComplete.QueryIdsAsync(appId, schemaId, filterNode, ct); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index cd778f8094..0490ce3b18 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -14,211 +14,210 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents; + +public partial class MongoContentRepository : ISnapshotStore, IDeleter { - public partial class MongoContentRepository : ISnapshotStore, IDeleter + IAsyncEnumerable> ISnapshotStore.ReadAllAsync( + CancellationToken ct) { - IAsyncEnumerable> ISnapshotStore.ReadAllAsync( - CancellationToken ct) - { - return collectionComplete.StreamAll(ct) - .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version, true)); - } + return collectionComplete.StreamAll(ct) + .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version, true)); + } - async Task> ISnapshotStore.ReadAsync(DomainId key, - CancellationToken ct) + async Task> ISnapshotStore.ReadAsync(DomainId key, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentRepository/ReadAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentRepository/ReadAsync")) + var existing = + await collectionComplete.FindAsync(key, ct); + + // Support for all versions, where we do not have full snapshots in the collection. + if (existing?.IsSnapshot == true) { - var existing = - await collectionComplete.FindAsync(key, ct); + return new SnapshotResult(existing.DocumentId, existing.ToState(), existing.Version); + } - // Support for all versions, where we do not have full snapshots in the collection. - if (existing?.IsSnapshot == true) - { - return new SnapshotResult(existing.DocumentId, existing.ToState(), existing.Version); - } + return new SnapshotResult(default, null!, EtagVersion.Empty); + } + } - return new SnapshotResult(default, null!, EtagVersion.Empty); - } + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentRepository/DeleteAppAsync")) + { + await collectionComplete.DeleteAppAsync(app.Id, ct); + await collectionPublished.DeleteAppAsync(app.Id, ct); } + } - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) + async Task ISnapshotStore.ClearAsync( + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentRepository/ClearAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentRepository/DeleteAppAsync")) - { - await collectionComplete.DeleteAppAsync(app.Id, ct); - await collectionPublished.DeleteAppAsync(app.Id, ct); - } + await collectionComplete.ClearAsync(ct); + await collectionPublished.ClearAsync(ct); } + } - async Task ISnapshotStore.ClearAsync( - CancellationToken ct) + async Task ISnapshotStore.RemoveAsync(DomainId key, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentRepository/RemoveAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentRepository/ClearAsync")) + // Some data is corrupt and might throw an exception if we do not ignore it. + if (key == DomainId.Empty) { - await collectionComplete.ClearAsync(ct); - await collectionPublished.ClearAsync(ct); + return; } + + await Task.WhenAll( + collectionComplete.RemoveAsync(key, ct), + collectionPublished.RemoveAsync(key, ct)); } + } - async Task ISnapshotStore.RemoveAsync(DomainId key, - CancellationToken ct) + async Task ISnapshotStore.WriteAsync(SnapshotWriteJob job, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentRepository/RemoveAsync")) + // Some data is corrupt and might throw an exception if we do not ignore it. + if (!IsValid(job.Value)) { - // Some data is corrupt and might throw an exception if we do not ignore it. - if (key == DomainId.Empty) - { - return; - } + return; + } + if (!CanUseTransactions) + { + // If transactions are not supported we update the documents without version checks, + // otherwise we would not be able to recover from inconsistencies. await Task.WhenAll( - collectionComplete.RemoveAsync(key, ct), - collectionPublished.RemoveAsync(key, ct)); + UpsertCompleteAsync(job, default), + UpsertPublishedAsync(job, default)); + return; } - } - async Task ISnapshotStore.WriteAsync(SnapshotWriteJob job, - CancellationToken ct) - { - using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteAsync")) + using (var session = await database.Client.StartSessionAsync(cancellationToken: ct)) { - // Some data is corrupt and might throw an exception if we do not ignore it. - if (!IsValid(job.Value)) + // Make an update with full transaction support to be more consistent. + await session.WithTransactionAsync(async (session, ct) => { - return; - } - - if (!CanUseTransactions) - { - // If transactions are not supported we update the documents without version checks, - // otherwise we would not be able to recover from inconsistencies. - await Task.WhenAll( - UpsertCompleteAsync(job, default), - UpsertPublishedAsync(job, default)); - return; - } - - using (var session = await database.Client.StartSessionAsync(cancellationToken: ct)) - { - // Make an update with full transaction support to be more consistent. - await session.WithTransactionAsync(async (session, ct) => - { - await UpsertVersionedCompleteAsync(session, job, ct); - await UpsertVersionedPublishedAsync(session, job, ct); - return true; - }, null, ct); - } + await UpsertVersionedCompleteAsync(session, job, ct); + await UpsertVersionedPublishedAsync(session, job, ct); + return true; + }, null, ct); } } + } - async Task ISnapshotStore.WriteManyAsync(IEnumerable> jobs, - CancellationToken ct) + async Task ISnapshotStore.WriteManyAsync(IEnumerable> jobs, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteManyAsync")) { - using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteManyAsync")) + var collectionUpdates = new Dictionary, List>(); + + var add = new Action, MongoContentEntity>((collection, entity) => { - var collectionUpdates = new Dictionary, List>(); + collectionUpdates.GetOrAddNew(collection).Add(entity); + }); - var add = new Action, MongoContentEntity>((collection, entity) => - { - collectionUpdates.GetOrAddNew(collection).Add(entity); - }); + foreach (var job in jobs) + { + var isValid = IsValid(job.Value); - foreach (var job in jobs) + if (isValid && ShouldWritePublished(job.Value)) { - var isValid = IsValid(job.Value); - - if (isValid && ShouldWritePublished(job.Value)) - { - await collectionPublished.AddCollectionsAsync( - await MongoContentEntity.CreatePublishedAsync(job, appProvider), add, ct); - } - - if (isValid) - { - await collectionComplete.AddCollectionsAsync( - await MongoContentEntity.CreateCompleteAsync(job, appProvider), add, ct); - } + await collectionPublished.AddCollectionsAsync( + await MongoContentEntity.CreatePublishedAsync(job, appProvider), add, ct); } - var parallelOptions = new ParallelOptions - { - CancellationToken = ct, - // This is just an estimate, but we do not want ot have unlimited parallelism. - MaxDegreeOfParallelism = 8 - }; - - // Make one update per collection. - await Parallel.ForEachAsync(collectionUpdates, parallelOptions, (update, ct) => + if (isValid) { - return new ValueTask(update.Key.InsertManyAsync(update.Value, InsertUnordered, ct)); - }); + await collectionComplete.AddCollectionsAsync( + await MongoContentEntity.CreateCompleteAsync(job, appProvider), add, ct); + } } - } - private async Task UpsertPublishedAsync(SnapshotWriteJob job, - CancellationToken ct) - { - if (ShouldWritePublished(job.Value)) + var parallelOptions = new ParallelOptions { - var entityJob = job.As(await MongoContentEntity.CreatePublishedAsync(job, appProvider)); + CancellationToken = ct, + // This is just an estimate, but we do not want ot have unlimited parallelism. + MaxDegreeOfParallelism = 8 + }; - await collectionPublished.UpsertAsync(entityJob, ct); - } - else + // Make one update per collection. + await Parallel.ForEachAsync(collectionUpdates, parallelOptions, (update, ct) => { - await collectionPublished.RemoveAsync(job.Key, ct); - } + return new ValueTask(update.Key.InsertManyAsync(update.Value, InsertUnordered, ct)); + }); } + } - private async Task UpsertVersionedPublishedAsync(IClientSessionHandle session, SnapshotWriteJob job, - CancellationToken ct) + private async Task UpsertPublishedAsync(SnapshotWriteJob job, + CancellationToken ct) + { + if (ShouldWritePublished(job.Value)) { - if (ShouldWritePublished(job.Value)) - { - var entityJob = job.As(await MongoContentEntity.CreatePublishedAsync(job, appProvider)); + var entityJob = job.As(await MongoContentEntity.CreatePublishedAsync(job, appProvider)); - await collectionPublished.UpsertVersionedAsync(session, entityJob, ct); - } - else - { - await collectionPublished.RemoveAsync(session, job.Key, ct); - } + await collectionPublished.UpsertAsync(entityJob, ct); } - - private async Task UpsertCompleteAsync(SnapshotWriteJob job, - CancellationToken ct) + else { - var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider)); - - await collectionComplete.UpsertAsync(entityJob, ct); + await collectionPublished.RemoveAsync(job.Key, ct); } + } - private async Task UpsertVersionedCompleteAsync(IClientSessionHandle session, SnapshotWriteJob job, - CancellationToken ct) + private async Task UpsertVersionedPublishedAsync(IClientSessionHandle session, SnapshotWriteJob job, + CancellationToken ct) + { + if (ShouldWritePublished(job.Value)) { - var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider)); + var entityJob = job.As(await MongoContentEntity.CreatePublishedAsync(job, appProvider)); - await collectionComplete.UpsertVersionedAsync(session, entityJob, ct); + await collectionPublished.UpsertVersionedAsync(session, entityJob, ct); } - - private static bool ShouldWritePublished(ContentDomainObject.State value) + else { - // Only published content is written to the published collection. - return value.Status == Status.Published && !value.IsDeleted; + await collectionPublished.RemoveAsync(session, job.Key, ct); } + } - private static bool IsValid(ContentDomainObject.State state) - { - // Some data is corrupt and might throw an exception during migration if we do not skip them. - return - state.AppId != null && - state.AppId.Id != DomainId.Empty && - state.CurrentVersion != null && - state.SchemaId != null && - state.SchemaId.Id != DomainId.Empty; - } + private async Task UpsertCompleteAsync(SnapshotWriteJob job, + CancellationToken ct) + { + var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider)); + + await collectionComplete.UpsertAsync(entityJob, ct); + } + + private async Task UpsertVersionedCompleteAsync(IClientSessionHandle session, SnapshotWriteJob job, + CancellationToken ct) + { + var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider)); + + await collectionComplete.UpsertVersionedAsync(session, entityJob, ct); + } + + private static bool ShouldWritePublished(ContentDomainObject.State value) + { + // Only published content is written to the published collection. + return value.Status == Status.Published && !value.IsDeleted; + } + + private static bool IsValid(ContentDomainObject.State state) + { + // Some data is corrupt and might throw an exception during migration if we do not skip them. + return + state.AppId != null && + state.AppId.Id != DomainId.Empty && + state.CurrentVersion != null && + state.SchemaId != null && + state.SchemaId.Id != DomainId.Empty; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs index f629e3b677..c03912bb8d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs @@ -11,99 +11,98 @@ using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries.OData; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +public static class Adapt { - public static class Adapt - { - private static Dictionary pathMap; - private static Dictionary propertyMap; + private static Dictionary pathMap; + private static Dictionary propertyMap; - public static IReadOnlyDictionary PropertyMap + public static IReadOnlyDictionary PropertyMap + { + get { - get + if (propertyMap == null) { - if (propertyMap == null) - { - propertyMap = - BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps - .ToDictionary( - x => x.MemberName, - x => x.ElementName, - StringComparer.OrdinalIgnoreCase); - } - - return propertyMap; + propertyMap = + BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps + .ToDictionary( + x => x.MemberName, + x => x.ElementName, + StringComparer.OrdinalIgnoreCase); } + + return propertyMap; } + } - public static IReadOnlyDictionary PathMap + public static IReadOnlyDictionary PathMap + { + get { - get + if (pathMap == null) { - if (pathMap == null) - { - pathMap = PropertyMap.ToDictionary(x => x.Key, x => (PropertyPath)x.Value); - } - - return pathMap; + pathMap = PropertyMap.ToDictionary(x => x.Key, x => (PropertyPath)x.Value); } + + return pathMap; } + } - public static PropertyPath MapPath(PropertyPath path) + public static PropertyPath MapPath(PropertyPath path) + { + if (path.Count == 1 && PathMap.TryGetValue(path[0], out var mappedPath)) { - if (path.Count == 1 && PathMap.TryGetValue(path[0], out var mappedPath)) - { - return mappedPath; - } + return mappedPath; + } - var result = new List(path); + var result = new List(path); - if (result.Count > 0) - { - if (PropertyMap.TryGetValue(path[0], out var mapped)) - { - result[0] = mapped; - } - } - - for (var i = 1; i < path.Count; i++) + if (result.Count > 0) + { + if (PropertyMap.TryGetValue(path[0], out var mapped)) { - result[i] = result[i].UnescapeEdmField().JsonToBsonName().JsonEscape(); + result[0] = mapped; } - - return result; } - public static ClrQuery AdjustToModel(this ClrQuery query, DomainId appId) + for (var i = 1; i < path.Count; i++) { - if (query.Filter != null) - { - query.Filter = AdaptionVisitor.AdaptFilter(query.Filter); - } + result[i] = result[i].UnescapeEdmField().JsonToBsonName().JsonEscape(); + } - if (query.Filter != null) - { - query.Filter = AdaptIdVisitor.AdaptFilter(query.Filter, appId); - } + return result; + } - if (query.Sort != null) - { - query.Sort = query.Sort.Select(x => new SortNode(MapPath(x.Path), x.Order)).ToList(); - } + public static ClrQuery AdjustToModel(this ClrQuery query, DomainId appId) + { + if (query.Filter != null) + { + query.Filter = AdaptionVisitor.AdaptFilter(query.Filter); + } - return query; + if (query.Filter != null) + { + query.Filter = AdaptIdVisitor.AdaptFilter(query.Filter, appId); } - public static FilterNode? AdjustToModel(this FilterNode filter, DomainId appId) + if (query.Sort != null) { - var result = AdaptionVisitor.AdaptFilter(filter); + query.Sort = query.Sort.Select(x => new SortNode(MapPath(x.Path), x.Order)).ToList(); + } - if (result != null) - { - result = AdaptIdVisitor.AdaptFilter(result, appId); - } + return query; + } - return result; + public static FilterNode? AdjustToModel(this FilterNode filter, DomainId appId) + { + var result = AdaptionVisitor.AdaptFilter(filter); + + if (result != null) + { + result = AdaptIdVisitor.AdaptFilter(result, appId); } + + return result; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs index 553cac1797..a889e5cd57 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs @@ -8,31 +8,30 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +internal sealed class AdaptionVisitor : TransformVisitor { - internal sealed class AdaptionVisitor : TransformVisitor + private static readonly AdaptionVisitor Instance = new AdaptionVisitor(); + + private AdaptionVisitor() { - private static readonly AdaptionVisitor Instance = new AdaptionVisitor(); + } - private AdaptionVisitor() - { - } + public static FilterNode? AdaptFilter(FilterNode filter) + { + return filter.Accept(Instance, None.Value); + } - public static FilterNode? AdaptFilter(FilterNode filter) + public override FilterNode Visit(CompareFilter nodeIn, None args) + { + if (string.Equals(nodeIn.Path[0], "id", StringComparison.OrdinalIgnoreCase)) { - return filter.Accept(Instance, None.Value); + return nodeIn; } - public override FilterNode Visit(CompareFilter nodeIn, None args) - { - if (string.Equals(nodeIn.Path[0], "id", StringComparison.OrdinalIgnoreCase)) - { - return nodeIn; - } + var path = Adapt.MapPath(nodeIn.Path); - var path = Adapt.MapPath(nodeIn.Path); - - return nodeIn with { Path = path }; - } + return nodeIn with { Path = path }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs index 77d64fdaba..20a764f462 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs @@ -13,118 +13,117 @@ using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +public static class Extensions { - public static class Extensions + public sealed class StatusOnly { - public sealed class StatusOnly - { - [BsonId] - [BsonElement("_id")] - public DomainId DocumentId { get; set; } + [BsonId] + [BsonElement("_id")] + public DomainId DocumentId { get; set; } - [BsonRequired] - [BsonElement("id")] - public DomainId Id { get; set; } + [BsonRequired] + [BsonElement("id")] + public DomainId Id { get; set; } - [BsonRequired] - [BsonElement("_si")] - public DomainId IndexedSchemaId { get; set; } + [BsonRequired] + [BsonElement("_si")] + public DomainId IndexedSchemaId { get; set; } - [BsonRequired] - [BsonElement("ss")] - public Status Status { get; set; } - } + [BsonRequired] + [BsonElement("ss")] + public Status Status { get; set; } + } - public sealed class IdOnly - { - [BsonId] - [BsonElement("_id")] - public DomainId Id { get; set; } + public sealed class IdOnly + { + [BsonId] + [BsonElement("_id")] + public DomainId Id { get; set; } - [BsonElement(nameof(Joined))] - public MongoContentEntity[] Joined { get; set; } - } + [BsonElement(nameof(Joined))] + public MongoContentEntity[] Joined { get; set; } + } - public static bool IsSatisfiedByIndex(this ClrQuery query) - { - return - query.Sort is { Count: 2 } && - query.Sort[0].Path.ToString() == "mt" && - query.Sort[0].Order == SortOrder.Descending && - query.Sort[1].Path.ToString() == "id" && - query.Sort[1].Order == SortOrder.Ascending; - } + public static bool IsSatisfiedByIndex(this ClrQuery query) + { + return + query.Sort is { Count: 2 } && + query.Sort[0].Path.ToString() == "mt" && + query.Sort[0].Order == SortOrder.Descending && + query.Sort[1].Path.ToString() == "id" && + query.Sort[1].Order == SortOrder.Ascending; + } - public static async Task> QueryContentsAsync(this IMongoCollection collection, FilterDefinition filter, ClrQuery query, - CancellationToken ct) + public static async Task> QueryContentsAsync(this IMongoCollection collection, FilterDefinition filter, ClrQuery query, + CancellationToken ct) + { + if (query.Skip > 0 && !query.IsSatisfiedByIndex()) { - if (query.Skip > 0 && !query.IsSatisfiedByIndex()) + // If we have to skip over items, we could reach the limit of the sort buffer, therefore get the ids and all filter fields only + // in a first iteration and get the actual content in the a second query. + var projection = Builders.Projection.Include("_id"); + + foreach (var field in query.GetAllFields()) { - // If we have to skip over items, we could reach the limit of the sort buffer, therefore get the ids and all filter fields only - // in a first iteration and get the actual content in the a second query. - var projection = Builders.Projection.Include("_id"); - - foreach (var field in query.GetAllFields()) - { - projection = projection.Include(field); - } - - if (query.Random > 0) - { - var ids = - await collection.Aggregate() - .Match(filter) - .Project(projection) - .QuerySort(query) - .QuerySkip(query) - .QueryLimit(query) - .ToListAsync(ct); - - var randomIds = ids.Select(x => x.Id).TakeRandom(query.Random); - - var documents = - await collection.Find(Builders.Filter.In(x => x.Id, randomIds)) - .ToListAsync(ct); - - return documents.Shuffle().ToList(); - } - - var joined = + projection = projection.Include(field); + } + + if (query.Random > 0) + { + var ids = await collection.Aggregate() .Match(filter) .Project(projection) .QuerySort(query) .QuerySkip(query) .QueryLimit(query) - .Lookup(collection, x => x.Id, x => x.DocumentId, x => x.Joined) .ToListAsync(ct); - return joined.Select(x => x.Joined[0]).ToList(); + var randomIds = ids.Select(x => x.Id).TakeRandom(query.Random); + + var documents = + await collection.Find(Builders.Filter.In(x => x.Id, randomIds)) + .ToListAsync(ct); + + return documents.Shuffle().ToList(); } - var result = - collection.Find(filter) + var joined = + await collection.Aggregate() + .Match(filter) + .Project(projection) .QuerySort(query) - .QueryLimit(query) .QuerySkip(query) - .ToListRandomAsync(collection, query.Random, ct); + .QueryLimit(query) + .Lookup(collection, x => x.Id, x => x.DocumentId, x => x.Joined) + .ToListAsync(ct); - return await result; + return joined.Select(x => x.Joined[0]).ToList(); } - public static Task> FindStatusAsync(this IMongoCollection collection, - FilterDefinition filter, - CancellationToken ct) - { - var projections = Builders.Projection; - - return collection.Find(filter) - .Project(projections - .Include(x => x.Id) - .Include(x => x.IndexedSchemaId) - .Include(x => x.Status)) - .ToListAsync(ct); - } + var result = + collection.Find(filter) + .QuerySort(query) + .QueryLimit(query) + .QuerySkip(query) + .ToListRandomAsync(collection, query.Random, ct); + + return await result; + } + + public static Task> FindStatusAsync(this IMongoCollection collection, + FilterDefinition filter, + CancellationToken ct) + { + var projections = Builders.Projection; + + return collection.Find(filter) + .Project(projections + .Include(x => x.Id) + .Include(x => x.IndexedSchemaId) + .Include(x => x.Status)) + .ToListAsync(ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs index d1e366e7c5..58d60ff2e0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs @@ -8,20 +8,19 @@ using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +public abstract class OperationBase : MongoBase { - public abstract class OperationBase : MongoBase - { - public IMongoCollection Collection { get; private set; } + public IMongoCollection Collection { get; private set; } - public void Setup(IMongoCollection collection) - { - Collection = collection; - } + public void Setup(IMongoCollection collection) + { + Collection = collection; + } - public virtual IEnumerable> CreateIndexes() - { - yield break; - } + public virtual IEnumerable> CreateIndexes() + { + yield break; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs index 09c8042f71..8e32dbf47c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs @@ -10,50 +10,49 @@ using Squidex.Domain.Apps.Entities.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +public sealed class QueryAsStream : OperationBase { - public sealed class QueryAsStream : OperationBase + public override IEnumerable> CreateIndexes() { - public override IEnumerable> CreateIndexes() - { - yield return new CreateIndexModel(Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.IndexedSchemaId)); - } + yield return new CreateIndexModel(Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.IndexedSchemaId)); + } - public async IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, - [EnumeratorCancellation] CancellationToken ct) - { - var filter = CreateFilter(appId, schemaIds); + public async IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, + [EnumeratorCancellation] CancellationToken ct) + { + var filter = CreateFilter(appId, schemaIds); - using (var cursor = await Collection.Find(filter).ToCursorAsync(ct)) + using (var cursor = await Collection.Find(filter).ToCursorAsync(ct)) + { + while (await cursor.MoveNextAsync(ct)) { - while (await cursor.MoveNextAsync(ct)) + foreach (var entity in cursor.Current) { - foreach (var entity in cursor.Current) - { - yield return entity; - } + yield return entity; } } } + } - private static FilterDefinition CreateFilter(DomainId appId, HashSet? schemaIds) + private static FilterDefinition CreateFilter(DomainId appId, HashSet? schemaIds) + { + var filters = new List> { - var filters = new List> - { - Filter.Gt(x => x.LastModified, default), - Filter.Gt(x => x.Id, default), - Filter.Eq(x => x.IndexedAppId, appId) - }; + Filter.Gt(x => x.LastModified, default), + Filter.Gt(x => x.Id, default), + Filter.Eq(x => x.IndexedAppId, appId) + }; - if (schemaIds != null) - { - filters.Add(Filter.In(x => x.IndexedSchemaId, schemaIds)); - } - - return Filter.And(filters); + if (schemaIds != null) + { + filters.Add(Filter.In(x => x.IndexedSchemaId, schemaIds)); } + + return Filter.And(filters); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs index 96dfc5aba1..ee5df6ef43 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs @@ -10,23 +10,22 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +internal sealed class QueryById : OperationBase { - internal sealed class QueryById : OperationBase + public async Task QueryAsync(ISchemaEntity schema, DomainId id, + CancellationToken ct) { - public async Task QueryAsync(ISchemaEntity schema, DomainId id, - CancellationToken ct) - { - var filter = Filter.Eq(x => x.DocumentId, DomainId.Combine(schema.AppId, id)); - - var contentEntity = await Collection.Find(filter).FirstOrDefaultAsync(ct); + var filter = Filter.Eq(x => x.DocumentId, DomainId.Combine(schema.AppId, id)); - if (contentEntity == null || contentEntity.IndexedSchemaId != schema.Id) - { - return null; - } + var contentEntity = await Collection.Find(filter).FirstOrDefaultAsync(ct); - return contentEntity; + if (contentEntity == null || contentEntity.IndexedSchemaId != schema.Id) + { + return null; } + + return contentEntity; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs index 0f384b5cdf..6fff3f6242 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs @@ -16,92 +16,91 @@ using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +internal sealed class QueryByIds : OperationBase { - internal sealed class QueryByIds : OperationBase + public async Task> QueryIdsAsync(DomainId appId, HashSet ids, + CancellationToken ct) { - public async Task> QueryIdsAsync(DomainId appId, HashSet ids, - CancellationToken ct) + if (ids == null || ids.Count == 0) { - if (ids == null || ids.Count == 0) - { - return ReadonlyList.Empty(); - } - - var filter = CreateFilter(appId, null, ids); - - var contentEntities = await Collection.FindStatusAsync(filter, ct); - - return contentEntities.Select(x => new ContentIdStatus(x.IndexedSchemaId, x.Id, x.Status)).ToList(); + return ReadonlyList.Empty(); } - public async Task> QueryAsync(IAppEntity app, List schemas, Q q, - CancellationToken ct) - { - if (q.Ids == null || q.Ids.Count == 0) - { - return ResultList.Empty(); - } - - var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.Ids.ToHashSet()); + var filter = CreateFilter(appId, null, ids); - var contentEntities = await FindContentsAsync(q.Query, filter, ct); - var contentTotal = (long)contentEntities.Count; + var contentEntities = await Collection.FindStatusAsync(filter, ct); - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) - { - if (q.NoTotal) - { - contentTotal = -1; - } - else - { - contentTotal = await Collection.Find(filter).CountDocumentsAsync(ct); - } - } - - return ResultList.Create(contentTotal, contentEntities); - } + return contentEntities.Select(x => new ContentIdStatus(x.IndexedSchemaId, x.Id, x.Status)).ToList(); + } - private async Task> FindContentsAsync(ClrQuery query, FilterDefinition filter, - CancellationToken ct) + public async Task> QueryAsync(IAppEntity app, List schemas, Q q, + CancellationToken ct) + { + if (q.Ids == null || q.Ids.Count == 0) { - var result = - Collection.Find(filter) - .QueryLimit(query) - .QuerySkip(query) - .ToListRandomAsync(Collection, query.Random, ct); - - return await result; + return ResultList.Empty(); } - private static FilterDefinition CreateFilter(DomainId appId, IEnumerable? schemaIds, HashSet ids) - { - var filters = new List>(); + var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.Ids.ToHashSet()); - var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList(); + var contentEntities = await FindContentsAsync(q.Query, filter, ct); + var contentTotal = (long)contentEntities.Count; - if (documentIds.Count > 1) + if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + { + if (q.NoTotal) { - filters.Add( - Filter.Or( - Filter.In(x => x.DocumentId, documentIds))); + contentTotal = -1; } else { - filters.Add( - Filter.Or( - Filter.Eq(x => x.DocumentId, documentIds[0]))); + contentTotal = await Collection.Find(filter).CountDocumentsAsync(ct); } + } - if (schemaIds != null) - { - filters.Add(Filter.In(x => x.IndexedSchemaId, schemaIds)); - } + return ResultList.Create(contentTotal, contentEntities); + } + + private async Task> FindContentsAsync(ClrQuery query, FilterDefinition filter, + CancellationToken ct) + { + var result = + Collection.Find(filter) + .QueryLimit(query) + .QuerySkip(query) + .ToListRandomAsync(Collection, query.Random, ct); + + return await result; + } + + private static FilterDefinition CreateFilter(DomainId appId, IEnumerable? schemaIds, HashSet ids) + { + var filters = new List>(); - filters.Add(Filter.Ne(x => x.IsDeleted, true)); + var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList(); - return Filter.And(filters); + if (documentIds.Count > 1) + { + filters.Add( + Filter.Or( + Filter.In(x => x.DocumentId, documentIds))); } + else + { + filters.Add( + Filter.Or( + Filter.Eq(x => x.DocumentId, documentIds[0]))); + } + + if (schemaIds != null) + { + filters.Add(Filter.In(x => x.IndexedSchemaId, schemaIds)); + } + + filters.Add(Filter.Ne(x => x.IsDeleted, true)); + + return Filter.And(filters); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs index d7100a455a..df2901be24 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs @@ -14,183 +14,182 @@ using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +internal sealed class QueryByQuery : OperationBase { - internal sealed class QueryByQuery : OperationBase - { - private readonly MongoCountCollection countCollection; + private readonly MongoCountCollection countCollection; - public QueryByQuery(MongoCountCollection countCollection) - { - this.countCollection = countCollection; - } + public QueryByQuery(MongoCountCollection countCollection) + { + this.countCollection = countCollection; + } - public override IEnumerable> CreateIndexes() - { - yield return new CreateIndexModel(Index - .Descending(x => x.LastModified) - .Ascending(x => x.Id) - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.ReferencedIds)); - - yield return new CreateIndexModel(Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted) - .Descending(x => x.LastModified)); - } + public override IEnumerable> CreateIndexes() + { + yield return new CreateIndexModel(Index + .Descending(x => x.LastModified) + .Ascending(x => x.Id) + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.ReferencedIds)); + + yield return new CreateIndexModel(Index + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.IsDeleted) + .Descending(x => x.LastModified)); + } - public async Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, - CancellationToken ct) - { - // We need to translate the query names to the document field names in MongoDB. - var adjustedFilter = filterNode.AdjustToModel(appId); + public async Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, + CancellationToken ct) + { + // We need to translate the query names to the document field names in MongoDB. + var adjustedFilter = filterNode.AdjustToModel(appId); - var filter = BuildFilter(appId, schemaId, adjustedFilter); + var filter = BuildFilter(appId, schemaId, adjustedFilter); - var contentEntities = await Collection.FindStatusAsync(filter, ct); - var contentResults = contentEntities.Select(x => new ContentIdStatus(x.IndexedSchemaId, x.Id, x.Status)).ToList(); + var contentEntities = await Collection.FindStatusAsync(filter, ct); + var contentResults = contentEntities.Select(x => new ContentIdStatus(x.IndexedSchemaId, x.Id, x.Status)).ToList(); - return contentResults; - } + return contentResults; + } - public async Task> QueryAsync(IAppEntity app, List schemas, Q q, - CancellationToken ct) - { - // We need to translate the query names to the document field names in MongoDB. - var query = q.Query.AdjustToModel(app.Id); + public async Task> QueryAsync(IAppEntity app, List schemas, Q q, + CancellationToken ct) + { + // We need to translate the query names to the document field names in MongoDB. + var query = q.Query.AdjustToModel(app.Id); - var (filter, isDefault) = CreateFilter(app.Id, schemas.Select(x => x.Id), query, q.Reference, q.CreatedBy); + var (filter, isDefault) = CreateFilter(app.Id, schemas.Select(x => x.Id), query, q.Reference, q.CreatedBy); - var contentEntities = await Collection.QueryContentsAsync(filter, query, ct); - var contentTotal = (long)contentEntities.Count; + var contentEntities = await Collection.QueryContentsAsync(filter, query, ct); + var contentTotal = (long)contentEntities.Count; - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + { + if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) { - if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) - { - contentTotal = -1; - } - else if (query.IsSatisfiedByIndex()) - { - // It is faster to filter with sorting when there is an index, because it forces the index to be used. - contentTotal = await Collection.Find(filter).QuerySort(query).CountDocumentsAsync(ct); - } - else - { - contentTotal = await Collection.Find(filter).CountDocumentsAsync(ct); - } + contentTotal = -1; + } + else if (query.IsSatisfiedByIndex()) + { + // It is faster to filter with sorting when there is an index, because it forces the index to be used. + contentTotal = await Collection.Find(filter).QuerySort(query).CountDocumentsAsync(ct); + } + else + { + contentTotal = await Collection.Find(filter).CountDocumentsAsync(ct); } - - return ResultList.Create(contentTotal, contentEntities); } - public async Task> QueryAsync(ISchemaEntity schema, Q q, - CancellationToken ct) - { - // We need to translate the query names to the document field names in MongoDB. - var query = q.Query.AdjustToModel(schema.AppId.Id); - - // Default means that no other filters are applied and we only query by app and schema. - var (filter, isDefault) = CreateFilter(schema.AppId.Id, Enumerable.Repeat(schema.Id, 1), query, q.Reference, q.CreatedBy); + return ResultList.Create(contentTotal, contentEntities); + } - var contentEntities = await Collection.QueryContentsAsync(filter, query, ct); - var contentTotal = (long)contentEntities.Count; + public async Task> QueryAsync(ISchemaEntity schema, Q q, + CancellationToken ct) + { + // We need to translate the query names to the document field names in MongoDB. + var query = q.Query.AdjustToModel(schema.AppId.Id); - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) - { - if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) - { - contentTotal = -1; - } - else if (isDefault) - { - // Cache total count by app and schema because no other filters are applied (aka default). - var totalKey = $"{schema.AppId.Id}_{schema.Id}"; - - contentTotal = await countCollection.GetOrAddAsync(totalKey, ct => Collection.Find(filter).CountDocumentsAsync(ct), ct); - } - else if (query.IsSatisfiedByIndex()) - { - // It is faster to filter with sorting when there is an index, because it forces the index to be used. - contentTotal = await Collection.Find(filter).QuerySort(query).CountDocumentsAsync(ct); - } - else - { - contentTotal = await Collection.Find(filter).CountDocumentsAsync(ct); - } - } + // Default means that no other filters are applied and we only query by app and schema. + var (filter, isDefault) = CreateFilter(schema.AppId.Id, Enumerable.Repeat(schema.Id, 1), query, q.Reference, q.CreatedBy); - return ResultList.Create(contentTotal, contentEntities); - } + var contentEntities = await Collection.QueryContentsAsync(filter, query, ct); + var contentTotal = (long)contentEntities.Count; - private static FilterDefinition BuildFilter(DomainId appId, DomainId schemaId, - FilterNode? filter) + if (contentTotal >= q.Query.Take || q.Query.Skip > 0) { - var filters = new List> + if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) + { + contentTotal = -1; + } + else if (isDefault) { - Filter.Exists(x => x.LastModified), - Filter.Exists(x => x.Id), - Filter.Eq(x => x.IndexedAppId, appId), - Filter.Eq(x => x.IndexedSchemaId, schemaId) - }; + // Cache total count by app and schema because no other filters are applied (aka default). + var totalKey = $"{schema.AppId.Id}_{schema.Id}"; - if (filter?.HasField("dl") != true) + contentTotal = await countCollection.GetOrAddAsync(totalKey, ct => Collection.Find(filter).CountDocumentsAsync(ct), ct); + } + else if (query.IsSatisfiedByIndex()) { - filters.Add(Filter.Ne(x => x.IsDeleted, true)); + // It is faster to filter with sorting when there is an index, because it forces the index to be used. + contentTotal = await Collection.Find(filter).QuerySort(query).CountDocumentsAsync(ct); } - - if (filter != null) + else { - filters.Add(filter.BuildFilter()); + contentTotal = await Collection.Find(filter).CountDocumentsAsync(ct); } + } + + return ResultList.Create(contentTotal, contentEntities); + } + + private static FilterDefinition BuildFilter(DomainId appId, DomainId schemaId, + FilterNode? filter) + { + var filters = new List> + { + Filter.Exists(x => x.LastModified), + Filter.Exists(x => x.Id), + Filter.Eq(x => x.IndexedAppId, appId), + Filter.Eq(x => x.IndexedSchemaId, schemaId) + }; - return Filter.And(filters); + if (filter?.HasField("dl") != true) + { + filters.Add(Filter.Ne(x => x.IsDeleted, true)); } - private static (FilterDefinition, bool) CreateFilter(DomainId appId, IEnumerable schemaIds, ClrQuery? query, - DomainId referenced, RefToken? createdBy) + if (filter != null) { - var filters = new List> - { - Filter.Gt(x => x.LastModified, default), - Filter.Gt(x => x.Id, default), - Filter.Eq(x => x.IndexedAppId, appId), - Filter.In(x => x.IndexedSchemaId, schemaIds) - }; + filters.Add(filter.BuildFilter()); + } - var isDefault = false; + return Filter.And(filters); + } - if (query?.HasFilterField("dl") != true) - { - filters.Add(Filter.Ne(x => x.IsDeleted, true)); + private static (FilterDefinition, bool) CreateFilter(DomainId appId, IEnumerable schemaIds, ClrQuery? query, + DomainId referenced, RefToken? createdBy) + { + var filters = new List> + { + Filter.Gt(x => x.LastModified, default), + Filter.Gt(x => x.Id, default), + Filter.Eq(x => x.IndexedAppId, appId), + Filter.In(x => x.IndexedSchemaId, schemaIds) + }; - isDefault = true; - } + var isDefault = false; - if (query?.Filter != null) - { - filters.Add(query.Filter.BuildFilter()); + if (query?.HasFilterField("dl") != true) + { + filters.Add(Filter.Ne(x => x.IsDeleted, true)); - isDefault = false; - } + isDefault = true; + } - if (referenced != default) - { - filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced)); + if (query?.Filter != null) + { + filters.Add(query.Filter.BuildFilter()); - isDefault = false; - } + isDefault = false; + } - if (createdBy != null) - { - filters.Add(Filter.Eq(x => x.CreatedBy, createdBy)); + if (referenced != default) + { + filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced)); - isDefault = false; - } + isDefault = false; + } - return (Filter.And(filters), isDefault); + if (createdBy != null) + { + filters.Add(Filter.Eq(x => x.CreatedBy, createdBy)); + + isDefault = false; } + + return (Filter.And(filters), isDefault); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs index ca664a1eaf..ba853375f0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs @@ -17,183 +17,182 @@ using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +internal sealed class QueryInDedicatedCollection : MongoBase { - internal sealed class QueryInDedicatedCollection : MongoBase - { - private readonly ConcurrentDictionary<(DomainId, DomainId), Task>> collections = - new ConcurrentDictionary<(DomainId, DomainId), Task>>(); + private readonly ConcurrentDictionary<(DomainId, DomainId), Task>> collections = + new ConcurrentDictionary<(DomainId, DomainId), Task>>(); - private readonly IMongoClient mongoClient; - private readonly string prefixDatabase; - private readonly string prefixCollection; + private readonly IMongoClient mongoClient; + private readonly string prefixDatabase; + private readonly string prefixCollection; - public QueryInDedicatedCollection(IMongoClient mongoClient, string prefixDatabase, string prefixCollection) - { - this.mongoClient = mongoClient; - this.prefixDatabase = prefixDatabase; - this.prefixCollection = prefixCollection; - } + public QueryInDedicatedCollection(IMongoClient mongoClient, string prefixDatabase, string prefixCollection) + { + this.mongoClient = mongoClient; + this.prefixDatabase = prefixDatabase; + this.prefixCollection = prefixCollection; + } - public Task> GetCollectionAsync(DomainId appId, DomainId schemaId) - { + public Task> GetCollectionAsync(DomainId appId, DomainId schemaId) + { #pragma warning disable MA0106 // Avoid closure by using an overload with the 'factoryArgument' parameter - return collections.GetOrAdd((appId, schemaId), async key => - { - var (appId, schemaId) = key; - - var schemaDatabase = mongoClient.GetDatabase($"{prefixDatabase}_{appId}"); - var schemaCollection = schemaDatabase.GetCollection($"{prefixCollection}_{schemaId}"); - - await schemaCollection.Indexes.CreateManyAsync( - new[] - { - new CreateIndexModel(Index - .Descending(x => x.LastModified) - .Ascending(x => x.Id) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.ReferencedIds)), - new CreateIndexModel(Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted) - .Descending(x => x.LastModified)) - }); - - return schemaCollection; - }); + return collections.GetOrAdd((appId, schemaId), async key => + { + var (appId, schemaId) = key; + + var schemaDatabase = mongoClient.GetDatabase($"{prefixDatabase}_{appId}"); + var schemaCollection = schemaDatabase.GetCollection($"{prefixCollection}_{schemaId}"); + + await schemaCollection.Indexes.CreateManyAsync( + new[] + { + new CreateIndexModel(Index + .Descending(x => x.LastModified) + .Ascending(x => x.Id) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.ReferencedIds)), + new CreateIndexModel(Index + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.IsDeleted) + .Descending(x => x.LastModified)) + }); + + return schemaCollection; + }); #pragma warning restore MA0106 // Avoid closure by using an overload with the 'factoryArgument' parameter - } + } - public async Task> QueryIdsAsync(IAppEntity app, ISchemaEntity schema, FilterNode filterNode, - CancellationToken ct) - { - // We need to translate the filter names to the document field names in MongoDB. - var adjustedFilter = filterNode.AdjustToModel(app.Id); + public async Task> QueryIdsAsync(IAppEntity app, ISchemaEntity schema, FilterNode filterNode, + CancellationToken ct) + { + // We need to translate the filter names to the document field names in MongoDB. + var adjustedFilter = filterNode.AdjustToModel(app.Id); - var filter = BuildFilter(adjustedFilter); + var filter = BuildFilter(adjustedFilter); - var contentCollection = await GetCollectionAsync(schema.AppId.Id, schema.Id); - var contentEntities = await contentCollection.FindStatusAsync(filter, ct); - var contentResults = contentEntities.Select(x => new ContentIdStatus(x.IndexedSchemaId, x.Id, x.Status)).ToList(); + var contentCollection = await GetCollectionAsync(schema.AppId.Id, schema.Id); + var contentEntities = await contentCollection.FindStatusAsync(filter, ct); + var contentResults = contentEntities.Select(x => new ContentIdStatus(x.IndexedSchemaId, x.Id, x.Status)).ToList(); - return contentResults; - } + return contentResults; + } - public async Task> QueryAsync(ISchemaEntity schema, Q q, - CancellationToken ct) - { - // We need to translate the query names to the document field names in MongoDB. - var query = q.Query.AdjustToModel(schema.AppId.Id); + public async Task> QueryAsync(ISchemaEntity schema, Q q, + CancellationToken ct) + { + // We need to translate the query names to the document field names in MongoDB. + var query = q.Query.AdjustToModel(schema.AppId.Id); - var filter = CreateFilter(query, q.Reference, q.CreatedBy); + var filter = CreateFilter(query, q.Reference, q.CreatedBy); - var contentCollection = await GetCollectionAsync(schema.AppId.Id, schema.Id); - var contentEntities = await contentCollection.QueryContentsAsync(filter, query, ct); - var contentTotal = (long)contentEntities.Count; + var contentCollection = await GetCollectionAsync(schema.AppId.Id, schema.Id); + var contentEntities = await contentCollection.QueryContentsAsync(filter, query, ct); + var contentTotal = (long)contentEntities.Count; - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + { + if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) { - if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) - { - contentTotal = -1; - } - else if (query.IsSatisfiedByIndex()) - { - // It is faster to filter with sorting when there is an index, because it forces the index to be used. - contentTotal = await contentCollection.Find(filter).QuerySort(query).CountDocumentsAsync(ct); - } - else - { - contentTotal = await contentCollection.Find(filter).CountDocumentsAsync(ct); - } + contentTotal = -1; + } + else if (query.IsSatisfiedByIndex()) + { + // It is faster to filter with sorting when there is an index, because it forces the index to be used. + contentTotal = await contentCollection.Find(filter).QuerySort(query).CountDocumentsAsync(ct); + } + else + { + contentTotal = await contentCollection.Find(filter).CountDocumentsAsync(ct); } - - return ResultList.Create(contentTotal, contentEntities); } - public async Task UpsertAsync(SnapshotWriteJob job, - CancellationToken ct = default) - { - var collection = await GetCollectionAsync(job.Value.AppId.Id, job.Value.SchemaId.Id); + return ResultList.Create(contentTotal, contentEntities); + } - await collection.ReplaceOneAsync(Filter.Eq(x => x.DocumentId, job.Key), job.Value, UpsertReplace, ct); - } + public async Task UpsertAsync(SnapshotWriteJob job, + CancellationToken ct = default) + { + var collection = await GetCollectionAsync(job.Value.AppId.Id, job.Value.SchemaId.Id); - public async Task UpsertVersionedAsync(IClientSessionHandle session, SnapshotWriteJob job, - CancellationToken ct = default) - { - var collection = await GetCollectionAsync(job.Value.AppId.Id, job.Value.SchemaId.Id); + await collection.ReplaceOneAsync(Filter.Eq(x => x.DocumentId, job.Key), job.Value, UpsertReplace, ct); + } - await collection.UpsertVersionedAsync(session, job, ct); - } + public async Task UpsertVersionedAsync(IClientSessionHandle session, SnapshotWriteJob job, + CancellationToken ct = default) + { + var collection = await GetCollectionAsync(job.Value.AppId.Id, job.Value.SchemaId.Id); - public async Task RemoveAsync(MongoContentEntity value, - CancellationToken ct = default) - { - var collection = await GetCollectionAsync(value.AppId.Id, value.SchemaId.Id); + await collection.UpsertVersionedAsync(session, job, ct); + } - await collection.DeleteOneAsync(x => x.DocumentId == value.DocumentId, null, ct); - } + public async Task RemoveAsync(MongoContentEntity value, + CancellationToken ct = default) + { + var collection = await GetCollectionAsync(value.AppId.Id, value.SchemaId.Id); + + await collection.DeleteOneAsync(x => x.DocumentId == value.DocumentId, null, ct); + } - public async Task RemoveAsync(IClientSessionHandle session, MongoContentEntity value, - CancellationToken ct = default) + public async Task RemoveAsync(IClientSessionHandle session, MongoContentEntity value, + CancellationToken ct = default) + { + var collection = await GetCollectionAsync(value.AppId.Id, value.SchemaId.Id); + + await collection.DeleteOneAsync(session, x => x.DocumentId == value.DocumentId, null, ct); + } + + private static FilterDefinition BuildFilter(FilterNode? filter) + { + var filters = new List> { - var collection = await GetCollectionAsync(value.AppId.Id, value.SchemaId.Id); + Filter.Exists(x => x.LastModified), + Filter.Exists(x => x.Id) + }; - await collection.DeleteOneAsync(session, x => x.DocumentId == value.DocumentId, null, ct); + if (filter?.HasField("dl") != true) + { + filters.Add(Filter.Ne(x => x.IsDeleted, true)); } - private static FilterDefinition BuildFilter(FilterNode? filter) + if (filter != null) { - var filters = new List> - { - Filter.Exists(x => x.LastModified), - Filter.Exists(x => x.Id) - }; + filters.Add(filter.BuildFilter()); + } - if (filter?.HasField("dl") != true) - { - filters.Add(Filter.Ne(x => x.IsDeleted, true)); - } + return Filter.And(filters); + } - if (filter != null) - { - filters.Add(filter.BuildFilter()); - } + private static FilterDefinition CreateFilter(ClrQuery? query, + DomainId referenced, RefToken? createdBy) + { + var filters = new List> + { + Filter.Gt(x => x.LastModified, default), + Filter.Gt(x => x.Id, default) + }; - return Filter.And(filters); + if (query?.HasFilterField("dl") != true) + { + filters.Add(Filter.Ne(x => x.IsDeleted, true)); } - private static FilterDefinition CreateFilter(ClrQuery? query, - DomainId referenced, RefToken? createdBy) + if (query?.Filter != null) { - var filters = new List> - { - Filter.Gt(x => x.LastModified, default), - Filter.Gt(x => x.Id, default) - }; - - if (query?.HasFilterField("dl") != true) - { - filters.Add(Filter.Ne(x => x.IsDeleted, true)); - } - - if (query?.Filter != null) - { - filters.Add(query.Filter.BuildFilter()); - } - - if (referenced != default) - { - filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced)); - } + filters.Add(query.Filter.BuildFilter()); + } - if (createdBy != null) - { - filters.Add(Filter.Eq(x => x.CreatedBy, createdBy)); - } + if (referenced != default) + { + filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced)); + } - return Filter.And(filters); + if (createdBy != null) + { + filters.Add(Filter.Eq(x => x.CreatedBy, createdBy)); } + + return Filter.And(filters); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs index 1973b00549..30dc859506 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs @@ -12,51 +12,50 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +internal sealed class QueryReferences : OperationBase { - internal sealed class QueryReferences : OperationBase + private readonly QueryByIds queryByIds; + + public sealed class ReferencedIdsOnly { - private readonly QueryByIds queryByIds; + [BsonId] + [BsonElement("_id")] + public DomainId DocumentId { get; set; } - public sealed class ReferencedIdsOnly - { - [BsonId] - [BsonElement("_id")] - public DomainId DocumentId { get; set; } + [BsonRequired] + [BsonElement("rf")] + public HashSet? ReferencedIds { get; set; } + } - [BsonRequired] - [BsonElement("rf")] - public HashSet? ReferencedIds { get; set; } - } + public QueryReferences(QueryByIds queryByIds) + { + this.queryByIds = queryByIds; + } - public QueryReferences(QueryByIds queryByIds) + public async Task> QueryAsync(IAppEntity app, List schemas, Q q, + CancellationToken ct) + { + var find = + Collection + .Find(Filter.Eq(x => x.DocumentId, DomainId.Combine(app.Id, q.Referencing))) + .Project(Projection.Include(x => x.ReferencedIds)); + + var contentEntity = await find.FirstOrDefaultAsync(ct); + + if (contentEntity == null) { - this.queryByIds = queryByIds; + throw new DomainObjectNotFoundException(q.Referencing.ToString()); } - public async Task> QueryAsync(IAppEntity app, List schemas, Q q, - CancellationToken ct) + if (contentEntity.ReferencedIds == null || contentEntity.ReferencedIds.Count == 0) { - var find = - Collection - .Find(Filter.Eq(x => x.DocumentId, DomainId.Combine(app.Id, q.Referencing))) - .Project(Projection.Include(x => x.ReferencedIds)); - - var contentEntity = await find.FirstOrDefaultAsync(ct); - - if (contentEntity == null) - { - throw new DomainObjectNotFoundException(q.Referencing.ToString()); - } - - if (contentEntity.ReferencedIds == null || contentEntity.ReferencedIds.Count == 0) - { - return ResultList.Empty(); - } + return ResultList.Empty(); + } - q = q.WithReferencing(default).WithIds(contentEntity.ReferencedIds!); + q = q.WithReferencing(default).WithIds(contentEntity.ReferencedIds!); - return await queryByIds.QueryAsync(app, schemas, q, ct); - } + return await queryByIds.QueryAsync(app, schemas, q, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs index b13ef1ad89..43df890d11 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs @@ -9,33 +9,32 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +internal sealed class QueryReferrers : OperationBase { - internal sealed class QueryReferrers : OperationBase + public override IEnumerable> CreateIndexes() { - public override IEnumerable> CreateIndexes() - { - yield return new CreateIndexModel(Index - .Ascending(x => x.ReferencedIds) - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted)); - } + yield return new CreateIndexModel(Index + .Ascending(x => x.ReferencedIds) + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted)); + } - public async Task CheckExistsAsync(DomainId appId, DomainId contentId, - CancellationToken ct) - { - var filter = - Filter.And( - Filter.AnyEq(x => x.ReferencedIds, contentId), - Filter.Eq(x => x.IndexedAppId, appId), - Filter.Ne(x => x.IsDeleted, true), - Filter.Ne(x => x.Id, contentId)); + public async Task CheckExistsAsync(DomainId appId, DomainId contentId, + CancellationToken ct) + { + var filter = + Filter.And( + Filter.AnyEq(x => x.ReferencedIds, contentId), + Filter.Eq(x => x.IndexedAppId, appId), + Filter.Ne(x => x.IsDeleted, true), + Filter.Ne(x => x.Id, contentId)); - var hasReferrerAsync = - await Collection.Find(filter).Only(x => x.Id) - .AnyAsync(ct); + var hasReferrerAsync = + await Collection.Find(filter).Only(x => x.Id) + .AnyAsync(ct); - return hasReferrerAsync; - } + return hasReferrerAsync; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs index ab764edc89..3d7c1d2762 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs @@ -15,49 +15,48 @@ #pragma warning disable MA0073 // Avoid comparison with bool constant -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; + +internal sealed class QueryScheduled : OperationBase { - internal sealed class QueryScheduled : OperationBase + public override IEnumerable> CreateIndexes() + { + yield return new CreateIndexModel(Index + .Ascending(x => x.ScheduledAt) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IndexedSchemaId)); + } + + public async Task> QueryAsync(IAppEntity app, List schemas, Q q, + CancellationToken ct) + { + var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.ScheduledFrom!.Value, q.ScheduledTo!.Value); + + var contentEntities = await Collection.Find(filter).Limit(100).ToListAsync(ct); + var contentTotal = (long)contentEntities.Count; + + return ResultList.Create(contentTotal, contentEntities); + } + + public IAsyncEnumerable QueryAsync(Instant now, + CancellationToken ct) + { + var find = Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true).Not(x => x.Data); + + return find.ToAsyncEnumerable(ct); + } + + private static FilterDefinition CreateFilter(DomainId appId, + IEnumerable schemaIds, + Instant scheduledFrom, + Instant scheduledTo) { - public override IEnumerable> CreateIndexes() - { - yield return new CreateIndexModel(Index - .Ascending(x => x.ScheduledAt) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IndexedSchemaId)); - } - - public async Task> QueryAsync(IAppEntity app, List schemas, Q q, - CancellationToken ct) - { - var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.ScheduledFrom!.Value, q.ScheduledTo!.Value); - - var contentEntities = await Collection.Find(filter).Limit(100).ToListAsync(ct); - var contentTotal = (long)contentEntities.Count; - - return ResultList.Create(contentTotal, contentEntities); - } - - public IAsyncEnumerable QueryAsync(Instant now, - CancellationToken ct) - { - var find = Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true).Not(x => x.Data); - - return find.ToAsyncEnumerable(ct); - } - - private static FilterDefinition CreateFilter(DomainId appId, - IEnumerable schemaIds, - Instant scheduledFrom, - Instant scheduledTo) - { - return Filter.And( - Filter.Gte(x => x.ScheduledAt, scheduledFrom), - Filter.Lte(x => x.ScheduledAt, scheduledTo), - Filter.Ne(x => x.IsDeleted, true), - Filter.Eq(x => x.IndexedAppId, appId), - Filter.In(x => x.IndexedSchemaId, schemaIds)); - } + return Filter.And( + Filter.Gte(x => x.ScheduledAt, scheduledFrom), + Filter.Lte(x => x.ScheduledAt, scheduledTo), + Filter.Ne(x => x.IsDeleted, true), + Filter.Eq(x => x.IndexedAppId, appId), + Filter.In(x => x.IndexedSchemaId, schemaIds)); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs index a1236dc9a7..1fc9f2cfd6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs @@ -13,87 +13,86 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.History +namespace Squidex.Domain.Apps.Entities.MongoDb.History; + +public sealed class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository, IDeleter { - public sealed class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository, IDeleter + static MongoHistoryEventRepository() { - static MongoHistoryEventRepository() + BsonClassMap.RegisterClassMap(cm => { - BsonClassMap.RegisterClassMap(cm => - { - cm.AutoMap(); - - cm.MapProperty(x => x.OwnerId) - .SetElementName("AppId"); + cm.AutoMap(); - cm.MapProperty(x => x.EventType) - .SetElementName("Message"); - }); - } + cm.MapProperty(x => x.OwnerId) + .SetElementName("AppId"); - public MongoHistoryEventRepository(IMongoDatabase database) - : base(database) - { - } + cm.MapProperty(x => x.EventType) + .SetElementName("Message"); + }); + } - protected override string CollectionName() - { - return "Projections_History"; - } + public MongoHistoryEventRepository(IMongoDatabase database) + : base(database) + { + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.OwnerId) - .Ascending(x => x.Channel) - .Descending(x => x.Created) - .Descending(x => x.Version)), - new CreateIndexModel( - Index - .Ascending(x => x.OwnerId) - .Descending(x => x.Created) - .Descending(x => x.Version)) - }, ct); - } + protected override string CollectionName() + { + return "Projections_History"; + } - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - await Collection.DeleteManyAsync(Filter.Eq(x => x.OwnerId, app.Id), ct); - } + new CreateIndexModel( + Index + .Ascending(x => x.OwnerId) + .Ascending(x => x.Channel) + .Descending(x => x.Created) + .Descending(x => x.Version)), + new CreateIndexModel( + Index + .Ascending(x => x.OwnerId) + .Descending(x => x.Created) + .Descending(x => x.Version)) + }, ct); + } - public async Task> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, - CancellationToken ct = default) - { - var find = - !string.IsNullOrWhiteSpace(channelPrefix) ? - Collection.Find(x => x.OwnerId == ownerId && x.Channel == channelPrefix) : - Collection.Find(x => x.OwnerId == ownerId); + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.OwnerId, app.Id), ct); + } - return await find.SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(ct); - } + public async Task> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, + CancellationToken ct = default) + { + var find = + !string.IsNullOrWhiteSpace(channelPrefix) ? + Collection.Find(x => x.OwnerId == ownerId && x.Channel == channelPrefix) : + Collection.Find(x => x.OwnerId == ownerId); - public Task InsertManyAsync(IEnumerable historyEvents, - CancellationToken ct = default) - { - var writes = historyEvents - .Select(x => - new ReplaceOneModel(Filter.Eq(y => y.Id, x.Id), x) - { - IsUpsert = true - }) - .ToList(); + return await find.SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(ct); + } - if (writes.Count == 0) - { - return Task.CompletedTask; - } + public Task InsertManyAsync(IEnumerable historyEvents, + CancellationToken ct = default) + { + var writes = historyEvents + .Select(x => + new ReplaceOneModel(Filter.Eq(y => y.Id, x.Id), x) + { + IsUpsert = true + }) + .ToList(); - return Collection.BulkWriteAsync(writes, BulkUnordered, ct); + if (writes.Count == 0) + { + return Task.CompletedTask; } + + return Collection.BulkWriteAsync(writes, BulkUnordered, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs index 8d2d381006..d0dc0c5446 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs @@ -10,77 +10,76 @@ using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Tasks; -namespace Squidex.Domain.Apps.Entities.MongoDb +namespace Squidex.Domain.Apps.Entities.MongoDb; + +internal sealed class MongoCountCollection : MongoRepositoryBase { - internal sealed class MongoCountCollection : MongoRepositoryBase + private readonly string name; + + public MongoCountCollection(IMongoDatabase database, string name) + : base(database) { - private readonly string name; + this.name = $"{name}_Count"; + } - public MongoCountCollection(IMongoDatabase database, string name) - : base(database) - { - this.name = $"{name}_Count"; - } + protected override string CollectionName() + { + return name; + } - protected override string CollectionName() - { - return name; - } + public async Task GetOrAddAsync(string key, Func> provider, + CancellationToken ct) + { + var (cachedTotal, isOutdated) = await CountAsync(key, ct); - public async Task GetOrAddAsync(string key, Func> provider, - CancellationToken ct) + // This is our hardcoded limit at the moment. Usually schemas are much smaller anyway. + if (cachedTotal < 5_000) { - var (cachedTotal, isOutdated) = await CountAsync(key, ct); - - // This is our hardcoded limit at the moment. Usually schemas are much smaller anyway. - if (cachedTotal < 5_000) - { - // We always want to have up to date collection sizes for smaller schemas. - return await RefreshTotalAsync(key, cachedTotal, provider, ct); - } - - if (isOutdated) - { - // If we have a loot of items, the query might be slow and therefore we execute it in the background. - RefreshTotalAsync(key, cachedTotal, provider, ct).Forget(); - } - - return cachedTotal; + // We always want to have up to date collection sizes for smaller schemas. + return await RefreshTotalAsync(key, cachedTotal, provider, ct); } - private async Task RefreshTotalAsync(string key, long cachedCount, Func> provider, - CancellationToken ct) + if (isOutdated) { - var actualCount = await provider(ct); + // If we have a loot of items, the query might be slow and therefore we execute it in the background. + RefreshTotalAsync(key, cachedTotal, provider, ct).Forget(); + } - if (actualCount != cachedCount) - { - var now = SystemClock.Instance.GetCurrentInstant(); + return cachedTotal; + } - await Collection.UpdateOneAsync(x => x.Key == key, - Update - .Set(x => x.Key, key) - .SetOnInsert(x => x.Count, actualCount) - .SetOnInsert(x => x.Created, now), - Upsert, ct); - } + private async Task RefreshTotalAsync(string key, long cachedCount, Func> provider, + CancellationToken ct) + { + var actualCount = await provider(ct); - return actualCount; + if (actualCount != cachedCount) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + await Collection.UpdateOneAsync(x => x.Key == key, + Update + .Set(x => x.Key, key) + .SetOnInsert(x => x.Count, actualCount) + .SetOnInsert(x => x.Created, now), + Upsert, ct); } - private async Task<(long, bool)> CountAsync(string key, - CancellationToken ct) - { - var entity = await Collection.Find(x => x.Key == key).FirstOrDefaultAsync(ct); + return actualCount; + } - if (entity != null) - { - var now = SystemClock.Instance.GetCurrentInstant(); + private async Task<(long, bool)> CountAsync(string key, + CancellationToken ct) + { + var entity = await Collection.Find(x => x.Key == key).FirstOrDefaultAsync(ct); - return (entity.Count, now - entity.Created > Duration.FromSeconds(10)); - } + if (entity != null) + { + var now = SystemClock.Instance.GetCurrentInstant(); - return (0, true); + return (entity.Count, now - entity.Created > Duration.FromSeconds(10)); } + + return (0, true); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountEntity.cs index 0cf5a98993..ff9cf2b5a5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountEntity.cs @@ -8,20 +8,19 @@ using MongoDB.Bson.Serialization.Attributes; using NodaTime; -namespace Squidex.Domain.Apps.Entities.MongoDb +namespace Squidex.Domain.Apps.Entities.MongoDb; + +internal sealed class MongoCountEntity { - internal sealed class MongoCountEntity - { - [BsonId] - [BsonElement("_id")] - public string Key { get; set; } + [BsonId] + [BsonElement("_id")] + public string Key { get; set; } - [BsonRequired] - [BsonElement(nameof(Count))] - public long Count { get; set; } + [BsonRequired] + [BsonElement(nameof(Count))] + public long Count { get; set; } - [BsonRequired] - [BsonElement(nameof(Created))] - public Instant Created { get; set; } - } + [BsonRequired] + [BsonElement(nameof(Created))] + public Instant Created { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs index d6cd8f1a5e..2c8c0c7b62 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs @@ -10,27 +10,26 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules; + +public sealed class MongoRuleEntity : MongoState { - public sealed class MongoRuleEntity : MongoState - { - [BsonRequired] - [BsonElement("_ai")] - public DomainId IndexedAppId { get; set; } + [BsonRequired] + [BsonElement("_ai")] + public DomainId IndexedAppId { get; set; } - [BsonRequired] - [BsonElement("_ri")] - public DomainId IndexedId { get; set; } + [BsonRequired] + [BsonElement("_ri")] + public DomainId IndexedId { get; set; } - [BsonRequired] - [BsonElement("_dl")] - public bool IndexedDeleted { get; set; } + [BsonRequired] + [BsonElement("_dl")] + public bool IndexedDeleted { get; set; } - public override void Prepare() - { - IndexedAppId = Document.AppId.Id; - IndexedDeleted = Document.IsDeleted; - IndexedId = Document.Id; - } + public override void Prepare() + { + IndexedAppId = Document.AppId.Id; + IndexedDeleted = Document.IsDeleted; + IndexedId = Document.Id; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs index ab5669e02c..c69da41674 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs @@ -15,83 +15,82 @@ using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules; + +public sealed class MongoRuleEventEntity : IRuleEventEntity { - public sealed class MongoRuleEventEntity : IRuleEventEntity + [BsonId] + [BsonElement("_id")] + public DomainId JobId { get; set; } + + [BsonRequired] + [BsonElement(nameof(AppId))] + public DomainId AppId { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement(nameof(RuleId))] + public DomainId RuleId { get; set; } + + [BsonRequired] + [BsonElement(nameof(Created))] + public Instant Created { get; set; } + + [BsonRequired] + [BsonElement(nameof(LastModified))] + public Instant LastModified { get; set; } + + [BsonRequired] + [BsonElement(nameof(Result))] + [BsonRepresentation(BsonType.String)] + public RuleResult Result { get; set; } + + [BsonRequired] + [BsonElement(nameof(JobResult))] + [BsonRepresentation(BsonType.String)] + public RuleJobResult JobResult { get; set; } + + [BsonRequired] + [BsonElement(nameof(Job))] + [BsonJson] + public RuleJob Job { get; set; } + + [BsonRequired] + [BsonElement(nameof(LastDump))] + public string? LastDump { get; set; } + + [BsonRequired] + [BsonElement(nameof(NumCalls))] + public int NumCalls { get; set; } + + [BsonRequired] + [BsonElement(nameof(Expires))] + public Instant Expires { get; set; } + + [BsonRequired] + [BsonElement(nameof(NextAttempt))] + public Instant? NextAttempt { get; set; } + + DomainId IWithId.Id { - [BsonId] - [BsonElement("_id")] - public DomainId JobId { get; set; } - - [BsonRequired] - [BsonElement(nameof(AppId))] - public DomainId AppId { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement(nameof(RuleId))] - public DomainId RuleId { get; set; } - - [BsonRequired] - [BsonElement(nameof(Created))] - public Instant Created { get; set; } - - [BsonRequired] - [BsonElement(nameof(LastModified))] - public Instant LastModified { get; set; } - - [BsonRequired] - [BsonElement(nameof(Result))] - [BsonRepresentation(BsonType.String)] - public RuleResult Result { get; set; } - - [BsonRequired] - [BsonElement(nameof(JobResult))] - [BsonRepresentation(BsonType.String)] - public RuleJobResult JobResult { get; set; } - - [BsonRequired] - [BsonElement(nameof(Job))] - [BsonJson] - public RuleJob Job { get; set; } - - [BsonRequired] - [BsonElement(nameof(LastDump))] - public string? LastDump { get; set; } - - [BsonRequired] - [BsonElement(nameof(NumCalls))] - public int NumCalls { get; set; } - - [BsonRequired] - [BsonElement(nameof(Expires))] - public Instant Expires { get; set; } - - [BsonRequired] - [BsonElement(nameof(NextAttempt))] - public Instant? NextAttempt { get; set; } - - DomainId IWithId.Id - { - get => JobId; - } + get => JobId; + } - DomainId IEntity.UniqueId - { - get => JobId; - } + DomainId IEntity.UniqueId + { + get => JobId; + } - public static MongoRuleEventEntity FromJob(RuleJob job, Instant? nextAttempt) + public static MongoRuleEventEntity FromJob(RuleJob job, Instant? nextAttempt) + { + var entity = new MongoRuleEventEntity { - var entity = new MongoRuleEventEntity - { - Job = job, - JobId = job.Id, - NextAttempt = nextAttempt - }; + Job = job, + JobId = job.Id, + NextAttempt = nextAttempt + }; - SimpleMapper.Map(job, entity); + SimpleMapper.Map(job, entity); - return entity; - } + return entity; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs index aabda00b3f..eee64024b6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs @@ -15,176 +15,175 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules; + +public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository, IDeleter { - public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository, IDeleter - { - private readonly MongoRuleStatisticsCollection statisticsCollection; + private readonly MongoRuleStatisticsCollection statisticsCollection; - public MongoRuleEventRepository(IMongoDatabase database) - : base(database) - { - statisticsCollection = new MongoRuleStatisticsCollection(database); - } + public MongoRuleEventRepository(IMongoDatabase database) + : base(database) + { + statisticsCollection = new MongoRuleStatisticsCollection(database); + } - protected override string CollectionName() - { - return "RuleEvents"; - } + protected override string CollectionName() + { + return "RuleEvents"; + } - protected override async Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) - { - await statisticsCollection.InitializeAsync(ct); - - await collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index.Ascending(x => x.NextAttempt)), - - new CreateIndexModel( - Index.Ascending(x => x.AppId).Descending(x => x.Created)), - - new CreateIndexModel( - Index - .Ascending(x => x.Expires), - new CreateIndexOptions - { - ExpireAfter = TimeSpan.Zero - }) - }, ct); - } + protected override async Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + await statisticsCollection.InitializeAsync(ct); - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) + await collection.Indexes.CreateManyAsync(new[] { - await statisticsCollection.DeleteAppAsync(app.Id, ct); + new CreateIndexModel( + Index.Ascending(x => x.NextAttempt)), - await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); - } - - public Task QueryPendingAsync(Instant now, Func callback, - CancellationToken ct = default) - { - return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct); - } + new CreateIndexModel( + Index.Ascending(x => x.AppId).Descending(x => x.Created)), - public async Task> QueryByAppAsync(DomainId appId, DomainId? ruleId = null, int skip = 0, int take = 20, - CancellationToken ct = default) - { - var filter = Filter.Eq(x => x.AppId, appId); + new CreateIndexModel( + Index + .Ascending(x => x.Expires), + new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero + }) + }, ct); + } - if (ruleId != null && ruleId.Value != DomainId.Empty) - { - filter = Filter.And(filter, Filter.Eq(x => x.RuleId, ruleId.Value)); - } + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await statisticsCollection.DeleteAppAsync(app.Id, ct); - var ruleEventEntities = await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.Created).ToListAsync(ct); - var ruleEventTotal = (long)ruleEventEntities.Count; + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } - if (ruleEventTotal >= take || skip > 0) - { - ruleEventTotal = await Collection.Find(filter).CountDocumentsAsync(ct); - } + public Task QueryPendingAsync(Instant now, Func callback, + CancellationToken ct = default) + { + return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct); + } - return ResultList.Create(ruleEventTotal, ruleEventEntities); - } + public async Task> QueryByAppAsync(DomainId appId, DomainId? ruleId = null, int skip = 0, int take = 20, + CancellationToken ct = default) + { + var filter = Filter.Eq(x => x.AppId, appId); - public async Task FindAsync(DomainId id, - CancellationToken ct = default) + if (ruleId != null && ruleId.Value != DomainId.Empty) { - var ruleEvent = - await Collection.Find(x => x.JobId == id) - .FirstOrDefaultAsync(ct); - - return ruleEvent; + filter = Filter.And(filter, Filter.Eq(x => x.RuleId, ruleId.Value)); } - public Task EnqueueAsync(DomainId id, Instant nextAttempt, - CancellationToken ct = default) + var ruleEventEntities = await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.Created).ToListAsync(ct); + var ruleEventTotal = (long)ruleEventEntities.Count; + + if (ruleEventTotal >= take || skip > 0) { - return Collection.UpdateOneAsync(x => x.JobId == id, Update.Set(x => x.NextAttempt, nextAttempt), cancellationToken: ct); + ruleEventTotal = await Collection.Find(filter).CountDocumentsAsync(ct); } - public async Task EnqueueAsync(RuleJob job, Instant? nextAttempt, - CancellationToken ct = default) - { - var entity = MongoRuleEventEntity.FromJob(job, nextAttempt); + return ResultList.Create(ruleEventTotal, ruleEventEntities); + } - await Collection.InsertOneIfNotExistsAsync(entity, ct); - } + public async Task FindAsync(DomainId id, + CancellationToken ct = default) + { + var ruleEvent = + await Collection.Find(x => x.JobId == id) + .FirstOrDefaultAsync(ct); - public Task CancelByEventAsync(DomainId id, - CancellationToken ct = default) - { - return Collection.UpdateOneAsync(x => x.JobId == id, - Update - .Set(x => x.NextAttempt, null) - .Set(x => x.JobResult, RuleJobResult.Cancelled), - cancellationToken: ct); - } + return ruleEvent; + } - public Task CancelByRuleAsync(DomainId ruleId, - CancellationToken ct = default) - { - return Collection.UpdateManyAsync(x => x.RuleId == ruleId, - Update - .Set(x => x.NextAttempt, null) - .Set(x => x.JobResult, RuleJobResult.Cancelled), - cancellationToken: ct); - } + public Task EnqueueAsync(DomainId id, Instant nextAttempt, + CancellationToken ct = default) + { + return Collection.UpdateOneAsync(x => x.JobId == id, Update.Set(x => x.NextAttempt, nextAttempt), cancellationToken: ct); + } - public Task CancelByAppAsync(DomainId appId, - CancellationToken ct = default) - { - return Collection.UpdateManyAsync(x => x.AppId == appId, - Update - .Set(x => x.NextAttempt, null) - .Set(x => x.JobResult, RuleJobResult.Cancelled), - cancellationToken: ct); - } + public async Task EnqueueAsync(RuleJob job, Instant? nextAttempt, + CancellationToken ct = default) + { + var entity = MongoRuleEventEntity.FromJob(job, nextAttempt); - public Task UpdateAsync(RuleJob job, RuleJobUpdate update, - CancellationToken ct = default) - { - Guard.NotNull(job); - Guard.NotNull(update); + await Collection.InsertOneIfNotExistsAsync(entity, ct); + } - return Task.WhenAll( - UpdateStatisticsAsync(job, update, ct), - UpdateEventAsync(job, update, ct)); - } + public Task CancelByEventAsync(DomainId id, + CancellationToken ct = default) + { + return Collection.UpdateOneAsync(x => x.JobId == id, + Update + .Set(x => x.NextAttempt, null) + .Set(x => x.JobResult, RuleJobResult.Cancelled), + cancellationToken: ct); + } - private Task UpdateEventAsync(RuleJob job, RuleJobUpdate update, - CancellationToken ct = default) - { - return Collection.UpdateOneAsync(x => x.JobId == job.Id, - Update - .Set(x => x.Result, update.ExecutionResult) - .Set(x => x.LastDump, update.ExecutionDump) - .Set(x => x.JobResult, update.JobResult) - .Set(x => x.NextAttempt, update.JobNext) - .Inc(x => x.NumCalls, 1), - cancellationToken: ct); - } + public Task CancelByRuleAsync(DomainId ruleId, + CancellationToken ct = default) + { + return Collection.UpdateManyAsync(x => x.RuleId == ruleId, + Update + .Set(x => x.NextAttempt, null) + .Set(x => x.JobResult, RuleJobResult.Cancelled), + cancellationToken: ct); + } + + public Task CancelByAppAsync(DomainId appId, + CancellationToken ct = default) + { + return Collection.UpdateManyAsync(x => x.AppId == appId, + Update + .Set(x => x.NextAttempt, null) + .Set(x => x.JobResult, RuleJobResult.Cancelled), + cancellationToken: ct); + } + + public Task UpdateAsync(RuleJob job, RuleJobUpdate update, + CancellationToken ct = default) + { + Guard.NotNull(job); + Guard.NotNull(update); + + return Task.WhenAll( + UpdateStatisticsAsync(job, update, ct), + UpdateEventAsync(job, update, ct)); + } - private async Task UpdateStatisticsAsync(RuleJob job, RuleJobUpdate update, - CancellationToken ct = default) + private Task UpdateEventAsync(RuleJob job, RuleJobUpdate update, + CancellationToken ct = default) + { + return Collection.UpdateOneAsync(x => x.JobId == job.Id, + Update + .Set(x => x.Result, update.ExecutionResult) + .Set(x => x.LastDump, update.ExecutionDump) + .Set(x => x.JobResult, update.JobResult) + .Set(x => x.NextAttempt, update.JobNext) + .Inc(x => x.NumCalls, 1), + cancellationToken: ct); + } + + private async Task UpdateStatisticsAsync(RuleJob job, RuleJobUpdate update, + CancellationToken ct = default) + { + if (update.ExecutionResult == RuleResult.Success) { - if (update.ExecutionResult == RuleResult.Success) - { - await statisticsCollection.IncrementSuccessAsync(job.AppId, job.RuleId, update.Finished, ct); - } - else - { - await statisticsCollection.IncrementFailedAsync(job.AppId, job.RuleId, update.Finished, ct); - } + await statisticsCollection.IncrementSuccessAsync(job.AppId, job.RuleId, update.Finished, ct); } - - public Task> QueryStatisticsByAppAsync(DomainId appId, - CancellationToken ct = default) + else { - return statisticsCollection.QueryByAppAsync(appId, ct); + await statisticsCollection.IncrementFailedAsync(job.AppId, job.RuleId, update.Finished, ct); } } + + public Task> QueryStatisticsByAppAsync(DomainId appId, + CancellationToken ct = default) + { + return statisticsCollection.QueryByAppAsync(appId, ct); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index 9fa28e72e9..dca4b4eaaa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -13,43 +13,42 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules; + +public sealed class MongoRuleRepository : MongoSnapshotStoreBase, IRuleRepository, IDeleter { - public sealed class MongoRuleRepository : MongoSnapshotStoreBase, IRuleRepository, IDeleter + public MongoRuleRepository(IMongoDatabase database) + : base(database) { - public MongoRuleRepository(IMongoDatabase database) - : base(database) - { - } + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId)) - }, ct); - } + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId)) + }, ct); + } - Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); - } + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); + } - public async Task> QueryAllAsync(DomainId appId, - CancellationToken ct = default) + public async Task> QueryAllAsync(DomainId appId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoRuleRepository/QueryAllAsync")) { - using (Telemetry.Activities.StartActivity("MongoRuleRepository/QueryAllAsync")) - { - var entities = - await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted) - .ToListAsync(ct); + var entities = + await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted) + .ToListAsync(ct); - return entities.Select(x => (IRuleEntity)x.Document).ToList(); - } + return entities.Select(x => (IRuleEntity)x.Document).ToList(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs index 1e43fbec9e..27beb92c4a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs @@ -12,79 +12,78 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules; + +public sealed class MongoRuleStatisticsCollection : MongoRepositoryBase { - public sealed class MongoRuleStatisticsCollection : MongoRepositoryBase + static MongoRuleStatisticsCollection() { - static MongoRuleStatisticsCollection() + BsonClassMap.RegisterClassMap(cm => { - BsonClassMap.RegisterClassMap(cm => - { - cm.AutoMap(); + cm.AutoMap(); - cm.SetIgnoreExtraElements(true); - }); - } + cm.SetIgnoreExtraElements(true); + }); + } - public MongoRuleStatisticsCollection(IMongoDatabase database) - : base(database) - { - } + public MongoRuleStatisticsCollection(IMongoDatabase database) + : base(database) + { + } - protected override string CollectionName() - { - return "RuleStatistics"; - } + protected override string CollectionName() + { + return "RuleStatistics"; + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) - { - return collection.Indexes.CreateOneAsync( - new CreateIndexModel( - Index - .Ascending(x => x.AppId) - .Ascending(x => x.RuleId)), - cancellationToken: ct); - } + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Index + .Ascending(x => x.AppId) + .Ascending(x => x.RuleId)), + cancellationToken: ct); + } - public async Task DeleteAppAsync(DomainId appId, - CancellationToken ct) - { - await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, appId), ct); - } + public async Task DeleteAppAsync(DomainId appId, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, appId), ct); + } - public async Task> QueryByAppAsync(DomainId appId, - CancellationToken ct) - { - var statistics = await Collection.Find(x => x.AppId == appId).ToListAsync(ct); + public async Task> QueryByAppAsync(DomainId appId, + CancellationToken ct) + { + var statistics = await Collection.Find(x => x.AppId == appId).ToListAsync(ct); - return statistics; - } + return statistics; + } - public Task IncrementSuccessAsync(DomainId appId, DomainId ruleId, Instant now, - CancellationToken ct) - { - return Collection.UpdateOneAsync( - x => x.AppId == appId && x.RuleId == ruleId, - Update - .Inc(x => x.NumSucceeded, 1) - .Set(x => x.LastExecuted, now) - .SetOnInsert(x => x.AppId, appId) - .SetOnInsert(x => x.RuleId, ruleId), - Upsert, ct); - } + public Task IncrementSuccessAsync(DomainId appId, DomainId ruleId, Instant now, + CancellationToken ct) + { + return Collection.UpdateOneAsync( + x => x.AppId == appId && x.RuleId == ruleId, + Update + .Inc(x => x.NumSucceeded, 1) + .Set(x => x.LastExecuted, now) + .SetOnInsert(x => x.AppId, appId) + .SetOnInsert(x => x.RuleId, ruleId), + Upsert, ct); + } - public Task IncrementFailedAsync(DomainId appId, DomainId ruleId, Instant now, - CancellationToken ct) - { - return Collection.UpdateOneAsync( - x => x.AppId == appId && x.RuleId == ruleId, - Update - .Inc(x => x.NumFailed, 1) - .Set(x => x.LastExecuted, now) - .SetOnInsert(x => x.AppId, appId) - .SetOnInsert(x => x.RuleId, ruleId), - Upsert, ct); - } + public Task IncrementFailedAsync(DomainId appId, DomainId ruleId, Instant now, + CancellationToken ct) + { + return Collection.UpdateOneAsync( + x => x.AppId == appId && x.RuleId == ruleId, + Update + .Inc(x => x.NumFailed, 1) + .Set(x => x.LastExecuted, now) + .SetOnInsert(x => x.AppId, appId) + .SetOnInsert(x => x.RuleId, ruleId), + Upsert, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs index 082bda68d7..21cd2664e4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs @@ -11,37 +11,36 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas; + +public sealed class MongoSchemaEntity : MongoState { - public sealed class MongoSchemaEntity : MongoState + [BsonRequired] + [BsonElement("_ai")] + public DomainId IndexedAppId { get; set; } + + [BsonRequired] + [BsonElement("_si")] + public DomainId IndexedId { get; set; } + + [BsonRequired] + [BsonElement("_sn")] + public string IndexedName { get; set; } + + [BsonRequired] + [BsonElement("_dl")] + public bool IndexedDeleted { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement("_ct")] + public Instant IndexedCreated { get; set; } + + public override void Prepare() { - [BsonRequired] - [BsonElement("_ai")] - public DomainId IndexedAppId { get; set; } - - [BsonRequired] - [BsonElement("_si")] - public DomainId IndexedId { get; set; } - - [BsonRequired] - [BsonElement("_sn")] - public string IndexedName { get; set; } - - [BsonRequired] - [BsonElement("_dl")] - public bool IndexedDeleted { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement("_ct")] - public Instant IndexedCreated { get; set; } - - public override void Prepare() - { - IndexedAppId = Document.AppId.Id; - IndexedDeleted = Document.IsDeleted; - IndexedId = Document.Id; - IndexedName = Document.SchemaDef.Name; - IndexedCreated = Document.Created; - } + IndexedAppId = Document.AppId.Id; + IndexedDeleted = Document.IsDeleted; + IndexedId = Document.Id; + IndexedName = Document.SchemaDef.Name; + IndexedCreated = Document.Created; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index 8a217217b1..a82c472f6a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -13,69 +13,68 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas; + +public sealed class MongoSchemaRepository : MongoSnapshotStoreBase, ISchemaRepository, IDeleter { - public sealed class MongoSchemaRepository : MongoSnapshotStoreBase, ISchemaRepository, IDeleter + public MongoSchemaRepository(IMongoDatabase database) + : base(database) { - public MongoSchemaRepository(IMongoDatabase database) - : base(database) - { - } + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IndexedName)) - }, ct); - } + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IndexedName)) + }, ct); + } - Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); - } + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); + } - public async Task> QueryAllAsync(DomainId appId, CancellationToken ct = default) + public async Task> QueryAllAsync(DomainId appId, CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSchemaRepository/QueryAllAsync")) { - using (Telemetry.Activities.StartActivity("MongoSchemaRepository/QueryAllAsync")) - { - var entities = - await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted) - .ToListAsync(ct); + var entities = + await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted) + .ToListAsync(ct); - return entities.Select(x => (ISchemaEntity)x.Document).ToList(); - } + return entities.Select(x => (ISchemaEntity)x.Document).ToList(); } + } - public async Task FindAsync(DomainId appId, DomainId id, - CancellationToken ct = default) + public async Task FindAsync(DomainId appId, DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSchemaRepository/FindAsync")) { - using (Telemetry.Activities.StartActivity("MongoSchemaRepository/FindAsync")) - { - var entity = - await Collection.Find(x => x.IndexedAppId == appId && x.IndexedId == id && !x.IndexedDeleted) - .FirstOrDefaultAsync(ct); + var entity = + await Collection.Find(x => x.IndexedAppId == appId && x.IndexedId == id && !x.IndexedDeleted) + .FirstOrDefaultAsync(ct); - return entity?.Document; - } + return entity?.Document; } + } - public async Task FindAsync(DomainId appId, string name, - CancellationToken ct = default) + public async Task FindAsync(DomainId appId, string name, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSchemaRepository/FindAsyncByName")) { - using (Telemetry.Activities.StartActivity("MongoSchemaRepository/FindAsyncByName")) - { - var entity = - await Collection.Find(x => x.IndexedAppId == appId && x.IndexedName == name && !x.IndexedDeleted) - .FirstOrDefaultAsync(ct); + var entity = + await Collection.Find(x => x.IndexedAppId == appId && x.IndexedName == name && !x.IndexedDeleted) + .FirstOrDefaultAsync(ct); - return entity?.Document; - } + return entity?.Document; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs index 3918c92414..85fe766d89 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs @@ -15,130 +15,129 @@ using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.ObjectPool; -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas; + +public sealed class MongoSchemasHash : MongoRepositoryBase, ISchemasHash, IEventConsumer, IDeleter { - public sealed class MongoSchemasHash : MongoRepositoryBase, ISchemasHash, IEventConsumer, IDeleter + public int BatchSize { - public int BatchSize - { - get => 1000; - } + get => 1000; + } - public int BatchDelay - { - get => 500; - } + public int BatchDelay + { + get => 500; + } - public string Name - { - get => GetType().Name; - } + public string Name + { + get => GetType().Name; + } - public string EventsFilter - { - get => "^schema-"; - } + public string EventsFilter + { + get => "^schema-"; + } - public MongoSchemasHash(IMongoDatabase database) - : base(database) - { - } + public MongoSchemasHash(IMongoDatabase database) + : base(database) + { + } - protected override string CollectionName() - { - return "SchemasHash"; - } + protected override string CollectionName() + { + return "SchemasHash"; + } - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); - } + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } - public Task On(IEnumerable> events) - { - var writes = new List>(); + public Task On(IEnumerable> events) + { + var writes = new List>(); - foreach (var @event in events) + foreach (var @event in events) + { + if (@event.Headers.Restored()) { - if (@event.Headers.Restored()) - { - continue; - } - - if (@event.Payload is SchemaEvent schemaEvent) - { - writes.Add( - new UpdateOneModel( - Filter.Eq(x => x.AppId, schemaEvent.AppId.Id), - Update - .Set($"s.{schemaEvent.SchemaId.Id}", @event.Headers.EventStreamNumber()) - .Set(x => x.Updated, @event.Headers.Timestamp())) - { - IsUpsert = true - }); - } + continue; } - if (writes.Count == 0) + if (@event.Payload is SchemaEvent schemaEvent) { - return Task.CompletedTask; + writes.Add( + new UpdateOneModel( + Filter.Eq(x => x.AppId, schemaEvent.AppId.Id), + Update + .Set($"s.{schemaEvent.SchemaId.Id}", @event.Headers.EventStreamNumber()) + .Set(x => x.Updated, @event.Headers.Timestamp())) + { + IsUpsert = true + }); } - - return Collection.BulkWriteAsync(writes, BulkUnordered); } - public async Task<(Instant Create, string Hash)> GetCurrentHashAsync(IAppEntity app, - CancellationToken ct = default) + if (writes.Count == 0) { - Guard.NotNull(app); - - var entity = await Collection.Find(x => x.AppId == app.Id).FirstOrDefaultAsync(ct); + return Task.CompletedTask; + } - if (entity == null) - { - return (default, string.Empty); - } + return Collection.BulkWriteAsync(writes, BulkUnordered); + } - var ids = - entity.SchemaVersions.Select(x => (x.Key, x.Value)) - .Union(Enumerable.Repeat((app.Id.ToString(), app.Version), 1)); + public async Task<(Instant Create, string Hash)> GetCurrentHashAsync(IAppEntity app, + CancellationToken ct = default) + { + Guard.NotNull(app); - var hash = CreateHash(ids); + var entity = await Collection.Find(x => x.AppId == app.Id).FirstOrDefaultAsync(ct); - return (entity.Updated, hash); + if (entity == null) + { + return (default, string.Empty); } - public ValueTask ComputeHashAsync(IAppEntity app, IEnumerable schemas, - CancellationToken ct = default) - { - var ids = - schemas.Select(x => (x.Id.ToString(), x.Version)) - .Union(Enumerable.Repeat((app.Id.ToString(), app.Version), 1)); + var ids = + entity.SchemaVersions.Select(x => (x.Key, x.Value)) + .Union(Enumerable.Repeat((app.Id.ToString(), app.Version), 1)); - var hash = CreateHash(ids); + var hash = CreateHash(ids); - return new ValueTask(hash); - } + return (entity.Updated, hash); + } + + public ValueTask ComputeHashAsync(IAppEntity app, IEnumerable schemas, + CancellationToken ct = default) + { + var ids = + schemas.Select(x => (x.Id.ToString(), x.Version)) + .Union(Enumerable.Repeat((app.Id.ToString(), app.Version), 1)); + + var hash = CreateHash(ids); + + return new ValueTask(hash); + } - private static string CreateHash(IEnumerable<(string, long)> ids) + private static string CreateHash(IEnumerable<(string, long)> ids) + { + var sb = DefaultPools.StringBuilder.Get(); + try { - var sb = DefaultPools.StringBuilder.Get(); - try - { - foreach (var (id, version) in ids.OrderBy(x => x.Item1)) - { - sb.Append(id); - sb.Append(version); - sb.Append(';'); - } - - return sb.ToString().ToSha256Base64(); - } - finally + foreach (var (id, version) in ids.OrderBy(x => x.Item1)) { - DefaultPools.StringBuilder.Return(sb); + sb.Append(id); + sb.Append(version); + sb.Append(';'); } + + return sb.ToString().ToSha256Base64(); + } + finally + { + DefaultPools.StringBuilder.Return(sb); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs index 3b418fcb6f..4a48735e8c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs @@ -9,20 +9,19 @@ using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas; + +public sealed class MongoSchemasHashEntity { - public sealed class MongoSchemasHashEntity - { - [BsonId] - [BsonElement("_id")] - public DomainId AppId { get; set; } + [BsonId] + [BsonElement("_id")] + public DomainId AppId { get; set; } - [BsonRequired] - [BsonElement("s")] - public Dictionary SchemaVersions { get; set; } + [BsonRequired] + [BsonElement("s")] + public Dictionary SchemaVersions { get; set; } - [BsonRequired] - [BsonElement("t")] - public Instant Updated { get; set; } - } + [BsonRequired] + [BsonElement("t")] + public Instant Updated { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamEntity.cs index 157ad536a7..68fd906def 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamEntity.cs @@ -11,28 +11,27 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Teams +namespace Squidex.Domain.Apps.Entities.MongoDb.Teams; + +public sealed class MongoTeamEntity : MongoState { - public sealed class MongoTeamEntity : MongoState - { - [BsonRequired] - [BsonElement("_ui")] - public string[] IndexedUserIds { get; set; } + [BsonRequired] + [BsonElement("_ui")] + public string[] IndexedUserIds { get; set; } - [BsonIgnoreIfDefault] - [BsonElement("_ct")] - public Instant IndexedCreated { get; set; } + [BsonIgnoreIfDefault] + [BsonElement("_ct")] + public Instant IndexedCreated { get; set; } - public override void Prepare() + public override void Prepare() + { + var users = new HashSet { - var users = new HashSet - { - Document.CreatedBy.Identifier - }; + Document.CreatedBy.Identifier + }; - users.AddRange(Document.Contributors.Keys); + users.AddRange(Document.Contributors.Keys); - IndexedUserIds = users.ToArray(); - } + IndexedUserIds = users.ToArray(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamRepository.cs index 33d122942e..745aaa7fe0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Teams/MongoTeamRepository.cs @@ -12,50 +12,49 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.MongoDb.Teams +namespace Squidex.Domain.Apps.Entities.MongoDb.Teams; + +public sealed class MongoTeamRepository : MongoSnapshotStoreBase, ITeamRepository { - public sealed class MongoTeamRepository : MongoSnapshotStoreBase, ITeamRepository + public MongoTeamRepository(IMongoDatabase database) + : base(database) { - public MongoTeamRepository(IMongoDatabase database) - : base(database) - { - } + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedUserIds)) - }, ct); - } + new CreateIndexModel( + Index + .Ascending(x => x.IndexedUserIds)) + }, ct); + } - public async Task> QueryAllAsync(string contributorId, - CancellationToken ct = default) + public async Task> QueryAllAsync(string contributorId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoTeamRepository/QueryAllAsync")) { - using (Telemetry.Activities.StartActivity("MongoTeamRepository/QueryAllAsync")) - { - var entities = - await Collection.Find(x => x.IndexedUserIds.Contains(contributorId)) - .ToListAsync(ct); - - return entities.Select(x => (ITeamEntity)x.Document).ToList(); - } + var entities = + await Collection.Find(x => x.IndexedUserIds.Contains(contributorId)) + .ToListAsync(ct); + + return entities.Select(x => (ITeamEntity)x.Document).ToList(); } + } - public async Task FindAsync(DomainId id, - CancellationToken ct = default) + public async Task FindAsync(DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoTeamRepository/FindAsync")) { - using (Telemetry.Activities.StartActivity("MongoTeamRepository/FindAsync")) - { - var entity = - await Collection.Find(x => x.DocumentId == id) - .FirstOrDefaultAsync(ct); - - return entity?.Document; - } + var entity = + await Collection.Find(x => x.DocumentId == id) + .FirstOrDefaultAsync(ct); + + return entity?.Document; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasIndexDefinition.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasIndexDefinition.cs index 7c30ace89d..c6b59a7110 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasIndexDefinition.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasIndexDefinition.cs @@ -9,197 +9,196 @@ using System.Net.Http.Json; using Squidex.Hosting.Configuration; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public static class AtlasIndexDefinition { - public static class AtlasIndexDefinition + private static readonly Dictionary FieldPaths = new Dictionary(); + private static readonly Dictionary FieldAnalyzers = new Dictionary { - private static readonly Dictionary FieldPaths = new Dictionary(); - private static readonly Dictionary FieldAnalyzers = new Dictionary - { - ["iv"] = "lucene.standard", - ["ar"] = "lucene.arabic", - ["hy"] = "lucene.armenian", - ["eu"] = "lucene.basque", - ["bn"] = "lucene.bengali", - ["br"] = "lucene.brazilian", - ["bg"] = "lucene.bulgarian", - ["ca"] = "lucene.catalan", - ["ko"] = "lucene.cjk", - ["da"] = "lucene.danish", - ["nl"] = "lucene.dutch", - ["en"] = "lucene.english", - ["fi"] = "lucene.finnish", - ["fr"] = "lucene.french", - ["gl"] = "lucene.galician", - ["de"] = "lucene.german", - ["el"] = "lucene.greek", - ["hi"] = "lucene.hindi", - ["hu"] = "lucene.hungarian", - ["id"] = "lucene.indonesian", - ["ga"] = "lucene.irish", - ["it"] = "lucene.italian", - ["jp"] = "lucene.japanese", - ["lv"] = "lucene.latvian", - ["no"] = "lucene.norwegian", - ["fa"] = "lucene.persian", - ["pt"] = "lucene.portuguese", - ["ro"] = "lucene.romanian", - ["ru"] = "lucene.russian", - ["zh"] = "lucene.smartcn", - ["es"] = "lucene.spanish", - ["sv"] = "lucene.swedish", - ["th"] = "lucene.thai", - ["tr"] = "lucene.turkish", - ["uk"] = "lucene.ukrainian" - }; + ["iv"] = "lucene.standard", + ["ar"] = "lucene.arabic", + ["hy"] = "lucene.armenian", + ["eu"] = "lucene.basque", + ["bn"] = "lucene.bengali", + ["br"] = "lucene.brazilian", + ["bg"] = "lucene.bulgarian", + ["ca"] = "lucene.catalan", + ["ko"] = "lucene.cjk", + ["da"] = "lucene.danish", + ["nl"] = "lucene.dutch", + ["en"] = "lucene.english", + ["fi"] = "lucene.finnish", + ["fr"] = "lucene.french", + ["gl"] = "lucene.galician", + ["de"] = "lucene.german", + ["el"] = "lucene.greek", + ["hi"] = "lucene.hindi", + ["hu"] = "lucene.hungarian", + ["id"] = "lucene.indonesian", + ["ga"] = "lucene.irish", + ["it"] = "lucene.italian", + ["jp"] = "lucene.japanese", + ["lv"] = "lucene.latvian", + ["no"] = "lucene.norwegian", + ["fa"] = "lucene.persian", + ["pt"] = "lucene.portuguese", + ["ro"] = "lucene.romanian", + ["ru"] = "lucene.russian", + ["zh"] = "lucene.smartcn", + ["es"] = "lucene.spanish", + ["sv"] = "lucene.swedish", + ["th"] = "lucene.thai", + ["tr"] = "lucene.turkish", + ["uk"] = "lucene.ukrainian" + }; + + public sealed class ErrorResponse + { + public string Detail { get; set; } - public sealed class ErrorResponse - { - public string Detail { get; set; } + public string ErrorCode { get; set; } + } - public string ErrorCode { get; set; } - } + static AtlasIndexDefinition() + { + FieldPaths = FieldAnalyzers.ToDictionary(x => x.Key, x => $"t.{x.Key}"); + } - static AtlasIndexDefinition() + public static string GetFieldName(string key) + { + if (FieldAnalyzers.ContainsKey(key)) { - FieldPaths = FieldAnalyzers.ToDictionary(x => x.Key, x => $"t.{x.Key}"); + return key; } - public static string GetFieldName(string key) + if (key.Length > 0) { - if (FieldAnalyzers.ContainsKey(key)) - { - return key; - } + var language = key[2..]; - if (key.Length > 0) + if (FieldAnalyzers.ContainsKey(language)) { - var language = key[2..]; - - if (FieldAnalyzers.ContainsKey(language)) - { - return language; - } + return language; } + } - return "iv"; + return "iv"; + } + + public static string GetFieldPath(string key) + { + if (FieldPaths.TryGetValue(key, out var path)) + { + return path; } - public static string GetFieldPath(string key) + if (key.Length > 0) { - if (FieldPaths.TryGetValue(key, out var path)) + var language = key[2..]; + + if (FieldPaths.TryGetValue(language, out path)) { return path; } + } - if (key.Length > 0) - { - var language = key[2..]; - - if (FieldPaths.TryGetValue(language, out path)) - { - return path; - } - } + return "t.iv"; + } - return "t.iv"; - } + public static async Task CreateIndexAsync(AtlasOptions options, + string database, + string collectionName, + CancellationToken ct) + { + var (index, name) = Create(database, collectionName); - public static async Task CreateIndexAsync(AtlasOptions options, - string database, - string collectionName, - CancellationToken ct) + using (var httpClient = new HttpClient(new HttpClientHandler { - var (index, name) = Create(database, collectionName); - - using (var httpClient = new HttpClient(new HttpClientHandler - { - Credentials = new NetworkCredential(options.PublicKey, options.PrivateKey, "cloud.mongodb.com") - })) - { - var url = $"https://cloud.mongodb.com/api/atlas/v1.0/groups/{options.GroupId}/clusters/{options.ClusterName}/fts/indexes"; + Credentials = new NetworkCredential(options.PublicKey, options.PrivateKey, "cloud.mongodb.com") + })) + { + var url = $"https://cloud.mongodb.com/api/atlas/v1.0/groups/{options.GroupId}/clusters/{options.ClusterName}/fts/indexes"; - var result = await httpClient.PostAsJsonAsync(url, index, ct); + var result = await httpClient.PostAsJsonAsync(url, index, ct); - if (result.IsSuccessStatusCode) - { - return name; - } + if (result.IsSuccessStatusCode) + { + return name; + } - var error = await result.Content.ReadFromJsonAsync(cancellationToken: ct); + var error = await result.Content.ReadFromJsonAsync(cancellationToken: ct); - if (error?.ErrorCode != "ATLAS_FTS_DUPLICATE_INDEX") - { - var message = new ConfigurationError($"Creating index failed with {result.StatusCode}: {error?.Detail}"); + if (error?.ErrorCode != "ATLAS_FTS_DUPLICATE_INDEX") + { + var message = new ConfigurationError($"Creating index failed with {result.StatusCode}: {error?.Detail}"); - throw new ConfigurationException(message); - } + throw new ConfigurationException(message); } - - return name; } - public static (object, string) Create(string database, string collectionName) - { - var name = $"{database}_{collectionName}_text".ToLowerInvariant(); + return name; + } - var texts = new - { - type = "document", - fields = new Dictionary(), - dynamic = false - }; + public static (object, string) Create(string database, string collectionName) + { + var name = $"{database}_{collectionName}_text".ToLowerInvariant(); + + var texts = new + { + type = "document", + fields = new Dictionary(), + dynamic = false + }; - var index = new + var index = new + { + collectionName, + database, + name, + mappings = new { - collectionName, - database, - name, - mappings = new + dynamic = false, + fields = new Dictionary { - dynamic = false, - fields = new Dictionary + ["_ai"] = new + { + type = "string", + analyzer = "lucene.keyword" + }, + ["_si"] = new { - ["_ai"] = new - { - type = "string", - analyzer = "lucene.keyword" - }, - ["_si"] = new - { - type = "string", - analyzer = "lucene.keyword" - }, - ["_ci"] = new - { - type = "string", - analyzer = "lucene.keyword" - }, - ["fa"] = new - { - type = "boolean" - }, - ["fp"] = new - { - type = "boolean" - }, - ["t"] = texts - } + type = "string", + analyzer = "lucene.keyword" + }, + ["_ci"] = new + { + type = "string", + analyzer = "lucene.keyword" + }, + ["fa"] = new + { + type = "boolean" + }, + ["fp"] = new + { + type = "boolean" + }, + ["t"] = texts } - }; - - foreach (var (field, analyzer) in FieldAnalyzers) - { - texts.fields[field] = new - { - type = "string", - analyzer, - searchAnalyzer = analyzer, - store = false - }; } + }; - return (index, name); + foreach (var (field, analyzer) in FieldAnalyzers) + { + texts.fields[field] = new + { + type = "string", + analyzer, + searchAnalyzer = analyzer, + store = false + }; } + + return (index, name); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasOptions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasOptions.cs index e71812463f..2e3b9ca216 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasOptions.cs @@ -5,27 +5,26 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public sealed class AtlasOptions { - public sealed class AtlasOptions - { - public string GroupId { get; set; } + public string GroupId { get; set; } - public string ClusterName { get; set; } + public string ClusterName { get; set; } - public string PublicKey { get; set; } + public string PublicKey { get; set; } - public string PrivateKey { get; set; } + public string PrivateKey { get; set; } - public bool FullTextEnabled { get; set; } + public bool FullTextEnabled { get; set; } - public bool IsConfigured() - { - return - !string.IsNullOrWhiteSpace(GroupId) && - !string.IsNullOrWhiteSpace(ClusterName) && - !string.IsNullOrWhiteSpace(PublicKey) && - !string.IsNullOrWhiteSpace(PrivateKey); - } + public bool IsConfigured() + { + return + !string.IsNullOrWhiteSpace(GroupId) && + !string.IsNullOrWhiteSpace(ClusterName) && + !string.IsNullOrWhiteSpace(PublicKey) && + !string.IsNullOrWhiteSpace(PrivateKey); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs index a543765616..212927f4d1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs @@ -17,144 +17,143 @@ using Squidex.Infrastructure; using LuceneQueryAnalyzer = Lucene.Net.QueryParsers.Classic.QueryParser; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public sealed class AtlasTextIndex : MongoTextIndexBase> { - public sealed class AtlasTextIndex : MongoTextIndexBase> + private static readonly LuceneQueryVisitor QueryVisitor = new LuceneQueryVisitor(AtlasIndexDefinition.GetFieldPath); + private static readonly LuceneQueryAnalyzer QueryParser = + new LuceneQueryAnalyzer(LuceneVersion.LUCENE_48, "*", + new StandardAnalyzer(LuceneVersion.LUCENE_48, CharArraySet.EMPTY_SET)); + private readonly AtlasOptions options; + private string index; + + public AtlasTextIndex(IMongoDatabase database, IOptions options) + : base(database) { - private static readonly LuceneQueryVisitor QueryVisitor = new LuceneQueryVisitor(AtlasIndexDefinition.GetFieldPath); - private static readonly LuceneQueryAnalyzer QueryParser = - new LuceneQueryAnalyzer(LuceneVersion.LUCENE_48, "*", - new StandardAnalyzer(LuceneVersion.LUCENE_48, CharArraySet.EMPTY_SET)); - private readonly AtlasOptions options; - private string index; - - public AtlasTextIndex(IMongoDatabase database, IOptions options) - : base(database) - { - this.options = options.Value; - } - - protected override async Task SetupCollectionAsync(IMongoCollection>> collection, - CancellationToken ct) - { - await base.SetupCollectionAsync(collection, ct); + this.options = options.Value; + } - index = await AtlasIndexDefinition.CreateIndexAsync(options, - Database.DatabaseNamespace.DatabaseName, CollectionName(), ct); - } + protected override async Task SetupCollectionAsync(IMongoCollection>> collection, + CancellationToken ct) + { + await base.SetupCollectionAsync(collection, ct); - protected override Dictionary BuildTexts(Dictionary source) - { - var texts = new Dictionary(); + index = await AtlasIndexDefinition.CreateIndexAsync(options, + Database.DatabaseNamespace.DatabaseName, CollectionName(), ct); + } - foreach (var (key, value) in source) - { - var text = value; + protected override Dictionary BuildTexts(Dictionary source) + { + var texts = new Dictionary(); - var languageCode = AtlasIndexDefinition.GetFieldName(key); + foreach (var (key, value) in source) + { + var text = value; - if (texts.TryGetValue(languageCode, out var existing)) - { - text = $"{existing} {value}"; - } + var languageCode = AtlasIndexDefinition.GetFieldName(key); - texts[languageCode] = text; + if (texts.TryGetValue(languageCode, out var existing)) + { + text = $"{existing} {value}"; } - return texts; + texts[languageCode] = text; } - public override async Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, - CancellationToken ct = default) - { - Guard.NotNull(app); - Guard.NotNull(query); + return texts; + } - var (search, take) = query; + public override async Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); - if (string.IsNullOrWhiteSpace(search)) - { - return null; - } + var (search, take) = query; + + if (string.IsNullOrWhiteSpace(search)) + { + return null; + } - var luceneQuery = QueryParser.Parse(search); + var luceneQuery = QueryParser.Parse(search); - var serveField = scope == SearchScope.All ? "fa" : "fp"; + var serveField = scope == SearchScope.All ? "fa" : "fp"; - var compound = new BsonDocument + var compound = new BsonDocument + { + ["must"] = new BsonArray + { + QueryVisitor.Visit(luceneQuery) + }, + ["filter"] = new BsonArray { - ["must"] = new BsonArray + new BsonDocument { - QueryVisitor.Visit(luceneQuery) + ["text"] = new BsonDocument + { + ["path"] = "_ai", + ["query"] = app.Id.ToString() + } }, - ["filter"] = new BsonArray + new BsonDocument { - new BsonDocument + ["equals"] = new BsonDocument { - ["text"] = new BsonDocument - { - ["path"] = "_ai", - ["query"] = app.Id.ToString() - } - }, - new BsonDocument - { - ["equals"] = new BsonDocument - { - ["path"] = serveField, - ["value"] = true - } + ["path"] = serveField, + ["value"] = true } } - }; + } + }; - if (query.PreferredSchemaId != null) + if (query.PreferredSchemaId != null) + { + compound["should"] = new BsonArray { - compound["should"] = new BsonArray + new BsonDocument { - new BsonDocument + ["text"] = new BsonDocument { - ["text"] = new BsonDocument - { - ["path"] = "_si", - ["query"] = query.PreferredSchemaId.Value.ToString() - } + ["path"] = "_si", + ["query"] = query.PreferredSchemaId.Value.ToString() } - }; - } - else if (query.RequiredSchemaIds?.Count > 0) - { - compound["should"] = new BsonArray(query.RequiredSchemaIds.Select(x => - new BsonDocument - { - ["text"] = new BsonDocument - { - ["path"] = "_si", - ["query"] = x.ToString() - } - })); - - compound["minimumShouldMatch"] = 1; - } - - var searchQuery = new BsonDocument - { - ["compound"] = compound + } }; + } + else if (query.RequiredSchemaIds?.Count > 0) + { + compound["should"] = new BsonArray(query.RequiredSchemaIds.Select(x => + new BsonDocument + { + ["text"] = new BsonDocument + { + ["path"] = "_si", + ["query"] = x.ToString() + } + })); - if (index != null) - { - searchQuery["index"] = index; - } + compound["minimumShouldMatch"] = 1; + } - var results = - await Collection.Aggregate().Search(searchQuery).Limit(take) - .Project( - Projection.Include(x => x.ContentId) - ) - .ToListAsync(ct); + var searchQuery = new BsonDocument + { + ["compound"] = compound + }; - return results.Select(x => x.ContentId).ToList(); + if (index != null) + { + searchQuery["index"] = index; } + + var results = + await Collection.Aggregate().Search(searchQuery).Limit(take) + .Project( + Projection.Include(x => x.ContentId) + ) + .ToListAsync(ct); + + return results.Select(x => x.ContentId).ToList(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs index 75456b1267..2acf5f86bd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs @@ -9,106 +9,105 @@ using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public sealed class CommandFactory : MongoBase> where T : class { - public sealed class CommandFactory : MongoBase> where T : class + private readonly Func, T> textBuilder; + + public CommandFactory(Func, T> textBuilder) { - private readonly Func, T> textBuilder; + this.textBuilder = textBuilder; + } - public CommandFactory(Func, T> textBuilder) + public void CreateCommands(IndexCommand command, List>> writes) + { + switch (command) { - this.textBuilder = textBuilder; + case DeleteIndexEntry delete: + DeleteEntry(delete, writes); + break; + case UpsertIndexEntry upsert: + UpsertEntry(upsert, writes); + break; + case UpdateIndexEntry update: + UpdateEntry(update, writes); + break; } + } - public void CreateCommands(IndexCommand command, List>> writes) - { - switch (command) + private void UpsertEntry(UpsertIndexEntry upsert, List>> writes) + { + writes.Add( + new UpdateOneModel>( + Filter.And( + Filter.Eq(x => x.DocId, upsert.DocId), + Filter.Exists(x => x.GeoField, false), + Filter.Exists(x => x.GeoObject, false)), + Update + .Set(x => x.ServeAll, upsert.ServeAll) + .Set(x => x.ServePublished, upsert.ServePublished) + .Set(x => x.Texts, BuildTexts(upsert)) + .SetOnInsert(x => x.Id, Guid.NewGuid().ToString()) + .SetOnInsert(x => x.DocId, upsert.DocId) + .SetOnInsert(x => x.AppId, upsert.AppId.Id) + .SetOnInsert(x => x.ContentId, upsert.ContentId) + .SetOnInsert(x => x.SchemaId, upsert.SchemaId.Id)) { - case DeleteIndexEntry delete: - DeleteEntry(delete, writes); - break; - case UpsertIndexEntry upsert: - UpsertEntry(upsert, writes); - break; - case UpdateIndexEntry update: - UpdateEntry(update, writes); - break; - } - } + IsUpsert = true + }); - private void UpsertEntry(UpsertIndexEntry upsert, List>> writes) + if (upsert.GeoObjects?.Any() == true) { - writes.Add( - new UpdateOneModel>( - Filter.And( - Filter.Eq(x => x.DocId, upsert.DocId), - Filter.Exists(x => x.GeoField, false), - Filter.Exists(x => x.GeoObject, false)), - Update - .Set(x => x.ServeAll, upsert.ServeAll) - .Set(x => x.ServePublished, upsert.ServePublished) - .Set(x => x.Texts, BuildTexts(upsert)) - .SetOnInsert(x => x.Id, Guid.NewGuid().ToString()) - .SetOnInsert(x => x.DocId, upsert.DocId) - .SetOnInsert(x => x.AppId, upsert.AppId.Id) - .SetOnInsert(x => x.ContentId, upsert.ContentId) - .SetOnInsert(x => x.SchemaId, upsert.SchemaId.Id)) - { - IsUpsert = true - }); - - if (upsert.GeoObjects?.Any() == true) + if (!upsert.IsNew) { - if (!upsert.IsNew) - { - writes.Add( - new DeleteOneModel>( - Filter.And( - Filter.Eq(x => x.DocId, upsert.DocId), - Filter.Exists(x => x.GeoField), - Filter.Exists(x => x.GeoObject)))); - } + writes.Add( + new DeleteOneModel>( + Filter.And( + Filter.Eq(x => x.DocId, upsert.DocId), + Filter.Exists(x => x.GeoField), + Filter.Exists(x => x.GeoObject)))); + } - foreach (var (field, geoObject) in upsert.GeoObjects) - { - writes.Add( - new InsertOneModel>( - new MongoTextIndexEntity - { - Id = Guid.NewGuid().ToString(), - AppId = upsert.AppId.Id, - DocId = upsert.DocId, - ContentId = upsert.ContentId, - GeoField = field, - GeoObject = geoObject, - SchemaId = upsert.SchemaId.Id, - ServeAll = upsert.ServeAll, - ServePublished = upsert.ServePublished - })); - } + foreach (var (field, geoObject) in upsert.GeoObjects) + { + writes.Add( + new InsertOneModel>( + new MongoTextIndexEntity + { + Id = Guid.NewGuid().ToString(), + AppId = upsert.AppId.Id, + DocId = upsert.DocId, + ContentId = upsert.ContentId, + GeoField = field, + GeoObject = geoObject, + SchemaId = upsert.SchemaId.Id, + ServeAll = upsert.ServeAll, + ServePublished = upsert.ServePublished + })); } } + } - private T? BuildTexts(UpsertIndexEntry upsert) - { - return upsert.Texts == null ? null : textBuilder(upsert.Texts); - } + private T? BuildTexts(UpsertIndexEntry upsert) + { + return upsert.Texts == null ? null : textBuilder(upsert.Texts); + } - private static void UpdateEntry(UpdateIndexEntry update, List>> writes) - { - writes.Add( - new UpdateOneModel>( - Filter.Eq(x => x.DocId, update.DocId), - Update - .Set(x => x.ServeAll, update.ServeAll) - .Set(x => x.ServePublished, update.ServePublished))); - } + private static void UpdateEntry(UpdateIndexEntry update, List>> writes) + { + writes.Add( + new UpdateOneModel>( + Filter.Eq(x => x.DocId, update.DocId), + Update + .Set(x => x.ServeAll, update.ServeAll) + .Set(x => x.ServePublished, update.ServePublished))); + } - private static void DeleteEntry(DeleteIndexEntry delete, List>> writes) - { - writes.Add( - new DeleteOneModel>( - Filter.Eq(x => x.DocId, delete.DocId))); - } + private static void DeleteEntry(DeleteIndexEntry delete, List>> writes) + { + writes.Add( + new DeleteOneModel>( + Filter.Eq(x => x.DocId, delete.DocId))); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneQueryVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneQueryVisitor.cs index 72301e480a..0cea6d45d1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneQueryVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneQueryVisitor.cs @@ -13,318 +13,317 @@ using MongoDB.Bson; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public sealed class LuceneQueryVisitor { - public sealed class LuceneQueryVisitor + private readonly Func? fieldConverter; + + public LuceneQueryVisitor(Func? fieldConverter = null) { - private readonly Func? fieldConverter; + this.fieldConverter = fieldConverter; + } - public LuceneQueryVisitor(Func? fieldConverter = null) + public BsonDocument Visit(Query query) + { + switch (query) { - this.fieldConverter = fieldConverter; + case BooleanQuery booleanQuery: + return VisitBoolean(booleanQuery); + case TermQuery termQuery: + return VisitTerm(termQuery); + case PhraseQuery phraseQuery: + return VisitPhrase(phraseQuery); + case WildcardQuery wildcardQuery: + return VisitWilcard(wildcardQuery); + case PrefixQuery prefixQuery: + return VisitPrefix(prefixQuery); + case FuzzyQuery fuzzyQuery: + return VisitFuzzy(fuzzyQuery); + case NumericRangeQuery rangeQuery: + return VisitNumericRange(rangeQuery); + case NumericRangeQuery rangeQuery: + return VisitNumericRange(rangeQuery); + case NumericRangeQuery rangeQuery: + return VisitNumericRange(rangeQuery); + case NumericRangeQuery rangeQuery: + return VisitNumericRange(rangeQuery); + case TermRangeQuery termRangeQuery: + return VisitTermRange(termRangeQuery); + default: + ThrowHelper.NotSupportedException(); + return default!; } + } - public BsonDocument Visit(Query query) + private BsonDocument VisitTermRange(TermRangeQuery termRangeQuery) + { + if (!TryParseValue(termRangeQuery.LowerTerm, out var min) || + !TryParseValue(termRangeQuery.UpperTerm, out var max)) { - switch (query) - { - case BooleanQuery booleanQuery: - return VisitBoolean(booleanQuery); - case TermQuery termQuery: - return VisitTerm(termQuery); - case PhraseQuery phraseQuery: - return VisitPhrase(phraseQuery); - case WildcardQuery wildcardQuery: - return VisitWilcard(wildcardQuery); - case PrefixQuery prefixQuery: - return VisitPrefix(prefixQuery); - case FuzzyQuery fuzzyQuery: - return VisitFuzzy(fuzzyQuery); - case NumericRangeQuery rangeQuery: - return VisitNumericRange(rangeQuery); - case NumericRangeQuery rangeQuery: - return VisitNumericRange(rangeQuery); - case NumericRangeQuery rangeQuery: - return VisitNumericRange(rangeQuery); - case NumericRangeQuery rangeQuery: - return VisitNumericRange(rangeQuery); - case TermRangeQuery termRangeQuery: - return VisitTermRange(termRangeQuery); - default: - ThrowHelper.NotSupportedException(); - return default!; - } + ThrowHelper.NotSupportedException(); + return default!; } - private BsonDocument VisitTermRange(TermRangeQuery termRangeQuery) - { - if (!TryParseValue(termRangeQuery.LowerTerm, out var min) || - !TryParseValue(termRangeQuery.UpperTerm, out var max)) - { - ThrowHelper.NotSupportedException(); - return default!; - } + var minField = termRangeQuery.IncludesLower ? "gte" : "gt"; + var maxField = termRangeQuery.IncludesUpper ? "lte" : "lt"; - var minField = termRangeQuery.IncludesLower ? "gte" : "gt"; - var maxField = termRangeQuery.IncludesUpper ? "lte" : "lt"; + var doc = new BsonDocument + { + ["path"] = GetPath(termRangeQuery.Field), + [minField] = BsonValue.Create(min), + [maxField] = BsonValue.Create(max) + }; - var doc = new BsonDocument - { - ["path"] = GetPath(termRangeQuery.Field), - [minField] = BsonValue.Create(min), - [maxField] = BsonValue.Create(max) - }; + ApplyBoost(termRangeQuery, doc); - ApplyBoost(termRangeQuery, doc); + return new BsonDocument + { + ["range"] = doc + }; + } - return new BsonDocument - { - ["range"] = doc - }; - } + private BsonDocument VisitNumericRange(NumericRangeQuery numericRangeQuery) where T : struct, IComparable + { + var minField = numericRangeQuery.IncludesMin ? "gte" : "gt"; + var maxField = numericRangeQuery.IncludesMin ? "lte" : "lt"; - private BsonDocument VisitNumericRange(NumericRangeQuery numericRangeQuery) where T : struct, IComparable + var doc = new BsonDocument { - var minField = numericRangeQuery.IncludesMin ? "gte" : "gt"; - var maxField = numericRangeQuery.IncludesMin ? "lte" : "lt"; + ["path"] = GetPath(numericRangeQuery.Field), + [minField] = BsonValue.Create(numericRangeQuery.Min), + [maxField] = BsonValue.Create(numericRangeQuery.Max) + }; - var doc = new BsonDocument - { - ["path"] = GetPath(numericRangeQuery.Field), - [minField] = BsonValue.Create(numericRangeQuery.Min), - [maxField] = BsonValue.Create(numericRangeQuery.Max) - }; + ApplyBoost(numericRangeQuery, doc); - ApplyBoost(numericRangeQuery, doc); + return new BsonDocument + { + ["range"] = doc + }; + } - return new BsonDocument - { - ["range"] = doc - }; - } + private BsonDocument VisitFuzzy(FuzzyQuery fuzzyQuery) + { + var doc = CreateDefaultDoc(fuzzyQuery, fuzzyQuery.Term); - private BsonDocument VisitFuzzy(FuzzyQuery fuzzyQuery) + if (fuzzyQuery.MaxEdits > 0) { - var doc = CreateDefaultDoc(fuzzyQuery, fuzzyQuery.Term); - - if (fuzzyQuery.MaxEdits > 0) + var fuzzy = new BsonDocument { - var fuzzy = new BsonDocument - { - ["maxEdits"] = fuzzyQuery.MaxEdits - }; - - if (fuzzyQuery.PrefixLength > 0) - { - fuzzy["prefixLength"] = fuzzyQuery.PrefixLength; - } + ["maxEdits"] = fuzzyQuery.MaxEdits + }; - doc["fuzzy"] = fuzzy; + if (fuzzyQuery.PrefixLength > 0) + { + fuzzy["prefixLength"] = fuzzyQuery.PrefixLength; } - return new BsonDocument - { - ["text"] = doc - }; + doc["fuzzy"] = fuzzy; } - private BsonDocument VisitPrefix(PrefixQuery prefixQuery) + return new BsonDocument { - var doc = CreateDefaultDoc(prefixQuery, new Term(prefixQuery.Prefix.Field, prefixQuery.Prefix.Text + "*")); + ["text"] = doc + }; + } - return new BsonDocument - { - ["wildcard"] = doc - }; - } + private BsonDocument VisitPrefix(PrefixQuery prefixQuery) + { + var doc = CreateDefaultDoc(prefixQuery, new Term(prefixQuery.Prefix.Field, prefixQuery.Prefix.Text + "*")); - private BsonDocument VisitWilcard(WildcardQuery wildcardQuery) + return new BsonDocument { - var doc = CreateDefaultDoc(wildcardQuery, wildcardQuery.Term); + ["wildcard"] = doc + }; + } - return new BsonDocument - { - ["wildcard"] = doc - }; - } + private BsonDocument VisitWilcard(WildcardQuery wildcardQuery) + { + var doc = CreateDefaultDoc(wildcardQuery, wildcardQuery.Term); - private BsonDocument VisitPhrase(PhraseQuery phraseQuery) + return new BsonDocument { - var terms = phraseQuery.GetTerms(); - - var doc = new BsonDocument - { - ["path"] = GetPath(terms[0].Field) - }; + ["wildcard"] = doc + }; + } - if (terms.Length == 1) - { - doc["query"] = terms[0].Text; - } - else - { - doc["query"] = new BsonArray(terms.Select(x => x.Text)); - } + private BsonDocument VisitPhrase(PhraseQuery phraseQuery) + { + var terms = phraseQuery.GetTerms(); - if (phraseQuery.Slop != 0) - { - doc["slop"] = phraseQuery.Slop; - } + var doc = new BsonDocument + { + ["path"] = GetPath(terms[0].Field) + }; - ApplyBoost(phraseQuery, doc); + if (terms.Length == 1) + { + doc["query"] = terms[0].Text; + } + else + { + doc["query"] = new BsonArray(terms.Select(x => x.Text)); + } - return new BsonDocument - { - ["phrase"] = doc - }; + if (phraseQuery.Slop != 0) + { + doc["slop"] = phraseQuery.Slop; } - private BsonDocument VisitTerm(TermQuery termQuery) + ApplyBoost(phraseQuery, doc); + + return new BsonDocument { - var doc = CreateDefaultDoc(termQuery, termQuery.Term); + ["phrase"] = doc + }; + } - return new BsonDocument - { - ["text"] = doc - }; - } + private BsonDocument VisitTerm(TermQuery termQuery) + { + var doc = CreateDefaultDoc(termQuery, termQuery.Term); - private BsonDocument VisitBoolean(BooleanQuery booleanQuery) + return new BsonDocument { - var doc = new BsonDocument(); + ["text"] = doc + }; + } - BsonArray? musts = null; - BsonArray? mustNots = null; - BsonArray? shoulds = null; + private BsonDocument VisitBoolean(BooleanQuery booleanQuery) + { + var doc = new BsonDocument(); - foreach (var clause in booleanQuery.Clauses) - { - var converted = Visit(clause.Query); + BsonArray? musts = null; + BsonArray? mustNots = null; + BsonArray? shoulds = null; - switch (clause.Occur) - { - case Occur.MUST: - musts ??= new BsonArray(); - musts.Add(converted); - break; - case Occur.SHOULD: - shoulds ??= new BsonArray(); - shoulds.Add(converted); - break; - case Occur.MUST_NOT: - mustNots ??= new BsonArray(); - mustNots.Add(converted); - break; - } - } + foreach (var clause in booleanQuery.Clauses) + { + var converted = Visit(clause.Query); - if (musts != null) + switch (clause.Occur) { - doc.Add("must", musts); + case Occur.MUST: + musts ??= new BsonArray(); + musts.Add(converted); + break; + case Occur.SHOULD: + shoulds ??= new BsonArray(); + shoulds.Add(converted); + break; + case Occur.MUST_NOT: + mustNots ??= new BsonArray(); + mustNots.Add(converted); + break; } + } - if (mustNots != null) - { - doc.Add("mustNot", mustNots); - } + if (musts != null) + { + doc.Add("must", musts); + } - if (shoulds != null) - { - doc.Add("should", shoulds); - } + if (mustNots != null) + { + doc.Add("mustNot", mustNots); + } - if (booleanQuery.MinimumNumberShouldMatch > 0) - { - doc["minimumShouldMatch"] = booleanQuery.MinimumNumberShouldMatch; - } + if (shoulds != null) + { + doc.Add("should", shoulds); + } - return new BsonDocument - { - ["compound"] = doc - }; + if (booleanQuery.MinimumNumberShouldMatch > 0) + { + doc["minimumShouldMatch"] = booleanQuery.MinimumNumberShouldMatch; } - private BsonDocument CreateDefaultDoc(Query query, Term term) + return new BsonDocument { - var doc = new BsonDocument - { - ["path"] = GetPath(term.Field), - ["query"] = term.Text - }; + ["compound"] = doc + }; + } + + private BsonDocument CreateDefaultDoc(Query query, Term term) + { + var doc = new BsonDocument + { + ["path"] = GetPath(term.Field), + ["query"] = term.Text + }; - ApplyBoost(query, doc); + ApplyBoost(query, doc); - return doc; - } + return doc; + } - private BsonValue GetPath(string field) + private BsonValue GetPath(string field) + { + if (field != "*" && fieldConverter != null) { - if (field != "*" && fieldConverter != null) - { - field = fieldConverter(field); - } + field = fieldConverter(field); + } - if (field.Contains('*', StringComparison.Ordinal)) + if (field.Contains('*', StringComparison.Ordinal)) + { + return new BsonDocument { - return new BsonDocument - { - ["wildcard"] = field - }; - } - - return field; + ["wildcard"] = field + }; } + return field; + } + #pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator - private static void ApplyBoost(Query query, BsonDocument doc) + private static void ApplyBoost(Query query, BsonDocument doc) + { + if (query.Boost != 1) { - if (query.Boost != 1) + doc["score"] = new BsonDocument { - doc["score"] = new BsonDocument - { - ["boost"] = query.Boost - }; - } + ["boost"] = query.Boost + }; } + } - private static bool TryParseValue(BytesRef bytes, out object result) + private static bool TryParseValue(BytesRef bytes, out object result) + { + result = null!; + + try { - result = null!; + var text = Encoding.ASCII.GetString(bytes.Bytes, bytes.Offset, bytes.Length); - try + if (!double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) { - var text = Encoding.ASCII.GetString(bytes.Bytes, bytes.Offset, bytes.Length); - - if (!double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) - { - return false; - } + return false; + } - var integer = (long)number; + var integer = (long)number; - if (number == integer) + if (number == integer) + { + if (integer is <= int.MaxValue and >= int.MinValue) { - if (integer is <= int.MaxValue and >= int.MinValue) - { - result = (int)integer; - } - else - { - result = integer; - } + result = (int)integer; } else { - result = number; + result = integer; } - - return true; } - catch (Exception) + else { - return false; + result = number; } + + return true; + } + catch (Exception) + { + return false; } -#pragma warning restore RECS0018 // Comparison of floating point numbers with equality operator } +#pragma warning restore RECS0018 // Comparison of floating point numbers with equality operator } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneSearchDefinitionExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneSearchDefinitionExtensions.cs index 8f2341c614..ed05797842 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneSearchDefinitionExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneSearchDefinitionExtensions.cs @@ -10,49 +10,48 @@ using MongoDB.Driver; using MongoDB.Driver.Linq; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public static class LuceneSearchDefinitionExtensions { - public static class LuceneSearchDefinitionExtensions + public static IAggregateFluent Search( + this IAggregateFluent aggregate, + BsonDocument search) { - public static IAggregateFluent Search( - this IAggregateFluent aggregate, - BsonDocument search) - { - const string OperatorName = "$search"; + const string OperatorName = "$search"; - var stage = new DelegatedPipelineStageDefinition( - OperatorName, - serializer => - { - var document = new BsonDocument(OperatorName, search); + var stage = new DelegatedPipelineStageDefinition( + OperatorName, + serializer => + { + var document = new BsonDocument(OperatorName, search); - return new RenderedPipelineStageDefinition(OperatorName, document, serializer); - }); + return new RenderedPipelineStageDefinition(OperatorName, document, serializer); + }); - return aggregate.AppendStage(stage); - } + return aggregate.AppendStage(stage); + } - private sealed class DelegatedPipelineStageDefinition : PipelineStageDefinition - { - private readonly Func, RenderedPipelineStageDefinition> renderer; + private sealed class DelegatedPipelineStageDefinition : PipelineStageDefinition + { + private readonly Func, RenderedPipelineStageDefinition> renderer; - public override string OperatorName { get; } + public override string OperatorName { get; } - public DelegatedPipelineStageDefinition(string operatorName, - Func, RenderedPipelineStageDefinition> renderer) - { - this.renderer = renderer; + public DelegatedPipelineStageDefinition(string operatorName, + Func, RenderedPipelineStageDefinition> renderer) + { + this.renderer = renderer; - OperatorName = operatorName; - } + OperatorName = operatorName; + } - public override RenderedPipelineStageDefinition Render( - IBsonSerializer inputSerializer, - IBsonSerializerRegistry serializerRegistry, - LinqProvider linqProvider) - { - return renderer(inputSerializer); - } + public override RenderedPipelineStageDefinition Render( + IBsonSerializer inputSerializer, + IBsonSerializerRegistry serializerRegistry, + LinqProvider linqProvider) + { + return renderer(inputSerializer); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs index aabe13c324..2594b7ded7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs @@ -7,34 +7,33 @@ using MongoDB.Driver; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public sealed class MongoTextIndex : MongoTextIndexBase> { - public sealed class MongoTextIndex : MongoTextIndexBase> + public MongoTextIndex(IMongoDatabase database) + : base(database) { - public MongoTextIndex(IMongoDatabase database) - : base(database) - { - } + } - protected override async Task SetupCollectionAsync(IMongoCollection>> collection, - CancellationToken ct) - { - await base.SetupCollectionAsync(collection, ct); + protected override async Task SetupCollectionAsync(IMongoCollection>> collection, + CancellationToken ct) + { + await base.SetupCollectionAsync(collection, ct); - await collection.Indexes.CreateOneAsync( - new CreateIndexModel>>( - Index - .Text("t.t") - .Ascending(x => x.AppId) - .Ascending(x => x.ServeAll) - .Ascending(x => x.ServePublished) - .Ascending(x => x.SchemaId)), - cancellationToken: ct); - } + await collection.Indexes.CreateOneAsync( + new CreateIndexModel>>( + Index + .Text("t.t") + .Ascending(x => x.AppId) + .Ascending(x => x.ServeAll) + .Ascending(x => x.ServePublished) + .Ascending(x => x.SchemaId)), + cancellationToken: ct); + } - protected override List BuildTexts(Dictionary source) - { - return source.Select(x => new MongoTextIndexEntityText { Text = x.Value }).ToList(); - } + protected override List BuildTexts(Dictionary source) + { + return source.Select(x => new MongoTextIndexEntityText { Text = x.Value }).ToList(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs index 39289ec1a7..80edd10702 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs @@ -14,212 +14,211 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public abstract class MongoTextIndexBase : MongoRepositoryBase>, ITextIndex, IDeleter where T : class { - public abstract class MongoTextIndexBase : MongoRepositoryBase>, ITextIndex, IDeleter where T : class - { - private readonly CommandFactory commandFactory; + private readonly CommandFactory commandFactory; - protected sealed class MongoTextResult - { - [BsonId] - [BsonElement] - public string Id { get; set; } + protected sealed class MongoTextResult + { + [BsonId] + [BsonElement] + public string Id { get; set; } - [BsonRequired] - [BsonElement("_ci")] - public DomainId ContentId { get; set; } + [BsonRequired] + [BsonElement("_ci")] + public DomainId ContentId { get; set; } - [BsonIgnoreIfDefault] - [BsonElement("score")] - public double Score { get; set; } - } + [BsonIgnoreIfDefault] + [BsonElement("score")] + public double Score { get; set; } + } - protected MongoTextIndexBase(IMongoDatabase database) - : base(database) - { + protected MongoTextIndexBase(IMongoDatabase database) + : base(database) + { #pragma warning disable MA0056 // Do not call overridable members in constructor - commandFactory = new CommandFactory(BuildTexts); + commandFactory = new CommandFactory(BuildTexts); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - protected override Task SetupCollectionAsync(IMongoCollection> collection, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection> collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel>( - Index.Ascending(x => x.DocId)), - - new CreateIndexModel>( - Index - .Ascending(x => x.AppId) - .Ascending(x => x.ServeAll) - .Ascending(x => x.ServePublished) - .Ascending(x => x.SchemaId) - .Ascending(x => x.GeoField) - .Geo2DSphere(x => x.GeoObject)) - }, ct); - } + new CreateIndexModel>( + Index.Ascending(x => x.DocId)), + + new CreateIndexModel>( + Index + .Ascending(x => x.AppId) + .Ascending(x => x.ServeAll) + .Ascending(x => x.ServePublished) + .Ascending(x => x.SchemaId) + .Ascending(x => x.GeoField) + .Geo2DSphere(x => x.GeoObject)) + }, ct); + } - protected override string CollectionName() - { - return "TextIndex"; - } + protected override string CollectionName() + { + return "TextIndex"; + } + + protected abstract T BuildTexts(Dictionary source); + + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } - protected abstract T BuildTexts(Dictionary source); + public async virtual Task ExecuteAsync(IndexCommand[] commands, + CancellationToken ct = default) + { + var writes = new List>>(commands.Length); - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) + foreach (var command in commands) { - await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + commandFactory.CreateCommands(command, writes); } - public async virtual Task ExecuteAsync(IndexCommand[] commands, - CancellationToken ct = default) + if (writes.Count == 0) { - var writes = new List>>(commands.Length); + return; + } - foreach (var command in commands) + try + { + await Collection.BulkWriteAsync(writes, BulkUnordered, ct); + } + catch (MongoBulkWriteException ex) + { + // Ignore invalid geo data. + if (ex.WriteErrors.Any(error => error.Code != MongoDbErrorCodes.Errror16755_InvalidGeoData)) { - commandFactory.CreateCommands(command, writes); + throw; } + } + } - if (writes.Count == 0) - { - return; - } + public virtual async Task?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); + + var findFilter = + Filter.And( + Filter.Eq(x => x.AppId, app.Id), + Filter.Eq(x => x.SchemaId, query.SchemaId), + Filter_ByScope(scope), + Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, query.Radius / 6378100)); + + var byGeo = + await GetCollection(scope).Find(findFilter).Limit(query.Take) + .Project(Projection.Include(x => x.ContentId)) + .ToListAsync(ct); + + return byGeo.Select(x => x.ContentId).ToList(); + } - try - { - await Collection.BulkWriteAsync(writes, BulkUnordered, ct); - } - catch (MongoBulkWriteException ex) - { - // Ignore invalid geo data. - if (ex.WriteErrors.Any(error => error.Code != MongoDbErrorCodes.Errror16755_InvalidGeoData)) - { - throw; - } - } - } + public virtual async Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); + + var (search, take) = query; - public virtual async Task?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, - CancellationToken ct = default) + if (string.IsNullOrWhiteSpace(search)) { - Guard.NotNull(app); - Guard.NotNull(query); - - var findFilter = - Filter.And( - Filter.Eq(x => x.AppId, app.Id), - Filter.Eq(x => x.SchemaId, query.SchemaId), - Filter_ByScope(scope), - Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, query.Radius / 6378100)); - - var byGeo = - await GetCollection(scope).Find(findFilter).Limit(query.Take) - .Project(Projection.Include(x => x.ContentId)) - .ToListAsync(ct); - - return byGeo.Select(x => x.ContentId).ToList(); + return null; } - public virtual async Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, - CancellationToken ct = default) - { - Guard.NotNull(app); - Guard.NotNull(query); + var result = new List<(DomainId Id, double Score)>(); - var (search, take) = query; + if (query.RequiredSchemaIds?.Count > 0) + { + await SearchBySchemaAsync(result, search, app, query.RequiredSchemaIds, scope, take, 1, ct); + } + else if (query.PreferredSchemaId == null) + { + await SearchByAppAsync(result, search, app, scope, take, 1, ct); + } + else + { + var halfBucket = take / 2; - if (string.IsNullOrWhiteSpace(search)) - { - return null; - } + var schemaIds = Enumerable.Repeat(query.PreferredSchemaId.Value, 1); - var result = new List<(DomainId Id, double Score)>(); + await SearchBySchemaAsync(result, search, app, schemaIds, scope, halfBucket, 1.1, ct); + await SearchByAppAsync(result, search, app, scope, halfBucket, 1, ct); + } - if (query.RequiredSchemaIds?.Count > 0) - { - await SearchBySchemaAsync(result, search, app, query.RequiredSchemaIds, scope, take, 1, ct); - } - else if (query.PreferredSchemaId == null) - { - await SearchByAppAsync(result, search, app, scope, take, 1, ct); - } - else - { - var halfBucket = take / 2; + return result.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList(); + } - var schemaIds = Enumerable.Repeat(query.PreferredSchemaId.Value, 1); + private Task SearchBySchemaAsync(List<(DomainId, double)> result, string text, IAppEntity app, IEnumerable schemaIds, SearchScope scope, int take, double factor, + CancellationToken ct = default) + { + var filter = + Filter.And( + Filter.Eq(x => x.AppId, app.Id), + Filter.In(x => x.SchemaId, schemaIds), + Filter_ByScope(scope), + Filter.Text(text, "none")); + + return SearchAsync(result, filter, scope, take, factor, ct); + } - await SearchBySchemaAsync(result, search, app, schemaIds, scope, halfBucket, 1.1, ct); - await SearchByAppAsync(result, search, app, scope, halfBucket, 1, ct); - } + private Task SearchByAppAsync(List<(DomainId, double)> result, string text, IAppEntity app, SearchScope scope, int take, double factor, + CancellationToken ct = default) + { + var filter = + Filter.And( + Filter.Eq(x => x.AppId, app.Id), + Filter.Exists(x => x.SchemaId), + Filter_ByScope(scope), + Filter.Text(text, "none")); + + return SearchAsync(result, filter, scope, take, factor, ct); + } - return result.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList(); - } + private async Task SearchAsync(List<(DomainId, double)> result, FilterDefinition> filter, SearchScope scope, int take, double factor, + CancellationToken ct = default) + { + var byText = + await GetCollection(scope).Find(filter).Limit(take) + .Project(Projection.Include(x => x.ContentId).MetaTextScore("score")).Sort(Sort.MetaTextScore("score")) + .ToListAsync(ct); - private Task SearchBySchemaAsync(List<(DomainId, double)> result, string text, IAppEntity app, IEnumerable schemaIds, SearchScope scope, int take, double factor, - CancellationToken ct = default) - { - var filter = - Filter.And( - Filter.Eq(x => x.AppId, app.Id), - Filter.In(x => x.SchemaId, schemaIds), - Filter_ByScope(scope), - Filter.Text(text, "none")); - - return SearchAsync(result, filter, scope, take, factor, ct); - } + result.AddRange(byText.Select(x => (x.ContentId, x.Score * factor))); + } - private Task SearchByAppAsync(List<(DomainId, double)> result, string text, IAppEntity app, SearchScope scope, int take, double factor, - CancellationToken ct = default) + private static FilterDefinition> Filter_ByScope(SearchScope scope) + { + if (scope == SearchScope.All) { - var filter = - Filter.And( - Filter.Eq(x => x.AppId, app.Id), - Filter.Exists(x => x.SchemaId), - Filter_ByScope(scope), - Filter.Text(text, "none")); - - return SearchAsync(result, filter, scope, take, factor, ct); + return Filter.Eq(x => x.ServeAll, true); } - - private async Task SearchAsync(List<(DomainId, double)> result, FilterDefinition> filter, SearchScope scope, int take, double factor, - CancellationToken ct = default) + else { - var byText = - await GetCollection(scope).Find(filter).Limit(take) - .Project(Projection.Include(x => x.ContentId).MetaTextScore("score")).Sort(Sort.MetaTextScore("score")) - .ToListAsync(ct); - - result.AddRange(byText.Select(x => (x.ContentId, x.Score * factor))); + return Filter.Eq(x => x.ServePublished, true); } + } - private static FilterDefinition> Filter_ByScope(SearchScope scope) + private IMongoCollection> GetCollection(SearchScope scope) + { + if (scope == SearchScope.All) { - if (scope == SearchScope.All) - { - return Filter.Eq(x => x.ServeAll, true); - } - else - { - return Filter.Eq(x => x.ServePublished, true); - } + return Collection; } - - private IMongoCollection> GetCollection(SearchScope scope) + else { - if (scope == SearchScope.All) - { - return Collection; - } - else - { - return Collection.WithReadPreference(ReadPreference.Secondary); - } + return Collection.WithReadPreference(ReadPreference.Secondary); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs index ff828c1418..165112aeb8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs @@ -11,53 +11,52 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public sealed class MongoTextIndexEntity { - public sealed class MongoTextIndexEntity - { - [BsonId] - [BsonElement("_id")] - public string Id { get; set; } - - [BsonRequired] - [BsonElement(nameof(DocId))] - public string DocId { get; set; } - - [BsonRequired] - [BsonElement("_ci")] - [BsonRepresentation(BsonType.String)] - public DomainId ContentId { get; set; } - - [BsonRequired] - [BsonElement("_ai")] - [BsonRepresentation(BsonType.String)] - public DomainId AppId { get; set; } - - [BsonRequired] - [BsonElement("_si")] - [BsonRepresentation(BsonType.String)] - public DomainId SchemaId { get; set; } - - [BsonRequired] - [BsonElement("fa")] - public bool ServeAll { get; set; } - - [BsonRequired] - [BsonElement("fp")] - public bool ServePublished { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("t")] - public T Texts { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("gf")] - public string GeoField { get; set; } - - [BsonIgnoreIfNull] - [BsonElement("go")] - [BsonJson] - [BsonRepresentation(BsonType.Document)] - public Geometry GeoObject { get; set; } - } + [BsonId] + [BsonElement("_id")] + public string Id { get; set; } + + [BsonRequired] + [BsonElement(nameof(DocId))] + public string DocId { get; set; } + + [BsonRequired] + [BsonElement("_ci")] + [BsonRepresentation(BsonType.String)] + public DomainId ContentId { get; set; } + + [BsonRequired] + [BsonElement("_ai")] + [BsonRepresentation(BsonType.String)] + public DomainId AppId { get; set; } + + [BsonRequired] + [BsonElement("_si")] + [BsonRepresentation(BsonType.String)] + public DomainId SchemaId { get; set; } + + [BsonRequired] + [BsonElement("fa")] + public bool ServeAll { get; set; } + + [BsonRequired] + [BsonElement("fp")] + public bool ServePublished { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("t")] + public T Texts { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("gf")] + public string GeoField { get; set; } + + [BsonIgnoreIfNull] + [BsonElement("go")] + [BsonJson] + [BsonRepresentation(BsonType.Document)] + public Geometry GeoObject { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntityText.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntityText.cs index 04c8f28c93..e881f2fcd4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntityText.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntityText.cs @@ -7,21 +7,20 @@ using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public sealed class MongoTextIndexEntityText { - public sealed class MongoTextIndexEntityText - { - [BsonRequired] - [BsonElement("t")] - public string Text { get; set; } + [BsonRequired] + [BsonElement("t")] + public string Text { get; set; } - [BsonIgnoreIfNull] - [BsonElement("language")] - public string Language { get; set; } = "none"; + [BsonIgnoreIfNull] + [BsonElement("language")] + public string Language { get; set; } = "none"; - public static MongoTextIndexEntityText FromText(string text) - { - return new MongoTextIndexEntityText { Text = text }; - } + public static MongoTextIndexEntityText FromText(string text) + { + return new MongoTextIndexEntityText { Text = text }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs index 0ae1e01e9c..fed0981e30 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs @@ -12,94 +12,93 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Entities.MongoDb.Text +namespace Squidex.Domain.Apps.Entities.MongoDb.Text; + +public sealed class MongoTextIndexerState : MongoRepositoryBase, ITextIndexerState, IDeleter { - public sealed class MongoTextIndexerState : MongoRepositoryBase, ITextIndexerState, IDeleter + static MongoTextIndexerState() { - static MongoTextIndexerState() + BsonClassMap.RegisterClassMap(cm => { - BsonClassMap.RegisterClassMap(cm => - { - cm.MapIdField(x => x.UniqueContentId); + cm.MapIdField(x => x.UniqueContentId); - cm.MapProperty(x => x.AppId) - .SetElementName("a"); + cm.MapProperty(x => x.AppId) + .SetElementName("a"); - cm.MapProperty(x => x.DocIdCurrent) - .SetElementName("c"); + cm.MapProperty(x => x.DocIdCurrent) + .SetElementName("c"); - cm.MapProperty(x => x.DocIdNew) - .SetElementName("n").SetIgnoreIfNull(true); + cm.MapProperty(x => x.DocIdNew) + .SetElementName("n").SetIgnoreIfNull(true); - cm.MapProperty(x => x.DocIdForPublished) - .SetElementName("p").SetIgnoreIfNull(true); - }); - } + cm.MapProperty(x => x.DocIdForPublished) + .SetElementName("p").SetIgnoreIfNull(true); + }); + } - public MongoTextIndexerState(IMongoDatabase database) - : base(database) - { - } + public MongoTextIndexerState(IMongoDatabase database) + : base(database) + { + } - protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct) + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel( - Index.Ascending(x => x.AppId)) - }, ct); - } + new CreateIndexModel( + Index.Ascending(x => x.AppId)) + }, ct); + } - protected override string CollectionName() - { - return "TextIndexerState"; - } + protected override string CollectionName() + { + return "TextIndexerState"; + } - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); - } + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } - public async Task> GetAsync(HashSet ids, - CancellationToken ct = default) - { - var entities = await Collection.Find(Filter.In(x => x.UniqueContentId, ids)).ToListAsync(ct); + public async Task> GetAsync(HashSet ids, + CancellationToken ct = default) + { + var entities = await Collection.Find(Filter.In(x => x.UniqueContentId, ids)).ToListAsync(ct); - return entities.ToDictionary(x => x.UniqueContentId); - } + return entities.ToDictionary(x => x.UniqueContentId); + } - public Task SetAsync(List updates, - CancellationToken ct = default) - { - var writes = new List>(); + public Task SetAsync(List updates, + CancellationToken ct = default) + { + var writes = new List>(); - foreach (var update in updates) + foreach (var update in updates) + { + if (update.IsDeleted) { - if (update.IsDeleted) - { - writes.Add( - new DeleteOneModel( - Filter.Eq(x => x.UniqueContentId, update.UniqueContentId))); - } - else - { - writes.Add( - new ReplaceOneModel( - Filter.Eq(x => x.UniqueContentId, update.UniqueContentId), update) - { - IsUpsert = true - }); - } + writes.Add( + new DeleteOneModel( + Filter.Eq(x => x.UniqueContentId, update.UniqueContentId))); } - - if (writes.Count == 0) + else { - return Task.CompletedTask; + writes.Add( + new ReplaceOneModel( + Filter.Eq(x => x.UniqueContentId, update.UniqueContentId), update) + { + IsUpsert = true + }); } + } - return Collection.BulkWriteAsync(writes, BulkUnordered, ct); + if (writes.Count == 0) + { + return Task.CompletedTask; } + + return Collection.BulkWriteAsync(writes, BulkUnordered, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs index a71ac826fe..93a5bdf9dc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -17,247 +17,246 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public sealed class AppProvider : IAppProvider { - public sealed class AppProvider : IAppProvider + private readonly ILocalCache localCache; + private readonly IAppsIndex indexForApps; + private readonly IRulesIndex indexForRules; + private readonly ISchemasIndex indexForSchemas; + private readonly ITeamsIndex indexForTeams; + + public AppProvider(IAppsIndex indexForApps, IRulesIndex indexForRules, ISchemasIndex indexForSchemas, ITeamsIndex indexForTeams, + ILocalCache localCache) { - private readonly ILocalCache localCache; - private readonly IAppsIndex indexForApps; - private readonly IRulesIndex indexForRules; - private readonly ISchemasIndex indexForSchemas; - private readonly ITeamsIndex indexForTeams; - - public AppProvider(IAppsIndex indexForApps, IRulesIndex indexForRules, ISchemasIndex indexForSchemas, ITeamsIndex indexForTeams, - ILocalCache localCache) - { - this.localCache = localCache; - this.indexForApps = indexForApps; - this.indexForRules = indexForRules; - this.indexForSchemas = indexForSchemas; - this.indexForTeams = indexForTeams; - } + this.localCache = localCache; + this.indexForApps = indexForApps; + this.indexForRules = indexForRules; + this.indexForSchemas = indexForSchemas; + this.indexForTeams = indexForTeams; + } - public async Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false, - CancellationToken ct = default) + public async Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false, + CancellationToken ct = default) + { + var app = await GetAppAsync(appId, canCache, ct); + + if (app == null) { - var app = await GetAppAsync(appId, canCache, ct); + return (null, null); + } - if (app == null) - { - return (null, null); - } + var schema = await GetSchemaAsync(appId, id, canCache, ct); - var schema = await GetSchemaAsync(appId, id, canCache, ct); + if (schema == null) + { + return (null, null); + } - if (schema == null) - { - return (null, null); - } + return (app, schema); + } - return (app, schema); - } + public async Task GetAppAsync(DomainId appId, bool canCache = false, + CancellationToken ct = default) + { + var cacheKey = AppCacheKey(appId); - public async Task GetAppAsync(DomainId appId, bool canCache = false, - CancellationToken ct = default) + var app = await GetOrCreate(cacheKey, () => { - var cacheKey = AppCacheKey(appId); + return indexForApps.GetAppAsync(appId, canCache, ct); + }); - var app = await GetOrCreate(cacheKey, () => - { - return indexForApps.GetAppAsync(appId, canCache, ct); - }); + if (app != null) + { + localCache.Add(AppCacheKey(app.Name), app); + } - if (app != null) - { - localCache.Add(AppCacheKey(app.Name), app); - } + return app; + } - return app; - } + public async Task GetAppAsync(string appName, bool canCache = false, + CancellationToken ct = default) + { + var cacheKey = AppCacheKey(appName); - public async Task GetAppAsync(string appName, bool canCache = false, - CancellationToken ct = default) + var app = await GetOrCreate(cacheKey, () => { - var cacheKey = AppCacheKey(appName); + return indexForApps.GetAppAsync(appName, canCache, ct); + }); - var app = await GetOrCreate(cacheKey, () => - { - return indexForApps.GetAppAsync(appName, canCache, ct); - }); + if (app != null) + { + localCache.Add(AppCacheKey(app.Id), app); + } - if (app != null) - { - localCache.Add(AppCacheKey(app.Id), app); - } + return app; + } - return app; - } + public async Task GetTeamAsync(DomainId teamId, + CancellationToken ct = default) + { + var cacheKey = TeamCacheKey(teamId); - public async Task GetTeamAsync(DomainId teamId, - CancellationToken ct = default) + var team = await GetOrCreate(cacheKey, () => { - var cacheKey = TeamCacheKey(teamId); + return indexForTeams.GetTeamAsync(teamId, ct); + }); - var team = await GetOrCreate(cacheKey, () => - { - return indexForTeams.GetTeamAsync(teamId, ct); - }); + return team; + } - return team; - } + public async Task GetSchemaAsync(DomainId appId, string name, bool canCache = false, + CancellationToken ct = default) + { + var cacheKey = SchemaCacheKey(appId, name); - public async Task GetSchemaAsync(DomainId appId, string name, bool canCache = false, - CancellationToken ct = default) + var schema = await GetOrCreate(cacheKey, () => { - var cacheKey = SchemaCacheKey(appId, name); - - var schema = await GetOrCreate(cacheKey, () => - { - return indexForSchemas.GetSchemaAsync(appId, name, canCache, ct); - }); - - if (schema != null) - { - localCache.Add(SchemaCacheKey(appId, schema.Id), schema); - } + return indexForSchemas.GetSchemaAsync(appId, name, canCache, ct); + }); - return schema; + if (schema != null) + { + localCache.Add(SchemaCacheKey(appId, schema.Id), schema); } - public async Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false, - CancellationToken ct = default) - { - var cacheKey = SchemaCacheKey(appId, id); + return schema; + } - var schema = await GetOrCreate(cacheKey, () => - { - return indexForSchemas.GetSchemaAsync(appId, id, canCache, ct); - }); + public async Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false, + CancellationToken ct = default) + { + var cacheKey = SchemaCacheKey(appId, id); - if (schema != null) - { - localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema); - } + var schema = await GetOrCreate(cacheKey, () => + { + return indexForSchemas.GetSchemaAsync(appId, id, canCache, ct); + }); - return schema; + if (schema != null) + { + localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema); } - public async Task> GetUserAppsAsync(string userId, PermissionSet permissions, - CancellationToken ct = default) + return schema; + } + + public async Task> GetUserAppsAsync(string userId, PermissionSet permissions, + CancellationToken ct = default) + { + var apps = await GetOrCreate($"GetUserApps({userId})", () => { - var apps = await GetOrCreate($"GetUserApps({userId})", () => - { - return indexForApps.GetAppsForUserAsync(userId, permissions, ct)!; - }); + return indexForApps.GetAppsForUserAsync(userId, permissions, ct)!; + }); - return apps?.ToList() ?? new List(); - } + return apps?.ToList() ?? new List(); + } - public async Task> GetTeamAppsAsync(DomainId teamId, - CancellationToken ct = default) + public async Task> GetTeamAppsAsync(DomainId teamId, + CancellationToken ct = default) + { + var apps = await GetOrCreate($"GetTeamApps({teamId})", () => { - var apps = await GetOrCreate($"GetTeamApps({teamId})", () => - { - return indexForApps.GetAppsForTeamAsync(teamId, ct)!; - }); + return indexForApps.GetAppsForTeamAsync(teamId, ct)!; + }); - return apps?.ToList() ?? new List(); - } + return apps?.ToList() ?? new List(); + } - public async Task> GetUserTeamsAsync(string userId, CancellationToken ct = default) + public async Task> GetUserTeamsAsync(string userId, CancellationToken ct = default) + { + var teams = await GetOrCreate($"GetUserTeams({userId})", () => { - var teams = await GetOrCreate($"GetUserTeams({userId})", () => - { - return indexForTeams.GetTeamsAsync(userId, ct)!; - }); + return indexForTeams.GetTeamsAsync(userId, ct)!; + }); - return teams?.ToList() ?? new List(); - } + return teams?.ToList() ?? new List(); + } - public async Task> GetSchemasAsync(DomainId appId, - CancellationToken ct = default) + public async Task> GetSchemasAsync(DomainId appId, + CancellationToken ct = default) + { + var schemas = await GetOrCreate($"GetSchemasAsync({appId})", () => { - var schemas = await GetOrCreate($"GetSchemasAsync({appId})", () => - { - return indexForSchemas.GetSchemasAsync(appId, ct)!; - }); + return indexForSchemas.GetSchemasAsync(appId, ct)!; + }); - if (schemas != null) + if (schemas != null) + { + foreach (var schema in schemas) { - foreach (var schema in schemas) - { - localCache.Add(SchemaCacheKey(appId, schema.Id), schema); - localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema); - } + localCache.Add(SchemaCacheKey(appId, schema.Id), schema); + localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema); } - - return schemas?.ToList() ?? new List(); } - public async Task> GetRulesAsync(DomainId appId, - CancellationToken ct = default) + return schemas?.ToList() ?? new List(); + } + + public async Task> GetRulesAsync(DomainId appId, + CancellationToken ct = default) + { + var rules = await GetOrCreate($"GetRulesAsync({appId})", () => { - var rules = await GetOrCreate($"GetRulesAsync({appId})", () => - { - return indexForRules.GetRulesAsync(appId, ct)!; - }); + return indexForRules.GetRulesAsync(appId, ct)!; + }); - return rules?.ToList() ?? new List(); - } + return rules?.ToList() ?? new List(); + } - public async Task GetRuleAsync(DomainId appId, DomainId id, - CancellationToken ct = default) - { - var rules = await GetRulesAsync(appId, ct); + public async Task GetRuleAsync(DomainId appId, DomainId id, + CancellationToken ct = default) + { + var rules = await GetRulesAsync(appId, ct); - return rules.Find(x => x.Id == id); - } + return rules.Find(x => x.Id == id); + } - public async Task GetOrCreate(object key, Func> creator) where T : class + public async Task GetOrCreate(object key, Func> creator) where T : class + { + if (localCache.TryGetValue(key, out var value)) { - if (localCache.TryGetValue(key, out var value)) + switch (value) { - switch (value) - { - case T typed: - return typed; - case Task typedTask: - return await typedTask; - default: - return null; - } + case T typed: + return typed; + case Task typedTask: + return await typedTask; + default: + return null; } + } - var result = creator(); + var result = creator(); - localCache.Add(key, result); + localCache.Add(key, result); - return await result; - } + return await result; + } - private static string AppCacheKey(DomainId appId) - { - return $"APPS_ID_{appId}"; - } + private static string AppCacheKey(DomainId appId) + { + return $"APPS_ID_{appId}"; + } - private static string AppCacheKey(string appName) - { - return $"APPS_NAME_{appName}"; - } + private static string AppCacheKey(string appName) + { + return $"APPS_NAME_{appName}"; + } - private static string TeamCacheKey(DomainId teamId) - { - return $"TEAMS_ID{teamId}"; - } + private static string TeamCacheKey(DomainId teamId) + { + return $"TEAMS_ID{teamId}"; + } - private static string SchemaCacheKey(DomainId appId, DomainId id) - { - return $"SCHEMAS_ID_{appId}_{id}"; - } + private static string SchemaCacheKey(DomainId appId, DomainId id) + { + return $"SCHEMAS_ID_{appId}_{id}"; + } - private static string SchemaCacheKey(DomainId appId, string name) - { - return $"SCHEMAS_NAME_{appId}_{name}"; - } + private static string SchemaCacheKey(DomainId appId, string name) + { + return $"SCHEMAS_NAME_{appId}_{name}"; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs index 5f9fae277c..972e453d0b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs @@ -10,88 +10,87 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public static class AppProviderExtensions { - public static class AppProviderExtensions + public static async Task GetComponentsAsync(this IAppProvider appProvider, ISchemaEntity schema, + CancellationToken ct = default) { - public static async Task GetComponentsAsync(this IAppProvider appProvider, ISchemaEntity schema, - CancellationToken ct = default) - { - Dictionary? result = null; + Dictionary? result = null; - var appId = schema.AppId.Id; + var appId = schema.AppId.Id; + + async Task ResolveWithIdsAsync(IField field, ReadonlyList? schemaIds) + { + if (schemaIds == null) + { + return; + } - async Task ResolveWithIdsAsync(IField field, ReadonlyList? schemaIds) + foreach (var schemaId in schemaIds) { - if (schemaIds == null) + if (schemaId == schema.Id) { - return; + result ??= new Dictionary(); + result[schemaId] = schema.SchemaDef; } - - foreach (var schemaId in schemaIds) + else if (result == null || !result.TryGetValue(schemaId, out _)) { - if (schemaId == schema.Id) + var resolvedEntity = await appProvider.GetSchemaAsync(appId, schemaId, false, ct); + + if (resolvedEntity != null) { result ??= new Dictionary(); - result[schemaId] = schema.SchemaDef; - } - else if (result == null || !result.TryGetValue(schemaId, out _)) - { - var resolvedEntity = await appProvider.GetSchemaAsync(appId, schemaId, false, ct); + result[schemaId] = resolvedEntity.SchemaDef; - if (resolvedEntity != null) - { - result ??= new Dictionary(); - result[schemaId] = resolvedEntity.SchemaDef; - - await ResolveSchemaAsync(resolvedEntity); - } + await ResolveSchemaAsync(resolvedEntity); } } } + } - async Task ResolveArrayAsync(IArrayField arrayField) + async Task ResolveArrayAsync(IArrayField arrayField) + { + foreach (var nestedField in arrayField.Fields) { - foreach (var nestedField in arrayField.Fields) - { - await ResolveFieldAsync(nestedField); - } + await ResolveFieldAsync(nestedField); } + } - async Task ResolveFieldAsync(IField field) + async Task ResolveFieldAsync(IField field) + { + switch (field) { - switch (field) - { - case IField component: - await ResolveWithIdsAsync(field, component.Properties.SchemaIds); - break; + case IField component: + await ResolveWithIdsAsync(field, component.Properties.SchemaIds); + break; - case IField components: - await ResolveWithIdsAsync(field, components.Properties.SchemaIds); - break; + case IField components: + await ResolveWithIdsAsync(field, components.Properties.SchemaIds); + break; - case IArrayField arrayField: - await ResolveArrayAsync(arrayField); - break; - } + case IArrayField arrayField: + await ResolveArrayAsync(arrayField); + break; } + } - async Task ResolveSchemaAsync(ISchemaEntity schema) + async Task ResolveSchemaAsync(ISchemaEntity schema) + { + foreach (var field in schema.SchemaDef.Fields) { - foreach (var field in schema.SchemaDef.Fields) - { - await ResolveFieldAsync(field); - } + await ResolveFieldAsync(field); } + } - await ResolveSchemaAsync(schema); + await ResolveSchemaAsync(schema); - if (result == null) - { - return ResolvedComponents.Empty; - } - - return new ResolvedComponents(result); + if (result == null) + { + return ResolvedComponents.Empty; } + + return new ResolvedComponents(result); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AlwaysCreateClientCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AlwaysCreateClientCommandMiddleware.cs index 43d801e1f0..d8c43468e6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AlwaysCreateClientCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AlwaysCreateClientCommandMiddleware.cs @@ -10,31 +10,30 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AlwaysCreateClientCommandMiddleware : ICommandMiddleware { - public sealed class AlwaysCreateClientCommandMiddleware : ICommandMiddleware + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + await next(context, ct); + + if (context.IsCompleted && context.Command is CreateApp createApp) { - await next(context, ct); + var appId = NamedId.Of(createApp.AppId, createApp.Name); - if (context.IsCompleted && context.Command is CreateApp createApp) + var publish = new Func(async command => { - var appId = NamedId.Of(createApp.AppId, createApp.Name); - - var publish = new Func(async command => - { - command.AppId = appId; + command.AppId = appId; - // If we have the app already it is not worth to cancel the step here. - var newContext = await context.CommandBus.PublishAsync(command, default); + // If we have the app already it is not worth to cancel the step here. + var newContext = await context.CommandBus.PublishAsync(command, default); - context.Complete(newContext.PlainResult); - }); + context.Complete(newContext.PlainResult); + }); - await publish(new AttachClient { Id = "default", Role = Role.Owner }); - } + await publish(new AttachClient { Id = "default", Role = Role.Owner }); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs index 06d9aa61f0..2073728b87 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEntityExtensions.cs @@ -7,13 +7,12 @@ using Squidex.Domain.Apps.Core; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public static class AppEntityExtensions { - public static class AppEntityExtensions + public static PartitionResolver PartitionResolver(this IAppEntity entity) { - public static PartitionResolver PartitionResolver(this IAppEntity entity) - { - return entity.Languages.ToResolver(); - } + return entity.Languages.ToResolver(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEventDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEventDeleter.cs index f5afaea46d..4574191b5e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEventDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEventDeleter.cs @@ -7,23 +7,22 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppEventDeleter : IDeleter { - public sealed class AppEventDeleter : IDeleter - { - private readonly IEventStore eventStore; + private readonly IEventStore eventStore; - public int Order => int.MaxValue; + public int Order => int.MaxValue; - public AppEventDeleter(IEventStore eventStore) - { - this.eventStore = eventStore; - } + public AppEventDeleter(IEventStore eventStore) + { + this.eventStore = eventStore; + } - public Task DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return eventStore.DeleteAsync($"^([a-zA-Z0-9]+)\\-{app.Id}", ct); - } + public Task DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return eventStore.DeleteAsync($"^([a-zA-Z0-9]+)\\-{app.Id}", ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs index 0a9842807a..3ff58c0a80 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppExtensions.cs @@ -9,37 +9,36 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public static class AppExtensions { - public static class AppExtensions + public static NamedId NamedId(this IAppEntity app) + { + return new NamedId(app.Id, app.Name); + } + + public static string DisplayName(this IAppEntity app) + { + return app.Label.Or(app.Name); + } + + public static bool TryGetContributorRole(this IAppEntity app, string id, bool isFrontend, [MaybeNullWhen(false)] out Role role) + { + role = null; + + return app.Contributors.TryGetValue(id, out var roleName) && app.TryGetRole(roleName, isFrontend, out role); + } + + public static bool TryGetClientRole(this IAppEntity app, string id, bool isFrontend, [MaybeNullWhen(false)] out Role role) + { + role = null; + + return app.Clients.TryGetValue(id, out var client) && app.TryGetRole(client.Role, isFrontend, out role); + } + + public static bool TryGetRole(this IAppEntity app, string roleName, bool isFrontend, [MaybeNullWhen(false)] out Role role) { - public static NamedId NamedId(this IAppEntity app) - { - return new NamedId(app.Id, app.Name); - } - - public static string DisplayName(this IAppEntity app) - { - return app.Label.Or(app.Name); - } - - public static bool TryGetContributorRole(this IAppEntity app, string id, bool isFrontend, [MaybeNullWhen(false)] out Role role) - { - role = null; - - return app.Contributors.TryGetValue(id, out var roleName) && app.TryGetRole(roleName, isFrontend, out role); - } - - public static bool TryGetClientRole(this IAppEntity app, string id, bool isFrontend, [MaybeNullWhen(false)] out Role role) - { - role = null; - - return app.Clients.TryGetValue(id, out var client) && app.TryGetRole(client.Role, isFrontend, out role); - } - - public static bool TryGetRole(this IAppEntity app, string roleName, bool isFrontend, [MaybeNullWhen(false)] out Role role) - { - return app.Roles.TryGet(app.Name, roleName, isFrontend, out role); - } + return app.Roles.TryGet(app.Name, roleName, isFrontend, out role); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index dd931913fd..44ef93b3e7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -11,191 +11,190 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppHistoryEventsCreator : HistoryEventsCreatorBase { - public sealed class AppHistoryEventsCreator : HistoryEventsCreatorBase + public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) { - public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage( - "history.apps.created"); + AddEventMessage( + "history.apps.created"); - AddEventMessage( - "history.apps.assetScriptsConfigured"); + AddEventMessage( + "history.apps.assetScriptsConfigured"); - AddEventMessage( - "history.apps.clientAdded"); + AddEventMessage( + "history.apps.clientAdded"); - AddEventMessage( - "history.apps.clientRevoked"); + AddEventMessage( + "history.apps.clientRevoked"); - AddEventMessage( - "history.apps.clientUpdated"); + AddEventMessage( + "history.apps.clientUpdated"); - AddEventMessage( - "history.apps.contributoreAssigned"); + AddEventMessage( + "history.apps.contributoreAssigned"); - AddEventMessage( - "history.apps.contributoreRemoved"); + AddEventMessage( + "history.apps.contributoreRemoved"); - AddEventMessage( - "history.apps.imageRemoved"); + AddEventMessage( + "history.apps.imageRemoved"); - AddEventMessage( - "history.apps.imageUploaded"); + AddEventMessage( + "history.apps.imageUploaded"); - AddEventMessage( - "history.apps.languagedAdded"); + AddEventMessage( + "history.apps.languagedAdded"); - AddEventMessage( - "history.apps.languagedRemoved"); + AddEventMessage( + "history.apps.languagedRemoved"); - AddEventMessage( - "history.apps.languagedUpdated"); + AddEventMessage( + "history.apps.languagedUpdated"); - AddEventMessage( - "history.apps.languagedSetToMaster"); + AddEventMessage( + "history.apps.languagedSetToMaster"); - AddEventMessage( - "history.apps.planChanged"); + AddEventMessage( + "history.apps.planChanged"); - AddEventMessage( - "history.apps.planReset"); + AddEventMessage( + "history.apps.planReset"); - AddEventMessage( - "history.apps.roleAdded"); + AddEventMessage( + "history.apps.roleAdded"); - AddEventMessage( - "history.apps.roleDeleted"); + AddEventMessage( + "history.apps.roleDeleted"); - AddEventMessage( - "history.apps.roleUpdated"); + AddEventMessage( + "history.apps.roleUpdated"); - AddEventMessage( - "history.apps.settingsUpdated"); + AddEventMessage( + "history.apps.settingsUpdated"); - AddEventMessage( - "history.apps.transfered"); + AddEventMessage( + "history.apps.transfered"); - AddEventMessage( - "history.apps.common.updated"); + AddEventMessage( + "history.apps.common.updated"); - AddEventMessage( - "history.apps.workflowAdded"); + AddEventMessage( + "history.apps.workflowAdded"); - AddEventMessage( - "history.apps.workflowDeleted"); + AddEventMessage( + "history.apps.workflowDeleted"); - AddEventMessage( - "history.apps.workflowUpdated"); - } + AddEventMessage( + "history.apps.workflowUpdated"); + } - private HistoryEvent? CreateEvent(IEvent @event) + private HistoryEvent? CreateEvent(IEvent @event) + { + switch (@event) { - switch (@event) - { - case AppCreated e: - return CreateGeneralEvent(e); - case AppContributorAssigned e: - return CreateContributorsEvent(e, e.ContributorId, e.Role); - case AppContributorRemoved e: - return CreateContributorsEvent(e, e.ContributorId); - case AppClientAttached e: - return CreateClientsEvent(e, e.Id); - case AppClientUpdated e: - return CreateClientsEvent(e, e.Id); - case AppClientRevoked e: - return CreateClientsEvent(e, e.Id); - case AppLanguageAdded e: - return CreateLanguagesEvent(e, e.Language); - case AppLanguageUpdated e: - return CreateLanguagesEvent(e, e.Language); - case AppMasterLanguageSet e: - return CreateLanguagesEvent(e, e.Language); - case AppLanguageRemoved e: - return CreateLanguagesEvent(e, e.Language); - case AppRoleAdded e: - return CreateRolesEvent(e, e.Name); - case AppRoleUpdated e: - return CreateRolesEvent(e, e.Name); - case AppRoleDeleted e: - return CreateRolesEvent(e, e.Name); - case AppPlanChanged e: - return CreatePlansEvent(e, e.PlanId); - case AppPlanReset e: - return CreatePlansEvent(e); - case AppSettingsUpdated e: - return CreateUIEvent(e); - case AppAssetsScriptsConfigured e: - return CreateAssetScriptsEvent(e); - case AppUpdated e: - return CreateGeneralEvent(e); - case AppTransfered e: - return CreateGeneralEvent(e); - case AppImageUploaded e: - return CreateGeneralEvent(e); - case AppImageRemoved e: - return CreateGeneralEvent(e); - case AppWorkflowAdded e: - return CreateWorkflowEvent(e, e.Name); - case AppWorkflowUpdated e: - return CreateWorkflowEvent(e); - case AppWorkflowDeleted e: - return CreateWorkflowEvent(e); - } - - return null; + case AppCreated e: + return CreateGeneralEvent(e); + case AppContributorAssigned e: + return CreateContributorsEvent(e, e.ContributorId, e.Role); + case AppContributorRemoved e: + return CreateContributorsEvent(e, e.ContributorId); + case AppClientAttached e: + return CreateClientsEvent(e, e.Id); + case AppClientUpdated e: + return CreateClientsEvent(e, e.Id); + case AppClientRevoked e: + return CreateClientsEvent(e, e.Id); + case AppLanguageAdded e: + return CreateLanguagesEvent(e, e.Language); + case AppLanguageUpdated e: + return CreateLanguagesEvent(e, e.Language); + case AppMasterLanguageSet e: + return CreateLanguagesEvent(e, e.Language); + case AppLanguageRemoved e: + return CreateLanguagesEvent(e, e.Language); + case AppRoleAdded e: + return CreateRolesEvent(e, e.Name); + case AppRoleUpdated e: + return CreateRolesEvent(e, e.Name); + case AppRoleDeleted e: + return CreateRolesEvent(e, e.Name); + case AppPlanChanged e: + return CreatePlansEvent(e, e.PlanId); + case AppPlanReset e: + return CreatePlansEvent(e); + case AppSettingsUpdated e: + return CreateUIEvent(e); + case AppAssetsScriptsConfigured e: + return CreateAssetScriptsEvent(e); + case AppUpdated e: + return CreateGeneralEvent(e); + case AppTransfered e: + return CreateGeneralEvent(e); + case AppImageUploaded e: + return CreateGeneralEvent(e); + case AppImageRemoved e: + return CreateGeneralEvent(e); + case AppWorkflowAdded e: + return CreateWorkflowEvent(e, e.Name); + case AppWorkflowUpdated e: + return CreateWorkflowEvent(e); + case AppWorkflowDeleted e: + return CreateWorkflowEvent(e); } - private HistoryEvent CreateGeneralEvent(IEvent e) - { - return ForEvent(e, "settings.general"); - } + return null; + } - private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null) - { - return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); - } + private HistoryEvent CreateGeneralEvent(IEvent e) + { + return ForEvent(e, "settings.general"); + } - private HistoryEvent CreateLanguagesEvent(IEvent e, Language language) - { - return ForEvent(e, "settings.languages").Param("Language", language); - } + private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null) + { + return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); + } - private HistoryEvent CreateRolesEvent(IEvent e, string name) - { - return ForEvent(e, "settings.roles").Param("Name", name); - } + private HistoryEvent CreateLanguagesEvent(IEvent e, Language language) + { + return ForEvent(e, "settings.languages").Param("Language", language); + } - private HistoryEvent CreateClientsEvent(IEvent e, string id) - { - return ForEvent(e, "settings.clients").Param("Id", id); - } + private HistoryEvent CreateRolesEvent(IEvent e, string name) + { + return ForEvent(e, "settings.roles").Param("Name", name); + } - private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) - { - return ForEvent(e, "settings.plan").Param("Plan", plan); - } + private HistoryEvent CreateClientsEvent(IEvent e, string id) + { + return ForEvent(e, "settings.clients").Param("Id", id); + } - private HistoryEvent CreateAssetScriptsEvent(IEvent e) - { - return ForEvent(e, "settings.assetScripts"); - } + private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) + { + return ForEvent(e, "settings.plan").Param("Plan", plan); + } - private HistoryEvent CreateWorkflowEvent(IEvent e, string? name = null) - { - return ForEvent(e, "settings.workflows").Param("Name", name); - } + private HistoryEvent CreateAssetScriptsEvent(IEvent e) + { + return ForEvent(e, "settings.assetScripts"); + } - private HistoryEvent CreateUIEvent(IEvent e) - { - return ForEvent(e, "settings.ui"); - } + private HistoryEvent CreateWorkflowEvent(IEvent e, string? name = null) + { + return ForEvent(e, "settings.workflows").Param("Name", name); + } - protected override Task CreateEventCoreAsync(Envelope @event) - { - return Task.FromResult(CreateEvent(@event.Payload)); - } + private HistoryEvent CreateUIEvent(IEvent e) + { + return ForEvent(e, "settings.ui"); + } + + protected override Task CreateEventCoreAsync(Envelope @event) + { + return Task.FromResult(CreateEvent(@event.Payload)); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs index 62c255e5b8..673a88bc79 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs @@ -12,97 +12,96 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppPermanentDeleter : IEventConsumer { - public sealed class AppPermanentDeleter : IEventConsumer + private readonly IEnumerable deleters; + private readonly IDomainObjectFactory factory; + private readonly HashSet consumingTypes; + + public string Name { - private readonly IEnumerable deleters; - private readonly IDomainObjectFactory factory; - private readonly HashSet consumingTypes; + get => GetType().Name; + } - public string Name - { - get => GetType().Name; - } + public string EventsFilter + { + get => "^app-"; + } - public string EventsFilter - { - get => "^app-"; - } + public AppPermanentDeleter(IEnumerable deleters, IDomainObjectFactory factory, TypeNameRegistry typeNameRegistry) + { + this.deleters = deleters.OrderBy(x => x.Order).ToList(); + this.factory = factory; - public AppPermanentDeleter(IEnumerable deleters, IDomainObjectFactory factory, TypeNameRegistry typeNameRegistry) + // Compute the event types names once for performance reasons and use hashset for extensibility. + consumingTypes = new HashSet { - this.deleters = deleters.OrderBy(x => x.Order).ToList(); - this.factory = factory; + typeNameRegistry.GetName(), + typeNameRegistry.GetName() + }; + } - // Compute the event types names once for performance reasons and use hashset for extensibility. - consumingTypes = new HashSet - { - typeNameRegistry.GetName(), - typeNameRegistry.GetName() - }; - } + public bool Handles(StoredEvent @event) + { + return consumingTypes.Contains(@event.Data.Type); + } - public bool Handles(StoredEvent @event) + public async Task On(Envelope @event) + { + if (@event.Headers.Restored()) { - return consumingTypes.Contains(@event.Data.Type); + return; } - public async Task On(Envelope @event) + switch (@event.Payload) { - if (@event.Headers.Restored()) - { - return; - } - - switch (@event.Payload) - { - case AppDeleted appArchived: - await OnArchiveAsync(appArchived); - break; - case AppContributorRemoved appContributorRemoved: - await OnAppContributorRemoved(appContributorRemoved); - break; - } + case AppDeleted appArchived: + await OnArchiveAsync(appArchived); + break; + case AppContributorRemoved appContributorRemoved: + await OnAppContributorRemoved(appContributorRemoved); + break; } + } - private async Task OnAppContributorRemoved(AppContributorRemoved appContributorRemoved) + private async Task OnAppContributorRemoved(AppContributorRemoved appContributorRemoved) + { + using (Telemetry.Activities.StartActivity("RemoveContributorFromSystem")) { - using (Telemetry.Activities.StartActivity("RemoveContributorFromSystem")) - { - var appId = appContributorRemoved.AppId.Id; + var appId = appContributorRemoved.AppId.Id; - foreach (var deleter in deleters) + foreach (var deleter in deleters) + { + using (Telemetry.Activities.StartActivity(deleter.GetType().Name)) { - using (Telemetry.Activities.StartActivity(deleter.GetType().Name)) - { - await deleter.DeleteContributorAsync(appId, appContributorRemoved.ContributorId, default); - } + await deleter.DeleteContributorAsync(appId, appContributorRemoved.ContributorId, default); } } } + } - private async Task OnArchiveAsync(AppDeleted appArchived) + private async Task OnArchiveAsync(AppDeleted appArchived) + { + using (Telemetry.Activities.StartActivity("RemoveAppFromSystem")) { - using (Telemetry.Activities.StartActivity("RemoveAppFromSystem")) - { - // Bypass our normal app resolve process, so that we can also retrieve the deleted app. - var app = factory.Create(appArchived.AppId.Id); + // Bypass our normal app resolve process, so that we can also retrieve the deleted app. + var app = factory.Create(appArchived.AppId.Id); - await app.EnsureLoadedAsync(); + await app.EnsureLoadedAsync(); - // If the app does not exist, the version is lower than zero. - if (app.Version < 0) - { - return; - } + // If the app does not exist, the version is lower than zero. + if (app.Version < 0) + { + return; + } - foreach (var deleter in deleters) + foreach (var deleter in deleters) + { + using (Telemetry.Activities.StartActivity(deleter.GetType().Name)) { - using (Telemetry.Activities.StartActivity(deleter.GetType().Name)) - { - await deleter.DeleteAppAsync(app.Snapshot, default); - } + await deleter.DeleteAppAsync(app.Snapshot, default); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs index 80bfa16df9..0b3ea3c7ff 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs @@ -10,72 +10,71 @@ using Squidex.Infrastructure; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppSettingsSearchSource : ISearchSource { - public sealed class AppSettingsSearchSource : ISearchSource - { - private const int MaxItems = 3; - private readonly IUrlGenerator urlGenerator; + private const int MaxItems = 3; + private readonly IUrlGenerator urlGenerator; - public AppSettingsSearchSource(IUrlGenerator urlGenerator) - { - this.urlGenerator = urlGenerator; - } + public AppSettingsSearchSource(IUrlGenerator urlGenerator) + { + this.urlGenerator = urlGenerator; + } - public Task SearchAsync(string query, Context context, - CancellationToken ct) - { - var result = new SearchResults(); + public Task SearchAsync(string query, Context context, + CancellationToken ct) + { + var result = new SearchResults(); - var appId = context.App.NamedId(); + var appId = context.App.NamedId(); - void Search(string term, string permissionId, Func, string> generate, SearchResultType type) + void Search(string term, string permissionId, Func, string> generate, SearchResultType type) + { + if (result.Count < MaxItems && term.Contains(query, StringComparison.OrdinalIgnoreCase)) { - if (result.Count < MaxItems && term.Contains(query, StringComparison.OrdinalIgnoreCase)) + if (context.Allows(permissionId)) { - if (context.Allows(permissionId)) - { - var url = generate(appId); + var url = generate(appId); - result.Add(term, type, url); - } + result.Add(term, type, url); } } + } - Search("Assets", PermissionIds.AppAssetsRead, - a => urlGenerator.AssetsUI(a), SearchResultType.Asset); + Search("Assets", PermissionIds.AppAssetsRead, + a => urlGenerator.AssetsUI(a), SearchResultType.Asset); - Search("Backups", PermissionIds.AppBackupsRead, - urlGenerator.BackupsUI, SearchResultType.Setting); + Search("Backups", PermissionIds.AppBackupsRead, + urlGenerator.BackupsUI, SearchResultType.Setting); - Search("Clients", PermissionIds.AppClientsRead, - urlGenerator.ClientsUI, SearchResultType.Setting); + Search("Clients", PermissionIds.AppClientsRead, + urlGenerator.ClientsUI, SearchResultType.Setting); - Search("Contributors", PermissionIds.AppContributorsRead, - urlGenerator.ContributorsUI, SearchResultType.Setting); + Search("Contributors", PermissionIds.AppContributorsRead, + urlGenerator.ContributorsUI, SearchResultType.Setting); - Search("Dashboard", PermissionIds.AppUsage, - urlGenerator.DashboardUI, SearchResultType.Dashboard); + Search("Dashboard", PermissionIds.AppUsage, + urlGenerator.DashboardUI, SearchResultType.Dashboard); - Search("Languages", PermissionIds.AppLanguagesRead, - urlGenerator.LanguagesUI, SearchResultType.Setting); + Search("Languages", PermissionIds.AppLanguagesRead, + urlGenerator.LanguagesUI, SearchResultType.Setting); - Search("Roles", PermissionIds.AppRolesRead, - urlGenerator.RolesUI, SearchResultType.Setting); + Search("Roles", PermissionIds.AppRolesRead, + urlGenerator.RolesUI, SearchResultType.Setting); - Search("Rules", PermissionIds.AppRulesRead, - urlGenerator.RulesUI, SearchResultType.Rule); + Search("Rules", PermissionIds.AppRulesRead, + urlGenerator.RulesUI, SearchResultType.Rule); - Search("Schemas", PermissionIds.AppSchemasRead, - urlGenerator.SchemasUI, SearchResultType.Schema); + Search("Schemas", PermissionIds.AppSchemasRead, + urlGenerator.SchemasUI, SearchResultType.Schema); - Search("Subscription", PermissionIds.AppPlansRead, - urlGenerator.PlansUI, SearchResultType.Setting); + Search("Subscription", PermissionIds.AppPlansRead, + urlGenerator.PlansUI, SearchResultType.Setting); - Search("Workflows", PermissionIds.AppWorkflowsRead, - urlGenerator.WorkflowsUI, SearchResultType.Setting); + Search("Workflows", PermissionIds.AppWorkflowsRead, + urlGenerator.WorkflowsUI, SearchResultType.Setting); - return Task.FromResult(result); - } + return Task.FromResult(result); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs index e75b8f3f21..0deea73e10 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs @@ -9,186 +9,185 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppUISettings : IAppUISettings, IDeleter { - public sealed class AppUISettings : IAppUISettings, IDeleter + private readonly IPersistenceFactory persistanceFactory; + + [CollectionName("UISettings")] + public sealed class State { - private readonly IPersistenceFactory persistanceFactory; + public JsonObject Settings { get; set; } = new JsonObject(); - [CollectionName("UISettings")] - public sealed class State + public bool Set(JsonObject settings) { - public JsonObject Settings { get; set; } = new JsonObject(); + var isChanged = false; - public bool Set(JsonObject settings) + if (!Settings.Equals(settings)) { - var isChanged = false; - - if (!Settings.Equals(settings)) - { - Settings = settings; - isChanged = true; - } - - return isChanged; + Settings = settings; + isChanged = true; } - public bool Set(string path, JsonValue value) - { - var container = GetContainer(path, true, out var key); - - if (container == null) - { - ThrowHelper.InvalidOperationException("Path does not lead to an object."); - return false; - } + return isChanged; + } - if (!container.TryGetValue(key, out var existing) || !existing.Equals(value)) - { - container[key] = value; - return true; - } + public bool Set(string path, JsonValue value) + { + var container = GetContainer(path, true, out var key); + if (container == null) + { + ThrowHelper.InvalidOperationException("Path does not lead to an object."); return false; } - public bool Remove(string path) + if (!container.TryGetValue(key, out var existing) || !existing.Equals(value)) { - var container = GetContainer(path, false, out var key); + container[key] = value; + return true; + } - if (container == null) - { - return false; - } + return false; + } - return container.Remove(key); - } + public bool Remove(string path) + { + var container = GetContainer(path, false, out var key); - private JsonObject? GetContainer(string path, bool add, out string key) + if (container == null) { - Guard.NotNullOrEmpty(path); + return false; + } + + return container.Remove(key); + } + + private JsonObject? GetContainer(string path, bool add, out string key) + { + Guard.NotNullOrEmpty(path); - var segments = path.Split('.'); + var segments = path.Split('.'); - key = segments[^1]; + key = segments[^1]; - var current = Settings; + var current = Settings; - if (segments.Length > 1) + if (segments.Length > 1) + { + foreach (var segment in segments.Take(segments.Length - 1)) { - foreach (var segment in segments.Take(segments.Length - 1)) + if (!current.TryGetValue(segment, out var found)) { - if (!current.TryGetValue(segment, out var found)) + if (add) { - if (add) - { - found = new JsonObject(); - - current[segment] = found; - } - else - { - return null; - } - } + found = new JsonObject(); - if (found.Value is JsonObject o) - { - current = o; + current[segment] = found; } else { return null; } } - } - return current; + if (found.Value is JsonObject o) + { + current = o; + } + else + { + return null; + } + } } - } - public AppUISettings(IPersistenceFactory persistanceFactory) - { - this.persistanceFactory = persistanceFactory; + return current; } + } - async Task IDeleter.DeleteContributorAsync(DomainId appId, string contributorId, - CancellationToken ct) - { - await ClearAsync(appId, contributorId, ct); - } + public AppUISettings(IPersistenceFactory persistanceFactory) + { + this.persistanceFactory = persistanceFactory; + } - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - await ClearAsync(app.Id, null, ct); + async Task IDeleter.DeleteContributorAsync(DomainId appId, string contributorId, + CancellationToken ct) + { + await ClearAsync(appId, contributorId, ct); + } - foreach (var userId in app.Contributors.Keys) - { - await ClearAsync(app.Id, userId, ct); - } - } + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await ClearAsync(app.Id, null, ct); - public async Task GetAsync(DomainId appId, string? userId, - CancellationToken ct = default) + foreach (var userId in app.Contributors.Keys) { - var state = await GetStateAsync(appId, userId, ct); - - return state.Value.Settings; + await ClearAsync(app.Id, userId, ct); } + } - public async Task RemoveAsync(DomainId appId, string? userId, string path, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, userId, ct); + public async Task GetAsync(DomainId appId, string? userId, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, userId, ct); - await state.UpdateAsync(s => s.Remove(path), ct: ct); - } + return state.Value.Settings; + } - public async Task SetAsync(DomainId appId, string? userId, string path, JsonValue value, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, userId, ct); + public async Task RemoveAsync(DomainId appId, string? userId, string path, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, userId, ct); - await state.UpdateAsync(s => s.Set(path, value), ct: ct); - } + await state.UpdateAsync(s => s.Remove(path), ct: ct); + } - public async Task SetAsync(DomainId appId, string? userId, JsonObject settings, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, userId, ct); + public async Task SetAsync(DomainId appId, string? userId, string path, JsonValue value, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, userId, ct); - await state.UpdateAsync(s => s.Set(settings), ct: ct); - } + await state.UpdateAsync(s => s.Set(path, value), ct: ct); + } - public async Task ClearAsync(DomainId appId, string? userId, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, userId, ct); + public async Task SetAsync(DomainId appId, string? userId, JsonObject settings, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, userId, ct); - await state.ClearAsync(ct); - } + await state.UpdateAsync(s => s.Set(settings), ct: ct); + } - private async Task> GetStateAsync(DomainId appId, string? userId, - CancellationToken ct) - { - var state = new SimpleState(persistanceFactory, GetType(), GetKey(appId, userId)); + public async Task ClearAsync(DomainId appId, string? userId, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, userId, ct); - await state.LoadAsync(ct); + await state.ClearAsync(ct); + } - return state; - } + private async Task> GetStateAsync(DomainId appId, string? userId, + CancellationToken ct) + { + var state = new SimpleState(persistanceFactory, GetType(), GetKey(appId, userId)); - private static string GetKey(DomainId appId, string? userId) + await state.LoadAsync(ct); + + return state; + } + + private static string GetKey(DomainId appId, string? userId) + { + if (!string.IsNullOrWhiteSpace(userId)) { - if (!string.IsNullOrWhiteSpace(userId)) - { - return $"{appId}_{userId}"; - } - else - { - return $"{appId}"; - } + return $"{appId}_{userId}"; + } + else + { + return $"{appId}"; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUsageDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUsageDeleter.cs index 35eb09680b..5475df784f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUsageDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUsageDeleter.cs @@ -7,21 +7,20 @@ using Squidex.Infrastructure.UsageTracking; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppUsageDeleter : IDeleter { - public sealed class AppUsageDeleter : IDeleter - { - private readonly IApiUsageTracker apiUsageTracker; + private readonly IApiUsageTracker apiUsageTracker; - public AppUsageDeleter(IApiUsageTracker apiUsageTracker) - { - this.apiUsageTracker = apiUsageTracker; - } + public AppUsageDeleter(IApiUsageTracker apiUsageTracker) + { + this.apiUsageTracker = apiUsageTracker; + } - public Task DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return apiUsageTracker.DeleteAsync(app.Id.ToString(), ct); - } + public Task DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return apiUsageTracker.DeleteAsync(app.Id.ToString(), ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index 08a681bc52..9ad459b6b8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -15,161 +15,160 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class BackupApps : IBackupHandler { - public sealed class BackupApps : IBackupHandler + private const string SettingsFile = "Settings.json"; + private const string AvatarFile = "Avatar.image"; + private readonly Rebuilder rebuilder; + private readonly IAppImageStore appImageStore; + private readonly IAppsIndex appsIndex; + private readonly IAppUISettings appUISettings; + private string? appReservation; + + public string Name { get; } = "Apps"; + + public BackupApps( + Rebuilder rebuilder, + IAppImageStore appImageStore, + IAppsIndex appsIndex, + IAppUISettings appUISettings) { - private const string SettingsFile = "Settings.json"; - private const string AvatarFile = "Avatar.image"; - private readonly Rebuilder rebuilder; - private readonly IAppImageStore appImageStore; - private readonly IAppsIndex appsIndex; - private readonly IAppUISettings appUISettings; - private string? appReservation; - - public string Name { get; } = "Apps"; - - public BackupApps( - Rebuilder rebuilder, - IAppImageStore appImageStore, - IAppsIndex appsIndex, - IAppUISettings appUISettings) - { - this.appsIndex = appsIndex; - this.appImageStore = appImageStore; - this.appUISettings = appUISettings; - this.rebuilder = rebuilder; - } + this.appsIndex = appsIndex; + this.appImageStore = appImageStore; + this.appUISettings = appUISettings; + this.rebuilder = rebuilder; + } - public async Task BackupEventAsync(Envelope @event, BackupContext context, - CancellationToken ct) + public async Task BackupEventAsync(Envelope @event, BackupContext context, + CancellationToken ct) + { + switch (@event.Payload) { - switch (@event.Payload) - { - case AppContributorAssigned appContributorAssigned: - context.UserMapping.Backup(appContributorAssigned.ContributorId); - break; - case AppImageUploaded: - await WriteAssetAsync(context.AppId, context.Writer, ct); - break; - } + case AppContributorAssigned appContributorAssigned: + context.UserMapping.Backup(appContributorAssigned.ContributorId); + break; + case AppImageUploaded: + await WriteAssetAsync(context.AppId, context.Writer, ct); + break; } + } - public async Task BackupAsync(BackupContext context, - CancellationToken ct) - { - var json = await appUISettings.GetAsync(context.AppId, null, ct); + public async Task BackupAsync(BackupContext context, + CancellationToken ct) + { + var json = await appUISettings.GetAsync(context.AppId, null, ct); - await context.Writer.WriteJsonAsync(SettingsFile, json, ct); - } + await context.Writer.WriteJsonAsync(SettingsFile, json, ct); + } - public async Task RestoreEventAsync(Envelope @event, RestoreContext context, - CancellationToken ct) + public async Task RestoreEventAsync(Envelope @event, RestoreContext context, + CancellationToken ct) + { + switch (@event.Payload) { - switch (@event.Payload) - { - case AppCreated appCreated: - { - await ReserveAppAsync(context.AppId, appCreated.Name, ct); + case AppCreated appCreated: + { + await ReserveAppAsync(context.AppId, appCreated.Name, ct); - break; - } + break; + } - case AppImageUploaded: - { - await ReadAssetAsync(context.AppId, context.Reader, ct); + case AppImageUploaded: + { + await ReadAssetAsync(context.AppId, context.Reader, ct); - break; - } + break; + } - case AppContributorAssigned contributorAssigned: + case AppContributorAssigned contributorAssigned: + { + if (!context.UserMapping.TryMap(contributorAssigned.ContributorId, out var user) || user.Equals(context.Initiator)) { - if (!context.UserMapping.TryMap(contributorAssigned.ContributorId, out var user) || user.Equals(context.Initiator)) - { - return false; - } - - contributorAssigned.ContributorId = user.Identifier; - break; + return false; } - case AppContributorRemoved contributorRemoved: - { - if (!context.UserMapping.TryMap(contributorRemoved.ContributorId, out var user) || user.Equals(context.Initiator)) - { - return false; - } + contributorAssigned.ContributorId = user.Identifier; + break; + } - contributorRemoved.ContributorId = user.Identifier; - break; + case AppContributorRemoved contributorRemoved: + { + if (!context.UserMapping.TryMap(contributorRemoved.ContributorId, out var user) || user.Equals(context.Initiator)) + { + return false; } - } - return true; + contributorRemoved.ContributorId = user.Identifier; + break; + } } - public async Task RestoreAsync(RestoreContext context, - CancellationToken ct) - { - var json = await context.Reader.ReadJsonAsync(SettingsFile, ct); + return true; + } - await appUISettings.SetAsync(context.AppId, null, json, ct); - } + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) + { + var json = await context.Reader.ReadJsonAsync(SettingsFile, ct); - private async Task ReserveAppAsync(DomainId appId, string appName, - CancellationToken ct) - { - appReservation = await appsIndex.ReserveAsync(appId, appName, ct); + await appUISettings.SetAsync(context.AppId, null, json, ct); + } - if (appReservation == null) - { - throw new BackupRestoreException("The app id or name is not available."); - } - } + private async Task ReserveAppAsync(DomainId appId, string appName, + CancellationToken ct) + { + appReservation = await appsIndex.ReserveAsync(appId, appName, ct); - public async Task CleanupRestoreErrorAsync(DomainId appId) + if (appReservation == null) { - await appsIndex.RemoveReservationAsync(appReservation); + throw new BackupRestoreException("The app id or name is not available."); } + } - public async Task CompleteRestoreAsync(RestoreContext context, string appName) - { - await rebuilder.InsertManyAsync(Enumerable.Repeat(context.AppId, 1), 1, default); + public async Task CleanupRestoreErrorAsync(DomainId appId) + { + await appsIndex.RemoveReservationAsync(appReservation); + } - await appsIndex.RemoveReservationAsync(appReservation); - } + public async Task CompleteRestoreAsync(RestoreContext context, string appName) + { + await rebuilder.InsertManyAsync(Enumerable.Repeat(context.AppId, 1), 1, default); + + await appsIndex.RemoveReservationAsync(appReservation); + } - private async Task WriteAssetAsync(DomainId appId, IBackupWriter writer, - CancellationToken ct) + private async Task WriteAssetAsync(DomainId appId, IBackupWriter writer, + CancellationToken ct) + { + try { - try - { - await using (var stream = await writer.OpenBlobAsync(AvatarFile, ct)) - { - await appImageStore.DownloadAsync(appId, stream, ct); - } - } - catch (AssetNotFoundException) + await using (var stream = await writer.OpenBlobAsync(AvatarFile, ct)) { + await appImageStore.DownloadAsync(appId, stream, ct); } } + catch (AssetNotFoundException) + { + } + } - private async Task ReadAssetAsync(DomainId appId, IBackupReader reader, - CancellationToken ct) + private async Task ReadAssetAsync(DomainId appId, IBackupReader reader, + CancellationToken ct) + { + try { - try - { - await using (var stream = await reader.OpenBlobAsync(AvatarFile, ct)) - { - await appImageStore.UploadAsync(appId, stream, ct); - } - } - catch (AssetAlreadyExistsException) - { - } - catch (FileNotFoundException) + await using (var stream = await reader.OpenBlobAsync(AvatarFile, ct)) { + await appImageStore.UploadAsync(appId, stream, ct); } } + catch (AssetAlreadyExistsException) + { + } + catch (FileNotFoundException) + { + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs index 0ce7500b3b..62a3926ab9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class AddLanguage : AppCommand { - public sealed class AddLanguage : AppCommand - { - public Language Language { get; set; } - } + public Language Language { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs index c1623759f8..1c6f23bdf4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class AddRole : AppCommand { - public sealed class AddRole : AppCommand - { - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs index 12c39549e5..f9628650f4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class AddWorkflow : AppCommand { - public sealed class AddWorkflow : AppCommand - { - public DomainId WorkflowId { get; set; } + public DomainId WorkflowId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public AddWorkflow() - { - WorkflowId = DomainId.NewGuid(); - } + public AddWorkflow() + { + WorkflowId = DomainId.NewGuid(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs index 7e28f64d1b..652bc9e699 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs @@ -7,18 +7,17 @@ using Roles = Squidex.Domain.Apps.Core.Apps.Role; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class AssignContributor : AppCommand { - public sealed class AssignContributor : AppCommand - { - public string ContributorId { get; set; } + public string ContributorId { get; set; } - public string Role { get; set; } = Roles.Editor; + public string Role { get; set; } = Roles.Editor; - public bool IgnoreActor { get; set; } + public bool IgnoreActor { get; set; } - public bool IgnorePlans { get; set; } + public bool IgnorePlans { get; set; } - public bool Invite { get; set; } - } + public bool Invite { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs index 12cca6c652..5046ead9db 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs @@ -7,19 +7,18 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class AttachClient : AppCommand { - public sealed class AttachClient : AppCommand - { - public string Id { get; set; } + public string Id { get; set; } - public string Secret { get; set; } + public string Secret { get; set; } - public string? Role { get; set; } + public string? Role { get; set; } - public AttachClient() - { - Secret = RandomHash.New(); - } + public AttachClient() + { + Secret = RandomHash.New(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs index 15be5174b3..370a30877e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class ChangePlan : AppCommand { - public sealed class ChangePlan : AppCommand - { - public bool FromCallback { get; set; } + public bool FromCallback { get; set; } - public string PlanId { get; set; } - } + public string PlanId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs index 13f5bf92d4..6143095052 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Core.Assets; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class ConfigureAssetScripts : AppCommand { - public sealed class ConfigureAssetScripts : AppCommand - { - public AssetScripts? Scripts { get; set; } - } + public AssetScripts? Scripts { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs index 72526a486e..ed277e2b4c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs @@ -8,24 +8,23 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class CreateApp : AppCommandBase, IAggregateCommand { - public sealed class CreateApp : AppCommandBase, IAggregateCommand - { - public DomainId AppId { get; set; } + public DomainId AppId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string? Template { get; set; } + public string? Template { get; set; } - public override DomainId AggregateId - { - get => AppId; - } + public override DomainId AggregateId + { + get => AppId; + } - public CreateApp() - { - AppId = DomainId.NewGuid(); - } + public CreateApp() + { + AppId = DomainId.NewGuid(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs index 8ea5a11c49..03a02402b0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class DeleteApp : AppCommand { - public sealed class DeleteApp : AppCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs index d18e03892f..27cc049056 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class DeleteRole : AppCommand { - public sealed class DeleteRole : AppCommand - { - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs index 98b29fc7dc..b4a4d588dd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class DeleteWorkflow : AppCommand { - public sealed class DeleteWorkflow : AppCommand - { - public DomainId WorkflowId { get; set; } - } + public DomainId WorkflowId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs index c88fa5c569..6aa1e7aeb8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveAppImage.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class RemoveAppImage : AppCommand { - public sealed class RemoveAppImage : AppCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs index d61035173d..87bd0d7739 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class RemoveContributor : AppCommand { - public sealed class RemoveContributor : AppCommand - { - public string ContributorId { get; set; } - } + public string ContributorId { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs index 1fe4743f80..c7e540b5c1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class RemoveLanguage : AppCommand { - public sealed class RemoveLanguage : AppCommand - { - public Language Language { get; set; } - } + public Language Language { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs index a545686a68..e633bdcda5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class RevokeClient : AppCommand { - public sealed class RevokeClient : AppCommand - { - public string Id { get; set; } - } + public string Id { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/TransferToTeam.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/TransferToTeam.cs index a5e36102b7..2f1b6f5e71 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/TransferToTeam.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/TransferToTeam.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class TransferToTeam : AppCommand { - public sealed class TransferToTeam : AppCommand - { - public DomainId? TeamId { get; set; } - } + public DomainId? TeamId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs index f479528120..62f74a2c1c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateApp.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class UpdateApp : AppCommand { - public sealed class UpdateApp : AppCommand - { - public string? Label { get; set; } + public string? Label { get; set; } - public string? Description { get; set; } - } + public string? Description { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs index eae5da7722..270a8eae70 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Core.Apps; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class UpdateAppSettings : AppCommand { - public sealed class UpdateAppSettings : AppCommand - { - public AppSettings Settings { get; set; } - } + public AppSettings Settings { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs index 11f15efb0c..4aa3bb0391 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class UpdateClient : AppCommand { - public sealed class UpdateClient : AppCommand - { - public string Id { get; set; } + public string Id { get; set; } - public string? Name { get; set; } + public string? Name { get; set; } - public string? Role { get; set; } + public string? Role { get; set; } - public long? ApiCallsLimit { get; set; } + public long? ApiCallsLimit { get; set; } - public long? ApiTrafficLimit { get; set; } + public long? ApiTrafficLimit { get; set; } - public bool? AllowAnonymous { get; set; } - } + public bool? AllowAnonymous { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs index b643f678ad..915596b8a5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs @@ -7,16 +7,15 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class UpdateLanguage : AppCommand { - public sealed class UpdateLanguage : AppCommand - { - public Language Language { get; set; } + public Language Language { get; set; } - public bool IsOptional { get; set; } + public bool IsOptional { get; set; } - public bool IsMaster { get; set; } + public bool IsMaster { get; set; } - public Language[]? Fallback { get; set; } - } + public Language[]? Fallback { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs index 2465f4b5ca..a16ca716b7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class UpdateRole : AppCommand { - public sealed class UpdateRole : AppCommand - { - public string Name { get; set; } + public string Name { get; set; } - public string[] Permissions { get; set; } + public string[] Permissions { get; set; } - public JsonObject Properties { get; set; } - } + public JsonObject Properties { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs index 5978a00a99..671783e565 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs @@ -8,12 +8,11 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class UpdateWorkflow : AppCommand { - public sealed class UpdateWorkflow : AppCommand - { - public DomainId WorkflowId { get; set; } + public DomainId WorkflowId { get; set; } - public Workflow Workflow { get; set; } - } + public Workflow Workflow { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs index 065fe76d20..d5b6e195ed 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UploadAppImage.cs @@ -7,10 +7,9 @@ using Squidex.Assets; -namespace Squidex.Domain.Apps.Entities.Apps.Commands +namespace Squidex.Domain.Apps.Entities.Apps.Commands; + +public sealed class UploadAppImage : AppCommand { - public sealed class UploadAppImage : AppCommand - { - public AssetFile File { get; set; } - } + public AssetFile File { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/_AppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/_AppCommand.cs index 4f676f1415..8e87485d56 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/_AppCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/_AppCommand.cs @@ -10,21 +10,20 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Apps.Commands -{ - public abstract class AppCommand : AppCommandBase, IAppCommand - { - public NamedId AppId { get; set; } +namespace Squidex.Domain.Apps.Entities.Apps.Commands; - public override DomainId AggregateId - { - get => AppId.Id; - } - } +public abstract class AppCommand : AppCommandBase, IAppCommand +{ + public NamedId AppId { get; set; } - // This command is needed as marker for middlewares. - public abstract class AppCommandBase : SquidexCommand, IAggregateCommand + public override DomainId AggregateId { - public abstract DomainId AggregateId { get; } + get => AppId.Id; } } + +// This command is needed as marker for middlewares. +public abstract class AppCommandBase : SquidexCommand, IAggregateCommand +{ + public abstract DomainId AggregateId { get; } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppImageStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppImageStore.cs index 5e609c89e1..e67a1355bd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppImageStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppImageStore.cs @@ -10,47 +10,46 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class DefaultAppImageStore : IAppImageStore { - public sealed class DefaultAppImageStore : IAppImageStore + private readonly IAssetStore assetStore; + private readonly AssetOptions options; + + public DefaultAppImageStore(IAssetStore assetStore, + IOptions options) { - private readonly IAssetStore assetStore; - private readonly AssetOptions options; + this.assetStore = assetStore; - public DefaultAppImageStore(IAssetStore assetStore, - IOptions options) - { - this.assetStore = assetStore; + this.options = options.Value; + } - this.options = options.Value; - } + public Task DownloadAsync(DomainId appId, Stream stream, + CancellationToken ct = default) + { + var fileName = GetFileName(appId); - public Task DownloadAsync(DomainId appId, Stream stream, - CancellationToken ct = default) - { - var fileName = GetFileName(appId); + return assetStore.DownloadAsync(fileName, stream, default, ct); + } - return assetStore.DownloadAsync(fileName, stream, default, ct); - } + public Task UploadAsync(DomainId appId, Stream stream, + CancellationToken ct = default) + { + var fileName = GetFileName(appId); - public Task UploadAsync(DomainId appId, Stream stream, - CancellationToken ct = default) - { - var fileName = GetFileName(appId); + return assetStore.UploadAsync(fileName, stream, true, ct); + } - return assetStore.UploadAsync(fileName, stream, true, ct); + private string GetFileName(DomainId appId) + { + if (options.FolderPerApp) + { + return $"{appId}/thumbnail"; } - - private string GetFileName(DomainId appId) + else { - if (options.FolderPerApp) - { - return $"{appId}/thumbnail"; - } - else - { - return appId.ToString(); - } + return appId.ToString(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs index 33701d1fe3..2c98c4fb67 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs @@ -12,167 +12,166 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Log; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class DefaultAppLogStore : IAppLogStore, IDeleter { - public sealed class DefaultAppLogStore : IAppLogStore, IDeleter + private const string FieldAuthClientId = "AuthClientId"; + private const string FieldAuthUserId = "AuthUserId"; + private const string FieldBytes = "Bytes"; + private const string FieldCosts = "Costs"; + private const string FieldCacheStatus = "CacheStatus"; + private const string FieldCacheServer = "CacheServer"; + private const string FieldCacheTTL = "CacheTTL"; + private const string FieldCacheHits = "CacheHits"; + private const string FieldRequestElapsedMs = "RequestElapsedMs"; + private const string FieldRequestMethod = "RequestMethod"; + private const string FieldRequestPath = "RequestPath"; + private const string FieldStatusCode = "StatusCode"; + private const string FieldTimestamp = "Timestamp"; + + private static readonly CsvConfiguration CsvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture) { - private const string FieldAuthClientId = "AuthClientId"; - private const string FieldAuthUserId = "AuthUserId"; - private const string FieldBytes = "Bytes"; - private const string FieldCosts = "Costs"; - private const string FieldCacheStatus = "CacheStatus"; - private const string FieldCacheServer = "CacheServer"; - private const string FieldCacheTTL = "CacheTTL"; - private const string FieldCacheHits = "CacheHits"; - private const string FieldRequestElapsedMs = "RequestElapsedMs"; - private const string FieldRequestMethod = "RequestMethod"; - private const string FieldRequestPath = "RequestPath"; - private const string FieldStatusCode = "StatusCode"; - private const string FieldTimestamp = "Timestamp"; - - private static readonly CsvConfiguration CsvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture) - { - DetectDelimiter = false, - Delimiter = "|", - LeaveOpen = true - }; + DetectDelimiter = false, + Delimiter = "|", + LeaveOpen = true + }; - private readonly IRequestLogStore requestLogStore; + private readonly IRequestLogStore requestLogStore; - public DefaultAppLogStore(IRequestLogStore requestLogStore) - { - this.requestLogStore = requestLogStore; - } + public DefaultAppLogStore(IRequestLogStore requestLogStore) + { + this.requestLogStore = requestLogStore; + } + + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return requestLogStore.DeleteAsync(app.Id.ToString(), ct); + } - Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) + public Task LogAsync(DomainId appId, RequestLog request, + CancellationToken ct = default) + { + if (!requestLogStore.IsEnabled) { - return requestLogStore.DeleteAsync(app.Id.ToString(), ct); + return Task.CompletedTask; } - public Task LogAsync(DomainId appId, RequestLog request, - CancellationToken ct = default) + var storedRequest = new Request { - if (!requestLogStore.IsEnabled) - { - return Task.CompletedTask; - } + Key = appId.ToString(), + Timestamp = request.Timestamp + }; - var storedRequest = new Request - { - Key = appId.ToString(), - Timestamp = request.Timestamp - }; - - Append(storedRequest, FieldAuthClientId, request.UserClientId); - Append(storedRequest, FieldAuthUserId, request.UserId); - Append(storedRequest, FieldBytes, request.Bytes); - Append(storedRequest, FieldCacheHits, request.CacheHits); - Append(storedRequest, FieldCacheServer, request.CacheServer); - Append(storedRequest, FieldCacheStatus, request.CacheStatus); - Append(storedRequest, FieldCacheTTL, request.CacheTTL); - Append(storedRequest, FieldCosts, request.Costs); - Append(storedRequest, FieldRequestElapsedMs, request.ElapsedMs); - Append(storedRequest, FieldRequestMethod, request.RequestMethod); - Append(storedRequest, FieldRequestPath, request.RequestPath); - Append(storedRequest, FieldStatusCode, request.StatusCode); - - return requestLogStore.LogAsync(storedRequest, ct); - } + Append(storedRequest, FieldAuthClientId, request.UserClientId); + Append(storedRequest, FieldAuthUserId, request.UserId); + Append(storedRequest, FieldBytes, request.Bytes); + Append(storedRequest, FieldCacheHits, request.CacheHits); + Append(storedRequest, FieldCacheServer, request.CacheServer); + Append(storedRequest, FieldCacheStatus, request.CacheStatus); + Append(storedRequest, FieldCacheTTL, request.CacheTTL); + Append(storedRequest, FieldCosts, request.Costs); + Append(storedRequest, FieldRequestElapsedMs, request.ElapsedMs); + Append(storedRequest, FieldRequestMethod, request.RequestMethod); + Append(storedRequest, FieldRequestPath, request.RequestPath); + Append(storedRequest, FieldStatusCode, request.StatusCode); + + return requestLogStore.LogAsync(storedRequest, ct); + } - public async Task ReadLogAsync(DomainId appId, DateTime fromDate, DateTime toDate, Stream stream, - CancellationToken ct = default) - { - Guard.NotNull(appId); + public async Task ReadLogAsync(DomainId appId, DateTime fromDate, DateTime toDate, Stream stream, + CancellationToken ct = default) + { + Guard.NotNull(appId); - var writer = new StreamWriter(stream, Encoding.UTF8, 4096, true); - try + var writer = new StreamWriter(stream, Encoding.UTF8, 4096, true); + try + { + await using (var csv = new CsvWriter(writer, CsvConfiguration)) { - await using (var csv = new CsvWriter(writer, CsvConfiguration)) + csv.WriteField(FieldTimestamp); + csv.WriteField(FieldRequestPath); + csv.WriteField(FieldRequestMethod); + csv.WriteField(FieldRequestElapsedMs); + csv.WriteField(FieldCosts); + csv.WriteField(FieldAuthClientId); + csv.WriteField(FieldAuthUserId); + csv.WriteField(FieldBytes); + csv.WriteField(FieldCacheHits); + csv.WriteField(FieldCacheServer); + csv.WriteField(FieldCacheStatus); + csv.WriteField(FieldCacheTTL); + csv.WriteField(FieldStatusCode); + + await csv.NextRecordAsync(); + + await foreach (var request in requestLogStore.QueryAllAsync(appId.ToString(), fromDate, toDate, ct)) { - csv.WriteField(FieldTimestamp); - csv.WriteField(FieldRequestPath); - csv.WriteField(FieldRequestMethod); - csv.WriteField(FieldRequestElapsedMs); - csv.WriteField(FieldCosts); - csv.WriteField(FieldAuthClientId); - csv.WriteField(FieldAuthUserId); - csv.WriteField(FieldBytes); - csv.WriteField(FieldCacheHits); - csv.WriteField(FieldCacheServer); - csv.WriteField(FieldCacheStatus); - csv.WriteField(FieldCacheTTL); - csv.WriteField(FieldStatusCode); + csv.WriteField(request.Timestamp.ToString()); + csv.WriteField(GetString(request, FieldRequestPath)); + csv.WriteField(GetString(request, FieldRequestMethod)); + csv.WriteField(GetDouble(request, FieldRequestElapsedMs)); + csv.WriteField(GetDouble(request, FieldCosts)); + csv.WriteField(GetString(request, FieldAuthClientId)); + csv.WriteField(GetString(request, FieldAuthUserId)); + csv.WriteField(GetLong(request, FieldBytes)); + csv.WriteField(GetLong(request, FieldCacheHits)); + csv.WriteField(GetString(request, FieldCacheServer)); + csv.WriteField(GetString(request, FieldCacheStatus)); + csv.WriteField(GetLong(request, FieldCacheTTL)); + csv.WriteField(GetLong(request, FieldStatusCode)); await csv.NextRecordAsync(); - - await foreach (var request in requestLogStore.QueryAllAsync(appId.ToString(), fromDate, toDate, ct)) - { - csv.WriteField(request.Timestamp.ToString()); - csv.WriteField(GetString(request, FieldRequestPath)); - csv.WriteField(GetString(request, FieldRequestMethod)); - csv.WriteField(GetDouble(request, FieldRequestElapsedMs)); - csv.WriteField(GetDouble(request, FieldCosts)); - csv.WriteField(GetString(request, FieldAuthClientId)); - csv.WriteField(GetString(request, FieldAuthUserId)); - csv.WriteField(GetLong(request, FieldBytes)); - csv.WriteField(GetLong(request, FieldCacheHits)); - csv.WriteField(GetString(request, FieldCacheServer)); - csv.WriteField(GetString(request, FieldCacheStatus)); - csv.WriteField(GetLong(request, FieldCacheTTL)); - csv.WriteField(GetLong(request, FieldStatusCode)); - - await csv.NextRecordAsync(); - } } } - finally - { - await writer.FlushAsync(); - } } - - private static void Append(Request request, string key, string? value) + finally { - if (!string.IsNullOrWhiteSpace(value)) - { - request.Properties[key] = value; - } + await writer.FlushAsync(); } + } - private static void Append(Request request, string key, object? value) + private static void Append(Request request, string key, string? value) + { + if (!string.IsNullOrWhiteSpace(value)) { - if (value != null) - { - Append(request, key, Convert.ToString(value, CultureInfo.InvariantCulture)); - } + request.Properties[key] = value; } + } - private static string? GetString(Request request, string key) + private static void Append(Request request, string key, object? value) + { + if (value != null) { - return request.Properties.GetValueOrDefault(key, string.Empty); + Append(request, key, Convert.ToString(value, CultureInfo.InvariantCulture)); } + } - private static double? GetDouble(Request request, string key) - { - if (request.Properties.TryGetValue(key, out var value) && - double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) - { - return result; - } + private static string? GetString(Request request, string key) + { + return request.Properties.GetValueOrDefault(key, string.Empty); + } - return null; + private static double? GetDouble(Request request, string key) + { + if (request.Properties.TryGetValue(key, out var value) && + double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; } - private static long? GetLong(Request request, string key) - { - if (request.Properties.TryGetValue(key, out var value) && - long.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) - { - return result; - } + return null; + } - return null; + private static long? GetLong(Request request, string key) + { + if (request.Properties.TryGetValue(key, out var value) && + long.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; } + + return null; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs index 7eda5795da..97eb4089c9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs @@ -11,64 +11,63 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject; + +public sealed class AppCommandMiddleware : AggregateCommandMiddleware { - public sealed class AppCommandMiddleware : AggregateCommandMiddleware + private readonly IAppImageStore appImageStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IContextProvider contextProvider; + + public AppCommandMiddleware(IDomainObjectFactory domainObjectFactory, + IAppImageStore appImageStore, IAssetThumbnailGenerator assetThumbnailGenerator, IContextProvider contextProvider) + : base(domainObjectFactory) { - private readonly IAppImageStore appImageStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly IContextProvider contextProvider; + this.appImageStore = appImageStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + this.contextProvider = contextProvider; + } - public AppCommandMiddleware(IDomainObjectFactory domainObjectFactory, - IAppImageStore appImageStore, IAssetThumbnailGenerator assetThumbnailGenerator, IContextProvider contextProvider) - : base(domainObjectFactory) + public override async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is UploadAppImage uploadImage) { - this.appImageStore = appImageStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; - this.contextProvider = contextProvider; + await UploadAsync(uploadImage, ct); } - public override async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - if (context.Command is UploadAppImage uploadImage) - { - await UploadAsync(uploadImage, ct); - } + await base.HandleAsync(context, next, ct); + } - await base.HandleAsync(context, next, ct); + protected override Task EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) + { + if (result.Payload is IAppEntity app) + { + contextProvider.Context.App = app; } - protected override Task EnrichResultAsync(CommandContext context, CommandResult result, - CancellationToken ct) - { - if (result.Payload is IAppEntity app) - { - contextProvider.Context.App = app; - } + return base.EnrichResultAsync(context, result, ct); + } - return base.EnrichResultAsync(context, result, ct); - } + private async Task UploadAsync(UploadAppImage uploadImage, + CancellationToken ct) + { + var file = uploadImage.File; - private async Task UploadAsync(UploadAppImage uploadImage, - CancellationToken ct) + await using (var uploadStream = file.OpenRead()) { - var file = uploadImage.File; + var image = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, file.MimeType, ct); - await using (var uploadStream = file.OpenRead()) + if (image == null) { - var image = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, file.MimeType, ct); - - if (image == null) - { - throw new ValidationException(T.Get("apps.notImage")); - } + throw new ValidationException(T.Get("apps.notImage")); } + } - await using (var uploadStream = file.OpenRead()) - { - await appImageStore.UploadAsync(uploadImage.AppId.Id, uploadStream, ct); - } + await using (var uploadStream = file.OpenRead()) + { + await appImageStore.UploadAsync(uploadImage.AppId.Id, uploadStream, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs index cf1b08bb2a..df36687d44 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs @@ -17,225 +17,224 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject; + +public partial class AppDomainObject { - public partial class AppDomainObject + [CollectionName("Apps")] + public sealed class State : DomainObjectState, IAppEntity { - [CollectionName("Apps")] - public sealed class State : DomainObjectState, IAppEntity - { - public string Name { get; set; } + public string Name { get; set; } - public string Label { get; set; } + public string Label { get; set; } - public string Description { get; set; } + public string Description { get; set; } - public DomainId? TeamId { get; set; } + public DomainId? TeamId { get; set; } - public Contributors Contributors { get; set; } = Contributors.Empty; + public Contributors Contributors { get; set; } = Contributors.Empty; - public Roles Roles { get; set; } = Roles.Empty; + public Roles Roles { get; set; } = Roles.Empty; - public AssignedPlan? Plan { get; set; } + public AssignedPlan? Plan { get; set; } - public AppClients Clients { get; set; } = AppClients.Empty; + public AppClients Clients { get; set; } = AppClients.Empty; - public AppImage? Image { get; set; } + public AppImage? Image { get; set; } - public AppSettings Settings { get; set; } = AppSettings.Empty; + public AppSettings Settings { get; set; } = AppSettings.Empty; - public AssetScripts AssetScripts { get; set; } = new AssetScripts(); + public AssetScripts AssetScripts { get; set; } = new AssetScripts(); - public LanguagesConfig Languages { get; set; } = LanguagesConfig.English; + public LanguagesConfig Languages { get; set; } = LanguagesConfig.English; - public Workflows Workflows { get; set; } = Workflows.Empty; + public Workflows Workflows { get; set; } = Workflows.Empty; - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - [JsonIgnore] - public DomainId UniqueId - { - get => Id; - } + [JsonIgnore] + public DomainId UniqueId + { + get => Id; + } - public override bool ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) + { + switch (@event) { - switch (@event) - { - case AppCreated e: - { - Id = e.AppId.Id; - - SimpleMapper.Map(e, this); - return true; - } + case AppCreated e: + { + Id = e.AppId.Id; - case AppUpdated e when Is.Change(Label, e.Label) || Is.Change(Description, e.Description): - { - SimpleMapper.Map(e, this); - return true; - } - - case AppTransfered e when Is.Change(TeamId, e.TeamId): - { - SimpleMapper.Map(e, this); - return true; - } + SimpleMapper.Map(e, this); + return true; + } - case AppSettingsUpdated e when Is.Change(Settings, e.Settings): - return UpdateSettings(e.Settings); + case AppUpdated e when Is.Change(Label, e.Label) || Is.Change(Description, e.Description): + { + SimpleMapper.Map(e, this); + return true; + } - case AppAssetsScriptsConfigured e when Is.Change(e.Scripts, AssetScripts): - return UpdateAssetScripts(e.Scripts); + case AppTransfered e when Is.Change(TeamId, e.TeamId): + { + SimpleMapper.Map(e, this); + return true; + } - case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): - return UpdatePlan(e.ToPlan()); + case AppSettingsUpdated e when Is.Change(Settings, e.Settings): + return UpdateSettings(e.Settings); - case AppPlanReset e when Plan != null: - return UpdatePlan(null); + case AppAssetsScriptsConfigured e when Is.Change(e.Scripts, AssetScripts): + return UpdateAssetScripts(e.Scripts); - case AppImageUploaded e: - return UpdateImage(e.Image); + case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): + return UpdatePlan(e.ToPlan()); - case AppImageRemoved e when Image != null: - return UpdateImage(null); + case AppPlanReset e when Plan != null: + return UpdatePlan(null); - case AppContributorAssigned e: - return UpdateContributors(e, (e, c) => c.Assign(e.ContributorId, e.Role)); + case AppImageUploaded e: + return UpdateImage(e.Image); - case AppContributorRemoved e: - return UpdateContributors(e, (e, c) => c.Remove(e.ContributorId)); + case AppImageRemoved e when Image != null: + return UpdateImage(null); - case AppClientAttached e: - return UpdateClients(e, (e, c) => c.Add(e.Id, e.Secret, e.Role)); + case AppContributorAssigned e: + return UpdateContributors(e, (e, c) => c.Assign(e.ContributorId, e.Role)); - case AppClientUpdated e: - return UpdateClients(e, (e, c) => c.Update(e.Id, e.Name, e.Role, e.ApiCallsLimit, e.ApiTrafficLimit, e.AllowAnonymous)); + case AppContributorRemoved e: + return UpdateContributors(e, (e, c) => c.Remove(e.ContributorId)); - case AppClientRevoked e: - return UpdateClients(e, (e, c) => c.Revoke(e.Id)); + case AppClientAttached e: + return UpdateClients(e, (e, c) => c.Add(e.Id, e.Secret, e.Role)); - case AppWorkflowAdded e: - return UpdateWorkflows(e, (e, w) => w.Add(e.WorkflowId, e.Name)); + case AppClientUpdated e: + return UpdateClients(e, (e, c) => c.Update(e.Id, e.Name, e.Role, e.ApiCallsLimit, e.ApiTrafficLimit, e.AllowAnonymous)); - case AppWorkflowUpdated e: - return UpdateWorkflows(e, (e, w) => w.Update(e.WorkflowId, e.Workflow)); + case AppClientRevoked e: + return UpdateClients(e, (e, c) => c.Revoke(e.Id)); - case AppWorkflowDeleted e: - return UpdateWorkflows(e, (e, w) => w.Remove(e.WorkflowId)); + case AppWorkflowAdded e: + return UpdateWorkflows(e, (e, w) => w.Add(e.WorkflowId, e.Name)); - case AppRoleAdded e: - return UpdateRoles(e, (e, r) => r.Add(e.Name)); + case AppWorkflowUpdated e: + return UpdateWorkflows(e, (e, w) => w.Update(e.WorkflowId, e.Workflow)); - case AppRoleUpdated e: - return UpdateRoles(e, (e, r) => r.Update(e.Name, e.ToPermissions(), e.Properties)); + case AppWorkflowDeleted e: + return UpdateWorkflows(e, (e, w) => w.Remove(e.WorkflowId)); - case AppRoleDeleted e: - return UpdateRoles(e, (e, r) => r.Remove(e.Name)); + case AppRoleAdded e: + return UpdateRoles(e, (e, r) => r.Add(e.Name)); - case AppLanguageAdded e: - return UpdateLanguages(e, (e, l) => l.Set(e.Language)); + case AppRoleUpdated e: + return UpdateRoles(e, (e, r) => r.Update(e.Name, e.ToPermissions(), e.Properties)); - case AppLanguageRemoved e: - return UpdateLanguages(e, (e, l) => l.Remove(e.Language)); + case AppRoleDeleted e: + return UpdateRoles(e, (e, r) => r.Remove(e.Name)); - case AppLanguageUpdated e: - return UpdateLanguages(e, (e, l) => - { - l = l.Set(e.Language, e.IsOptional, e.Fallback); + case AppLanguageAdded e: + return UpdateLanguages(e, (e, l) => l.Set(e.Language)); - if (e.IsMaster) - { - l = Languages.MakeMaster(e.Language); - } + case AppLanguageRemoved e: + return UpdateLanguages(e, (e, l) => l.Remove(e.Language)); - return l; - }); + case AppLanguageUpdated e: + return UpdateLanguages(e, (e, l) => + { + l = l.Set(e.Language, e.IsOptional, e.Fallback); - case AppDeleted: + if (e.IsMaster) { - Plan = null; - - IsDeleted = true; - return true; + l = Languages.MakeMaster(e.Language); } - } - return false; + return l; + }); + + case AppDeleted: + { + Plan = null; + + IsDeleted = true; + return true; + } } - private bool UpdateContributors(T @event, Func update) - { - var previous = Contributors; + return false; + } - Contributors = update(@event, previous); + private bool UpdateContributors(T @event, Func update) + { + var previous = Contributors; - return !ReferenceEquals(previous, Contributors); - } + Contributors = update(@event, previous); - private bool UpdateClients(T @event, Func update) - { - var previous = Clients; + return !ReferenceEquals(previous, Contributors); + } - Clients = update(@event, previous); + private bool UpdateClients(T @event, Func update) + { + var previous = Clients; - return !ReferenceEquals(previous, Clients); - } + Clients = update(@event, previous); - private bool UpdateLanguages(T @event, Func update) - { - var previous = Languages; + return !ReferenceEquals(previous, Clients); + } - Languages = update(@event, previous); + private bool UpdateLanguages(T @event, Func update) + { + var previous = Languages; - return !ReferenceEquals(previous, Languages); - } + Languages = update(@event, previous); - private bool UpdateRoles(T @event, Func update) - { - var previous = Roles; + return !ReferenceEquals(previous, Languages); + } - Roles = update(@event, previous); + private bool UpdateRoles(T @event, Func update) + { + var previous = Roles; - return !ReferenceEquals(previous, Roles); - } + Roles = update(@event, previous); - private bool UpdateWorkflows(T @event, Func update) - { - var previous = Workflows; + return !ReferenceEquals(previous, Roles); + } - Workflows = update(@event, previous); + private bool UpdateWorkflows(T @event, Func update) + { + var previous = Workflows; - return !ReferenceEquals(previous, Workflows); - } + Workflows = update(@event, previous); - private bool UpdateImage(AppImage? image) - { - Image = image; + return !ReferenceEquals(previous, Workflows); + } - return true; - } + private bool UpdateImage(AppImage? image) + { + Image = image; - private bool UpdateAssetScripts(AssetScripts? scripts) - { - AssetScripts = scripts ?? new AssetScripts(); + return true; + } - return true; - } + private bool UpdateAssetScripts(AssetScripts? scripts) + { + AssetScripts = scripts ?? new AssetScripts(); - private bool UpdateSettings(AppSettings settings) - { - Settings = settings; + return true; + } - return true; - } + private bool UpdateSettings(AppSettings settings) + { + Settings = settings; - private bool UpdatePlan(AssignedPlan? plan) - { - Plan = plan; + return true; + } - return true; - } + private bool UpdatePlan(AssignedPlan? plan) + { + Plan = plan; + + return true; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs index 6455cb85ef..568aec5284 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs @@ -22,477 +22,476 @@ #pragma warning disable MA0022 // Return Task.FromResult instead of returning null -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject; + +public partial class AppDomainObject : DomainObject { - public partial class AppDomainObject : DomainObject + private readonly IServiceProvider serviceProvider; + + public AppDomainObject(DomainId id, IPersistenceFactory persistence, ILogger log, + IServiceProvider serviceProvider) + : base(id, persistence, log) { - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public AppDomainObject(DomainId id, IPersistenceFactory persistence, ILogger log, - IServiceProvider serviceProvider) - : base(id, persistence, log) - { - this.serviceProvider = serviceProvider; - } + protected override bool IsDeleted(State snapshot) + { + return snapshot.IsDeleted; + } - protected override bool IsDeleted(State snapshot) - { - return snapshot.IsDeleted; - } + protected override bool CanAcceptCreation(ICommand command) + { + return command is AppCommandBase; + } - protected override bool CanAcceptCreation(ICommand command) - { - return command is AppCommandBase; - } + protected override bool CanAccept(ICommand command) + { + return command is AppCommand update && Equals(update?.AppId?.Id, Snapshot.Id); + } - protected override bool CanAccept(ICommand command) + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + switch (command) { - return command is AppCommand update && Equals(update?.AppId?.Id, Snapshot.Id); - } + case CreateApp create: + return CreateReturn(create, c => + { + GuardApp.CanCreate(c); - public override Task ExecuteAsync(IAggregateCommand command, - CancellationToken ct) - { - switch (command) - { - case CreateApp create: - return CreateReturn(create, c => - { - GuardApp.CanCreate(c); + Create(c); - Create(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateApp update: + return UpdateReturn(update, c => + { + GuardApp.CanUpdate(c); - case UpdateApp update: - return UpdateReturn(update, c => - { - GuardApp.CanUpdate(c); + Update(c); - Update(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case TransferToTeam transfer: + return UpdateReturnAsync(transfer, async (c, ct) => + { + await GuardApp.CanTransfer(c, Snapshot, AppProvider, ct); - case TransferToTeam transfer: - return UpdateReturnAsync(transfer, async (c, ct) => - { - await GuardApp.CanTransfer(c, Snapshot, AppProvider, ct); + Transfer(c); - Transfer(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateAppSettings updateSettings: + return UpdateReturn(updateSettings, c => + { + GuardApp.CanUpdateSettings(c); - case UpdateAppSettings updateSettings: - return UpdateReturn(updateSettings, c => - { - GuardApp.CanUpdateSettings(c); + UpdateSettings(c); - UpdateSettings(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UploadAppImage uploadImage: + return UpdateReturn(uploadImage, c => + { + GuardApp.CanUploadImage(c); - case UploadAppImage uploadImage: - return UpdateReturn(uploadImage, c => - { - GuardApp.CanUploadImage(c); + UploadImage(c); - UploadImage(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case RemoveAppImage removeImage: + return UpdateReturn(removeImage, c => + { + GuardApp.CanRemoveImage(c); - case RemoveAppImage removeImage: - return UpdateReturn(removeImage, c => - { - GuardApp.CanRemoveImage(c); + RemoveImage(c); - RemoveImage(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case ConfigureAssetScripts configureAssetScripts: + return UpdateReturn(configureAssetScripts, c => + { + GuardApp.CanUpdateAssetScripts(c); - case ConfigureAssetScripts configureAssetScripts: - return UpdateReturn(configureAssetScripts, c => - { - GuardApp.CanUpdateAssetScripts(c); + ConfigureAssetScripts(c); - ConfigureAssetScripts(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case AssignContributor assignContributor: + return UpdateReturnAsync(assignContributor, async (c, ct) => + { + await GuardAppContributors.CanAssign(c, Snapshot, Users, Plan); - case AssignContributor assignContributor: - return UpdateReturnAsync(assignContributor, async (c, ct) => - { - await GuardAppContributors.CanAssign(c, Snapshot, Users, Plan); + AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); - AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case RemoveContributor removeContributor: + return UpdateReturn(removeContributor, c => + { + GuardAppContributors.CanRemove(c, Snapshot); - case RemoveContributor removeContributor: - return UpdateReturn(removeContributor, c => - { - GuardAppContributors.CanRemove(c, Snapshot); + RemoveContributor(c); - RemoveContributor(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case AttachClient attachClient: + return UpdateReturn(attachClient, c => + { + GuardAppClients.CanAttach(c, Snapshot); - case AttachClient attachClient: - return UpdateReturn(attachClient, c => - { - GuardAppClients.CanAttach(c, Snapshot); + AttachClient(c); - AttachClient(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateClient updateClient: + return UpdateReturn(updateClient, c => + { + GuardAppClients.CanUpdate(c, Snapshot); - case UpdateClient updateClient: - return UpdateReturn(updateClient, c => - { - GuardAppClients.CanUpdate(c, Snapshot); + UpdateClient(c); - UpdateClient(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case RevokeClient revokeClient: + return UpdateReturn(revokeClient, c => + { + GuardAppClients.CanRevoke(c, Snapshot); - case RevokeClient revokeClient: - return UpdateReturn(revokeClient, c => - { - GuardAppClients.CanRevoke(c, Snapshot); + RevokeClient(c); - RevokeClient(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case AddWorkflow addWorkflow: + return UpdateReturn(addWorkflow, c => + { + GuardAppWorkflows.CanAdd(c); - case AddWorkflow addWorkflow: - return UpdateReturn(addWorkflow, c => - { - GuardAppWorkflows.CanAdd(c); + AddWorkflow(c); - AddWorkflow(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateWorkflow updateWorkflow: + return UpdateReturn(updateWorkflow, c => + { + GuardAppWorkflows.CanUpdate(c, Snapshot); - case UpdateWorkflow updateWorkflow: - return UpdateReturn(updateWorkflow, c => - { - GuardAppWorkflows.CanUpdate(c, Snapshot); + UpdateWorkflow(c); - UpdateWorkflow(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case DeleteWorkflow deleteWorkflow: + return UpdateReturn(deleteWorkflow, c => + { + GuardAppWorkflows.CanDelete(c, Snapshot); - case DeleteWorkflow deleteWorkflow: - return UpdateReturn(deleteWorkflow, c => - { - GuardAppWorkflows.CanDelete(c, Snapshot); + DeleteWorkflow(c); - DeleteWorkflow(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case AddLanguage addLanguage: + return UpdateReturn(addLanguage, c => + { + GuardAppLanguages.CanAdd(c, Snapshot); - case AddLanguage addLanguage: - return UpdateReturn(addLanguage, c => - { - GuardAppLanguages.CanAdd(c, Snapshot); + AddLanguage(c); - AddLanguage(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case RemoveLanguage removeLanguage: + return UpdateReturn(removeLanguage, c => + { + GuardAppLanguages.CanRemove(c, Snapshot); - case RemoveLanguage removeLanguage: - return UpdateReturn(removeLanguage, c => - { - GuardAppLanguages.CanRemove(c, Snapshot); + RemoveLanguage(c); - RemoveLanguage(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateLanguage updateLanguage: + return UpdateReturn(updateLanguage, c => + { + GuardAppLanguages.CanUpdate(c, Snapshot); - case UpdateLanguage updateLanguage: - return UpdateReturn(updateLanguage, c => - { - GuardAppLanguages.CanUpdate(c, Snapshot); + UpdateLanguage(c); - UpdateLanguage(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case AddRole addRole: + return UpdateReturn(addRole, c => + { + GuardAppRoles.CanAdd(c, Snapshot); - case AddRole addRole: - return UpdateReturn(addRole, c => - { - GuardAppRoles.CanAdd(c, Snapshot); + AddRole(c); - AddRole(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case DeleteRole deleteRole: + return UpdateReturn(deleteRole, c => + { + GuardAppRoles.CanDelete(c, Snapshot); - case DeleteRole deleteRole: - return UpdateReturn(deleteRole, c => - { - GuardAppRoles.CanDelete(c, Snapshot); + DeleteRole(c); - DeleteRole(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateRole updateRole: + return UpdateReturn(updateRole, c => + { + GuardAppRoles.CanUpdate(c, Snapshot); - case UpdateRole updateRole: - return UpdateReturn(updateRole, c => - { - GuardAppRoles.CanUpdate(c, Snapshot); + UpdateRole(c); - UpdateRole(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case DeleteApp delete: + return UpdateAsync(delete, async (c, ct) => + { + await BillingManager.UnsubscribeAsync(c.Actor.Identifier, Snapshot, default); - case DeleteApp delete: - return UpdateAsync(delete, async (c, ct) => - { - await BillingManager.UnsubscribeAsync(c.Actor.Identifier, Snapshot, default); + DeleteApp(c); + }, ct); - DeleteApp(c); - }, ct); + case ChangePlan changePlan: + return UpdateReturnAsync(changePlan, async (c, ct) => + { + GuardApp.CanChangePlan(c, Snapshot, BillingPlans); - case ChangePlan changePlan: - return UpdateReturnAsync(changePlan, async (c, ct) => + if (string.Equals(FreePlan?.Id, c.PlanId, StringComparison.Ordinal)) { - GuardApp.CanChangePlan(c, Snapshot, BillingPlans); - - if (string.Equals(FreePlan?.Id, c.PlanId, StringComparison.Ordinal)) + if (!c.FromCallback) { - if (!c.FromCallback) - { - await BillingManager.UnsubscribeAsync(c.Actor.Identifier, Snapshot, default); - } + await BillingManager.UnsubscribeAsync(c.Actor.Identifier, Snapshot, default); + } - ResetPlan(c); + ResetPlan(c); - return new PlanChangedResult(c.PlanId, true, null); - } - else + return new PlanChangedResult(c.PlanId, true, null); + } + else + { + if (!c.FromCallback) { - if (!c.FromCallback) - { - var redirectUri = await BillingManager.MustRedirectToPortalAsync(c.Actor.Identifier, Snapshot, c.PlanId, ct); - - if (redirectUri != null) - { - return new PlanChangedResult(c.PlanId, false, redirectUri); - } + var redirectUri = await BillingManager.MustRedirectToPortalAsync(c.Actor.Identifier, Snapshot, c.PlanId, ct); - await BillingManager.SubscribeAsync(c.Actor.Identifier, Snapshot, changePlan.PlanId, default); + if (redirectUri != null) + { + return new PlanChangedResult(c.PlanId, false, redirectUri); } - ChangePlan(c); - - return new PlanChangedResult(c.PlanId); + await BillingManager.SubscribeAsync(c.Actor.Identifier, Snapshot, changePlan.PlanId, default); } - }, ct); - default: - ThrowHelper.NotSupportedException(); - return default!; - } - } + ChangePlan(c); - private void Create(CreateApp command) - { - var appId = NamedId.Of(command.AppId, command.Name); - - void RaiseInitial(T @event) where T : AppEvent - { - Raise(command, @event, appId); - } + return new PlanChangedResult(c.PlanId); + } + }, ct); - RaiseInitial(new AppCreated()); + default: + ThrowHelper.NotSupportedException(); + return default!; + } + } - var actor = command.Actor; + private void Create(CreateApp command) + { + var appId = NamedId.Of(command.AppId, command.Name); - if (actor.IsUser) - { - RaiseInitial(new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }); - } + void RaiseInitial(T @event) where T : AppEvent + { + Raise(command, @event, appId); + } - var settings = serviceProvider.GetService()?.Settings; + RaiseInitial(new AppCreated()); - if (settings != null) - { - RaiseInitial(new AppSettingsUpdated { Settings = settings }); - } - } + var actor = command.Actor; - private void ChangePlan(ChangePlan command) + if (actor.IsUser) { - Raise(command, new AppPlanChanged()); + RaiseInitial(new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }); } - private void ResetPlan(ChangePlan command) - { - Raise(command, new AppPlanReset()); - } + var settings = serviceProvider.GetService()?.Settings; - private void Update(UpdateApp command) + if (settings != null) { - Raise(command, new AppUpdated()); + RaiseInitial(new AppSettingsUpdated { Settings = settings }); } + } - private void Transfer(TransferToTeam command) - { - Raise(command, new AppTransfered()); - } + private void ChangePlan(ChangePlan command) + { + Raise(command, new AppPlanChanged()); + } - private void UpdateSettings(UpdateAppSettings command) - { - Raise(command, new AppSettingsUpdated()); - } + private void ResetPlan(ChangePlan command) + { + Raise(command, new AppPlanReset()); + } - private void ConfigureAssetScripts(ConfigureAssetScripts command) - { - Raise(command, new AppAssetsScriptsConfigured()); - } + private void Update(UpdateApp command) + { + Raise(command, new AppUpdated()); + } - private void UpdateClient(UpdateClient command) - { - Raise(command, new AppClientUpdated()); - } + private void Transfer(TransferToTeam command) + { + Raise(command, new AppTransfered()); + } - private void UploadImage(UploadAppImage command) - { - Raise(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) }); - } + private void UpdateSettings(UpdateAppSettings command) + { + Raise(command, new AppSettingsUpdated()); + } - private void RemoveImage(RemoveAppImage command) - { - Raise(command, new AppImageRemoved()); - } + private void ConfigureAssetScripts(ConfigureAssetScripts command) + { + Raise(command, new AppAssetsScriptsConfigured()); + } - private void UpdateLanguage(UpdateLanguage command) - { - Raise(command, new AppLanguageUpdated()); - } + private void UpdateClient(UpdateClient command) + { + Raise(command, new AppClientUpdated()); + } - private void AssignContributor(AssignContributor command, bool isAdded) - { - Raise(command, new AppContributorAssigned { IsAdded = isAdded }); - } + private void UploadImage(UploadAppImage command) + { + Raise(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) }); + } - private void RemoveContributor(RemoveContributor command) - { - Raise(command, new AppContributorRemoved()); - } + private void RemoveImage(RemoveAppImage command) + { + Raise(command, new AppImageRemoved()); + } - private void AttachClient(AttachClient command) - { - Raise(command, new AppClientAttached()); - } + private void UpdateLanguage(UpdateLanguage command) + { + Raise(command, new AppLanguageUpdated()); + } - private void RevokeClient(RevokeClient command) - { - Raise(command, new AppClientRevoked()); - } + private void AssignContributor(AssignContributor command, bool isAdded) + { + Raise(command, new AppContributorAssigned { IsAdded = isAdded }); + } - private void AddWorkflow(AddWorkflow command) - { - Raise(command, new AppWorkflowAdded()); - } + private void RemoveContributor(RemoveContributor command) + { + Raise(command, new AppContributorRemoved()); + } - private void UpdateWorkflow(UpdateWorkflow command) - { - Raise(command, new AppWorkflowUpdated()); - } + private void AttachClient(AttachClient command) + { + Raise(command, new AppClientAttached()); + } - private void DeleteWorkflow(DeleteWorkflow command) - { - Raise(command, new AppWorkflowDeleted()); - } + private void RevokeClient(RevokeClient command) + { + Raise(command, new AppClientRevoked()); + } - private void AddLanguage(AddLanguage command) - { - Raise(command, new AppLanguageAdded()); - } + private void AddWorkflow(AddWorkflow command) + { + Raise(command, new AppWorkflowAdded()); + } - private void RemoveLanguage(RemoveLanguage command) - { - Raise(command, new AppLanguageRemoved()); - } + private void UpdateWorkflow(UpdateWorkflow command) + { + Raise(command, new AppWorkflowUpdated()); + } - private void AddRole(AddRole command) - { - Raise(command, new AppRoleAdded()); - } + private void DeleteWorkflow(DeleteWorkflow command) + { + Raise(command, new AppWorkflowDeleted()); + } - private void DeleteRole(DeleteRole command) - { - Raise(command, new AppRoleDeleted()); - } + private void AddLanguage(AddLanguage command) + { + Raise(command, new AppLanguageAdded()); + } - private void UpdateRole(UpdateRole command) - { - Raise(command, new AppRoleUpdated()); - } + private void RemoveLanguage(RemoveLanguage command) + { + Raise(command, new AppLanguageRemoved()); + } - private void DeleteApp(DeleteApp command) - { - Raise(command, new AppDeleted()); - } + private void AddRole(AddRole command) + { + Raise(command, new AppRoleAdded()); + } - private void Raise(T command, TEvent @event, NamedId? id = null) where T : class where TEvent : AppEvent - { - SimpleMapper.Map(command, @event); + private void DeleteRole(DeleteRole command) + { + Raise(command, new AppRoleDeleted()); + } - @event.AppId ??= id ?? Snapshot.NamedId(); + private void UpdateRole(UpdateRole command) + { + Raise(command, new AppRoleUpdated()); + } - RaiseEvent(Envelope.Create(@event)); - } + private void DeleteApp(DeleteApp command) + { + Raise(command, new AppDeleted()); + } - private IAppProvider AppProvider - { - get => serviceProvider.GetRequiredService(); - } + private void Raise(T command, TEvent @event, NamedId? id = null) where T : class where TEvent : AppEvent + { + SimpleMapper.Map(command, @event); - private IBillingPlans BillingPlans - { - get => serviceProvider.GetRequiredService(); - } + @event.AppId ??= id ?? Snapshot.NamedId(); - private IBillingManager BillingManager - { - get => serviceProvider.GetRequiredService(); - } + RaiseEvent(Envelope.Create(@event)); + } - private IUserResolver Users - { - get => serviceProvider.GetRequiredService(); - } + private IAppProvider AppProvider + { + get => serviceProvider.GetRequiredService(); + } - private Plan FreePlan - { - get => BillingPlans.GetFreePlan(); - } + private IBillingPlans BillingPlans + { + get => serviceProvider.GetRequiredService(); + } - private Plan Plan - { - get => BillingPlans.GetActualPlan(Snapshot.Plan?.PlanId).Plan; - } + private IBillingManager BillingManager + { + get => serviceProvider.GetRequiredService(); + } + + private IUserResolver Users + { + get => serviceProvider.GetRequiredService(); + } + + private Plan FreePlan + { + get => BillingPlans.GetFreePlan(); + } + + private Plan Plan + { + get => BillingPlans.GetActualPlan(Snapshot.Plan?.PlanId).Plan; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs index f11280cafa..4ae7775bad 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs @@ -12,171 +12,170 @@ using Squidex.Infrastructure.Validation; using Squidex.Text; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public static class GuardApp { - public static class GuardApp + public static void CanCreate(CreateApp command) { - public static void CanCreate(CreateApp command) - { - Guard.NotNull(command); + Guard.NotNull(command); - Validate.It(e => + Validate.It(e => + { + if (!command.Name.IsSlug()) { - if (!command.Name.IsSlug()) - { - e(Not.ValidSlug(nameof(command.Name)), nameof(command.Name)); - } - }); - } + e(Not.ValidSlug(nameof(command.Name)), nameof(command.Name)); + } + }); + } - public static void CanUploadImage(UploadAppImage command) - { - Guard.NotNull(command); + public static void CanUploadImage(UploadAppImage command) + { + Guard.NotNull(command); - Validate.It(e => + Validate.It(e => + { + if (command.File == null) { - if (command.File == null) - { - e(Not.Defined(nameof(command.File)), nameof(command.File)); - } - }); - } + e(Not.Defined(nameof(command.File)), nameof(command.File)); + } + }); + } - public static void CanUpdate(UpdateApp command) - { - Guard.NotNull(command); - } + public static void CanUpdate(UpdateApp command) + { + Guard.NotNull(command); + } - public static void CanRemoveImage(RemoveAppImage command) - { - Guard.NotNull(command); - } + public static void CanRemoveImage(RemoveAppImage command) + { + Guard.NotNull(command); + } - public static void CanUpdateAssetScripts(ConfigureAssetScripts command) - { - Guard.NotNull(command); - } + public static void CanUpdateAssetScripts(ConfigureAssetScripts command) + { + Guard.NotNull(command); + } + + public static void CanUpdateSettings(UpdateAppSettings command) + { + Guard.NotNull(command); - public static void CanUpdateSettings(UpdateAppSettings command) + Validate.It(e => { - Guard.NotNull(command); + var prefix = nameof(command.Settings); - Validate.It(e => + var settings = command.Settings; + + if (settings == null) { - var prefix = nameof(command.Settings); + e(Not.Defined(nameof(settings)), prefix); + return; + } - var settings = command.Settings; + var patternsPrefix = $"{prefix}.{nameof(settings.Patterns)}"; - if (settings == null) + if (settings.Patterns == null) + { + e(Not.Defined(nameof(settings.Patterns)), patternsPrefix); + } + else + { + settings.Patterns.Foreach((pattern, index) => { - e(Not.Defined(nameof(settings)), prefix); - return; - } + var patternPrefix = $"{patternsPrefix}[{index}]"; - var patternsPrefix = $"{prefix}.{nameof(settings.Patterns)}"; + if (string.IsNullOrWhiteSpace(pattern.Name)) + { + e(Not.Defined(nameof(pattern.Name)), $"{patternPrefix}.{nameof(pattern.Name)}"); + } - if (settings.Patterns == null) - { - e(Not.Defined(nameof(settings.Patterns)), patternsPrefix); - } - else - { - settings.Patterns.Foreach((pattern, index) => + if (string.IsNullOrWhiteSpace(pattern.Regex)) { - var patternPrefix = $"{patternsPrefix}[{index}]"; + e(Not.Defined(nameof(pattern.Regex)), $"{patternPrefix}.{nameof(pattern.Regex)}"); + } + }); + } - if (string.IsNullOrWhiteSpace(pattern.Name)) - { - e(Not.Defined(nameof(pattern.Name)), $"{patternPrefix}.{nameof(pattern.Name)}"); - } + var editorsPrefix = $"{prefix}.{nameof(settings.Editors)}"; - if (string.IsNullOrWhiteSpace(pattern.Regex)) - { - e(Not.Defined(nameof(pattern.Regex)), $"{patternPrefix}.{nameof(pattern.Regex)}"); - } - }); - } + if (settings.Editors == null) + { + e(Not.Defined(nameof(settings.Editors)), editorsPrefix); + } + else + { + settings.Editors.Foreach((editor, index) => + { + var editorPrefix = $"{editorsPrefix}[{index}]"; - var editorsPrefix = $"{prefix}.{nameof(settings.Editors)}"; + if (string.IsNullOrWhiteSpace(editor.Name)) + { + e(Not.Defined(nameof(editor.Name)), $"{editorPrefix}.{nameof(editor.Name)}"); + } - if (settings.Editors == null) - { - e(Not.Defined(nameof(settings.Editors)), editorsPrefix); - } - else - { - settings.Editors.Foreach((editor, index) => + if (string.IsNullOrWhiteSpace(editor.Url)) { - var editorPrefix = $"{editorsPrefix}[{index}]"; - - if (string.IsNullOrWhiteSpace(editor.Name)) - { - e(Not.Defined(nameof(editor.Name)), $"{editorPrefix}.{nameof(editor.Name)}"); - } - - if (string.IsNullOrWhiteSpace(editor.Url)) - { - e(Not.Defined(nameof(editor.Url)), $"{editorPrefix}.{nameof(editor.Url)}"); - } - }); - } - }); - } - - public static Task CanTransfer(TransferToTeam command, IAppEntity app, IAppProvider appProvider, CancellationToken ct) - { - Guard.NotNull(command); + e(Not.Defined(nameof(editor.Url)), $"{editorPrefix}.{nameof(editor.Url)}"); + } + }); + } + }); + } + + public static Task CanTransfer(TransferToTeam command, IAppEntity app, IAppProvider appProvider, CancellationToken ct) + { + Guard.NotNull(command); - return Validate.It(async e => + return Validate.It(async e => + { + if (command.TeamId == null) { - if (command.TeamId == null) - { - return; - } + return; + } - var team = await appProvider.GetTeamAsync(command.TeamId.Value, ct); + var team = await appProvider.GetTeamAsync(command.TeamId.Value, ct); - if (team == null || !team.Contributors.ContainsKey(command.Actor.Identifier)) - { - e(T.Get("apps.transfer.teamNotFound")); - } + if (team == null || !team.Contributors.ContainsKey(command.Actor.Identifier)) + { + e(T.Get("apps.transfer.teamNotFound")); + } - if (app.Plan != null) - { - e(T.Get("apps.transfer.planAssigned")); - } - }); - } + if (app.Plan != null) + { + e(T.Get("apps.transfer.planAssigned")); + } + }); + } - public static void CanChangePlan(ChangePlan command, IAppEntity app, IBillingPlans billingPlans) - { - Guard.NotNull(command); + public static void CanChangePlan(ChangePlan command, IAppEntity app, IBillingPlans billingPlans) + { + Guard.NotNull(command); - Validate.It(e => + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.PlanId)) { - if (string.IsNullOrWhiteSpace(command.PlanId)) - { - e(Not.Defined(nameof(command.PlanId)), nameof(command.PlanId)); - return; - } + e(Not.Defined(nameof(command.PlanId)), nameof(command.PlanId)); + return; + } - if (billingPlans.GetPlan(command.PlanId) == null) - { - e(T.Get("apps.plans.notFound"), nameof(command.PlanId)); - } + if (billingPlans.GetPlan(command.PlanId) == null) + { + e(T.Get("apps.plans.notFound"), nameof(command.PlanId)); + } - if (app.TeamId != null) - { - e(T.Get("apps.plans.assignedToTeam")); - } + if (app.TeamId != null) + { + e(T.Get("apps.plans.assignedToTeam")); + } - var plan = app.Plan; + var plan = app.Plan; - if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) - { - e(T.Get("apps.plans.notPlanOwner")); - } - }); - } + if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) + { + e(T.Get("apps.plans.notPlanOwner")); + } + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppClients.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppClients.cs index 5a7f43eea4..15d6fa74b1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppClients.cs @@ -11,87 +11,86 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public static class GuardAppClients { - public static class GuardAppClients + public static void CanAttach(AttachClient command, IAppEntity app) { - public static void CanAttach(AttachClient command, IAppEntity app) + Guard.NotNull(command); + + Validate.It(e => { - Guard.NotNull(command); + var clients = app.Clients; - Validate.It(e => + if (string.IsNullOrWhiteSpace(command.Id)) { - var clients = app.Clients; - - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("ClientId"), nameof(command.Id)); - } - else if (clients.ContainsKey(command.Id)) - { - e(T.Get("apps.clients.idAlreadyExists")); - } - }); - } + e(Not.Defined("ClientId"), nameof(command.Id)); + } + else if (clients.ContainsKey(command.Id)) + { + e(T.Get("apps.clients.idAlreadyExists")); + } + }); + } - public static void CanRevoke(RevokeClient command, IAppEntity app) - { - Guard.NotNull(command); + public static void CanRevoke(RevokeClient command, IAppEntity app) + { + Guard.NotNull(command); - GetClientOrThrow(app.Clients, command.Id); + GetClientOrThrow(app.Clients, command.Id); - Validate.It(e => + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Id)) { - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("ClientId"), nameof(command.Id)); - } - }); - } + e(Not.Defined("ClientId"), nameof(command.Id)); + } + }); + } - public static void CanUpdate(UpdateClient command, IAppEntity app) - { - Guard.NotNull(command); + public static void CanUpdate(UpdateClient command, IAppEntity app) + { + Guard.NotNull(command); - GetClientOrThrow(app.Clients, command.Id); + GetClientOrThrow(app.Clients, command.Id); - Validate.It(e => + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Id)) { - if (string.IsNullOrWhiteSpace(command.Id)) - { - e(Not.Defined("Clientd"), nameof(command.Id)); - } - - if (command.Role != null && !app.Roles.Contains(command.Role)) - { - e(Not.Valid(nameof(command.Role)), nameof(command.Role)); - } - - if (command.ApiCallsLimit is < 0) - { - e(Not.GreaterEqualsThan(nameof(command.ApiCallsLimit), "0"), nameof(command.ApiCallsLimit)); - } - - if (command.ApiTrafficLimit is < 0) - { - e(Not.GreaterEqualsThan(nameof(command.ApiTrafficLimit), "0"), nameof(command.ApiTrafficLimit)); - } - }); - } + e(Not.Defined("Clientd"), nameof(command.Id)); + } - private static AppClient? GetClientOrThrow(AppClients clients, string id) - { - if (string.IsNullOrWhiteSpace(id)) + if (command.Role != null && !app.Roles.Contains(command.Role)) + { + e(Not.Valid(nameof(command.Role)), nameof(command.Role)); + } + + if (command.ApiCallsLimit is < 0) { - return null; + e(Not.GreaterEqualsThan(nameof(command.ApiCallsLimit), "0"), nameof(command.ApiCallsLimit)); } - if (!clients.TryGetValue(id, out var client)) + if (command.ApiTrafficLimit is < 0) { - throw new DomainObjectNotFoundException(id); + e(Not.GreaterEqualsThan(nameof(command.ApiTrafficLimit), "0"), nameof(command.ApiTrafficLimit)); } + }); + } - return client; + private static AppClient? GetClientOrThrow(AppClients clients, string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return null; } + + if (!clients.TryGetValue(id, out var client)) + { + throw new DomainObjectNotFoundException(id); + } + + return client; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs index a4daee6bc7..e993bdb83c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppContributors.cs @@ -13,77 +13,76 @@ using Squidex.Infrastructure.Validation; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public static class GuardAppContributors { - public static class GuardAppContributors + public static Task CanAssign(AssignContributor command, IAppEntity app, IUserResolver users, Plan plan) { - public static Task CanAssign(AssignContributor command, IAppEntity app, IUserResolver users, Plan plan) - { - Guard.NotNull(command); + Guard.NotNull(command); - var contributors = app.Contributors; + var contributors = app.Contributors; + + return Validate.It(async e => + { + if (!app.Roles.Contains(command.Role)) + { + e(Not.Valid(nameof(command.Role)), nameof(command.Role)); + } - return Validate.It(async e => + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); + } + else { - if (!app.Roles.Contains(command.Role)) + var user = await users.FindByIdAsync(command.ContributorId); + + if (user == null) { - e(Not.Valid(nameof(command.Role)), nameof(command.Role)); + throw new DomainObjectNotFoundException(command.ContributorId); } - if (string.IsNullOrWhiteSpace(command.ContributorId)) + if (!command.IgnoreActor && string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) { - e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); + throw new DomainForbiddenException(T.Get("apps.contributors.cannotChangeYourself")); } - else - { - var user = await users.FindByIdAsync(command.ContributorId); - - if (user == null) - { - throw new DomainObjectNotFoundException(command.ContributorId); - } - - if (!command.IgnoreActor && string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) - { - throw new DomainForbiddenException(T.Get("apps.contributors.cannotChangeYourself")); - } - if (!command.IgnorePlans && !contributors.TryGetValue(command.ContributorId, out _)) + if (!command.IgnorePlans && !contributors.TryGetValue(command.ContributorId, out _)) + { + if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) { - if (plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors) - { - e(T.Get("apps.contributors.maxReached")); - } + e(T.Get("apps.contributors.maxReached")); } } - }); - } + } + }); + } - public static void CanRemove(RemoveContributor command, IAppEntity app) - { - Guard.NotNull(command); + public static void CanRemove(RemoveContributor command, IAppEntity app) + { + Guard.NotNull(command); - var contributors = app.Contributors; + var contributors = app.Contributors; - Validate.It(e => + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.ContributorId)) { - if (string.IsNullOrWhiteSpace(command.ContributorId)) - { - e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); - } + e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); + } - var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); + var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); - if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) - { - e(T.Get("apps.contributors.onlyOneOwner")); - } - }); - - if (!contributors.ContainsKey(command.ContributorId)) + if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) { - throw new DomainObjectNotFoundException(command.ContributorId); + e(T.Get("apps.contributors.onlyOneOwner")); } + }); + + if (!contributors.ContainsKey(command.ContributorId)) + { + throw new DomainObjectNotFoundException(command.ContributorId); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppLanguages.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppLanguages.cs index 3ce3142ea2..33ae204237 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppLanguages.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppLanguages.cs @@ -11,107 +11,106 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public static class GuardAppLanguages { - public static class GuardAppLanguages + public static void CanAdd(AddLanguage command, IAppEntity app) { - public static void CanAdd(AddLanguage command, IAppEntity app) + Guard.NotNull(command); + + Validate.It(e => { - Guard.NotNull(command); + var languages = app.Languages; + var language = command.Language; - Validate.It(e => + if (language == null) { - var languages = app.Languages; - var language = command.Language; + e(Not.Defined(nameof(command.Language)), nameof(command.Language)); + } + else if (app.Languages.Contains(command.Language)) + { + e(T.Get("apps.languages.languageAlreadyAdded")); + } + }); + } - if (language == null) - { - e(Not.Defined(nameof(command.Language)), nameof(command.Language)); - } - else if (app.Languages.Contains(command.Language)) - { - e(T.Get("apps.languages.languageAlreadyAdded")); - } - }); - } + public static void CanRemove(RemoveLanguage command, IAppEntity app) + { + Guard.NotNull(command); - public static void CanRemove(RemoveLanguage command, IAppEntity app) + Validate.It(e => { - Guard.NotNull(command); + var languages = app.Languages; + var language = command.Language; - Validate.It(e => + if (language == null) { - var languages = app.Languages; - var language = command.Language; + e(Not.Defined(nameof(command.Language)), nameof(command.Language)); + } + else + { + CheckLanguageExists(languages, language); - if (language == null) + if (languages.IsMaster(language)) { - e(Not.Defined(nameof(command.Language)), nameof(command.Language)); + e(T.Get("apps.languages.masterLanguageNotRemovable")); } - else - { - CheckLanguageExists(languages, language); + } + }); + } - if (languages.IsMaster(language)) - { - e(T.Get("apps.languages.masterLanguageNotRemovable")); - } - } - }); - } + public static void CanUpdate(UpdateLanguage command, IAppEntity app) + { + Guard.NotNull(command); - public static void CanUpdate(UpdateLanguage command, IAppEntity app) + Validate.It(e => { - Guard.NotNull(command); + var languages = app.Languages; + var language = command.Language; - Validate.It(e => + if (language == null) + { + e(Not.Defined(nameof(command.Language)), nameof(command.Language)); + } + else { - var languages = app.Languages; - var language = command.Language; + CheckLanguageExists(languages, language); - if (language == null) + if (languages.IsMaster(language) || command.IsMaster) { - e(Not.Defined(nameof(command.Language)), nameof(command.Language)); - } - else - { - CheckLanguageExists(languages, language); - - if (languages.IsMaster(language) || command.IsMaster) + if (command.IsOptional) { - if (command.IsOptional) - { - e(T.Get("apps.languages.masterLanguageNotOptional"), nameof(command.IsMaster)); - } - - if (command.Fallback?.Length > 0) - { - e(T.Get("apps.languages.masterLanguageNoFallbacks"), nameof(command.Fallback)); - } + e(T.Get("apps.languages.masterLanguageNotOptional"), nameof(command.IsMaster)); } - if (command.Fallback == null) + if (command.Fallback?.Length > 0) { - return; + e(T.Get("apps.languages.masterLanguageNoFallbacks"), nameof(command.Fallback)); } + } - foreach (var fallback in command.Fallback) + if (command.Fallback == null) + { + return; + } + + foreach (var fallback in command.Fallback) + { + if (!languages.Contains(fallback)) { - if (!languages.Contains(fallback)) - { - e(T.Get("apps.languages.fallbackNotFound", new { fallback }), nameof(command.Fallback)); - } + e(T.Get("apps.languages.fallbackNotFound", new { fallback }), nameof(command.Fallback)); } } - }); - } + } + }); + } - private static void CheckLanguageExists(LanguagesConfig languages, Language language) + private static void CheckLanguageExists(LanguagesConfig languages, Language language) + { + if (!languages.Contains(language)) { - if (!languages.Contains(language)) - { - throw new DomainObjectNotFoundException(language); - } + throw new DomainObjectNotFoundException(language); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppRoles.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppRoles.cs index 6ad268a7ac..07b07f20ce 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppRoles.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppRoles.cs @@ -11,97 +11,96 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public static class GuardAppRoles { - public static class GuardAppRoles + public static void CanAdd(AddRole command, IAppEntity app) { - public static void CanAdd(AddRole command, IAppEntity app) - { - Guard.NotNull(command); + Guard.NotNull(command); - var roles = app.Roles; + var roles = app.Roles; - Validate.It(e => + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Name)) { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined(nameof(command.Name)), nameof(command.Name)); - } - else if (roles.Contains(command.Name)) - { - e(T.Get("apps.roles.nameAlreadyExists")); - } - }); - } + e(Not.Defined(nameof(command.Name)), nameof(command.Name)); + } + else if (roles.Contains(command.Name)) + { + e(T.Get("apps.roles.nameAlreadyExists")); + } + }); + } - public static void CanDelete(DeleteRole command, IAppEntity app) - { - Guard.NotNull(command); + public static void CanDelete(DeleteRole command, IAppEntity app) + { + Guard.NotNull(command); - var roles = app.Roles; + var roles = app.Roles; - CheckRoleExists(roles, command.Name); + CheckRoleExists(roles, command.Name); - Validate.It(e => + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Name)) { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined(nameof(command.Name)), nameof(command.Name)); - } - else if (Roles.IsDefault(command.Name)) - { - e(T.Get("apps.roles.defaultRoleNotRemovable")); - } - - if (app.Clients.Values.Any(x => string.Equals(x.Role, command.Name, StringComparison.OrdinalIgnoreCase))) - { - e(T.Get("apps.roles.usedRoleByClientsNotRemovable")); - } - - if (app.Contributors.Values.Any(x => string.Equals(x, command.Name, StringComparison.OrdinalIgnoreCase))) - { - e(T.Get("apps.roles.usedRoleByContributorsNotRemovable")); - } - }); - } + e(Not.Defined(nameof(command.Name)), nameof(command.Name)); + } + else if (Roles.IsDefault(command.Name)) + { + e(T.Get("apps.roles.defaultRoleNotRemovable")); + } - public static void CanUpdate(UpdateRole command, IAppEntity app) - { - Guard.NotNull(command); + if (app.Clients.Values.Any(x => string.Equals(x.Role, command.Name, StringComparison.OrdinalIgnoreCase))) + { + e(T.Get("apps.roles.usedRoleByClientsNotRemovable")); + } - var roles = app.Roles; + if (app.Contributors.Values.Any(x => string.Equals(x, command.Name, StringComparison.OrdinalIgnoreCase))) + { + e(T.Get("apps.roles.usedRoleByContributorsNotRemovable")); + } + }); + } - CheckRoleExists(roles, command.Name); + public static void CanUpdate(UpdateRole command, IAppEntity app) + { + Guard.NotNull(command); - Validate.It(e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined(nameof(command.Name)), nameof(command.Name)); - } - else if (Roles.IsDefault(command.Name)) - { - e(T.Get("apps.roles.defaultRoleNotUpdateable")); - } - - if (command.Permissions == null) - { - e(Not.Defined(nameof(command.Permissions)), nameof(command.Permissions)); - } - }); - } + var roles = app.Roles; - private static void CheckRoleExists(Roles roles, string name) + CheckRoleExists(roles, command.Name); + + Validate.It(e => { - if (string.IsNullOrWhiteSpace(name) || Roles.IsDefault(name)) + if (string.IsNullOrWhiteSpace(command.Name)) { - return; + e(Not.Defined(nameof(command.Name)), nameof(command.Name)); + } + else if (Roles.IsDefault(command.Name)) + { + e(T.Get("apps.roles.defaultRoleNotUpdateable")); } - if (!roles.ContainsCustom(name)) + if (command.Permissions == null) { - throw new DomainObjectNotFoundException(name); + e(Not.Defined(nameof(command.Permissions)), nameof(command.Permissions)); } + }); + } + + private static void CheckRoleExists(Roles roles, string name) + { + if (string.IsNullOrWhiteSpace(name) || Roles.IsDefault(name)) + { + return; + } + + if (!roles.ContainsCustom(name)) + { + throw new DomainObjectNotFoundException(name); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppWorkflows.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppWorkflows.cs index 8984d2c0e2..385acdb6b7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppWorkflows.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppWorkflows.cs @@ -11,102 +11,101 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public static class GuardAppWorkflows { - public static class GuardAppWorkflows + public static void CanAdd(AddWorkflow command) { - public static void CanAdd(AddWorkflow command) - { - Guard.NotNull(command); + Guard.NotNull(command); - Validate.It(e => + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Name)) { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined(nameof(command.Name)), nameof(command.Name)); - } - }); - } + e(Not.Defined(nameof(command.Name)), nameof(command.Name)); + } + }); + } - public static void CanUpdate(UpdateWorkflow command, IAppEntity app) - { - Guard.NotNull(command); + public static void CanUpdate(UpdateWorkflow command, IAppEntity app) + { + Guard.NotNull(command); - var workflows = app.Workflows; + var workflows = app.Workflows; - CheckWorkflowExists(workflows, command.WorkflowId); + CheckWorkflowExists(workflows, command.WorkflowId); - Validate.It(e => + Validate.It(e => + { + if (command.Workflow == null) { - if (command.Workflow == null) - { - e(Not.Defined(nameof(command.Workflow)), nameof(command.Workflow)); - return; - } + e(Not.Defined(nameof(command.Workflow)), nameof(command.Workflow)); + return; + } - var workflow = command.Workflow; + var workflow = command.Workflow; - if (!workflow.Steps.ContainsKey(workflow.Initial)) - { - e(Not.Defined("InitialStep"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); - } + if (!workflow.Steps.ContainsKey(workflow.Initial)) + { + e(Not.Defined("InitialStep"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); + } - if (workflow.Initial == Status.Published) - { - e(T.Get("workflows.publishedIsInitial"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); - } + if (workflow.Initial == Status.Published) + { + e(T.Get("workflows.publishedIsInitial"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); + } - var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}"; + var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}"; - if (!workflow.Steps.ContainsKey(Status.Published)) + if (!workflow.Steps.ContainsKey(Status.Published)) + { + e(T.Get("workflows.publishedNotDefined"), stepsPrefix); + } + + foreach (var step in workflow.Steps) + { + var stepPrefix = $"{stepsPrefix}.{step.Key}"; + + if (step.Value == null) { - e(T.Get("workflows.publishedNotDefined"), stepsPrefix); + e(Not.Defined("WorkflowStep"), stepPrefix); } - - foreach (var step in workflow.Steps) + else { - var stepPrefix = $"{stepsPrefix}.{step.Key}"; - - if (step.Value == null) + foreach (var (status, transition) in step.Value.Transitions) { - e(Not.Defined("WorkflowStep"), stepPrefix); - } - else - { - foreach (var (status, transition) in step.Value.Transitions) - { - var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{status}"; + var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{status}"; - if (!workflow.Steps.ContainsKey(status)) - { - e(T.Get("workflows.publishedStepNotFound"), transitionPrefix); - } + if (!workflow.Steps.ContainsKey(status)) + { + e(T.Get("workflows.publishedStepNotFound"), transitionPrefix); + } - if (transition == null) - { - e(Not.Defined("WorkflowTransition"), transitionPrefix); - } + if (transition == null) + { + e(Not.Defined("WorkflowTransition"), transitionPrefix); } } } - }); - } + } + }); + } - public static void CanDelete(DeleteWorkflow command, IAppEntity app) - { - Guard.NotNull(command); + public static void CanDelete(DeleteWorkflow command, IAppEntity app) + { + Guard.NotNull(command); - var workflows = app.Workflows; + var workflows = app.Workflows; - CheckWorkflowExists(workflows, command.WorkflowId); - } + CheckWorkflowExists(workflows, command.WorkflowId); + } - private static void CheckWorkflowExists(Workflows workflows, DomainId id) + private static void CheckWorkflowExists(Workflows workflows, DomainId id) + { + if (!workflows.ContainsKey(id)) { - if (!workflows.ContainsKey(id)) - { - throw new DomainObjectNotFoundException(id.ToString()); - } + throw new DomainObjectNotFoundException(id.ToString()); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs index d77f8d715e..928dd3a892 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -11,40 +11,39 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public interface IAppEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion { - public interface IAppEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - string Name { get; } + string Name { get; } - string? Label { get; } + string? Label { get; } - string? Description { get; } + string? Description { get; } - DomainId? TeamId { get; } + DomainId? TeamId { get; } - Roles Roles { get; } + Roles Roles { get; } - AssignedPlan? Plan { get; } + AssignedPlan? Plan { get; } - Contributors Contributors { get; } + Contributors Contributors { get; } - AppImage? Image { get; } + AppImage? Image { get; } - AppClients Clients { get; } + AppClients Clients { get; } - AppSettings Settings { get; } + AppSettings Settings { get; } - AssetScripts AssetScripts { get; } + AssetScripts AssetScripts { get; } - LanguagesConfig Languages { get; } + LanguagesConfig Languages { get; } - Workflows Workflows { get; } + Workflows Workflows { get; } - bool IsDeleted { get; } - } + bool IsDeleted { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppImageStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppImageStore.cs index 581820f1b2..b65c2a9a95 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppImageStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppImageStore.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public interface IAppImageStore { - public interface IAppImageStore - { - Task UploadAsync(DomainId appId, Stream stream, - CancellationToken ct = default); + Task UploadAsync(DomainId appId, Stream stream, + CancellationToken ct = default); - Task DownloadAsync(DomainId appId, Stream stream, - CancellationToken ct = default); - } + Task DownloadAsync(DomainId appId, Stream stream, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs index 7a02396f40..a1181ecbf5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public interface IAppLogStore { - public interface IAppLogStore - { - Task LogAsync(DomainId appId, RequestLog request, - CancellationToken ct = default); + Task LogAsync(DomainId appId, RequestLog request, + CancellationToken ct = default); - Task ReadLogAsync(DomainId appId, DateTime fromDate, DateTime toDate, Stream stream, - CancellationToken ct = default); - } + Task ReadLogAsync(DomainId appId, DateTime fromDate, DateTime toDate, Stream stream, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs index 875fb4ff43..4ddd8d530f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs @@ -8,20 +8,19 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public interface IAppUISettings { - public interface IAppUISettings - { - Task GetAsync(DomainId appId, string? userId, - CancellationToken ct = default); + Task GetAsync(DomainId appId, string? userId, + CancellationToken ct = default); - Task SetAsync(DomainId appId, string? userId, string path, JsonValue value, - CancellationToken ct = default); + Task SetAsync(DomainId appId, string? userId, string path, JsonValue value, + CancellationToken ct = default); - Task SetAsync(DomainId appId, string? userId, JsonObject settings, - CancellationToken ct = default); + Task SetAsync(DomainId appId, string? userId, JsonObject settings, + CancellationToken ct = default); - Task RemoveAsync(DomainId appId, string? userId, string path, - CancellationToken ct = default); - } + Task RemoveAsync(DomainId appId, string? userId, string path, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs index 1f7b79b620..61e95a4c42 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -17,232 +17,231 @@ using Squidex.Infrastructure.Validation; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Apps.Indexes +namespace Squidex.Domain.Apps.Entities.Apps.Indexes; + +public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable { - public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); + private readonly IAppRepository appRepository; + private readonly IReplicatedCache appCache; + private readonly NameReservationState namesState; + + public AppsIndex(IAppRepository appRepository, IReplicatedCache appCache, + IPersistenceFactory persistenceFactory) { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); - private readonly IAppRepository appRepository; - private readonly IReplicatedCache appCache; - private readonly NameReservationState namesState; + this.appRepository = appRepository; + this.appCache = appCache; - public AppsIndex(IAppRepository appRepository, IReplicatedCache appCache, - IPersistenceFactory persistenceFactory) - { - this.appRepository = appRepository; - this.appCache = appCache; + namesState = new NameReservationState(persistenceFactory, "Apps"); + } - namesState = new NameReservationState(persistenceFactory, "Apps"); - } + public Task InitializeAsync( + CancellationToken ct) + { + return namesState.LoadAsync(ct); + } - public Task InitializeAsync( - CancellationToken ct) - { - return namesState.LoadAsync(ct); - } + public Task RemoveReservationAsync(string? token, + CancellationToken ct = default) + { + return namesState.RemoveReservationAsync(token, ct); + } - public Task RemoveReservationAsync(string? token, - CancellationToken ct = default) + public async Task ReserveAsync(DomainId id, string name, + CancellationToken ct = default) + { + if (await appRepository.FindAsync(name, ct) != null) { - return namesState.RemoveReservationAsync(token, ct); + return null; } - public async Task ReserveAsync(DomainId id, string name, - CancellationToken ct = default) + return await namesState.ReserveAsync(id, name, ct); + } + + public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppsForUserAsync")) { - if (await appRepository.FindAsync(name, ct) != null) + var apps = await appRepository.QueryAllAsync(userId, permissions.ToAppNames(), ct); + + foreach (var app in apps.Where(IsValid)) { - return null; + await CacheItAsync(app); } - return await namesState.ReserveAsync(id, name, ct); + return apps.Where(IsValid).ToList(); } + } - public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions, - CancellationToken ct = default) + public async Task> GetAppsForTeamAsync(DomainId teamId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppsForTeamAsync")) { - using (Telemetry.Activities.StartActivity("AppsIndex/GetAppsForUserAsync")) - { - var apps = await appRepository.QueryAllAsync(userId, permissions.ToAppNames(), ct); + var apps = await appRepository.QueryAllAsync(teamId, ct); - foreach (var app in apps.Where(IsValid)) - { - await CacheItAsync(app); - } - - return apps.Where(IsValid).ToList(); - } - } - - public async Task> GetAppsForTeamAsync(DomainId teamId, - CancellationToken ct = default) - { - using (Telemetry.Activities.StartActivity("AppsIndex/GetAppsForTeamAsync")) + foreach (var app in apps.Where(IsValid)) { - var apps = await appRepository.QueryAllAsync(teamId, ct); - - foreach (var app in apps.Where(IsValid)) - { - await CacheItAsync(app); - } - - return apps.Where(IsValid).ToList(); + await CacheItAsync(app); } + + return apps.Where(IsValid).ToList(); } + } - public async Task GetAppAsync(string name, bool canCache = false, - CancellationToken ct = default) + public async Task GetAppAsync(string name, bool canCache = false, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppByNameAsync")) { - using (Telemetry.Activities.StartActivity("AppsIndex/GetAppByNameAsync")) + if (canCache) { - if (canCache) + if (appCache.TryGetValue(GetCacheKey(name), out var v) && v is IAppEntity cacheApp) { - if (appCache.TryGetValue(GetCacheKey(name), out var v) && v is IAppEntity cacheApp) - { - return cacheApp; - } + return cacheApp; } + } - var app = await appRepository.FindAsync(name, ct); - - if (!IsValid(app)) - { - app = null; - } + var app = await appRepository.FindAsync(name, ct); - if (app != null) - { - await CacheItAsync(app); - } + if (!IsValid(app)) + { + app = null; + } - return app; + if (app != null) + { + await CacheItAsync(app); } + + return app; } + } - public async Task GetAppAsync(DomainId appId, bool canCache = false, - CancellationToken ct = default) + public async Task GetAppAsync(DomainId appId, bool canCache = false, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppAsync")) { - using (Telemetry.Activities.StartActivity("AppsIndex/GetAppAsync")) + if (canCache) { - if (canCache) + if (appCache.TryGetValue(GetCacheKey(appId), out var cached) && cached is IAppEntity cachedApp) { - if (appCache.TryGetValue(GetCacheKey(appId), out var cached) && cached is IAppEntity cachedApp) - { - return cachedApp; - } + return cachedApp; } + } - var app = await appRepository.FindAsync(appId, ct); + var app = await appRepository.FindAsync(appId, ct); - if (!IsValid(app)) - { - app = null; - } - - if (app != null) - { - await CacheItAsync(app); - } + if (!IsValid(app)) + { + app = null; + } - return app; + if (app != null) + { + await CacheItAsync(app); } + + return app; } + } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - var command = context.Command; + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + var command = context.Command; - if (command is CreateApp createApp) - { - var token = await CheckAppAsync(createApp, ct); - try - { - await next(context, ct); - } - finally - { - // Always remove the reservation and therefore do not pass over cancellation token. - await namesState.RemoveReservationAsync(token, default); - } - } - else + if (command is CreateApp createApp) + { + var token = await CheckAppAsync(createApp, ct); + try { await next(context, ct); } - - if (context.IsCompleted) + finally { - switch (command) - { - case CreateApp create: - await OnCreateAsync(create); - break; - case DeleteApp delete: - await OnDeleteAsync(delete); - break; - case AppCommand update: - await OnUpdateAsync(update); - break; - } + // Always remove the reservation and therefore do not pass over cancellation token. + await namesState.RemoveReservationAsync(token, default); } } - - private async Task CheckAppAsync(CreateApp command, - CancellationToken ct) + else { - var token = await ReserveAsync(command.AppId, command.Name, ct); + await next(context, ct); + } - if (token == null) + if (context.IsCompleted) + { + switch (command) { - throw new ValidationException(T.Get("apps.nameAlreadyExists")); + case CreateApp create: + await OnCreateAsync(create); + break; + case DeleteApp delete: + await OnDeleteAsync(delete); + break; + case AppCommand update: + await OnUpdateAsync(update); + break; } - - return token; } + } - private async Task OnCreateAsync(CreateApp create) - { - await InvalidateItAsync(create.AppId, create.Name); - } + private async Task CheckAppAsync(CreateApp command, + CancellationToken ct) + { + var token = await ReserveAsync(command.AppId, command.Name, ct); - private async Task OnDeleteAsync(DeleteApp delete) + if (token == null) { - await InvalidateItAsync(delete.AppId.Id, delete.AppId.Name); + throw new ValidationException(T.Get("apps.nameAlreadyExists")); } - private async Task OnUpdateAsync(AppCommand update) - { - await InvalidateItAsync(update.AppId.Id, update.AppId.Name); - } + return token; + } - private static string GetCacheKey(DomainId id) - { - return $"{typeof(AppsIndex)}_Apps_Id_{id}"; - } + private async Task OnCreateAsync(CreateApp create) + { + await InvalidateItAsync(create.AppId, create.Name); + } - private static string GetCacheKey(string name) - { - return $"{typeof(AppsIndex)}_Apps_Name_{name}"; - } + private async Task OnDeleteAsync(DeleteApp delete) + { + await InvalidateItAsync(delete.AppId.Id, delete.AppId.Name); + } - private static bool IsValid(IAppEntity? app) - { - return app is { Version: > EtagVersion.Empty, IsDeleted: false }; - } + private async Task OnUpdateAsync(AppCommand update) + { + await InvalidateItAsync(update.AppId.Id, update.AppId.Name); + } - private Task InvalidateItAsync(DomainId id, string name) - { - return appCache.RemoveAsync( - GetCacheKey(id), - GetCacheKey(name)); - } + private static string GetCacheKey(DomainId id) + { + return $"{typeof(AppsIndex)}_Apps_Id_{id}"; + } - private Task CacheItAsync(IAppEntity app) - { - return Task.WhenAll( - appCache.AddAsync(GetCacheKey(app.Id), app, CacheDuration), - appCache.AddAsync(GetCacheKey(app.Name), app, CacheDuration)); - } + private static string GetCacheKey(string name) + { + return $"{typeof(AppsIndex)}_Apps_Name_{name}"; + } + + private static bool IsValid(IAppEntity? app) + { + return app is { Version: > EtagVersion.Empty, IsDeleted: false }; + } + + private Task InvalidateItAsync(DomainId id, string name) + { + return appCache.RemoveAsync( + GetCacheKey(id), + GetCacheKey(name)); + } + + private Task CacheItAsync(IAppEntity app) + { + return Task.WhenAll( + appCache.AddAsync(GetCacheKey(app.Id), app, CacheDuration), + appCache.AddAsync(GetCacheKey(app.Name), app, CacheDuration)); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs index 0422cb0a47..703fb1170c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs @@ -8,26 +8,25 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; -namespace Squidex.Domain.Apps.Entities.Apps.Indexes +namespace Squidex.Domain.Apps.Entities.Apps.Indexes; + +public interface IAppsIndex { - public interface IAppsIndex - { - Task> GetAppsForUserAsync(string userId, PermissionSet permissions, - CancellationToken ct = default); + Task> GetAppsForUserAsync(string userId, PermissionSet permissions, + CancellationToken ct = default); - Task> GetAppsForTeamAsync(DomainId teamId, - CancellationToken ct = default); + Task> GetAppsForTeamAsync(DomainId teamId, + CancellationToken ct = default); - Task GetAppAsync(string name, bool canCache = false, - CancellationToken ct = default); + Task GetAppAsync(string name, bool canCache = false, + CancellationToken ct = default); - Task GetAppAsync(DomainId appId, bool canCache = false, - CancellationToken ct = default); + Task GetAppAsync(DomainId appId, bool canCache = false, + CancellationToken ct = default); - Task ReserveAsync(DomainId id, string name, - CancellationToken ct = default); + Task ReserveAsync(DomainId id, string name, + CancellationToken ct = default); - Task RemoveReservationAsync(string? token, - CancellationToken ct = default); - } + Task RemoveReservationAsync(string? token, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/InitialSettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/InitialSettings.cs index 2278a63010..7502506a6a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/InitialSettings.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/InitialSettings.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Core.Apps; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class InitialSettings { - public sealed class InitialSettings - { - public AppSettings Settings { get; set; } - } + public AppSettings Settings { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs index 241ea992f4..4a9af7a1f2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs @@ -14,52 +14,51 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Apps.Plans; + +public sealed class RestrictAppsCommandMiddleware : ICommandMiddleware { - public sealed class RestrictAppsCommandMiddleware : ICommandMiddleware + private readonly RestrictAppsOptions usageOptions; + private readonly IUserResolver userResolver; + + public RestrictAppsCommandMiddleware(IOptions usageOptions, IUserResolver userResolver) { - private readonly RestrictAppsOptions usageOptions; - private readonly IUserResolver userResolver; + this.usageOptions = usageOptions.Value; + this.userResolver = userResolver; + } - public RestrictAppsCommandMiddleware(IOptions usageOptions, IUserResolver userResolver) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (usageOptions.MaximumNumberOfApps <= 0 || context.Command is not CreateApp createApp || createApp.Actor.IsClient) { - this.usageOptions = usageOptions.Value; - this.userResolver = userResolver; + await next(context, ct); + return; } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - if (usageOptions.MaximumNumberOfApps <= 0 || context.Command is not CreateApp createApp || createApp.Actor.IsClient) - { - await next(context, ct); - return; - } + var totalApps = 0; - var totalApps = 0; + var user = await userResolver.FindByIdAsync(createApp.Actor.Identifier, ct); - var user = await userResolver.FindByIdAsync(createApp.Actor.Identifier, ct); + if (user != null) + { + totalApps = user.Claims.GetTotalApps(); - if (user != null) + if (totalApps >= usageOptions.MaximumNumberOfApps) { - totalApps = user.Claims.GetTotalApps(); - - if (totalApps >= usageOptions.MaximumNumberOfApps) - { - throw new ValidationException(T.Get("apps.maximumTotalReached")); - } + throw new ValidationException(T.Get("apps.maximumTotalReached")); } + } - await next(context, ct); + await next(context, ct); - if (context.IsCompleted && user != null) - { - var newAppsCount = totalApps + 1; - var newAppsValue = newAppsCount.ToString(CultureInfo.InvariantCulture); + if (context.IsCompleted && user != null) + { + var newAppsCount = totalApps + 1; + var newAppsValue = newAppsCount.ToString(CultureInfo.InvariantCulture); - // Always update the user and therefore do nto pass cancellation token. - await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.TotalApps, newAppsValue, true, default); - } + // Always update the user and therefore do nto pass cancellation token. + await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.TotalApps, newAppsValue, true, default); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsOptions.cs index b782d0fc32..beafb4d966 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsOptions.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Apps.Plans; + +public sealed class RestrictAppsOptions { - public sealed class RestrictAppsOptions - { - public int MaximumNumberOfApps { get; set; } = 0; - } + public int MaximumNumberOfApps { get; set; } = 0; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs index 4fd6ecb37b..4770f56705 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -7,20 +7,19 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Repositories +namespace Squidex.Domain.Apps.Entities.Apps.Repositories; + +public interface IAppRepository { - public interface IAppRepository - { - Task> QueryAllAsync(string contributorId, IEnumerable names, - CancellationToken ct = default); + Task> QueryAllAsync(string contributorId, IEnumerable names, + CancellationToken ct = default); - Task> QueryAllAsync(DomainId teamId, - CancellationToken ct = default); + Task> QueryAllAsync(DomainId teamId, + CancellationToken ct = default); - Task FindAsync(DomainId id, - CancellationToken ct = default); + Task FindAsync(DomainId id, + CancellationToken ct = default); - Task FindAsync(string name, - CancellationToken ct = default); - } + Task FindAsync(string name, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/RequestLog.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/RequestLog.cs index 57fa4e923e..1ff21e5c1d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/RequestLog.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/RequestLog.cs @@ -7,34 +7,33 @@ using NodaTime; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public struct RequestLog { - public struct RequestLog - { - public Instant Timestamp; + public Instant Timestamp; - public string? RequestMethod; + public string? RequestMethod; - public string? RequestPath; + public string? RequestPath; - public string? UserId; + public string? UserId; - public string? UserClientId; + public string? UserClientId; - public string? CacheServer; + public string? CacheServer; - public string? CacheStatus; + public string? CacheStatus; - public int StatusCode; + public int StatusCode; - public int CacheTTL; + public int CacheTTL; - public long CacheHits; + public long CacheHits; - public long ElapsedMs; + public long ElapsedMs; - public long Bytes; + public long Bytes; - public double Costs; - } + public double Costs; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs index 3eeb344dfe..b1360d198f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs @@ -9,85 +9,84 @@ using Squidex.Infrastructure.Security; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class RolePermissionsProvider { - public sealed class RolePermissionsProvider + private readonly List forAppSchemas = new List(); + private readonly List forAppWithoutSchemas = new List(); + private readonly IAppProvider appProvider; + + public RolePermissionsProvider(IAppProvider appProvider) { - private readonly List forAppSchemas = new List(); - private readonly List forAppWithoutSchemas = new List(); - private readonly IAppProvider appProvider; + this.appProvider = appProvider; - public RolePermissionsProvider(IAppProvider appProvider) + foreach (var field in typeof(PermissionIds).GetFields(BindingFlags.Public | BindingFlags.Static)) { - this.appProvider = appProvider; - - foreach (var field in typeof(PermissionIds).GetFields(BindingFlags.Public | BindingFlags.Static)) + if (field.IsLiteral && !field.IsInitOnly) { - if (field.IsLiteral && !field.IsInitOnly) - { - var value = field.GetValue(null) as string; + var value = field.GetValue(null) as string; - if (value?.StartsWith(PermissionIds.App, StringComparison.OrdinalIgnoreCase) == true) + if (value?.StartsWith(PermissionIds.App, StringComparison.OrdinalIgnoreCase) == true) + { + if (value.IndexOf("{schema}", PermissionIds.App.Length, StringComparison.OrdinalIgnoreCase) >= 0) + { + forAppSchemas.Add(value); + } + else { - if (value.IndexOf("{schema}", PermissionIds.App.Length, StringComparison.OrdinalIgnoreCase) >= 0) - { - forAppSchemas.Add(value); - } - else - { - forAppWithoutSchemas.Add(value); - } + forAppWithoutSchemas.Add(value); } } } } + } - public async Task> GetPermissionsAsync(IAppEntity app) - { - var schemaNames = await GetSchemaNamesAsync(app); - - var result = new List { Permission.Any }; + public async Task> GetPermissionsAsync(IAppEntity app) + { + var schemaNames = await GetSchemaNamesAsync(app); - foreach (var permission in forAppWithoutSchemas) - { - if (permission.Length > PermissionIds.App.Length + 1) - { - var trimmed = permission[(PermissionIds.App.Length + 1)..]; + var result = new List { Permission.Any }; - if (trimmed.Length > 0) - { - result.Add(trimmed); - } - } - } - - foreach (var permission in forAppSchemas) + foreach (var permission in forAppWithoutSchemas) + { + if (permission.Length > PermissionIds.App.Length + 1) { var trimmed = permission[(PermissionIds.App.Length + 1)..]; - foreach (var schema in schemaNames) + if (trimmed.Length > 0) { - var replaced = trimmed.Replace("{schema}", schema, StringComparison.Ordinal); - - result.Add(replaced); + result.Add(trimmed); } } - - return result; } - private async Task> GetSchemaNamesAsync(IAppEntity app) + foreach (var permission in forAppSchemas) { - var schemas = await appProvider.GetSchemasAsync(app.Id); + var trimmed = permission[(PermissionIds.App.Length + 1)..]; - var schemaNames = new List + foreach (var schema in schemaNames) { - Permission.Any - }; - - schemaNames.AddRange(schemas.Select(x => x.SchemaDef.Name)); + var replaced = trimmed.Replace("{schema}", schema, StringComparison.Ordinal); - return schemaNames; + result.Add(replaced); + } } + + return result; + } + + private async Task> GetSchemaNamesAsync(IAppEntity app) + { + var schemas = await appProvider.GetSchemasAsync(app.Id); + + var schemaNames = new List + { + Permission.Any + }; + + schemaNames.AddRange(schemas.Select(x => x.SchemaDef.Name)); + + return schemaNames; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/StringLogger.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/StringLogger.cs index 4d40cf239e..2ca3c71360 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/StringLogger.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/StringLogger.cs @@ -10,109 +10,108 @@ using Squidex.Infrastructure; using Squidex.Log; -namespace Squidex.Domain.Apps.Entities.Apps.Templates -{ - public sealed class StringLogger : ILogger, ILogLine - { - private const int MaxActionLength = 40; - private readonly List lines = new List(); - private readonly List errors = new List(); - private string startedLine = string.Empty; +namespace Squidex.Domain.Apps.Entities.Apps.Templates; - public bool CanWriteToSameLine => false; +public sealed class StringLogger : ILogger, ILogLine +{ + private const int MaxActionLength = 40; + private readonly List lines = new List(); + private readonly List errors = new List(); + private string startedLine = string.Empty; - public void Flush(ISemanticLog log, string template) - { - var mesage = string.Join('\n', lines); + public bool CanWriteToSameLine => false; - log.LogInformation(w => w - .WriteProperty("message", $"CLI executed or template {template}.") - .WriteProperty("template", template) - .WriteArray("steps", a => - { - foreach (var line in lines) - { - a.WriteValue(line); - } - })); + public void Flush(ISemanticLog log, string template) + { + var mesage = string.Join('\n', lines); - if (errors.Count > 0) + log.LogInformation(w => w + .WriteProperty("message", $"CLI executed or template {template}.") + .WriteProperty("template", template) + .WriteArray("steps", a => { - throw new DomainException($"Template failed with {errors[0]}"); - } - } + foreach (var line in lines) + { + a.WriteValue(line); + } + })); - public void Dispose() + if (errors.Count > 0) { + throw new DomainException($"Template failed with {errors[0]}"); } + } - public void StepStart(string message) - { - if (message.Length > MaxActionLength - 3) - { - var length = MaxActionLength - 3; + public void Dispose() + { + } - message = message[..length]; - } + public void StepStart(string message) + { + if (message.Length > MaxActionLength - 3) + { + var length = MaxActionLength - 3; - startedLine = $"{message.PadRight(MaxActionLength, '.')}..."; + message = message[..length]; } - public void StepSuccess(string? details = null) - { - if (!string.IsNullOrWhiteSpace(details)) - { - AddToLine($"succeeded ({details})."); - } - else - { - AddToLine("succeeded"); - } - } + startedLine = $"{message.PadRight(MaxActionLength, '.')}..."; + } - public void StepFailed(string reason) + public void StepSuccess(string? details = null) + { + if (!string.IsNullOrWhiteSpace(details)) { - AddToErrors(reason); - AddToLine($"failed: {reason.TrimEnd('.')}."); + AddToLine($"succeeded ({details})."); } - - public void StepSkipped(string reason) + else { - AddToLine($"skipped: {reason.TrimEnd('.')}."); + AddToLine("succeeded"); } + } - public void WriteLine() - { - lines.Add(string.Empty); - } + public void StepFailed(string reason) + { + AddToErrors(reason); + AddToLine($"failed: {reason.TrimEnd('.')}."); + } - public void WriteLine(string message) - { - lines.Add(message); - } + public void StepSkipped(string reason) + { + AddToLine($"skipped: {reason.TrimEnd('.')}."); + } - public void WriteLine(string message, params object?[] args) - { - lines.Add(string.Format(CultureInfo.InvariantCulture, message, args)); - } + public void WriteLine() + { + lines.Add(string.Empty); + } - private void AddToErrors(string reason) - { - errors.Add(reason); - } + public void WriteLine(string message) + { + lines.Add(message); + } - private void AddToLine(string message) - { - startedLine += message; + public void WriteLine(string message, params object?[] args) + { + lines.Add(string.Format(CultureInfo.InvariantCulture, message, args)); + } - lines.Add(startedLine); + private void AddToErrors(string reason) + { + errors.Add(reason); + } - startedLine = string.Empty; - } + private void AddToLine(string message) + { + startedLine += message; - public ILogLine WriteSameLine() - { - return this; - } + lines.Add(startedLine); + + startedLine = string.Empty; + } + + public ILogLine WriteSameLine() + { + return this; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Template.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Template.cs index 1e08c8b107..b46740951b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Template.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Template.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Apps.Templates -{ - public sealed record Template(string Name, string Title, string Description, bool IsStarter); -} +namespace Squidex.Domain.Apps.Entities.Apps.Templates; + +public sealed record Template(string Name, string Title, string Description, bool IsStarter); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs index 200a4f8773..95aed0701d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs @@ -24,114 +24,113 @@ using Squidex.Infrastructure.Commands; using Squidex.Log; -namespace Squidex.Domain.Apps.Entities.Apps.Templates +namespace Squidex.Domain.Apps.Entities.Apps.Templates; + +public sealed class TemplateCommandMiddleware : ICommandMiddleware { - public sealed class TemplateCommandMiddleware : ICommandMiddleware + private readonly TemplatesClient templatesClient; + private readonly TemplatesOptions templateOptions; + private readonly IUrlGenerator urlGenerator; + private readonly ISemanticLog log; + + public TemplateCommandMiddleware(TemplatesClient templatesClient, IOptions templateOptions, IUrlGenerator urlGenerator, + ISemanticLog log) { - private readonly TemplatesClient templatesClient; - private readonly TemplatesOptions templateOptions; - private readonly IUrlGenerator urlGenerator; - private readonly ISemanticLog log; + this.templatesClient = templatesClient; + this.templateOptions = templateOptions.Value; + this.urlGenerator = urlGenerator; - public TemplateCommandMiddleware(TemplatesClient templatesClient, IOptions templateOptions, IUrlGenerator urlGenerator, - ISemanticLog log) - { - this.templatesClient = templatesClient; - this.templateOptions = templateOptions.Value; - this.urlGenerator = urlGenerator; + this.log = log; + } - this.log = log; + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + await next(context, ct); + + if (context.IsCompleted && context.Command is CreateApp createApp) + { + await ApplyTemplateAsync(context.Result(), createApp.Template); } + } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + private async Task ApplyTemplateAsync(IAppEntity app, string? template) + { + if (string.IsNullOrWhiteSpace(template)) { - await next(context, ct); + return; + } - if (context.IsCompleted && context.Command is CreateApp createApp) - { - await ApplyTemplateAsync(context.Result(), createApp.Template); - } + var repository = await templatesClient.GetRepositoryUrl(template); + + if (string.IsNullOrEmpty(repository)) + { + log.LogWarning(w => w + .WriteProperty("message", "Template not found.") + .WriteProperty("template", template)); + return; } - private async Task ApplyTemplateAsync(IAppEntity app, string? template) + using (var cliLog = new StringLogger()) { - if (string.IsNullOrWhiteSpace(template)) + try { - return; - } - - var repository = await templatesClient.GetRepositoryUrl(template); + var session = CreateSession(app); - if (string.IsNullOrEmpty(repository)) - { - log.LogWarning(w => w - .WriteProperty("message", "Template not found.") - .WriteProperty("template", template)); - return; - } + var syncService = await CreateSyncServiceAsync(repository, session); + var syncOptions = new SyncOptions(); - using (var cliLog = new StringLogger()) - { - try + var targets = new ISynchronizer[] { - var session = CreateSession(app); - - var syncService = await CreateSyncServiceAsync(repository, session); - var syncOptions = new SyncOptions(); - - var targets = new ISynchronizer[] - { - new AppSynchronizer(cliLog), - new AssetFoldersSynchronizer(cliLog), - new AssetsSynchronizer(cliLog), - new RulesSynchronizer(cliLog), - new SchemasSynchronizer(cliLog), - new WorkflowsSynchronizer(cliLog), - new ContentsSynchronizer(cliLog) - }; - - foreach (var target in targets) - { - await target.ImportAsync(syncService, syncOptions, session); - } - } - finally + new AppSynchronizer(cliLog), + new AssetFoldersSynchronizer(cliLog), + new AssetsSynchronizer(cliLog), + new RulesSynchronizer(cliLog), + new SchemasSynchronizer(cliLog), + new WorkflowsSynchronizer(cliLog), + new ContentsSynchronizer(cliLog) + }; + + foreach (var target in targets) { - cliLog.Flush(log, template); + await target.ImportAsync(syncService, syncOptions, session); } } + finally + { + cliLog.Flush(log, template); + } } + } - private static async Task CreateSyncServiceAsync(string repository, ISession session) - { - var fs = await FileSystems.CreateAsync(repository, session.WorkingDirectory); - - return new SyncService(fs, session); - } + private static async Task CreateSyncServiceAsync(string repository, ISession session) + { + var fs = await FileSystems.CreateAsync(repository, session.WorkingDirectory); - private ISession CreateSession(IAppEntity app) - { - var client = app.Clients.First(); + return new SyncService(fs, session); + } - var url = templateOptions.LocalUrl; + private ISession CreateSession(IAppEntity app) + { + var client = app.Clients.First(); - if (string.IsNullOrEmpty(url)) - { - url = urlGenerator.Root(); - } + var url = templateOptions.LocalUrl; - return new Session( - app.Name, - new DirectoryInfo(Path.GetTempPath()), - new SquidexClientManager(new SquidexOptions - { - Configurator = AcceptAllCertificatesConfigurator.Instance, - AppName = app.Name, - ClientId = $"{app.Name}:{client.Key}", - ClientSecret = client.Value.Secret, - Url = url - })); + if (string.IsNullOrEmpty(url)) + { + url = urlGenerator.Root(); } + + return new Session( + app.Name, + new DirectoryInfo(Path.GetTempPath()), + new SquidexClientManager(new SquidexOptions + { + Configurator = AcceptAllCertificatesConfigurator.Instance, + AppName = app.Name, + ClientId = $"{app.Name}:{client.Key}", + ClientSecret = client.Value.Secret, + Url = url + })); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateRepository.cs index e3baecc257..8442aa0f8b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateRepository.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Templates +namespace Squidex.Domain.Apps.Entities.Apps.Templates; + +public sealed class TemplateRepository { - public sealed class TemplateRepository - { - public string ContentUrl { get; set; } + public string ContentUrl { get; set; } - public string GitUrl { get; set; } - } + public string GitUrl { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs index bc78677466..6becd489b6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs @@ -9,99 +9,98 @@ using Microsoft.Extensions.Options; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Apps.Templates +namespace Squidex.Domain.Apps.Entities.Apps.Templates; + +public sealed class TemplatesClient { - public sealed class TemplatesClient - { - private static readonly Regex Regex = new Regex("\\* \\[(?.*)\\]\\((?<Name>.*)\\/README\\.md\\): (?<Description>.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - private readonly IHttpClientFactory httpClientFactory; - private readonly TemplatesOptions options; + private static readonly Regex Regex = new Regex("\\* \\[(?<Title>.*)\\]\\((?<Name>.*)\\/README\\.md\\): (?<Description>.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private readonly IHttpClientFactory httpClientFactory; + private readonly TemplatesOptions options; - public TemplatesClient(IHttpClientFactory httpClientFactory, IOptions<TemplatesOptions> options) - { - this.httpClientFactory = httpClientFactory; + public TemplatesClient(IHttpClientFactory httpClientFactory, IOptions<TemplatesOptions> options) + { + this.httpClientFactory = httpClientFactory; - this.options = options.Value; - } + this.options = options.Value; + } - public async Task<string?> GetRepositoryUrl(string name, - CancellationToken ct = default) + public async Task<string?> GetRepositoryUrl(string name, + CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) { - using (var httpClient = httpClientFactory.CreateClient()) + var result = new List<Template>(); + + foreach (var repository in options.Repositories.OrEmpty()) { - var result = new List<Template>(); + var url = $"{repository.ContentUrl}/README.md"; - foreach (var repository in options.Repositories.OrEmpty()) - { - var url = $"{repository.ContentUrl}/README.md"; + var text = await httpClient.GetStringAsync(url, ct); - var text = await httpClient.GetStringAsync(url, ct); + foreach (Match match in Regex.Matches(text)) + { + var currentName = match.Groups["Name"].Value; - foreach (Match match in Regex.Matches(text)) + if (currentName == name) { - var currentName = match.Groups["Name"].Value; - - if (currentName == name) - { - return $"{repository.GitUrl ?? repository.ContentUrl}?folder={name}"; - } + return $"{repository.GitUrl ?? repository.ContentUrl}?folder={name}"; } } - - return null; } + + return null; } + } - public async Task<List<Template>> GetTemplatesAsync( - CancellationToken ct = default) + public async Task<List<Template>> GetTemplatesAsync( + CancellationToken ct = default) + { + using (var httpClient = httpClientFactory.CreateClient()) { - using (var httpClient = httpClientFactory.CreateClient()) - { - var result = new List<Template>(); + var result = new List<Template>(); - foreach (var repository in options.Repositories.OrEmpty()) - { - var url = $"{repository.ContentUrl}/README.md"; + foreach (var repository in options.Repositories.OrEmpty()) + { + var url = $"{repository.ContentUrl}/README.md"; - var text = await httpClient.GetStringAsync(url, ct); + var text = await httpClient.GetStringAsync(url, ct); - foreach (Match match in Regex.Matches(text)) - { - var title = match.Groups["Title"].Value; + foreach (Match match in Regex.Matches(text)) + { + var title = match.Groups["Title"].Value; - result.Add(new Template( - match.Groups["Name"].Value, - title, - match.Groups["Description"].Value, - title.StartsWith("Starter ", StringComparison.OrdinalIgnoreCase))); - } + result.Add(new Template( + match.Groups["Name"].Value, + title, + match.Groups["Description"].Value, + title.StartsWith("Starter ", StringComparison.OrdinalIgnoreCase))); } - - return result; } + + return result; } + } - public async Task<string?> GetDetailAsync(string name, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(name); + public async Task<string?> GetDetailAsync(string name, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(name); - using (var httpClient = httpClientFactory.CreateClient()) + using (var httpClient = httpClientFactory.CreateClient()) + { + foreach (var repository in options.Repositories.OrEmpty()) { - foreach (var repository in options.Repositories.OrEmpty()) - { - var url = $"{repository.ContentUrl}/{name}/README.md"; + var url = $"{repository.ContentUrl}/{name}/README.md"; - var response = await httpClient.GetAsync(url, ct); + var response = await httpClient.GetAsync(url, ct); - if (response.IsSuccessStatusCode) - { - return await response.Content.ReadAsStringAsync(ct); - } + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadAsStringAsync(ct); } } - - return null; } + + return null; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesOptions.cs index cb376ff59b..83abc86a72 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesOptions.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Apps.Templates +namespace Squidex.Domain.Apps.Entities.Apps.Templates; + +public sealed class TemplatesOptions { - public sealed class TemplatesOptions - { - public string? LocalUrl { get; set; } + public string? LocalUrl { get; set; } - public TemplateRepository[] Repositories { get; set; } - } + public TemplateRepository[] Repositories { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCache.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCache.cs index 1abde32b9a..ee347bc7d8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCache.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCache.cs @@ -10,13 +10,12 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetCache : QueryCache<DomainId, IEnrichedAssetEntity>, IAssetCache { - public sealed class AssetCache : QueryCache<DomainId, IEnrichedAssetEntity>, IAssetCache + public AssetCache(IMemoryCache? memoryCache, IOptions<AssetOptions> options) + : base(options.Value.CanCache ? memoryCache : null) { - public AssetCache(IMemoryCache? memoryCache, IOptions<AssetOptions> options) - : base(options.Value.CanCache ? memoryCache : null) - { - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index 7ffc9dbb5c..3ad9bf3402 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -17,126 +17,125 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetChangedTriggerHandler : IRuleTriggerHandler, ISubscriptionEventCreator { - public sealed class AssetChangedTriggerHandler : IRuleTriggerHandler, ISubscriptionEventCreator - { - private readonly IScriptEngine scriptEngine; - private readonly IAssetLoader assetLoader; - private readonly IAssetRepository assetRepository; + private readonly IScriptEngine scriptEngine; + private readonly IAssetLoader assetLoader; + private readonly IAssetRepository assetRepository; - public bool CanCreateSnapshotEvents => true; + public bool CanCreateSnapshotEvents => true; - public Type TriggerType => typeof(AssetChangedTriggerV2); + public Type TriggerType => typeof(AssetChangedTriggerV2); - public AssetChangedTriggerHandler( - IScriptEngine scriptEngine, - IAssetLoader assetLoader, - IAssetRepository assetRepository) - { - this.scriptEngine = scriptEngine; - this.assetLoader = assetLoader; - this.assetRepository = assetRepository; - } + public AssetChangedTriggerHandler( + IScriptEngine scriptEngine, + IAssetLoader assetLoader, + IAssetRepository assetRepository) + { + this.scriptEngine = scriptEngine; + this.assetLoader = assetLoader; + this.assetRepository = assetRepository; + } - public bool Handles(AppEvent @event) - { - return @event is AssetEvent and not AssetMoved; - } + public bool Handles(AppEvent @event) + { + return @event is AssetEvent and not AssetMoved; + } - public async IAsyncEnumerable<EnrichedEvent> CreateSnapshotEventsAsync(RuleContext context, - [EnumeratorCancellation] CancellationToken ct) + public async IAsyncEnumerable<EnrichedEvent> CreateSnapshotEventsAsync(RuleContext context, + [EnumeratorCancellation] CancellationToken ct) + { + await foreach (var asset in assetRepository.StreamAll(context.AppId.Id, ct)) { - await foreach (var asset in assetRepository.StreamAll(context.AppId.Id, ct)) + var result = new EnrichedAssetEvent { - var result = new EnrichedAssetEvent - { - Type = EnrichedAssetEventType.Created - }; - - SimpleMapper.Map(asset, result); - - result.Actor = asset.LastModifiedBy; - result.PixelHeight = asset.Metadata?.GetPixelHeight(); - result.PixelWidth = asset.Metadata?.GetPixelWidth(); - result.Name = "AssetQueried"; + Type = EnrichedAssetEventType.Created + }; - yield return result; - } - } + SimpleMapper.Map(asset, result); - public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct) - { - yield return await CreateEnrichedEventsCoreAsync(@event, ct); - } + result.Actor = asset.LastModifiedBy; + result.PixelHeight = asset.Metadata?.GetPixelHeight(); + result.PixelWidth = asset.Metadata?.GetPixelWidth(); + result.Name = "AssetQueried"; - public async ValueTask<EnrichedEvent?> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, - CancellationToken ct) - { - return await CreateEnrichedEventsCoreAsync(@event, ct); + yield return result; } + } - private async ValueTask<EnrichedEvent> CreateEnrichedEventsCoreAsync(Envelope<AppEvent> @event, - CancellationToken ct) - { - var assetEvent = (AssetEvent)@event.Payload; + public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, + [EnumeratorCancellation] CancellationToken ct) + { + yield return await CreateEnrichedEventsCoreAsync(@event, ct); + } - var result = new EnrichedAssetEvent(); + public async ValueTask<EnrichedEvent?> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, + CancellationToken ct) + { + return await CreateEnrichedEventsCoreAsync(@event, ct); + } - var asset = await assetLoader.GetAsync( - assetEvent.AppId.Id, - assetEvent.AssetId, - @event.Headers.EventStreamNumber(), - ct); + private async ValueTask<EnrichedEvent> CreateEnrichedEventsCoreAsync(Envelope<AppEvent> @event, + CancellationToken ct) + { + var assetEvent = (AssetEvent)@event.Payload; - if (asset != null) - { - SimpleMapper.Map(asset, result); + var result = new EnrichedAssetEvent(); - result.PixelHeight = asset.Metadata?.GetPixelHeight(); - result.PixelWidth = asset.Metadata?.GetPixelWidth(); - result.AssetType = asset.Type; - } + var asset = await assetLoader.GetAsync( + assetEvent.AppId.Id, + assetEvent.AssetId, + @event.Headers.EventStreamNumber(), + ct); - // Use the concrete event to map properties that are not part of app event. - SimpleMapper.Map(assetEvent, result); + if (asset != null) + { + SimpleMapper.Map(asset, result); - switch (@event.Payload) - { - case AssetCreated: - result.Type = EnrichedAssetEventType.Created; - break; - case AssetAnnotated: - result.Type = EnrichedAssetEventType.Annotated; - break; - case AssetUpdated: - result.Type = EnrichedAssetEventType.Updated; - break; - case AssetDeleted: - result.Type = EnrichedAssetEventType.Deleted; - break; - } - - return result; + result.PixelHeight = asset.Metadata?.GetPixelHeight(); + result.PixelWidth = asset.Metadata?.GetPixelWidth(); + result.AssetType = asset.Type; } - public bool Trigger(EnrichedEvent @event, RuleContext context) + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map(assetEvent, result); + + switch (@event.Payload) { - var trigger = (AssetChangedTriggerV2)context.Rule.Trigger; + case AssetCreated: + result.Type = EnrichedAssetEventType.Created; + break; + case AssetAnnotated: + result.Type = EnrichedAssetEventType.Annotated; + break; + case AssetUpdated: + result.Type = EnrichedAssetEventType.Updated; + break; + case AssetDeleted: + result.Type = EnrichedAssetEventType.Deleted; + break; + } - if (string.IsNullOrWhiteSpace(trigger.Condition)) - { - return true; - } + return result; + } - // Script vars are just wrappers over dictionaries for better performance. - var vars = new EventScriptVars - { - Event = @event - }; + public bool Trigger(EnrichedEvent @event, RuleContext context) + { + var trigger = (AssetChangedTriggerV2)context.Rule.Trigger; - return scriptEngine.Evaluate(vars, trigger.Condition); + if (string.IsNullOrWhiteSpace(trigger.Condition)) + { + return true; } + + // Script vars are just wrappers over dictionaries for better performance. + var vars = new EventScriptVars + { + Event = @event + }; + + return scriptEngine.Evaluate(vars, trigger.Condition); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupProcess.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupProcess.cs index bedd18dd90..ad2eba42bd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupProcess.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupProcess.cs @@ -9,36 +9,35 @@ using Squidex.Infrastructure.Timers; using tusdotnet.Interfaces; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetCleanupProcess : IBackgroundProcess { - public sealed class AssetCleanupProcess : IBackgroundProcess + private readonly ITusExpirationStore expirationStore; + private CompletionTimer timer; + + public AssetCleanupProcess(ITusExpirationStore expirationStore) + { + this.expirationStore = expirationStore; + } + + public Task StartAsync( + CancellationToken ct) + { + timer = new CompletionTimer((int)TimeSpan.FromMinutes(10).TotalMilliseconds, CleanupAsync); + + return Task.CompletedTask; + } + + public Task StopAsync( + CancellationToken ct) + { + return timer?.StopAsync() ?? Task.CompletedTask; + } + + public Task CleanupAsync( + CancellationToken ct) { - private readonly ITusExpirationStore expirationStore; - private CompletionTimer timer; - - public AssetCleanupProcess(ITusExpirationStore expirationStore) - { - this.expirationStore = expirationStore; - } - - public Task StartAsync( - CancellationToken ct) - { - timer = new CompletionTimer((int)TimeSpan.FromMinutes(10).TotalMilliseconds, CleanupAsync); - - return Task.CompletedTask; - } - - public Task StopAsync( - CancellationToken ct) - { - return timer?.StopAsync() ?? Task.CompletedTask; - } - - public Task CleanupAsync( - CancellationToken ct) - { - return expirationStore.RemoveExpiredFilesAsync(ct); - } + return expirationStore.RemoveExpiredFilesAsync(ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDuplicate.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDuplicate.cs index 93821c5259..90c4e62bee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDuplicate.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDuplicate.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed record AssetDuplicate(IEnrichedAssetEntity Asset); -} +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed record AssetDuplicate(IEnrichedAssetEntity Asset); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs index e0e751d9d4..895af65b3e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs @@ -9,62 +9,61 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetEntity : IEnrichedAssetEntity { - public sealed class AssetEntity : IEnrichedAssetEntity - { - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public Instant Created { get; set; } + public Instant Created { get; set; } - public Instant LastModified { get; set; } + public Instant LastModified { get; set; } - public RefToken CreatedBy { get; set; } + public RefToken CreatedBy { get; set; } - public RefToken LastModifiedBy { get; set; } + public RefToken LastModifiedBy { get; set; } - public HashSet<string> Tags { get; set; } + public HashSet<string> Tags { get; set; } - public HashSet<string> TagNames { get; set; } + public HashSet<string> TagNames { get; set; } - public long Version { get; set; } + public long Version { get; set; } - public string FileName { get; set; } + public string FileName { get; set; } - public string FileHash { get; set; } + public string FileHash { get; set; } - public string MimeType { get; set; } + public string MimeType { get; set; } - public string Slug { get; set; } + public string Slug { get; set; } - public string MetadataText { get; set; } + public string MetadataText { get; set; } - public string? EditToken { get; set; } + public string? EditToken { get; set; } - public long FileSize { get; set; } + public long FileSize { get; set; } - public long FileVersion { get; set; } + public long FileVersion { get; set; } - public bool IsProtected { get; set; } + public bool IsProtected { get; set; } - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - public AssetMetadata Metadata { get; set; } + public AssetMetadata Metadata { get; set; } - public AssetType Type { get; set; } + public AssetType Type { get; set; } - public DomainId AssetId - { - get => Id; - } + public DomainId AssetId + { + get => Id; + } - public DomainId UniqueId - { - get => DomainId.Combine(AppId, Id); - } + public DomainId UniqueId + { + get => DomainId.Combine(AppId, Id); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs index 44feae2099..e7798fdd38 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public static class AssetExtensions { - public static class AssetExtensions - { - private const string HeaderNoEnrichment = "X-NoAssetEnrichment"; + private const string HeaderNoEnrichment = "X-NoAssetEnrichment"; - public static bool ShouldSkipAssetEnrichment(this Context context) - { - return context.Headers.ContainsKey(HeaderNoEnrichment); - } + public static bool ShouldSkipAssetEnrichment(this Context context) + { + return context.Headers.ContainsKey(HeaderNoEnrichment); + } - public static ICloneBuilder WithoutAssetEnrichment(this ICloneBuilder builder, bool value = true) - { - return builder.WithBoolean(HeaderNoEnrichment, value); - } + public static ICloneBuilder WithoutAssetEnrichment(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderNoEnrichment, value); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs index cc2d111ebf..19bb2d4a1f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs @@ -10,35 +10,34 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetHistoryEventsCreator : HistoryEventsCreatorBase { - public sealed class AssetHistoryEventsCreator : HistoryEventsCreatorBase + public AssetHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) { - public AssetHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage<AssetCreated>( - "history.assets.uploaded"); + AddEventMessage<AssetCreated>( + "history.assets.uploaded"); - AddEventMessage<AssetUpdated>( - "history.assets.replaced"); + AddEventMessage<AssetUpdated>( + "history.assets.replaced"); - AddEventMessage<AssetAnnotated>( - "history.assets.updated"); - } - - protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event) - { - HistoryEvent? result = null; + AddEventMessage<AssetAnnotated>( + "history.assets.updated"); + } - if (@event.Payload is AssetEvent assetEvent) - { - var channel = $"assets.{assetEvent.AssetId}"; + protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event) + { + HistoryEvent? result = null; - result = ForEvent(@event.Payload, channel); - } + if (@event.Payload is AssetEvent assetEvent) + { + var channel = $"assets.{assetEvent.AssetId}"; - return Task.FromResult(result); + result = ForEvent(@event.Payload, channel); } + + return Task.FromResult(result); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs index 9dd5601dc8..0d1848caf7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs @@ -5,24 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetOptions { - public sealed class AssetOptions - { - public bool FolderPerApp { get; set; } = false; + public bool FolderPerApp { get; set; } = false; - public bool CanCache { get; set; } + public bool CanCache { get; set; } - public int DefaultPageSize { get; set; } = 200; + public int DefaultPageSize { get; set; } = 200; - public int MaxResults { get; set; } = 200; + public int MaxResults { get; set; } = 200; - public long MaxSize { get; set; } = 5 * 1024 * 1024; + public long MaxSize { get; set; } = 5 * 1024 * 1024; - public string? CDN { get; set; } + public string? CDN { get; set; } - public TimeSpan TimeoutFind { get; set; } = TimeSpan.FromSeconds(1); + public TimeSpan TimeoutFind { get; set; } = TimeSpan.FromSeconds(1); - public TimeSpan TimeoutQuery { get; set; } = TimeSpan.FromSeconds(5); - } + public TimeSpan TimeoutQuery { get; set; } = TimeSpan.FromSeconds(5); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs index 38ffe979a3..1343d54e36 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs @@ -10,56 +10,55 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetPermanentDeleter : IEventConsumer { - public sealed class AssetPermanentDeleter : IEventConsumer + private readonly IAssetFileStore assetFileStore; + private readonly HashSet<string> consumingTypes; + + public string Name { - private readonly IAssetFileStore assetFileStore; - private readonly HashSet<string> consumingTypes; + get => GetType().Name; + } - public string Name - { - get => GetType().Name; - } + public string EventsFilter + { + get => "^asset-"; + } - public string EventsFilter - { - get => "^asset-"; - } + public AssetPermanentDeleter(IAssetFileStore assetFileStore, TypeNameRegistry typeNameRegistry) + { + this.assetFileStore = assetFileStore; - public AssetPermanentDeleter(IAssetFileStore assetFileStore, TypeNameRegistry typeNameRegistry) + // Compute the event types names once for performance reasons and use hashset for extensibility. + consumingTypes = new HashSet<string> { - this.assetFileStore = assetFileStore; + typeNameRegistry.GetName<AssetDeleted>() + }; + } - // Compute the event types names once for performance reasons and use hashset for extensibility. - consumingTypes = new HashSet<string> - { - typeNameRegistry.GetName<AssetDeleted>() - }; - } + public bool Handles(StoredEvent @event) + { + return consumingTypes.Contains(@event.Data.Type); + } - public bool Handles(StoredEvent @event) + public async Task On(Envelope<IEvent> @event) + { + if (@event.Headers.Restored()) { - return consumingTypes.Contains(@event.Data.Type); + return; } - public async Task On(Envelope<IEvent> @event) + if (@event.Payload is AssetDeleted assetDeleted) { - if (@event.Headers.Restored()) + try { - return; + await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId); } - - if (@event.Payload is AssetDeleted assetDeleted) + catch (AssetNotFoundException) { - try - { - await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId); - } - catch (AssetNotFoundException) - { - return; - } + return; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs index 340166e033..40d68a8e39 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs @@ -7,15 +7,14 @@ using Squidex.Text; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public static class AssetSlug { - public static class AssetSlug - { - private static readonly HashSet<char> Dot = new HashSet<char>(new[] { '.' }); + private static readonly HashSet<char> Dot = new HashSet<char>(new[] { '.' }); - public static string ToAssetSlug(this string value) - { - return value.Slugify(Dot); - } + public static string ToAssetSlug(this string value) + { + return value.Slugify(Dot); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs index 7a414baa61..dd4537efb7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Assets -{ - public sealed record AssetStats(DateTime Date, long TotalCount, long TotalSize); -} +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed record AssetStats(DateTime Date, long TotalCount, long TotalSize); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs index e7d1b0d631..2fd13f0caa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs @@ -8,21 +8,20 @@ using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Apps; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetTagsDeleter : IDeleter { - public sealed class AssetTagsDeleter : IDeleter - { - private readonly ITagService tagService; + private readonly ITagService tagService; - public AssetTagsDeleter(ITagService tagService) - { - this.tagService = tagService; - } + public AssetTagsDeleter(ITagService tagService) + { + this.tagService = tagService; + } - public Task DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return tagService.ClearAsync(app.Id, TagGroups.Assets, ct); - } + public Task DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return tagService.ClearAsync(app.Id, TagGroups.Assets, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs index dda14f7c2f..0d88e2bb08 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs @@ -12,36 +12,35 @@ #pragma warning disable CS0649 -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public partial class AssetUsageTracker : IDeleter { - public partial class AssetUsageTracker : IDeleter + private readonly IAssetLoader assetLoader; + private readonly ISnapshotStore<State> store; + private readonly ITagService tagService; + private readonly IUsageGate usageGate; + + [CollectionName("Index_TagHistory")] + public sealed class State + { + public HashSet<string>? Tags { get; set; } + } + + public AssetUsageTracker(IUsageGate usageGate, IAssetLoader assetLoader, ITagService tagService, + ISnapshotStore<State> store) + { + this.usageGate = usageGate; + this.assetLoader = assetLoader; + this.tagService = tagService; + this.store = store; + + ClearCache(); + } + + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) { - private readonly IAssetLoader assetLoader; - private readonly ISnapshotStore<State> store; - private readonly ITagService tagService; - private readonly IUsageGate usageGate; - - [CollectionName("Index_TagHistory")] - public sealed class State - { - public HashSet<string>? Tags { get; set; } - } - - public AssetUsageTracker(IUsageGate usageGate, IAssetLoader assetLoader, ITagService tagService, - ISnapshotStore<State> store) - { - this.usageGate = usageGate; - this.assetLoader = assetLoader; - this.tagService = tagService; - this.store = store; - - ClearCache(); - } - - Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return usageGate.DeleteAssetUsageAsync(app.Id, ct); - } + return usageGate.DeleteAssetUsageAsync(app.Id, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs index fa9c487d07..4e8629cc03 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs @@ -15,191 +15,190 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public partial class AssetUsageTracker : IEventConsumer { - public partial class AssetUsageTracker : IEventConsumer + private IMemoryCache memoryCache; + + public int BatchSize { - private IMemoryCache memoryCache; + get => 1000; + } - public int BatchSize - { - get => 1000; - } + public int BatchDelay + { + get => 1000; + } - public int BatchDelay - { - get => 1000; - } + public string Name + { + get => GetType().Name; + } - public string Name - { - get => GetType().Name; - } + public string EventsFilter + { + get => "^asset-"; + } - public string EventsFilter - { - get => "^asset-"; - } + private void ClearCache() + { + memoryCache?.Dispose(); + memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + } - private void ClearCache() - { - memoryCache?.Dispose(); - memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - } + public async Task ClearAsync() + { + // Will not remove data, but reset alls counts to zero. + await tagService.ClearAsync(); - public async Task ClearAsync() - { - // Will not remove data, but reset alls counts to zero. - await tagService.ClearAsync(); + // Also clear the store and cache, because otherwise we would use data from the future when querying old tags. + ClearCache(); - // Also clear the store and cache, because otherwise we would use data from the future when querying old tags. - ClearCache(); + await store.ClearAsync(); - await store.ClearAsync(); + await usageGate.DeleteAssetsUsageAsync(); + } - await usageGate.DeleteAssetsUsageAsync(); + public async Task On(IEnumerable<Envelope<IEvent>> events) + { + foreach (var @event in events) + { + // Usage tracking is done in the backgroud, therefore we do no use any batching. + await TrackUsageAsync(@event); } - public async Task On(IEnumerable<Envelope<IEvent>> events) - { - foreach (var @event in events) - { - // Usage tracking is done in the backgroud, therefore we do no use any batching. - await TrackUsageAsync(@event); - } + // Event consumers should only do one task, but too many consumers also hurt performance. + await AddTagsAsync(events); + } - // Event consumers should only do one task, but too many consumers also hurt performance. - await AddTagsAsync(events); - } + private async Task AddTagsAsync(IEnumerable<Envelope<IEvent>> events) + { + var tagsPerApp = new Dictionary<DomainId, Dictionary<string, int>>(); + var tagsPerAsset = new Dictionary<DomainId, State>(); - private async Task AddTagsAsync(IEnumerable<Envelope<IEvent>> events) + void AddTagsToStore(DomainId appId, HashSet<string>? tagIds, int count) { - var tagsPerApp = new Dictionary<DomainId, Dictionary<string, int>>(); - var tagsPerAsset = new Dictionary<DomainId, State>(); - - void AddTagsToStore(DomainId appId, HashSet<string>? tagIds, int count) + if (tagIds != null) { - if (tagIds != null) - { - var perApp = tagsPerApp.GetOrAddNew(appId); + var perApp = tagsPerApp.GetOrAddNew(appId); - foreach (var tag in tagIds) - { - perApp[tag] = perApp.GetValueOrDefault(tag) + count; - } + foreach (var tag in tagIds) + { + perApp[tag] = perApp.GetValueOrDefault(tag) + count; } } + } - void AddTagsToCache(DomainId key, HashSet<string>? tags, long version) - { - var state = new State { Tags = tags }; - - // Write tags to a buffer so that we can write them to a store in batches. - tagsPerAsset[key] = state; + void AddTagsToCache(DomainId key, HashSet<string>? tags, long version) + { + var state = new State { Tags = tags }; - // Write to the cache immediately, to be available for the next event. Use a relatively long cache time for live updates. - memoryCache.Set(key, state, TimeSpan.FromHours(1)); - } + // Write tags to a buffer so that we can write them to a store in batches. + tagsPerAsset[key] = state; - foreach (var @event in events) - { - var typedEvent = (AssetEvent)@event.Payload; + // Write to the cache immediately, to be available for the next event. Use a relatively long cache time for live updates. + memoryCache.Set(key, state, TimeSpan.FromHours(1)); + } - var appId = typedEvent.AppId.Id; - var assetId = typedEvent.AssetId; - var assetKey = @event.Headers.AggregateId(); - var version = @event.Headers.EventStreamNumber(); + foreach (var @event in events) + { + var typedEvent = (AssetEvent)@event.Payload; - switch (typedEvent) - { - case AssetCreated assetCreated: - { - AddTagsToStore(appId, assetCreated.Tags, 1); - AddTagsToCache(assetKey, assetCreated.Tags, version); - break; - } - - case AssetAnnotated assetAnnotated when assetAnnotated.Tags != null: - { - var oldTags = await GetAndUpdateOldTagsAsync(appId, assetId, assetKey, version, default); - - AddTagsToStore(appId, assetAnnotated.Tags, 1); - AddTagsToStore(appId, oldTags, -1); - AddTagsToCache(assetKey, assetAnnotated.Tags, version); - break; - } - - case AssetDeleted assetDeleted: - { - // We need the old tags here for permanent deletions. - var oldTags = - assetDeleted.OldTags ?? - await GetAndUpdateOldTagsAsync(appId, assetId, assetKey, version, default); - - AddTagsToStore(appId, oldTags, -1); - break; - } - } - } + var appId = typedEvent.AppId.Id; + var assetId = typedEvent.AssetId; + var assetKey = @event.Headers.AggregateId(); + var version = @event.Headers.EventStreamNumber(); - // There is no good solution for batching anyway, so there is no need to build a method for that. - foreach (var (appId, updates) in tagsPerApp) + switch (typedEvent) { - await tagService.UpdateAsync(appId, TagGroups.Assets, updates); - } + case AssetCreated assetCreated: + { + AddTagsToStore(appId, assetCreated.Tags, 1); + AddTagsToCache(assetKey, assetCreated.Tags, version); + break; + } - await store.WriteManyAsync(tagsPerAsset.Select(x => new SnapshotWriteJob<State>(x.Key, x.Value, 0))); - } + case AssetAnnotated assetAnnotated when assetAnnotated.Tags != null: + { + var oldTags = await GetAndUpdateOldTagsAsync(appId, assetId, assetKey, version, default); - private async Task<HashSet<string>?> GetAndUpdateOldTagsAsync(DomainId appId, DomainId assetId, DomainId key, long version, - CancellationToken ct) - { - // Store the latest tags in memory for fast access. - if (memoryCache.TryGetValue<State>(key, out var state)) - { - return state.Tags; - } + AddTagsToStore(appId, assetAnnotated.Tags, 1); + AddTagsToStore(appId, oldTags, -1); + AddTagsToCache(assetKey, assetAnnotated.Tags, version); + break; + } - var stored = await store.ReadAsync(key, ct); + case AssetDeleted assetDeleted: + { + // We need the old tags here for permanent deletions. + var oldTags = + assetDeleted.OldTags ?? + await GetAndUpdateOldTagsAsync(appId, assetId, assetKey, version, default); - // Stored state can be null, if not serialized yet. - if (stored.Value != null) - { - return stored.Value.Tags; + AddTagsToStore(appId, oldTags, -1); + break; + } } + } - // Some deleted events (like permanent deletion) have version of zero, but there is not previous event. - if (version == 0) - { - return null; - } + // There is no good solution for batching anyway, so there is no need to build a method for that. + foreach (var (appId, updates) in tagsPerApp) + { + await tagService.UpdateAsync(appId, TagGroups.Assets, updates); + } - // This will replay a lot of events, so it is the slowest alternative. - var previousAsset = await assetLoader.GetAsync(appId, assetId, version - 1, ct); + await store.WriteManyAsync(tagsPerAsset.Select(x => new SnapshotWriteJob<State>(x.Key, x.Value, 0))); + } - return previousAsset?.Tags; + private async Task<HashSet<string>?> GetAndUpdateOldTagsAsync(DomainId appId, DomainId assetId, DomainId key, long version, + CancellationToken ct) + { + // Store the latest tags in memory for fast access. + if (memoryCache.TryGetValue<State>(key, out var state)) + { + return state.Tags; } - private Task TrackUsageAsync(Envelope<IEvent> @event) + var stored = await store.ReadAsync(key, ct); + + // Stored state can be null, if not serialized yet. + if (stored.Value != null) { - switch (@event.Payload) - { - case AssetCreated assetCreated: - return usageGate.TrackAssetAsync(assetCreated.AppId.Id, GetDate(@event), assetCreated.FileSize, 1); + return stored.Value.Tags; + } - case AssetUpdated assetUpdated: - return usageGate.TrackAssetAsync(assetUpdated.AppId.Id, GetDate(@event), assetUpdated.FileSize, 0); + // Some deleted events (like permanent deletion) have version of zero, but there is not previous event. + if (version == 0) + { + return null; + } - case AssetDeleted assetDeleted: - return usageGate.TrackAssetAsync(assetDeleted.AppId.Id, GetDate(@event), -assetDeleted.DeletedSize, -1); - } + // This will replay a lot of events, so it is the slowest alternative. + var previousAsset = await assetLoader.GetAsync(appId, assetId, version - 1, ct); - return Task.CompletedTask; - } + return previousAsset?.Tags; + } - private static DateTime GetDate(Envelope<IEvent> @event) + private Task TrackUsageAsync(Envelope<IEvent> @event) + { + switch (@event.Payload) { - return @event.Headers.Timestamp().ToDateTimeUtc().Date; + case AssetCreated assetCreated: + return usageGate.TrackAssetAsync(assetCreated.AppId.Id, GetDate(@event), assetCreated.FileSize, 1); + + case AssetUpdated assetUpdated: + return usageGate.TrackAssetAsync(assetUpdated.AppId.Id, GetDate(@event), assetUpdated.FileSize, 0); + + case AssetDeleted assetDeleted: + return usageGate.TrackAssetAsync(assetDeleted.AppId.Id, GetDate(@event), -assetDeleted.DeletedSize, -1); } + + return Task.CompletedTask; + } + + private static DateTime GetDate(Envelope<IEvent> @event) + { + return @event.Headers.Timestamp().ToDateTimeUtc().Date; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs index a3cce8b5e6..7290ccd550 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs @@ -18,204 +18,203 @@ using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetsFluidExtension : IFluidExtension { - public sealed class AssetsFluidExtension : IFluidExtension + private static readonly FluidValue ErrorNullAsset = FluidValue.Create(null); + private static readonly FluidValue ErrorNoAsset = new StringValue("NoAsset"); + private static readonly FluidValue ErrorNoImage = new StringValue("NoImage"); + private static readonly FluidValue ErrorTooBig = new StringValue("ErrorTooBig"); + private readonly IServiceProvider serviceProvider; + + private sealed class AssetTag : ArgumentsTag { - private static readonly FluidValue ErrorNullAsset = FluidValue.Create(null); - private static readonly FluidValue ErrorNoAsset = new StringValue("NoAsset"); - private static readonly FluidValue ErrorNoImage = new StringValue("NoImage"); - private static readonly FluidValue ErrorTooBig = new StringValue("ErrorTooBig"); private readonly IServiceProvider serviceProvider; - private sealed class AssetTag : ArgumentsTag + public AssetTag(IServiceProvider serviceProvider) { - private readonly IServiceProvider serviceProvider; - - public AssetTag(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } + this.serviceProvider = serviceProvider; + } - public override async ValueTask<Completion> WriteToAsync(TextWriter writer, - TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) + public override async ValueTask<Completion> WriteToAsync(TextWriter writer, + TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) + { + if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) { - if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) - { - var id = await arguments[1].Expression.EvaluateAsync(context); + var id = await arguments[1].Expression.EvaluateAsync(context); - var asset = await ResolveAssetAsync(serviceProvider, enrichedEvent.AppId.Id, id); + var asset = await ResolveAssetAsync(serviceProvider, enrichedEvent.AppId.Id, id); - if (asset != null) - { - var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); + if (asset != null) + { + var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); - context.SetValue(name, asset); - } + context.SetValue(name, asset); } - - return Completion.Normal; } - } - public AssetsFluidExtension(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; + return Completion.Normal; } + } - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - memberAccessStrategy.Register<IAssetEntity>(); - memberAccessStrategy.Register<IAssetInfo>(); - memberAccessStrategy.Register<IWithId<DomainId>>(); - memberAccessStrategy.Register<IEntity>(); - memberAccessStrategy.Register<IEntityWithCreatedBy>(); - memberAccessStrategy.Register<IEntityWithLastModifiedBy>(); - memberAccessStrategy.Register<IEntityWithVersion>(); - memberAccessStrategy.Register<IEnrichedAssetEntity>(); - - AddAssetFilter(); - AddAssetTextFilter(); - } + public AssetsFluidExtension(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } - private void AddAssetFilter() + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + memberAccessStrategy.Register<IAssetEntity>(); + memberAccessStrategy.Register<IAssetInfo>(); + memberAccessStrategy.Register<IWithId<DomainId>>(); + memberAccessStrategy.Register<IEntity>(); + memberAccessStrategy.Register<IEntityWithCreatedBy>(); + memberAccessStrategy.Register<IEntityWithLastModifiedBy>(); + memberAccessStrategy.Register<IEntityWithVersion>(); + memberAccessStrategy.Register<IEnrichedAssetEntity>(); + + AddAssetFilter(); + AddAssetTextFilter(); + } + + private void AddAssetFilter() + { + TemplateContext.GlobalFilters.AddAsyncFilter("asset", async (input, arguments, context) => { - TemplateContext.GlobalFilters.AddAsyncFilter("asset", async (input, arguments, context) => + if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) { - if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) - { - var asset = await ResolveAssetAsync(serviceProvider, enrichedEvent.AppId.Id, input); - - if (asset == null) - { - return ErrorNullAsset; - } + var asset = await ResolveAssetAsync(serviceProvider, enrichedEvent.AppId.Id, input); - return FluidValue.Create(asset); + if (asset == null) + { + return ErrorNullAsset; } - return ErrorNullAsset; - }); - } + return FluidValue.Create(asset); + } + + return ErrorNullAsset; + }); + } - private void AddAssetTextFilter() + private void AddAssetTextFilter() + { + TemplateContext.GlobalFilters.AddAsyncFilter("assetText", async (input, arguments, context) => { - TemplateContext.GlobalFilters.AddAsyncFilter("assetText", async (input, arguments, context) => + if (input is not ObjectValue objectValue) { - if (input is not ObjectValue objectValue) + return ErrorNoAsset; + } + + async Task<FluidValue> ResolveAssetTextAsync(AssetRef asset) + { + if (asset.FileSize > 256_000) { - return ErrorNoAsset; + return ErrorTooBig; } - async Task<FluidValue> ResolveAssetTextAsync(AssetRef asset) - { - if (asset.FileSize > 256_000) - { - return ErrorTooBig; - } + var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>(); - var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>(); + var encoding = arguments.At(0).ToStringValue()?.ToUpperInvariant(); + var encoded = await asset.GetTextAsync(encoding, assetFileStore, default); - var encoding = arguments.At(0).ToStringValue()?.ToUpperInvariant(); - var encoded = await asset.GetTextAsync(encoding, assetFileStore, default); + return new StringValue(encoded); + } - return new StringValue(encoded); - } + switch (objectValue.ToObjectValue()) + { + case IAssetEntity asset: + return await ResolveAssetTextAsync(asset.ToRef()); - switch (objectValue.ToObjectValue()) - { - case IAssetEntity asset: - return await ResolveAssetTextAsync(asset.ToRef()); + case EnrichedAssetEvent @event: + return await ResolveAssetTextAsync(@event.ToRef()); + } - case EnrichedAssetEvent @event: - return await ResolveAssetTextAsync(@event.ToRef()); - } + return ErrorNoAsset; + }); + TemplateContext.GlobalFilters.AddAsyncFilter("assetBlurHash", async (input, arguments, context) => + { + if (input is not ObjectValue objectValue) + { return ErrorNoAsset; - }); + } - TemplateContext.GlobalFilters.AddAsyncFilter("assetBlurHash", async (input, arguments, context) => + async Task<FluidValue> ResolveAssetHashAsync(AssetRef asset) { - if (input is not ObjectValue objectValue) + if (asset.FileSize > 512_000) { - return ErrorNoAsset; + return ErrorTooBig; } - async Task<FluidValue> ResolveAssetHashAsync(AssetRef asset) + if (asset.Type != AssetType.Image) { - if (asset.FileSize > 512_000) - { - return ErrorTooBig; - } - - if (asset.Type != AssetType.Image) - { - return ErrorNoImage; - } - - var options = new BlurOptions(); - - var arg0 = arguments.At(0); - var arg1 = arguments.At(1); - - if (arg0.Type == FluidValues.Number) - { - options.ComponentX = (int)arg0.ToNumberValue(); - } - - if (arg1.Type == FluidValues.Number) - { - options.ComponentX = (int)arg1.ToNumberValue(); - } + return ErrorNoImage; + } - var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>(); - var assetThumbnailGenerator = serviceProvider.GetRequiredService<IAssetThumbnailGenerator>(); + var options = new BlurOptions(); - var blur = await asset.GetBlurHashAsync(options, assetFileStore, assetThumbnailGenerator, default); + var arg0 = arguments.At(0); + var arg1 = arguments.At(1); - return new StringValue(blur); + if (arg0.Type == FluidValues.Number) + { + options.ComponentX = (int)arg0.ToNumberValue(); } - switch (objectValue.ToObjectValue()) + if (arg1.Type == FluidValues.Number) { - case IAssetEntity asset: - return await ResolveAssetHashAsync(asset.ToRef()); - - case EnrichedAssetEvent @event: - return await ResolveAssetHashAsync(@event.ToRef()); + options.ComponentX = (int)arg1.ToNumberValue(); } - return ErrorNoAsset; - }); - } + var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>(); + var assetThumbnailGenerator = serviceProvider.GetRequiredService<IAssetThumbnailGenerator>(); - public void RegisterLanguageExtensions(FluidParserFactory factory) - { - factory.RegisterTag("asset", new AssetTag(serviceProvider)); - } - - private static async Task<IAssetEntity?> ResolveAssetAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id) - { - var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); + var blur = await asset.GetBlurHashAsync(options, assetFileStore, assetThumbnailGenerator, default); - var app = await appProvider.GetAppAsync(appId); + return new StringValue(blur); + } - if (app == null) + switch (objectValue.ToObjectValue()) { - return null; + case IAssetEntity asset: + return await ResolveAssetHashAsync(asset.ToRef()); + + case EnrichedAssetEvent @event: + return await ResolveAssetHashAsync(@event.ToRef()); } - var domainId = DomainId.Create(id.ToStringValue()); + return ErrorNoAsset; + }); + } - var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>(); + public void RegisterLanguageExtensions(FluidParserFactory factory) + { + factory.RegisterTag("asset", new AssetTag(serviceProvider)); + } - var requestContext = - Context.Admin(app).Clone(b => b - .WithoutTotal()); + private static async Task<IAssetEntity?> ResolveAssetAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id) + { + var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); - var asset = await assetQuery.FindAsync(requestContext, domainId); + var app = await appProvider.GetAppAsync(appId); - return asset; + if (app == null) + { + return null; } + + var domainId = DomainId.Create(id.ToStringValue()); + + var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>(); + + var requestContext = + Context.Admin(app).Clone(b => b + .WithoutTotal()); + + var asset = await assetQuery.FindAsync(requestContext, domainId); + + return asset; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs index 35496edad4..39a7642d21 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs @@ -19,253 +19,252 @@ using Squidex.Domain.Apps.Entities.Properties; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor { - public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor + private delegate void GetAssetsDelegate(JsValue references, Action<JsValue> callback); + private delegate void GetAssetTextDelegate(JsValue asset, Action<JsValue> callback, JsValue? encoding); + private delegate void GetBlurHashDelegate(JsValue asset, Action<JsValue> callback, JsValue? componentX, JsValue? componentY); + private readonly IServiceProvider serviceProvider; + + public AssetsJintExtension(IServiceProvider serviceProvider) { - private delegate void GetAssetsDelegate(JsValue references, Action<JsValue> callback); - private delegate void GetAssetTextDelegate(JsValue asset, Action<JsValue> callback, JsValue? encoding); - private delegate void GetBlurHashDelegate(JsValue asset, Action<JsValue> callback, JsValue? componentX, JsValue? componentY); - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public AssetsJintExtension(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } + public void ExtendAsync(ScriptExecutionContext context) + { + AddAssetText(context); + AddAssetBlurHash(context); + AddAsset(context); + } - public void ExtendAsync(ScriptExecutionContext context) + public void Describe(AddDescription describe, ScriptScope scope) + { + if (!scope.HasFlag(ScriptScope.Async)) { - AddAssetText(context); - AddAssetBlurHash(context); - AddAsset(context); + return; } - public void Describe(AddDescription describe, ScriptScope scope) - { - if (!scope.HasFlag(ScriptScope.Async)) - { - return; - } + describe(JsonType.Function, "getAssets(ids, callback)", + Resources.ScriptingGetAssets); - describe(JsonType.Function, "getAssets(ids, callback)", - Resources.ScriptingGetAssets); + describe(JsonType.Function, "getAsset(ids, callback)", + Resources.ScriptingGetAsset); - describe(JsonType.Function, "getAsset(ids, callback)", - Resources.ScriptingGetAsset); + describe(JsonType.Function, "getAssetText(asset, callback, encoding?)", + Resources.ScriptingGetAssetText); - describe(JsonType.Function, "getAssetText(asset, callback, encoding?)", - Resources.ScriptingGetAssetText); + describe(JsonType.Function, "getAssetBlurHash(asset, callback, x?, y?)", + Resources.ScriptingGetBlurHash); + } - describe(JsonType.Function, "getAssetBlurHash(asset, callback, x?, y?)", - Resources.ScriptingGetBlurHash); + private void AddAsset(ScriptExecutionContext context) + { + if (!context.TryGetValue<DomainId>("appId", out var appId)) + { + return; } - private void AddAsset(ScriptExecutionContext context) + if (!context.TryGetValue<ClaimsPrincipal>("user", out var user)) { - if (!context.TryGetValue<DomainId>("appId", out var appId)) - { - return; - } - - if (!context.TryGetValue<ClaimsPrincipal>("user", out var user)) - { - return; - } + return; + } - var getAssets = new GetAssetsDelegate((references, callback) => - { - GetAssets(context, appId, user, references, callback); - }); + var getAssets = new GetAssetsDelegate((references, callback) => + { + GetAssets(context, appId, user, references, callback); + }); - context.Engine.SetValue("getAsset", getAssets); - context.Engine.SetValue("getAssets", getAssets); - } + context.Engine.SetValue("getAsset", getAssets); + context.Engine.SetValue("getAssets", getAssets); + } - private void AddAssetText(ScriptExecutionContext context) + private void AddAssetText(ScriptExecutionContext context) + { + var action = new GetAssetTextDelegate((references, callback, encoding) => { - var action = new GetAssetTextDelegate((references, callback, encoding) => - { - GetText(context, references, callback, encoding); - }); + GetText(context, references, callback, encoding); + }); - context.Engine.SetValue("getAssetText", action); - } + context.Engine.SetValue("getAssetText", action); + } - private void AddAssetBlurHash(ScriptExecutionContext context) + private void AddAssetBlurHash(ScriptExecutionContext context) + { + var getBlurHash = new GetBlurHashDelegate((input, callback, componentX, componentY) => { - var getBlurHash = new GetBlurHashDelegate((input, callback, componentX, componentY) => - { - GetBlurHash(context, input, callback, componentX, componentY); - }); + GetBlurHash(context, input, callback, componentX, componentY); + }); - context.Engine.SetValue("getAssetBlurHash", getBlurHash); - } + context.Engine.SetValue("getAssetBlurHash", getBlurHash); + } + + private void GetText(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? encoding) + { + Guard.NotNull(callback); - private void GetText(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? encoding) + context.Schedule(async (scheduler, ct) => { - Guard.NotNull(callback); + if (input is not ObjectWrapper objectWrapper) + { + scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset")); + return; + } - context.Schedule(async (scheduler, ct) => + async Task ResolveAssetText(AssetRef asset) { - if (input is not ObjectWrapper objectWrapper) + if (asset.FileSize > 256_000) { - scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset")); + scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorTooBig")); return; } - async Task ResolveAssetText(AssetRef asset) + var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>(); + try { - if (asset.FileSize > 256_000) - { - scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorTooBig")); - return; - } - - var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>(); - try - { - var text = await asset.GetTextAsync(encoding?.ToString(), assetFileStore, ct); - - scheduler.Run(callback, JsValue.FromObject(context.Engine, text)); - } - catch - { - scheduler.Run(callback, JsValue.Null); - } - } + var text = await asset.GetTextAsync(encoding?.ToString(), assetFileStore, ct); - switch (objectWrapper.Target) + scheduler.Run(callback, JsValue.FromObject(context.Engine, text)); + } + catch { - case IAssetEntity asset: - await ResolveAssetText(asset.ToRef()); - break; + scheduler.Run(callback, JsValue.Null); + } + } - case EnrichedAssetEvent e: - await ResolveAssetText(e.ToRef()); - break; + switch (objectWrapper.Target) + { + case IAssetEntity asset: + await ResolveAssetText(asset.ToRef()); + break; - default: - scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset")); - break; - } - }); - } + case EnrichedAssetEvent e: + await ResolveAssetText(e.ToRef()); + break; - private void GetBlurHash(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? componentX, JsValue? componentY) + default: + scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset")); + break; + } + }); + } + + private void GetBlurHash(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? componentX, JsValue? componentY) + { + Guard.NotNull(callback); + + context.Schedule(async (scheduler, ct) => { - Guard.NotNull(callback); + if (input is not ObjectWrapper objectWrapper) + { + scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset")); + return; + } - context.Schedule(async (scheduler, ct) => + async Task ResolveHashAsync(AssetRef asset) { - if (input is not ObjectWrapper objectWrapper) + if (asset.FileSize > 512_000 || asset.Type != AssetType.Image) { - scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset")); + scheduler.Run(callback, JsValue.Null); return; } - async Task ResolveHashAsync(AssetRef asset) + var options = new BlurOptions(); + + if (componentX?.IsNumber() == true) { - if (asset.FileSize > 512_000 || asset.Type != AssetType.Image) - { - scheduler.Run(callback, JsValue.Null); - return; - } - - var options = new BlurOptions(); - - if (componentX?.IsNumber() == true) - { - options.ComponentX = (int)componentX.AsNumber(); - } - - if (componentY?.IsNumber() == true) - { - options.ComponentX = (int)componentX.AsNumber(); - } - - var assetThumbnailGenerator = serviceProvider.GetRequiredService<IAssetThumbnailGenerator>(); - var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>(); - try - { - var hash = await asset.GetBlurHashAsync(options, assetFileStore, assetThumbnailGenerator, ct); - - scheduler.Run(callback, JsValue.FromObject(context.Engine, hash)); - } - catch - { - scheduler.Run(callback, JsValue.Null); - } + options.ComponentX = (int)componentX.AsNumber(); } - switch (objectWrapper.Target) + if (componentY?.IsNumber() == true) { - case IAssetEntity asset: - await ResolveHashAsync(asset.ToRef()); - break; - - case EnrichedAssetEvent @event: - await ResolveHashAsync(@event.ToRef()); - break; - - default: - scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset")); - break; + options.ComponentX = (int)componentX.AsNumber(); } - }); - } - - private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback) - { - Guard.NotNull(callback); - - context.Schedule(async (scheduler, ct) => - { - var ids = references.ToIds(); - if (ids.Count == 0) + var assetThumbnailGenerator = serviceProvider.GetRequiredService<IAssetThumbnailGenerator>(); + var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>(); + try { - var emptyAssets = Array.Empty<IEnrichedAssetEntity>(); + var hash = await asset.GetBlurHashAsync(options, assetFileStore, assetThumbnailGenerator, ct); - scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyAssets)); - return; + scheduler.Run(callback, JsValue.FromObject(context.Engine, hash)); } - - var app = await GetAppAsync(appId, ct); - - if (app == null) + catch { - var emptyAssets = Array.Empty<IEnrichedAssetEntity>(); - - scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyAssets)); - return; + scheduler.Run(callback, JsValue.Null); } + } - var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>(); + switch (objectWrapper.Target) + { + case IAssetEntity asset: + await ResolveHashAsync(asset.ToRef()); + break; - var requestContext = - new Context(user, app).Clone(b => b - .WithoutTotal()); + case EnrichedAssetEvent @event: + await ResolveHashAsync(@event.ToRef()); + break; - var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), ct); + default: + scheduler.Run(callback, JsValue.FromObject(context.Engine, "ErrorNoAsset")); + break; + } + }); + } - scheduler.Run(callback, JsValue.FromObject(context.Engine, assets.ToArray())); - return; - }); - } + private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback) + { + Guard.NotNull(callback); - private async Task<IAppEntity> GetAppAsync(DomainId appId, - CancellationToken ct) + context.Schedule(async (scheduler, ct) => { - var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); + var ids = references.ToIds(); + + if (ids.Count == 0) + { + var emptyAssets = Array.Empty<IEnrichedAssetEntity>(); + + scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyAssets)); + return; + } - var app = await appProvider.GetAppAsync(appId, false, ct); + var app = await GetAppAsync(appId, ct); if (app == null) { - throw new JavaScriptException("App does not exist."); + var emptyAssets = Array.Empty<IEnrichedAssetEntity>(); + + scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyAssets)); + return; } - return app; + var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>(); + + var requestContext = + new Context(user, app).Clone(b => b + .WithoutTotal()); + + var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), ct); + + scheduler.Run(callback, JsValue.FromObject(context.Engine, assets.ToArray())); + return; + }); + } + + private async Task<IAppEntity> GetAppAsync(DomainId appId, + CancellationToken ct) + { + var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); + + var app = await appProvider.GetAppAsync(appId, false, ct); + + if (app == null) + { + throw new JavaScriptException("App does not exist."); } + + return app; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsSearchSource.cs index 3f53acb1d3..f881e94928 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsSearchSource.cs @@ -11,45 +11,44 @@ using Squidex.Infrastructure.Queries; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class AssetsSearchSource : ISearchSource { - public sealed class AssetsSearchSource : ISearchSource + private readonly IAssetQueryService assetQuery; + private readonly IUrlGenerator urlGenerator; + + public AssetsSearchSource(IAssetQueryService assetQuery, IUrlGenerator urlGenerator) { - private readonly IAssetQueryService assetQuery; - private readonly IUrlGenerator urlGenerator; + this.assetQuery = assetQuery; - public AssetsSearchSource(IAssetQueryService assetQuery, IUrlGenerator urlGenerator) - { - this.assetQuery = assetQuery; + this.urlGenerator = urlGenerator; + } - this.urlGenerator = urlGenerator; - } + public async Task<SearchResults> SearchAsync(string query, Context context, + CancellationToken ct) + { + var result = new SearchResults(); - public async Task<SearchResults> SearchAsync(string query, Context context, - CancellationToken ct) + if (context.UserPermissions.Allows(PermissionIds.AppAssetsRead, context.App.Name)) { - var result = new SearchResults(); + var filter = ClrFilter.Contains("fileName", query); - if (context.UserPermissions.Allows(PermissionIds.AppAssetsRead, context.App.Name)) - { - var filter = ClrFilter.Contains("fileName", query); - - var clrQuery = new ClrQuery { Filter = filter, Take = 5 }; + var clrQuery = new ClrQuery { Filter = filter, Take = 5 }; - var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithQuery(clrQuery), ct); + var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithQuery(clrQuery), ct); - if (assets.Count > 0) + if (assets.Count > 0) + { + foreach (var asset in assets) { - foreach (var asset in assets) - { - var url = urlGenerator.AssetsUI(context.App.NamedId(), asset.Id.ToString()); + var url = urlGenerator.AssetsUI(context.App.NamedId(), asset.Id.ToString()); - result.Add(asset.FileName, SearchResultType.Asset, url); - } + result.Add(asset.FileName, SearchResultType.Asset, url); } } - - return result; } + + return result; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index 9864257074..6b198cc40a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -15,197 +15,196 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class BackupAssets : IBackupHandler { - public sealed class BackupAssets : IBackupHandler + private const int BatchSize = 100; + private const string TagsFile = "AssetTags.json"; + private const string TagsAliasFile = "AssetTagsAlias.json"; + private readonly HashSet<DomainId> assetIds = new HashSet<DomainId>(); + private readonly HashSet<DomainId> assetFolderIds = new HashSet<DomainId>(); + private readonly Rebuilder rebuilder; + private readonly IAssetFileStore assetFileStore; + private readonly ITagService tagService; + + public string Name { get; } = "Assets"; + + public BackupAssets(Rebuilder rebuilder, IAssetFileStore assetFileStore, ITagService tagService) + { + this.rebuilder = rebuilder; + this.assetFileStore = assetFileStore; + this.tagService = tagService; + } + + public Task BackupAsync(BackupContext context, + CancellationToken ct) { - private const int BatchSize = 100; - private const string TagsFile = "AssetTags.json"; - private const string TagsAliasFile = "AssetTagsAlias.json"; - private readonly HashSet<DomainId> assetIds = new HashSet<DomainId>(); - private readonly HashSet<DomainId> assetFolderIds = new HashSet<DomainId>(); - private readonly Rebuilder rebuilder; - private readonly IAssetFileStore assetFileStore; - private readonly ITagService tagService; - - public string Name { get; } = "Assets"; - - public BackupAssets(Rebuilder rebuilder, IAssetFileStore assetFileStore, ITagService tagService) + return BackupTagsAsync(context, ct); + } + + public Task BackupEventAsync(Envelope<IEvent> @event, BackupContext context, + CancellationToken ct) + { + switch (@event.Payload) { - this.rebuilder = rebuilder; - this.assetFileStore = assetFileStore; - this.tagService = tagService; + case AssetCreated assetCreated: + return WriteAssetAsync( + assetCreated.AppId.Id, + assetCreated.AssetId, + assetCreated.FileVersion, + context.Writer, + ct); + case AssetUpdated assetUpdated: + return WriteAssetAsync( + assetUpdated.AppId.Id, + assetUpdated.AssetId, + assetUpdated.FileVersion, + context.Writer, + ct); } - public Task BackupAsync(BackupContext context, - CancellationToken ct) + return Task.CompletedTask; + } + + public async Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, + CancellationToken ct) + { + switch (@event.Payload) { - return BackupTagsAsync(context, ct); + case AppCreated: + // Restore the tags first so that the processing of consecutive events have the necessary structure. + await RestoreTagsAsync(context, ct); + break; + case AssetFolderCreated: + assetFolderIds.Add(@event.Headers.AggregateId()); + break; + case AssetCreated assetCreated: + assetIds.Add(@event.Headers.AggregateId()); + + await ReadAssetAsync( + assetCreated.AppId.Id, + assetCreated.AssetId, + assetCreated.FileVersion, + context.Reader, + ct); + break; + case AssetUpdated assetUpdated: + await ReadAssetAsync( + assetUpdated.AppId.Id, + assetUpdated.AssetId, + assetUpdated.FileVersion, + context.Reader, + ct); + break; } - public Task BackupEventAsync(Envelope<IEvent> @event, BackupContext context, - CancellationToken ct) - { - switch (@event.Payload) - { - case AssetCreated assetCreated: - return WriteAssetAsync( - assetCreated.AppId.Id, - assetCreated.AssetId, - assetCreated.FileVersion, - context.Writer, - ct); - case AssetUpdated assetUpdated: - return WriteAssetAsync( - assetUpdated.AppId.Id, - assetUpdated.AssetId, - assetUpdated.FileVersion, - context.Writer, - ct); - } + return true; + } - return Task.CompletedTask; + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) + { + if (assetIds.Count > 0) + { + await rebuilder.InsertManyAsync<AssetDomainObject, AssetDomainObject.State>(assetIds, BatchSize, ct); } - public async Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, - CancellationToken ct) + if (assetFolderIds.Count > 0) { - switch (@event.Payload) - { - case AppCreated: - // Restore the tags first so that the processing of consecutive events have the necessary structure. - await RestoreTagsAsync(context, ct); - break; - case AssetFolderCreated: - assetFolderIds.Add(@event.Headers.AggregateId()); - break; - case AssetCreated assetCreated: - assetIds.Add(@event.Headers.AggregateId()); - - await ReadAssetAsync( - assetCreated.AppId.Id, - assetCreated.AssetId, - assetCreated.FileVersion, - context.Reader, - ct); - break; - case AssetUpdated assetUpdated: - await ReadAssetAsync( - assetUpdated.AppId.Id, - assetUpdated.AssetId, - assetUpdated.FileVersion, - context.Reader, - ct); - break; - } - - return true; + await rebuilder.InsertManyAsync<AssetFolderDomainObject, AssetFolderDomainObject.State>(assetFolderIds, BatchSize, ct); } + } - public async Task RestoreAsync(RestoreContext context, - CancellationToken ct) - { - if (assetIds.Count > 0) - { - await rebuilder.InsertManyAsync<AssetDomainObject, AssetDomainObject.State>(assetIds, BatchSize, ct); - } + private async Task RestoreTagsAsync(RestoreContext context, + CancellationToken ct) + { + var export = new TagsExport(); - if (assetFolderIds.Count > 0) - { - await rebuilder.InsertManyAsync<AssetFolderDomainObject, AssetFolderDomainObject.State>(assetFolderIds, BatchSize, ct); - } + if (await context.Reader.HasFileAsync(TagsFile, ct)) + { + export.Tags = await context.Reader.ReadJsonAsync<Dictionary<string, Tag>>(TagsFile, ct); } - private async Task RestoreTagsAsync(RestoreContext context, - CancellationToken ct) + // For backwards compabibility we store the tags and the aliases in different locations. + if (await context.Reader.HasFileAsync(TagsAliasFile, ct)) { - var export = new TagsExport(); + export.Alias = await context.Reader.ReadJsonAsync<Dictionary<string, string>>(TagsAliasFile, ct); + } - if (await context.Reader.HasFileAsync(TagsFile, ct)) - { - export.Tags = await context.Reader.ReadJsonAsync<Dictionary<string, Tag>>(TagsFile, ct); - } + if (export.Alias == null && export.Tags == null) + { + return; + } - // For backwards compabibility we store the tags and the aliases in different locations. - if (await context.Reader.HasFileAsync(TagsAliasFile, ct)) + if (export.Tags != null) + { + // Import the tags without count, because they will populated later by the event processor. + foreach (var (_, tag) in export.Tags) { - export.Alias = await context.Reader.ReadJsonAsync<Dictionary<string, string>>(TagsAliasFile, ct); + tag.Count = 0; } + } - if (export.Alias == null && export.Tags == null) - { - return; - } + await tagService.RebuildTagsAsync(context.AppId, TagGroups.Assets, export, ct); + } - if (export.Tags != null) - { - // Import the tags without count, because they will populated later by the event processor. - foreach (var (_, tag) in export.Tags) - { - tag.Count = 0; - } - } + private async Task BackupTagsAsync(BackupContext context, + CancellationToken ct) + { + var tags = await tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets, ct); - await tagService.RebuildTagsAsync(context.AppId, TagGroups.Assets, export, ct); + if (tags.Tags != null) + { + // Export the tags with count, even though we do not need it. But in general it makes the code easier. + await context.Writer.WriteJsonAsync(TagsFile, tags.Tags, ct); } - private async Task BackupTagsAsync(BackupContext context, - CancellationToken ct) + if (tags.Alias?.Count > 0) { - var tags = await tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets, ct); - - if (tags.Tags != null) - { - // Export the tags with count, even though we do not need it. But in general it makes the code easier. - await context.Writer.WriteJsonAsync(TagsFile, tags.Tags, ct); - } - - if (tags.Alias?.Count > 0) - { - // For backwards compabibility we store the tags and the aliases in different locations. - await context.Writer.WriteJsonAsync(TagsAliasFile, tags.Alias, ct); - } + // For backwards compabibility we store the tags and the aliases in different locations. + await context.Writer.WriteJsonAsync(TagsAliasFile, tags.Alias, ct); } + } - private async Task WriteAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupWriter writer, - CancellationToken ct) + private async Task WriteAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupWriter writer, + CancellationToken ct) + { + try { - try - { - var fileName = GetName(assetId, fileVersion); + var fileName = GetName(assetId, fileVersion); - await using (var stream = await writer.OpenBlobAsync(fileName, ct)) - { - await assetFileStore.DownloadAsync(appId, assetId, fileVersion, null, stream, default, ct); - } - } - catch (AssetNotFoundException) + await using (var stream = await writer.OpenBlobAsync(fileName, ct)) { - return; + await assetFileStore.DownloadAsync(appId, assetId, fileVersion, null, stream, default, ct); } } + catch (AssetNotFoundException) + { + return; + } + } - private async Task ReadAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupReader reader, - CancellationToken ct) + private async Task ReadAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupReader reader, + CancellationToken ct) + { + try { - try - { - var fileName = GetName(assetId, fileVersion); + var fileName = GetName(assetId, fileVersion); - await using (var stream = await reader.OpenBlobAsync(fileName, ct)) - { - await assetFileStore.UploadAsync(appId, assetId, fileVersion, null, stream, true, ct); - } - } - catch (FileNotFoundException) + await using (var stream = await reader.OpenBlobAsync(fileName, ct)) { - return; + await assetFileStore.UploadAsync(appId, assetId, fileVersion, null, stream, true, ct); } } - - private static string GetName(DomainId assetId, long fileVersion) + catch (FileNotFoundException) { - return $"{assetId}_{fileVersion}.asset"; + return; } } + + private static string GetName(DomainId assetId, long fileVersion) + { + return $"{assetId}_{fileVersion}.asset"; + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs index 40d8e5a21d..692cc79263 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs @@ -7,18 +7,17 @@ using Squidex.Domain.Apps.Core.Assets; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class AnnotateAsset : AssetCommand { - public sealed class AnnotateAsset : AssetCommand - { - public string? FileName { get; set; } + public string? FileName { get; set; } - public string? Slug { get; set; } + public string? Slug { get; set; } - public bool? IsProtected { get; set; } + public bool? IsProtected { get; set; } - public HashSet<string> Tags { get; set; } + public HashSet<string> Tags { get; set; } - public AssetMetadata? Metadata { get; set; } - } + public AssetMetadata? Metadata { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssetType.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssetType.cs index 3cf40c50a3..f3780e43c5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssetType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssetType.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public enum BulkUpdateAssetType { - public enum BulkUpdateAssetType - { - Annotate, - Move, - Delete - } + Annotate, + Move, + Delete } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssets.cs index e53bbd27a1..63981084ea 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateAssets.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class BulkUpdateAssets : SquidexCommand, IAppCommand { - public sealed class BulkUpdateAssets : SquidexCommand, IAppCommand - { - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public bool CheckReferrers { get; set; } + public bool CheckReferrers { get; set; } - public BulkUpdateJob[]? Jobs { get; set; } - } + public BulkUpdateJob[]? Jobs { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateJob.cs index a0e067c888..7e26b47053 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/BulkUpdateJob.cs @@ -8,28 +8,27 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class BulkUpdateJob { - public sealed class BulkUpdateJob - { - public BulkUpdateAssetType Type { get; set; } + public BulkUpdateAssetType Type { get; set; } - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public string? FileName { get; set; } + public string? FileName { get; set; } - public string? Slug { get; set; } + public string? Slug { get; set; } - public bool? IsProtected { get; set; } + public bool? IsProtected { get; set; } - public bool Permanent { get; set; } + public bool Permanent { get; set; } - public HashSet<string> Tags { get; set; } + public HashSet<string> Tags { get; set; } - public AssetMetadata? Metadata { get; set; } + public AssetMetadata? Metadata { get; set; } - public long ExpectedVersion { get; set; } = EtagVersion.Any; - } + public long ExpectedVersion { get; set; } = EtagVersion.Any; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index 66ced59c8b..b7afcc0695 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs @@ -8,24 +8,23 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class CreateAsset : UploadAssetCommand, IMoveAssetCommand { - public sealed class CreateAsset : UploadAssetCommand, IMoveAssetCommand - { - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public bool Duplicate { get; set; } + public bool Duplicate { get; set; } - public bool OptimizeValidation { get; set; } + public bool OptimizeValidation { get; set; } - public CreateAsset() - { - AssetId = DomainId.NewGuid(); - } + public CreateAsset() + { + AssetId = DomainId.NewGuid(); + } - public MoveAsset AsMove() - { - return SimpleMapper.Map(this, new MoveAsset()); - } + public MoveAsset AsMove() + { + return SimpleMapper.Map(this, new MoveAsset()); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs index 8df36b453f..bb002a8b49 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs @@ -7,19 +7,18 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class CreateAssetFolder : AssetFolderCommand { - public sealed class CreateAssetFolder : AssetFolderCommand - { - public string FolderName { get; set; } + public string FolderName { get; set; } - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public bool OptimizeValidation { get; set; } + public bool OptimizeValidation { get; set; } - public CreateAssetFolder() - { - AssetFolderId = DomainId.NewGuid(); - } + public CreateAssetFolder() + { + AssetFolderId = DomainId.NewGuid(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs index f321fdfd9c..091e952ec0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class DeleteAsset : AssetCommand { - public sealed class DeleteAsset : AssetCommand - { - public bool CheckReferrers { get; set; } + public bool CheckReferrers { get; set; } - public bool Permanent { get; set; } - } + public bool Permanent { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs index 8100c8b639..3acec59377 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class DeleteAssetFolder : AssetFolderCommand { - public sealed class DeleteAssetFolder : AssetFolderCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/IMoveAssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/IMoveAssetCommand.cs index 92f826360a..a420ba83ec 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/IMoveAssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/IMoveAssetCommand.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public interface IMoveAssetCommand : IAppCommand { - public interface IMoveAssetCommand : IAppCommand - { - DomainId ParentId { get; set; } - } + DomainId ParentId { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs index 8df0d6faf0..532d3f529e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs @@ -7,12 +7,11 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class MoveAsset : AssetCommand { - public sealed class MoveAsset : AssetCommand - { - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public bool OptimizeValidation { get; set; } - } + public bool OptimizeValidation { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs index 6dc8528bee..2c47a75a10 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs @@ -7,12 +7,11 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class MoveAssetFolder : AssetFolderCommand { - public sealed class MoveAssetFolder : AssetFolderCommand - { - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public bool OptimizeValidation { get; set; } - } + public bool OptimizeValidation { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs index 86e2896926..965af8748a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class RenameAssetFolder : AssetFolderCommand { - public sealed class RenameAssetFolder : AssetFolderCommand - { - public string FolderName { get; set; } - } + public string FolderName { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs index 3f9c01566f..035a9d0ebd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class UpdateAsset : UploadAssetCommand { - public sealed class UpdateAsset : UploadAssetCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs index 43f5e9b457..50f8e0641d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs @@ -8,18 +8,17 @@ using Squidex.Assets; using Squidex.Domain.Apps.Core.Assets; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public abstract class UploadAssetCommand : AssetCommand { - public abstract class UploadAssetCommand : AssetCommand - { - public HashSet<string> Tags { get; set; } = new HashSet<string>(); + public HashSet<string> Tags { get; set; } = new HashSet<string>(); - public AssetFile File { get; set; } + public AssetFile File { get; set; } - public AssetMetadata Metadata { get; } = new AssetMetadata(); + public AssetMetadata Metadata { get; } = new AssetMetadata(); - public AssetType Type { get; set; } + public AssetType Type { get; set; } - public string FileHash { get; set; } - } + public string FileHash { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpsertAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpsertAsset.cs index 0b932f1ab1..cccf51cf66 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpsertAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpsertAsset.cs @@ -8,34 +8,33 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public sealed class UpsertAsset : UploadAssetCommand { - public sealed class UpsertAsset : UploadAssetCommand - { - public DomainId? ParentId { get; set; } + public DomainId? ParentId { get; set; } - public bool Duplicate { get; set; } = true; + public bool Duplicate { get; set; } = true; - public bool OptimizeValidation { get; set; } + public bool OptimizeValidation { get; set; } - public UpsertAsset() - { - AssetId = DomainId.NewGuid(); - } + public UpsertAsset() + { + AssetId = DomainId.NewGuid(); + } - public CreateAsset AsCreate() - { - return SimpleMapper.Map(this, new CreateAsset()); - } + public CreateAsset AsCreate() + { + return SimpleMapper.Map(this, new CreateAsset()); + } - public UpdateAsset AsUpdate() - { - return SimpleMapper.Map(this, new UpdateAsset()); - } + public UpdateAsset AsUpdate() + { + return SimpleMapper.Map(this, new UpdateAsset()); + } - public MoveAsset AsMove(DomainId parentId) - { - return SimpleMapper.Map(this, new MoveAsset { ParentId = parentId }); - } + public MoveAsset AsMove(DomainId parentId) + { + return SimpleMapper.Map(this, new MoveAsset { ParentId = parentId }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetCommand.cs index 6f356ec331..72cd64af80 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetCommand.cs @@ -10,25 +10,24 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public abstract class AssetCommand : AssetCommandBase { - public abstract class AssetCommand : AssetCommandBase - { - public DomainId AssetId { get; set; } + public DomainId AssetId { get; set; } - public bool DoNotScript { get; set; } + public bool DoNotScript { get; set; } - public override DomainId AggregateId - { - get => DomainId.Combine(AppId, AssetId); - } + public override DomainId AggregateId + { + get => DomainId.Combine(AppId, AssetId); } +} - // This command is needed as marker for middlewares. - public abstract class AssetCommandBase : SquidexCommand, IAppCommand, IAggregateCommand - { - public NamedId<DomainId> AppId { get; set; } +// This command is needed as marker for middlewares. +public abstract class AssetCommandBase : SquidexCommand, IAppCommand, IAggregateCommand +{ + public NamedId<DomainId> AppId { get; set; } - public abstract DomainId AggregateId { get; } - } + public abstract DomainId AggregateId { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetFolderCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetFolderCommand.cs index 21fb707eeb..4be89a15cb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetFolderCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/_AssetFolderCommand.cs @@ -10,23 +10,22 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Assets.Commands +namespace Squidex.Domain.Apps.Entities.Assets.Commands; + +public abstract class AssetFolderCommand : AssetFolderCommandBase { - public abstract class AssetFolderCommand : AssetFolderCommandBase - { - public DomainId AssetFolderId { get; set; } + public DomainId AssetFolderId { get; set; } - public override DomainId AggregateId - { - get => DomainId.Combine(AppId, AssetFolderId); - } + public override DomainId AggregateId + { + get => DomainId.Combine(AppId, AssetFolderId); } +} - // This command is needed as marker for middlewares. - public abstract class AssetFolderCommandBase : SquidexCommand, IAppCommand, IAggregateCommand - { - public NamedId<DomainId> AppId { get; set; } +// This command is needed as marker for middlewares. +public abstract class AssetFolderCommandBase : SquidexCommand, IAppCommand, IAggregateCommand +{ + public NamedId<DomainId> AppId { get; set; } - public abstract DomainId AggregateId { get; } - } + public abstract DomainId AggregateId { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs index db4497bf00..43eb1e87c3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs @@ -12,166 +12,165 @@ using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class DefaultAssetFileStore : IAssetFileStore, IDeleter { - public sealed class DefaultAssetFileStore : IAssetFileStore, IDeleter + private readonly IAssetStore assetStore; + private readonly IAssetRepository assetRepository; + private readonly AssetOptions options; + + public DefaultAssetFileStore( + IAssetStore assetStore, + IAssetRepository assetRepository, + IOptions<AssetOptions> options) { - private readonly IAssetStore assetStore; - private readonly IAssetRepository assetRepository; - private readonly AssetOptions options; - - public DefaultAssetFileStore( - IAssetStore assetStore, - IAssetRepository assetRepository, - IOptions<AssetOptions> options) - { - this.assetStore = assetStore; - this.assetRepository = assetRepository; + this.assetStore = assetStore; + this.assetRepository = assetRepository; - this.options = options.Value; - } + this.options = options.Value; + } - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + if (options.FolderPerApp) { - if (options.FolderPerApp) - { - await assetStore.DeleteByPrefixAsync($"{app.Id}/", ct); - } - else + await assetStore.DeleteByPrefixAsync($"{app.Id}/", ct); + } + else + { + await foreach (var asset in assetRepository.StreamAll(app.Id, ct)) { - await foreach (var asset in assetRepository.StreamAll(app.Id, ct)) - { - await DeleteAsync(app.Id, asset.Id, ct); - } + await DeleteAsync(app.Id, asset.Id, ct); } } + } - public string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix) - { - var fileName = GetFileName(appId, id, fileVersion, suffix); + public string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix) + { + var fileName = GetFileName(appId, id, fileVersion, suffix); - return assetStore.GeneratePublicUrl(fileName); - } + return assetStore.GeneratePublicUrl(fileName); + } - public async Task<long> GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, - CancellationToken ct = default) + public async Task<long> GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, + CancellationToken ct = default) + { + try { - try - { - var fileNameNew = GetFileName(appId, id, fileVersion, suffix); + var fileNameNew = GetFileName(appId, id, fileVersion, suffix); - return await assetStore.GetSizeAsync(fileNameNew, ct); - } - catch (AssetNotFoundException) when (!options.FolderPerApp) - { - var fileNameOld = GetFileName(id, fileVersion, suffix); + return await assetStore.GetSizeAsync(fileNameNew, ct); + } + catch (AssetNotFoundException) when (!options.FolderPerApp) + { + var fileNameOld = GetFileName(id, fileVersion, suffix); - return await assetStore.GetSizeAsync(fileNameOld, ct); - } + return await assetStore.GetSizeAsync(fileNameOld, ct); } + } - public async Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, - CancellationToken ct = default) + public async Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, + CancellationToken ct = default) + { + try { - try - { - var fileNameNew = GetFileName(appId, id, fileVersion, suffix); - - await assetStore.DownloadAsync(fileNameNew, stream, range, ct); - } - catch (AssetNotFoundException) when (!options.FolderPerApp) - { - var fileNameOld = GetFileName(id, fileVersion, suffix); + var fileNameNew = GetFileName(appId, id, fileVersion, suffix); - await assetStore.DownloadAsync(fileNameOld, stream, range, ct); - } + await assetStore.DownloadAsync(fileNameNew, stream, range, ct); } - - public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, - CancellationToken ct = default) + catch (AssetNotFoundException) when (!options.FolderPerApp) { - var fileName = GetFileName(appId, id, fileVersion, suffix); + var fileNameOld = GetFileName(id, fileVersion, suffix); - return assetStore.UploadAsync(fileName, stream, overwrite, ct); + await assetStore.DownloadAsync(fileNameOld, stream, range, ct); } + } - public Task UploadAsync(string tempFile, Stream stream, - CancellationToken ct = default) + public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, + CancellationToken ct = default) + { + var fileName = GetFileName(appId, id, fileVersion, suffix); + + return assetStore.UploadAsync(fileName, stream, overwrite, ct); + } + + public Task UploadAsync(string tempFile, Stream stream, + CancellationToken ct = default) + { + return assetStore.UploadAsync(tempFile, stream, false, ct); + } + + public Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, + CancellationToken ct = default) + { + var fileName = GetFileName(appId, id, fileVersion, suffix); + + return assetStore.CopyAsync(tempFile, fileName, ct); + } + + public Task DeleteAsync(DomainId appId, DomainId id, + CancellationToken ct = default) + { + if (options.FolderPerApp) { - return assetStore.UploadAsync(tempFile, stream, false, ct); + return assetStore.DeleteByPrefixAsync($"{appId}/{id}", ct); } - - public Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, - CancellationToken ct = default) + else { - var fileName = GetFileName(appId, id, fileVersion, suffix); + var fileNameOld = GetFileName(id); + var fileNameNew = GetFileName(appId, id); - return assetStore.CopyAsync(tempFile, fileName, ct); + return Task.WhenAll( + assetStore.DeleteByPrefixAsync(fileNameOld, ct), + assetStore.DeleteByPrefixAsync(fileNameNew, ct)); } + } - public Task DeleteAsync(DomainId appId, DomainId id, - CancellationToken ct = default) + public Task DeleteAsync(string tempFile, + CancellationToken ct = default) + { + return assetStore.DeleteAsync(tempFile, ct); + } + + private string GetFileName(DomainId id, long fileVersion = -1, string? suffix = null) + { + return GetFileName(default, id, fileVersion, suffix); + } + + private string GetFileName(DomainId appId, DomainId id, long fileVersion = -1, string? suffix = null) + { + var sb = new StringBuilder(20); + + if (appId != default) { + sb.Append(appId); + if (options.FolderPerApp) { - return assetStore.DeleteByPrefixAsync($"{appId}/{id}", ct); + sb.Append('/'); } else { - var fileNameOld = GetFileName(id); - var fileNameNew = GetFileName(appId, id); - - return Task.WhenAll( - assetStore.DeleteByPrefixAsync(fileNameOld, ct), - assetStore.DeleteByPrefixAsync(fileNameNew, ct)); + sb.Append('_'); } } - public Task DeleteAsync(string tempFile, - CancellationToken ct = default) - { - return assetStore.DeleteAsync(tempFile, ct); - } + sb.Append(id); - private string GetFileName(DomainId id, long fileVersion = -1, string? suffix = null) + if (fileVersion >= 0) { - return GetFileName(default, id, fileVersion, suffix); + sb.Append('_'); + sb.Append(fileVersion); } - private string GetFileName(DomainId appId, DomainId id, long fileVersion = -1, string? suffix = null) + if (!string.IsNullOrWhiteSpace(suffix)) { - var sb = new StringBuilder(20); - - if (appId != default) - { - sb.Append(appId); - - if (options.FolderPerApp) - { - sb.Append('/'); - } - else - { - sb.Append('_'); - } - } - - sb.Append(id); - - if (fileVersion >= 0) - { - sb.Append('_'); - sb.Append(fileVersion); - } - - if (!string.IsNullOrWhiteSpace(suffix)) - { - sb.Append('_'); - sb.Append(suffix); - } - - return sb.ToString(); + sb.Append('_'); + sb.Append(suffix); } + + return sb.ToString(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs index c437eb6098..71f870ebc5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs @@ -11,172 +11,171 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public sealed class AssetCommandMiddleware : CachingDomainObjectMiddleware<AssetCommand, AssetDomainObject, AssetDomainObject.State> { - public sealed class AssetCommandMiddleware : CachingDomainObjectMiddleware<AssetCommand, AssetDomainObject, AssetDomainObject.State> + private readonly IAssetFileStore assetFileStore; + private readonly IAssetEnricher assetEnricher; + private readonly IAssetQueryService assetQuery; + private readonly IContextProvider contextProvider; + private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources; + + public AssetCommandMiddleware( + IDomainObjectFactory domainObjectFactory, + IDomainObjectCache domainObjectCache, + IAssetEnricher assetEnricher, + IAssetFileStore assetFileStore, + IAssetQueryService assetQuery, + IContextProvider contextProvider, + IEnumerable<IAssetMetadataSource> assetMetadataSources) + : base(domainObjectFactory, domainObjectCache) { - private readonly IAssetFileStore assetFileStore; - private readonly IAssetEnricher assetEnricher; - private readonly IAssetQueryService assetQuery; - private readonly IContextProvider contextProvider; - private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources; - - public AssetCommandMiddleware( - IDomainObjectFactory domainObjectFactory, - IDomainObjectCache domainObjectCache, - IAssetEnricher assetEnricher, - IAssetFileStore assetFileStore, - IAssetQueryService assetQuery, - IContextProvider contextProvider, - IEnumerable<IAssetMetadataSource> assetMetadataSources) - : base(domainObjectFactory, domainObjectCache) - { - this.assetEnricher = assetEnricher; - this.assetFileStore = assetFileStore; - this.assetMetadataSources = assetMetadataSources.OrderBy(x => x.Order).ToList(); - this.assetQuery = assetQuery; - this.contextProvider = contextProvider; - } + this.assetEnricher = assetEnricher; + this.assetFileStore = assetFileStore; + this.assetMetadataSources = assetMetadataSources.OrderBy(x => x.Order).ToList(); + this.assetQuery = assetQuery; + this.contextProvider = contextProvider; + } - public override async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + public override async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + switch (context.Command) { - switch (context.Command) - { - case CreateAsset create: - await UploadWithDuplicateCheckAsync(context, create, create.Duplicate, next, ct); - break; + case CreateAsset create: + await UploadWithDuplicateCheckAsync(context, create, create.Duplicate, next, ct); + break; - case UpsertAsset upsert: - await UploadWithDuplicateCheckAsync(context, upsert, upsert.Duplicate, next, ct); - break; + case UpsertAsset upsert: + await UploadWithDuplicateCheckAsync(context, upsert, upsert.Duplicate, next, ct); + break; - case MoveAsset move: - await base.HandleAsync(context, next, ct); - break; + case MoveAsset move: + await base.HandleAsync(context, next, ct); + break; - case UpdateAsset upload: - await UploadAndHandleAsync(context, upload, next, ct); - break; + case UpdateAsset upload: + await UploadAndHandleAsync(context, upload, next, ct); + break; - default: - await base.HandleAsync(context, next, ct); - break; - } + default: + await base.HandleAsync(context, next, ct); + break; } + } - private async Task UploadWithDuplicateCheckAsync(CommandContext context, UploadAssetCommand command, bool duplicate, NextDelegate next, - CancellationToken ct) + private async Task UploadWithDuplicateCheckAsync(CommandContext context, UploadAssetCommand command, bool duplicate, NextDelegate next, + CancellationToken ct) + { + var tempFile = context.ContextId.ToString(); + + try { - var tempFile = context.ContextId.ToString(); + await EnrichWithHashAndUploadAsync(command, tempFile, ct); - try + if (!duplicate) { - await EnrichWithHashAndUploadAsync(command, tempFile, ct); - - if (!duplicate) + var existing = + await assetQuery.FindByHashAsync(contextProvider.Context, + command.FileHash, + command.File.FileName, + command.File.FileSize, + ct); + + if (existing != null) { - var existing = - await assetQuery.FindByHashAsync(contextProvider.Context, - command.FileHash, - command.File.FileName, - command.File.FileSize, - ct); - - if (existing != null) - { - context.Complete(new AssetDuplicate(existing)); - - await next(context, ct); - return; - } + context.Complete(new AssetDuplicate(existing)); + + await next(context, ct); + return; } + } - await EnrichWithMetadataAsync(command, ct); + await EnrichWithMetadataAsync(command, ct); - await base.HandleAsync(context, next, ct); - } - finally - { - await assetFileStore.DeleteAsync(tempFile, ct); - } + await base.HandleAsync(context, next, ct); } - - private async Task UploadAndHandleAsync(CommandContext context, UploadAssetCommand command, NextDelegate next, - CancellationToken ct) + finally { - var tempFile = context.ContextId.ToString(); + await assetFileStore.DeleteAsync(tempFile, ct); + } + } - try - { - await EnrichWithHashAndUploadAsync(command, tempFile, ct); - await EnrichWithMetadataAsync(command, ct); + private async Task UploadAndHandleAsync(CommandContext context, UploadAssetCommand command, NextDelegate next, + CancellationToken ct) + { + var tempFile = context.ContextId.ToString(); - await base.HandleAsync(context, next, ct); - } - finally - { - await assetFileStore.DeleteAsync(tempFile, ct); - } - } + try + { + await EnrichWithHashAndUploadAsync(command, tempFile, ct); + await EnrichWithMetadataAsync(command, ct); - protected override async Task<object> EnrichResultAsync(CommandContext context, CommandResult result, - CancellationToken ct) + await base.HandleAsync(context, next, ct); + } + finally { - var payload = await base.EnrichResultAsync(context, result, ct); + await assetFileStore.DeleteAsync(tempFile, ct); + } + } - if (payload is IAssetEntity asset) + protected override async Task<object> EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) + { + var payload = await base.EnrichResultAsync(context, result, ct); + + if (payload is IAssetEntity asset) + { + if (result.IsChanged && context.Command is UploadAssetCommand) { - if (result.IsChanged && context.Command is UploadAssetCommand) + var tempFile = context.ContextId.ToString(); + try { - var tempFile = context.ContextId.ToString(); - try - { - await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion, null, ct); - } - catch (AssetAlreadyExistsException) when (context.Command is not UpsertAsset) - { - throw; - } + await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion, null, ct); } - - if (payload is not IEnrichedAssetEntity) + catch (AssetAlreadyExistsException) when (context.Command is not UpsertAsset) { - payload = await assetEnricher.EnrichAsync(asset, contextProvider.Context, ct); + throw; } } - return payload; + if (payload is not IEnrichedAssetEntity) + { + payload = await assetEnricher.EnrichAsync(asset, contextProvider.Context, ct); + } } - private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile, - CancellationToken ct) + return payload; + } + + private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile, + CancellationToken ct) + { + await using (var uploadStream = command.File.OpenRead()) { - await using (var uploadStream = command.File.OpenRead()) + await using (var hashStream = new HasherStream(uploadStream, HashAlgorithmName.SHA256)) { - await using (var hashStream = new HasherStream(uploadStream, HashAlgorithmName.SHA256)) - { - await assetFileStore.UploadAsync(tempFile, hashStream, ct); + await assetFileStore.UploadAsync(tempFile, hashStream, ct); - command.FileHash = ComputeHash(command.File, hashStream); - } + command.FileHash = ComputeHash(command.File, hashStream); } } + } - private static string ComputeHash(AssetFile file, HasherStream hashStream) - { - var steamHash = hashStream.GetHashStringAndReset(); + private static string ComputeHash(AssetFile file, HasherStream hashStream) + { + var steamHash = hashStream.GetHashStringAndReset(); - return $"{steamHash}{file.FileName}{file.FileSize}".ToSha256Base64(); - } + return $"{steamHash}{file.FileName}{file.FileSize}".ToSha256Base64(); + } - private async Task EnrichWithMetadataAsync(UploadAssetCommand command, - CancellationToken ct) + private async Task EnrichWithMetadataAsync(UploadAssetCommand command, + CancellationToken ct) + { + foreach (var metadataSource in assetMetadataSources) { - foreach (var metadataSource in assetMetadataSources) - { - await metadataSource.EnhanceAsync(command, ct); - } + await metadataSource.EnhanceAsync(command, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs index 8d2ef97583..83bbdec877 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs @@ -13,155 +13,154 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public partial class AssetDomainObject { - public partial class AssetDomainObject + public sealed class State : DomainObjectState<State>, IAssetEntity { - public sealed class State : DomainObjectState<State>, IAssetEntity - { - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public string FileName { get; set; } + public string FileName { get; set; } - public string FileHash { get; set; } + public string FileHash { get; set; } - public string MimeType { get; set; } + public string MimeType { get; set; } - public string Slug { get; set; } + public string Slug { get; set; } - public long FileVersion { get; set; } + public long FileVersion { get; set; } - public long FileSize { get; set; } + public long FileSize { get; set; } - public long TotalSize { get; set; } + public long TotalSize { get; set; } - public bool IsProtected { get; set; } + public bool IsProtected { get; set; } - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - public HashSet<string> Tags { get; set; } + public HashSet<string> Tags { get; set; } - public AssetMetadata Metadata { get; set; } + public AssetMetadata Metadata { get; set; } - public AssetType Type { get; set; } + public AssetType Type { get; set; } - [JsonIgnore] - public DomainId AssetId - { - get => Id; - } + [JsonIgnore] + public DomainId AssetId + { + get => Id; + } - [JsonIgnore] - public DomainId UniqueId - { - get => DomainId.Combine(AppId, Id); - } + [JsonIgnore] + public DomainId UniqueId + { + get => DomainId.Combine(AppId, Id); + } - public override bool ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) + { + switch (@event) { - switch (@event) - { - case AssetCreated e: - { - Id = e.AssetId; - - SimpleMapper.Map(e, this); + case AssetCreated e: + { + Id = e.AssetId; - if (string.IsNullOrWhiteSpace(Slug)) - { - Slug = FileName.ToAssetSlug(); - } + SimpleMapper.Map(e, this); - TotalSize += e.FileSize; - - EnsureProperties(); - return true; - } - - case AssetUpdated e when Is.Change(e.FileHash, FileHash): + if (string.IsNullOrWhiteSpace(Slug)) { - SimpleMapper.Map(e, this); - - TotalSize += e.FileSize; - - EnsureProperties(); - return true; + Slug = FileName.ToAssetSlug(); } - case AssetAnnotated e: - { - var hasChanged = false; + TotalSize += e.FileSize; - if (Is.OptionalChange(FileName, e.FileName)) - { - FileName = e.FileName; + EnsureProperties(); + return true; + } - hasChanged = true; - } + case AssetUpdated e when Is.Change(e.FileHash, FileHash): + { + SimpleMapper.Map(e, this); - if (Is.OptionalChange(Slug, e.Slug)) - { - Slug = e.Slug; + TotalSize += e.FileSize; - hasChanged = true; - } + EnsureProperties(); + return true; + } - if (Is.OptionalChange(IsProtected, e.IsProtected)) - { - IsProtected = e.IsProtected.Value; + case AssetAnnotated e: + { + var hasChanged = false; - hasChanged = true; - } + if (Is.OptionalChange(FileName, e.FileName)) + { + FileName = e.FileName; - if (Is.OptionalSetChange(Tags, e.Tags)) - { - Tags = e.Tags; + hasChanged = true; + } - hasChanged = true; - } + if (Is.OptionalChange(Slug, e.Slug)) + { + Slug = e.Slug; - if (Is.OptionalMapChange(Metadata, e.Metadata)) - { - Metadata = e.Metadata; + hasChanged = true; + } - hasChanged = true; - } + if (Is.OptionalChange(IsProtected, e.IsProtected)) + { + IsProtected = e.IsProtected.Value; - EnsureProperties(); - return hasChanged; + hasChanged = true; } - case AssetMoved e when e.ParentId != ParentId: + if (Is.OptionalSetChange(Tags, e.Tags)) { - ParentId = e.ParentId; + Tags = e.Tags; - EnsureProperties(); - return true; + hasChanged = true; } - case AssetDeleted: + if (Is.OptionalMapChange(Metadata, e.Metadata)) { - IsDeleted = true; - return true; + Metadata = e.Metadata; + + hasChanged = true; } - } - return false; + EnsureProperties(); + return hasChanged; + } + + case AssetMoved e when e.ParentId != ParentId: + { + ParentId = e.ParentId; + + EnsureProperties(); + return true; + } + + case AssetDeleted: + { + IsDeleted = true; + return true; + } + } + + return false; + } + + private void EnsureProperties() + { + if (Tags == null) + { + Tags = new HashSet<string>(); } - private void EnsureProperties() + if (Metadata == null) { - if (Tags == null) - { - Tags = new HashSet<string>(); - } - - if (Metadata == null) - { - Metadata = new AssetMetadata(); - } + Metadata = new AssetMetadata(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs index 519f8f1589..d2ab859add 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs @@ -18,254 +18,253 @@ #pragma warning disable MA0022 // Return Task.FromResult instead of returning null -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public partial class AssetDomainObject : DomainObject<AssetDomainObject.State> { - public partial class AssetDomainObject : DomainObject<AssetDomainObject.State> + private readonly IServiceProvider serviceProvider; + + public AssetDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<AssetDomainObject> log, + IServiceProvider serviceProvider) + : base(id, persistence, log) { - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public AssetDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<AssetDomainObject> log, - IServiceProvider serviceProvider) - : base(id, persistence, log) - { - this.serviceProvider = serviceProvider; - } + protected override bool IsDeleted(State snapshot) + { + return snapshot.IsDeleted; + } - protected override bool IsDeleted(State snapshot) - { - return snapshot.IsDeleted; - } + protected override bool CanRecreate() + { + return true; + } - protected override bool CanRecreate() - { - return true; - } + protected override bool CanRecreate(IEvent @event) + { + return @event is AssetCreated; + } - protected override bool CanRecreate(IEvent @event) - { - return @event is AssetCreated; - } + protected override bool CanAcceptCreation(ICommand command) + { + return command is AssetCommandBase; + } - protected override bool CanAcceptCreation(ICommand command) - { - return command is AssetCommandBase; - } + protected override bool CanAccept(ICommand command) + { + return command is AssetCommand assetCommand && + Equals(assetCommand.AppId, Snapshot.AppId) && + Equals(assetCommand.AssetId, Snapshot.Id); + } - protected override bool CanAccept(ICommand command) + public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + switch (command) { - return command is AssetCommand assetCommand && - Equals(assetCommand.AppId, Snapshot.AppId) && - Equals(assetCommand.AssetId, Snapshot.Id); - } + case UpsertAsset upsert: + return UpsertReturnAsync(upsert, async (c, ct) => + { + var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct) - { - switch (command) - { - case UpsertAsset upsert: - return UpsertReturnAsync(upsert, async (c, ct) => + if (Version > EtagVersion.Empty && !IsDeleted(Snapshot)) { - var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - - if (Version > EtagVersion.Empty && !IsDeleted(Snapshot)) - { - await UpdateCore(c.AsUpdate(), operation); - } - else - { - await CreateCore(c.AsCreate(), operation); - } - - if (Is.OptionalChange(Snapshot.ParentId, c.ParentId)) - { - await MoveCore(c.AsMove(c.ParentId.Value), operation); - } - - return Snapshot; - }, ct); - - case CreateAsset create: - return CreateReturnAsync(create, async (c, ct) => + await UpdateCore(c.AsUpdate(), operation); + } + else + { + await CreateCore(c.AsCreate(), operation); + } + + if (Is.OptionalChange(Snapshot.ParentId, c.ParentId)) { - var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await MoveCore(c.AsMove(c.ParentId.Value), operation); + } - await CreateCore(c, operation); + return Snapshot; + }, ct); - if (Is.Change(Snapshot.ParentId, c.ParentId)) - { - await MoveCore(c.AsMove(), operation); - } + case CreateAsset create: + return CreateReturnAsync(create, async (c, ct) => + { + var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - return Snapshot; - }, ct); + await CreateCore(c, operation); - case AnnotateAsset annotate: - return UpdateReturnAsync(annotate, async (c, ct) => + if (Is.Change(Snapshot.ParentId, c.ParentId)) { - var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await MoveCore(c.AsMove(), operation); + } - await AnnotateCore(c, operation); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case AnnotateAsset annotate: + return UpdateReturnAsync(annotate, async (c, ct) => + { + var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - case UpdateAsset update: - return UpdateReturnAsync(update, async (c, ct) => - { - var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await AnnotateCore(c, operation); - await UpdateCore(c, operation); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateAsset update: + return UpdateReturnAsync(update, async (c, ct) => + { + var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - case MoveAsset move: - return UpdateReturnAsync(move, async (c, ct) => - { - var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await UpdateCore(c, operation); - await MoveCore(c, operation); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case MoveAsset move: + return UpdateReturnAsync(move, async (c, ct) => + { + var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - case DeleteAsset { Permanent: true } delete: - return DeletePermanentAsync(delete, async (c, ct) => - { - var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await MoveCore(c, operation); - await DeleteCore(c, operation); - }, ct); + return Snapshot; + }, ct); - case DeleteAsset delete: - return UpdateAsync(delete, async (c, ct) => - { - var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); + case DeleteAsset { Permanent: true } delete: + return DeletePermanentAsync(delete, async (c, ct) => + { + var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - await DeleteCore(c, operation); - }, ct); + await DeleteCore(c, operation); + }, ct); - default: - ThrowHelper.NotSupportedException(); - return default!; - } - } + case DeleteAsset delete: + return UpdateAsync(delete, async (c, ct) => + { + var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - private async Task CreateCore(CreateAsset create, AssetOperation operation) - { - if (!create.OptimizeValidation) - { - await operation.MustMoveToValidFolder(create.ParentId); - } - - if (!create.DoNotScript) - { - await operation.ExecuteCreateScriptAsync(create); - } - - if (create.Tags != null) - { - create.Tags = await operation.GetTagIdsAsync(create.Tags); - } - - Create(create); + await DeleteCore(c, operation); + }, ct); + + default: + ThrowHelper.NotSupportedException(); + return default!; } + } - private async Task AnnotateCore(AnnotateAsset annotate, AssetOperation operation) + private async Task CreateCore(CreateAsset create, AssetOperation operation) + { + if (!create.OptimizeValidation) { - if (!annotate.DoNotScript) - { - await operation.ExecuteAnnotateScriptAsync(annotate); - } - - if (annotate.Tags != null) - { - annotate.Tags = await operation.GetTagIdsAsync(annotate.Tags); - } - - Annotate(annotate); + await operation.MustMoveToValidFolder(create.ParentId); } - private async Task UpdateCore(UpdateAsset update, AssetOperation operation) + if (!create.DoNotScript) { - if (!update.DoNotScript) - { - await operation.ExecuteUpdateScriptAsync(update); - } - - Update(update); + await operation.ExecuteCreateScriptAsync(create); } - private async Task MoveCore(MoveAsset move, AssetOperation operation) + if (create.Tags != null) { - if (!move.OptimizeValidation) - { - await operation.MustMoveToValidFolder(move.ParentId); - } + create.Tags = await operation.GetTagIdsAsync(create.Tags); + } - if (!move.DoNotScript) - { - await operation.ExecuteMoveScriptAsync(move); - } + Create(create); + } - Move(move); + private async Task AnnotateCore(AnnotateAsset annotate, AssetOperation operation) + { + if (!annotate.DoNotScript) + { + await operation.ExecuteAnnotateScriptAsync(annotate); } - private async Task DeleteCore(DeleteAsset delete, AssetOperation operation) + if (annotate.Tags != null) { - if (delete.CheckReferrers) - { - await operation.CheckReferrersAsync(); - } + annotate.Tags = await operation.GetTagIdsAsync(annotate.Tags); + } - if (!delete.DoNotScript) - { - await operation.ExecuteDeleteScriptAsync(delete); - } + Annotate(annotate); + } - Delete(delete); + private async Task UpdateCore(UpdateAsset update, AssetOperation operation) + { + if (!update.DoNotScript) + { + await operation.ExecuteUpdateScriptAsync(update); } - private void Create(CreateAsset command) + Update(update); + } + + private async Task MoveCore(MoveAsset move, AssetOperation operation) + { + if (!move.OptimizeValidation) { - Raise(command, new AssetCreated - { - MimeType = command.File.MimeType, - FileName = command.File.FileName, - FileSize = command.File.FileSize, - Slug = command.File.FileName.ToAssetSlug() - }); + await operation.MustMoveToValidFolder(move.ParentId); } - private void Update(UpdateAsset command) + if (!move.DoNotScript) { - Raise(command, new AssetUpdated - { - MimeType = command.File.MimeType, - FileVersion = Snapshot.FileVersion + 1, - FileSize = command.File.FileSize - }); + await operation.ExecuteMoveScriptAsync(move); } - private void Annotate(AnnotateAsset command) + Move(move); + } + + private async Task DeleteCore(DeleteAsset delete, AssetOperation operation) + { + if (delete.CheckReferrers) { - Raise(command, new AssetAnnotated()); + await operation.CheckReferrersAsync(); } - private void Move(MoveAsset command) + if (!delete.DoNotScript) { - Raise(command, new AssetMoved()); + await operation.ExecuteDeleteScriptAsync(delete); } - private void Delete(DeleteAsset command) + Delete(delete); + } + + private void Create(CreateAsset command) + { + Raise(command, new AssetCreated { - Raise(command, new AssetDeleted { OldTags = Snapshot.Tags, DeletedSize = Snapshot.TotalSize }); - } + MimeType = command.File.MimeType, + FileName = command.File.FileName, + FileSize = command.File.FileSize, + Slug = command.File.FileName.ToAssetSlug() + }); + } - private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent + private void Update(UpdateAsset command) + { + Raise(command, new AssetUpdated { - RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); - } + MimeType = command.File.MimeType, + FileVersion = Snapshot.FileVersion + 1, + FileSize = command.File.FileSize + }); + } + + private void Annotate(AnnotateAsset command) + { + Raise(command, new AssetAnnotated()); + } + + private void Move(MoveAsset command) + { + Raise(command, new AssetMoved()); + } + + private void Delete(DeleteAsset command) + { + Raise(command, new AssetDeleted { OldTags = Snapshot.Tags, DeletedSize = Snapshot.TotalSize }); + } + + private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent + { + RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs index 3bf73e437c..8825f3d242 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs @@ -12,59 +12,58 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public sealed partial class AssetFolderDomainObject { - public sealed partial class AssetFolderDomainObject + public sealed class State : DomainObjectState<State>, IAssetFolderEntity { - public sealed class State : DomainObjectState<State>, IAssetFolderEntity - { - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public string FolderName { get; set; } + public string FolderName { get; set; } - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - [JsonIgnore] - public DomainId UniqueId - { - get => DomainId.Combine(AppId, Id); - } + [JsonIgnore] + public DomainId UniqueId + { + get => DomainId.Combine(AppId, Id); + } - public override bool ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) + { + switch (@event) { - switch (@event) - { - case AssetFolderCreated e: - { - Id = e.AssetFolderId; + case AssetFolderCreated e: + { + Id = e.AssetFolderId; - SimpleMapper.Map(e, this); - return true; - } + SimpleMapper.Map(e, this); + return true; + } - case AssetFolderRenamed e when Is.OptionalChange(FolderName, e.FolderName): - { - FolderName = e.FolderName; - return true; - } + case AssetFolderRenamed e when Is.OptionalChange(FolderName, e.FolderName): + { + FolderName = e.FolderName; + return true; + } - case AssetFolderMoved e when Is.Change(ParentId, e.ParentId): - { - ParentId = e.ParentId; - return true; - } + case AssetFolderMoved e when Is.Change(ParentId, e.ParentId): + { + ParentId = e.ParentId; + return true; + } - case AssetFolderDeleted: - { - IsDeleted = true; - return true; - } - } - - return false; + case AssetFolderDeleted: + { + IsDeleted = true; + return true; + } } + + return false; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs index e1e480d33d..a6581697bb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs @@ -18,135 +18,134 @@ #pragma warning disable MA0022 // Return Task.FromResult instead of returning null -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDomainObject.State> { - public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDomainObject.State> - { - private readonly IServiceProvider serviceProvider; + private readonly IServiceProvider serviceProvider; - public AssetFolderDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<AssetFolderDomainObject> log, - IServiceProvider serviceProvider) - : base(id, persistence, log) - { - this.serviceProvider = serviceProvider; - } + public AssetFolderDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<AssetFolderDomainObject> log, + IServiceProvider serviceProvider) + : base(id, persistence, log) + { + this.serviceProvider = serviceProvider; + } - protected override bool IsDeleted(State snapshot) - { - return Snapshot.IsDeleted; - } + protected override bool IsDeleted(State snapshot) + { + return Snapshot.IsDeleted; + } - protected override bool CanAcceptCreation(ICommand command) - { - return command is AssetFolderCommandBase; - } + protected override bool CanAcceptCreation(ICommand command) + { + return command is AssetFolderCommandBase; + } - protected override bool CanAccept(ICommand command) - { - return command is AssetFolderCommand assetFolderCommand && - Equals(assetFolderCommand.AppId, Snapshot.AppId) && - Equals(assetFolderCommand.AssetFolderId, Snapshot.Id); - } + protected override bool CanAccept(ICommand command) + { + return command is AssetFolderCommand assetFolderCommand && + Equals(assetFolderCommand.AppId, Snapshot.AppId) && + Equals(assetFolderCommand.AssetFolderId, Snapshot.Id); + } - public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct) + public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + switch (command) { - switch (command) - { - case CreateAssetFolder create: - return CreateReturnAsync(create, async (c, ct) => - { - await CreateCore(c, c); - - return Snapshot; - }, ct); - - case MoveAssetFolder move: - return UpdateReturnAsync(move, async (c, ct) => - { - await MoveCore(c); - - return Snapshot; - }, ct); - - case RenameAssetFolder rename: - return UpdateReturnAsync(rename, async (c, ct) => - { - await RenameCore(c); - - return Snapshot; - }, ct); - - case DeleteAssetFolder delete: - return Update(delete, c => - { - Delete(c); - }, ct); - - default: - ThrowHelper.NotSupportedException(); - return default!; - } + case CreateAssetFolder create: + return CreateReturnAsync(create, async (c, ct) => + { + await CreateCore(c, c); + + return Snapshot; + }, ct); + + case MoveAssetFolder move: + return UpdateReturnAsync(move, async (c, ct) => + { + await MoveCore(c); + + return Snapshot; + }, ct); + + case RenameAssetFolder rename: + return UpdateReturnAsync(rename, async (c, ct) => + { + await RenameCore(c); + + return Snapshot; + }, ct); + + case DeleteAssetFolder delete: + return Update(delete, c => + { + Delete(c); + }, ct); + + default: + ThrowHelper.NotSupportedException(); + return default!; } + } - private async Task CreateCore(CreateAssetFolder create, CreateAssetFolder c) - { - var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot); - - operation.MustHaveName(c.FolderName); + private async Task CreateCore(CreateAssetFolder create, CreateAssetFolder c) + { + var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot); - if (!c.OptimizeValidation) - { - await operation.MustMoveToValidFolder(c.ParentId); - } + operation.MustHaveName(c.FolderName); - Create(create); + if (!c.OptimizeValidation) + { + await operation.MustMoveToValidFolder(c.ParentId); } - private async Task MoveCore(MoveAssetFolder c) - { - var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot); + Create(create); + } - if (!c.OptimizeValidation) - { - await operation.MustMoveToValidFolder(c.ParentId); - } + private async Task MoveCore(MoveAssetFolder c) + { + var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot); - Move(c); + if (!c.OptimizeValidation) + { + await operation.MustMoveToValidFolder(c.ParentId); } - private async Task RenameCore(RenameAssetFolder c) - { - var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot); + Move(c); + } - operation.MustHaveName(c.FolderName); + private async Task RenameCore(RenameAssetFolder c) + { + var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot); - Rename(c); - } + operation.MustHaveName(c.FolderName); - private void Create(CreateAssetFolder command) - { - Raise(command, new AssetFolderCreated()); - } + Rename(c); + } - private void Move(MoveAssetFolder command) - { - Raise(command, new AssetFolderMoved()); - } + private void Create(CreateAssetFolder command) + { + Raise(command, new AssetFolderCreated()); + } - private void Rename(RenameAssetFolder command) - { - Raise(command, new AssetFolderRenamed()); - } + private void Move(MoveAssetFolder command) + { + Raise(command, new AssetFolderMoved()); + } - private void Delete(DeleteAssetFolder command) - { - Raise(command, new AssetFolderDeleted()); - } + private void Rename(RenameAssetFolder command) + { + Raise(command, new AssetFolderRenamed()); + } - private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent - { - RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); - } + private void Delete(DeleteAssetFolder command) + { + Raise(command, new AssetFolderDeleted()); + } + + private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent + { + RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderOperation.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderOperation.cs index 3a92260869..88505ca57c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderOperation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderOperation.cs @@ -9,35 +9,34 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public sealed class AssetFolderOperation : OperationContextBase<AssetFolderCommand, IAssetFolderEntity> { - public sealed class AssetFolderOperation : OperationContextBase<AssetFolderCommand, IAssetFolderEntity> + public AssetFolderOperation(IServiceProvider serviceProvider, Func<IAssetFolderEntity> snapshot) + : base(serviceProvider, snapshot) { - public AssetFolderOperation(IServiceProvider serviceProvider, Func<IAssetFolderEntity> snapshot) - : base(serviceProvider, snapshot) - { - Guard.NotNull(serviceProvider); - } + Guard.NotNull(serviceProvider); + } - public static async Task<AssetFolderOperation> CreateAsync(IServiceProvider services, AssetFolderCommand command, Func<IAssetFolderEntity> snapshot) - { - var appProvider = services.GetRequiredService<IAppProvider>(); + public static async Task<AssetFolderOperation> CreateAsync(IServiceProvider services, AssetFolderCommand command, Func<IAssetFolderEntity> snapshot) + { + var appProvider = services.GetRequiredService<IAppProvider>(); - var app = await appProvider.GetAppAsync(command.AppId.Id); + var app = await appProvider.GetAppAsync(command.AppId.Id); - if (app == null) - { - throw new DomainObjectNotFoundException(command.AppId.Id.ToString()); - } + if (app == null) + { + throw new DomainObjectNotFoundException(command.AppId.Id.ToString()); + } - var id = command.AssetFolderId; + var id = command.AssetFolderId; - return new AssetFolderOperation(services, snapshot) - { - App = app, - Command = command, - CommandId = id - }; - } + return new AssetFolderOperation(services, snapshot) + { + App = app, + Command = command, + CommandId = id + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetOperation.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetOperation.cs index 1c0cf03e69..0f4bfcefa3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetOperation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetOperation.cs @@ -9,35 +9,34 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public sealed class AssetOperation : OperationContextBase<AssetCommand, IAssetEntity> { - public sealed class AssetOperation : OperationContextBase<AssetCommand, IAssetEntity> + public AssetOperation(IServiceProvider serviceProvider, Func<IAssetEntity> snapshot) + : base(serviceProvider, snapshot) { - public AssetOperation(IServiceProvider serviceProvider, Func<IAssetEntity> snapshot) - : base(serviceProvider, snapshot) - { - Guard.NotNull(serviceProvider); - } + Guard.NotNull(serviceProvider); + } - public static async Task<AssetOperation> CreateAsync(IServiceProvider services, AssetCommand command, Func<IAssetEntity> snapshot) - { - var appProvider = services.GetRequiredService<IAppProvider>(); + public static async Task<AssetOperation> CreateAsync(IServiceProvider services, AssetCommand command, Func<IAssetEntity> snapshot) + { + var appProvider = services.GetRequiredService<IAppProvider>(); - var app = await appProvider.GetAppAsync(command.AppId.Id); + var app = await appProvider.GetAppAsync(command.AppId.Id); - if (app == null) - { - throw new DomainObjectNotFoundException(command.AppId.Id.ToString()); - } + if (app == null) + { + throw new DomainObjectNotFoundException(command.AppId.Id.ToString()); + } - var id = command.AssetId; + var id = command.AssetId; - return new AssetOperation(services, snapshot) - { - App = app, - Command = command, - CommandId = id - }; - } + return new AssetOperation(services, snapshot) + { + App = app, + Command = command, + CommandId = id + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs index b51fd08289..dd4786cbbd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs @@ -19,207 +19,206 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable RECS0082 // Parameter has the same name as a member and hides it -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public sealed class AssetsBulkUpdateCommandMiddleware : ICommandMiddleware { - public sealed class AssetsBulkUpdateCommandMiddleware : ICommandMiddleware - { - private readonly IContextProvider contextProvider; - private readonly ILogger<AssetsBulkUpdateCommandMiddleware> log; + private readonly IContextProvider contextProvider; + private readonly ILogger<AssetsBulkUpdateCommandMiddleware> log; - private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command, - CancellationToken CancellationToken); + private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command, + CancellationToken CancellationToken); - private sealed record BulkTask( - ICommandBus Bus, - int JobIndex, - BulkUpdateJob CommandJob, - BulkUpdateAssets Command, - ConcurrentBag<BulkUpdateResultItem> Results, - CancellationToken Aborted); + private sealed record BulkTask( + ICommandBus Bus, + int JobIndex, + BulkUpdateJob CommandJob, + BulkUpdateAssets Command, + ConcurrentBag<BulkUpdateResultItem> Results, + CancellationToken Aborted); - public AssetsBulkUpdateCommandMiddleware(IContextProvider contextProvider, ILogger<AssetsBulkUpdateCommandMiddleware> log) - { - this.contextProvider = contextProvider; + public AssetsBulkUpdateCommandMiddleware(IContextProvider contextProvider, ILogger<AssetsBulkUpdateCommandMiddleware> log) + { + this.contextProvider = contextProvider; - this.log = log; - } + this.log = log; + } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is BulkUpdateAssets bulkUpdates) { - if (context.Command is BulkUpdateAssets bulkUpdates) + if (bulkUpdates.Jobs?.Length > 0) { - if (bulkUpdates.Jobs?.Length > 0) + var executionOptions = new ExecutionDataflowBlockOptions { - var executionOptions = new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) - }; + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) + }; - // Each job can create exactly one command. - var createCommandsBlock = new TransformBlock<BulkTask, BulkTaskCommand?>(task => + // Each job can create exactly one command. + var createCommandsBlock = new TransformBlock<BulkTask, BulkTaskCommand?>(task => + { + try { - try - { - return CreateCommand(task); - } - catch (OperationCanceledException ex) - { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); - } - }, executionOptions); + return CreateCommand(task); + } + catch (OperationCanceledException ex) + { + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); + } + }, executionOptions); - // Execute the commands in batches - var executeCommandBlock = new ActionBlock<BulkTaskCommand?>(async command => + // Execute the commands in batches + var executeCommandBlock = new ActionBlock<BulkTaskCommand?>(async command => + { + try { - try - { - if (command != null) - { - await ExecuteCommandAsync(command); - } - } - catch (OperationCanceledException ex) + if (command != null) { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); + await ExecuteCommandAsync(command); } - }, executionOptions); + } + catch (OperationCanceledException ex) + { + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); + } + }, executionOptions); - createCommandsBlock.BidirectionalLinkTo(executeCommandBlock); + createCommandsBlock.BidirectionalLinkTo(executeCommandBlock); - contextProvider.Context.Change(b => b - .WithoutAssetEnrichment() - .WithoutCleanup() - .WithUnpublished(true) - .WithoutTotal()); + contextProvider.Context.Change(b => b + .WithoutAssetEnrichment() + .WithoutCleanup() + .WithUnpublished(true) + .WithoutTotal()); - var results = new ConcurrentBag<BulkUpdateResultItem>(); + var results = new ConcurrentBag<BulkUpdateResultItem>(); - for (var i = 0; i < bulkUpdates.Jobs.Length; i++) + for (var i = 0; i < bulkUpdates.Jobs.Length; i++) + { + var task = new BulkTask( + context.CommandBus, + i, + bulkUpdates.Jobs[i], + bulkUpdates, + results, + ct); + + if (!await createCommandsBlock.SendAsync(task, ct)) { - var task = new BulkTask( - context.CommandBus, - i, - bulkUpdates.Jobs[i], - bulkUpdates, - results, - ct); - - if (!await createCommandsBlock.SendAsync(task, ct)) - { - break; - } + break; } + } - createCommandsBlock.Complete(); + createCommandsBlock.Complete(); - // Wait for all commands to be executed. - await executeCommandBlock.Completion; + // Wait for all commands to be executed. + await executeCommandBlock.Completion; - context.Complete(new BulkUpdateResult(results)); - } - else - { - context.Complete(new BulkUpdateResult()); - } + context.Complete(new BulkUpdateResult(results)); } else { - await next(context, ct); + context.Complete(new BulkUpdateResult()); } } + else + { + await next(context, ct); + } + } - private async Task ExecuteCommandAsync(BulkTaskCommand bulkCommand) + private async Task ExecuteCommandAsync(BulkTaskCommand bulkCommand) + { + var (task, id, command, ct) = bulkCommand; + try { - var (task, id, command, ct) = bulkCommand; - try - { - await task.Bus.PublishAsync(command, ct); + await task.Bus.PublishAsync(command, ct); - task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex)); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to execute asset bulk job with index {index} of type {type}.", - task.JobIndex, - task.CommandJob.Type); + task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex)); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to execute asset bulk job with index {index} of type {type}.", + task.JobIndex, + task.CommandJob.Type); - task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex, ex)); - } + task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex, ex)); } + } - private BulkTaskCommand? CreateCommand(BulkTask task) + private BulkTaskCommand? CreateCommand(BulkTask task) + { + var id = task.CommandJob.Id; + try { - var id = task.CommandJob.Id; - try - { - var command = CreateCommandCore(task); + var command = CreateCommandCore(task); - // Set the asset id here in case we have another way to resolve ids. - command.AssetId = id; + // Set the asset id here in case we have another way to resolve ids. + command.AssetId = id; - return new BulkTaskCommand(task, id, command, task.Aborted); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to execute asset bulk job with index {index} of type {type}.", - task.JobIndex, - task.CommandJob.Type); + return new BulkTaskCommand(task, id, command, task.Aborted); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to execute asset bulk job with index {index} of type {type}.", + task.JobIndex, + task.CommandJob.Type); - task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex, ex)); - return null; - } + task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex, ex)); + return null; } + } - private AssetCommand CreateCommandCore(BulkTask task) - { - var job = task.CommandJob; + private AssetCommand CreateCommandCore(BulkTask task) + { + var job = task.CommandJob; - switch (job.Type) - { - case BulkUpdateAssetType.Annotate: - { - var command = new AnnotateAsset(); + switch (job.Type) + { + case BulkUpdateAssetType.Annotate: + { + var command = new AnnotateAsset(); - EnrichAndCheckPermission(task, command, PermissionIds.AppAssetsUpdate); - return command; - } + EnrichAndCheckPermission(task, command, PermissionIds.AppAssetsUpdate); + return command; + } - case BulkUpdateAssetType.Move: - { - var command = new MoveAsset(); + case BulkUpdateAssetType.Move: + { + var command = new MoveAsset(); - EnrichAndCheckPermission(task, command, PermissionIds.AppAssetsUpdate); - return command; - } + EnrichAndCheckPermission(task, command, PermissionIds.AppAssetsUpdate); + return command; + } - case BulkUpdateAssetType.Delete: - { - var command = new DeleteAsset(); + case BulkUpdateAssetType.Delete: + { + var command = new DeleteAsset(); - EnrichAndCheckPermission(task, command, PermissionIds.AppAssetsDelete); - return command; - } + EnrichAndCheckPermission(task, command, PermissionIds.AppAssetsDelete); + return command; + } - default: - ThrowHelper.NotSupportedException(); - return default!; - } + default: + ThrowHelper.NotSupportedException(); + return default!; } + } - private void EnrichAndCheckPermission<T>(BulkTask task, T command, string permissionId) where T : AssetCommand - { - SimpleMapper.Map(task.Command, command); - SimpleMapper.Map(task.CommandJob, command); - - if (!contextProvider.Context.Allows(permissionId)) - { - throw new DomainForbiddenException("Forbidden"); - } + private void EnrichAndCheckPermission<T>(BulkTask task, T command, string permissionId) where T : AssetCommand + { + SimpleMapper.Map(task.Command, command); + SimpleMapper.Map(task.CommandJob, command); - command.ExpectedVersion = task.Command.ExpectedVersion; + if (!contextProvider.Context.Allows(permissionId)) + { + throw new DomainForbiddenException("Forbidden"); } + + command.ExpectedVersion = task.Command.ExpectedVersion; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs index f316b94094..e655347d58 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs @@ -10,116 +10,115 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; + +public sealed class ScriptMetadataWrapper : IDictionary<string, object?> { - public sealed class ScriptMetadataWrapper : IDictionary<string, object?> + private readonly AssetMetadata metadata; + + public int Count { - private readonly AssetMetadata metadata; + get => metadata.Count; + } - public int Count - { - get => metadata.Count; - } + public ICollection<string> Keys + { + get => metadata.Keys; + } - public ICollection<string> Keys - { - get => metadata.Keys; - } + public ICollection<object?> Values + { + get => metadata.Values.Cast<object?>().ToList(); + } - public ICollection<object?> Values - { - get => metadata.Values.Cast<object?>().ToList(); - } + public object? this[string key] + { + get => metadata[key]; + set => metadata[key] = JsonValue.Create(value); + } - public object? this[string key] - { - get => metadata[key]; - set => metadata[key] = JsonValue.Create(value); - } + public bool IsReadOnly + { + get => false; + } - public bool IsReadOnly - { - get => false; - } + public ScriptMetadataWrapper(AssetMetadata metadata) + { + this.metadata = metadata; + } - public ScriptMetadataWrapper(AssetMetadata metadata) + public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) + { + if (metadata.TryGetValue(key, out var temp)) { - this.metadata = metadata; + value = temp; + return true; } - - public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) + else { - if (metadata.TryGetValue(key, out var temp)) - { - value = temp; - return true; - } - else - { - value = null; - return false; - } + value = null; + return false; } + } - public void Add(string key, object? value) - { - metadata.Add(key, JsonValue.Create(value)); - } + public void Add(string key, object? value) + { + metadata.Add(key, JsonValue.Create(value)); + } - public void Add(KeyValuePair<string, object?> item) - { - Add(item.Key, item.Value); - } + public void Add(KeyValuePair<string, object?> item) + { + Add(item.Key, item.Value); + } - public bool Remove(string key) - { - return metadata.Remove(key); - } + public bool Remove(string key) + { + return metadata.Remove(key); + } - public bool Remove(KeyValuePair<string, object?> item) - { - return false; - } + public bool Remove(KeyValuePair<string, object?> item) + { + return false; + } - public void Clear() - { - metadata.Clear(); - } + public void Clear() + { + metadata.Clear(); + } - public bool Contains(KeyValuePair<string, object?> item) - { - return false; - } + public bool Contains(KeyValuePair<string, object?> item) + { + return false; + } - public bool ContainsKey(string key) - { - return metadata.ContainsKey(key); - } + public bool ContainsKey(string key) + { + return metadata.ContainsKey(key); + } - public void CopyTo(KeyValuePair<string, object?>[] array, int arrayIndex) - { - var i = arrayIndex; + public void CopyTo(KeyValuePair<string, object?>[] array, int arrayIndex) + { + var i = arrayIndex; - foreach (var item in metadata) + foreach (var item in metadata) + { + if (i >= array.Length) { - if (i >= array.Length) - { - break; - } - - array[i] = new KeyValuePair<string, object?>(item.Key, item.Value); - i++; + break; } - } - public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() - { - return metadata.Select(x => new KeyValuePair<string, object?>(x.Key, x.Value)).GetEnumerator(); + array[i] = new KeyValuePair<string, object?>(item.Key, item.Value); + i++; } + } - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)metadata).GetEnumerator(); - } + public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() + { + return metadata.Select(x => new KeyValuePair<string, object?>(x.Key, x.Value)).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)metadata).GetEnumerator(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs index dd40e608d1..d1332627c8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs @@ -10,196 +10,195 @@ using Squidex.Infrastructure; using Squidex.Text; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; + +public static class ScriptingExtensions { - public static class ScriptingExtensions + private static readonly ScriptOptions Options = new ScriptOptions { - private static readonly ScriptOptions Options = new ScriptOptions - { - AsContext = true, - CanDisallow = true, - CanReject = true - }; + AsContext = true, + CanDisallow = true, + CanReject = true + }; - public static async Task ExecuteCreateScriptAsync(this AssetOperation operation, CreateAsset create) + public static async Task ExecuteCreateScriptAsync(this AssetOperation operation, CreateAsset create) + { + var script = operation.App.AssetScripts?.Create; + + if (string.IsNullOrWhiteSpace(script)) { - var script = operation.App.AssetScripts?.Create; + return; + } + + var parentPath = await GetPathAsync(operation, create.ParentId); - if (string.IsNullOrWhiteSpace(script)) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new AssetScriptVars + { + // Tags and metadata are mutable and can be changed from the scripts, but not replaced. + Command = new AssetCommandScriptVars { - return; - } + FileHash = create.FileHash, + FileName = create.File.FileName, + FileSlug = create.File.FileName.Slugify(), + FileSize = create.File.FileSize, + Metadata = create.Metadata, + MimeType = create.File.MimeType, + ParentId = create.ParentId, + ParentPath = parentPath, + Tags = create.Tags + }, + Operation = "Create" + }; - var parentPath = await GetPathAsync(operation, create.ParentId); + await ExecuteScriptAsync(operation, script, vars); + } - // Script vars are just wrappers over dictionaries for better performance. - var vars = new AssetScriptVars - { - // Tags and metadata are mutable and can be changed from the scripts, but not replaced. - Command = new AssetCommandScriptVars - { - FileHash = create.FileHash, - FileName = create.File.FileName, - FileSlug = create.File.FileName.Slugify(), - FileSize = create.File.FileSize, - Metadata = create.Metadata, - MimeType = create.File.MimeType, - ParentId = create.ParentId, - ParentPath = parentPath, - Tags = create.Tags - }, - Operation = "Create" - }; - - await ExecuteScriptAsync(operation, script, vars); - } + public static Task ExecuteUpdateScriptAsync(this AssetOperation operation, UpdateAsset update) + { + var script = operation.App.AssetScripts?.Update; - public static Task ExecuteUpdateScriptAsync(this AssetOperation operation, UpdateAsset update) + if (string.IsNullOrWhiteSpace(script)) { - var script = operation.App.AssetScripts?.Update; + return Task.CompletedTask; + } - if (string.IsNullOrWhiteSpace(script)) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new AssetScriptVars + { + // Tags and metadata are mutable and can be changed from the scripts, but not replaced. + Command = new AssetCommandScriptVars { - return Task.CompletedTask; - } + Metadata = update.Metadata, + FileHash = update.FileHash, + FileName = update.File.FileName, + FileSize = update.File.FileSize, + MimeType = update.File.MimeType, + Tags = update.Tags + }, + Operation = "Update" + }; - // Script vars are just wrappers over dictionaries for better performance. - var vars = new AssetScriptVars - { - // Tags and metadata are mutable and can be changed from the scripts, but not replaced. - Command = new AssetCommandScriptVars - { - Metadata = update.Metadata, - FileHash = update.FileHash, - FileName = update.File.FileName, - FileSize = update.File.FileSize, - MimeType = update.File.MimeType, - Tags = update.Tags - }, - Operation = "Update" - }; - - return ExecuteScriptAsync(operation, script, vars); - } + return ExecuteScriptAsync(operation, script, vars); + } + + public static Task ExecuteAnnotateScriptAsync(this AssetOperation operation, AnnotateAsset annotate) + { + var script = operation.App.AssetScripts?.Annotate; - public static Task ExecuteAnnotateScriptAsync(this AssetOperation operation, AnnotateAsset annotate) + if (string.IsNullOrWhiteSpace(script)) { - var script = operation.App.AssetScripts?.Annotate; + return Task.CompletedTask; + } - if (string.IsNullOrWhiteSpace(script)) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new AssetScriptVars + { + // Tags are mutable and can be changed from the scripts, but not replaced. + Command = new AssetCommandScriptVars { - return Task.CompletedTask; - } + IsProtected = annotate.IsProtected, + Metadata = annotate.Metadata, + FileName = annotate.FileName, + FileSlug = annotate.Slug, + Tags = annotate.Tags + }, + Operation = "Annotate" + }; - // Script vars are just wrappers over dictionaries for better performance. - var vars = new AssetScriptVars - { - // Tags are mutable and can be changed from the scripts, but not replaced. - Command = new AssetCommandScriptVars - { - IsProtected = annotate.IsProtected, - Metadata = annotate.Metadata, - FileName = annotate.FileName, - FileSlug = annotate.Slug, - Tags = annotate.Tags - }, - Operation = "Annotate" - }; - - return ExecuteScriptAsync(operation, script, vars); - } + return ExecuteScriptAsync(operation, script, vars); + } - public static async Task ExecuteMoveScriptAsync(this AssetOperation operation, MoveAsset move) + public static async Task ExecuteMoveScriptAsync(this AssetOperation operation, MoveAsset move) + { + var script = operation.App.AssetScripts?.Move; + + if (string.IsNullOrWhiteSpace(script)) { - var script = operation.App.AssetScripts?.Move; + return; + } + + var parentPath = await GetPathAsync(operation, move.ParentId); - if (string.IsNullOrWhiteSpace(script)) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new AssetScriptVars + { + Command = new AssetCommandScriptVars { - return; - } + ParentId = move.ParentId, + ParentPath = parentPath + }, + Operation = "Move" + }; + + await ExecuteScriptAsync(operation, script, vars); + } - var parentPath = await GetPathAsync(operation, move.ParentId); + public static Task ExecuteDeleteScriptAsync(this AssetOperation operation, DeleteAsset delete) + { + var script = operation.App.AssetScripts?.Delete; - // Script vars are just wrappers over dictionaries for better performance. - var vars = new AssetScriptVars - { - Command = new AssetCommandScriptVars - { - ParentId = move.ParentId, - ParentPath = parentPath - }, - Operation = "Move" - }; - - await ExecuteScriptAsync(operation, script, vars); + if (string.IsNullOrWhiteSpace(script)) + { + return Task.CompletedTask; } - public static Task ExecuteDeleteScriptAsync(this AssetOperation operation, DeleteAsset delete) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new AssetScriptVars { - var script = operation.App.AssetScripts?.Delete; - - if (string.IsNullOrWhiteSpace(script)) + Command = new AssetCommandScriptVars { - return Task.CompletedTask; - } + Permanent = delete.Permanent + }, + Operation = "Delete" + }; - // Script vars are just wrappers over dictionaries for better performance. - var vars = new AssetScriptVars - { - Command = new AssetCommandScriptVars - { - Permanent = delete.Permanent - }, - Operation = "Delete" - }; - - return ExecuteScriptAsync(operation, script, vars); - } + return ExecuteScriptAsync(operation, script, vars); + } - private static async Task ExecuteScriptAsync(AssetOperation operation, string script, AssetScriptVars vars) - { - var snapshot = operation.Snapshot; + private static async Task ExecuteScriptAsync(AssetOperation operation, string script, AssetScriptVars vars) + { + var snapshot = operation.Snapshot; - var parentPath = await GetPathAsync(operation, snapshot.ParentId); + var parentPath = await GetPathAsync(operation, snapshot.ParentId); - // Script vars are just wrappers over dictionaries for better performance. - var asset = new AssetEntityScriptVars - { - Metadata = snapshot.Metadata, - FileHash = snapshot.FileHash, - FileName = snapshot.FileName, - FileSize = snapshot.FileSize, - FileSlug = snapshot.Slug, - FileVersion = snapshot.FileVersion, - IsProtected = snapshot.IsProtected, - MimeType = snapshot.MimeType, - ParentId = snapshot.ParentId, - ParentPath = parentPath, - Tags = snapshot.Tags - }; + // Script vars are just wrappers over dictionaries for better performance. + var asset = new AssetEntityScriptVars + { + Metadata = snapshot.Metadata, + FileHash = snapshot.FileHash, + FileName = snapshot.FileName, + FileSize = snapshot.FileSize, + FileSlug = snapshot.Slug, + FileVersion = snapshot.FileVersion, + IsProtected = snapshot.IsProtected, + MimeType = snapshot.MimeType, + ParentId = snapshot.ParentId, + ParentPath = parentPath, + Tags = snapshot.Tags + }; - vars.AppId = operation.App.Id; - vars.AppName = operation.App.Name; - vars.AssetId = operation.CommandId; - vars.Asset = asset; - vars.User = operation.User; + vars.AppId = operation.App.Id; + vars.AppName = operation.App.Name; + vars.AssetId = operation.CommandId; + vars.Asset = asset; + vars.User = operation.User; - var scriptEngine = operation.Resolve<IScriptEngine>(); + var scriptEngine = operation.Resolve<IScriptEngine>(); - await scriptEngine.ExecuteAsync(vars, script, Options); - } + await scriptEngine.ExecuteAsync(vars, script, Options); + } - private static async Task<Array> GetPathAsync(AssetOperation operation, DomainId parentId) + private static async Task<Array> GetPathAsync(AssetOperation operation, DomainId parentId) + { + if (parentId == default) { - if (parentId == default) - { - return Array.Empty<object>(); - } + return Array.Empty<object>(); + } - var assetQuery = operation.Resolve<IAssetQueryService>(); - var assetPath = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId); + var assetQuery = operation.Resolve<IAssetQueryService>(); + var assetPath = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId); - return assetPath.Select(x => new { id = x.Id, folderName = x.FolderName }).ToArray(); - } + return assetPath.Select(x => new { id = x.Id, folderName = x.FolderName }).ToArray(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/TagsExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/TagsExtensions.cs index 7ddc2abc2f..a7d5ba1330 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/TagsExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/TagsExtensions.cs @@ -8,24 +8,23 @@ using Squidex.Domain.Apps.Core.Tags; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; + +public static class TagsExtensions { - public static class TagsExtensions + public static async Task<HashSet<string>> GetTagIdsAsync(this AssetOperation operation, HashSet<string>? names) { - public static async Task<HashSet<string>> GetTagIdsAsync(this AssetOperation operation, HashSet<string>? names) - { - var result = new HashSet<string>(names?.Count ?? 0); - - if (names != null) - { - var tagService = operation.Resolve<ITagService>(); + var result = new HashSet<string>(names?.Count ?? 0); - var normalized = await tagService.GetTagIdsAsync(operation.App.Id, TagGroups.Assets, names); + if (names != null) + { + var tagService = operation.Resolve<ITagService>(); - result.AddRange(normalized.Values); - } + var normalized = await tagService.GetTagIdsAsync(operation.App.Id, TagGroups.Assets, names); - return result; + result.AddRange(normalized.Values); } + + return result; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ValidationExtensions.cs index 86c2a455dc..a5f6d76d5a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ValidationExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ValidationExtensions.cs @@ -12,81 +12,80 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; + +public static class ValidationExtensions { - public static class ValidationExtensions + public static void MustHaveName(this AssetFolderOperation operation, string folderName) { - public static void MustHaveName(this AssetFolderOperation operation, string folderName) + if (string.IsNullOrWhiteSpace(folderName)) { - if (string.IsNullOrWhiteSpace(folderName)) - { - operation.AddError(Not.Defined(nameof(folderName)), "FolderName"); - } - - operation.ThrowOnErrors(); + operation.AddError(Not.Defined(nameof(folderName)), "FolderName"); } - public static async Task MustMoveToValidFolder(this AssetOperation operation, DomainId parentId) - { - // If moved to root folder or not moved at all, we can just skip the validation. - if (parentId == DomainId.Empty || parentId == operation.Snapshot.ParentId) - { - return; - } + operation.ThrowOnErrors(); + } - var assetQuery = operation.Resolve<IAssetQueryService>(); + public static async Task MustMoveToValidFolder(this AssetOperation operation, DomainId parentId) + { + // If moved to root folder or not moved at all, we can just skip the validation. + if (parentId == DomainId.Empty || parentId == operation.Snapshot.ParentId) + { + return; + } - var path = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId); + var assetQuery = operation.Resolve<IAssetQueryService>(); - if (path.Count == 0) - { - operation.AddError(T.Get("assets.folderNotFound"), nameof(MoveAsset.ParentId)).ThrowOnErrors(); - } + var path = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId); - operation.ThrowOnErrors(); + if (path.Count == 0) + { + operation.AddError(T.Get("assets.folderNotFound"), nameof(MoveAsset.ParentId)).ThrowOnErrors(); } - public static async Task MustMoveToValidFolder(this AssetFolderOperation operation, DomainId parentId) + operation.ThrowOnErrors(); + } + + public static async Task MustMoveToValidFolder(this AssetFolderOperation operation, DomainId parentId) + { + // If moved to root folder or not moved at all, we can just skip the validation. + if (parentId == DomainId.Empty || parentId == operation.Snapshot.ParentId) { - // If moved to root folder or not moved at all, we can just skip the validation. - if (parentId == DomainId.Empty || parentId == operation.Snapshot.ParentId) - { - return; - } + return; + } - var assetQuery = operation.Resolve<IAssetQueryService>(); + var assetQuery = operation.Resolve<IAssetQueryService>(); - var path = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId); + var path = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId); - if (path.Count == 0) - { - operation.AddError(T.Get("assets.folderNotFound"), nameof(MoveAssetFolder.ParentId)); - } - else if (operation.CommandId != DomainId.Empty) + if (path.Count == 0) + { + operation.AddError(T.Get("assets.folderNotFound"), nameof(MoveAssetFolder.ParentId)); + } + else if (operation.CommandId != DomainId.Empty) + { + var indexOfSelf = path.IndexOf(x => x.Id == operation.CommandId); + var indexOfParent = path.IndexOf(x => x.Id == parentId); + + // If we would move the folder to its own parent (the parent comes first in the path), we would create a recursion. + if (indexOfSelf >= 0 && indexOfParent > indexOfSelf) { - var indexOfSelf = path.IndexOf(x => x.Id == operation.CommandId); - var indexOfParent = path.IndexOf(x => x.Id == parentId); - - // If we would move the folder to its own parent (the parent comes first in the path), we would create a recursion. - if (indexOfSelf >= 0 && indexOfParent > indexOfSelf) - { - operation.AddError(T.Get("assets.folderRecursion"), nameof(MoveAssetFolder.ParentId)); - } + operation.AddError(T.Get("assets.folderRecursion"), nameof(MoveAssetFolder.ParentId)); } - - operation.ThrowOnErrors(); } - public static async Task CheckReferrersAsync(this AssetOperation operation) - { - var contentRepository = operation.Resolve<IContentRepository>(); + operation.ThrowOnErrors(); + } + + public static async Task CheckReferrersAsync(this AssetOperation operation) + { + var contentRepository = operation.Resolve<IContentRepository>(); - var hasReferrer = await contentRepository.HasReferrersAsync(operation.App.Id, operation.CommandId, SearchScope.All, default); + var hasReferrer = await contentRepository.HasReferrersAsync(operation.App.Id, operation.CommandId, SearchScope.All, default); - if (hasReferrer) - { - throw new DomainException(T.Get("assets.referenced"), "OBJECT_REFERENCED"); - } + if (hasReferrer) + { + throw new DomainException(T.Get("assets.referenced"), "OBJECT_REFERENCED"); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs index a7ad91b971..0be7f4ae3c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs @@ -13,161 +13,160 @@ using TagLib.Image; using static TagLib.File; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class FileTagAssetMetadataSource : IAssetMetadataSource { - public sealed class FileTagAssetMetadataSource : IAssetMetadataSource + private sealed class FileAbstraction : IFileAbstraction { - private sealed class FileAbstraction : IFileAbstraction - { - private readonly AssetFile file; + private readonly AssetFile file; - public string Name - { - get => file.FileName; - } + public string Name + { + get => file.FileName; + } - public Stream ReadStream - { - get => file.OpenRead(); - } + public Stream ReadStream + { + get => file.OpenRead(); + } - public Stream WriteStream - { - get => throw new NotSupportedException(); - } + public Stream WriteStream + { + get => throw new NotSupportedException(); + } - public FileAbstraction(AssetFile file) - { - this.file = file; - } + public FileAbstraction(AssetFile file) + { + this.file = file; + } - public void CloseStream(Stream stream) - { - stream.Close(); - } + public void CloseStream(Stream stream) + { + stream.Close(); } + } - public Task EnhanceAsync(UploadAssetCommand command, - CancellationToken ct) + public Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) + { + try { - try + using (var file = Create(new FileAbstraction(command.File), ReadStyle.Average)) { - using (var file = Create(new FileAbstraction(command.File), ReadStyle.Average)) + if (file.Properties == null) { - if (file.Properties == null) - { - return Task.CompletedTask; - } + return Task.CompletedTask; + } - var type = file.Properties.MediaTypes; + var type = file.Properties.MediaTypes; - if (type == MediaTypes.Audio) - { - command.Type = AssetType.Audio; - } - else if (type == MediaTypes.Photo) - { - command.Type = AssetType.Image; - } - else if (type.HasFlag(MediaTypes.Video)) - { - command.Type = AssetType.Video; - } + if (type == MediaTypes.Audio) + { + command.Type = AssetType.Audio; + } + else if (type == MediaTypes.Photo) + { + command.Type = AssetType.Image; + } + else if (type.HasFlag(MediaTypes.Video)) + { + command.Type = AssetType.Video; + } - var pw = file.Properties.PhotoWidth; - var ph = file.Properties.PhotoHeight; + var pw = file.Properties.PhotoWidth; + var ph = file.Properties.PhotoHeight; - if (pw > 0 && ph > 0) - { - command.Metadata.SetPixelWidth(pw); - command.Metadata.SetPixelHeight(ph); - } + if (pw > 0 && ph > 0) + { + command.Metadata.SetPixelWidth(pw); + command.Metadata.SetPixelHeight(ph); + } - void TryAddString(string name, string? value) + void TryAddString(string name, string? value) + { + if (!string.IsNullOrWhiteSpace(value)) { - if (!string.IsNullOrWhiteSpace(value)) - { - command.Metadata.Add(name, value); - } + command.Metadata.Add(name, value); } + } - void TryAddInt(string name, int? value) + void TryAddInt(string name, int? value) + { + if (value > 0) { - if (value > 0) - { - command.Metadata.Add(name, (double)value.Value); - } + command.Metadata.Add(name, (double)value.Value); } + } - void TryAddDouble(string name, double? value) + void TryAddDouble(string name, double? value) + { + if (value > 0) { - if (value > 0) - { - command.Metadata.Add(name, value.Value); - } + command.Metadata.Add(name, value.Value); } + } - void TryAddTimeSpan(string name, TimeSpan value) + void TryAddTimeSpan(string name, TimeSpan value) + { + if (value != TimeSpan.Zero) { - if (value != TimeSpan.Zero) - { - command.Metadata.Add(name, value.ToString()); - } + command.Metadata.Add(name, value.ToString()); } + } - if (file.Tag is ImageTag imageTag) - { - TryAddDouble("latitude", imageTag.Latitude); - TryAddDouble("longitude", imageTag.Longitude); - - TryAddString("created", imageTag.DateTime?.ToIso8601()); - } + if (file.Tag is ImageTag imageTag) + { + TryAddDouble("latitude", imageTag.Latitude); + TryAddDouble("longitude", imageTag.Longitude); - TryAddTimeSpan("duration", file.Properties.Duration); + TryAddString("created", imageTag.DateTime?.ToIso8601()); + } - TryAddInt("bitsPerSample", file.Properties.BitsPerSample); - TryAddInt("audioBitrate", file.Properties.AudioBitrate); - TryAddInt("audioChannels", file.Properties.AudioChannels); - TryAddInt("audioSampleRate", file.Properties.AudioSampleRate); - TryAddInt("imageQuality", file.Properties.PhotoQuality); + TryAddTimeSpan("duration", file.Properties.Duration); - TryAddInt(AssetMetadata.VideoWidth, file.Properties.VideoWidth); - TryAddInt(AssetMetadata.VideoHeight, file.Properties.VideoHeight); + TryAddInt("bitsPerSample", file.Properties.BitsPerSample); + TryAddInt("audioBitrate", file.Properties.AudioBitrate); + TryAddInt("audioChannels", file.Properties.AudioChannels); + TryAddInt("audioSampleRate", file.Properties.AudioSampleRate); + TryAddInt("imageQuality", file.Properties.PhotoQuality); - TryAddString("description", file.Properties.Description); - } + TryAddInt(AssetMetadata.VideoWidth, file.Properties.VideoWidth); + TryAddInt(AssetMetadata.VideoHeight, file.Properties.VideoHeight); - return Task.CompletedTask; - } - catch - { - return Task.CompletedTask; + TryAddString("description", file.Properties.Description); } + + return Task.CompletedTask; } + catch + { + return Task.CompletedTask; + } + } - public IEnumerable<string> Format(IAssetEntity asset) + public IEnumerable<string> Format(IAssetEntity asset) + { + if (asset.Type == AssetType.Video) { - if (asset.Type == AssetType.Video) - { - var w = asset.Metadata.GetVideoWidth(); - var h = asset.Metadata.GetVideoHeight(); + var w = asset.Metadata.GetVideoWidth(); + var h = asset.Metadata.GetVideoHeight(); - if (w != null && h != null) - { - yield return $"{w}x{h}pt"; - } + if (w != null && h != null) + { + yield return $"{w}x{h}pt"; + } - if (asset.Metadata.TryGetString("duration", out var duration)) - { - yield return duration; - } + if (asset.Metadata.TryGetString("duration", out var duration)) + { + yield return duration; } - else if (asset.Type == AssetType.Audio) + } + else if (asset.Type == AssetType.Audio) + { + if (asset.Metadata.TryGetString("duration", out var duration)) { - if (asset.Metadata.TryGetString("duration", out var duration)) - { - yield return duration; - } + yield return duration; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs index ba96d654d6..9d5a3709e3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs @@ -8,29 +8,28 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class FileTypeAssetMetadataSource : IAssetMetadataSource { - public sealed class FileTypeAssetMetadataSource : IAssetMetadataSource + public Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) { - public Task EnhanceAsync(UploadAssetCommand command, - CancellationToken ct) + if (command.Tags != null) { - if (command.Tags != null) - { - var extension = command.File?.FileName?.FileType(); + var extension = command.File?.FileName?.FileType(); - if (!string.IsNullOrWhiteSpace(extension)) - { - command.Tags.Add($"type/{extension.ToLowerInvariant()}"); - } + if (!string.IsNullOrWhiteSpace(extension)) + { + command.Tags.Add($"type/{extension.ToLowerInvariant()}"); } - - return Task.CompletedTask; } - public IEnumerable<string> Format(IAssetEntity asset) - { - yield break; - } + return Task.CompletedTask; + } + + public IEnumerable<string> Format(IAssetEntity asset) + { + yield break; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCache.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCache.cs index b3875376f1..f1ff762b26 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCache.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCache.cs @@ -8,9 +8,8 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetCache : IQueryCache<DomainId, IEnrichedAssetEntity> { - public interface IAssetCache : IQueryCache<DomainId, IEnrichedAssetEntity> - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs index 6aac50ca0d..14c9fc4401 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetEnricher { - public interface IAssetEnricher - { - Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context, - CancellationToken ct); + Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context, + CancellationToken ct); - Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets, Context context, - CancellationToken ct); - } + Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets, Context context, + CancellationToken ct); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs index 90eaa41858..d24b993871 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs @@ -8,22 +8,21 @@ using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion, + IEntityWithTags, + IAssetInfo { - public interface IAssetEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion, - IEntityWithTags, - IAssetInfo - { - NamedId<DomainId> AppId { get; } + NamedId<DomainId> AppId { get; } - DomainId ParentId { get; } + DomainId ParentId { get; } - bool IsProtected { get; } + bool IsProtected { get; } - long FileVersion { get; } - } + long FileVersion { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs index 1c02e015f6..94110ce187 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs @@ -8,31 +8,30 @@ using Squidex.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetFileStore { - public interface IAssetFileStore - { - string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix); + string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix); - Task<long> GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, - CancellationToken ct = default); + Task<long> GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, + CancellationToken ct = default); - Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, - CancellationToken ct = default); + Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, + CancellationToken ct = default); - Task UploadAsync(string tempFile, Stream stream, - CancellationToken ct = default); + Task UploadAsync(string tempFile, Stream stream, + CancellationToken ct = default); - Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, - CancellationToken ct = default); + Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, + CancellationToken ct = default); - Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, - CancellationToken ct = default); + Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, + CancellationToken ct = default); - Task DeleteAsync(string tempFile, - CancellationToken ct = default); + Task DeleteAsync(string tempFile, + CancellationToken ct = default); - Task DeleteAsync(DomainId appId, DomainId id, - CancellationToken ct = default); - } + Task DeleteAsync(DomainId appId, DomainId id, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs index 2bd9573b7f..b504339484 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetFolderEntity : IEntity, IEntityWithVersion { - public interface IAssetFolderEntity : IEntity, IEntityWithVersion - { - NamedId<DomainId> AppId { get; set; } + NamedId<DomainId> AppId { get; set; } - string FolderName { get; set; } + string FolderName { get; set; } - DomainId ParentId { get; set; } - } + DomainId ParentId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs index 0de3c1df92..6db149abb0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetLoader { - public interface IAssetLoader - { - Task<IAssetEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default); - } + Task<IAssetEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs index 94982fd2a7..137009737a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs @@ -7,15 +7,14 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetMetadataSource { - public interface IAssetMetadataSource - { - int Order => 0; + int Order => 0; - Task EnhanceAsync(UploadAssetCommand command, - CancellationToken ct); + Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct); - IEnumerable<string> Format(IAssetEntity asset); - } + IEnumerable<string> Format(IAssetEntity asset); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index 21a4caf06b..b0337b00b1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -7,32 +7,31 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetQueryService { - public interface IAssetQueryService - { - Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q q, - CancellationToken ct = default); + Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q q, + CancellationToken ct = default); - Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId, - CancellationToken ct = default); + Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId, + CancellationToken ct = default); - Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(DomainId appId, DomainId parentId, - CancellationToken ct = default); + Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default); - Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(DomainId appId, DomainId id, - CancellationToken ct = default); + Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(DomainId appId, DomainId id, + CancellationToken ct = default); - Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize, - CancellationToken ct = default); + Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize, + CancellationToken ct = default); - Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default); + Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); - Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug, - CancellationToken ct = default); + Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug, + CancellationToken ct = default); - Task<IEnrichedAssetEntity?> FindGlobalAsync(Context context, DomainId id, - CancellationToken ct = default); - } + Task<IEnrichedAssetEntity?> FindGlobalAsync(Context context, DomainId id, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs index 56bd09cb57..bc95ca97ee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetUsageTracker.cs @@ -7,20 +7,19 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IAssetUsageTracker { - public interface IAssetUsageTracker - { - Task<IReadOnlyList<AssetStats>> QueryByAppAsync(DomainId appId, DateTime fromDate, DateTime toDate, - CancellationToken ct = default); + Task<IReadOnlyList<AssetStats>> QueryByAppAsync(DomainId appId, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); - Task<IReadOnlyList<AssetStats>> QueryByTeamAsync(DomainId teamId, DateTime fromDate, DateTime toDate, - CancellationToken ct = default); + Task<IReadOnlyList<AssetStats>> QueryByTeamAsync(DomainId teamId, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); - Task<long> GetTotalSizeByAppAsync(DomainId appId, - CancellationToken ct = default); + Task<long> GetTotalSizeByAppAsync(DomainId appId, + CancellationToken ct = default); - Task<long> GetTotalSizeByTeamAsync(DomainId teamId, - CancellationToken ct = default); - } + Task<long> GetTotalSizeByTeamAsync(DomainId teamId, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs index be336a5343..da70c0dba8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public interface IEnrichedAssetEntity : IAssetEntity { - public interface IEnrichedAssetEntity : IAssetEntity - { - HashSet<string> TagNames { get; } + HashSet<string> TagNames { get; } - string MetadataText { get; } + string MetadataText { get; } - string? EditToken { get; set; } - } + string? EditToken { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index c5497c756c..ec66d886d4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -9,104 +9,103 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class ImageAssetMetadataSource : IAssetMetadataSource { - public sealed class ImageAssetMetadataSource : IAssetMetadataSource + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + + public ImageAssetMetadataSource(IAssetThumbnailGenerator assetThumbnailGenerator) { - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } - public ImageAssetMetadataSource(IAssetThumbnailGenerator assetThumbnailGenerator) + public async Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) + { + if (command.Type is AssetType.Unknown or AssetType.Image) { - this.assetThumbnailGenerator = assetThumbnailGenerator; - } + var mimeType = command.File.MimeType; - public async Task EnhanceAsync(UploadAssetCommand command, - CancellationToken ct) - { - if (command.Type is AssetType.Unknown or AssetType.Image) - { - var mimeType = command.File.MimeType; + ImageInfo? imageInfo; - ImageInfo? imageInfo; + await using (var uploadStream = command.File.OpenRead()) + { + imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, mimeType, ct); + } - await using (var uploadStream = command.File.OpenRead()) - { - imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, mimeType, ct); - } + if (imageInfo != null) + { + var isSwapped = imageInfo.Orientation > ImageOrientation.TopLeft; - if (imageInfo != null) + if (command.File != null && isSwapped) { - var isSwapped = imageInfo.Orientation > ImageOrientation.TopLeft; + var tempFile = TempAssetFile.Create(command.File); - if (command.File != null && isSwapped) + await using (var uploadStream = command.File.OpenRead()) { - var tempFile = TempAssetFile.Create(command.File); - - await using (var uploadStream = command.File.OpenRead()) + await using (var tempStream = tempFile.OpenWrite()) { - await using (var tempStream = tempFile.OpenWrite()) - { - await assetThumbnailGenerator.FixOrientationAsync(uploadStream, mimeType, tempStream, ct); - } + await assetThumbnailGenerator.FixOrientationAsync(uploadStream, mimeType, tempStream, ct); } + } - await using (var tempStream = tempFile.OpenRead()) - { - imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(tempStream, mimeType, ct) ?? imageInfo; - } + await using (var tempStream = tempFile.OpenRead()) + { + imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(tempStream, mimeType, ct) ?? imageInfo; + } - await command.File.DisposeAsync(); + await command.File.DisposeAsync(); - command.File = tempFile; - } + command.File = tempFile; + } - if (command.Type == AssetType.Unknown || isSwapped) - { - command.Type = AssetType.Image; + if (command.Type == AssetType.Unknown || isSwapped) + { + command.Type = AssetType.Image; - command.Metadata.SetPixelWidth(imageInfo.PixelWidth); - command.Metadata.SetPixelHeight(imageInfo.PixelHeight); - } + command.Metadata.SetPixelWidth(imageInfo.PixelWidth); + command.Metadata.SetPixelHeight(imageInfo.PixelHeight); } } + } - if (command.Tags == null) - { - return; - } + if (command.Tags == null) + { + return; + } - if (command.Type == AssetType.Image) - { - command.Tags.Add("image"); + if (command.Type == AssetType.Image) + { + command.Tags.Add("image"); - var wh = command.Metadata.GetPixelWidth() + command.Metadata.GetPixelWidth(); + var wh = command.Metadata.GetPixelWidth() + command.Metadata.GetPixelWidth(); - if (wh > 2000) - { - command.Tags.Add("image/large"); - } - else if (wh > 1000) - { - command.Tags.Add("image/medium"); - } - else - { - command.Tags.Add("image/small"); - } + if (wh > 2000) + { + command.Tags.Add("image/large"); + } + else if (wh > 1000) + { + command.Tags.Add("image/medium"); + } + else + { + command.Tags.Add("image/small"); } } + } - public IEnumerable<string> Format(IAssetEntity asset) + public IEnumerable<string> Format(IAssetEntity asset) + { + if (asset.Type == AssetType.Image) { - if (asset.Type == AssetType.Image) - { - var w = asset.Metadata.GetPixelWidth(); - var h = asset.Metadata.GetPixelHeight(); + var w = asset.Metadata.GetPixelWidth(); + var h = asset.Metadata.GetPixelHeight(); - if (w != null && h != null) - { - yield return $"{w}x{h}px"; - } + if (w != null && h != null) + { + yield return $"{w}x{h}px"; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs index 4ed662d1a7..d64b5871ee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs @@ -13,148 +13,147 @@ using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +public sealed class AssetEnricher : IAssetEnricher { - public sealed class AssetEnricher : IAssetEnricher + private readonly ITagService tagService; + private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources; + private readonly IRequestCache requestCache; + private readonly IUrlGenerator urlGenerator; + private readonly IJsonSerializer serializer; + + public AssetEnricher(ITagService tagService, IEnumerable<IAssetMetadataSource> assetMetadataSources, IRequestCache requestCache, + IUrlGenerator urlGenerator, IJsonSerializer serializer) { - private readonly ITagService tagService; - private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources; - private readonly IRequestCache requestCache; - private readonly IUrlGenerator urlGenerator; - private readonly IJsonSerializer serializer; - - public AssetEnricher(ITagService tagService, IEnumerable<IAssetMetadataSource> assetMetadataSources, IRequestCache requestCache, - IUrlGenerator urlGenerator, IJsonSerializer serializer) - { - this.tagService = tagService; - this.assetMetadataSources = assetMetadataSources; - this.requestCache = requestCache; - this.urlGenerator = urlGenerator; - this.serializer = serializer; - } + this.tagService = tagService; + this.assetMetadataSources = assetMetadataSources; + this.requestCache = requestCache; + this.urlGenerator = urlGenerator; + this.serializer = serializer; + } - public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context, - CancellationToken ct) - { - Guard.NotNull(asset); - Guard.NotNull(context); + public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context, + CancellationToken ct) + { + Guard.NotNull(asset); + Guard.NotNull(context); - var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1), context, ct); + var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1), context, ct); - return enriched[0]; - } + return enriched[0]; + } + + public async Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets, Context context, + CancellationToken ct) + { + Guard.NotNull(assets); + Guard.NotNull(context); - public async Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets, Context context, - CancellationToken ct) + using (Telemetry.Activities.StartActivity("AssetEnricher/EnrichAsync")) { - Guard.NotNull(assets); - Guard.NotNull(context); + var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList(); - using (Telemetry.Activities.StartActivity("AssetEnricher/EnrichAsync")) + foreach (var asset in results) { - var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList(); - - foreach (var asset in results) - { - requestCache.AddDependency(asset.UniqueId, asset.Version); - } - - if (!context.ShouldSkipAssetEnrichment()) - { - await EnrichTagsAsync(results, ct); + requestCache.AddDependency(asset.UniqueId, asset.Version); + } - EnrichWithMetadataText(results); - EnrichWithEditTokens(results); - } + if (!context.ShouldSkipAssetEnrichment()) + { + await EnrichTagsAsync(results, ct); - return results; + EnrichWithMetadataText(results); + EnrichWithEditTokens(results); } + + return results; } + } - private void EnrichWithEditTokens(IEnumerable<IEnrichedAssetEntity> assets) - { - var url = urlGenerator.Root(); + private void EnrichWithEditTokens(IEnumerable<IEnrichedAssetEntity> assets) + { + var url = urlGenerator.Root(); - foreach (var asset in assets) + foreach (var asset in assets) + { + // We have to use these short names here because they are later read like this. + var token = new { - // We have to use these short names here because they are later read like this. - var token = new - { - a = asset.AppId.Name, - i = asset.Id.ToString(), - u = url - }; + a = asset.AppId.Name, + i = asset.Id.ToString(), + u = url + }; - var json = serializer.Serialize(token); + var json = serializer.Serialize(token); - asset.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); - } + asset.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); } + } - private void EnrichWithMetadataText(List<AssetEntity> results) - { - var sb = new StringBuilder(); + private void EnrichWithMetadataText(List<AssetEntity> results) + { + var sb = new StringBuilder(); - void Append(string? text) + void Append(string? text) + { + if (!string.IsNullOrWhiteSpace(text)) { - if (!string.IsNullOrWhiteSpace(text)) - { - sb.AppendIfNotEmpty(", "); - sb.Append(text); - } + sb.AppendIfNotEmpty(", "); + sb.Append(text); } + } - foreach (var asset in results) - { - sb.Clear(); + foreach (var asset in results) + { + sb.Clear(); - foreach (var source in assetMetadataSources) + foreach (var source in assetMetadataSources) + { + foreach (var metadata in source.Format(asset)) { - foreach (var metadata in source.Format(asset)) - { - Append(metadata); - } + Append(metadata); } + } - Append(asset.FileSize.ToReadableSize()); + Append(asset.FileSize.ToReadableSize()); - asset.MetadataText = sb.ToString(); - } + asset.MetadataText = sb.ToString(); } + } - private async Task EnrichTagsAsync(List<AssetEntity> assets, - CancellationToken ct) + private async Task EnrichTagsAsync(List<AssetEntity> assets, + CancellationToken ct) + { + foreach (var group in assets.GroupBy(x => x.AppId.Id)) { - foreach (var group in assets.GroupBy(x => x.AppId.Id)) - { - ct.ThrowIfCancellationRequested(); + ct.ThrowIfCancellationRequested(); - var tagsById = await CalculateTagsAsync(group, ct); + var tagsById = await CalculateTagsAsync(group, ct); - foreach (var asset in group) - { - asset.TagNames = new HashSet<string>(); + foreach (var asset in group) + { + asset.TagNames = new HashSet<string>(); - if (asset.Tags != null) + if (asset.Tags != null) + { + foreach (var id in asset.Tags) { - foreach (var id in asset.Tags) + if (tagsById.TryGetValue(id, out var name)) { - if (tagsById.TryGetValue(id, out var name)) - { - asset.TagNames.Add(name); - } + asset.TagNames.Add(name); } } } } } + } - private async Task<Dictionary<string, string>> CalculateTagsAsync(IGrouping<DomainId, IAssetEntity> group, - CancellationToken ct) - { - var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); + private async Task<Dictionary<string, string>> CalculateTagsAsync(IGrouping<DomainId, IAssetEntity> group, + CancellationToken ct) + { + var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); - return await tagService.GetTagNamesAsync(group.Key, TagGroups.Assets, uniqueIds, ct); - } + return await tagService.GetTagNamesAsync(group.Key, TagGroups.Assets, uniqueIds, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs index d9ffd21888..0e4969cf1c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs @@ -9,57 +9,56 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +public sealed class AssetLoader : IAssetLoader { - public sealed class AssetLoader : IAssetLoader + private readonly IDomainObjectFactory domainObjectFactory; + private readonly IDomainObjectCache domainObjectCache; + + public AssetLoader(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) + { + this.domainObjectFactory = domainObjectFactory; + this.domainObjectCache = domainObjectCache; + } + + public async Task<IAssetEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default) { - private readonly IDomainObjectFactory domainObjectFactory; - private readonly IDomainObjectCache domainObjectCache; + var uniqueId = DomainId.Combine(appId, id); + + var asset = await GetCachedAsync(uniqueId, version, ct); - public AssetLoader(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) + if (asset == null) { - this.domainObjectFactory = domainObjectFactory; - this.domainObjectCache = domainObjectCache; + asset = await GetAsync(uniqueId, version, ct); } - public async Task<IAssetEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default) + if (asset is not { Version: > EtagVersion.Empty } || (version > EtagVersion.Any && asset.Version != version)) { - var uniqueId = DomainId.Combine(appId, id); - - var asset = await GetCachedAsync(uniqueId, version, ct); - - if (asset == null) - { - asset = await GetAsync(uniqueId, version, ct); - } - - if (asset is not { Version: > EtagVersion.Empty } || (version > EtagVersion.Any && asset.Version != version)) - { - return null; - } - - return asset; + return null; } - private async Task<AssetDomainObject.State?> GetCachedAsync(DomainId uniqueId, long version, - CancellationToken ct) + return asset; + } + + private async Task<AssetDomainObject.State?> GetCachedAsync(DomainId uniqueId, long version, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("AssetLoader/GetCachedAsync")) { - using (Telemetry.Activities.StartActivity("AssetLoader/GetCachedAsync")) - { - return await domainObjectCache.GetAsync<AssetDomainObject.State>(uniqueId, version, ct); - } + return await domainObjectCache.GetAsync<AssetDomainObject.State>(uniqueId, version, ct); } + } - private async Task<AssetDomainObject.State> GetAsync(DomainId uniqueId, long version, - CancellationToken ct) + private async Task<AssetDomainObject.State> GetAsync(DomainId uniqueId, long version, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("AssetLoader/GetAsync")) { - using (Telemetry.Activities.StartActivity("AssetLoader/GetAsync")) - { - var contentObject = domainObjectFactory.Create<AssetDomainObject>(uniqueId); + var contentObject = domainObjectFactory.Create<AssetDomainObject>(uniqueId); - return await contentObject.GetSnapshotAsync(version, ct); - } + return await contentObject.GetSnapshotAsync(version, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs index 021c3bc38e..b2c8cec7e1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs @@ -18,143 +18,142 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +public class AssetQueryParser { - public class AssetQueryParser + private readonly QueryModel queryModel = AssetQueryModel.Build(); + private readonly IEdmModel edmModel; + private readonly IJsonSerializer serializer; + private readonly ITagService tagService; + private readonly AssetOptions options; + + public AssetQueryParser(IJsonSerializer serializer, ITagService tagService, IOptions<AssetOptions> options) { - private readonly QueryModel queryModel = AssetQueryModel.Build(); - private readonly IEdmModel edmModel; - private readonly IJsonSerializer serializer; - private readonly ITagService tagService; - private readonly AssetOptions options; + this.serializer = serializer; + this.tagService = tagService; + this.options = options.Value; - public AssetQueryParser(IJsonSerializer serializer, ITagService tagService, IOptions<AssetOptions> options) - { - this.serializer = serializer; - this.tagService = tagService; - this.options = options.Value; + edmModel = queryModel.ConvertToEdm("Squidex", "Asset"); + } - edmModel = queryModel.ConvertToEdm("Squidex", "Asset"); - } + public virtual async Task<Q> ParseAsync(Context context, Q q, + CancellationToken ct = default) + { + Guard.NotNull(context); + Guard.NotNull(q); - public virtual async Task<Q> ParseAsync(Context context, Q q, - CancellationToken ct = default) + using (Telemetry.Activities.StartActivity("AssetQueryParser/ParseAsync")) { - Guard.NotNull(context); - Guard.NotNull(q); - - using (Telemetry.Activities.StartActivity("AssetQueryParser/ParseAsync")) - { - var query = ParseClrQuery(q); + var query = ParseClrQuery(q); - await TransformTagAsync(context, query, ct); + await TransformTagAsync(context, query, ct); - WithSorting(query); - WithPaging(query, q); + WithSorting(query); + WithPaging(query, q); - q = q.WithQuery(query); + q = q.WithQuery(query); - if (context.ShouldSkipTotal()) - { - q = q.WithoutTotal(); - } - else if (context.ShouldSkipSlowTotal()) - { - q = q.WithoutSlowTotal(); - } - - return q; - } - } - - private ClrQuery ParseClrQuery(Q q) - { - var query = q.Query; - - if (!string.IsNullOrWhiteSpace(q?.QueryAsJson)) + if (context.ShouldSkipTotal()) { - query = ParseJson(q.QueryAsJson); + q = q.WithoutTotal(); } - else if (!string.IsNullOrWhiteSpace(q?.QueryAsOdata)) + else if (context.ShouldSkipSlowTotal()) { - query = ParseOData(q.QueryAsOdata); + q = q.WithoutSlowTotal(); } - return query; + return q; } + } - private void WithPaging(ClrQuery query, Q q) + private ClrQuery ParseClrQuery(Q q) + { + var query = q.Query; + + if (!string.IsNullOrWhiteSpace(q?.QueryAsJson)) + { + query = ParseJson(q.QueryAsJson); + } + else if (!string.IsNullOrWhiteSpace(q?.QueryAsOdata)) { - if (query.Take is <= 0 or long.MaxValue) + query = ParseOData(q.QueryAsOdata); + } + + return query; + } + + private void WithPaging(ClrQuery query, Q q) + { + if (query.Take is <= 0 or long.MaxValue) + { + if (q.Ids is { Count: > 0 }) { - if (q.Ids is { Count: > 0 }) - { - query.Take = q.Ids.Count; - } - else - { - query.Take = options.DefaultPageSize; - } + query.Take = q.Ids.Count; } - else if (query.Take > options.MaxResults) + else { - query.Take = options.MaxResults; + query.Take = options.DefaultPageSize; } } - - private static void WithSorting(ClrQuery query) + else if (query.Take > options.MaxResults) { - query.Sort ??= new List<SortNode>(); + query.Take = options.MaxResults; + } + } - if (query.Sort.Count == 0) - { - query.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending)); - } + private static void WithSorting(ClrQuery query) + { + query.Sort ??= new List<SortNode>(); - if (!query.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) - { - query.Sort.Add(new SortNode(new List<string> { "id" }, SortOrder.Ascending)); - } + if (query.Sort.Count == 0) + { + query.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending)); } - private async Task TransformTagAsync(Context context, ClrQuery query, - CancellationToken ct) + if (!query.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) { - if (query.Filter != null) - { - query.Filter = await FilterTagTransformer.TransformAsync(query.Filter, context.App.Id, tagService, ct); - } + query.Sort.Add(new SortNode(new List<string> { "id" }, SortOrder.Ascending)); } + } - private ClrQuery ParseJson(string json) + private async Task TransformTagAsync(Context context, ClrQuery query, + CancellationToken ct) + { + if (query.Filter != null) { - return queryModel.Parse(json, serializer); + query.Filter = await FilterTagTransformer.TransformAsync(query.Filter, context.App.Id, tagService, ct); } + } - private ClrQuery ParseOData(string odata) + private ClrQuery ParseJson(string json) + { + return queryModel.Parse(json, serializer); + } + + private ClrQuery ParseOData(string odata) + { + try { - try - { - return edmModel.ParseQuery(odata).ToQuery(); - } - catch (ValidationException) - { - throw; - } - catch (NotSupportedException) - { - throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); - } - catch (ODataException ex) - { - var message = ex.Message; + return edmModel.ParseQuery(odata).ToQuery(); + } + catch (ValidationException) + { + throw; + } + catch (NotSupportedException) + { + throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); + } + catch (ODataException ex) + { + var message = ex.Message; - throw new ValidationException(T.Get("common.odataFailure", new { odata, message }), ex); - } - catch (Exception) - { - throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); - } + throw new ValidationException(T.Get("common.odataFailure", new { odata, message }), ex); + } + catch (Exception) + { + throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs index 2ef1b7f665..8bf5e860f1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -9,306 +9,305 @@ using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +public sealed class AssetQueryService : IAssetQueryService { - public sealed class AssetQueryService : IAssetQueryService + private readonly IAssetEnricher assetEnricher; + private readonly IAssetRepository assetRepository; + private readonly IAssetLoader assetLoader; + private readonly IAssetFolderRepository assetFolderRepository; + private readonly AssetOptions options; + private readonly AssetQueryParser queryParser; + + public AssetQueryService( + IAssetEnricher assetEnricher, + IAssetRepository assetRepository, + IAssetLoader assetLoader, + IAssetFolderRepository assetFolderRepository, + IOptions<AssetOptions> options, + AssetQueryParser queryParser) { - private readonly IAssetEnricher assetEnricher; - private readonly IAssetRepository assetRepository; - private readonly IAssetLoader assetLoader; - private readonly IAssetFolderRepository assetFolderRepository; - private readonly AssetOptions options; - private readonly AssetQueryParser queryParser; - - public AssetQueryService( - IAssetEnricher assetEnricher, - IAssetRepository assetRepository, - IAssetLoader assetLoader, - IAssetFolderRepository assetFolderRepository, - IOptions<AssetOptions> options, - AssetQueryParser queryParser) - { - this.assetEnricher = assetEnricher; - this.assetRepository = assetRepository; - this.assetLoader = assetLoader; - this.assetFolderRepository = assetFolderRepository; - this.options = options.Value; - this.queryParser = queryParser; - } + this.assetEnricher = assetEnricher; + this.assetRepository = assetRepository; + this.assetLoader = assetLoader; + this.assetFolderRepository = assetFolderRepository; + this.options = options.Value; + this.queryParser = queryParser; + } - public async Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(DomainId appId, DomainId id, - CancellationToken ct = default) + public async Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(DomainId appId, DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("AssetQueryService/FindAssetFolderAsync")) { - using (Telemetry.Activities.StartActivity("AssetQueryService/FindAssetFolderAsync")) + var result = new List<IAssetFolderEntity>(); + + while (id != DomainId.Empty) { - var result = new List<IAssetFolderEntity>(); + var folder = await FindFolderCoreAsync(appId, id, ct); - while (id != DomainId.Empty) + if (folder == null || result.Any(x => x.Id == folder.Id)) { - var folder = await FindFolderCoreAsync(appId, id, ct); - - if (folder == null || result.Any(x => x.Id == folder.Id)) - { - result.Clear(); - break; - } - - result.Insert(0, folder); - - id = folder.ParentId; + result.Clear(); + break; } - return result; + result.Insert(0, folder); + + id = folder.ParentId; } + + return result; } + } - public async Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(DomainId appId, DomainId parentId, - CancellationToken ct = default) + public async Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("AssetQueryService/QueryAssetFoldersAsync")) { - using (Telemetry.Activities.StartActivity("AssetQueryService/QueryAssetFoldersAsync")) - { - var assetFolders = await QueryFoldersCoreAsync(appId, parentId, ct); + var assetFolders = await QueryFoldersCoreAsync(appId, parentId, ct); - return assetFolders; - } + return assetFolders; } + } - public async Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId, - CancellationToken ct = default) + public async Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("AssetQueryService/QueryAssetFoldersAsync")) { - using (Telemetry.Activities.StartActivity("AssetQueryService/QueryAssetFoldersAsync")) - { - var assetFolders = await QueryFoldersCoreAsync(context, parentId, ct); + var assetFolders = await QueryFoldersCoreAsync(context, parentId, ct); - return assetFolders; - } + return assetFolders; } + } + + public async Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize, + CancellationToken ct = default) + { + Guard.NotNull(context); - public async Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize, - CancellationToken ct = default) + using (Telemetry.Activities.StartActivity("AssetQueryService/FindByHashAsync")) { - Guard.NotNull(context); + var asset = await FindByHashCoreAsync(context, hash, fileName, fileSize, ct); - using (Telemetry.Activities.StartActivity("AssetQueryService/FindByHashAsync")) + if (asset == null) { - var asset = await FindByHashCoreAsync(context, hash, fileName, fileSize, ct); - - if (asset == null) - { - return null; - } - - return await TransformAsync(context, asset, ct); + return null; } + + return await TransformAsync(context, asset, ct); } + } + + public async Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug, + CancellationToken ct = default) + { + Guard.NotNull(context); - public async Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug, - CancellationToken ct = default) + using (Telemetry.Activities.StartActivity("AssetQueryService/FindBySlugAsync")) { - Guard.NotNull(context); + var asset = await FindBySlugCoreAsync(context, slug, ct); - using (Telemetry.Activities.StartActivity("AssetQueryService/FindBySlugAsync")) + if (asset == null) { - var asset = await FindBySlugCoreAsync(context, slug, ct); - - if (asset == null) - { - return null; - } - - return await TransformAsync(context, asset, ct); + return null; } + + return await TransformAsync(context, asset, ct); } + } - public async Task<IEnrichedAssetEntity?> FindGlobalAsync(Context context, DomainId id, - CancellationToken ct = default) + public async Task<IEnrichedAssetEntity?> FindGlobalAsync(Context context, DomainId id, + CancellationToken ct = default) + { + Guard.NotNull(context); + + using (Telemetry.Activities.StartActivity("AssetQueryService/FindGlobalAsync")) { - Guard.NotNull(context); + var asset = await FindCoreAsync(id, ct); - using (Telemetry.Activities.StartActivity("AssetQueryService/FindGlobalAsync")) + if (asset == null) { - var asset = await FindCoreAsync(id, ct); - - if (asset == null) - { - return null; - } - - return await TransformAsync(context, asset, ct); + return null; } + + return await TransformAsync(context, asset, ct); } + } - public async Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default) + public async Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default) + { + Guard.NotNull(context); + + using (Telemetry.Activities.StartActivity("AssetQueryService/FindAsync")) { - Guard.NotNull(context); + IAssetEntity? asset; - using (Telemetry.Activities.StartActivity("AssetQueryService/FindAsync")) + if (version > EtagVersion.Empty) { - IAssetEntity? asset; - - if (version > EtagVersion.Empty) - { - asset = await assetLoader.GetAsync(context.App.Id, id, version, ct); - } - else - { - asset = await FindCoreAsync(context, id, ct); - } - - if (asset == null) - { - return null; - } - - return await TransformAsync(context, asset, ct); + asset = await assetLoader.GetAsync(context.App.Id, id, version, ct); } - } - - public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q q, - CancellationToken ct = default) - { - Guard.NotNull(context); - - if (q == null) + else { - return ResultList.Empty<IEnrichedAssetEntity>(); + asset = await FindCoreAsync(context, id, ct); } - using (Telemetry.Activities.StartActivity("AssetQueryService/QueryAsync")) + if (asset == null) { - q = await queryParser.ParseAsync(context, q, ct); + return null; + } - var assets = await QueryCoreAsync(context, parentId, q, ct); + return await TransformAsync(context, asset, ct); + } + } - if (q.Ids is { Count: > 0 }) - { - assets = assets.SortSet(x => x.Id, q.Ids); - } + public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q q, + CancellationToken ct = default) + { + Guard.NotNull(context); - return await TransformAsync(context, assets, ct); - } + if (q == null) + { + return ResultList.Empty<IEnrichedAssetEntity>(); } - private async Task<IResultList<IEnrichedAssetEntity>> TransformAsync(Context context, IResultList<IAssetEntity> assets, - CancellationToken ct) + using (Telemetry.Activities.StartActivity("AssetQueryService/QueryAsync")) { - var transformed = await TransformCoreAsync(context, assets, ct); + q = await queryParser.ParseAsync(context, q, ct); - return ResultList.Create(assets.Total, transformed); - } + var assets = await QueryCoreAsync(context, parentId, q, ct); - private async Task<IEnrichedAssetEntity> TransformAsync(Context context, IAssetEntity asset, - CancellationToken ct) - { - var transformed = await TransformCoreAsync(context, Enumerable.Repeat(asset, 1), ct); + if (q.Ids is { Count: > 0 }) + { + assets = assets.SortSet(x => x.Id, q.Ids); + } - return transformed[0]; + return await TransformAsync(context, assets, ct); } + } - private async Task<IReadOnlyList<IEnrichedAssetEntity>> TransformCoreAsync(Context context, IEnumerable<IAssetEntity> assets, - CancellationToken ct) + private async Task<IResultList<IEnrichedAssetEntity>> TransformAsync(Context context, IResultList<IAssetEntity> assets, + CancellationToken ct) + { + var transformed = await TransformCoreAsync(context, assets, ct); + + return ResultList.Create(assets.Total, transformed); + } + + private async Task<IEnrichedAssetEntity> TransformAsync(Context context, IAssetEntity asset, + CancellationToken ct) + { + var transformed = await TransformCoreAsync(context, Enumerable.Repeat(asset, 1), ct); + + return transformed[0]; + } + + private async Task<IReadOnlyList<IEnrichedAssetEntity>> TransformCoreAsync(Context context, IEnumerable<IAssetEntity> assets, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("AssetQueryService/TransformCoreAsync")) { - using (Telemetry.Activities.StartActivity("AssetQueryService/TransformCoreAsync")) - { - return await assetEnricher.EnrichAsync(assets, context, ct); - } + return await assetEnricher.EnrichAsync(assets, context, ct); } + } - private async Task<IResultList<IAssetFolderEntity>> QueryFoldersCoreAsync(Context context, DomainId parentId, - CancellationToken ct) + private async Task<IResultList<IAssetFolderEntity>> QueryFoldersCoreAsync(Context context, DomainId parentId, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutQuery); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutQuery); - return await assetFolderRepository.QueryAsync(context.App.Id, parentId, combined.Token); - } + return await assetFolderRepository.QueryAsync(context.App.Id, parentId, combined.Token); } + } - private async Task<IResultList<IAssetFolderEntity>> QueryFoldersCoreAsync(DomainId appId, DomainId parentId, - CancellationToken ct) + private async Task<IResultList<IAssetFolderEntity>> QueryFoldersCoreAsync(DomainId appId, DomainId parentId, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutQuery); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutQuery); - return await assetFolderRepository.QueryAsync(appId, parentId, combined.Token); - } + return await assetFolderRepository.QueryAsync(appId, parentId, combined.Token); } + } - private async Task<IResultList<IAssetEntity>> QueryCoreAsync(Context context, DomainId? parentId, Q q, - CancellationToken ct) + private async Task<IResultList<IAssetEntity>> QueryCoreAsync(Context context, DomainId? parentId, Q q, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutQuery); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutQuery); - return await assetRepository.QueryAsync(context.App.Id, parentId, q, combined.Token); - } + return await assetRepository.QueryAsync(context.App.Id, parentId, q, combined.Token); } + } - private async Task<IAssetFolderEntity?> FindFolderCoreAsync(DomainId appId, DomainId id, - CancellationToken ct) + private async Task<IAssetFolderEntity?> FindFolderCoreAsync(DomainId appId, DomainId id, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutFind); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutFind); - return await assetFolderRepository.FindAssetFolderAsync(appId, id, combined.Token); - } + return await assetFolderRepository.FindAssetFolderAsync(appId, id, combined.Token); } + } - private async Task<IAssetEntity?> FindByHashCoreAsync(Context context, string hash, string fileName, long fileSize, - CancellationToken ct) + private async Task<IAssetEntity?> FindByHashCoreAsync(Context context, string hash, string fileName, long fileSize, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutFind); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutFind); - return await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize, combined.Token); - } + return await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize, combined.Token); } + } - private async Task<IAssetEntity?> FindBySlugCoreAsync(Context context, string slug, - CancellationToken ct) + private async Task<IAssetEntity?> FindBySlugCoreAsync(Context context, string slug, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutFind); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutFind); - return await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, combined.Token); - } + return await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, combined.Token); } + } - private async Task<IAssetEntity?> FindCoreAsync(DomainId id, - CancellationToken ct) + private async Task<IAssetEntity?> FindCoreAsync(DomainId id, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutFind); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutFind); - return await assetRepository.FindAssetAsync(id, combined.Token); - } + return await assetRepository.FindAssetAsync(id, combined.Token); } + } - private async Task<IAssetEntity?> FindCoreAsync(Context context, DomainId id, - CancellationToken ct) + private async Task<IAssetEntity?> FindCoreAsync(Context context, DomainId id, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutFind); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutFind); - return await assetRepository.FindAssetAsync(context.App.Id, id, combined.Token); - } + return await assetRepository.FindAssetAsync(context.App.Id, id, combined.Token); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs index 9319c2cdb5..244206a583 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs @@ -11,39 +11,38 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +internal sealed class FilterTagTransformer : AsyncTransformVisitor<ClrValue, FilterTagTransformer.Args> { - internal sealed class FilterTagTransformer : AsyncTransformVisitor<ClrValue, FilterTagTransformer.Args> - { - private static readonly FilterTagTransformer Instance = new FilterTagTransformer(); + private static readonly FilterTagTransformer Instance = new FilterTagTransformer(); - public record struct Args(DomainId AppId, ITagService TagService, CancellationToken CancellationToken); + public record struct Args(DomainId AppId, ITagService TagService, CancellationToken CancellationToken); - private FilterTagTransformer() - { - } + private FilterTagTransformer() + { + } - public static ValueTask<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> nodeIn, DomainId appId, ITagService tagService, - CancellationToken ct) - { - var args = new Args(appId, tagService, ct); + public static ValueTask<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> nodeIn, DomainId appId, ITagService tagService, + CancellationToken ct) + { + var args = new Args(appId, tagService, ct); - return nodeIn.Accept(Instance, args); - } + return nodeIn.Accept(Instance, args); + } - public override async ValueTask<FilterNode<ClrValue>?> Visit(CompareFilter<ClrValue> nodeIn, Args args) + public override async ValueTask<FilterNode<ClrValue>?> Visit(CompareFilter<ClrValue> nodeIn, Args args) + { + if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue) { - if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue) - { - var tagNames = await args.TagService.GetTagIdsAsync(args.AppId, TagGroups.Assets, HashSet.Of(stringValue), args.CancellationToken); + var tagNames = await args.TagService.GetTagIdsAsync(args.AppId, TagGroups.Assets, HashSet.Of(stringValue), args.CancellationToken); - if (tagNames.TryGetValue(stringValue, out var normalized)) - { - return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, normalized); - } + if (tagNames.TryGetValue(stringValue, out var normalized)) + { + return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, normalized); } - - return nodeIn; } + + return nodeIn; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs index facde1401a..91a8c128cd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs @@ -11,60 +11,59 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class RebuildFiles { - public sealed class RebuildFiles + private static readonly MemoryStream DummyStream = new MemoryStream(Encoding.UTF8.GetBytes("dummy")); + private readonly IAssetFileStore assetFileStore; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + + public RebuildFiles( + IAssetFileStore assetFileStore, + IEventFormatter eventFormatter, + IEventStore eventStore) { - private static readonly MemoryStream DummyStream = new MemoryStream(Encoding.UTF8.GetBytes("dummy")); - private readonly IAssetFileStore assetFileStore; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; + this.assetFileStore = assetFileStore; + this.eventStore = eventStore; + this.eventFormatter = eventFormatter; + } - public RebuildFiles( - IAssetFileStore assetFileStore, - IEventFormatter eventFormatter, - IEventStore eventStore) + public async Task RepairAsync( + CancellationToken ct = default) + { + await foreach (var storedEvent in eventStore.QueryAllAsync("^asset\\-", ct: ct)) { - this.assetFileStore = assetFileStore; - this.eventStore = eventStore; - this.eventFormatter = eventFormatter; - } + var @event = eventFormatter.ParseIfKnown(storedEvent); - public async Task RepairAsync( - CancellationToken ct = default) - { - await foreach (var storedEvent in eventStore.QueryAllAsync("^asset\\-", ct: ct)) + if (@event != null) { - var @event = eventFormatter.ParseIfKnown(storedEvent); - - if (@event != null) + switch (@event.Payload) { - switch (@event.Payload) - { - case AssetCreated assetCreated: - await TryRepairAsync(assetCreated.AppId, assetCreated.AssetId, assetCreated.FileVersion, ct); - break; - case AssetUpdated assetUpdated: - await TryRepairAsync(assetUpdated.AppId, assetUpdated.AssetId, assetUpdated.FileVersion, ct); - break; - } + case AssetCreated assetCreated: + await TryRepairAsync(assetCreated.AppId, assetCreated.AssetId, assetCreated.FileVersion, ct); + break; + case AssetUpdated assetUpdated: + await TryRepairAsync(assetUpdated.AppId, assetUpdated.AssetId, assetUpdated.FileVersion, ct); + break; } } } + } - private async Task TryRepairAsync(NamedId<DomainId> appId, DomainId id, long fileVersion, - CancellationToken ct) + private async Task TryRepairAsync(NamedId<DomainId> appId, DomainId id, long fileVersion, + CancellationToken ct) + { + try { - try - { - await assetFileStore.GetFileSizeAsync(appId.Id, id, fileVersion, null, ct); - } - catch (AssetNotFoundException) - { - DummyStream.Position = 0; + await assetFileStore.GetFileSizeAsync(appId.Id, id, fileVersion, null, ct); + } + catch (AssetNotFoundException) + { + DummyStream.Position = 0; - await assetFileStore.UploadAsync(appId.Id, id, fileVersion, null, DummyStream, ct: ct); - } + await assetFileStore.UploadAsync(appId.Id, id, fileVersion, null, DummyStream, ct: ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs index c343d7459d..a38b43c4a7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs @@ -13,92 +13,91 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class RecursiveDeleter : IEventConsumer { - public sealed class RecursiveDeleter : IEventConsumer + private readonly ICommandBus commandBus; + private readonly IAssetRepository assetRepository; + private readonly IAssetFolderRepository assetFolderRepository; + private readonly ILogger<RecursiveDeleter> log; + private readonly HashSet<string> consumingTypes; + + public string Name { - private readonly ICommandBus commandBus; - private readonly IAssetRepository assetRepository; - private readonly IAssetFolderRepository assetFolderRepository; - private readonly ILogger<RecursiveDeleter> log; - private readonly HashSet<string> consumingTypes; + get => GetType().Name; + } - public string Name - { - get => GetType().Name; - } + public string EventsFilter + { + get => "^assetFolder-"; + } - public string EventsFilter - { - get => "^assetFolder-"; - } + public RecursiveDeleter( + ICommandBus commandBus, + IAssetRepository assetRepository, + IAssetFolderRepository assetFolderRepository, + TypeNameRegistry typeNameRegistry, + ILogger<RecursiveDeleter> log) + { + this.commandBus = commandBus; + this.assetRepository = assetRepository; + this.assetFolderRepository = assetFolderRepository; + + this.log = log; - public RecursiveDeleter( - ICommandBus commandBus, - IAssetRepository assetRepository, - IAssetFolderRepository assetFolderRepository, - TypeNameRegistry typeNameRegistry, - ILogger<RecursiveDeleter> log) + // Compute the event types names once for performance reasons and use hashset for extensibility. + consumingTypes = new HashSet<string> { - this.commandBus = commandBus; - this.assetRepository = assetRepository; - this.assetFolderRepository = assetFolderRepository; + typeNameRegistry.GetName<AssetFolderDeleted>() + }; + } - this.log = log; + public bool Handles(StoredEvent @event) + { + return consumingTypes.Contains(@event.Data.Type); + } - // Compute the event types names once for performance reasons and use hashset for extensibility. - consumingTypes = new HashSet<string> - { - typeNameRegistry.GetName<AssetFolderDeleted>() - }; + public async Task On(Envelope<IEvent> @event) + { + if (@event.Headers.Restored()) + { + return; } - public bool Handles(StoredEvent @event) + if (@event.Payload is not AssetFolderDeleted folderDeleted) { - return consumingTypes.Contains(@event.Data.Type); + return; } - public async Task On(Envelope<IEvent> @event) + async Task PublishAsync(SquidexCommand command) { - if (@event.Headers.Restored()) + try { - return; - } + command.Actor = folderDeleted.Actor; - if (@event.Payload is not AssetFolderDeleted folderDeleted) - { - return; + await commandBus.PublishAsync(command, default); } - - async Task PublishAsync(SquidexCommand command) + catch (Exception ex) { - try - { - command.Actor = folderDeleted.Actor; - - await commandBus.PublishAsync(command, default); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to delete asset recursively."); - } + log.LogError(ex, "Failed to delete asset recursively."); } + } - var appId = folderDeleted.AppId; + var appId = folderDeleted.AppId; - var childAssetFolders = await assetFolderRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId); + var childAssetFolders = await assetFolderRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId); - foreach (var assetFolderId in childAssetFolders) - { - await PublishAsync(new DeleteAssetFolder { AppId = appId, AssetFolderId = assetFolderId }); - } + foreach (var assetFolderId in childAssetFolders) + { + await PublishAsync(new DeleteAssetFolder { AppId = appId, AssetFolderId = assetFolderId }); + } - var childAssets = await assetRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId); + var childAssets = await assetRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId); - foreach (var assetId in childAssets) - { - await PublishAsync(new DeleteAsset { AppId = appId, AssetId = assetId }); - } + foreach (var assetId in childAssets) + { + await PublishAsync(new DeleteAsset { AppId = appId, AssetId = assetId }); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs index 4e0839a8c5..d928b9704d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Repositories +namespace Squidex.Domain.Apps.Entities.Assets.Repositories; + +public interface IAssetFolderRepository { - public interface IAssetFolderRepository - { - Task<IResultList<IAssetFolderEntity>> QueryAsync(DomainId appId, DomainId parentId, - CancellationToken ct = default); + Task<IResultList<IAssetFolderEntity>> QueryAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default); - Task<IReadOnlyList<DomainId>> QueryChildIdsAsync(DomainId appId, DomainId parentId, - CancellationToken ct = default); + Task<IReadOnlyList<DomainId>> QueryChildIdsAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default); - Task<IAssetFolderEntity?> FindAssetFolderAsync(DomainId appId, DomainId id, - CancellationToken ct = default); - } + Task<IAssetFolderEntity?> FindAssetFolderAsync(DomainId appId, DomainId id, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index f4c3da946f..2912c687a8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -7,32 +7,31 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Assets.Repositories +namespace Squidex.Domain.Apps.Entities.Assets.Repositories; + +public interface IAssetRepository { - public interface IAssetRepository - { - IAsyncEnumerable<IAssetEntity> StreamAll(DomainId appId, - CancellationToken ct = default); + IAsyncEnumerable<IAssetEntity> StreamAll(DomainId appId, + CancellationToken ct = default); - Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, Q q, - CancellationToken ct = default); + Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, Q q, + CancellationToken ct = default); - Task<IReadOnlyList<DomainId>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids, - CancellationToken ct = default); + Task<IReadOnlyList<DomainId>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids, + CancellationToken ct = default); - Task<IReadOnlyList<DomainId>> QueryChildIdsAsync(DomainId appId, DomainId parentId, - CancellationToken ct = default); + Task<IReadOnlyList<DomainId>> QueryChildIdsAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default); - Task<IAssetEntity?> FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, - CancellationToken ct = default); + Task<IAssetEntity?> FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, + CancellationToken ct = default); - Task<IAssetEntity?> FindAssetBySlugAsync(DomainId appId, string slug, - CancellationToken ct = default); + Task<IAssetEntity?> FindAssetBySlugAsync(DomainId appId, string slug, + CancellationToken ct = default); - Task<IAssetEntity?> FindAssetAsync(DomainId id, - CancellationToken ct = default); + Task<IAssetEntity?> FindAssetAsync(DomainId id, + CancellationToken ct = default); - Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id, - CancellationToken ct = default); - } + Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs index 23cd10da84..c5f1ebf1e8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs @@ -10,52 +10,51 @@ using Squidex.Infrastructure.Validation; using Squidex.Text; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class SvgAssetMetadataSource : IAssetMetadataSource { - public sealed class SvgAssetMetadataSource : IAssetMetadataSource + private const int FileSizeLimit = 2 * 1024 * 1024; // 2MB + + public async Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) { - private const int FileSizeLimit = 2 * 1024 * 1024; // 2MB + var isSvg = + command.File.MimeType == "image/svg+xml" || + command.File.FileName.EndsWith(".svg", StringComparison.OrdinalIgnoreCase); - public async Task EnhanceAsync(UploadAssetCommand command, - CancellationToken ct) + if (isSvg) { - var isSvg = - command.File.MimeType == "image/svg+xml" || - command.File.FileName.EndsWith(".svg", StringComparison.OrdinalIgnoreCase); + command.Tags.Add("image"); - if (isSvg) + if (command.File.FileSize < FileSizeLimit) { - command.Tags.Add("image"); - - if (command.File.FileSize < FileSizeLimit) + try { - try + using (var reader = new StreamReader(command.File.OpenRead())) { - using (var reader = new StreamReader(command.File.OpenRead())) - { - var text = await reader.ReadToEndAsync(); + var text = await reader.ReadToEndAsync(); - if (!text.IsValidSvg()) - { - throw new ValidationException(T.Get("validation.notAnValidSvg")); - } + if (!text.IsValidSvg()) + { + throw new ValidationException(T.Get("validation.notAnValidSvg")); } } - catch (ValidationException) - { - throw; - } - catch - { - return; - } + } + catch (ValidationException) + { + throw; + } + catch + { + return; } } } + } - public IEnumerable<string> Format(IAssetEntity asset) - { - yield break; - } + public IEnumerable<string> Format(IAssetEntity asset) + { + yield break; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs index a1a909c3d2..4930e8f8e0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs @@ -15,79 +15,78 @@ #pragma warning disable MA0048 // File name must match type name #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Assets -{ - public record struct AssetRef( - DomainId AppId, - DomainId Id, - long FileVersion, - long FileSize, - string MimeType, - AssetType Type); +namespace Squidex.Domain.Apps.Entities.Assets; + +public record struct AssetRef( + DomainId AppId, + DomainId Id, + long FileVersion, + long FileSize, + string MimeType, + AssetType Type); - public static class Transformations +public static class Transformations +{ + public static AssetRef ToRef(this EnrichedAssetEvent @event) { - public static AssetRef ToRef(this EnrichedAssetEvent @event) - { - return new AssetRef( - @event.AppId.Id, - @event.Id, - @event.FileVersion, - @event.FileSize, - @event.MimeType, - @event.AssetType); - } + return new AssetRef( + @event.AppId.Id, + @event.Id, + @event.FileVersion, + @event.FileSize, + @event.MimeType, + @event.AssetType); + } - public static AssetRef ToRef(this IAssetEntity asset) - { - return new AssetRef( - asset.AppId.Id, - asset.Id, - asset.FileVersion, - asset.FileSize, - asset.MimeType, - asset.Type); - } + public static AssetRef ToRef(this IAssetEntity asset) + { + return new AssetRef( + asset.AppId.Id, + asset.Id, + asset.FileVersion, + asset.FileSize, + asset.MimeType, + asset.Type); + } - public static async Task<string> GetTextAsync(this AssetRef asset, string? encoding, - IAssetFileStore assetFileStore, - CancellationToken ct = default) + public static async Task<string> GetTextAsync(this AssetRef asset, string? encoding, + IAssetFileStore assetFileStore, + CancellationToken ct = default) + { + using (var stream = DefaultPools.MemoryStream.GetStream()) { - using (var stream = DefaultPools.MemoryStream.GetStream()) - { - await assetFileStore.DownloadAsync(asset.AppId, asset.Id, asset.FileVersion, null, stream, default, ct); + await assetFileStore.DownloadAsync(asset.AppId, asset.Id, asset.FileVersion, null, stream, default, ct); - stream.Position = 0; + stream.Position = 0; - var bytes = stream.ToArray(); + var bytes = stream.ToArray(); - switch (encoding?.ToLowerInvariant()) - { - case "base64": - return Convert.ToBase64String(bytes); - case "ascii": - return Encoding.ASCII.GetString(bytes); - case "unicode": - return Encoding.Unicode.GetString(bytes); - default: - return Encoding.UTF8.GetString(bytes); - } + switch (encoding?.ToLowerInvariant()) + { + case "base64": + return Convert.ToBase64String(bytes); + case "ascii": + return Encoding.ASCII.GetString(bytes); + case "unicode": + return Encoding.Unicode.GetString(bytes); + default: + return Encoding.UTF8.GetString(bytes); } } + } - public static async Task<string?> GetBlurHashAsync(this AssetRef asset, BlurOptions options, - IAssetFileStore assetFileStore, - IAssetThumbnailGenerator assetThumbnails, - CancellationToken ct = default) + public static async Task<string?> GetBlurHashAsync(this AssetRef asset, BlurOptions options, + IAssetFileStore assetFileStore, + IAssetThumbnailGenerator assetThumbnails, + CancellationToken ct = default) + { + using (var stream = DefaultPools.MemoryStream.GetStream()) { - using (var stream = DefaultPools.MemoryStream.GetStream()) - { - await assetFileStore.DownloadAsync(asset.AppId, asset.Id, asset.FileVersion, null, stream, default, ct); + await assetFileStore.DownloadAsync(asset.AppId, asset.Id, asset.FileVersion, null, stream, default, ct); - stream.Position = 0; + stream.Position = 0; - return await assetThumbnails.ComputeBlurHashAsync(stream, asset.MimeType, options, ct); - } + return await assetThumbnails.ComputeBlurHashAsync(stream, asset.MimeType, options, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContext.cs index ec1553d7b1..4967be5250 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContext.cs @@ -7,18 +7,17 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class BackupContext : BackupContextBase { - public sealed class BackupContext : BackupContextBase - { - public IBackupWriter Writer { get; } + public IBackupWriter Writer { get; } - public BackupContext(DomainId appId, IUserMapping userMapping, IBackupWriter writer) - : base(appId, userMapping) - { - Guard.NotNull(writer); + public BackupContext(DomainId appId, IUserMapping userMapping, IBackupWriter writer) + : base(appId, userMapping) + { + Guard.NotNull(writer); - Writer = writer; - } + Writer = writer; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContextBase.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContextBase.cs index 415d211157..f84e604749 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContextBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupContextBase.cs @@ -7,26 +7,25 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public abstract class BackupContextBase { - public abstract class BackupContextBase - { - public IUserMapping UserMapping { get; } + public IUserMapping UserMapping { get; } - public DomainId AppId { get; set; } + public DomainId AppId { get; set; } - public RefToken Initiator - { - get => UserMapping.Initiator; - } + public RefToken Initiator + { + get => UserMapping.Initiator; + } - protected BackupContextBase(DomainId appId, IUserMapping userMapping) - { - Guard.NotNull(userMapping); + protected BackupContextBase(DomainId appId, IUserMapping userMapping) + { + Guard.NotNull(userMapping); - AppId = appId; + AppId = appId; - UserMapping = userMapping; - } + UserMapping = userMapping; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.Run.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.Run.cs index 5b309e7c68..dc73c2b787 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.Run.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.Run.cs @@ -10,45 +10,44 @@ #pragma warning disable MA0040 // Flow the cancellation token -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed partial class BackupProcessor { - public sealed partial class BackupProcessor + // Use a run to store all state that is necessary for a single run. + private sealed class Run : IDisposable { - // Use a run to store all state that is necessary for a single run. - private sealed class Run : IDisposable - { - private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); - private readonly CancellationTokenSource cancellationLinked; + private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly CancellationTokenSource cancellationLinked; - public IEnumerable<IBackupHandler> Handlers { get; init; } + public IEnumerable<IBackupHandler> Handlers { get; init; } - public RefToken Actor { get; init; } + public RefToken Actor { get; init; } - public BackupJob Job { get; init; } + public BackupJob Job { get; init; } - public CancellationToken CancellationToken => cancellationLinked.Token; + public CancellationToken CancellationToken => cancellationLinked.Token; - public Run(CancellationToken ct) - { - cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); - } + public Run(CancellationToken ct) + { + cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); + } - public void Dispose() + public void Dispose() + { + cancellationSource.Dispose(); + cancellationLinked.Dispose(); + } + + public void Cancel() + { + try { - cancellationSource.Dispose(); - cancellationLinked.Dispose(); + cancellationSource.Cancel(); } - - public void Cancel() + catch (ObjectDisposedException) { - try - { - cancellationSource.Cancel(); - } - catch (ObjectDisposedException) - { - // Cancellation token might have been disposed, if the run is completed. - } + // Cancellation token might have been disposed, if the run is completed. } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs index ac5cdddabf..64b9e66a37 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs @@ -19,256 +19,255 @@ #pragma warning disable MA0040 // Flow the cancellation token -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed partial class BackupProcessor { - public sealed partial class BackupProcessor + private readonly IBackupArchiveLocation backupArchiveLocation; + private readonly IBackupArchiveStore backupArchiveStore; + private readonly IBackupHandlerFactory backupHandlerFactory; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly IUserResolver userResolver; + private readonly ILogger<BackupProcessor> log; + private readonly SimpleState<BackupState> state; + private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); + private readonly DomainId appId; + private Run? currentRun; + + public IClock Clock { get; set; } = SystemClock.Instance; + + public BackupProcessor( + DomainId appId, + IBackupArchiveLocation backupArchiveLocation, + IBackupArchiveStore backupArchiveStore, + IBackupHandlerFactory backupHandlerFactory, + IEventFormatter eventFormatter, + IEventStore eventStore, + IPersistenceFactory<BackupState> persistenceFactory, + IUserResolver userResolver, + ILogger<BackupProcessor> log) + { + this.appId = appId; + this.backupArchiveLocation = backupArchiveLocation; + this.backupArchiveStore = backupArchiveStore; + this.backupHandlerFactory = backupHandlerFactory; + this.eventFormatter = eventFormatter; + this.eventStore = eventStore; + this.userResolver = userResolver; + this.log = log; + + // Enable locking for the parallel operations that might write stuff. + state = new SimpleState<BackupState>(persistenceFactory, GetType(), appId, true); + } + + public async Task LoadAsync( + CancellationToken ct) { - private readonly IBackupArchiveLocation backupArchiveLocation; - private readonly IBackupArchiveStore backupArchiveStore; - private readonly IBackupHandlerFactory backupHandlerFactory; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly IUserResolver userResolver; - private readonly ILogger<BackupProcessor> log; - private readonly SimpleState<BackupState> state; - private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); - private readonly DomainId appId; - private Run? currentRun; - - public IClock Clock { get; set; } = SystemClock.Instance; - - public BackupProcessor( - DomainId appId, - IBackupArchiveLocation backupArchiveLocation, - IBackupArchiveStore backupArchiveStore, - IBackupHandlerFactory backupHandlerFactory, - IEventFormatter eventFormatter, - IEventStore eventStore, - IPersistenceFactory<BackupState> persistenceFactory, - IUserResolver userResolver, - ILogger<BackupProcessor> log) + await state.LoadAsync(ct); + + if (state.Value.Jobs.RemoveAll(x => x.Stopped == null) > 0) { - this.appId = appId; - this.backupArchiveLocation = backupArchiveLocation; - this.backupArchiveStore = backupArchiveStore; - this.backupHandlerFactory = backupHandlerFactory; - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; - this.userResolver = userResolver; - this.log = log; - - // Enable locking for the parallel operations that might write stuff. - state = new SimpleState<BackupState>(persistenceFactory, GetType(), appId, true); + // This should actually never happen, so we log with warning. + log.LogWarning("Removed unfinished backups for app {appId} after start.", appId); + + await state.WriteAsync(ct); } + } - public async Task LoadAsync( - CancellationToken ct) + public Task ClearAsync() + { + return scheduler.ScheduleAsync(async _ => { - await state.LoadAsync(ct); + log.LogInformation("Clearing backups for app {appId}.", appId); - if (state.Value.Jobs.RemoveAll(x => x.Stopped == null) > 0) + foreach (var backup in state.Value.Jobs) { - // This should actually never happen, so we log with warning. - log.LogWarning("Removed unfinished backups for app {appId} after start.", appId); - - await state.WriteAsync(ct); + await backupArchiveStore.DeleteAsync(backup.Id, default); } - } - public Task ClearAsync() + await state.ClearAsync(default); + }); + } + + public Task BackupAsync(RefToken actor, + CancellationToken ct) + { + return scheduler.ScheduleAsync(async _ => { - return scheduler.ScheduleAsync(async _ => + if (currentRun != null) { - log.LogInformation("Clearing backups for app {appId}.", appId); - - foreach (var backup in state.Value.Jobs) - { - await backupArchiveStore.DeleteAsync(backup.Id, default); - } + throw new DomainException(T.Get("backups.alreadyRunning")); + } - await state.ClearAsync(default); - }); - } + state.Value.EnsureCanStart(); - public Task BackupAsync(RefToken actor, - CancellationToken ct) - { - return scheduler.ScheduleAsync(async _ => + // Set the current run first to indicate that we are running a rule at the moment. + var run = currentRun = new Run(ct) { - if (currentRun != null) + Actor = actor, + Job = new BackupJob { - throw new DomainException(T.Get("backups.alreadyRunning")); - } + Id = DomainId.NewGuid(), + Started = Clock.GetCurrentInstant(), + Status = JobStatus.Started + }, + Handlers = backupHandlerFactory.CreateMany() + }; - state.Value.EnsureCanStart(); + log.LogInformation("Starting new backup with backup id '{backupId}' for app {appId}.", run.Job.Id, appId); - // Set the current run first to indicate that we are running a rule at the moment. - var run = currentRun = new Run(ct) - { - Actor = actor, - Job = new BackupJob - { - Id = DomainId.NewGuid(), - Started = Clock.GetCurrentInstant(), - Status = JobStatus.Started - }, - Handlers = backupHandlerFactory.CreateMany() - }; + state.Value.Jobs.Insert(0, run.Job); + try + { + await ProcessAsync(run, run.CancellationToken); + } + finally + { + // Unset the run to indicate that we are done. + currentRun.Dispose(); + currentRun = null; + } + }, ct); + } - log.LogInformation("Starting new backup with backup id '{backupId}' for app {appId}.", run.Job.Id, appId); + private async Task ProcessAsync(Run run, + CancellationToken ct) + { + try + { + await state.WriteAsync(run.CancellationToken); - state.Value.Jobs.Insert(0, run.Job); - try - { - await ProcessAsync(run, run.CancellationToken); - } - finally + await using (var stream = backupArchiveLocation.OpenStream(run.Job.Id)) + { + using (var writer = await backupArchiveLocation.OpenWriterAsync(stream, ct)) { - // Unset the run to indicate that we are done. - currentRun.Dispose(); - currentRun = null; - } - }, ct); - } + await writer.WriteVersionAsync(); - private async Task ProcessAsync(Run run, - CancellationToken ct) - { - try - { - await state.WriteAsync(run.CancellationToken); + var backupUsers = new UserMapping(run.Actor); + var backupContext = new BackupContext(appId, backupUsers, writer); - await using (var stream = backupArchiveLocation.OpenStream(run.Job.Id)) - { - using (var writer = await backupArchiveLocation.OpenWriterAsync(stream, ct)) + await foreach (var storedEvent in eventStore.QueryAllAsync(GetFilter(), ct: ct)) { - await writer.WriteVersionAsync(); - - var backupUsers = new UserMapping(run.Actor); - var backupContext = new BackupContext(appId, backupUsers, writer); + var @event = eventFormatter.Parse(storedEvent); - await foreach (var storedEvent in eventStore.QueryAllAsync(GetFilter(), ct: ct)) + if (@event.Payload is SquidexEvent { Actor: { } } squidexEvent) { - var @event = eventFormatter.Parse(storedEvent); - - if (@event.Payload is SquidexEvent { Actor: { } } squidexEvent) - { - backupUsers.Backup(squidexEvent.Actor); - } - - foreach (var handler in run.Handlers) - { - await handler.BackupEventAsync(@event, backupContext, ct); - } - - writer.WriteEvent(storedEvent, ct); - - await LogAsync(run, writer.WrittenEvents, writer.WrittenAttachments); + backupUsers.Backup(squidexEvent.Actor); } foreach (var handler in run.Handlers) { - ct.ThrowIfCancellationRequested(); - - await handler.BackupAsync(backupContext, ct); + await handler.BackupEventAsync(@event, backupContext, ct); } - foreach (var handler in run.Handlers) - { - ct.ThrowIfCancellationRequested(); + writer.WriteEvent(storedEvent, ct); - await handler.CompleteBackupAsync(backupContext); - } + await LogAsync(run, writer.WrittenEvents, writer.WrittenAttachments); + } - await backupUsers.StoreAsync(writer, userResolver, ct); + foreach (var handler in run.Handlers) + { + ct.ThrowIfCancellationRequested(); + + await handler.BackupAsync(backupContext, ct); } - stream.Position = 0; + foreach (var handler in run.Handlers) + { + ct.ThrowIfCancellationRequested(); - ct.ThrowIfCancellationRequested(); + await handler.CompleteBackupAsync(backupContext); + } - await backupArchiveStore.UploadAsync(run.Job.Id, stream, ct); + await backupUsers.StoreAsync(writer, userResolver, ct); } - await SetStatusAsync(run, JobStatus.Completed); - } - catch (Exception ex) - { - await SetStatusAsync(run, JobStatus.Failed); + stream.Position = 0; + + ct.ThrowIfCancellationRequested(); - log.LogError(ex, "Failed to make backup with backup id '{backupId}'.", run.Job.Id); + await backupArchiveStore.UploadAsync(run.Job.Id, stream, ct); } - } - private string GetFilter() + await SetStatusAsync(run, JobStatus.Completed); + } + catch (Exception ex) { - return $"^[^\\-]*-{Regex.Escape(appId.ToString())}"; + await SetStatusAsync(run, JobStatus.Failed); + + log.LogError(ex, "Failed to make backup with backup id '{backupId}'.", run.Job.Id); } + } - public Task DeleteAsync(DomainId id) - { - return scheduler.ScheduleAsync(async _ => - { - var job = state.Value.Jobs.Find(x => x.Id == id); + private string GetFilter() + { + return $"^[^\\-]*-{Regex.Escape(appId.ToString())}"; + } - if (job == null) - { - throw new DomainObjectNotFoundException(id.ToString()); - } + public Task DeleteAsync(DomainId id) + { + return scheduler.ScheduleAsync(async _ => + { + var job = state.Value.Jobs.Find(x => x.Id == id); - log.LogInformation("Deleting backup with backup id '{backupId}' for app {appId}.", job.Id, appId); + if (job == null) + { + throw new DomainObjectNotFoundException(id.ToString()); + } - if (currentRun?.Job == job) - { - currentRun.Cancel(); - } - else - { - await RemoveAsync(job); - } - }); - } + log.LogInformation("Deleting backup with backup id '{backupId}' for app {appId}.", job.Id, appId); - private async Task RemoveAsync(BackupJob job) - { - try + if (currentRun?.Job == job) { - await backupArchiveStore.DeleteAsync(job.Id); + currentRun.Cancel(); } - catch (Exception ex) + else { - log.LogError(ex, "Failed to make remove with backup id '{backupId}'.", job.Id); + await RemoveAsync(job); } + }); + } - state.Value.Jobs.Remove(job); - - await state.WriteAsync(); + private async Task RemoveAsync(BackupJob job) + { + try + { + await backupArchiveStore.DeleteAsync(job.Id); } - - private Task SetStatusAsync(Run run, JobStatus status) + catch (Exception ex) { - var now = Clock.GetCurrentInstant(); + log.LogError(ex, "Failed to make remove with backup id '{backupId}'.", job.Id); + } - run.Job.Status = status; + state.Value.Jobs.Remove(job); - if (status == JobStatus.Failed || status == JobStatus.Completed) - { - run.Job.Stopped = now; - } - else if (status == JobStatus.Started) - { - run.Job.Started = now; - } + await state.WriteAsync(); + } - return state.WriteAsync(ct: default); - } + private Task SetStatusAsync(Run run, JobStatus status) + { + var now = Clock.GetCurrentInstant(); - private Task LogAsync(Run run, int numEvents, int numAttachments) - { - run.Job.HandledEvents = numEvents; - run.Job.HandledAssets = numAttachments; + run.Job.Status = status; - return state.WriteAsync(100, run.CancellationToken); + if (status == JobStatus.Failed || status == JobStatus.Completed) + { + run.Job.Stopped = now; + } + else if (status == JobStatus.Started) + { + run.Job.Started = now; } + + return state.WriteAsync(ct: default); + } + + private Task LogAsync(Run run, int numEvents, int numAttachments) + { + run.Job.HandledEvents = numEvents; + run.Job.HandledAssets = numAttachments; + + return state.WriteAsync(100, run.CancellationToken); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs index 01d2f612b0..94f4ad6aa1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -14,118 +14,117 @@ using Squidex.Infrastructure.Json; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class BackupReader : DisposableObjectBase, IBackupReader { - public sealed class BackupReader : DisposableObjectBase, IBackupReader - { - private readonly ZipArchive archive; - private readonly IJsonSerializer serializer; - private int readEvents; - private int readAttachments; + private readonly ZipArchive archive; + private readonly IJsonSerializer serializer; + private int readEvents; + private int readAttachments; - public int ReadEvents - { - get => readEvents; - } + public int ReadEvents + { + get => readEvents; + } - public int ReadAttachments - { - get => readAttachments; - } + public int ReadAttachments + { + get => readAttachments; + } - public BackupReader(IJsonSerializer serializer, Stream stream) - { - Guard.NotNull(serializer); + public BackupReader(IJsonSerializer serializer, Stream stream) + { + Guard.NotNull(serializer); - this.serializer = serializer; + this.serializer = serializer; - archive = new ZipArchive(stream, ZipArchiveMode.Read, false); - } + archive = new ZipArchive(stream, ZipArchiveMode.Read, false); + } - protected override void DisposeObject(bool disposing) + protected override void DisposeObject(bool disposing) + { + if (disposing) { - if (disposing) - { - archive.Dispose(); - } + archive.Dispose(); } + } - public Task<Stream> OpenBlobAsync(string name, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(name); + public Task<Stream> OpenBlobAsync(string name, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(name); - var entry = GetEntry(name); + var entry = GetEntry(name); - return Task.FromResult(entry.Open()); - } + return Task.FromResult(entry.Open()); + } - public async Task<T> ReadJsonAsync<T>(string name, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(name); + public async Task<T> ReadJsonAsync<T>(string name, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(name); - var entry = GetEntry(name); + var entry = GetEntry(name); - await using (var stream = entry.Open()) - { - return serializer.Deserialize<T>(stream, null); - } + await using (var stream = entry.Open()) + { + return serializer.Deserialize<T>(stream, null); } + } - public Task<bool> HasFileAsync(string name, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(name); + public Task<bool> HasFileAsync(string name, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(name); - var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); - return Task.FromResult(attachmentEntry?.Length > 0); - } + return Task.FromResult(attachmentEntry?.Length > 0); + } - private ZipArchiveEntry GetEntry(string name) + private ZipArchiveEntry GetEntry(string name) + { + var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + + if (attachmentEntry == null || attachmentEntry.Length == 0) { - var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + throw new FileNotFoundException("Cannot find attachment.", name); + } - if (attachmentEntry == null || attachmentEntry.Length == 0) - { - throw new FileNotFoundException("Cannot find attachment.", name); - } + readAttachments++; - readAttachments++; + return attachmentEntry; + } - return attachmentEntry; - } + public async IAsyncEnumerable<(string Stream, Envelope<IEvent> Event)> ReadEventsAsync( + IEventStreamNames eventStreamNames, + IEventFormatter eventFormatter, + [EnumeratorCancellation] CancellationToken ct = default) + { + Guard.NotNull(eventFormatter); + Guard.NotNull(eventStreamNames); - public async IAsyncEnumerable<(string Stream, Envelope<IEvent> Event)> ReadEventsAsync( - IEventStreamNames eventStreamNames, - IEventFormatter eventFormatter, - [EnumeratorCancellation] CancellationToken ct = default) + while (!ct.IsCancellationRequested) { - Guard.NotNull(eventFormatter); - Guard.NotNull(eventStreamNames); + var entry = archive.GetEntry(ArchiveHelper.GetEventPath(readEvents)); - while (!ct.IsCancellationRequested) + if (entry == null) { - var entry = archive.GetEntry(ArchiveHelper.GetEventPath(readEvents)); - - if (entry == null) - { - break; - } - - await using (var stream = entry.Open()) - { - var storedEvent = serializer.Deserialize<CompatibleStoredEvent>(stream).ToStoredEvent(); + break; + } - var eventStream = storedEvent.StreamName; - var eventEnvelope = eventFormatter.Parse(storedEvent); + await using (var stream = entry.Open()) + { + var storedEvent = serializer.Deserialize<CompatibleStoredEvent>(stream).ToStoredEvent(); - yield return (eventStream, eventEnvelope); - } + var eventStream = storedEvent.StreamName; + var eventEnvelope = eventFormatter.Parse(storedEvent); - readEvents++; + yield return (eventStream, eventEnvelope); } + + readEvents++; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs index 6d581493dc..8c4d528151 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupRestoreException.cs @@ -7,19 +7,18 @@ using System.Runtime.Serialization; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +[Serializable] +public class BackupRestoreException : Exception { - [Serializable] - public class BackupRestoreException : Exception + public BackupRestoreException(string message, Exception? inner = null) + : base(message, inner) { - public BackupRestoreException(string message, Exception? inner = null) - : base(message, inner) - { - } + } - protected BackupRestoreException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected BackupRestoreException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs index a34a0bbc03..19f9d76137 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs @@ -11,89 +11,88 @@ using Squidex.Infrastructure.States; using Squidex.Messaging; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class BackupService : IBackupService, IDeleter { - public sealed class BackupService : IBackupService, IDeleter + private readonly SimpleState<BackupRestoreState> restoreState; + private readonly IPersistenceFactory<BackupState> persistenceFactoryBackup; + private readonly IMessageBus messaging; + + public BackupService( + IPersistenceFactory<BackupRestoreState> persistenceFactoryRestore, + IPersistenceFactory<BackupState> persistenceFactoryBackup, + IMessageBus messaging) + { + this.persistenceFactoryBackup = persistenceFactoryBackup; + this.messaging = messaging; + + restoreState = new SimpleState<BackupRestoreState>(persistenceFactoryRestore, GetType(), "Default"); + } + + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return messaging.PublishAsync(new BackupClear(app.Id), ct: ct); + } + + public async Task StartBackupAsync(DomainId appId, RefToken actor, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, ct); + + state.Value.EnsureCanStart(); + + await messaging.PublishAsync(new BackupStart(appId, actor), ct: ct); + } + + public async Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName, + CancellationToken ct = default) + { + await restoreState.LoadAsync(ct); + + restoreState.Value.Job?.EnsureCanStart(); + + await messaging.PublishAsync(new BackupRestore(actor, url, newAppName), ct: ct); + } + + public Task DeleteBackupAsync(DomainId appId, DomainId backupId, + CancellationToken ct = default) { - private readonly SimpleState<BackupRestoreState> restoreState; - private readonly IPersistenceFactory<BackupState> persistenceFactoryBackup; - private readonly IMessageBus messaging; - - public BackupService( - IPersistenceFactory<BackupRestoreState> persistenceFactoryRestore, - IPersistenceFactory<BackupState> persistenceFactoryBackup, - IMessageBus messaging) - { - this.persistenceFactoryBackup = persistenceFactoryBackup; - this.messaging = messaging; - - restoreState = new SimpleState<BackupRestoreState>(persistenceFactoryRestore, GetType(), "Default"); - } - - Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - return messaging.PublishAsync(new BackupClear(app.Id), ct: ct); - } - - public async Task StartBackupAsync(DomainId appId, RefToken actor, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, ct); - - state.Value.EnsureCanStart(); - - await messaging.PublishAsync(new BackupStart(appId, actor), ct: ct); - } - - public async Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName, - CancellationToken ct = default) - { - await restoreState.LoadAsync(ct); - - restoreState.Value.Job?.EnsureCanStart(); - - await messaging.PublishAsync(new BackupRestore(actor, url, newAppName), ct: ct); - } - - public Task DeleteBackupAsync(DomainId appId, DomainId backupId, - CancellationToken ct = default) - { - return messaging.PublishAsync(new BackupDelete(appId, backupId), ct: ct); - } - - public async Task<IRestoreJob> GetRestoreAsync( - CancellationToken ct = default) - { - await restoreState.LoadAsync(ct); - - return restoreState.Value.Job ?? new RestoreJob(); - } - - public async Task<List<IBackupJob>> GetBackupsAsync(DomainId appId, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, ct); - - return state.Value.Jobs.OfType<IBackupJob>().ToList(); - } - - public async Task<IBackupJob?> GetBackupAsync(DomainId appId, DomainId backupId, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, ct); - - return state.Value.Jobs.Find(x => x.Id == backupId); - } - - private async Task<SimpleState<BackupState>> GetStateAsync(DomainId appId, - CancellationToken ct) - { - var state = new SimpleState<BackupState>(persistenceFactoryBackup, GetType(), appId); - - await state.LoadAsync(ct); - - return state; - } + return messaging.PublishAsync(new BackupDelete(appId, backupId), ct: ct); + } + + public async Task<IRestoreJob> GetRestoreAsync( + CancellationToken ct = default) + { + await restoreState.LoadAsync(ct); + + return restoreState.Value.Job ?? new RestoreJob(); + } + + public async Task<List<IBackupJob>> GetBackupsAsync(DomainId appId, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, ct); + + return state.Value.Jobs.OfType<IBackupJob>().ToList(); + } + + public async Task<IBackupJob?> GetBackupAsync(DomainId appId, DomainId backupId, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, ct); + + return state.Value.Jobs.Find(x => x.Id == backupId); + } + + private async Task<SimpleState<BackupState>> GetStateAsync(DomainId appId, + CancellationToken ct) + { + var state = new SimpleState<BackupState>(persistenceFactoryBackup, GetType(), appId); + + await state.LoadAsync(ct); + + return state; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs index e6cddcd7dc..591330d3ca 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupVersion.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public enum BackupVersion { - public enum BackupVersion - { - V2, - V1 - } + V2, + V1 } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWorker.cs index 8da5ae697c..c7dbac6f3b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWorker.cs @@ -10,80 +10,79 @@ using Squidex.Infrastructure; using Squidex.Messaging; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class BackupWorker : + IMessageHandler<BackupRestore>, + IMessageHandler<BackupStart>, + IMessageHandler<BackupDelete>, + IMessageHandler<BackupClear>, + IInitializable { - public sealed class BackupWorker : - IMessageHandler<BackupRestore>, - IMessageHandler<BackupStart>, - IMessageHandler<BackupDelete>, - IMessageHandler<BackupClear>, - IInitializable + private readonly Dictionary<DomainId, Task<BackupProcessor>> backupProcessors = new Dictionary<DomainId, Task<BackupProcessor>>(); + private readonly Func<DomainId, BackupProcessor> backupFactory; + private readonly RestoreProcessor restoreProcessor; + + public BackupWorker(IServiceProvider serviceProvider) { - private readonly Dictionary<DomainId, Task<BackupProcessor>> backupProcessors = new Dictionary<DomainId, Task<BackupProcessor>>(); - private readonly Func<DomainId, BackupProcessor> backupFactory; - private readonly RestoreProcessor restoreProcessor; + var objectFactory = ActivatorUtilities.CreateFactory(typeof(BackupProcessor), new[] { typeof(DomainId) }); - public BackupWorker(IServiceProvider serviceProvider) + backupFactory = key => { - var objectFactory = ActivatorUtilities.CreateFactory(typeof(BackupProcessor), new[] { typeof(DomainId) }); - - backupFactory = key => - { - return (BackupProcessor)objectFactory(serviceProvider, new object[] { key }); - }; + return (BackupProcessor)objectFactory(serviceProvider, new object[] { key }); + }; - restoreProcessor = serviceProvider.GetRequiredService<RestoreProcessor>(); - } + restoreProcessor = serviceProvider.GetRequiredService<RestoreProcessor>(); + } - public Task InitializeAsync( - CancellationToken ct) - { - return restoreProcessor.LoadAsync(ct); - } + public Task InitializeAsync( + CancellationToken ct) + { + return restoreProcessor.LoadAsync(ct); + } - public Task HandleAsync(BackupRestore message, - CancellationToken ct) - { - return restoreProcessor.RestoreAsync(message.Url, message.Actor, message.NewAppName, ct); - } + public Task HandleAsync(BackupRestore message, + CancellationToken ct) + { + return restoreProcessor.RestoreAsync(message.Url, message.Actor, message.NewAppName, ct); + } - public async Task HandleAsync(BackupStart message, - CancellationToken ct) - { - var processor = await GetBackupProcessorAsync(message.AppId); + public async Task HandleAsync(BackupStart message, + CancellationToken ct) + { + var processor = await GetBackupProcessorAsync(message.AppId); - await processor.BackupAsync(message.Actor, ct); - } + await processor.BackupAsync(message.Actor, ct); + } - public async Task HandleAsync(BackupDelete message, - CancellationToken ct) - { - var processor = await GetBackupProcessorAsync(message.AppId); + public async Task HandleAsync(BackupDelete message, + CancellationToken ct) + { + var processor = await GetBackupProcessorAsync(message.AppId); - await processor.DeleteAsync(message.Id); - } + await processor.DeleteAsync(message.Id); + } - public async Task HandleAsync(BackupClear message, - CancellationToken ct) - { - var processor = await GetBackupProcessorAsync(message.AppId); + public async Task HandleAsync(BackupClear message, + CancellationToken ct) + { + var processor = await GetBackupProcessorAsync(message.AppId); - await processor.ClearAsync(); - } + await processor.ClearAsync(); + } - private Task<BackupProcessor> GetBackupProcessorAsync(DomainId appId) + private Task<BackupProcessor> GetBackupProcessorAsync(DomainId appId) + { + lock (backupProcessors) { - lock (backupProcessors) + return backupProcessors.GetOrAdd(appId, async key => { - return backupProcessors.GetOrAdd(appId, async key => - { - var processor = backupFactory(key); + var processor = backupFactory(key); - await processor.LoadAsync(default); + await processor.LoadAsync(default); - return processor; - }); - } + return processor; + }); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs index 6bbfb44a19..e3faa5e45a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs @@ -12,95 +12,94 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class BackupWriter : DisposableObjectBase, IBackupWriter { - public sealed class BackupWriter : DisposableObjectBase, IBackupWriter - { - private readonly ZipArchive archive; - private readonly IJsonSerializer serializer; - private readonly Func<StoredEvent, CompatibleStoredEvent> converter; - private int writtenEvents; - private int writtenAttachments; + private readonly ZipArchive archive; + private readonly IJsonSerializer serializer; + private readonly Func<StoredEvent, CompatibleStoredEvent> converter; + private int writtenEvents; + private int writtenAttachments; - public int WrittenEvents - { - get => writtenEvents; - } + public int WrittenEvents + { + get => writtenEvents; + } - public int WrittenAttachments - { - get => writtenAttachments; - } + public int WrittenAttachments + { + get => writtenAttachments; + } - public BackupWriter(IJsonSerializer serializer, Stream stream, bool keepOpen = false, BackupVersion version = BackupVersion.V2) - { - Guard.NotNull(serializer); + public BackupWriter(IJsonSerializer serializer, Stream stream, bool keepOpen = false, BackupVersion version = BackupVersion.V2) + { + Guard.NotNull(serializer); - this.serializer = serializer; + this.serializer = serializer; - converter = - version == BackupVersion.V1 ? - new Func<StoredEvent, CompatibleStoredEvent>(CompatibleStoredEvent.V1) : - new Func<StoredEvent, CompatibleStoredEvent>(CompatibleStoredEvent.V2); + converter = + version == BackupVersion.V1 ? + new Func<StoredEvent, CompatibleStoredEvent>(CompatibleStoredEvent.V1) : + new Func<StoredEvent, CompatibleStoredEvent>(CompatibleStoredEvent.V2); - archive = new ZipArchive(stream, ZipArchiveMode.Create, keepOpen); - } + archive = new ZipArchive(stream, ZipArchiveMode.Create, keepOpen); + } - protected override void DisposeObject(bool disposing) + protected override void DisposeObject(bool disposing) + { + if (disposing) { - if (disposing) - { - archive.Dispose(); - } + archive.Dispose(); } + } - public Task<Stream> OpenBlobAsync(string name, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(name); - - writtenAttachments++; + public Task<Stream> OpenBlobAsync(string name, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(name); - var entry = GetEntry(name); + writtenAttachments++; - return Task.FromResult(entry.Open()); - } + var entry = GetEntry(name); - public async Task WriteJsonAsync(string name, object value, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(name); + return Task.FromResult(entry.Open()); + } - writtenAttachments++; + public async Task WriteJsonAsync(string name, object value, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(name); - var entry = GetEntry(name); + writtenAttachments++; - await using (var stream = entry.Open()) - { - serializer.Serialize(value, stream); - } - } + var entry = GetEntry(name); - private ZipArchiveEntry GetEntry(string name) + await using (var stream = entry.Open()) { - return archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); + serializer.Serialize(value, stream); } + } - public void WriteEvent(StoredEvent storedEvent, - CancellationToken ct = default) - { - Guard.NotNull(storedEvent); + private ZipArchiveEntry GetEntry(string name) + { + return archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); + } - var eventEntry = archive.CreateEntry(ArchiveHelper.GetEventPath(writtenEvents)); + public void WriteEvent(StoredEvent storedEvent, + CancellationToken ct = default) + { + Guard.NotNull(storedEvent); - using (var stream = eventEntry.Open()) - { - var @event = converter(storedEvent); + var eventEntry = archive.CreateEntry(ArchiveHelper.GetEventPath(writtenEvents)); - serializer.Serialize(@event, stream); - } + using (var stream = eventEntry.Open()) + { + var @event = converter(storedEvent); - writtenEvents++; + serializer.Serialize(@event, stream); } + + writtenEvents++; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs index c021f954b0..06a30f91b7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs @@ -5,56 +5,55 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public static class CompatibilityExtensions { - public static class CompatibilityExtensions - { - private const string VersionFile = "Version.json"; - private static readonly FileVersion None = new FileVersion(); - private static readonly FileVersion Expected = new FileVersion { Major = 5 }; + private const string VersionFile = "Version.json"; + private static readonly FileVersion None = new FileVersion(); + private static readonly FileVersion Expected = new FileVersion { Major = 5 }; #pragma warning disable MA0077 // A class that provides Equals(T) should implement IEquatable<T> - public sealed class FileVersion + public sealed class FileVersion #pragma warning restore MA0077 // A class that provides Equals(T) should implement IEquatable<T> - { - public int Major { get; set; } + { + public int Major { get; set; } - public bool Equals(FileVersion other) - { - return Major == other.Major; - } + public bool Equals(FileVersion other) + { + return Major == other.Major; } + } + + public static Task WriteVersionAsync(this IBackupWriter writer) + { + return writer.WriteJsonAsync(VersionFile, Expected); + } - public static Task WriteVersionAsync(this IBackupWriter writer) + public static async Task CheckCompatibilityAsync(this IBackupReader reader) + { + var current = await reader.ReadVersionAsync(); + + if (None.Equals(current)) { - return writer.WriteJsonAsync(VersionFile, Expected); + return; } - public static async Task CheckCompatibilityAsync(this IBackupReader reader) + if (!Expected.Equals(current)) { - var current = await reader.ReadVersionAsync(); - - if (None.Equals(current)) - { - return; - } - - if (!Expected.Equals(current)) - { - throw new BackupRestoreException("Backup file is not compatible with this version."); - } + throw new BackupRestoreException("Backup file is not compatible with this version."); } + } - private static async Task<FileVersion> ReadVersionAsync(this IBackupReader reader) + private static async Task<FileVersion> ReadVersionAsync(this IBackupReader reader) + { + try + { + return await reader.ReadJsonAsync<FileVersion>(VersionFile); + } + catch { - try - { - return await reader.ReadJsonAsync<FileVersion>(VersionFile); - } - catch - { - return None; - } + return None; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs index 77e2b074a8..c48bc07855 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs @@ -8,44 +8,43 @@ using Squidex.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class DefaultBackupArchiveStore : IBackupArchiveStore { - public sealed class DefaultBackupArchiveStore : IBackupArchiveStore - { - private readonly IAssetStore assetStore; + private readonly IAssetStore assetStore; - public DefaultBackupArchiveStore(IAssetStore assetStore) - { - this.assetStore = assetStore; - } + public DefaultBackupArchiveStore(IAssetStore assetStore) + { + this.assetStore = assetStore; + } - public Task DownloadAsync(DomainId backupId, Stream stream, - CancellationToken ct = default) - { - var fileName = GetFileName(backupId); + public Task DownloadAsync(DomainId backupId, Stream stream, + CancellationToken ct = default) + { + var fileName = GetFileName(backupId); - return assetStore.DownloadAsync(fileName, stream, default, ct); - } + return assetStore.DownloadAsync(fileName, stream, default, ct); + } - public Task UploadAsync(DomainId backupId, Stream stream, - CancellationToken ct = default) - { - var fileName = GetFileName(backupId); + public Task UploadAsync(DomainId backupId, Stream stream, + CancellationToken ct = default) + { + var fileName = GetFileName(backupId); - return assetStore.UploadAsync(fileName, stream, true, ct); - } + return assetStore.UploadAsync(fileName, stream, true, ct); + } - public Task DeleteAsync(DomainId backupId, - CancellationToken ct = default) - { - var fileName = GetFileName(backupId); + public Task DeleteAsync(DomainId backupId, + CancellationToken ct = default) + { + var fileName = GetFileName(backupId); - return assetStore.DeleteAsync(fileName, ct); - } + return assetStore.DeleteAsync(fileName, ct); + } - private static string GetFileName(DomainId backupId) - { - return $"{backupId}_0"; - } + private static string GetFileName(DomainId backupId) + { + return $"{backupId}_0"; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupHandlerFactory.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupHandlerFactory.cs index b5f0a0c053..f479fdc69b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupHandlerFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupHandlerFactory.cs @@ -7,20 +7,19 @@ using Microsoft.Extensions.DependencyInjection; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class DefaultBackupHandlerFactory : IBackupHandlerFactory { - public sealed class DefaultBackupHandlerFactory : IBackupHandlerFactory - { - private readonly IServiceProvider serviceProvider; + private readonly IServiceProvider serviceProvider; - public DefaultBackupHandlerFactory(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } + public DefaultBackupHandlerFactory(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } - public IEnumerable<IBackupHandler> CreateMany() - { - return serviceProvider.GetRequiredService<IEnumerable<IBackupHandler>>(); - } + public IEnumerable<IBackupHandler> CreateMany() + { + return serviceProvider.GetRequiredService<IEnumerable<IBackupHandler>>(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs index e194918fe3..8721861fe1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs @@ -5,44 +5,43 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Backup.Helpers +namespace Squidex.Domain.Apps.Entities.Backup.Helpers; + +public static class ArchiveHelper { - public static class ArchiveHelper + private const int MaxAttachmentFolders = 1000; + private const int MaxEventsPerFolder = 1000; + + public static string GetAttachmentPath(string name) { - private const int MaxAttachmentFolders = 1000; - private const int MaxEventsPerFolder = 1000; + name = name.ToLowerInvariant(); - public static string GetAttachmentPath(string name) - { - name = name.ToLowerInvariant(); + var attachmentFolder = SimpleHash(name) % MaxAttachmentFolders; + var attachmentPath = $"attachments/{attachmentFolder}/{name}"; - var attachmentFolder = SimpleHash(name) % MaxAttachmentFolders; - var attachmentPath = $"attachments/{attachmentFolder}/{name}"; + return attachmentPath; + } - return attachmentPath; - } + public static string GetEventPath(int index) + { + var eventFolder = index / MaxEventsPerFolder; + var eventPath = $"events/{eventFolder}/{index}.json"; - public static string GetEventPath(int index) - { - var eventFolder = index / MaxEventsPerFolder; - var eventPath = $"events/{eventFolder}/{index}.json"; + return eventPath; + } - return eventPath; - } + private static int SimpleHash(string value) + { + var hash = 17; - private static int SimpleHash(string value) + foreach (var c in value) { - var hash = 17; - - foreach (var c in value) + unchecked { - unchecked - { - hash = (hash * 23) + c.GetHashCode(); - } + hash = (hash * 23) + c.GetHashCode(); } - - return Math.Abs(hash); } + + return Math.Abs(hash); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs index 28024fb2dc..3ffd95883a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs @@ -7,16 +7,15 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IBackupArchiveLocation { - public interface IBackupArchiveLocation - { - Stream OpenStream(DomainId backupId); + Stream OpenStream(DomainId backupId); - Task<IBackupWriter> OpenWriterAsync(Stream stream, - CancellationToken ct); + Task<IBackupWriter> OpenWriterAsync(Stream stream, + CancellationToken ct); - Task<IBackupReader> OpenReaderAsync(Uri url, DomainId id, - CancellationToken ct); - } + Task<IBackupReader> OpenReaderAsync(Uri url, DomainId id, + CancellationToken ct); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveStore.cs index 8ad5429a36..c8ed2c9780 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveStore.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IBackupArchiveStore { - public interface IBackupArchiveStore - { - Task UploadAsync(DomainId backupId, Stream stream, - CancellationToken ct = default); + Task UploadAsync(DomainId backupId, Stream stream, + CancellationToken ct = default); - Task DownloadAsync(DomainId backupId, Stream stream, - CancellationToken ct = default); + Task DownloadAsync(DomainId backupId, Stream stream, + CancellationToken ct = default); - Task DeleteAsync(DomainId backupId, - CancellationToken ct = default); - } + Task DeleteAsync(DomainId backupId, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs index 093054ece4..f90739351c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs @@ -8,49 +8,48 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IBackupHandler { - public interface IBackupHandler + string Name { get; } + + public Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, + CancellationToken ct) + { + return Task.FromResult(true); + } + + public Task BackupEventAsync(Envelope<IEvent> @event, BackupContext context, + CancellationToken ct) + { + return Task.CompletedTask; + } + + public Task RestoreAsync(RestoreContext context, + CancellationToken ct) + { + return Task.CompletedTask; + } + + public Task BackupAsync(BackupContext context, + CancellationToken ct) + { + return Task.CompletedTask; + } + + public Task CleanupRestoreErrorAsync(DomainId appId) + { + return Task.CompletedTask; + } + + public Task CompleteRestoreAsync(RestoreContext context, string appName) + { + return Task.CompletedTask; + } + + public Task CompleteBackupAsync(BackupContext context) { - string Name { get; } - - public Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, - CancellationToken ct) - { - return Task.FromResult(true); - } - - public Task BackupEventAsync(Envelope<IEvent> @event, BackupContext context, - CancellationToken ct) - { - return Task.CompletedTask; - } - - public Task RestoreAsync(RestoreContext context, - CancellationToken ct) - { - return Task.CompletedTask; - } - - public Task BackupAsync(BackupContext context, - CancellationToken ct) - { - return Task.CompletedTask; - } - - public Task CleanupRestoreErrorAsync(DomainId appId) - { - return Task.CompletedTask; - } - - public Task CompleteRestoreAsync(RestoreContext context, string appName) - { - return Task.CompletedTask; - } - - public Task CompleteBackupAsync(BackupContext context) - { - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandlerFactory.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandlerFactory.cs index 9fc7e0130d..12e45cdb1b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandlerFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandlerFactory.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IBackupHandlerFactory { - public interface IBackupHandlerFactory - { - IEnumerable<IBackupHandler> CreateMany(); - } + IEnumerable<IBackupHandler> CreateMany(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs index 1cc4df501c..13698b5e44 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupJob.cs @@ -8,18 +8,17 @@ using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IBackupJob : IWithId<DomainId> { - public interface IBackupJob : IWithId<DomainId> - { - Instant Started { get; } + Instant Started { get; } - Instant? Stopped { get; } + Instant? Stopped { get; } - int HandledEvents { get; } + int HandledEvents { get; } - int HandledAssets { get; } + int HandledAssets { get; } - JobStatus Status { get; } - } + JobStatus Status { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs index c0fc9e60b1..8153b05113 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs @@ -8,26 +8,25 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IBackupReader : IDisposable { - public interface IBackupReader : IDisposable - { - int ReadAttachments { get; } + int ReadAttachments { get; } - int ReadEvents { get; } + int ReadEvents { get; } - Task<Stream> OpenBlobAsync(string name, - CancellationToken ct = default); + Task<Stream> OpenBlobAsync(string name, + CancellationToken ct = default); - Task<T> ReadJsonAsync<T>(string name, - CancellationToken ct = default); + Task<T> ReadJsonAsync<T>(string name, + CancellationToken ct = default); - Task<bool> HasFileAsync(string name, - CancellationToken ct = default); + Task<bool> HasFileAsync(string name, + CancellationToken ct = default); - IAsyncEnumerable<(string Stream, Envelope<IEvent> Event)> ReadEventsAsync( - IEventStreamNames eventStreamNames, - IEventFormatter eventFormatter, - CancellationToken ct = default); - } + IAsyncEnumerable<(string Stream, Envelope<IEvent> Event)> ReadEventsAsync( + IEventStreamNames eventStreamNames, + IEventFormatter eventFormatter, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs index 27de3e77b9..866114a442 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs @@ -7,26 +7,25 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IBackupService { - public interface IBackupService - { - Task StartBackupAsync(DomainId appId, RefToken actor, - CancellationToken ct = default); + Task StartBackupAsync(DomainId appId, RefToken actor, + CancellationToken ct = default); - Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName, - CancellationToken ct = default); + Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName, + CancellationToken ct = default); - Task<IRestoreJob> GetRestoreAsync( - CancellationToken ct = default); + Task<IRestoreJob> GetRestoreAsync( + CancellationToken ct = default); - Task<List<IBackupJob>> GetBackupsAsync(DomainId appId, - CancellationToken ct = default); + Task<List<IBackupJob>> GetBackupsAsync(DomainId appId, + CancellationToken ct = default); - Task<IBackupJob?> GetBackupAsync(DomainId appId, DomainId backupId, - CancellationToken ct = default); + Task<IBackupJob?> GetBackupAsync(DomainId appId, DomainId backupId, + CancellationToken ct = default); - Task DeleteBackupAsync(DomainId appId, DomainId backupId, - CancellationToken ct = default); - } + Task DeleteBackupAsync(DomainId appId, DomainId backupId, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupWriter.cs index eb1a73fb2f..b2347119ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupWriter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupWriter.cs @@ -7,21 +7,20 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IBackupWriter : IDisposable { - public interface IBackupWriter : IDisposable - { - int WrittenAttachments { get; } + int WrittenAttachments { get; } - int WrittenEvents { get; } + int WrittenEvents { get; } - Task<Stream> OpenBlobAsync(string name, - CancellationToken ct = default); + Task<Stream> OpenBlobAsync(string name, + CancellationToken ct = default); - void WriteEvent(StoredEvent storedEvent, - CancellationToken ct = default); + void WriteEvent(StoredEvent storedEvent, + CancellationToken ct = default); - Task WriteJsonAsync(string name, object value, - CancellationToken ct = default); - } + Task WriteJsonAsync(string name, object value, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs index 2d4410869a..95029c8530 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreJob.cs @@ -7,18 +7,17 @@ using NodaTime; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IRestoreJob { - public interface IRestoreJob - { - Uri Url { get; } + Uri Url { get; } - Instant Started { get; } + Instant Started { get; } - Instant? Stopped { get; } + Instant? Stopped { get; } - List<string> Log { get; } + List<string> Log { get; } - JobStatus Status { get; } - } + JobStatus Status { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IUserMapping.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IUserMapping.cs index 9816d6d475..0c3b50b899 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IUserMapping.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IUserMapping.cs @@ -7,18 +7,17 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public interface IUserMapping { - public interface IUserMapping - { - RefToken Initiator { get; } + RefToken Initiator { get; } - void Backup(RefToken token); + void Backup(RefToken token); - void Backup(string userId); + void Backup(string userId); - bool TryMap(RefToken token, out RefToken result); + bool TryMap(RefToken token, out RefToken result); - bool TryMap(string userId, out RefToken result); - } + bool TryMap(string userId, out RefToken result); } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs index 26f6f541c9..6ab7109dd0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/JobStatus.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public enum JobStatus { - public enum JobStatus - { - Created, - Started, - Completed, - Failed - } + Created, + Started, + Completed, + Failed } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/Messages.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Messages.cs index 12d153b0ca..df7537bcf7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/Messages.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/Messages.cs @@ -10,13 +10,12 @@ #pragma warning disable MA0048 // File name must match type name #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Backup -{ - public sealed record BackupRestore(RefToken Actor, Uri Url, string? NewAppName = null); +namespace Squidex.Domain.Apps.Entities.Backup; - public sealed record BackupStart(DomainId AppId, RefToken Actor); +public sealed record BackupRestore(RefToken Actor, Uri Url, string? NewAppName = null); - public sealed record BackupDelete(DomainId AppId, DomainId Id); +public sealed record BackupStart(DomainId AppId, RefToken Actor); - public sealed record BackupClear(DomainId AppId); -} +public sealed record BackupDelete(DomainId AppId, DomainId Id); + +public sealed record BackupClear(DomainId AppId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs index a630c33cf7..5a0f36f0fe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs @@ -11,114 +11,113 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Backup.Model +namespace Squidex.Domain.Apps.Entities.Backup.Model; + +public sealed class CompatibleStoredEvent { - public sealed class CompatibleStoredEvent - { - [JsonPropertyName("n")] - public NewEvent NewEvent { get; set; } + [JsonPropertyName("n")] + public NewEvent NewEvent { get; set; } - [JsonPropertyName("streamName")] - public string StreamName { get; set; } + [JsonPropertyName("streamName")] + public string StreamName { get; set; } - [JsonPropertyName("eventPosition")] - public string EventPosition { get; set; } + [JsonPropertyName("eventPosition")] + public string EventPosition { get; set; } - [JsonPropertyName("eventStreamNumber")] - public long EventStreamNumber { get; set; } + [JsonPropertyName("eventStreamNumber")] + public long EventStreamNumber { get; set; } - [JsonPropertyName("data")] - public CompatibleEventData Data { get; set; } + [JsonPropertyName("data")] + public CompatibleEventData Data { get; set; } - public static CompatibleStoredEvent V1(StoredEvent stored) + public static CompatibleStoredEvent V1(StoredEvent stored) + { + return new CompatibleStoredEvent { - return new CompatibleStoredEvent - { - Data = CompatibleEventData.V1(stored.Data), - EventPosition = stored.EventPosition, - EventStreamNumber = stored.EventStreamNumber, - StreamName = stored.StreamName - }; - } + Data = CompatibleEventData.V1(stored.Data), + EventPosition = stored.EventPosition, + EventStreamNumber = stored.EventStreamNumber, + StreamName = stored.StreamName + }; + } + + public static CompatibleStoredEvent V2(StoredEvent stored) + { + return new CompatibleStoredEvent { NewEvent = NewEvent.V2(stored) }; + } - public static CompatibleStoredEvent V2(StoredEvent stored) + public StoredEvent ToStoredEvent() + { + if (NewEvent != null) { - return new CompatibleStoredEvent { NewEvent = NewEvent.V2(stored) }; + return NewEvent.ToStoredEvent(); } - - public StoredEvent ToStoredEvent() + else { - if (NewEvent != null) - { - return NewEvent.ToStoredEvent(); - } - else - { - var data = Data.ToData(); - - return new StoredEvent(StreamName, EventPosition, EventStreamNumber, data); - } + var data = Data.ToData(); + + return new StoredEvent(StreamName, EventPosition, EventStreamNumber, data); } } +} - public sealed class CompatibleEventData - { - [JsonPropertyName("type")] - public string Type { get; set; } +public sealed class CompatibleEventData +{ + [JsonPropertyName("type")] + public string Type { get; set; } - [JsonPropertyName("metadata")] - public EnvelopeHeaders EventHeaders { get; set; } + [JsonPropertyName("metadata")] + public EnvelopeHeaders EventHeaders { get; set; } - [JsonPropertyName("payload")] - [JsonConverter(typeof(UnsafeRawJsonConverter))] - public string EventPayload { get; set; } + [JsonPropertyName("payload")] + [JsonConverter(typeof(UnsafeRawJsonConverter))] + public string EventPayload { get; set; } - public static CompatibleEventData V1(EventData data) - { - return new CompatibleEventData - { - Type = data.Type, - EventPayload = data.Payload, - EventHeaders = data.Headers - }; - } - - public EventData ToData() + public static CompatibleEventData V1(EventData data) + { + return new CompatibleEventData { - return new EventData(Type, EventHeaders, EventPayload); - } + Type = data.Type, + EventPayload = data.Payload, + EventHeaders = data.Headers + }; } - public sealed class NewEvent + public EventData ToData() { - [JsonPropertyName("t")] - public string EventType { get; set; } + return new EventData(Type, EventHeaders, EventPayload); + } +} + +public sealed class NewEvent +{ + [JsonPropertyName("t")] + public string EventType { get; set; } - [JsonPropertyName("s")] - public string StreamName { get; set; } + [JsonPropertyName("s")] + public string StreamName { get; set; } - [JsonPropertyName("p")] - public string EventPayload { get; set; } + [JsonPropertyName("p")] + public string EventPayload { get; set; } - [JsonPropertyName("h")] - public EnvelopeHeaders EventHeaders { get; set; } + [JsonPropertyName("h")] + public EnvelopeHeaders EventHeaders { get; set; } - public static NewEvent V2(StoredEvent stored) + public static NewEvent V2(StoredEvent stored) + { + return new NewEvent { - return new NewEvent - { - EventType = stored.Data.Type, - EventHeaders = stored.Data.Headers, - EventPayload = stored.Data.Payload, - StreamName = stored.StreamName - }; - } + EventType = stored.Data.Type, + EventHeaders = stored.Data.Headers, + EventPayload = stored.Data.Payload, + StreamName = stored.StreamName + }; + } - public StoredEvent ToStoredEvent() - { - var data = new EventData(EventType, EventHeaders, EventPayload); + public StoredEvent ToStoredEvent() + { + var data = new EventData(EventType, EventHeaders, EventPayload); - return new StoredEvent(StreamName, "0", -1, data); - } + return new StoredEvent(StreamName, "0", -1, data); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreContext.cs index 8e58b555ec..0c420cd4ed 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreContext.cs @@ -7,22 +7,21 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class RestoreContext : BackupContextBase { - public sealed class RestoreContext : BackupContextBase - { - public IBackupReader Reader { get; } + public IBackupReader Reader { get; } - public DomainId PreviousAppId { get; set; } + public DomainId PreviousAppId { get; set; } - public RestoreContext(DomainId appId, IUserMapping userMapping, IBackupReader reader, DomainId previousAppId) - : base(appId, userMapping) - { - Guard.NotNull(reader); + public RestoreContext(DomainId appId, IUserMapping userMapping, IBackupReader reader, DomainId previousAppId) + : base(appId, userMapping) + { + Guard.NotNull(reader); - Reader = reader; + Reader = reader; - PreviousAppId = previousAppId; - } + PreviousAppId = previousAppId; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.Run.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.Run.cs index d99db2e260..2f066da070 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.Run.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.Run.cs @@ -7,51 +7,50 @@ using Squidex.Domain.Apps.Entities.Backup.State; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed partial class RestoreProcessor { - public sealed partial class RestoreProcessor + // Use a run to store all state that is necessary for a single run. + private sealed class Run : IDisposable { - // Use a run to store all state that is necessary for a single run. - private sealed class Run : IDisposable - { - private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); - private readonly CancellationTokenSource cancellationLinked; + private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly CancellationTokenSource cancellationLinked; - public IEnumerable<IBackupHandler> Handlers { get; init; } + public IEnumerable<IBackupHandler> Handlers { get; init; } - public IBackupReader Reader { get; set; } + public IBackupReader Reader { get; set; } - public RestoreJob Job { get; init; } + public RestoreJob Job { get; init; } - public RestoreContext Context { get; set; } + public RestoreContext Context { get; set; } - public StreamMapper StreamMapper { get; set; } + public StreamMapper StreamMapper { get; set; } - public CancellationToken CancellationToken => cancellationLinked.Token; + public CancellationToken CancellationToken => cancellationLinked.Token; - public Run(CancellationToken ct) - { - cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); - } + public Run(CancellationToken ct) + { + cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); + } - public void Dispose() - { - Reader?.Dispose(); + public void Dispose() + { + Reader?.Dispose(); - cancellationSource.Dispose(); - cancellationLinked.Dispose(); - } + cancellationSource.Dispose(); + cancellationLinked.Dispose(); + } - public void Cancel() + public void Cancel() + { + try + { + cancellationSource.Cancel(); + } + catch (ObjectDisposedException) { - try - { - cancellationSource.Cancel(); - } - catch (ObjectDisposedException) - { - // Cancellation token might have been disposed, if the run is completed. - } + // Cancellation token might have been disposed, if the run is completed. } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs index 0aa4d0099d..80b888bbfe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs @@ -21,432 +21,431 @@ using Squidex.Infrastructure.Translations; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed partial class RestoreProcessor { - public sealed partial class RestoreProcessor + private readonly IBackupArchiveLocation backupArchiveLocation; + private readonly IBackupHandlerFactory backupHandlerFactory; + private readonly ICommandBus commandBus; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly IEventStreamNames eventStreamNames; + private readonly IUserResolver userResolver; + private readonly ILogger<RestoreProcessor> log; + private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); + private readonly SimpleState<BackupRestoreState> state; + private Run? currentRun; + + public IClock Clock { get; set; } = SystemClock.Instance; + + public RestoreProcessor( + IBackupArchiveLocation backupArchiveLocation, + IBackupHandlerFactory backupHandlerFactory, + ICommandBus commandBus, + IEventFormatter eventFormatter, + IEventStore eventStore, + IEventStreamNames eventStreamNames, + IPersistenceFactory<BackupRestoreState> persistenceFactory, + IUserResolver userResolver, + ILogger<RestoreProcessor> log) + { + this.backupArchiveLocation = backupArchiveLocation; + this.backupHandlerFactory = backupHandlerFactory; + this.commandBus = commandBus; + this.eventFormatter = eventFormatter; + this.eventStore = eventStore; + this.eventStreamNames = eventStreamNames; + this.userResolver = userResolver; + this.log = log; + + // Enable locking for the parallel operations that might write stuff. + state = new SimpleState<BackupRestoreState>(persistenceFactory, GetType(), "Default", true); + } + + public async Task LoadAsync( + CancellationToken ct) { - private readonly IBackupArchiveLocation backupArchiveLocation; - private readonly IBackupHandlerFactory backupHandlerFactory; - private readonly ICommandBus commandBus; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly IEventStreamNames eventStreamNames; - private readonly IUserResolver userResolver; - private readonly ILogger<RestoreProcessor> log; - private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); - private readonly SimpleState<BackupRestoreState> state; - private Run? currentRun; - - public IClock Clock { get; set; } = SystemClock.Instance; - - public RestoreProcessor( - IBackupArchiveLocation backupArchiveLocation, - IBackupHandlerFactory backupHandlerFactory, - ICommandBus commandBus, - IEventFormatter eventFormatter, - IEventStore eventStore, - IEventStreamNames eventStreamNames, - IPersistenceFactory<BackupRestoreState> persistenceFactory, - IUserResolver userResolver, - ILogger<RestoreProcessor> log) + await state.LoadAsync(ct); + + if (state.Value.Job?.Status == JobStatus.Started) { - this.backupArchiveLocation = backupArchiveLocation; - this.backupHandlerFactory = backupHandlerFactory; - this.commandBus = commandBus; - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; - this.eventStreamNames = eventStreamNames; - this.userResolver = userResolver; - this.log = log; - - // Enable locking for the parallel operations that might write stuff. - state = new SimpleState<BackupRestoreState>(persistenceFactory, GetType(), "Default", true); + state.Value.Job.Status = JobStatus.Failed; + + await state.WriteAsync(ct); } + } + + public Task RestoreAsync(Uri url, RefToken actor, string? newAppName, + CancellationToken ct) + { + Guard.NotNull(url); + Guard.NotNull(actor); - public async Task LoadAsync( - CancellationToken ct) + if (!string.IsNullOrWhiteSpace(newAppName)) { - await state.LoadAsync(ct); + Guard.ValidSlug(newAppName); + } - if (state.Value.Job?.Status == JobStatus.Started) + return scheduler.ScheduleAsync(async ct => + { + if (currentRun != null) { - state.Value.Job.Status = JobStatus.Failed; - - await state.WriteAsync(ct); + throw new DomainException(T.Get("backups.restoreRunning")); } - } - public Task RestoreAsync(Uri url, RefToken actor, string? newAppName, - CancellationToken ct) - { - Guard.NotNull(url); - Guard.NotNull(actor); + state.Value.Job?.EnsureCanStart(); - if (!string.IsNullOrWhiteSpace(newAppName)) + // Set the current run first to indicate that we are running a rule at the moment. + var run = currentRun = new Run(ct) + { + Job = new RestoreJob + { + Id = DomainId.NewGuid(), + NewAppName = newAppName, + Actor = actor, + Started = Clock.GetCurrentInstant(), + Status = JobStatus.Started, + Url = url + }, + Handlers = backupHandlerFactory.CreateMany() + }; + + state.Value.Job = run.Job; + try { - Guard.ValidSlug(newAppName); + await ProcessAsync(run, run.CancellationToken); } + finally + { + // Unset the run to indicate that we are done. + currentRun.Dispose(); + currentRun = null; + } + }, ct); + } - return scheduler.ScheduleAsync(async ct => + private async Task ProcessAsync(Run run, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("RestoreBackup")) + { + try { - if (currentRun != null) - { - throw new DomainException(T.Get("backups.restoreRunning")); - } + await state.WriteAsync(run.CancellationToken); - state.Value.Job?.EnsureCanStart(); + await LogAsync(run, "Started. The restore process has the following steps:"); + await LogAsync(run, " * Download backup"); + await LogAsync(run, " * Restore events and attachments."); + await LogAsync(run, " * Restore all objects like app, schemas and contents"); + await LogAsync(run, " * Complete the restore operation for all objects"); - // Set the current run first to indicate that we are running a rule at the moment. - var run = currentRun = new Run(ct) - { - Job = new RestoreJob - { - Id = DomainId.NewGuid(), - NewAppName = newAppName, - Actor = actor, - Started = Clock.GetCurrentInstant(), - Status = JobStatus.Started, - Url = url - }, - Handlers = backupHandlerFactory.CreateMany() - }; - - state.Value.Job = run.Job; - try + log.LogInformation("Backup with job id {backupId} with from URL '{url}' started.", run.Job.Id, run.Job.Url); + + run.Reader = await DownloadAsync(run, ct); + + await run.Reader.CheckCompatibilityAsync(); + + using (Telemetry.Activities.StartActivity("ReadEvents")) { - await ProcessAsync(run, run.CancellationToken); + await ReadEventsAsync(run, ct); } - finally + + if (run.Context == null) { - // Unset the run to indicate that we are done. - currentRun.Dispose(); - currentRun = null; + throw new BackupRestoreException("Backup has no event."); } - }, ct); - } - private async Task ProcessAsync(Run run, - CancellationToken ct) - { - using (Telemetry.Activities.StartActivity("RestoreBackup")) - { - try + foreach (var handler in run.Handlers) { - await state.WriteAsync(run.CancellationToken); - - await LogAsync(run, "Started. The restore process has the following steps:"); - await LogAsync(run, " * Download backup"); - await LogAsync(run, " * Restore events and attachments."); - await LogAsync(run, " * Restore all objects like app, schemas and contents"); - await LogAsync(run, " * Complete the restore operation for all objects"); - - log.LogInformation("Backup with job id {backupId} with from URL '{url}' started.", run.Job.Id, run.Job.Url); - - run.Reader = await DownloadAsync(run, ct); - - await run.Reader.CheckCompatibilityAsync(); - - using (Telemetry.Activities.StartActivity("ReadEvents")) + using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/RestoreAsync")) { - await ReadEventsAsync(run, ct); + await handler.RestoreAsync(run.Context, ct); } - if (run.Context == null) - { - throw new BackupRestoreException("Backup has no event."); - } + await LogAsync(run, $"Restored {handler.Name}"); + } - foreach (var handler in run.Handlers) + foreach (var handler in run.Handlers) + { + using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/CompleteRestoreAsync")) { - using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/RestoreAsync")) - { - await handler.RestoreAsync(run.Context, ct); - } - - await LogAsync(run, $"Restored {handler.Name}"); + await handler.CompleteRestoreAsync(run.Context, run.Job.NewAppName!); } - foreach (var handler in run.Handlers) - { - using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/CompleteRestoreAsync")) - { - await handler.CompleteRestoreAsync(run.Context, run.Job.NewAppName!); - } - - await LogAsync(run, $"Completed {handler.Name}"); - } + await LogAsync(run, $"Completed {handler.Name}"); + } - // Add the current user to the app, so that the admin can see it and verify integrity. - await AssignContributorAsync(run); + // Add the current user to the app, so that the admin can see it and verify integrity. + await AssignContributorAsync(run); - await SetStatusAsync(run, JobStatus.Completed, "Completed, Yeah!"); + await SetStatusAsync(run, JobStatus.Completed, "Completed, Yeah!"); - log.LogInformation("Backup with job id {backupId} from URL '{url}' completed.", run.Job.Id, run.Job.Url); - } - catch (Exception ex) - { - // Cleanup as soon as possible. - await CleanupAsync(run); + log.LogInformation("Backup with job id {backupId} from URL '{url}' completed.", run.Job.Id, run.Job.Url); + } + catch (Exception ex) + { + // Cleanup as soon as possible. + await CleanupAsync(run); - var message = "Failed with internal error."; + var message = "Failed with internal error."; - switch (ex) - { - case BackupRestoreException backupException: - message = backupException.Message; - break; - case FileNotFoundException fileNotFoundException: - message = fileNotFoundException.Message; - break; - } + switch (ex) + { + case BackupRestoreException backupException: + message = backupException.Message; + break; + case FileNotFoundException fileNotFoundException: + message = fileNotFoundException.Message; + break; + } - await SetStatusAsync(run, JobStatus.Failed, message); + await SetStatusAsync(run, JobStatus.Failed, message); - log.LogError(ex, "Backup with job id {backupId} from URL '{url}' failed.", run.Job.Id, run.Job.Url); - } + log.LogError(ex, "Backup with job id {backupId} from URL '{url}' failed.", run.Job.Id, run.Job.Url); } } + } - private async Task AssignContributorAsync(Run run) + private async Task AssignContributorAsync(Run run) + { + if (run.Job.Actor?.IsUser != true) { - if (run.Job.Actor?.IsUser != true) - { - await LogAsync(run, "Current user not assigned because restore was triggered by client."); - return; - } + await LogAsync(run, "Current user not assigned because restore was triggered by client."); + return; + } - try + try + { + // Add the current user to the app, so that the admin can see it and verify integrity. + await PublishAsync(run, new AssignContributor { - // Add the current user to the app, so that the admin can see it and verify integrity. - await PublishAsync(run, new AssignContributor - { - ContributorId = run.Job.Actor.Identifier, - IgnoreActor = true, - IgnorePlans = true, - Role = Role.Owner - }); + ContributorId = run.Job.Actor.Identifier, + IgnoreActor = true, + IgnorePlans = true, + Role = Role.Owner + }); - await LogAsync(run, "Assigned current user."); - } - catch (DomainException ex) - { - await LogAsync(run, $"Failed to assign contributor: {ex.Message}"); - } + await LogAsync(run, "Assigned current user."); } + catch (DomainException ex) + { + await LogAsync(run, $"Failed to assign contributor: {ex.Message}"); + } + } - private Task PublishAsync(Run run, AppCommand command) + private Task PublishAsync(Run run, AppCommand command) + { + command.Actor = run.Job.Actor; + + if (command is IAppCommand appCommand) { - command.Actor = run.Job.Actor; + appCommand.AppId = run.Job.AppId; + } - if (command is IAppCommand appCommand) - { - appCommand.AppId = run.Job.AppId; - } + return commandBus.PublishAsync(command, default); + } - return commandBus.PublishAsync(command, default); + private async Task CleanupAsync(Run run) + { + if (run.Job.AppId == null) + { + return; } - private async Task CleanupAsync(Run run) + foreach (var handler in run.Handlers) { - if (run.Job.AppId == null) + try { - return; + await handler.CleanupRestoreErrorAsync(run.Job.AppId.Id); } - - foreach (var handler in run.Handlers) + catch (Exception ex) { - try - { - await handler.CleanupRestoreErrorAsync(run.Job.AppId.Id); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to clean up restore."); - } + log.LogError(ex, "Failed to clean up restore."); } } + } - private async Task<IBackupReader> DownloadAsync(Run run, - CancellationToken ct) + private async Task<IBackupReader> DownloadAsync(Run run, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("Download")) { - using (Telemetry.Activities.StartActivity("Download")) - { - await LogAsync(run, "Downloading Backup"); + await LogAsync(run, "Downloading Backup"); - var reader = await backupArchiveLocation.OpenReaderAsync(run.Job.Url, run.Job.Id, ct); + var reader = await backupArchiveLocation.OpenReaderAsync(run.Job.Url, run.Job.Id, ct); - await LogAsync(run, "Downloaded Backup"); + await LogAsync(run, "Downloaded Backup"); - return reader; - } + return reader; } + } - private async Task ReadEventsAsync(Run run, - CancellationToken ct) - { - const int BatchSize = 100; + private async Task ReadEventsAsync(Run run, + CancellationToken ct) + { + const int BatchSize = 100; - var handled = 0; + var handled = 0; - var writeBlock = new ActionBlock<(string, Envelope<IEvent>)[]>(async batch => + var writeBlock = new ActionBlock<(string, Envelope<IEvent>)[]>(async batch => + { + try { - try - { - var commits = new List<EventCommit>(batch.Length); - - foreach (var (stream, @event) in batch) - { - var offset = run.StreamMapper.GetStreamOffset(stream); + var commits = new List<EventCommit>(batch.Length); - commits.Add(EventCommit.Create(stream, offset, @event, eventFormatter)); - } - - await eventStore.AppendUnsafeAsync(commits, ct); - - handled += commits.Count; - - await LogAsync(run, $"Reading {run.Reader.ReadEvents}/{handled} events and {run.Reader.ReadAttachments} attachments completed.", true); - } - catch (OperationCanceledException ex) + foreach (var (stream, @event) in batch) { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); + var offset = run.StreamMapper.GetStreamOffset(stream); + + commits.Add(EventCommit.Create(stream, offset, @event, eventFormatter)); } - }, new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1, - BoundedCapacity = 2 - }); - var batchBlock = new BatchBlock<(string, Envelope<IEvent>)>(BatchSize, new GroupingDataflowBlockOptions - { - BoundedCapacity = BatchSize * 2 - }); + await eventStore.AppendUnsafeAsync(commits, ct); - batchBlock.BidirectionalLinkTo(writeBlock); + handled += commits.Count; - await foreach (var job in run.Reader.ReadEventsAsync(eventStreamNames, eventFormatter, ct)) + await LogAsync(run, $"Reading {run.Reader.ReadEvents}/{handled} events and {run.Reader.ReadAttachments} attachments completed.", true); + } + catch (OperationCanceledException ex) { - var newStream = await HandleEventAsync(run, job.Stream, job.Event, ct); - - if (newStream != null) - { - if (!await batchBlock.SendAsync((newStream, job.Event), ct)) - { - break; - } - } + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); } + }, new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = 1, + MaxMessagesPerTask = 1, + BoundedCapacity = 2 + }); - batchBlock.Complete(); + var batchBlock = new BatchBlock<(string, Envelope<IEvent>)>(BatchSize, new GroupingDataflowBlockOptions + { + BoundedCapacity = BatchSize * 2 + }); - await writeBlock.Completion; - } + batchBlock.BidirectionalLinkTo(writeBlock); - private async Task<string?> HandleEventAsync(Run run, string stream, Envelope<IEvent> @event, - CancellationToken ct = default) + await foreach (var job in run.Reader.ReadEventsAsync(eventStreamNames, eventFormatter, ct)) { - if (@event.Payload is AppCreated appCreated) - { - var previousAppId = appCreated.AppId.Id; - - if (!string.IsNullOrWhiteSpace(run.Job.NewAppName)) - { - appCreated.Name = run.Job.NewAppName; + var newStream = await HandleEventAsync(run, job.Stream, job.Event, ct); - run.Job.AppId = NamedId.Of(DomainId.NewGuid(), run.Job.NewAppName); - } - else + if (newStream != null) + { + if (!await batchBlock.SendAsync((newStream, job.Event), ct)) { - run.Job.AppId = NamedId.Of(DomainId.NewGuid(), appCreated.Name); + break; } + } + } - await CreateContextAsync(run, previousAppId, ct); + batchBlock.Complete(); - run.StreamMapper = new StreamMapper(run.Context); - } + await writeBlock.Completion; + } - if (@event.Payload is SquidexEvent { Actor: { } } squidexEvent) + private async Task<string?> HandleEventAsync(Run run, string stream, Envelope<IEvent> @event, + CancellationToken ct = default) + { + if (@event.Payload is AppCreated appCreated) + { + var previousAppId = appCreated.AppId.Id; + + if (!string.IsNullOrWhiteSpace(run.Job.NewAppName)) { - if (run.Context.UserMapping.TryMap(squidexEvent.Actor, out var newUser)) - { - squidexEvent.Actor = newUser; - } - } + appCreated.Name = run.Job.NewAppName; - if (@event.Payload is AppEvent appEvent) + run.Job.AppId = NamedId.Of(DomainId.NewGuid(), run.Job.NewAppName); + } + else { - appEvent.AppId = run.Job.AppId; + run.Job.AppId = NamedId.Of(DomainId.NewGuid(), appCreated.Name); } - var (newStream, id) = run.StreamMapper.Map(stream); + await CreateContextAsync(run, previousAppId, ct); - @event.SetAggregateId(id); - @event.SetRestored(); + run.StreamMapper = new StreamMapper(run.Context); + } - foreach (var handler in run.Handlers) + if (@event.Payload is SquidexEvent { Actor: { } } squidexEvent) + { + if (run.Context.UserMapping.TryMap(squidexEvent.Actor, out var newUser)) { - if (!await handler.RestoreEventAsync(@event, run.Context, ct)) - { - return null; - } + squidexEvent.Actor = newUser; } - - return newStream; } - private async Task CreateContextAsync(Run run, DomainId previousAppId, - CancellationToken ct) + if (@event.Payload is AppEvent appEvent) { - var userMapping = new UserMapping(run.Job.Actor); + appEvent.AppId = run.Job.AppId; + } - using (Telemetry.Activities.StartActivity("CreateUsers")) - { - await LogAsync(run, "Creating Users"); + var (newStream, id) = run.StreamMapper.Map(stream); - await userMapping.RestoreAsync(run.Reader, userResolver, ct); + @event.SetAggregateId(id); + @event.SetRestored(); - await LogAsync(run, "Created Users"); + foreach (var handler in run.Handlers) + { + if (!await handler.RestoreEventAsync(@event, run.Context, ct)) + { + return null; } - - run.Context = new RestoreContext(run.Job.AppId.Id, userMapping, run.Reader, previousAppId); } - private Task SetStatusAsync(Run run, JobStatus status, string message) - { - var now = Clock.GetCurrentInstant(); + return newStream; + } - run.Job.Status = status; + private async Task CreateContextAsync(Run run, DomainId previousAppId, + CancellationToken ct) + { + var userMapping = new UserMapping(run.Job.Actor); - if (status == JobStatus.Failed || status == JobStatus.Completed) - { - run.Job.Stopped = now; - } - else if (status == JobStatus.Started) - { - run.Job.Started = now; - } + using (Telemetry.Activities.StartActivity("CreateUsers")) + { + await LogAsync(run, "Creating Users"); - run.Job.Log.Add($"{now}: {message}"); + await userMapping.RestoreAsync(run.Reader, userResolver, ct); - return state.WriteAsync(ct: default); + await LogAsync(run, "Created Users"); } - private Task LogAsync(Run run, string message, bool replace = false) + run.Context = new RestoreContext(run.Job.AppId.Id, userMapping, run.Reader, previousAppId); + } + + private Task SetStatusAsync(Run run, JobStatus status, string message) + { + var now = Clock.GetCurrentInstant(); + + run.Job.Status = status; + + if (status == JobStatus.Failed || status == JobStatus.Completed) { - var now = Clock.GetCurrentInstant(); + run.Job.Stopped = now; + } + else if (status == JobStatus.Started) + { + run.Job.Started = now; + } - if (replace && run.Job.Log.Count > 0) - { - run.Job.Log[^1] = $"{now}: {message}"; - } - else - { - run.Job.Log.Add($"{now}: {message}"); - } + run.Job.Log.Add($"{now}: {message}"); + + return state.WriteAsync(ct: default); + } + + private Task LogAsync(Run run, string message, bool replace = false) + { + var now = Clock.GetCurrentInstant(); - return state.WriteAsync(100, run.CancellationToken); + if (replace && run.Job.Log.Count > 0) + { + run.Job.Log[^1] = $"{now}: {message}"; } + else + { + run.Job.Log.Add($"{now}: {message}"); + } + + return state.WriteAsync(100, run.CancellationToken); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupJob.cs index c58c9f665a..8267a0f4eb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupJob.cs @@ -8,20 +8,19 @@ using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup.State +namespace Squidex.Domain.Apps.Entities.Backup.State; + +public sealed class BackupJob : IBackupJob { - public sealed class BackupJob : IBackupJob - { - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public Instant Started { get; set; } + public Instant Started { get; set; } - public Instant? Stopped { get; set; } + public Instant? Stopped { get; set; } - public int HandledEvents { get; set; } + public int HandledEvents { get; set; } - public int HandledAssets { get; set; } + public int HandledAssets { get; set; } - public JobStatus Status { get; set; } - } + public JobStatus Status { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs index b12dd5c3f4..b0930fbd3a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Backup.State +namespace Squidex.Domain.Apps.Entities.Backup.State; + +public class BackupRestoreState { - public class BackupRestoreState - { - public RestoreJob? Job { get; set; } - } + public RestoreJob? Job { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs index fcb981912f..22735552cd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs @@ -8,23 +8,22 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.Backup.State +namespace Squidex.Domain.Apps.Entities.Backup.State; + +public sealed class BackupState { - public sealed class BackupState - { - public List<BackupJob> Jobs { get; set; } = new List<BackupJob>(); + public List<BackupJob> Jobs { get; set; } = new List<BackupJob>(); - public void EnsureCanStart() + public void EnsureCanStart() + { + if (Jobs.Any(x => x.Status == JobStatus.Started)) { - if (Jobs.Any(x => x.Status == JobStatus.Started)) - { - throw new DomainException(T.Get("backups.alreadyRunning")); - } + throw new DomainException(T.Get("backups.alreadyRunning")); + } - if (Jobs.Count >= 10) - { - throw new DomainException(T.Get("backups.maxReached", new { max = 10 })); - } + if (Jobs.Count >= 10) + { + throw new DomainException(T.Get("backups.maxReached", new { max = 10 })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreJob.cs index 1e186821cc..d9da4eece4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreJob.cs @@ -9,36 +9,35 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.Backup.State +namespace Squidex.Domain.Apps.Entities.Backup.State; + +public sealed class RestoreJob : IRestoreJob { - public sealed class RestoreJob : IRestoreJob - { - public string AppName { get; set; } + public string AppName { get; set; } - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public RefToken Actor { get; set; } + public RefToken Actor { get; set; } - public Uri Url { get; set; } + public Uri Url { get; set; } - public Instant Started { get; set; } + public Instant Started { get; set; } - public Instant? Stopped { get; set; } + public Instant? Stopped { get; set; } - public List<string> Log { get; set; } = new List<string>(); + public List<string> Log { get; set; } = new List<string>(); - public JobStatus Status { get; set; } + public JobStatus Status { get; set; } - public string? NewAppName { get; set; } + public string? NewAppName { get; set; } - public void EnsureCanStart() + public void EnsureCanStart() + { + if (Status == JobStatus.Started) { - if (Status == JobStatus.Started) - { - throw new DomainException(T.Get("backups.restoreRunning")); - } + throw new DomainException(T.Get("backups.restoreRunning")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/StreamMapper.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/StreamMapper.cs index d448e4ad64..67a38a67b2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/StreamMapper.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/StreamMapper.cs @@ -7,68 +7,67 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class StreamMapper { - public sealed class StreamMapper + private readonly Dictionary<string, long> streams = new Dictionary<string, long>(1000); + private readonly RestoreContext context; + private readonly DomainId brokenAppId; + + public StreamMapper(RestoreContext context) { - private readonly Dictionary<string, long> streams = new Dictionary<string, long>(1000); - private readonly RestoreContext context; - private readonly DomainId brokenAppId; + Guard.NotNull(context); - public StreamMapper(RestoreContext context) - { - Guard.NotNull(context); + this.context = context; - this.context = context; + brokenAppId = DomainId.Combine(context.PreviousAppId, context.PreviousAppId); + } - brokenAppId = DomainId.Combine(context.PreviousAppId, context.PreviousAppId); - } + public (string Stream, DomainId) Map(string stream) + { + Guard.NotNullOrEmpty(stream); - public (string Stream, DomainId) Map(string stream) - { - Guard.NotNullOrEmpty(stream); + var typeIndex = stream.IndexOf("-", StringComparison.Ordinal); + var typeName = stream[..typeIndex]; + + var id = DomainId.Create(stream[(typeIndex + 1)..]); - var typeIndex = stream.IndexOf("-", StringComparison.Ordinal); - var typeName = stream[..typeIndex]; + if (id.Equals(context.PreviousAppId) || id.Equals(brokenAppId)) + { + id = context.AppId; + } + else + { + var separator = DomainId.IdSeparator; - var id = DomainId.Create(stream[(typeIndex + 1)..]); + var secondId = id.ToString().AsSpan(); - if (id.Equals(context.PreviousAppId) || id.Equals(brokenAppId)) + var indexOfSecondPart = secondId.IndexOf(separator, StringComparison.Ordinal); + if (indexOfSecondPart > 0 && indexOfSecondPart < secondId.Length - separator.Length - 1) { - id = context.AppId; + secondId = secondId[(indexOfSecondPart + separator.Length)..]; } - else - { - var separator = DomainId.IdSeparator; - var secondId = id.ToString().AsSpan(); - - var indexOfSecondPart = secondId.IndexOf(separator, StringComparison.Ordinal); - if (indexOfSecondPart > 0 && indexOfSecondPart < secondId.Length - separator.Length - 1) - { - secondId = secondId[(indexOfSecondPart + separator.Length)..]; - } + id = DomainId.Combine(context.AppId, DomainId.Create(secondId.ToString())); + } - id = DomainId.Combine(context.AppId, DomainId.Create(secondId.ToString())); - } + stream = $"{typeName}-{id}"; - stream = $"{typeName}-{id}"; + return (stream, id); + } - return (stream, id); - } + public long GetStreamOffset(string streamName) + { + Guard.NotNullOrEmpty(streamName); - public long GetStreamOffset(string streamName) + if (!streams.TryGetValue(streamName, out var offset)) { - Guard.NotNullOrEmpty(streamName); - - if (!streams.TryGetValue(streamName, out var offset)) - { - offset = EtagVersion.Empty; - } + offset = EtagVersion.Empty; + } - streams[streamName] = offset + 1; + streams[streamName] = offset + 1; - return offset; - } + return offset; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs index 8d31cca07f..c875330c7c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs @@ -9,98 +9,97 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +[ExcludeFromCodeCoverage] +public sealed class TempFolderBackupArchiveLocation : IBackupArchiveLocation { - [ExcludeFromCodeCoverage] - public sealed class TempFolderBackupArchiveLocation : IBackupArchiveLocation + private readonly IJsonSerializer serializer; + + public TempFolderBackupArchiveLocation(IJsonSerializer serializer) + { + this.serializer = serializer; + } + + public async Task<IBackupReader> OpenReaderAsync(Uri url, DomainId id, + CancellationToken ct) { - private readonly IJsonSerializer serializer; + Stream stream; - public TempFolderBackupArchiveLocation(IJsonSerializer serializer) + if (string.Equals(url.Scheme, "file", StringComparison.OrdinalIgnoreCase)) { - this.serializer = serializer; + stream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read); } - - public async Task<IBackupReader> OpenReaderAsync(Uri url, DomainId id, - CancellationToken ct) + else { - Stream stream; + stream = OpenStream(id); - if (string.Equals(url.Scheme, "file", StringComparison.OrdinalIgnoreCase)) - { - stream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read); - } - else + HttpResponseMessage? response = null; + try { - stream = OpenStream(id); - - HttpResponseMessage? response = null; - try + using (var client = new HttpClient()) { - using (var client = new HttpClient()) - { - client.Timeout = TimeSpan.FromHours(1); + client.Timeout = TimeSpan.FromHours(1); - response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); - response.EnsureSuccessStatusCode(); + response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); + response.EnsureSuccessStatusCode(); - await using (var sourceStream = await response.Content.ReadAsStreamAsync(ct)) - { - await sourceStream.CopyToAsync(stream, ct); - } + await using (var sourceStream = await response.Content.ReadAsStreamAsync(ct)) + { + await sourceStream.CopyToAsync(stream, ct); } } - catch (HttpRequestException ex) - { - var statusCode = response != null ? (int)response.StatusCode : 0; - - throw new BackupRestoreException($"Cannot download the archive. Got status code {statusCode}: {ex.Message}.", ex); - } - finally - { - response?.Dispose(); - } } - - try - { - return new BackupReader(serializer, stream); - } - catch (IOException) + catch (HttpRequestException ex) { - await stream.DisposeAsync(); + var statusCode = response != null ? (int)response.StatusCode : 0; - throw new BackupRestoreException("The backup archive is corrupt and cannot be opened."); + throw new BackupRestoreException($"Cannot download the archive. Got status code {statusCode}: {ex.Message}.", ex); } - catch (Exception) + finally { - await stream.DisposeAsync(); - - throw; + response?.Dispose(); } } - public Stream OpenStream(DomainId backupId) + try { - var tempFile = Path.Combine(Path.GetTempPath(), backupId + ".zip"); - - var fileStream = new FileStream( - tempFile, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.None, - 4096, - FileOptions.DeleteOnClose); - - return fileStream; + return new BackupReader(serializer, stream); } + catch (IOException) + { + await stream.DisposeAsync(); - public Task<IBackupWriter> OpenWriterAsync(Stream stream, - CancellationToken ct) + throw new BackupRestoreException("The backup archive is corrupt and cannot be opened."); + } + catch (Exception) { - var writer = new BackupWriter(serializer, stream, true); + await stream.DisposeAsync(); - return Task.FromResult<IBackupWriter>(writer); + throw; } } + + public Stream OpenStream(DomainId backupId) + { + var tempFile = Path.Combine(Path.GetTempPath(), backupId + ".zip"); + + var fileStream = new FileStream( + tempFile, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.None, + 4096, + FileOptions.DeleteOnClose); + + return fileStream; + } + + public Task<IBackupWriter> OpenWriterAsync(Stream stream, + CancellationToken ct) + { + var writer = new BackupWriter(serializer, stream, true); + + return Task.FromResult<IBackupWriter>(writer); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs index 426c01794a..4897e600ae 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs @@ -8,114 +8,113 @@ using Squidex.Infrastructure; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public sealed class UserMapping : IUserMapping { - public sealed class UserMapping : IUserMapping + private const string UsersFile = "Users.json"; + private readonly Dictionary<string, RefToken> userMap = new Dictionary<string, RefToken>(); + private readonly RefToken initiator; + + public RefToken Initiator { - private const string UsersFile = "Users.json"; - private readonly Dictionary<string, RefToken> userMap = new Dictionary<string, RefToken>(); - private readonly RefToken initiator; + get => initiator; + } - public RefToken Initiator - { - get => initiator; - } + public UserMapping(RefToken initiator) + { + Guard.NotNull(initiator); - public UserMapping(RefToken initiator) - { - Guard.NotNull(initiator); + this.initiator = initiator; + } - this.initiator = initiator; - } + public void Backup(RefToken token) + { + Guard.NotNull(token); - public void Backup(RefToken token) + if (!token.IsUser) { - Guard.NotNull(token); + return; + } - if (!token.IsUser) - { - return; - } + userMap[token.Identifier] = token; + } - userMap[token.Identifier] = token; - } + public void Backup(string userId) + { + Guard.NotNullOrEmpty(userId); - public void Backup(string userId) + if (!userMap.ContainsKey(userId)) { - Guard.NotNullOrEmpty(userId); - - if (!userMap.ContainsKey(userId)) - { - userMap[userId] = RefToken.User(userId); - } + userMap[userId] = RefToken.User(userId); } + } - public async Task StoreAsync(IBackupWriter writer, IUserResolver userResolver, - CancellationToken ct = default) - { - Guard.NotNull(writer); - Guard.NotNull(userResolver); + public async Task StoreAsync(IBackupWriter writer, IUserResolver userResolver, + CancellationToken ct = default) + { + Guard.NotNull(writer); + Guard.NotNull(userResolver); - var users = await userResolver.QueryManyAsync(userMap.Keys.ToArray(), ct); + var users = await userResolver.QueryManyAsync(userMap.Keys.ToArray(), ct); - var json = users.ToDictionary(x => x.Key, x => x.Value.Email); + var json = users.ToDictionary(x => x.Key, x => x.Value.Email); - await writer.WriteJsonAsync(UsersFile, json, ct); - } + await writer.WriteJsonAsync(UsersFile, json, ct); + } - public async Task RestoreAsync(IBackupReader reader, IUserResolver userResolver, - CancellationToken ct = default) - { - Guard.NotNull(reader); - Guard.NotNull(userResolver); + public async Task RestoreAsync(IBackupReader reader, IUserResolver userResolver, + CancellationToken ct = default) + { + Guard.NotNull(reader); + Guard.NotNull(userResolver); - var json = await reader.ReadJsonAsync<Dictionary<string, string>>(UsersFile, ct); + var json = await reader.ReadJsonAsync<Dictionary<string, string>>(UsersFile, ct); - foreach (var (userId, email) in json) - { - var (user, _) = await userResolver.CreateUserIfNotExistsAsync(email, false, ct); + foreach (var (userId, email) in json) + { + var (user, _) = await userResolver.CreateUserIfNotExistsAsync(email, false, ct); - if (user != null) - { - userMap[userId] = RefToken.User(user.Id); - } + if (user != null) + { + userMap[userId] = RefToken.User(user.Id); } } + } - public bool TryMap(string userId, out RefToken result) - { - Guard.NotNullOrEmpty(userId); - - result = initiator; + public bool TryMap(string userId, out RefToken result) + { + Guard.NotNullOrEmpty(userId); - if (userMap.TryGetValue(userId, out var mapped)) - { - result = mapped; - return true; - } + result = initiator; - return false; + if (userMap.TryGetValue(userId, out var mapped)) + { + result = mapped; + return true; } - public bool TryMap(RefToken token, out RefToken result) - { - Guard.NotNull(token); + return false; + } - result = initiator; + public bool TryMap(RefToken token, out RefToken result) + { + Guard.NotNull(token); - if (token.IsClient) - { - result = token; - return true; - } + result = initiator; - if (userMap.TryGetValue(token.Identifier, out var mapped)) - { - result = mapped; - return true; - } + if (token.IsClient) + { + result = token; + return true; + } - return false; + if (userMap.TryGetValue(token.Identifier, out var mapped)) + { + result = mapped; + return true; } + + return false; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/ConfigPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/ConfigPlansProvider.cs index 759a4f33a4..771dcd7110 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/ConfigPlansProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/ConfigPlansProvider.cs @@ -5,78 +5,77 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public sealed class ConfigPlansProvider : IBillingPlans { - public sealed class ConfigPlansProvider : IBillingPlans + private static readonly Plan Infinite = new Plan { - private static readonly Plan Infinite = new Plan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1, - BlockingApiCalls = -1 - }; + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1, + BlockingApiCalls = -1 + }; - private readonly Dictionary<string, Plan> plansById = new Dictionary<string, Plan>(StringComparer.OrdinalIgnoreCase); - private readonly List<Plan> plans = new List<Plan>(); - private readonly Plan freePlan; + private readonly Dictionary<string, Plan> plansById = new Dictionary<string, Plan>(StringComparer.OrdinalIgnoreCase); + private readonly List<Plan> plans = new List<Plan>(); + private readonly Plan freePlan; - public ConfigPlansProvider(IEnumerable<Plan> config) + public ConfigPlansProvider(IEnumerable<Plan> config) + { + plans.AddRange(config.OrderBy(x => x.MaxApiCalls)); + + foreach (var plan in config.OrderBy(x => x.MaxApiCalls)) { - plans.AddRange(config.OrderBy(x => x.MaxApiCalls)); + plansById[plan.Id] = plan; - foreach (var plan in config.OrderBy(x => x.MaxApiCalls)) + if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) { - plansById[plan.Id] = plan; - - if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) - { - plansById[plan.YearlyId] = plan; - } + plansById[plan.YearlyId] = plan; } - - freePlan = config.FirstOrDefault(x => x.IsFree) ?? Infinite; } - public IEnumerable<Plan> GetAvailablePlans() - { - return plans; - } + freePlan = config.FirstOrDefault(x => x.IsFree) ?? Infinite; + } - public bool IsConfiguredPlan(string? planId) - { - return planId != null && plansById.ContainsKey(planId); - } + public IEnumerable<Plan> GetAvailablePlans() + { + return plans; + } - public Plan? GetPlan(string? planId) + public bool IsConfiguredPlan(string? planId) + { + return planId != null && plansById.ContainsKey(planId); + } + + public Plan? GetPlan(string? planId) + { + return plansById.GetValueOrDefault(planId ?? string.Empty); + } + + public Plan GetFreePlan() + { + return freePlan; + } + + public (Plan Plan, string PlanId) GetActualPlan(string? planId) + { + if (planId == null || !plansById.TryGetValue(planId, out var plan)) { - return plansById.GetValueOrDefault(planId ?? string.Empty); + var result = GetFreePlan(); + + return (result, result.Id); } - public Plan GetFreePlan() + if (plan.YearlyId != null && plan.YearlyId == planId) { - return freePlan; + return (plan, plan.YearlyId); } - - public (Plan Plan, string PlanId) GetActualPlan(string? planId) + else { - if (planId == null || !plansById.TryGetValue(planId, out var plan)) - { - var result = GetFreePlan(); - - return (result, result.Id); - } - - if (plan.YearlyId != null && plan.YearlyId == planId) - { - return (plan, plan.YearlyId); - } - else - { - return (plan, plan.Id); - } + return (plan, plan.Id); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingManager.cs index b44aef49b0..1a0b437806 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingManager.cs @@ -8,38 +8,37 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Teams; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public interface IBillingManager { - public interface IBillingManager - { - Task<Uri?> GetPortalLinkAsync(string userId, IAppEntity app, - CancellationToken ct = default); + Task<Uri?> GetPortalLinkAsync(string userId, IAppEntity app, + CancellationToken ct = default); - Task<Uri?> GetPortalLinkAsync(string userId, ITeamEntity team, - CancellationToken ct = default); + Task<Uri?> GetPortalLinkAsync(string userId, ITeamEntity team, + CancellationToken ct = default); - Task<ReferralInfo?> GetReferralInfoAsync(string userId, IAppEntity app, - CancellationToken ct = default); + Task<ReferralInfo?> GetReferralInfoAsync(string userId, IAppEntity app, + CancellationToken ct = default); - Task<ReferralInfo?> GetReferralInfoAsync(string userId, ITeamEntity team, - CancellationToken ct = default); + Task<ReferralInfo?> GetReferralInfoAsync(string userId, ITeamEntity team, + CancellationToken ct = default); - Task<Uri?> MustRedirectToPortalAsync(string userId, IAppEntity app, string? planId, - CancellationToken ct = default); + Task<Uri?> MustRedirectToPortalAsync(string userId, IAppEntity app, string? planId, + CancellationToken ct = default); - Task<Uri?> MustRedirectToPortalAsync(string userId, ITeamEntity team, string? planId, - CancellationToken ct = default); + Task<Uri?> MustRedirectToPortalAsync(string userId, ITeamEntity team, string? planId, + CancellationToken ct = default); - Task SubscribeAsync(string userId, IAppEntity app, string planId, - CancellationToken ct = default); + Task SubscribeAsync(string userId, IAppEntity app, string planId, + CancellationToken ct = default); - Task SubscribeAsync(string userId, ITeamEntity team, string planId, - CancellationToken ct = default); + Task SubscribeAsync(string userId, ITeamEntity team, string planId, + CancellationToken ct = default); - Task UnsubscribeAsync(string userId, IAppEntity app, - CancellationToken ct = default); + Task UnsubscribeAsync(string userId, IAppEntity app, + CancellationToken ct = default); - Task UnsubscribeAsync(string userId, ITeamEntity team, - CancellationToken ct = default); - } + Task UnsubscribeAsync(string userId, ITeamEntity team, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingPlans.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingPlans.cs index 00abfd9610..bcab8e55fd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingPlans.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/IBillingPlans.cs @@ -5,18 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public interface IBillingPlans { - public interface IBillingPlans - { - IEnumerable<Plan> GetAvailablePlans(); + IEnumerable<Plan> GetAvailablePlans(); - bool IsConfiguredPlan(string? planId); + bool IsConfiguredPlan(string? planId); - Plan? GetPlan(string? planId); + Plan? GetPlan(string? planId); - Plan GetFreePlan(); + Plan GetFreePlan(); - (Plan Plan, string PlanId) GetActualPlan(string? planId); - } + (Plan Plan, string PlanId) GetActualPlan(string? planId); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/IUsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/IUsageGate.cs index 02cbec79d0..d729b687c2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/IUsageGate.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/IUsageGate.cs @@ -9,32 +9,31 @@ using Squidex.Domain.Apps.Entities.Teams; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public interface IUsageGate { - public interface IUsageGate - { - Task<bool> IsBlockedAsync(IAppEntity app, string? clientId, DateTime date, - CancellationToken ct = default); + Task<bool> IsBlockedAsync(IAppEntity app, string? clientId, DateTime date, + CancellationToken ct = default); - Task TrackRequestAsync(IAppEntity app, string? clientId, DateTime date, double costs, long elapsedMs, long bytes, - CancellationToken ct = default); + Task TrackRequestAsync(IAppEntity app, string? clientId, DateTime date, double costs, long elapsedMs, long bytes, + CancellationToken ct = default); - Task TrackAssetAsync(DomainId appId, DateTime date, long fileSize, long count, - CancellationToken ct = default); + Task TrackAssetAsync(DomainId appId, DateTime date, long fileSize, long count, + CancellationToken ct = default); - Task DeleteAssetUsageAsync(DomainId appId, - CancellationToken ct = default); + Task DeleteAssetUsageAsync(DomainId appId, + CancellationToken ct = default); - Task DeleteAssetsUsageAsync( - CancellationToken ct = default); + Task DeleteAssetsUsageAsync( + CancellationToken ct = default); - Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, - CancellationToken ct = default); + Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, + CancellationToken ct = default); - Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, - CancellationToken ct = default); + Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, + CancellationToken ct = default); - Task<(Plan Plan, string PlanId)> GetPlanForTeamAsync(ITeamEntity team, - CancellationToken ct = default); - } + Task<(Plan Plan, string PlanId)> GetPlanForTeamAsync(ITeamEntity team, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/Messages.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/Messages.cs index ca48c3d65e..9dd3afe9e8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/Messages.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/Messages.cs @@ -9,16 +9,15 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public sealed record UsageTrackingCheck { - public sealed record UsageTrackingCheck - { - public DomainId AppId { get; init; } + public DomainId AppId { get; init; } - public long Usage { get; init; } + public long Usage { get; init; } - public long UsageLimit { get; init; } + public long UsageLimit { get; init; } - public string[] Users { get; init; } - } + public string[] Users { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/NoopBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/NoopBillingManager.cs index 4ad3579891..e13553f22e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/NoopBillingManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/NoopBillingManager.cs @@ -8,68 +8,67 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Teams; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public sealed class NoopBillingManager : IBillingManager { - public sealed class NoopBillingManager : IBillingManager + public Task<Uri?> GetPortalLinkAsync(string userId, IAppEntity app, + CancellationToken ct = default) { - public Task<Uri?> GetPortalLinkAsync(string userId, IAppEntity app, - CancellationToken ct = default) - { - return Task.FromResult<Uri?>(null); - } + return Task.FromResult<Uri?>(null); + } - public Task<Uri?> GetPortalLinkAsync(string userId, ITeamEntity team, - CancellationToken ct = default) - { - return Task.FromResult<Uri?>(null); - } + public Task<Uri?> GetPortalLinkAsync(string userId, ITeamEntity team, + CancellationToken ct = default) + { + return Task.FromResult<Uri?>(null); + } - public Task<ReferralInfo?> GetReferralInfoAsync(string userId, IAppEntity app, - CancellationToken ct = default) - { - return Task.FromResult<ReferralInfo?>(null); - } + public Task<ReferralInfo?> GetReferralInfoAsync(string userId, IAppEntity app, + CancellationToken ct = default) + { + return Task.FromResult<ReferralInfo?>(null); + } - public Task<ReferralInfo?> GetReferralInfoAsync(string userId, ITeamEntity team, - CancellationToken ct = default) - { - return Task.FromResult<ReferralInfo?>(null); - } + public Task<ReferralInfo?> GetReferralInfoAsync(string userId, ITeamEntity team, + CancellationToken ct = default) + { + return Task.FromResult<ReferralInfo?>(null); + } - public Task<Uri?> MustRedirectToPortalAsync(string userId, IAppEntity app, string? planId, - CancellationToken ct = default) - { - return Task.FromResult<Uri?>(null); - } + public Task<Uri?> MustRedirectToPortalAsync(string userId, IAppEntity app, string? planId, + CancellationToken ct = default) + { + return Task.FromResult<Uri?>(null); + } - public Task<Uri?> MustRedirectToPortalAsync(string userId, ITeamEntity team, string? planId, - CancellationToken ct = default) - { - return Task.FromResult<Uri?>(null); - } + public Task<Uri?> MustRedirectToPortalAsync(string userId, ITeamEntity team, string? planId, + CancellationToken ct = default) + { + return Task.FromResult<Uri?>(null); + } - public Task SubscribeAsync(string userId, IAppEntity app, string planId, - CancellationToken ct = default) - { - return Task.CompletedTask; - } + public Task SubscribeAsync(string userId, IAppEntity app, string planId, + CancellationToken ct = default) + { + return Task.CompletedTask; + } - public Task SubscribeAsync(string userId, ITeamEntity team, string planId, - CancellationToken ct = default) - { - return Task.CompletedTask; - } + public Task SubscribeAsync(string userId, ITeamEntity team, string planId, + CancellationToken ct = default) + { + return Task.CompletedTask; + } - public Task UnsubscribeAsync(string userId, IAppEntity app, - CancellationToken ct = default) - { - return Task.CompletedTask; - } + public Task UnsubscribeAsync(string userId, IAppEntity app, + CancellationToken ct = default) + { + return Task.CompletedTask; + } - public Task UnsubscribeAsync(string userId, ITeamEntity team, - CancellationToken ct = default) - { - return Task.CompletedTask; - } + public Task UnsubscribeAsync(string userId, ITeamEntity team, + CancellationToken ct = default) + { + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/Plan.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/Plan.cs index a12227b11a..cb139232d8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/Plan.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/Plan.cs @@ -5,34 +5,33 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public sealed record Plan { - public sealed record Plan - { - public string Id { get; init; } + public string Id { get; init; } - public string Name { get; init; } + public string Name { get; init; } - public string Costs { get; init; } + public string Costs { get; init; } - public string? ConfirmText { get; init; } + public string? ConfirmText { get; init; } - public string? YearlyCosts { get; init; } + public string? YearlyCosts { get; init; } - public string? YearlyId { get; init; } + public string? YearlyId { get; init; } - public string? YearlyConfirmText { get; init; } + public string? YearlyConfirmText { get; init; } - public long BlockingApiCalls { get; init; } + public long BlockingApiCalls { get; init; } - public long MaxApiCalls { get; init; } + public long MaxApiCalls { get; init; } - public long MaxApiBytes { get; init; } + public long MaxApiBytes { get; init; } - public long MaxAssetSize { get; init; } + public long MaxAssetSize { get; init; } - public long MaxContributors { get; init; } + public long MaxContributors { get; init; } - public bool IsFree { get; init; } - } + public bool IsFree { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/PlanChangedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/PlanChangedResult.cs index bd7d4c936f..95c5dd37ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/PlanChangedResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/PlanChangedResult.cs @@ -7,9 +7,8 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public sealed record PlanChangedResult(string PlanId, bool Unsubscribed = false, Uri? RedirectUri = null) { - public sealed record PlanChangedResult(string PlanId, bool Unsubscribed = false, Uri? RedirectUri = null) - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/ReferralInfo.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/ReferralInfo.cs index ebae45dd90..b2b0c2ce00 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/ReferralInfo.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/ReferralInfo.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Billing -{ - public sealed record ReferralInfo(string Code, string Earned, string Condition); -} +namespace Squidex.Domain.Apps.Entities.Billing; + +public sealed record ReferralInfo(string Code, string Earned, string Condition); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs index 97cc8de7ca..ae296789a2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs @@ -15,300 +15,299 @@ using Squidex.Infrastructure.UsageTracking; using Squidex.Messaging; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public sealed class UsageGate : IUsageGate, IAssetUsageTracker { - public sealed class UsageGate : IUsageGate, IAssetUsageTracker + private const string CounterTotalCount = "TotalAssets"; + private const string CounterTotalSize = "TotalSize"; + private static readonly DateTime SummaryDate = default; + private readonly IApiUsageTracker apiUsageTracker; + private readonly IAppProvider appProvider; + private readonly IBillingPlans billingPlans; + private readonly IMemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly IMessageBus messaging; + private readonly IUsageTracker usageTracker; + + public UsageGate( + IAppProvider appProvider, + IApiUsageTracker apiUsageTracker, + IBillingPlans billingPlans, + IMessageBus messaging, + IUsageTracker usageTracker) { - private const string CounterTotalCount = "TotalAssets"; - private const string CounterTotalSize = "TotalSize"; - private static readonly DateTime SummaryDate = default; - private readonly IApiUsageTracker apiUsageTracker; - private readonly IAppProvider appProvider; - private readonly IBillingPlans billingPlans; - private readonly IMemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly IMessageBus messaging; - private readonly IUsageTracker usageTracker; - - public UsageGate( - IAppProvider appProvider, - IApiUsageTracker apiUsageTracker, - IBillingPlans billingPlans, - IMessageBus messaging, - IUsageTracker usageTracker) - { - this.appProvider = appProvider; - this.apiUsageTracker = apiUsageTracker; - this.billingPlans = billingPlans; - this.messaging = messaging; - this.usageTracker = usageTracker; - } + this.appProvider = appProvider; + this.apiUsageTracker = apiUsageTracker; + this.billingPlans = billingPlans; + this.messaging = messaging; + this.usageTracker = usageTracker; + } - public Task DeleteAssetUsageAsync(DomainId appId, - CancellationToken ct = default) - { - // Do not delete the team, as this is only called when an app is deleted. - return usageTracker.DeleteAsync(AppAssetsKey(appId), ct); - } + public Task DeleteAssetUsageAsync(DomainId appId, + CancellationToken ct = default) + { + // Do not delete the team, as this is only called when an app is deleted. + return usageTracker.DeleteAsync(AppAssetsKey(appId), ct); + } - public Task DeleteAssetsUsageAsync( - CancellationToken ct = default) - { - // Use a well defined prefix query for the deletion to improve performance. - return usageTracker.DeleteByKeyPatternAsync("^([a-zA-Z0-9]+)_Assets", ct); - } + public Task DeleteAssetsUsageAsync( + CancellationToken ct = default) + { + // Use a well defined prefix query for the deletion to improve performance. + return usageTracker.DeleteByKeyPatternAsync("^([a-zA-Z0-9]+)_Assets", ct); + } - public Task<long> GetTotalSizeByAppAsync(DomainId appId, - CancellationToken ct = default) - { - return GetTotalSizeAsync(AppAssetsKey(appId), ct); - } + public Task<long> GetTotalSizeByAppAsync(DomainId appId, + CancellationToken ct = default) + { + return GetTotalSizeAsync(AppAssetsKey(appId), ct); + } + + public Task<long> GetTotalSizeByTeamAsync(DomainId teamId, + CancellationToken ct = default) + { + return GetTotalSizeAsync(TeamAssetsKey(teamId), ct); + } + + private async Task<long> GetTotalSizeAsync(string key, + CancellationToken ct) + { + var counters = await usageTracker.GetAsync(key, SummaryDate, SummaryDate, null, ct); + + return counters.GetInt64(CounterTotalSize); + } + + public Task<IReadOnlyList<AssetStats>> QueryByAppAsync(DomainId appId, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + return QueryAsync(AppAssetsKey(appId), fromDate, toDate, ct); + } + + public Task<IReadOnlyList<AssetStats>> QueryByTeamAsync(DomainId teamId, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + return QueryAsync(TeamAssetsKey(teamId), fromDate, toDate, ct); + } + + private async Task<IReadOnlyList<AssetStats>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct) + { + var enriched = new List<AssetStats>(); + + var usages = await usageTracker.QueryAsync(key, fromDate, toDate, ct); - public Task<long> GetTotalSizeByTeamAsync(DomainId teamId, - CancellationToken ct = default) + if (usages.TryGetValue(usageTracker.FallbackCategory, out var byCategory1)) { - return GetTotalSizeAsync(TeamAssetsKey(teamId), ct); + AddCounters(enriched, byCategory1); } - private async Task<long> GetTotalSizeAsync(string key, - CancellationToken ct) + return enriched; + } + + private static void AddCounters(List<AssetStats> enriched, List<(DateTime, Counters)> details) + { + foreach (var (date, counters) in details) { - var counters = await usageTracker.GetAsync(key, SummaryDate, SummaryDate, null, ct); + var totalCount = counters.GetInt64(CounterTotalCount); + var totalSize = counters.GetInt64(CounterTotalSize); - return counters.GetInt64(CounterTotalSize); + enriched.Add(new AssetStats(date, totalCount, totalSize)); } + } - public Task<IReadOnlyList<AssetStats>> QueryByAppAsync(DomainId appId, DateTime fromDate, DateTime toDate, - CancellationToken ct = default) - { - return QueryAsync(AppAssetsKey(appId), fromDate, toDate, ct); - } + public async Task TrackRequestAsync(IAppEntity app, string? clientId, DateTime date, double costs, long elapsedMs, long bytes, + CancellationToken ct = default) + { + var appId = app.Id.ToString(); - public Task<IReadOnlyList<AssetStats>> QueryByTeamAsync(DomainId teamId, DateTime fromDate, DateTime toDate, - CancellationToken ct = default) + if (app.TeamId != null) { - return QueryAsync(TeamAssetsKey(teamId), fromDate, toDate, ct); + await apiUsageTracker.TrackAsync(date, app.TeamId.ToString()!, app.Name, costs, elapsedMs, bytes, ct); } - private async Task<IReadOnlyList<AssetStats>> QueryAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct) - { - var enriched = new List<AssetStats>(); + await apiUsageTracker.TrackAsync(date, appId, clientId, costs, elapsedMs, bytes, ct); + } - var usages = await usageTracker.QueryAsync(key, fromDate, toDate, ct); + public async Task<bool> IsBlockedAsync(IAppEntity app, string? clientId, DateTime date, + CancellationToken ct = default) + { + Guard.NotNull(app); - if (usages.TryGetValue(usageTracker.FallbackCategory, out var byCategory1)) - { - AddCounters(enriched, byCategory1); - } + // Resolve the plan from either the app or the assigned team. + var (plan, _, teamId) = await GetPlanForAppAsync(app, ct); - return enriched; - } + var appId = app.Id; + var blocking = false; + var blockLimit = plan.MaxApiCalls; + var referenceId = teamId ?? app.Id; - private static void AddCounters(List<AssetStats> enriched, List<(DateTime, Counters)> details) + if (blockLimit > 0 || plan.BlockingApiCalls > 0) { - foreach (var (date, counters) in details) - { - var totalCount = counters.GetInt64(CounterTotalCount); - var totalSize = counters.GetInt64(CounterTotalSize); + var usage = await apiUsageTracker.GetMonthCallsAsync(referenceId.ToString(), date, null, ct); - enriched.Add(new AssetStats(date, totalCount, totalSize)); - } - } + if (IsOver10Percent(blockLimit, usage) && IsAboutToBeLocked(date, blockLimit, usage) && !HasNotifiedBefore(appId)) + { + var notification = new UsageTrackingCheck + { + AppId = appId, + Usage = usage, + UsageLimit = blockLimit, + Users = GetUsers(app) + }; - public async Task TrackRequestAsync(IAppEntity app, string? clientId, DateTime date, double costs, long elapsedMs, long bytes, - CancellationToken ct = default) - { - var appId = app.Id.ToString(); + await messaging.PublishAsync(notification, ct: ct); - if (app.TeamId != null) - { - await apiUsageTracker.TrackAsync(date, app.TeamId.ToString()!, app.Name, costs, elapsedMs, bytes, ct); + TrackNotified(appId); } - await apiUsageTracker.TrackAsync(date, appId, clientId, costs, elapsedMs, bytes, ct); + blocking = plan.BlockingApiCalls > 0 && usage > plan.BlockingApiCalls; } - public async Task<bool> IsBlockedAsync(IAppEntity app, string? clientId, DateTime date, - CancellationToken ct = default) + if (!blocking) { - Guard.NotNull(app); - - // Resolve the plan from either the app or the assigned team. - var (plan, _, teamId) = await GetPlanForAppAsync(app, ct); - - var appId = app.Id; - var blocking = false; - var blockLimit = plan.MaxApiCalls; - var referenceId = teamId ?? app.Id; - - if (blockLimit > 0 || plan.BlockingApiCalls > 0) + if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && client.ApiCallsLimit > 0) { - var usage = await apiUsageTracker.GetMonthCallsAsync(referenceId.ToString(), date, null, ct); + var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), date, clientId, ct); - if (IsOver10Percent(blockLimit, usage) && IsAboutToBeLocked(date, blockLimit, usage) && !HasNotifiedBefore(appId)) - { - var notification = new UsageTrackingCheck - { - AppId = appId, - Usage = usage, - UsageLimit = blockLimit, - Users = GetUsers(app) - }; + blocking = usage >= client.ApiCallsLimit; + } + } - await messaging.PublishAsync(notification, ct: ct); + return blocking; + } - TrackNotified(appId); - } + private bool HasNotifiedBefore(DomainId appId) + { + return memoryCache.Get<bool>(appId); + } - blocking = plan.BlockingApiCalls > 0 && usage > plan.BlockingApiCalls; - } + private bool TrackNotified(DomainId appId) + { + return memoryCache.Set(appId, true, TimeSpan.FromHours(1)); + } - if (!blocking) - { - if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && client.ApiCallsLimit > 0) - { - var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), date, clientId, ct); + private static string[] GetUsers(IAppEntity app) + { + return app.Contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToArray(); + } - blocking = usage >= client.ApiCallsLimit; - } - } + private static bool IsOver10Percent(long limit, long usage) + { + return usage > limit * 0.1; + } - return blocking; - } + private static bool IsAboutToBeLocked(DateTime today, long limit, long usage) + { + var daysInMonth = DateTime.DaysInMonth(today.Year, today.Month); - private bool HasNotifiedBefore(DomainId appId) - { - return memoryCache.Get<bool>(appId); - } + var forecasted = ((float)usage / today.Day) * daysInMonth; - private bool TrackNotified(DomainId appId) - { - return memoryCache.Set(appId, true, TimeSpan.FromHours(1)); - } + return forecasted > limit; + } - private static string[] GetUsers(IAppEntity app) + public async Task TrackAssetAsync(DomainId appId, DateTime date, long fileSize, long count, + CancellationToken ct = default) + { + var counters = new Counters { - return app.Contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToArray(); - } + [CounterTotalSize] = fileSize, + [CounterTotalCount] = count + }; - private static bool IsOver10Percent(long limit, long usage) - { - return usage > limit * 0.1; - } + var appKey = AppAssetsKey(appId); - private static bool IsAboutToBeLocked(DateTime today, long limit, long usage) + var tasks = new List<Task> { - var daysInMonth = DateTime.DaysInMonth(today.Year, today.Month); + usageTracker.TrackAsync(date, appKey, null, counters, ct), + usageTracker.TrackAsync(SummaryDate, appKey, null, counters, ct) + }; - var forecasted = ((float)usage / today.Day) * daysInMonth; - - return forecasted > limit; - } + var (_, _, teamId) = await GetPlanForAppAsync(appId, ct); - public async Task TrackAssetAsync(DomainId appId, DateTime date, long fileSize, long count, - CancellationToken ct = default) + if (teamId != null) { - var counters = new Counters - { - [CounterTotalSize] = fileSize, - [CounterTotalCount] = count - }; - - var appKey = AppAssetsKey(appId); + var teamKey = TeamAssetsKey(teamId.Value); - var tasks = new List<Task> - { - usageTracker.TrackAsync(date, appKey, null, counters, ct), - usageTracker.TrackAsync(SummaryDate, appKey, null, counters, ct) - }; + tasks.Add(usageTracker.TrackAsync(date, teamKey, null, counters, ct)); + tasks.Add(usageTracker.TrackAsync(SummaryDate, teamKey, null, counters, ct)); + } - var (_, _, teamId) = await GetPlanForAppAsync(appId, ct); + await Task.WhenAll(tasks); + } - if (teamId != null) - { - var teamKey = TeamAssetsKey(teamId.Value); + public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, + CancellationToken ct = default) + { + Guard.NotNull(app); - tasks.Add(usageTracker.TrackAsync(date, teamKey, null, counters, ct)); - tasks.Add(usageTracker.TrackAsync(SummaryDate, teamKey, null, counters, ct)); - } + return memoryCache.GetOrCreateAsync(app, async x => + { + x.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); - await Task.WhenAll(tasks); - } + return await GetPlanCoreAsync(app, ct); + }); + } - public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, - CancellationToken ct = default) + public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, + CancellationToken ct = default) + { + return memoryCache.GetOrCreateAsync(appId, async x => { - Guard.NotNull(app); + x.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); - return memoryCache.GetOrCreateAsync(app, async x => - { - x.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); + return await GetPlanCoreAsync(appId, ct); + }); + } - return await GetPlanCoreAsync(app, ct); - }); - } + private async Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanCoreAsync(DomainId appId, + CancellationToken ct) + { + var app = await appProvider.GetAppAsync(appId, true, ct); - public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, - CancellationToken ct = default) + if (app == null) { - return memoryCache.GetOrCreateAsync(appId, async x => - { - x.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); + var freePlan = billingPlans.GetFreePlan(); - return await GetPlanCoreAsync(appId, ct); - }); + return (freePlan, freePlan.Id, null); } - private async Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanCoreAsync(DomainId appId, - CancellationToken ct) - { - var app = await appProvider.GetAppAsync(appId, true, ct); + return await GetPlanCoreAsync(app, ct); + } - if (app == null) - { - var freePlan = billingPlans.GetFreePlan(); + private async Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanCoreAsync(IAppEntity app, + CancellationToken ct) + { + if (app.TeamId != null) + { + var team = await appProvider.GetTeamAsync(app.TeamId.Value, ct); - return (freePlan, freePlan.Id, null); - } + var (plan, planId) = billingPlans.GetActualPlan(team?.Plan?.PlanId ?? app.Plan?.PlanId); - return await GetPlanCoreAsync(app, ct); + return (plan, planId, team?.Id); } - - private async Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanCoreAsync(IAppEntity app, - CancellationToken ct) + else { - if (app.TeamId != null) - { - var team = await appProvider.GetTeamAsync(app.TeamId.Value, ct); - - var (plan, planId) = billingPlans.GetActualPlan(team?.Plan?.PlanId ?? app.Plan?.PlanId); + var (plan, planId) = billingPlans.GetActualPlan(app.Plan?.PlanId); - return (plan, planId, team?.Id); - } - else - { - var (plan, planId) = billingPlans.GetActualPlan(app.Plan?.PlanId); - - return (plan, planId, null); - } + return (plan, planId, null); } + } - public Task<(Plan Plan, string PlanId)> GetPlanForTeamAsync(ITeamEntity team, - CancellationToken ct = default) - { - var (plan, planId) = billingPlans.GetActualPlan(team?.Plan?.PlanId); + public Task<(Plan Plan, string PlanId)> GetPlanForTeamAsync(ITeamEntity team, + CancellationToken ct = default) + { + var (plan, planId) = billingPlans.GetActualPlan(team?.Plan?.PlanId); - return Task.FromResult((plan, planId)); - } + return Task.FromResult((plan, planId)); + } - private static string AppAssetsKey(DomainId appId) - { - return $"{appId}_Assets"; - } + private static string AppAssetsKey(DomainId appId) + { + return $"{appId}_Assets"; + } - private static string TeamAssetsKey(DomainId appId) - { - return $"{appId}_TeamAssets"; - } + private static string TeamAssetsKey(DomainId appId) + { + return $"{appId}_TeamAssets"; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs index 2cdbb98bf1..36b14bcb5f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs @@ -13,105 +13,104 @@ using Squidex.Messaging; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public sealed class UsageNotifierWorker : IMessageHandler<UsageTrackingCheck>, IInitializable { - public sealed class UsageNotifierWorker : IMessageHandler<UsageTrackingCheck>, IInitializable + private static readonly TimeSpan TimeBetweenNotifications = TimeSpan.FromDays(3); + private readonly SimpleState<State> state; + private readonly IAppProvider appProvider; + private readonly IUserNotifications userNotifications; + private readonly IUserResolver userResolver; + + [CollectionName("UsageNotifications")] + public sealed class State + { + public Dictionary<DomainId, DateTime> NotificationsSent { get; set; } = new Dictionary<DomainId, DateTime>(); + } + + public IClock Clock { get; set; } = SystemClock.Instance; + + public UsageNotifierWorker(IPersistenceFactory<State> persistenceFactory, + IAppProvider appProvider, + IUserNotifications userNotifications, + IUserResolver userResolver) { - private static readonly TimeSpan TimeBetweenNotifications = TimeSpan.FromDays(3); - private readonly SimpleState<State> state; - private readonly IAppProvider appProvider; - private readonly IUserNotifications userNotifications; - private readonly IUserResolver userResolver; - - [CollectionName("UsageNotifications")] - public sealed class State + this.appProvider = appProvider; + this.userNotifications = userNotifications; + this.userResolver = userResolver; + + state = new SimpleState<State>(persistenceFactory, GetType(), DomainId.Create("Default")); + } + + public Task InitializeAsync( + CancellationToken ct) + { + return state.LoadAsync(ct); + } + + public async Task HandleAsync(UsageTrackingCheck notification, + CancellationToken ct) + { + if (!userNotifications.IsActive) { - public Dictionary<DomainId, DateTime> NotificationsSent { get; set; } = new Dictionary<DomainId, DateTime>(); + return; } - public IClock Clock { get; set; } = SystemClock.Instance; + var now = Clock.GetCurrentInstant().ToDateTimeUtc(); - public UsageNotifierWorker(IPersistenceFactory<State> persistenceFactory, - IAppProvider appProvider, - IUserNotifications userNotifications, - IUserResolver userResolver) + if (!HasBeenSentBefore(notification.AppId, now)) { - this.appProvider = appProvider; - this.userNotifications = userNotifications; - this.userResolver = userResolver; + await SendAsync(notification, ct); - state = new SimpleState<State>(persistenceFactory, GetType(), DomainId.Create("Default")); + await TrackNotifiedAsync(notification.AppId, now); } + } - public Task InitializeAsync( - CancellationToken ct) + private async Task SendAsync(UsageTrackingCheck notification, + CancellationToken ct) + { + if (!userNotifications.IsActive) { - return state.LoadAsync(ct); + return; } - public async Task HandleAsync(UsageTrackingCheck notification, - CancellationToken ct) - { - if (!userNotifications.IsActive) - { - return; - } - - var now = Clock.GetCurrentInstant().ToDateTimeUtc(); - - if (!HasBeenSentBefore(notification.AppId, now)) - { - await SendAsync(notification, ct); + var app = await appProvider.GetAppAsync(notification.AppId, true, ct); - await TrackNotifiedAsync(notification.AppId, now); - } + if (app == null) + { + return; } - private async Task SendAsync(UsageTrackingCheck notification, - CancellationToken ct) + foreach (var userId in notification.Users) { - if (!userNotifications.IsActive) - { - return; - } + var user = await userResolver.FindByIdOrEmailAsync(userId, ct); - var app = await appProvider.GetAppAsync(notification.AppId, true, ct); - - if (app == null) + if (user != null) { - return; - } - - foreach (var userId in notification.Users) - { - var user = await userResolver.FindByIdOrEmailAsync(userId, ct); - - if (user != null) - { - await userNotifications.SendUsageAsync(user, app, - notification.Usage, - notification.UsageLimit, ct); - } + await userNotifications.SendUsageAsync(user, app, + notification.Usage, + notification.UsageLimit, ct); } } + } - private bool HasBeenSentBefore(DomainId appId, DateTime now) + private bool HasBeenSentBefore(DomainId appId, DateTime now) + { + if (state.Value.NotificationsSent.TryGetValue(appId, out var lastSent)) { - if (state.Value.NotificationsSent.TryGetValue(appId, out var lastSent)) - { - var elapsed = now - lastSent; + var elapsed = now - lastSent; - return elapsed < TimeBetweenNotifications; - } - - return false; + return elapsed < TimeBetweenNotifications; } - private Task TrackNotifiedAsync(DomainId appId, DateTime now) - { - state.Value.NotificationsSent[appId] = now; + return false; + } - return state.WriteAsync(); - } + private Task TrackNotifiedAsync(DomainId appId, DateTime now) + { + state.Value.NotificationsSent[appId] = now; + + return state.WriteAsync(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResult.cs b/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResult.cs index 03573edfd1..3ff928e493 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResult.cs @@ -5,17 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public sealed class BulkUpdateResult : List<BulkUpdateResultItem> { - public sealed class BulkUpdateResult : List<BulkUpdateResultItem> + public BulkUpdateResult() { - public BulkUpdateResult() - { - } + } - public BulkUpdateResult(IEnumerable<BulkUpdateResultItem> source) - : base(source) - { - } + public BulkUpdateResult(IEnumerable<BulkUpdateResultItem> source) + : base(source) + { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResultItem.cs b/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResultItem.cs index 7729537322..04f2b98d99 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResultItem.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/BulkUpdateResultItem.cs @@ -9,7 +9,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities -{ - public sealed record BulkUpdateResultItem(DomainId? Id, int JobIndex, Exception? Exception = null); -} +namespace Squidex.Domain.Apps.Entities; + +public sealed record BulkUpdateResultItem(DomainId? Id, int JobIndex, Exception? Exception = null); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs index 13565080ab..7367eb2703 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Comments.Commands +namespace Squidex.Domain.Apps.Entities.Comments.Commands; + +public abstract class CommentTextCommand : CommentCommand { - public abstract class CommentTextCommand : CommentCommand - { - public string Text { get; set; } + public string Text { get; set; } - public string[]? Mentions { get; set; } - } + public string[]? Mentions { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs index b8e1e8d6cd..6934c166bf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Comments.Commands +namespace Squidex.Domain.Apps.Entities.Comments.Commands; + +public sealed class CreateComment : CommentTextCommand { - public sealed class CreateComment : CommentTextCommand - { - public bool IsMention { get; set; } + public bool IsMention { get; set; } - public Uri? Url { get; set; } + public Uri? Url { get; set; } - public CreateComment() - { - CommentId = DomainId.NewGuid(); - } + public CreateComment() + { + CommentId = DomainId.NewGuid(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs index 0db887bf9e..8281697a78 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Comments.Commands +namespace Squidex.Domain.Apps.Entities.Comments.Commands; + +public sealed class DeleteComment : CommentCommand { - public sealed class DeleteComment : CommentCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs index 1f4bb94a36..46307f83e9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Comments.Commands +namespace Squidex.Domain.Apps.Entities.Comments.Commands; + +public sealed class UpdateComment : CommentTextCommand { - public sealed class UpdateComment : CommentTextCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs index afbb766f9e..8a811b85e9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs @@ -10,40 +10,39 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Comments.Commands +namespace Squidex.Domain.Apps.Entities.Comments.Commands; + +public abstract class CommentCommand : CommentsCommand { - public abstract class CommentCommand : CommentsCommand - { - public DomainId CommentId { get; set; } - } + public DomainId CommentId { get; set; } +} - public abstract class CommentsCommand : CommentsCommandBase - { - public static readonly NamedId<DomainId> NoApp = NamedId.Of(DomainId.Empty, "none"); +public abstract class CommentsCommand : CommentsCommandBase +{ + public static readonly NamedId<DomainId> NoApp = NamedId.Of(DomainId.Empty, "none"); - public DomainId CommentsId { get; set; } + public DomainId CommentsId { get; set; } - public override DomainId AggregateId + public override DomainId AggregateId + { + get { - get + if (AppId.Id == default) { - if (AppId.Id == default) - { - return CommentsId; - } - else - { - return DomainId.Combine(AppId, CommentsId); - } + return CommentsId; + } + else + { + return DomainId.Combine(AppId, CommentsId); } } } +} - // This command is needed as marker for middlewares. - public abstract class CommentsCommandBase : SquidexCommand, IAppCommand, IAggregateCommand - { - public NamedId<DomainId> AppId { get; set; } +// This command is needed as marker for middlewares. +public abstract class CommentsCommandBase : SquidexCommand, IAppCommand, IAggregateCommand +{ + public NamedId<DomainId> AppId { get; set; } - public abstract DomainId AggregateId { get; } - } + public abstract DomainId AggregateId { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs index 679e19609f..c9f3217869 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs @@ -16,76 +16,75 @@ using Squidex.Infrastructure.Reflection; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public sealed class CommentTriggerHandler : IRuleTriggerHandler { - public sealed class CommentTriggerHandler : IRuleTriggerHandler + private readonly IScriptEngine scriptEngine; + private readonly IUserResolver userResolver; + + public Type TriggerType => typeof(CommentTrigger); + + public CommentTriggerHandler(IScriptEngine scriptEngine, IUserResolver userResolver) { - private readonly IScriptEngine scriptEngine; - private readonly IUserResolver userResolver; + this.scriptEngine = scriptEngine; - public Type TriggerType => typeof(CommentTrigger); + this.userResolver = userResolver; + } - public CommentTriggerHandler(IScriptEngine scriptEngine, IUserResolver userResolver) - { - this.scriptEngine = scriptEngine; + public bool Handles(AppEvent @event) + { + return @event is CommentCreated; + } - this.userResolver = userResolver; - } + public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, + [EnumeratorCancellation] CancellationToken ct) + { + var commentCreated = (CommentCreated)@event.Payload; - public bool Handles(AppEvent @event) + if (!(commentCreated.Mentions?.Length > 0)) { - return @event is CommentCreated; + yield break; } - public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct) + var users = await userResolver.QueryManyAsync(commentCreated.Mentions, ct); + + if (users.Count <= 0) { - var commentCreated = (CommentCreated)@event.Payload; + yield break; + } - if (!(commentCreated.Mentions?.Length > 0)) + foreach (var user in users.Values) + { + var enrichedEvent = new EnrichedCommentEvent { - yield break; - } - - var users = await userResolver.QueryManyAsync(commentCreated.Mentions, ct); + MentionedUser = user + }; - if (users.Count <= 0) - { - yield break; - } + enrichedEvent.Name = "UserMentioned"; - foreach (var user in users.Values) - { - var enrichedEvent = new EnrichedCommentEvent - { - MentionedUser = user - }; + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map(commentCreated, enrichedEvent); - enrichedEvent.Name = "UserMentioned"; + yield return enrichedEvent; + } + } - // Use the concrete event to map properties that are not part of app event. - SimpleMapper.Map(commentCreated, enrichedEvent); + public bool Trigger(EnrichedEvent @event, RuleContext context) + { + var trigger = (CommentTrigger)context.Rule.Trigger; - yield return enrichedEvent; - } + if (string.IsNullOrWhiteSpace(trigger.Condition)) + { + return true; } - public bool Trigger(EnrichedEvent @event, RuleContext context) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new EventScriptVars { - var trigger = (CommentTrigger)context.Rule.Trigger; + Event = @event + }; - if (string.IsNullOrWhiteSpace(trigger.Condition)) - { - return true; - } - - // Script vars are just wrappers over dictionaries for better performance. - var vars = new EventScriptVars - { - Event = @event - }; - - return scriptEngine.Evaluate(vars, trigger.Condition); - } + return scriptEngine.Evaluate(vars, trigger.Condition); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs index 022aa59487..c3c02bdd8f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs @@ -9,25 +9,24 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public sealed class CommentsLoader : ICommentsLoader { - public sealed class CommentsLoader : ICommentsLoader - { - private readonly IDomainObjectFactory domainObjectFactory; + private readonly IDomainObjectFactory domainObjectFactory; - public CommentsLoader(IDomainObjectFactory domainObjectFactory) - { - this.domainObjectFactory = domainObjectFactory; - } + public CommentsLoader(IDomainObjectFactory domainObjectFactory) + { + this.domainObjectFactory = domainObjectFactory; + } - public async Task<CommentsResult> GetCommentsAsync(DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default) - { - var stream = domainObjectFactory.Create<CommentsStream>(id); + public async Task<CommentsResult> GetCommentsAsync(DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default) + { + var stream = domainObjectFactory.Create<CommentsStream>(id); - await stream.LoadAsync(ct); + await stream.LoadAsync(ct); - return stream.GetComments(version); - } + return stream.GetComments(version); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs index c3ef110e67..6b51dd13c8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs @@ -10,87 +10,86 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public sealed class CommentsResult { - public sealed class CommentsResult - { - public List<Comment> CreatedComments { get; set; } = new List<Comment>(); + public List<Comment> CreatedComments { get; set; } = new List<Comment>(); - public List<Comment> UpdatedComments { get; set; } = new List<Comment>(); + public List<Comment> UpdatedComments { get; set; } = new List<Comment>(); - public List<DomainId> DeletedComments { get; set; } = new List<DomainId>(); + public List<DomainId> DeletedComments { get; set; } = new List<DomainId>(); - public long Version { get; set; } + public long Version { get; set; } - public static CommentsResult FromEvents(IEnumerable<Envelope<CommentsEvent>> events, long currentVersion, int lastVersion) - { - var result = new CommentsResult { Version = currentVersion }; + public static CommentsResult FromEvents(IEnumerable<Envelope<CommentsEvent>> events, long currentVersion, int lastVersion) + { + var result = new CommentsResult { Version = currentVersion }; - foreach (var @event in events.Skip(lastVersion < 0 ? 0 : lastVersion + 1)) + foreach (var @event in events.Skip(lastVersion < 0 ? 0 : lastVersion + 1)) + { + switch (@event.Payload) { - switch (@event.Payload) - { - case CommentDeleted deleted: + case CommentDeleted deleted: + { + var id = deleted.CommentId; + + if (result.CreatedComments.Any(x => x.Id == id)) { - var id = deleted.CommentId; - - if (result.CreatedComments.Any(x => x.Id == id)) - { - result.CreatedComments.RemoveAll(x => x.Id == id); - } - else if (result.UpdatedComments.Any(x => x.Id == id)) - { - result.UpdatedComments.RemoveAll(x => x.Id == id); - result.DeletedComments.Add(id); - } - else - { - result.DeletedComments.Add(id); - } - - break; + result.CreatedComments.RemoveAll(x => x.Id == id); } - - case CommentCreated created: + else if (result.UpdatedComments.Any(x => x.Id == id)) { - var comment = new Comment( - created.CommentId, - @event.Headers.Timestamp(), - @event.Payload.Actor, - created.Text, - created.Url); + result.UpdatedComments.RemoveAll(x => x.Id == id); + result.DeletedComments.Add(id); + } + else + { + result.DeletedComments.Add(id); + } + break; + } + + case CommentCreated created: + { + var comment = new Comment( + created.CommentId, + @event.Headers.Timestamp(), + @event.Payload.Actor, + created.Text, + created.Url); + + result.CreatedComments.Add(comment); + break; + } + + case CommentUpdated updated: + { + var id = updated.CommentId; + + var comment = new Comment( + id, + @event.Headers.Timestamp(), + @event.Payload.Actor, + updated.Text, + null); + + if (result.CreatedComments.Any(x => x.Id == id)) + { + result.CreatedComments.RemoveAll(x => x.Id == id); result.CreatedComments.Add(comment); - break; } - - case CommentUpdated updated: + else { - var id = updated.CommentId; - - var comment = new Comment( - id, - @event.Headers.Timestamp(), - @event.Payload.Actor, - updated.Text, - null); - - if (result.CreatedComments.Any(x => x.Id == id)) - { - result.CreatedComments.RemoveAll(x => x.Id == id); - result.CreatedComments.Add(comment); - } - else - { - result.UpdatedComments.Add(comment); - } - - break; + result.UpdatedComments.Add(comment); } - } - } - return result; + break; + } + } } + + return result; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs index 6331c4f8dc..a82e00f49a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs @@ -10,62 +10,61 @@ using Squidex.Infrastructure.Commands; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject +namespace Squidex.Domain.Apps.Entities.Comments.DomainObject; + +public sealed class CommentsCommandMiddleware : AggregateCommandMiddleware<CommentsCommandBase, CommentsStream> { - public sealed class CommentsCommandMiddleware : AggregateCommandMiddleware<CommentsCommandBase, CommentsStream> - { - private static readonly Regex MentionRegex = new Regex(@"@(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*", RegexOptions.Compiled | RegexOptions.ExplicitCapture, TimeSpan.FromMilliseconds(100)); - private readonly IUserResolver userResolver; + private static readonly Regex MentionRegex = new Regex(@"@(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*", RegexOptions.Compiled | RegexOptions.ExplicitCapture, TimeSpan.FromMilliseconds(100)); + private readonly IUserResolver userResolver; - public CommentsCommandMiddleware(IDomainObjectFactory domainObjectFactory, IUserResolver userResolver) - : base(domainObjectFactory) - { - this.userResolver = userResolver; - } + public CommentsCommandMiddleware(IDomainObjectFactory domainObjectFactory, IUserResolver userResolver) + : base(domainObjectFactory) + { + this.userResolver = userResolver; + } - public override async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + public override async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is CommentsCommand commentsCommand) { - if (context.Command is CommentsCommand commentsCommand) + if (commentsCommand is CreateComment createComment && !IsMention(createComment)) { - if (commentsCommand is CreateComment createComment && !IsMention(createComment)) - { - await MentionUsersAsync(createComment); - } + await MentionUsersAsync(createComment); } - - await base.HandleAsync(context, next, ct); } - private static bool IsMention(CreateComment createComment) - { - return createComment.IsMention; - } + await base.HandleAsync(context, next, ct); + } + + private static bool IsMention(CreateComment createComment) + { + return createComment.IsMention; + } - private async Task MentionUsersAsync(CommentTextCommand command) + private async Task MentionUsersAsync(CommentTextCommand command) + { + if (!string.IsNullOrWhiteSpace(command.Text)) { - if (!string.IsNullOrWhiteSpace(command.Text)) + var emails = MentionRegex.Matches(command.Text).Select(x => x.Value[1..]).ToArray(); + + if (emails.Length > 0) { - var emails = MentionRegex.Matches(command.Text).Select(x => x.Value[1..]).ToArray(); + var mentions = new List<string>(); - if (emails.Length > 0) + foreach (var email in emails) { - var mentions = new List<string>(); + var user = await userResolver.FindByIdOrEmailAsync(email); - foreach (var email in emails) + if (user != null) { - var user = await userResolver.FindByIdOrEmailAsync(email); - - if (user != null) - { - mentions.Add(user.Id); - } + mentions.Add(user.Id); } + } - if (mentions.Count > 0) - { - command.Mentions = mentions.ToArray(); - } + if (mentions.Count > 0) + { + command.Mentions = mentions.ToArray(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs index 5ae4ec39e4..a40721ac27 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs @@ -13,156 +13,155 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject +namespace Squidex.Domain.Apps.Entities.Comments.DomainObject; + +public class CommentsStream : IAggregate { - public class CommentsStream : IAggregate + private readonly List<Envelope<CommentsEvent>> uncommittedEvents = new List<Envelope<CommentsEvent>>(); + private readonly List<Envelope<CommentsEvent>> events = new List<Envelope<CommentsEvent>>(); + private readonly DomainId key; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly string streamName; + private long version = EtagVersion.Empty; + + private long Version => version; + + public CommentsStream( + DomainId key, + IEventFormatter eventFormatter, + IEventStore eventStore) { - private readonly List<Envelope<CommentsEvent>> uncommittedEvents = new List<Envelope<CommentsEvent>>(); - private readonly List<Envelope<CommentsEvent>> events = new List<Envelope<CommentsEvent>>(); - private readonly DomainId key; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly string streamName; - private long version = EtagVersion.Empty; - - private long Version => version; - - public CommentsStream( - DomainId key, - IEventFormatter eventFormatter, - IEventStore eventStore) - { - this.key = key; - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; + this.key = key; + this.eventFormatter = eventFormatter; + this.eventStore = eventStore; - streamName = $"comments-{key}"; - } + streamName = $"comments-{key}"; + } - public virtual async Task LoadAsync( - CancellationToken ct) - { - var storedEvents = await eventStore.QueryReverseAsync(streamName, 100, ct); + public virtual async Task LoadAsync( + CancellationToken ct) + { + var storedEvents = await eventStore.QueryReverseAsync(streamName, 100, ct); - foreach (var @event in storedEvents) - { - var parsedEvent = eventFormatter.Parse(@event); + foreach (var @event in storedEvents) + { + var parsedEvent = eventFormatter.Parse(@event); - version = @event.EventStreamNumber; + version = @event.EventStreamNumber; - events.Add(parsedEvent.To<CommentsEvent>()); - } + events.Add(parsedEvent.To<CommentsEvent>()); } + } - public virtual async Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct) - { - await LoadAsync(ct); + public virtual async Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + await LoadAsync(ct); - switch (command) - { - case CreateComment createComment: - return await Upsert(createComment, c => - { - GuardComments.CanCreate(c); + switch (command) + { + case CreateComment createComment: + return await Upsert(createComment, c => + { + GuardComments.CanCreate(c); - Create(c); - }, ct); + Create(c); + }, ct); - case UpdateComment updateComment: - return await Upsert(updateComment, c => - { - GuardComments.CanUpdate(c, key.ToString(), events); + case UpdateComment updateComment: + return await Upsert(updateComment, c => + { + GuardComments.CanUpdate(c, key.ToString(), events); - Update(c); - }, ct); + Update(c); + }, ct); - case DeleteComment deleteComment: - return await Upsert(deleteComment, c => - { - GuardComments.CanDelete(c, key.ToString(), events); + case DeleteComment deleteComment: + return await Upsert(deleteComment, c => + { + GuardComments.CanDelete(c, key.ToString(), events); - Delete(c); - }, ct); + Delete(c); + }, ct); - default: - ThrowHelper.NotSupportedException(); - return null!; - } + default: + ThrowHelper.NotSupportedException(); + return null!; } + } - private async Task<CommandResult> Upsert<TCommand>(TCommand command, Action<TCommand> handler, - CancellationToken ct) where TCommand : CommentsCommand + private async Task<CommandResult> Upsert<TCommand>(TCommand command, Action<TCommand> handler, + CancellationToken ct) where TCommand : CommentsCommand + { + Guard.NotNull(command); + Guard.NotNull(handler); + + if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) { - Guard.NotNull(command); - Guard.NotNull(handler); + throw new DomainObjectVersionException(key.ToString(), Version, command.ExpectedVersion); + } - if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) - { - throw new DomainObjectVersionException(key.ToString(), Version, command.ExpectedVersion); - } + var previousVersion = version; - var previousVersion = version; + try + { + handler(command); - try + if (uncommittedEvents.Count > 0) { - handler(command); - - if (uncommittedEvents.Count > 0) - { - var commitId = Guid.NewGuid(); + var commitId = Guid.NewGuid(); - var eventData = uncommittedEvents.Select(x => eventFormatter.ToEventData(x, commitId)).ToList(); + var eventData = uncommittedEvents.Select(x => eventFormatter.ToEventData(x, commitId)).ToList(); - await eventStore.AppendAsync(commitId, streamName, previousVersion, eventData, ct); - } - - events.AddRange(uncommittedEvents); - - return CommandResult.Empty(key, Version, previousVersion); + await eventStore.AppendAsync(commitId, streamName, previousVersion, eventData, ct); } - catch - { - version = previousVersion; - throw; - } - finally - { - uncommittedEvents.Clear(); - } - } + events.AddRange(uncommittedEvents); - public void Create(CreateComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentCreated())); + return CommandResult.Empty(key, Version, previousVersion); } - - public void Update(UpdateComment command) + catch { - RaiseEvent(SimpleMapper.Map(command, new CommentUpdated())); - } + version = previousVersion; - public void Delete(DeleteComment command) + throw; + } + finally { - RaiseEvent(SimpleMapper.Map(command, new CommentDeleted())); + uncommittedEvents.Clear(); } + } - private void RaiseEvent(CommentsEvent @event) - { - uncommittedEvents.Add(Envelope.Create(@event)); + public void Create(CreateComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentCreated())); + } - version++; - } + public void Update(UpdateComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentUpdated())); + } - public virtual List<Envelope<CommentsEvent>> GetUncommittedEvents() - { - return uncommittedEvents; - } + public void Delete(DeleteComment command) + { + RaiseEvent(SimpleMapper.Map(command, new CommentDeleted())); + } - public virtual CommentsResult GetComments(long sinceVersion = EtagVersion.Any) - { - return CommentsResult.FromEvents(events, Version, (int)sinceVersion); - } + private void RaiseEvent(CommentsEvent @event) + { + uncommittedEvents.Add(Envelope.Create(@event)); + + version++; + } + + public virtual List<Envelope<CommentsEvent>> GetUncommittedEvents() + { + return uncommittedEvents; + } + + public virtual CommentsResult GetComments(long sinceVersion = EtagVersion.Any) + { + return CommentsResult.FromEvents(events, Version, (int)sinceVersion); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs index 8e8572ae1c..68f7c406a9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs @@ -12,77 +12,76 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards; + +public static class GuardComments { - public static class GuardComments + public static void CanCreate(CreateComment command) { - public static void CanCreate(CreateComment command) - { - Guard.NotNull(command); - - Validate.It(e => - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - e(Not.Defined(nameof(command.Text)), nameof(command.Text)); - } - }); - } + Guard.NotNull(command); - public static void CanUpdate(UpdateComment command, string commentsId, List<Envelope<CommentsEvent>> events) + Validate.It(e => { - Guard.NotNull(command); - - var comment = FindComment(events, command.CommentId); - - if (!string.Equals(commentsId, command.Actor.Identifier, StringComparison.Ordinal) && !comment.Payload.Actor.Equals(command.Actor)) + if (string.IsNullOrWhiteSpace(command.Text)) { - throw new DomainException(T.Get("comments.notUserComment")); + e(Not.Defined(nameof(command.Text)), nameof(command.Text)); } + }); + } - Validate.It(e => - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - e(Not.Defined(nameof(command.Text)), nameof(command.Text)); - } - }); - } + public static void CanUpdate(UpdateComment command, string commentsId, List<Envelope<CommentsEvent>> events) + { + Guard.NotNull(command); - public static void CanDelete(DeleteComment command, string commentsId, List<Envelope<CommentsEvent>> events) - { - Guard.NotNull(command); + var comment = FindComment(events, command.CommentId); - var comment = FindComment(events, command.CommentId); + if (!string.Equals(commentsId, command.Actor.Identifier, StringComparison.Ordinal) && !comment.Payload.Actor.Equals(command.Actor)) + { + throw new DomainException(T.Get("comments.notUserComment")); + } - if (!string.Equals(commentsId, command.Actor.Identifier, StringComparison.Ordinal) && !comment.Payload.Actor.Equals(command.Actor)) + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Text)) { - throw new DomainException(T.Get("comments.notUserComment")); + e(Not.Defined(nameof(command.Text)), nameof(command.Text)); } - } + }); + } + + public static void CanDelete(DeleteComment command, string commentsId, List<Envelope<CommentsEvent>> events) + { + Guard.NotNull(command); - private static Envelope<CommentCreated> FindComment(List<Envelope<CommentsEvent>> events, DomainId commentId) + var comment = FindComment(events, command.CommentId); + + if (!string.Equals(commentsId, command.Actor.Identifier, StringComparison.Ordinal) && !comment.Payload.Actor.Equals(command.Actor)) { - Envelope<CommentCreated>? result = null; + throw new DomainException(T.Get("comments.notUserComment")); + } + } - foreach (var @event in events) + private static Envelope<CommentCreated> FindComment(List<Envelope<CommentsEvent>> events, DomainId commentId) + { + Envelope<CommentCreated>? result = null; + + foreach (var @event in events) + { + if (@event.Payload is CommentCreated created && created.CommentId == commentId) { - if (@event.Payload is CommentCreated created && created.CommentId == commentId) - { - result = @event.To<CommentCreated>(); - } - else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId) - { - result = null; - } + result = @event.To<CommentCreated>(); } - - if (result == null) + else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId) { - throw new DomainObjectNotFoundException(commentId.ToString()); + result = null; } + } - return result; + if (result == null) + { + throw new DomainObjectNotFoundException(commentId.ToString()); } + + return result; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs index 572f72a286..69a30c6526 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public interface ICommentsLoader { - public interface ICommentsLoader - { - Task<CommentsResult> GetCommentsAsync(DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default); - } + Task<CommentsResult> GetCommentsAsync(DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs index fedebc2229..85df75ec7f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public interface IWatchingService { - public interface IWatchingService - { - Task<string[]> GetWatchingUsersAsync(DomainId appId, string? resource, string userId, - CancellationToken ct = default); - } + Task<string[]> GetWatchingUsersAsync(DomainId appId, string? resource, string userId, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs index 451943f166..f7aa2f1461 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs @@ -9,54 +9,53 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public sealed class WatchingService : IWatchingService { - public sealed class WatchingService : IWatchingService + private readonly IPersistenceFactory<State> persistenceFactory; + + [CollectionName("Watches")] + public sealed class State { - private readonly IPersistenceFactory<State> persistenceFactory; + private static readonly Duration Timeout = Duration.FromMinutes(1); - [CollectionName("Watches")] - public sealed class State - { - private static readonly Duration Timeout = Duration.FromMinutes(1); + public Dictionary<string, Instant> Users { get; } = new Dictionary<string, Instant>(); - public Dictionary<string, Instant> Users { get; } = new Dictionary<string, Instant>(); + public (bool, string[]) Add(string watcherId, IClock clock) + { + var now = clock.GetCurrentInstant(); - public (bool, string[]) Add(string watcherId, IClock clock) + foreach (var (userId, lastSeen) in Users.ToList()) { - var now = clock.GetCurrentInstant(); + var timeSinceLastSeen = now - lastSeen; - foreach (var (userId, lastSeen) in Users.ToList()) + if (timeSinceLastSeen > Timeout) { - var timeSinceLastSeen = now - lastSeen; - - if (timeSinceLastSeen > Timeout) - { - Users.Remove(userId); - } + Users.Remove(userId); } + } - Users[watcherId] = now; + Users[watcherId] = now; - return (true, Users.Keys.ToArray()); - } + return (true, Users.Keys.ToArray()); } + } - public IClock Clock { get; set; } = SystemClock.Instance; + public IClock Clock { get; set; } = SystemClock.Instance; - public WatchingService(IPersistenceFactory<State> persistenceFactory) - { - this.persistenceFactory = persistenceFactory; - } + public WatchingService(IPersistenceFactory<State> persistenceFactory) + { + this.persistenceFactory = persistenceFactory; + } - public async Task<string[]> GetWatchingUsersAsync(DomainId appId, string? resource, string userId, - CancellationToken ct = default) - { - var state = new SimpleState<State>(persistenceFactory, GetType(), $"{appId}_{resource}"); + public async Task<string[]> GetWatchingUsersAsync(DomainId appId, string? resource, string userId, + CancellationToken ct = default) + { + var state = new SimpleState<State>(persistenceFactory, GetType(), $"{appId}_{resource}"); - await state.LoadAsync(ct); + await state.LoadAsync(ct); - return await state.UpdateAsync(x => x.Add(userId, Clock), ct: ct); - } + return await state.UpdateAsync(x => x.Add(userId, Clock), ct: ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs index 62bc2fc110..cd43c464e4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs @@ -17,206 +17,205 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class BackupContents : IBackupHandler { - public sealed class BackupContents : IBackupHandler + private const int BatchSize = 100; + private const string UrlsFile = "Urls.json"; + private readonly Dictionary<DomainId, HashSet<DomainId>> contentIdsBySchemaId = new Dictionary<DomainId, HashSet<DomainId>>(); + private readonly Rebuilder rebuilder; + private readonly IUrlGenerator urlGenerator; + private Urls? assetsUrlNew; + private Urls? assetsUrlOld; + + public string Name { get; } = "Contents"; + + public sealed class Urls { - private const int BatchSize = 100; - private const string UrlsFile = "Urls.json"; - private readonly Dictionary<DomainId, HashSet<DomainId>> contentIdsBySchemaId = new Dictionary<DomainId, HashSet<DomainId>>(); - private readonly Rebuilder rebuilder; - private readonly IUrlGenerator urlGenerator; - private Urls? assetsUrlNew; - private Urls? assetsUrlOld; + public string Assets { get; set; } - public string Name { get; } = "Contents"; + public string AssetsApp { get; set; } + } - public sealed class Urls + public BackupContents(Rebuilder rebuilder, IUrlGenerator urlGenerator) + { + this.rebuilder = rebuilder; + this.urlGenerator = urlGenerator; + } + + public async Task BackupEventAsync(Envelope<IEvent> @event, BackupContext context, + CancellationToken ct) + { + if (@event.Payload is AppCreated appCreated) { - public string Assets { get; set; } + var urls = GetUrls(appCreated.Name); - public string AssetsApp { get; set; } + await context.Writer.WriteJsonAsync(UrlsFile, urls, ct); } + } - public BackupContents(Rebuilder rebuilder, IUrlGenerator urlGenerator) + public async Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, + CancellationToken ct) + { + switch (@event.Payload) { - this.rebuilder = rebuilder; - this.urlGenerator = urlGenerator; + case AppCreated appCreated: + assetsUrlNew = GetUrls(appCreated.Name); + assetsUrlOld = await ReadUrlsAsync(context.Reader, ct); + break; + case SchemaDeleted schemaDeleted: + contentIdsBySchemaId.Remove(schemaDeleted.SchemaId.Id); + break; + case ContentCreated contentCreated: + contentIdsBySchemaId.GetOrAddNew(contentCreated.SchemaId.Id) + .Add(@event.Headers.AggregateId()); + + if (assetsUrlNew != null && assetsUrlOld != null) + { + ReplaceAssetUrl(contentCreated.Data); + } + + break; + case ContentUpdated contentUpdated: + if (assetsUrlNew != null && assetsUrlOld != null) + { + ReplaceAssetUrl(contentUpdated.Data); + } + + break; } - public async Task BackupEventAsync(Envelope<IEvent> @event, BackupContext context, - CancellationToken ct) + return true; + } + + private void ReplaceAssetUrl(ContentData data) + { + foreach (var field in data.Values) { - if (@event.Payload is AppCreated appCreated) + if (field != null) { - var urls = GetUrls(appCreated.Name); - - await context.Writer.WriteJsonAsync(UrlsFile, urls, ct); + ReplaceAssetUrl(field); } } + } - public async Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, - CancellationToken ct) + private void ReplaceAssetUrl(IDictionary<string, JsonValue> source) + { + List<(string, string)>? replacements = null; + + foreach (var (key, value) in source) { - switch (@event.Payload) + switch (value.Value) { - case AppCreated appCreated: - assetsUrlNew = GetUrls(appCreated.Name); - assetsUrlOld = await ReadUrlsAsync(context.Reader, ct); - break; - case SchemaDeleted schemaDeleted: - contentIdsBySchemaId.Remove(schemaDeleted.SchemaId.Id); - break; - case ContentCreated contentCreated: - contentIdsBySchemaId.GetOrAddNew(contentCreated.SchemaId.Id) - .Add(@event.Headers.AggregateId()); - - if (assetsUrlNew != null && assetsUrlOld != null) + case string s: { - ReplaceAssetUrl(contentCreated.Data); + var newValue = s.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); + + if (!ReferenceEquals(newValue, s)) + { + replacements ??= new List<(string, string)>(); + replacements.Add((key, newValue)); + break; + } + + newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets, StringComparison.Ordinal); + + if (!ReferenceEquals(newValue, s)) + { + replacements ??= new List<(string, string)>(); + replacements.Add((key, newValue)); + break; + } } break; - case ContentUpdated contentUpdated: - if (assetsUrlNew != null && assetsUrlOld != null) - { - ReplaceAssetUrl(contentUpdated.Data); - } + case JsonArray a: + ReplaceAssetUrl(a); break; - } - return true; + case JsonObject o: + ReplaceAssetUrl(o); + break; + } } - private void ReplaceAssetUrl(ContentData data) + if (replacements != null) { - foreach (var field in data.Values) + foreach (var (key, newValue) in replacements) { - if (field != null) - { - ReplaceAssetUrl(field); - } + source[key] = newValue; } } + } - private void ReplaceAssetUrl(IDictionary<string, JsonValue> source) + private void ReplaceAssetUrl(List<JsonValue> source) + { + for (var i = 0; i < source.Count; i++) { - List<(string, string)>? replacements = null; + var value = source[i]; - foreach (var (key, value) in source) + switch (value.Value) { - switch (value.Value) - { - case string s: + case string s: + { + var newValue = s.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); + + if (!ReferenceEquals(newValue, s)) { - var newValue = s.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); - - if (!ReferenceEquals(newValue, s)) - { - replacements ??= new List<(string, string)>(); - replacements.Add((key, newValue)); - break; - } - - newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets, StringComparison.Ordinal); - - if (!ReferenceEquals(newValue, s)) - { - replacements ??= new List<(string, string)>(); - replacements.Add((key, newValue)); - break; - } + source[i] = newValue; + break; } - break; + newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets, StringComparison.Ordinal); - case JsonArray a: - ReplaceAssetUrl(a); - break; - - case JsonObject o: - ReplaceAssetUrl(o); - break; - } - } - - if (replacements != null) - { - foreach (var (key, newValue) in replacements) - { - source[key] = newValue; - } - } - } - - private void ReplaceAssetUrl(List<JsonValue> source) - { - for (var i = 0; i < source.Count; i++) - { - var value = source[i]; - - switch (value.Value) - { - case string s: + if (!ReferenceEquals(newValue, s)) { - var newValue = s.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); - - if (!ReferenceEquals(newValue, s)) - { - source[i] = newValue; - break; - } - - newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets, StringComparison.Ordinal); - - if (!ReferenceEquals(newValue, s)) - { - source[i] = newValue; - break; - } + source[i] = newValue; + break; } + } - break; + break; - case JsonObject o: - ReplaceAssetUrl(o); - break; - } + case JsonObject o: + ReplaceAssetUrl(o); + break; } } + } - public async Task RestoreAsync(RestoreContext context, - CancellationToken ct) - { - var ids = contentIdsBySchemaId.Values.SelectMany(x => x); + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) + { + var ids = contentIdsBySchemaId.Values.SelectMany(x => x); - if (ids.Any()) - { - await rebuilder.InsertManyAsync<ContentDomainObject, ContentDomainObject.State>(ids, BatchSize, ct); - } + if (ids.Any()) + { + await rebuilder.InsertManyAsync<ContentDomainObject, ContentDomainObject.State>(ids, BatchSize, ct); } + } - private static async Task<Urls?> ReadUrlsAsync(IBackupReader reader, - CancellationToken ct) + private static async Task<Urls?> ReadUrlsAsync(IBackupReader reader, + CancellationToken ct) + { + try { - try - { - return await reader.ReadJsonAsync<Urls>(UrlsFile, ct); - } - catch - { - return null; - } + return await reader.ReadJsonAsync<Urls>(UrlsFile, ct); } - - private Urls GetUrls(string appName) + catch { - return new Urls - { - Assets = urlGenerator.AssetContentBase(), - AssetsApp = urlGenerator.AssetContentBase(appName) - }; + return null; } } + + private Urls GetUrls(string appName) + { + return new Urls + { + Assets = urlGenerator.AssetContentBase(), + AssetsApp = urlGenerator.AssetContentBase(appName) + }; + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContentType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContentType.cs index 51127b4d68..d992a9cba5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContentType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContentType.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public enum BulkUpdateContentType { - public enum BulkUpdateContentType - { - Upsert, - ChangeStatus, - Create, - Delete, - Patch, - Update, - Validate - } + Upsert, + ChangeStatus, + Create, + Delete, + Patch, + Update, + Validate } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs index bf4576feba..63d7febff4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs @@ -7,28 +7,27 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class BulkUpdateContents : SquidexCommand, IAppCommand, ISchemaCommand { - public sealed class BulkUpdateContents : SquidexCommand, IAppCommand, ISchemaCommand - { - public static readonly NamedId<DomainId> NoSchema = NamedId.Of(DomainId.Empty, "none"); + public static readonly NamedId<DomainId> NoSchema = NamedId.Of(DomainId.Empty, "none"); - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public NamedId<DomainId> SchemaId { get; set; } + public NamedId<DomainId> SchemaId { get; set; } - public bool Publish { get; set; } + public bool Publish { get; set; } - public bool DoNotValidate { get; set; } + public bool DoNotValidate { get; set; } - public bool DoNotValidateWorkflow { get; set; } + public bool DoNotValidateWorkflow { get; set; } - public bool DoNotScript { get; set; } + public bool DoNotScript { get; set; } - public bool CheckReferrers { get; set; } + public bool CheckReferrers { get; set; } - public bool OptimizeValidation { get; set; } + public bool OptimizeValidation { get; set; } - public BulkUpdateJob[]? Jobs { get; set; } - } + public BulkUpdateJob[]? Jobs { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs index 2e42d27e9a..c6e9b8de94 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs @@ -11,30 +11,29 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class BulkUpdateJob { - public sealed class BulkUpdateJob - { - public Query<JsonValue>? Query { get; set; } + public Query<JsonValue>? Query { get; set; } - public DomainId? Id { get; set; } + public DomainId? Id { get; set; } - public Status? Status { get; set; } + public Status? Status { get; set; } - public Instant? DueTime { get; set; } + public Instant? DueTime { get; set; } - public BulkUpdateContentType Type { get; set; } + public BulkUpdateContentType Type { get; set; } - public ContentData? Data { get; set; } + public ContentData? Data { get; set; } - public string? Schema { get; set; } + public string? Schema { get; set; } - public bool Patch { get; set; } + public bool Patch { get; set; } - public bool Permanent { get; set; } + public bool Permanent { get; set; } - public long ExpectedCount { get; set; } = 1; + public long ExpectedCount { get; set; } = 1; - public long ExpectedVersion { get; set; } = EtagVersion.Any; - } + public long ExpectedVersion { get; set; } = EtagVersion.Any; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CancelContentSchedule.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CancelContentSchedule.cs index a8f5367eda..ed46d0d425 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CancelContentSchedule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CancelContentSchedule.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class CancelContentSchedule : ContentCommand { - public sealed class CancelContentSchedule : ContentCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs index 5400396a8f..0f25debf29 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -9,22 +9,21 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class ChangeContentStatus : ContentCommand { - public sealed class ChangeContentStatus : ContentCommand - { - public Status Status { get; set; } + public Status Status { get; set; } - public Instant? DueTime { get; set; } + public Instant? DueTime { get; set; } - public DomainId? StatusJobId { get; set; } + public DomainId? StatusJobId { get; set; } - public bool CheckReferrers { get; set; } + public bool CheckReferrers { get; set; } - public bool DoNotValidate { get; set; } + public bool DoNotValidate { get; set; } - public bool DoNotValidateWorkflow { get; set; } + public bool DoNotValidateWorkflow { get; set; } - public bool OptimizeValidation { get; set; } - } + public bool OptimizeValidation { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs index 5cc92b7fab..511e0f0807 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs @@ -7,16 +7,15 @@ using Squidex.Domain.Apps.Core.Contents; -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public abstract class ContentDataCommand : ContentCommand { - public abstract class ContentDataCommand : ContentCommand - { - public ContentData Data { get; set; } + public ContentData Data { get; set; } - public bool DoNotValidate { get; set; } + public bool DoNotValidate { get; set; } - public bool DoNotValidateWorkflow { get; set; } + public bool DoNotValidateWorkflow { get; set; } - public bool OptimizeValidation { get; set; } - } + public bool OptimizeValidation { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs index ef5792c106..cd6308286e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs @@ -9,20 +9,19 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class CreateContent : ContentDataCommand, ISchemaCommand { - public sealed class CreateContent : ContentDataCommand, ISchemaCommand - { - public Status? Status { get; set; } + public Status? Status { get; set; } - public CreateContent() - { - ContentId = DomainId.NewGuid(); - } + public CreateContent() + { + ContentId = DomainId.NewGuid(); + } - public ChangeContentStatus AsChange(Status status) - { - return SimpleMapper.Map(this, new ChangeContentStatus { Status = status }); - } + public ChangeContentStatus AsChange(Status status) + { + return SimpleMapper.Map(this, new ChangeContentStatus { Status = status }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContentDraft.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContentDraft.cs index 60204a54e7..a5f23abb30 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContentDraft.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContentDraft.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class CreateContentDraft : ContentCommand { - public sealed class CreateContentDraft : ContentCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs index 03b6689f55..aa21b8abdc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class DeleteContent : ContentCommand { - public sealed class DeleteContent : ContentCommand - { - public bool CheckReferrers { get; set; } + public bool CheckReferrers { get; set; } - public bool Permanent { get; set; } - } + public bool Permanent { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContentDraft.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContentDraft.cs index ce67222f66..34f6f1995a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContentDraft.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContentDraft.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class DeleteContentDraft : ContentCommand { - public sealed class DeleteContentDraft : ContentCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs index c7d0588232..9fc8c661c5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class PatchContent : UpdateContent { - public sealed class PatchContent : UpdateContent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs index 664926a90e..3e6fbbe538 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public class UpdateContent : ContentDataCommand { - public class UpdateContent : ContentDataCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs index b6e3637ec3..70906fb916 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs @@ -9,34 +9,33 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class UpsertContent : ContentDataCommand, ISchemaCommand { - public sealed class UpsertContent : ContentDataCommand, ISchemaCommand - { - public Status? Status { get; set; } + public Status? Status { get; set; } - public bool CheckReferrers { get; set; } + public bool CheckReferrers { get; set; } - public bool Patch { get; set; } + public bool Patch { get; set; } - public UpsertContent() - { - ContentId = DomainId.NewGuid(); - } + public UpsertContent() + { + ContentId = DomainId.NewGuid(); + } - public CreateContent AsCreate() - { - return SimpleMapper.Map(this, new CreateContent()); - } + public CreateContent AsCreate() + { + return SimpleMapper.Map(this, new CreateContent()); + } - public UpdateContent AsUpdate() - { - return SimpleMapper.Map(this, new UpdateContent()); - } + public UpdateContent AsUpdate() + { + return SimpleMapper.Map(this, new UpdateContent()); + } - public ChangeContentStatus AsChange(Status status) - { - return SimpleMapper.Map(this, new ChangeContentStatus { Status = status }); - } + public ChangeContentStatus AsChange(Status status) + { + return SimpleMapper.Map(this, new ChangeContentStatus { Status = status }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ValidateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ValidateContent.cs index cace4c5a9e..7a5da63705 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ValidateContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ValidateContent.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public sealed class ValidateContent : ContentCommand { - public sealed class ValidateContent : ContentCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/_ContentCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/_ContentCommand.cs index 33f40a4dea..dd5bb0289c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/_ContentCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/_ContentCommand.cs @@ -10,26 +10,25 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Contents.Commands +namespace Squidex.Domain.Apps.Entities.Contents.Commands; + +public abstract class ContentCommand : ContentCommandBase { - public abstract class ContentCommand : ContentCommandBase - { - public DomainId ContentId { get; set; } + public DomainId ContentId { get; set; } - public bool DoNotScript { get; set; } + public bool DoNotScript { get; set; } - public override DomainId AggregateId - { - get => DomainId.Combine(AppId, ContentId); - } + public override DomainId AggregateId + { + get => DomainId.Combine(AppId, ContentId); } +} - public abstract class ContentCommandBase : SquidexCommand, IAppCommand, ISchemaCommand, IAggregateCommand - { - public NamedId<DomainId> AppId { get; set; } +public abstract class ContentCommandBase : SquidexCommand, IAppCommand, ISchemaCommand, IAggregateCommand +{ + public NamedId<DomainId> AppId { get; set; } - public NamedId<DomainId> SchemaId { get; set; } + public NamedId<DomainId> SchemaId { get; set; } - public abstract DomainId AggregateId { get; } - } + public abstract DomainId AggregateId { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCache.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCache.cs index 945795b1a3..daa9c874d0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCache.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCache.cs @@ -10,13 +10,12 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ContentCache : QueryCache<DomainId, IEnrichedContentEntity>, IContentCache { - public sealed class ContentCache : QueryCache<DomainId, IEnrichedContentEntity>, IContentCache + public ContentCache(IMemoryCache? memoryCache, IOptions<ContentOptions> options) + : base(options.Value.CanCache ? memoryCache : null) { - public ContentCache(IMemoryCache? memoryCache, IOptions<ContentOptions> options) - : base(options.Value.CanCache ? memoryCache : null) - { - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index eeda6457a5..3a3d244813 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -22,226 +22,225 @@ #pragma warning disable SA1013 // Closing braces should be spaced correctly -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ContentChangedTriggerHandler : IRuleTriggerHandler, ISubscriptionEventCreator { - public sealed class ContentChangedTriggerHandler : IRuleTriggerHandler, ISubscriptionEventCreator - { - private readonly IScriptEngine scriptEngine; - private readonly IContentLoader contentLoader; - private readonly IContentRepository contentRepository; + private readonly IScriptEngine scriptEngine; + private readonly IContentLoader contentLoader; + private readonly IContentRepository contentRepository; - public bool CanCreateSnapshotEvents => true; + public bool CanCreateSnapshotEvents => true; - public Type TriggerType => typeof(ContentChangedTriggerV2); + public Type TriggerType => typeof(ContentChangedTriggerV2); - public bool Handles(AppEvent appEvent) - { - return appEvent is ContentEvent; - } + public bool Handles(AppEvent appEvent) + { + return appEvent is ContentEvent; + } - public ContentChangedTriggerHandler( - IScriptEngine scriptEngine, - IContentLoader contentLoader, - IContentRepository contentRepository) - { - this.scriptEngine = scriptEngine; - this.contentLoader = contentLoader; - this.contentRepository = contentRepository; - } + public ContentChangedTriggerHandler( + IScriptEngine scriptEngine, + IContentLoader contentLoader, + IContentRepository contentRepository) + { + this.scriptEngine = scriptEngine; + this.contentLoader = contentLoader; + this.contentRepository = contentRepository; + } - public async IAsyncEnumerable<EnrichedEvent> CreateSnapshotEventsAsync(RuleContext context, - [EnumeratorCancellation] CancellationToken ct) - { - var trigger = (ContentChangedTriggerV2)context.Rule.Trigger; + public async IAsyncEnumerable<EnrichedEvent> CreateSnapshotEventsAsync(RuleContext context, + [EnumeratorCancellation] CancellationToken ct) + { + var trigger = (ContentChangedTriggerV2)context.Rule.Trigger; - var schemaIds = - trigger.Schemas?.Count > 0 ? - trigger.Schemas.Select(x => x.SchemaId).Distinct().ToHashSet() : - null; + var schemaIds = + trigger.Schemas?.Count > 0 ? + trigger.Schemas.Select(x => x.SchemaId).Distinct().ToHashSet() : + null; - await foreach (var content in contentRepository.StreamAll(context.AppId.Id, schemaIds, ct)) + await foreach (var content in contentRepository.StreamAll(context.AppId.Id, schemaIds, ct)) + { + var result = new EnrichedContentEvent { - var result = new EnrichedContentEvent - { - Type = EnrichedContentEventType.Created - }; + Type = EnrichedContentEventType.Created + }; - SimpleMapper.Map(content, result); + SimpleMapper.Map(content, result); - result.Actor = content.LastModifiedBy; - result.Name = $"ContentQueried({content.SchemaId.Name.ToPascalCase()})"; + result.Actor = content.LastModifiedBy; + result.Name = $"ContentQueried({content.SchemaId.Name.ToPascalCase()})"; - yield return result; - } + yield return result; } + } - public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct) - { - yield return await CreateEnrichedEventsCoreAsync(@event, ct); - } + public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, + [EnumeratorCancellation] CancellationToken ct) + { + yield return await CreateEnrichedEventsCoreAsync(@event, ct); + } - public async ValueTask<EnrichedEvent?> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, - CancellationToken ct) - { - return await CreateEnrichedEventsCoreAsync(@event, ct); - } + public async ValueTask<EnrichedEvent?> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, + CancellationToken ct) + { + return await CreateEnrichedEventsCoreAsync(@event, ct); + } - private async ValueTask<EnrichedEvent> CreateEnrichedEventsCoreAsync(Envelope<AppEvent> @event, - CancellationToken ct) - { - var contentEvent = (ContentEvent)@event.Payload; + private async ValueTask<EnrichedEvent> CreateEnrichedEventsCoreAsync(Envelope<AppEvent> @event, + CancellationToken ct) + { + var contentEvent = (ContentEvent)@event.Payload; - var result = new EnrichedContentEvent(); + var result = new EnrichedContentEvent(); - var content = - await contentLoader.GetAsync( - contentEvent.AppId.Id, - contentEvent.ContentId, - @event.Headers.EventStreamNumber(), - ct); + var content = + await contentLoader.GetAsync( + contentEvent.AppId.Id, + contentEvent.ContentId, + @event.Headers.EventStreamNumber(), + ct); - if (content != null) - { - SimpleMapper.Map(content, result); - } + if (content != null) + { + SimpleMapper.Map(content, result); + } - // Use the concrete event to map properties that are not part of app event. - SimpleMapper.Map(contentEvent, result); + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map(contentEvent, result); - switch (@event.Payload) - { - case ContentCreated: - result.Type = EnrichedContentEventType.Created; - break; - case ContentDeleted: - result.Type = EnrichedContentEventType.Deleted; - break; - case ContentStatusChanged { Change: StatusChange.Published }: - result.Type = EnrichedContentEventType.Published; - break; - case ContentStatusChanged { Change: StatusChange.Unpublished }: - result.Type = EnrichedContentEventType.Unpublished; - break; - case ContentStatusChanged { Change: StatusChange.Change }: - result.Type = EnrichedContentEventType.StatusChanged; - break; - case ContentUpdated: - { - result.Type = EnrichedContentEventType.Updated; + switch (@event.Payload) + { + case ContentCreated: + result.Type = EnrichedContentEventType.Created; + break; + case ContentDeleted: + result.Type = EnrichedContentEventType.Deleted; + break; + case ContentStatusChanged { Change: StatusChange.Published }: + result.Type = EnrichedContentEventType.Published; + break; + case ContentStatusChanged { Change: StatusChange.Unpublished }: + result.Type = EnrichedContentEventType.Unpublished; + break; + case ContentStatusChanged { Change: StatusChange.Change }: + result.Type = EnrichedContentEventType.StatusChanged; + break; + case ContentUpdated: + { + result.Type = EnrichedContentEventType.Updated; - if (content != null) + if (content != null) + { + var previousContent = + await contentLoader.GetAsync( + content.AppId.Id, + content.Id, + content.Version - 1, + ct); + + if (previousContent != null) { - var previousContent = - await contentLoader.GetAsync( - content.AppId.Id, - content.Id, - content.Version - 1, - ct); - - if (previousContent != null) - { - result.DataOld = previousContent.Data; - } + result.DataOld = previousContent.Data; } - - break; } - } - return result; + break; + } } - public string? GetName(AppEvent @event) - { - switch (@event) - { - case ContentCreated e: - return $"{e.SchemaId.Name.ToPascalCase()}Created"; - case ContentDeleted e: - return $"{e.SchemaId.Name.ToPascalCase()}Deleted"; - case ContentStatusChanged { Change: StatusChange.Published } e: - return $"{e.SchemaId.Name.ToPascalCase()}Published"; - case ContentStatusChanged { Change: StatusChange.Unpublished } e: - return $"{e.SchemaId.Name.ToPascalCase()}Unpublished"; - case ContentStatusChanged { Change: StatusChange.Change } e: - return $"{e.SchemaId.Name.ToPascalCase()}StatusChanged"; - case ContentUpdated e: - return $"{e.SchemaId.Name.ToPascalCase()}Updated"; - } + return result; + } - return null; + public string? GetName(AppEvent @event) + { + switch (@event) + { + case ContentCreated e: + return $"{e.SchemaId.Name.ToPascalCase()}Created"; + case ContentDeleted e: + return $"{e.SchemaId.Name.ToPascalCase()}Deleted"; + case ContentStatusChanged { Change: StatusChange.Published } e: + return $"{e.SchemaId.Name.ToPascalCase()}Published"; + case ContentStatusChanged { Change: StatusChange.Unpublished } e: + return $"{e.SchemaId.Name.ToPascalCase()}Unpublished"; + case ContentStatusChanged { Change: StatusChange.Change } e: + return $"{e.SchemaId.Name.ToPascalCase()}StatusChanged"; + case ContentUpdated e: + return $"{e.SchemaId.Name.ToPascalCase()}Updated"; } - public bool Trigger(Envelope<AppEvent> @event, RuleContext context) + return null; + } + + public bool Trigger(Envelope<AppEvent> @event, RuleContext context) + { + var trigger = (ContentChangedTriggerV2)context.Rule.Trigger; + + if (trigger.HandleAll) { - var trigger = (ContentChangedTriggerV2)context.Rule.Trigger; + return true; + } - if (trigger.HandleAll) - { - return true; - } + if (trigger.Schemas != null) + { + var contentEvent = (ContentEvent)@event.Payload; - if (trigger.Schemas != null) + foreach (var schema in trigger.Schemas) { - var contentEvent = (ContentEvent)@event.Payload; - - foreach (var schema in trigger.Schemas) + if (MatchsSchema(schema, contentEvent.SchemaId)) { - if (MatchsSchema(schema, contentEvent.SchemaId)) - { - return true; - } + return true; } } - - return false; } - public bool Trigger(EnrichedEvent @event, RuleContext context) + return false; + } + + public bool Trigger(EnrichedEvent @event, RuleContext context) + { + var trigger = (ContentChangedTriggerV2)context.Rule.Trigger; + + if (trigger.HandleAll) { - var trigger = (ContentChangedTriggerV2)context.Rule.Trigger; + return true; + } - if (trigger.HandleAll) - { - return true; - } + if (trigger.Schemas != null) + { + var contentEvent = (EnrichedContentEvent)@event; - if (trigger.Schemas != null) + foreach (var schema in trigger.Schemas) { - var contentEvent = (EnrichedContentEvent)@event; - - foreach (var schema in trigger.Schemas) + if (MatchsSchema(schema, contentEvent.SchemaId) && MatchsCondition(schema, contentEvent)) { - if (MatchsSchema(schema, contentEvent.SchemaId) && MatchsCondition(schema, contentEvent)) - { - return true; - } + return true; } } - - return false; } - private static bool MatchsSchema(ContentChangedTriggerSchemaV2? schema, NamedId<DomainId> schemaId) + return false; + } + + private static bool MatchsSchema(ContentChangedTriggerSchemaV2? schema, NamedId<DomainId> schemaId) + { + return schemaId != null && schemaId.Id == schema?.SchemaId; + } + + private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) + { + if (string.IsNullOrWhiteSpace(schema.Condition)) { - return schemaId != null && schemaId.Id == schema?.SchemaId; + return true; } - private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new EventScriptVars { - if (string.IsNullOrWhiteSpace(schema.Condition)) - { - return true; - } + Event = @event + }; - // Script vars are just wrappers over dictionaries for better performance. - var vars = new EventScriptVars - { - Event = @event - }; - - return scriptEngine.Evaluate(vars, schema.Condition); - } + return scriptEngine.Evaluate(vars, schema.Condition); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index a89e86e9d0..1a598c757a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -10,59 +10,58 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ContentEntity : IEnrichedContentEntity { - public sealed class ContentEntity : IEnrichedContentEntity - { - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public NamedId<DomainId> SchemaId { get; set; } + public NamedId<DomainId> SchemaId { get; set; } - public long Version { get; set; } + public long Version { get; set; } - public Instant Created { get; set; } + public Instant Created { get; set; } - public Instant LastModified { get; set; } + public Instant LastModified { get; set; } - public RefToken CreatedBy { get; set; } + public RefToken CreatedBy { get; set; } - public RefToken LastModifiedBy { get; set; } + public RefToken LastModifiedBy { get; set; } - public ContentData Data { get; set; } + public ContentData Data { get; set; } - public ContentData? ReferenceData { get; set; } + public ContentData? ReferenceData { get; set; } - public ScheduleJob? ScheduleJob { get; set; } + public ScheduleJob? ScheduleJob { get; set; } - public Status? NewStatus { get; set; } + public Status? NewStatus { get; set; } - public Status Status { get; set; } + public Status Status { get; set; } - public StatusInfo[]? NextStatuses { get; set; } + public StatusInfo[]? NextStatuses { get; set; } - public bool CanUpdate { get; set; } + public bool CanUpdate { get; set; } - public bool IsSingleton { get; set; } + public bool IsSingleton { get; set; } - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - public string SchemaDisplayName { get; set; } + public string SchemaDisplayName { get; set; } - public string StatusColor { get; set; } + public string StatusColor { get; set; } - public string? NewStatusColor { get; set; } + public string? NewStatusColor { get; set; } - public string? ScheduledStatusColor { get; set; } + public string? ScheduledStatusColor { get; set; } - public string? EditToken { get; set; } + public string? EditToken { get; set; } - public RootField[]? ReferenceFields { get; set; } + public RootField[]? ReferenceFields { get; set; } - public DomainId UniqueId - { - get => DomainId.Combine(AppId, Id); - } + public DomainId UniqueId + { + get => DomainId.Combine(AppId, Id); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentExtensions.cs index 9afdf66deb..6901765b90 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentExtensions.cs @@ -11,142 +11,141 @@ #pragma warning disable IDE0060 // Remove unused parameter -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public static class ContentExtensions { - public static class ContentExtensions - { - private const string HeaderFlatten = "X-Flatten"; - private const string HeaderLanguages = "X-Languages"; - private const string HeaderNoCleanup = "X-NoCleanup"; - private const string HeaderNoEnrichment = "X-NoEnrichment"; - private const string HeaderNoResolveLanguages = "X-NoResolveLanguages"; - private const string HeaderResolveFlow = "X-ResolveFlow"; - private const string HeaderResolveUrls = "X-Resolve-Urls"; - private const string HeaderUnpublished = "X-Unpublished"; - private static readonly char[] Separators = { ',', ';' }; - - public static void AddCacheHeaders(this Context context, IRequestCache cache) - { - cache.AddHeader(HeaderFlatten); - cache.AddHeader(HeaderLanguages); - cache.AddHeader(HeaderNoCleanup); - cache.AddHeader(HeaderNoEnrichment); - cache.AddHeader(HeaderNoResolveLanguages); - cache.AddHeader(HeaderResolveFlow); - cache.AddHeader(HeaderResolveUrls); - cache.AddHeader(HeaderUnpublished); - } + private const string HeaderFlatten = "X-Flatten"; + private const string HeaderLanguages = "X-Languages"; + private const string HeaderNoCleanup = "X-NoCleanup"; + private const string HeaderNoEnrichment = "X-NoEnrichment"; + private const string HeaderNoResolveLanguages = "X-NoResolveLanguages"; + private const string HeaderResolveFlow = "X-ResolveFlow"; + private const string HeaderResolveUrls = "X-Resolve-Urls"; + private const string HeaderUnpublished = "X-Unpublished"; + private static readonly char[] Separators = { ',', ';' }; + + public static void AddCacheHeaders(this Context context, IRequestCache cache) + { + cache.AddHeader(HeaderFlatten); + cache.AddHeader(HeaderLanguages); + cache.AddHeader(HeaderNoCleanup); + cache.AddHeader(HeaderNoEnrichment); + cache.AddHeader(HeaderNoResolveLanguages); + cache.AddHeader(HeaderResolveFlow); + cache.AddHeader(HeaderResolveUrls); + cache.AddHeader(HeaderUnpublished); + } - public static Status EditingStatus(this IContentEntity content) - { - return content.NewStatus ?? content.Status; - } + public static Status EditingStatus(this IContentEntity content) + { + return content.NewStatus ?? content.Status; + } - public static bool IsPublished(this IContentEntity content) - { - return content.EditingStatus() == Status.Published; - } + public static bool IsPublished(this IContentEntity content) + { + return content.EditingStatus() == Status.Published; + } - public static SearchScope Scope(this Context context) - { - return context.ShouldProvideUnpublished() || context.IsFrontendClient ? SearchScope.All : SearchScope.Published; - } + public static SearchScope Scope(this Context context) + { + return context.ShouldProvideUnpublished() || context.IsFrontendClient ? SearchScope.All : SearchScope.Published; + } - public static bool ShouldSkipCleanup(this Context context) - { - return context.Headers.ContainsKey(HeaderNoCleanup); - } + public static bool ShouldSkipCleanup(this Context context) + { + return context.Headers.ContainsKey(HeaderNoCleanup); + } - public static ICloneBuilder WithoutCleanup(this ICloneBuilder builder, bool value = true) - { - return builder.WithBoolean(HeaderNoCleanup, value); - } + public static ICloneBuilder WithoutCleanup(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderNoCleanup, value); + } - public static bool ShouldSkipContentEnrichment(this Context context) - { - return context.Headers.ContainsKey(HeaderNoEnrichment); - } + public static bool ShouldSkipContentEnrichment(this Context context) + { + return context.Headers.ContainsKey(HeaderNoEnrichment); + } - public static ICloneBuilder WithoutContentEnrichment(this ICloneBuilder builder, bool value = true) - { - return builder.WithBoolean(HeaderNoEnrichment, value); - } + public static ICloneBuilder WithoutContentEnrichment(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderNoEnrichment, value); + } - public static bool ShouldProvideUnpublished(this Context context) - { - return context.Headers.ContainsKey(HeaderUnpublished); - } + public static bool ShouldProvideUnpublished(this Context context) + { + return context.Headers.ContainsKey(HeaderUnpublished); + } - public static ICloneBuilder WithUnpublished(this ICloneBuilder builder, bool value = true) - { - return builder.WithBoolean(HeaderUnpublished, value); - } + public static ICloneBuilder WithUnpublished(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderUnpublished, value); + } - public static bool ShouldFlatten(this Context context) - { - return context.Headers.ContainsKey(HeaderFlatten); - } + public static bool ShouldFlatten(this Context context) + { + return context.Headers.ContainsKey(HeaderFlatten); + } - public static ICloneBuilder WithFlatten(this ICloneBuilder builder, bool value = true) - { - return builder.WithBoolean(HeaderFlatten, value); - } + public static ICloneBuilder WithFlatten(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderFlatten, value); + } - public static bool ShouldResolveFlow(this Context context) - { - return context.Headers.ContainsKey(HeaderResolveFlow); - } + public static bool ShouldResolveFlow(this Context context) + { + return context.Headers.ContainsKey(HeaderResolveFlow); + } - public static ICloneBuilder WithResolveFlow(this ICloneBuilder builder, bool value = true) - { - return builder.WithBoolean(HeaderResolveFlow, value); - } + public static ICloneBuilder WithResolveFlow(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderResolveFlow, value); + } - public static bool ShouldResolveLanguages(this Context context) - { - return !context.Headers.ContainsKey(HeaderNoResolveLanguages); - } + public static bool ShouldResolveLanguages(this Context context) + { + return !context.Headers.ContainsKey(HeaderNoResolveLanguages); + } - public static ICloneBuilder WithoutResolveLanguages(this ICloneBuilder builder, bool value = true) + public static ICloneBuilder WithoutResolveLanguages(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderNoResolveLanguages, value); + } + + public static IEnumerable<string> AssetUrls(this Context context) + { + if (context.Headers.TryGetValue(HeaderResolveUrls, out var value)) { - return builder.WithBoolean(HeaderNoResolveLanguages, value); + return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToHashSet(); } - public static IEnumerable<string> AssetUrls(this Context context) - { - if (context.Headers.TryGetValue(HeaderResolveUrls, out var value)) - { - return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToHashSet(); - } + return Enumerable.Empty<string>(); + } - return Enumerable.Empty<string>(); - } + public static ICloneBuilder WithAssetUrlsToResolve(this ICloneBuilder builder, IEnumerable<string>? fieldNames) + { + return builder.WithStrings(HeaderResolveUrls, fieldNames); + } - public static ICloneBuilder WithAssetUrlsToResolve(this ICloneBuilder builder, IEnumerable<string>? fieldNames) + public static IEnumerable<Language> Languages(this Context context) + { + if (context.Headers.TryGetValue(HeaderLanguages, out var value)) { - return builder.WithStrings(HeaderResolveUrls, fieldNames); - } + var languages = new HashSet<Language>(); - public static IEnumerable<Language> Languages(this Context context) - { - if (context.Headers.TryGetValue(HeaderLanguages, out var value)) + foreach (var iso2Code in value.Split(Separators, StringSplitOptions.RemoveEmptyEntries)) { - var languages = new HashSet<Language>(); - - foreach (var iso2Code in value.Split(Separators, StringSplitOptions.RemoveEmptyEntries)) - { - languages.Add(iso2Code); - } - - return languages; + languages.Add(iso2Code); } - return Enumerable.Empty<Language>(); + return languages; } - public static ICloneBuilder WithLanguages(this ICloneBuilder builder, IEnumerable<string> fieldNames) - { - return builder.WithStrings(HeaderLanguages, fieldNames); - } + return Enumerable.Empty<Language>(); + } + + public static ICloneBuilder WithLanguages(this ICloneBuilder builder, IEnumerable<string> fieldNames) + { + return builder.WithStrings(HeaderLanguages, fieldNames); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs index aa5ac59fc0..070cac44d2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs @@ -11,75 +11,74 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase { - public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase + public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) { - public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage<ContentCreated>( - "history.contents.created"); + AddEventMessage<ContentCreated>( + "history.contents.created"); - AddEventMessage<ContentUpdated>( - "history.contents.updated"); + AddEventMessage<ContentUpdated>( + "history.contents.updated"); - AddEventMessage<ContentDeleted>( - "history.contents.deleted"); + AddEventMessage<ContentDeleted>( + "history.contents.deleted"); - AddEventMessage<ContentDraftCreated>( - "history.contents.draftCreated"); + AddEventMessage<ContentDraftCreated>( + "history.contents.draftCreated"); - AddEventMessage<ContentDraftDeleted>( - "history.contents.draftDeleted"); + AddEventMessage<ContentDraftDeleted>( + "history.contents.draftDeleted"); - AddEventMessage<ContentSchedulingCancelled>( - "history.contents.scheduleFailed"); + AddEventMessage<ContentSchedulingCancelled>( + "history.contents.scheduleFailed"); - AddEventMessage<ContentStatusChanged>( - "history.statusChanged"); + AddEventMessage<ContentStatusChanged>( + "history.statusChanged"); - AddEventMessage<ContentStatusScheduled>( - "history.contents.scheduleCompleted"); - } + AddEventMessage<ContentStatusScheduled>( + "history.contents.scheduleCompleted"); + } + + protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event) + { + HistoryEvent? result = null; - protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event) + if (@event.Payload is ContentEvent contentEvent) { - HistoryEvent? result = null; + var channel = $"contents.{contentEvent.ContentId}"; - if (@event.Payload is ContentEvent contentEvent) + if (@event.Payload is SchemaEvent schemaEvent) { - var channel = $"contents.{contentEvent.ContentId}"; - - if (@event.Payload is SchemaEvent schemaEvent) + if (schemaEvent.SchemaId == null) { - if (schemaEvent.SchemaId == null) - { - return Task.FromResult<HistoryEvent?>(null); - } - - channel = $"schemas.{schemaEvent.SchemaId.Id}.{channel}"; + return Task.FromResult<HistoryEvent?>(null); } - result = ForEvent(@event.Payload, channel); + channel = $"schemas.{schemaEvent.SchemaId.Id}.{channel}"; + } - if (@event.Payload is SchemaEvent schemaEvent2) - { - result = result.Param("Schema", schemaEvent2.SchemaId.Name); - } + result = ForEvent(@event.Payload, channel); - if (@event.Payload is ContentStatusChanged contentStatusChanged) - { - result = result.Param("Status", contentStatusChanged.Status); - } + if (@event.Payload is SchemaEvent schemaEvent2) + { + result = result.Param("Schema", schemaEvent2.SchemaId.Name); + } - if (@event.Payload is ContentStatusScheduled contentStatusScheduled) - { - result = result.Param("Status", contentStatusScheduled.Status); - } + if (@event.Payload is ContentStatusChanged contentStatusChanged) + { + result = result.Param("Status", contentStatusChanged.Status); } - return Task.FromResult(result); + if (@event.Payload is ContentStatusScheduled contentStatusScheduled) + { + result = result.Param("Status", contentStatusScheduled.Status); + } } + + return Task.FromResult(result); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs index f198a186d9..54bf13745c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs @@ -5,24 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ContentOptions { - public sealed class ContentOptions - { - public bool CanCache { get; set; } + public bool CanCache { get; set; } - public bool OptimizeForSelfHosting { get; set; } + public bool OptimizeForSelfHosting { get; set; } - public bool UseTransactions { get; set; } + public bool UseTransactions { get; set; } - public int DefaultPageSize { get; set; } = 200; + public int DefaultPageSize { get; set; } = 200; - public int MaxResults { get; set; } = 200; + public int MaxResults { get; set; } = 200; - public string? CDN { get; set; } + public string? CDN { get; set; } - public TimeSpan TimeoutFind { get; set; } = TimeSpan.FromSeconds(1); + public TimeSpan TimeoutFind { get; set; } = TimeSpan.FromSeconds(1); - public TimeSpan TimeoutQuery { get; set; } = TimeSpan.FromSeconds(5); - } + public TimeSpan TimeoutQuery { get; set; } = TimeSpan.FromSeconds(5); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerProcess.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerProcess.cs index 23b39a42ac..eddef8adcb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerProcess.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerProcess.cs @@ -14,83 +14,82 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Timers; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ContentSchedulerProcess : IBackgroundProcess { - public sealed class ContentSchedulerProcess : IBackgroundProcess + private readonly IContentRepository contentRepository; + private readonly ICommandBus commandBus; + private readonly ILogger<ContentSchedulerProcess> log; + private CompletionTimer timer; + + public IClock Clock { get; set; } = SystemClock.Instance; + + public ContentSchedulerProcess( + IContentRepository contentRepository, + ICommandBus commandBus, + ILogger<ContentSchedulerProcess> log) { - private readonly IContentRepository contentRepository; - private readonly ICommandBus commandBus; - private readonly ILogger<ContentSchedulerProcess> log; - private CompletionTimer timer; + this.commandBus = commandBus; + this.contentRepository = contentRepository; + this.log = log; + } - public IClock Clock { get; set; } = SystemClock.Instance; + public Task StartAsync( + CancellationToken ct) + { + timer = new CompletionTimer((int)TimeSpan.FromSeconds(10).TotalMilliseconds, PublishAsync); - public ContentSchedulerProcess( - IContentRepository contentRepository, - ICommandBus commandBus, - ILogger<ContentSchedulerProcess> log) - { - this.commandBus = commandBus; - this.contentRepository = contentRepository; - this.log = log; - } + return Task.CompletedTask; + } - public Task StartAsync( - CancellationToken ct) - { - timer = new CompletionTimer((int)TimeSpan.FromSeconds(10).TotalMilliseconds, PublishAsync); + public Task StopAsync( + CancellationToken ct) + { + return timer?.StopAsync() ?? Task.CompletedTask; + } - return Task.CompletedTask; - } + public async Task PublishAsync( + CancellationToken ct = default) + { + var now = Clock.GetCurrentInstant(); - public Task StopAsync( - CancellationToken ct) + await foreach (var content in contentRepository.QueryScheduledWithoutDataAsync(now, ct)) { - return timer?.StopAsync() ?? Task.CompletedTask; + await TryPublishAsync(content); } + } - public async Task PublishAsync( - CancellationToken ct = default) - { - var now = Clock.GetCurrentInstant(); - - await foreach (var content in contentRepository.QueryScheduledWithoutDataAsync(now, ct)) - { - await TryPublishAsync(content); - } - } + private async Task TryPublishAsync(IContentEntity content) + { + var id = content.Id; - private async Task TryPublishAsync(IContentEntity content) + try { - var id = content.Id; + var job = content.ScheduleJob; - try + if (job != null) { - var job = content.ScheduleJob; - - if (job != null) + var command = new ChangeContentStatus { - var command = new ChangeContentStatus - { - Actor = job.ScheduledBy, - AppId = content.AppId, - ContentId = id, - SchemaId = content.SchemaId, - Status = job.Status, - StatusJobId = job.Id - }; + Actor = job.ScheduledBy, + AppId = content.AppId, + ContentId = id, + SchemaId = content.SchemaId, + Status = job.Status, + StatusJobId = job.Id + }; - await commandBus.PublishAsync(command, default); - } - } - catch (DomainObjectNotFoundException) - { - await contentRepository.ResetScheduledAsync(content.UniqueId, default); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to execute scheduled status change for content '{contentId}'.", content.Id); + await commandBus.PublishAsync(command, default); } } + catch (DomainObjectNotFoundException) + { + await contentRepository.ResetScheduledAsync(content.UniqueId, default); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to execute scheduled status change for content '{contentId}'.", content.Id); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs index 25bac531e8..5bc5869fba 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs @@ -17,126 +17,125 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ContentsSearchSource : ISearchSource { - public sealed class ContentsSearchSource : ISearchSource + private readonly IAppProvider appProvider; + private readonly IContentQueryService contentQuery; + private readonly ITextIndex contentTextIndexer; + private readonly IUrlGenerator urlGenerator; + + public ContentsSearchSource( + IAppProvider appProvider, + IContentQueryService contentQuery, + ITextIndex contentTextIndexer, + IUrlGenerator urlGenerator) + { + this.appProvider = appProvider; + this.contentQuery = contentQuery; + this.contentTextIndexer = contentTextIndexer; + this.urlGenerator = urlGenerator; + } + + public async Task<SearchResults> SearchAsync(string query, Context context, + CancellationToken ct) { - private readonly IAppProvider appProvider; - private readonly IContentQueryService contentQuery; - private readonly ITextIndex contentTextIndexer; - private readonly IUrlGenerator urlGenerator; - - public ContentsSearchSource( - IAppProvider appProvider, - IContentQueryService contentQuery, - ITextIndex contentTextIndexer, - IUrlGenerator urlGenerator) + var result = new SearchResults(); + + var schemaIds = await GetSchemaIdsAsync(context, ct); + + if (schemaIds.Count == 0) { - this.appProvider = appProvider; - this.contentQuery = contentQuery; - this.contentTextIndexer = contentTextIndexer; - this.urlGenerator = urlGenerator; + return result; } - public async Task<SearchResults> SearchAsync(string query, Context context, - CancellationToken ct) + var textQuery = new TextQuery($"{query}~", 10) { - var result = new SearchResults(); + RequiredSchemaIds = schemaIds + }; - var schemaIds = await GetSchemaIdsAsync(context, ct); + var ids = await contentTextIndexer.SearchAsync(context.App, textQuery, context.Scope(), ct); - if (schemaIds.Count == 0) - { - return result; - } - - var textQuery = new TextQuery($"{query}~", 10) - { - RequiredSchemaIds = schemaIds - }; - - var ids = await contentTextIndexer.SearchAsync(context.App, textQuery, context.Scope(), ct); + if (ids == null || ids.Count == 0) + { + return result; + } - if (ids == null || ids.Count == 0) - { - return result; - } + var appId = context.App.NamedId(); - var appId = context.App.NamedId(); + var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).WithoutTotal(), ct); - var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).WithoutTotal(), ct); + foreach (var content in contents) + { + var url = urlGenerator.ContentUI(appId, content.SchemaId, content.Id); - foreach (var content in contents) - { - var url = urlGenerator.ContentUI(appId, content.SchemaId, content.Id); + var name = FormatName(content, context.App.Languages.Master); - var name = FormatName(content, context.App.Languages.Master); + result.Add(name, SearchResultType.Content, url, content.SchemaDisplayName); + } - result.Add(name, SearchResultType.Content, url, content.SchemaDisplayName); - } + return result; + } - return result; - } + private async Task<List<DomainId>> GetSchemaIdsAsync(Context context, + CancellationToken ct) + { + var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); - private async Task<List<DomainId>> GetSchemaIdsAsync(Context context, - CancellationToken ct) - { - var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); + return schemas.Where(x => HasPermission(context, x.SchemaDef.Name)).Select(x => x.Id).ToList(); + } - return schemas.Where(x => HasPermission(context, x.SchemaDef.Name)).Select(x => x.Id).ToList(); - } + private static bool HasPermission(Context context, string schemaName) + { + return context.UserPermissions.Allows(PermissionIds.AppContentsReadOwn, context.App.Name, schemaName); + } - private static bool HasPermission(Context context, string schemaName) - { - return context.UserPermissions.Allows(PermissionIds.AppContentsReadOwn, context.App.Name, schemaName); - } + private static string FormatName(IEnrichedContentEntity content, string masterLanguage) + { + var sb = new StringBuilder(); - private static string FormatName(IEnrichedContentEntity content, string masterLanguage) + JsonValue? GetValue(ContentData? data, RootField field) { - var sb = new StringBuilder(); - - JsonValue? GetValue(ContentData? data, RootField field) + if (data != null && data.TryGetValue(field.Name, out var fieldValue) && fieldValue != null) { - if (data != null && data.TryGetValue(field.Name, out var fieldValue) && fieldValue != null) - { - var isInvariant = field.Partitioning.Equals(Partitioning.Invariant); + var isInvariant = field.Partitioning.Equals(Partitioning.Invariant); - if (isInvariant && fieldValue.TryGetValue("iv", out var value)) - { - return value; - } - - if (!isInvariant && fieldValue.TryGetValue(masterLanguage, out value)) - { - return value; - } + if (isInvariant && fieldValue.TryGetValue("iv", out var value)) + { + return value; } - return null; + if (!isInvariant && fieldValue.TryGetValue(masterLanguage, out value)) + { + return value; + } } - if (content.ReferenceFields != null) + return null; + } + + if (content.ReferenceFields != null) + { + foreach (var field in content.ReferenceFields) { - foreach (var field in content.ReferenceFields) - { - var value = GetValue(content.ReferenceData, field) ?? GetValue(content.Data, field); + var value = GetValue(content.ReferenceData, field) ?? GetValue(content.Data, field); - var formatted = StringFormatter.Format(field, value ?? default); + var formatted = StringFormatter.Format(field, value ?? default); - if (!string.IsNullOrWhiteSpace(formatted)) - { - sb.AppendIfNotEmpty(", "); - sb.Append(formatted); - } + if (!string.IsNullOrWhiteSpace(formatted)) + { + sb.AppendIfNotEmpty(", "); + sb.Append(formatted); } } + } - if (sb.Length == 0) - { - return "Content"; - } - - return sb.ToString(); + if (sb.Length == 0) + { + return "Content"; } + + return sb.ToString(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs index c86a92c319..dbf6e802cf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs @@ -11,112 +11,111 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Tasks; -namespace Squidex.Domain.Apps.Entities.Contents.Counter +namespace Squidex.Domain.Apps.Entities.Contents.Counter; + +public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor { - public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor + private delegate long CounterReset(string name, long value = 0); + private delegate void CounterResetV2(string name, Action<JsValue>? callback = null, long value = 0); + private readonly ICounterService counterService; + + public CounterJintExtension(ICounterService counterService) { - private delegate long CounterReset(string name, long value = 0); - private delegate void CounterResetV2(string name, Action<JsValue>? callback = null, long value = 0); - private readonly ICounterService counterService; + this.counterService = counterService; + } - public CounterJintExtension(ICounterService counterService) + public void Extend(ScriptExecutionContext context) + { + if (!context.TryGetValue<DomainId>("appId", out var appId)) { - this.counterService = counterService; + return; } - public void Extend(ScriptExecutionContext context) + var increment = new Func<string, long>(name => { - if (!context.TryGetValue<DomainId>("appId", out var appId)) - { - return; - } + return Increment(appId, name); + }); - var increment = new Func<string, long>(name => - { - return Increment(appId, name); - }); + context.Engine.SetValue("incrementCounter", increment); - context.Engine.SetValue("incrementCounter", increment); + var reset = new CounterReset((name, value) => + { + return Reset(appId, name, value); + }); - var reset = new CounterReset((name, value) => - { - return Reset(appId, name, value); - }); + context.Engine.SetValue("resetCounter", reset); + } - context.Engine.SetValue("resetCounter", reset); + public void ExtendAsync(ScriptExecutionContext context) + { + if (!context.TryGetValue<DomainId>("appId", out var appId)) + { + return; } - public void ExtendAsync(ScriptExecutionContext context) + var increment = new Action<string, Action<JsValue>>((name, callback) => { - if (!context.TryGetValue<DomainId>("appId", out var appId)) - { - return; - } + IncrementV2(context, appId, name, callback); + }); - var increment = new Action<string, Action<JsValue>>((name, callback) => - { - IncrementV2(context, appId, name, callback); - }); + context.Engine.SetValue("incrementCounterV2", increment); - context.Engine.SetValue("incrementCounterV2", increment); + var reset = new CounterResetV2((name, callback, value) => + { + ResetV2(context, appId, name, callback, value); + }); - var reset = new CounterResetV2((name, callback, value) => - { - ResetV2(context, appId, name, callback, value); - }); + context.Engine.SetValue("resetCounterV2", reset); + } - context.Engine.SetValue("resetCounterV2", reset); - } + public void Describe(AddDescription describe, ScriptScope scope) + { + describe(JsonType.Function, "incrementCounter(name)", + Resources.ScriptingIncrementCounter); - public void Describe(AddDescription describe, ScriptScope scope) - { - describe(JsonType.Function, "incrementCounter(name)", - Resources.ScriptingIncrementCounter); + describe(JsonType.Function, "incrementCounterV2(name, callback?)", + Resources.ScriptingIncrementCounterV2); - describe(JsonType.Function, "incrementCounterV2(name, callback?)", - Resources.ScriptingIncrementCounterV2); + describe(JsonType.Function, "resetCounter(name, value?)", + Resources.ScriptingResetCounter); - describe(JsonType.Function, "resetCounter(name, value?)", - Resources.ScriptingResetCounter); + describe(JsonType.Function, "resetCounter(name, callback?, value?)", + Resources.ScriptingResetCounterV2); + } - describe(JsonType.Function, "resetCounter(name, callback?, value?)", - Resources.ScriptingResetCounterV2); - } + private long Increment(DomainId appId, string name) + { + return AsyncHelper.Sync(() => counterService.IncrementAsync(appId, name)); + } - private long Increment(DomainId appId, string name) + private void IncrementV2(ScriptExecutionContext context, DomainId appId, string name, Action<JsValue> callback) + { + context.Schedule(async (scheduler, ct) => { - return AsyncHelper.Sync(() => counterService.IncrementAsync(appId, name)); - } + var result = await counterService.IncrementAsync(appId, name, ct); - private void IncrementV2(ScriptExecutionContext context, DomainId appId, string name, Action<JsValue> callback) - { - context.Schedule(async (scheduler, ct) => + if (callback != null) { - var result = await counterService.IncrementAsync(appId, name, ct); + scheduler.Run(callback, JsValue.FromObject(context.Engine, result)); + } + }); + } - if (callback != null) - { - scheduler.Run(callback, JsValue.FromObject(context.Engine, result)); - } - }); - } + private long Reset(DomainId appId, string name, long value) + { + return AsyncHelper.Sync(() => counterService.ResetAsync(appId, name, value)); + } - private long Reset(DomainId appId, string name, long value) + private void ResetV2(ScriptExecutionContext context, DomainId appId, string name, Action<JsValue>? callback, long value) + { + context.Schedule(async (scheduler, ct) => { - return AsyncHelper.Sync(() => counterService.ResetAsync(appId, name, value)); - } + var result = await counterService.ResetAsync(appId, name, value, ct); - private void ResetV2(ScriptExecutionContext context, DomainId appId, string name, Action<JsValue>? callback, long value) - { - context.Schedule(async (scheduler, ct) => + if (callback != null) { - var result = await counterService.ResetAsync(appId, name, value, ct); - - if (callback != null) - { - scheduler.Run(callback, JsValue.FromObject(context.Engine, result)); - } - }); - } + scheduler.Run(callback, JsValue.FromObject(context.Engine, result)); + } + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs index d52068c472..78e494e92f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs @@ -9,73 +9,72 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Contents.Counter +namespace Squidex.Domain.Apps.Entities.Contents.Counter; + +public sealed class CounterService : ICounterService, IDeleter { - public sealed class CounterService : ICounterService, IDeleter + private readonly IPersistenceFactory<State> persistenceFactory; + + [CollectionName("Counters")] + public sealed class State { - private readonly IPersistenceFactory<State> persistenceFactory; + public Dictionary<string, long> Counters { get; set; } = new Dictionary<string, long>(); - [CollectionName("Counters")] - public sealed class State + public bool Increment(string name) { - public Dictionary<string, long> Counters { get; set; } = new Dictionary<string, long>(); - - public bool Increment(string name) - { - Counters[name] = Counters.GetValueOrDefault(name) + 1; - - return true; - } + Counters[name] = Counters.GetValueOrDefault(name) + 1; - public bool Reset(string name, long value) - { - Counters[name] = value; - - return true; - } + return true; } - public CounterService(IPersistenceFactory<State> persistenceFactory) + public bool Reset(string name, long value) { - this.persistenceFactory = persistenceFactory; + Counters[name] = value; + + return true; } + } - async Task IDeleter.DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - var state = await GetStateAsync(app.Id, ct); + public CounterService(IPersistenceFactory<State> persistenceFactory) + { + this.persistenceFactory = persistenceFactory; + } - await state.ClearAsync(ct); - } + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + var state = await GetStateAsync(app.Id, ct); - public async Task<long> IncrementAsync(DomainId appId, string name, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, ct); + await state.ClearAsync(ct); + } - await state.UpdateAsync(x => x.Increment(name), ct: ct); + public async Task<long> IncrementAsync(DomainId appId, string name, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, ct); - return state.Value.Counters[name]; - } + await state.UpdateAsync(x => x.Increment(name), ct: ct); - public async Task<long> ResetAsync(DomainId appId, string name, long value, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, ct); + return state.Value.Counters[name]; + } + + public async Task<long> ResetAsync(DomainId appId, string name, long value, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, ct); - await state.UpdateAsync(x => x.Reset(name, value), ct: ct); + await state.UpdateAsync(x => x.Reset(name, value), ct: ct); - return state.Value.Counters[name]; - } + return state.Value.Counters[name]; + } - private async Task<SimpleState<State>> GetStateAsync(DomainId appId, - CancellationToken ct) - { - var state = new SimpleState<State>(persistenceFactory, GetType(), appId); + private async Task<SimpleState<State>> GetStateAsync(DomainId appId, + CancellationToken ct) + { + var state = new SimpleState<State>(persistenceFactory, GetType(), appId); - await state.LoadAsync(ct); + await state.LoadAsync(ct); - return state; - } + return state; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterService.cs index 1ed274b62c..45a490cf54 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterService.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Counter +namespace Squidex.Domain.Apps.Entities.Contents.Counter; + +public interface ICounterService { - public interface ICounterService - { - Task<long> IncrementAsync(DomainId appId, string name, - CancellationToken ct = default); + Task<long> IncrementAsync(DomainId appId, string name, + CancellationToken ct = default); - Task<long> ResetAsync(DomainId appId, string name, long value, - CancellationToken ct = default); - } + Task<long> ResetAsync(DomainId appId, string name, long value, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs index 5eeaa43070..b87550083e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -9,95 +9,94 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class DefaultContentWorkflow : IContentWorkflow { - public sealed class DefaultContentWorkflow : IContentWorkflow + private static readonly StatusInfo InfoArchived = new StatusInfo(Status.Archived, StatusColors.Archived); + private static readonly StatusInfo InfoDraft = new StatusInfo(Status.Draft, StatusColors.Draft); + private static readonly StatusInfo InfoPublished = new StatusInfo(Status.Published, StatusColors.Published); + + private static readonly StatusInfo[] All = { - private static readonly StatusInfo InfoArchived = new StatusInfo(Status.Archived, StatusColors.Archived); - private static readonly StatusInfo InfoDraft = new StatusInfo(Status.Draft, StatusColors.Draft); - private static readonly StatusInfo InfoPublished = new StatusInfo(Status.Published, StatusColors.Published); + InfoArchived, + InfoDraft, + InfoPublished + }; - private static readonly StatusInfo[] All = + private static readonly Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> Flow = + new Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> { - InfoArchived, - InfoDraft, - InfoPublished + [Status.Archived] = (InfoArchived, new[] + { + InfoDraft + }), + [Status.Draft] = (InfoDraft, new[] + { + InfoArchived, + InfoPublished + }), + [Status.Published] = (InfoPublished, new[] + { + InfoDraft, + InfoArchived + }) }; - private static readonly Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> Flow = - new Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> - { - [Status.Archived] = (InfoArchived, new[] - { - InfoDraft - }), - [Status.Draft] = (InfoDraft, new[] - { - InfoArchived, - InfoPublished - }), - [Status.Published] = (InfoPublished, new[] - { - InfoDraft, - InfoArchived - }) - }; - - public ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema) - { - return ValueTask.FromResult(Status.Draft); - } + public ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema) + { + return ValueTask.FromResult(Status.Draft); + } - public ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) - { - return ValueTask.FromResult(true); - } + public ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) + { + return ValueTask.FromResult(true); + } - public ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status) - { - var result = status == Status.Published && schema.SchemaDef.Properties.ValidateOnPublish; + public ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status) + { + var result = status == Status.Published && schema.SchemaDef.Properties.ValidateOnPublish; - return ValueTask.FromResult(result); - } + return ValueTask.FromResult(result); + } - public ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) - { - var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next); + public ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) + { + var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next); - return ValueTask.FromResult(result); - } + return ValueTask.FromResult(result); + } - public ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) - { - var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next); + public ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) + { + var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next); - return ValueTask.FromResult(result); - } + return ValueTask.FromResult(result); + } - public ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) - { - var result = status != Status.Archived; + public ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + { + var result = status != Status.Archived; - return ValueTask.FromResult(result); - } + return ValueTask.FromResult(result); + } - public ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status) - { - var result = Flow.GetValueOrDefault(status).Info; + public ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status) + { + var result = Flow.GetValueOrDefault(status).Info; - return ValueTask.FromResult<StatusInfo?>(result); - } + return ValueTask.FromResult<StatusInfo?>(result); + } - public ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) - { - var result = Flow.TryGetValue(status, out var step) ? step.Transitions : Array.Empty<StatusInfo>(); + public ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + { + var result = Flow.TryGetValue(status, out var step) ? step.Transitions : Array.Empty<StatusInfo>(); - return ValueTask.FromResult(result); - } + return ValueTask.FromResult(result); + } - public ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema) - { - return ValueTask.FromResult(All); - } + public ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema) + { + return ValueTask.FromResult(All); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs index 8e09916c49..9a0591a535 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs @@ -9,44 +9,43 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class DefaultWorkflowsValidator : IWorkflowsValidator { - public sealed class DefaultWorkflowsValidator : IWorkflowsValidator + private readonly IAppProvider appProvider; + + public DefaultWorkflowsValidator(IAppProvider appProvider) { - private readonly IAppProvider appProvider; + this.appProvider = appProvider; + } - public DefaultWorkflowsValidator(IAppProvider appProvider) - { - this.appProvider = appProvider; - } + public async Task<IReadOnlyList<string>> ValidateAsync(DomainId appId, Workflows workflows) + { + Guard.NotNull(workflows); - public async Task<IReadOnlyList<string>> ValidateAsync(DomainId appId, Workflows workflows) + var errors = new List<string>(); + + if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1) { - Guard.NotNull(workflows); + errors.Add(T.Get("workflows.overlap")); + } - var errors = new List<string>(); + var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList(); - if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1) + foreach (var schemaId in uniqueSchemaIds) + { + if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1) { - errors.Add(T.Get("workflows.overlap")); - } + var schema = await appProvider.GetSchemaAsync(appId, schemaId); - var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList(); - - foreach (var schemaId in uniqueSchemaIds) - { - if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1) + if (schema != null) { - var schema = await appProvider.GetSchemaAsync(appId, schemaId); - - if (schema != null) - { - errors.Add(T.Get("workflows.schemaOverlap", new { schema = schema.SchemaDef.Name })); - } + errors.Add(T.Get("workflows.schemaOverlap", new { schema = schema.SchemaDef.Name })); } } - - return errors; } + + return errors; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs index f3b2c3a8b8..9ab52530a5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs @@ -9,35 +9,34 @@ using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public sealed class ContentCommandMiddleware : CachingDomainObjectMiddleware<ContentCommand, ContentDomainObject, ContentDomainObject.State> { - public sealed class ContentCommandMiddleware : CachingDomainObjectMiddleware<ContentCommand, ContentDomainObject, ContentDomainObject.State> + private readonly IContentEnricher contentEnricher; + private readonly IContextProvider contextProvider; + + public ContentCommandMiddleware( + IDomainObjectFactory domainObjectFactory, + IDomainObjectCache domainObjectCache, + IContentEnricher contentEnricher, + IContextProvider contextProvider) + : base(domainObjectFactory, domainObjectCache) { - private readonly IContentEnricher contentEnricher; - private readonly IContextProvider contextProvider; + this.contentEnricher = contentEnricher; + this.contextProvider = contextProvider; + } - public ContentCommandMiddleware( - IDomainObjectFactory domainObjectFactory, - IDomainObjectCache domainObjectCache, - IContentEnricher contentEnricher, - IContextProvider contextProvider) - : base(domainObjectFactory, domainObjectCache) - { - this.contentEnricher = contentEnricher; - this.contextProvider = contextProvider; - } + protected override async Task<object> EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) + { + var payload = await base.EnrichResultAsync(context, result, ct); - protected override async Task<object> EnrichResultAsync(CommandContext context, CommandResult result, - CancellationToken ct) + if (payload is IContentEntity content and not IEnrichedContentEntity) { - var payload = await base.EnrichResultAsync(context, result, ct); - - if (payload is IContentEntity content and not IEnrichedContentEntity) - { - payload = await contentEnricher.EnrichAsync(content, true, contextProvider.Context, ct); - } - - return payload; + payload = await contentEnricher.EnrichAsync(content, true, contextProvider.Context, ct); } + + return payload; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs index ad0f5fdce3..122b204909 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs @@ -12,152 +12,151 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public partial class ContentDomainObject { - public partial class ContentDomainObject + public sealed class State : DomainObjectState<State>, IContentEntity { - public sealed class State : DomainObjectState<State>, IContentEntity - { - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public NamedId<DomainId> SchemaId { get; set; } + public NamedId<DomainId> SchemaId { get; set; } - public ContentVersion? NewVersion { get; set; } + public ContentVersion? NewVersion { get; set; } - public ContentVersion CurrentVersion { get; set; } + public ContentVersion CurrentVersion { get; set; } - public ScheduleJob? ScheduleJob { get; set; } + public ScheduleJob? ScheduleJob { get; set; } - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - [JsonIgnore] - public DomainId UniqueId - { - get => DomainId.Combine(AppId, Id); - } + [JsonIgnore] + public DomainId UniqueId + { + get => DomainId.Combine(AppId, Id); + } - [JsonIgnore] - public ContentData Data - { - get => NewVersion?.Data ?? CurrentData; - } + [JsonIgnore] + public ContentData Data + { + get => NewVersion?.Data ?? CurrentData; + } - [JsonIgnore] - public ContentData CurrentData - { - get => CurrentVersion.Data; - } + [JsonIgnore] + public ContentData CurrentData + { + get => CurrentVersion.Data; + } - [JsonIgnore] - public Status? NewStatus - { - get => NewVersion?.Status; - } + [JsonIgnore] + public Status? NewStatus + { + get => NewVersion?.Status; + } - [JsonIgnore] - public Status Status - { - get => CurrentVersion?.Status ?? default; - } + [JsonIgnore] + public Status Status + { + get => CurrentVersion?.Status ?? default; + } - public override bool ApplyEvent(IEvent @event, EnvelopeHeaders headers) + public override bool ApplyEvent(IEvent @event, EnvelopeHeaders headers) + { + switch (@event) { - switch (@event) - { - case ContentCreated e: - { - Id = e.ContentId; + case ContentCreated e: + { + Id = e.ContentId; - SimpleMapper.Map(e, this); + SimpleMapper.Map(e, this); - CurrentVersion = new ContentVersion(e.Status, e.Data); + CurrentVersion = new ContentVersion(e.Status, e.Data); - break; - } + break; + } - case ContentDraftCreated e: - { - var newData = e.MigratedData?.UseSameFields(CurrentData) ?? CurrentData; + case ContentDraftCreated e: + { + var newData = e.MigratedData?.UseSameFields(CurrentData) ?? CurrentData; - NewVersion = new ContentVersion(e.Status, newData); + NewVersion = new ContentVersion(e.Status, newData); - ScheduleJob = null; + ScheduleJob = null; - break; - } + break; + } - case ContentDraftDeleted: - { - NewVersion = null; + case ContentDraftDeleted: + { + NewVersion = null; - ScheduleJob = null; + ScheduleJob = null; - break; - } + break; + } - case ContentStatusChanged e: - { - ScheduleJob = null; + case ContentStatusChanged e: + { + ScheduleJob = null; - if (NewVersion != null) + if (NewVersion != null) + { + if (e.Status == Status.Published) { - if (e.Status == Status.Published) - { - CurrentVersion = new ContentVersion(e.Status, NewVersion.Data.UseSameFields(CurrentData)); - - NewVersion = null; - } - else - { - NewVersion = NewVersion.WithStatus(e.Status); - } + CurrentVersion = new ContentVersion(e.Status, NewVersion.Data.UseSameFields(CurrentData)); + + NewVersion = null; } else { - CurrentVersion = CurrentVersion.WithStatus(e.Status); + NewVersion = NewVersion.WithStatus(e.Status); } - - break; } - - case ContentSchedulingCancelled: + else { - ScheduleJob = null; - - break; + CurrentVersion = CurrentVersion.WithStatus(e.Status); } - case ContentStatusScheduled e: - { - ScheduleJob = ScheduleJob.Build(e.Status, e.Actor, e.DueTime); + break; + } - break; - } + case ContentSchedulingCancelled: + { + ScheduleJob = null; - case ContentUpdated e: - { - if (NewVersion != null) - { - NewVersion = NewVersion.WithData(e.Data.UseSameFields(Data)); - } - else - { - CurrentVersion = CurrentVersion.WithData(e.Data.UseSameFields(CurrentData)); - } + break; + } - break; - } + case ContentStatusScheduled e: + { + ScheduleJob = ScheduleJob.Build(e.Status, e.Actor, e.DueTime); - case ContentDeleted: - { - IsDeleted = true; + break; + } - break; + case ContentUpdated e: + { + if (NewVersion != null) + { + NewVersion = NewVersion.WithData(e.Data.UseSameFields(Data)); + } + else + { + CurrentVersion = CurrentVersion.WithData(e.Data.UseSameFields(CurrentData)); } - } - return true; + break; + } + + case ContentDeleted: + { + IsDeleted = true; + + break; + } } + + return true; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index 5bf4ee03c4..ff088b87a5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -22,474 +22,473 @@ #pragma warning disable MA0022 // Return Task.FromResult instead of returning null -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public partial class ContentDomainObject : DomainObject<ContentDomainObject.State> { - public partial class ContentDomainObject : DomainObject<ContentDomainObject.State> + private readonly IServiceProvider serviceProvider; + + public ContentDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<ContentDomainObject> log, + IServiceProvider serviceProvider) + : base(id, persistence, log) { - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public ContentDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<ContentDomainObject> log, - IServiceProvider serviceProvider) - : base(id, persistence, log) - { - this.serviceProvider = serviceProvider; - } + protected override bool IsDeleted(State snapshot) + { + return snapshot.IsDeleted; + } - protected override bool IsDeleted(State snapshot) - { - return snapshot.IsDeleted; - } + protected override bool CanAcceptCreation(ICommand command) + { + return command is ContentCommandBase; + } - protected override bool CanAcceptCreation(ICommand command) - { - return command is ContentCommandBase; - } + protected override bool CanRecreate() + { + return true; + } - protected override bool CanRecreate() - { - return true; - } + protected override bool CanRecreate(IEvent @event) + { + return @event is ContentCreated; + } - protected override bool CanRecreate(IEvent @event) - { - return @event is ContentCreated; - } + protected override bool CanAccept(ICommand command) + { + return command is ContentCommand contentCommand && + Equals(contentCommand.AppId, Snapshot.AppId) && + Equals(contentCommand.SchemaId, Snapshot.SchemaId) && + Equals(contentCommand.ContentId, Snapshot.Id); + } - protected override bool CanAccept(ICommand command) + public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + switch (command) { - return command is ContentCommand contentCommand && - Equals(contentCommand.AppId, Snapshot.AppId) && - Equals(contentCommand.SchemaId, Snapshot.SchemaId) && - Equals(contentCommand.ContentId, Snapshot.Id); - } + case UpsertContent upsertContent: + return UpsertReturnAsync(upsertContent, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct) - { - switch (command) - { - case UpsertContent upsertContent: - return UpsertReturnAsync(upsertContent, async (c, ct) => + if (Version <= EtagVersion.Empty || IsDeleted(Snapshot)) + { + await CreateCore(c.AsCreate(), operation); + } + else if (c.Patch) { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await PatchCore(c.AsUpdate(), operation); + } + else + { + await UpdateCore(c.AsUpdate(), operation); + } - if (Version <= EtagVersion.Empty || IsDeleted(Snapshot)) - { - await CreateCore(c.AsCreate(), operation); - } - else if (c.Patch) - { - await PatchCore(c.AsUpdate(), operation); - } - else - { - await UpdateCore(c.AsUpdate(), operation); - } + if (Is.OptionalChange(operation.Snapshot.EditingStatus(), c.Status)) + { + await ChangeCore(c.AsChange(c.Status.Value), operation); + } - if (Is.OptionalChange(operation.Snapshot.EditingStatus(), c.Status)) - { - await ChangeCore(c.AsChange(c.Status.Value), operation); - } + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case CreateContent createContent: + return CreateReturnAsync(createContent, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - case CreateContent createContent: - return CreateReturnAsync(createContent, async (c, ct) => + await CreateCore(c, operation); + + if (operation.Schema.SchemaDef.Type == SchemaType.Singleton) + { + ChangeStatus(c.AsChange(Status.Published)); + } + else if (Is.OptionalChange(Snapshot.Status, c.Status)) { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await ChangeCore(c.AsChange(c.Status.Value), operation); + } - await CreateCore(c, operation); + return Snapshot; + }, ct); - if (operation.Schema.SchemaDef.Type == SchemaType.Singleton) - { - ChangeStatus(c.AsChange(Status.Published)); - } - else if (Is.OptionalChange(Snapshot.Status, c.Status)) - { - await ChangeCore(c.AsChange(c.Status.Value), operation); - } + case ValidateContent validate: + return UpdateReturnAsync(validate, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - return Snapshot; - }, ct); + await ValidateCore(operation); - case ValidateContent validate: - return UpdateReturnAsync(validate, async (c, ct) => - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + return true; + }, ct); - await ValidateCore(operation); + case CreateContentDraft createDraft: + return UpdateReturnAsync(createDraft, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - return true; - }, ct); + await CreateDraftCore(c, operation); - case CreateContentDraft createDraft: - return UpdateReturnAsync(createDraft, async (c, ct) => - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + return Snapshot; + }, ct); - await CreateDraftCore(c, operation); + case DeleteContentDraft deleteDraft: + return UpdateReturnAsync(deleteDraft, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - return Snapshot; - }, ct); + DeleteDraftCore(c, operation); - case DeleteContentDraft deleteDraft: - return UpdateReturnAsync(deleteDraft, async (c, ct) => - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + return Snapshot; + }, ct); - DeleteDraftCore(c, operation); - - return Snapshot; - }, ct); + case PatchContent patchContent: + return UpdateReturnAsync(patchContent, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - case PatchContent patchContent: - return UpdateReturnAsync(patchContent, async (c, ct) => - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await PatchCore(c, operation); - await PatchCore(c, operation); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateContent updateContent: + return UpdateReturnAsync(updateContent, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - case UpdateContent updateContent: - return UpdateReturnAsync(updateContent, async (c, ct) => - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + await UpdateCore(c, operation); - await UpdateCore(c, operation); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case CancelContentSchedule cancelContentSchedule: + return UpdateReturnAsync(cancelContentSchedule, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - case CancelContentSchedule cancelContentSchedule: - return UpdateReturnAsync(cancelContentSchedule, async (c, ct) => - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + CancelChangeCore(c, operation); - CancelChangeCore(c, operation); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case ChangeContentStatus changeContentStatus: + return UpdateReturnAsync(changeContentStatus, async (c, ct) => + { + try + { + if (c.DueTime > SystemClock.Instance.GetCurrentInstant()) + { + ChangeStatusScheduled(c, c.DueTime.Value); + } + else + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - case ChangeContentStatus changeContentStatus: - return UpdateReturnAsync(changeContentStatus, async (c, ct) => + await ChangeCore(c, operation); + } + } + catch (Exception) { - try + if (Snapshot.ScheduleJob != null && Snapshot.ScheduleJob.Id == c.StatusJobId) { - if (c.DueTime > SystemClock.Instance.GetCurrentInstant()) - { - ChangeStatusScheduled(c, c.DueTime.Value); - } - else - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - - await ChangeCore(c, operation); - } + CancelChangeStatus(c); } - catch (Exception) + else { - if (Snapshot.ScheduleJob != null && Snapshot.ScheduleJob.Id == c.StatusJobId) - { - CancelChangeStatus(c); - } - else - { - throw; - } + throw; } + } - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case DeleteContent { Permanent: true } deleteContent: - return DeletePermanentAsync(deleteContent, async (c, ct) => - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + case DeleteContent { Permanent: true } deleteContent: + return DeletePermanentAsync(deleteContent, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - await DeleteCore(c, operation); - }, ct); + await DeleteCore(c, operation); + }, ct); - case DeleteContent deleteContent: - return UpdateAsync(deleteContent, async (c, ct) => - { - var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); + case DeleteContent deleteContent: + return UpdateAsync(deleteContent, async (c, ct) => + { + var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - await DeleteCore(c, operation); - }, ct); + await DeleteCore(c, operation); + }, ct); - default: - ThrowHelper.NotSupportedException(); - return default!; - } + default: + ThrowHelper.NotSupportedException(); + return default!; } + } + + private async Task CreateCore(CreateContent c, ContentOperation operation) + { + operation.MustNotCreateComponent(); + operation.MustNotCreateSingleton(); + operation.MustNotCreateForUnpublishedSchema(); + operation.MustHaveData(c.Data); - private async Task CreateCore(CreateContent c, ContentOperation operation) + if (!c.DoNotValidate) { - operation.MustNotCreateComponent(); - operation.MustNotCreateSingleton(); - operation.MustNotCreateForUnpublishedSchema(); - operation.MustHaveData(c.Data); + await operation.ValidateInputAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished()); + } - if (!c.DoNotValidate) - { - await operation.ValidateInputAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished()); - } + var status = await operation.GetInitialStatusAsync(); + + if (!c.DoNotScript) + { + c.Data = await operation.ExecuteCreateScriptAsync(c.Data, status); + } - var status = await operation.GetInitialStatusAsync(); + operation.GenerateDefaultValues(c.Data); - if (!c.DoNotScript) - { - c.Data = await operation.ExecuteCreateScriptAsync(c.Data, status); - } + if (!c.DoNotValidate) + { + await operation.ValidateContentAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished()); + } - operation.GenerateDefaultValues(c.Data); + Create(c, status); + } - if (!c.DoNotValidate) - { - await operation.ValidateContentAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished()); - } + private async Task ChangeCore(ChangeContentStatus c, ContentOperation operation) + { + operation.MustHavePermission(PermissionIds.AppContentsChangeStatus); + operation.MustNotChangeSingleton(c.Status); - Create(c, status); + if (c.Status == Snapshot.EditingStatus()) + { + return; } - private async Task ChangeCore(ChangeContentStatus c, ContentOperation operation) + if (c.DoNotValidateWorkflow) { - operation.MustHavePermission(PermissionIds.AppContentsChangeStatus); - operation.MustNotChangeSingleton(c.Status); - - if (c.Status == Snapshot.EditingStatus()) - { - return; - } + await operation.CheckStatusAsync(c.Status); + } + else + { + await operation.CheckTransitionAsync(c.Status); + } - if (c.DoNotValidateWorkflow) - { - await operation.CheckStatusAsync(c.Status); - } - else - { - await operation.CheckTransitionAsync(c.Status); - } + if (!c.DoNotScript) + { + var newData = await operation.ExecuteChangeScriptAsync(c.Status, GetChange(c.Status)); - if (!c.DoNotScript) + if (!newData.Equals(Snapshot.Data)) { - var newData = await operation.ExecuteChangeScriptAsync(c.Status, GetChange(c.Status)); + var previousEvent = + GetUncomittedEvents().Select(x => x.Payload) + .OfType<ContentDataCommand>().FirstOrDefault(); - if (!newData.Equals(Snapshot.Data)) + if (previousEvent != null) { - var previousEvent = - GetUncomittedEvents().Select(x => x.Payload) - .OfType<ContentDataCommand>().FirstOrDefault(); - - if (previousEvent != null) - { - previousEvent.Data = newData; - } - else if (!newData.Equals(Snapshot.Data)) - { - Update(c, newData); - } + previousEvent.Data = newData; + } + else if (!newData.Equals(Snapshot.Data)) + { + Update(c, newData); } } - - if (c.CheckReferrers && Snapshot.IsPublished()) - { - await operation.CheckReferrersAsync(); - } - - if (!c.DoNotValidate && await operation.ShouldValidateAsync(c.Status)) - { - await operation.ValidateContentAndInputAsync(Snapshot.Data, c.OptimizeValidation, true); - } - - ChangeStatus(c); } - private async Task UpdateCore(UpdateContent c, ContentOperation operation) + if (c.CheckReferrers && Snapshot.IsPublished()) { - operation.MustHavePermission(PermissionIds.AppContentsUpdate); - operation.MustHaveData(c.Data); + await operation.CheckReferrersAsync(); + } - if (!c.DoNotValidate) - { - await operation.ValidateInputAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished()); - } + if (!c.DoNotValidate && await operation.ShouldValidateAsync(c.Status)) + { + await operation.ValidateContentAndInputAsync(Snapshot.Data, c.OptimizeValidation, true); + } - if (!c.DoNotValidateWorkflow) - { - await operation.CheckUpdateAsync(); - } + ChangeStatus(c); + } - var newData = c.Data; + private async Task UpdateCore(UpdateContent c, ContentOperation operation) + { + operation.MustHavePermission(PermissionIds.AppContentsUpdate); + operation.MustHaveData(c.Data); - if (newData.Equals(Snapshot.Data)) - { - return; - } + if (!c.DoNotValidate) + { + await operation.ValidateInputAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished()); + } - if (!c.DoNotScript) - { - newData = await operation.ExecuteUpdateScriptAsync(newData); - } + if (!c.DoNotValidateWorkflow) + { + await operation.CheckUpdateAsync(); + } - if (!c.DoNotValidate) - { - await operation.ValidateContentAsync(newData, c.OptimizeValidation, Snapshot.IsPublished()); - } + var newData = c.Data; - Update(c, newData); + if (newData.Equals(Snapshot.Data)) + { + return; } - private async Task PatchCore(UpdateContent c, ContentOperation operation) + if (!c.DoNotScript) { - operation.MustHavePermission(PermissionIds.AppContentsUpdate); - operation.MustHaveData(c.Data); + newData = await operation.ExecuteUpdateScriptAsync(newData); + } - if (!c.DoNotValidate) - { - await operation.ValidateInputPartialAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished()); - } + if (!c.DoNotValidate) + { + await operation.ValidateContentAsync(newData, c.OptimizeValidation, Snapshot.IsPublished()); + } - if (!c.DoNotValidateWorkflow) - { - await operation.CheckUpdateAsync(); - } + Update(c, newData); + } - var newData = c.Data.MergeInto(Snapshot.Data); + private async Task PatchCore(UpdateContent c, ContentOperation operation) + { + operation.MustHavePermission(PermissionIds.AppContentsUpdate); + operation.MustHaveData(c.Data); - if (newData.Equals(Snapshot.Data)) - { - return; - } + if (!c.DoNotValidate) + { + await operation.ValidateInputPartialAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished()); + } - if (!c.DoNotScript) - { - newData = await operation.ExecuteUpdateScriptAsync(newData); - } + if (!c.DoNotValidateWorkflow) + { + await operation.CheckUpdateAsync(); + } - if (!c.DoNotValidate) - { - await operation.ValidateContentAsync(newData, c.OptimizeValidation, Snapshot.IsPublished()); - } + var newData = c.Data.MergeInto(Snapshot.Data); - Update(c, newData); + if (newData.Equals(Snapshot.Data)) + { + return; } - private void CancelChangeCore(CancelContentSchedule c, ContentOperation operation) + if (!c.DoNotScript) { - operation.MustHavePermission(PermissionIds.AppContentsChangeStatusCancel); - - if (Snapshot.ScheduleJob != null) - { - CancelChangeStatus(c); - } + newData = await operation.ExecuteUpdateScriptAsync(newData); } - private async Task ValidateCore(ContentOperation operation) + if (!c.DoNotValidate) { - operation.MustHavePermission(PermissionIds.AppContentsRead); - - await operation.ValidateContentAndInputAsync(Snapshot.Data, false, Snapshot.IsPublished()); + await operation.ValidateContentAsync(newData, c.OptimizeValidation, Snapshot.IsPublished()); } - private async Task CreateDraftCore(CreateContentDraft c, ContentOperation operation) - { - operation.MustHavePermission(PermissionIds.AppContentsVersionCreate); - operation.MustCreateDraft(); + Update(c, newData); + } - var status = await operation.GetInitialStatusAsync(); + private void CancelChangeCore(CancelContentSchedule c, ContentOperation operation) + { + operation.MustHavePermission(PermissionIds.AppContentsChangeStatusCancel); - CreateDraft(c, status); + if (Snapshot.ScheduleJob != null) + { + CancelChangeStatus(c); } + } - private void DeleteDraftCore(DeleteContentDraft c, ContentOperation operation) - { - operation.MustHavePermission(PermissionIds.AppContentsVersionDelete); - operation.MustDeleteDraft(); + private async Task ValidateCore(ContentOperation operation) + { + operation.MustHavePermission(PermissionIds.AppContentsRead); - DeleteDraft(c); - } + await operation.ValidateContentAndInputAsync(Snapshot.Data, false, Snapshot.IsPublished()); + } - private async Task DeleteCore(DeleteContent c, ContentOperation operation) - { - operation.MustHavePermission(PermissionIds.AppContentsDelete); - operation.MustNotDeleteSingleton(); + private async Task CreateDraftCore(CreateContentDraft c, ContentOperation operation) + { + operation.MustHavePermission(PermissionIds.AppContentsVersionCreate); + operation.MustCreateDraft(); - if (!c.DoNotScript) - { - await operation.ExecuteDeleteScriptAsync(c.Permanent); - } + var status = await operation.GetInitialStatusAsync(); - if (c.CheckReferrers) - { - await operation.CheckReferrersAsync(); - } + CreateDraft(c, status); + } - Delete(c); - } + private void DeleteDraftCore(DeleteContentDraft c, ContentOperation operation) + { + operation.MustHavePermission(PermissionIds.AppContentsVersionDelete); + operation.MustDeleteDraft(); - private void Create(CreateContent command, Status status) - { - Raise(command, new ContentCreated { Status = status }); - } + DeleteDraft(c); + } - private void Update(ContentCommand command, ContentData data) - { - Raise(command, new ContentUpdated { Data = data }); - } + private async Task DeleteCore(DeleteContent c, ContentOperation operation) + { + operation.MustHavePermission(PermissionIds.AppContentsDelete); + operation.MustNotDeleteSingleton(); - private void ChangeStatus(ChangeContentStatus command) + if (!c.DoNotScript) { - Raise(command, new ContentStatusChanged { Change = GetChange(command.Status) }); + await operation.ExecuteDeleteScriptAsync(c.Permanent); } - private void ChangeStatusScheduled(ChangeContentStatus command, Instant dueTime) + if (c.CheckReferrers) { - Raise(command, new ContentStatusScheduled { DueTime = dueTime }); + await operation.CheckReferrersAsync(); } - private void CancelChangeStatus(ContentCommand command) - { - Raise(command, new ContentSchedulingCancelled()); - } + Delete(c); + } - private void CreateDraft(CreateContentDraft command, Status status) - { - Raise(command, new ContentDraftCreated { Status = status }); - } + private void Create(CreateContent command, Status status) + { + Raise(command, new ContentCreated { Status = status }); + } - private void Delete(DeleteContent command) - { - Raise(command, new ContentDeleted()); - } + private void Update(ContentCommand command, ContentData data) + { + Raise(command, new ContentUpdated { Data = data }); + } + + private void ChangeStatus(ChangeContentStatus command) + { + Raise(command, new ContentStatusChanged { Change = GetChange(command.Status) }); + } + + private void ChangeStatusScheduled(ChangeContentStatus command, Instant dueTime) + { + Raise(command, new ContentStatusScheduled { DueTime = dueTime }); + } + + private void CancelChangeStatus(ContentCommand command) + { + Raise(command, new ContentSchedulingCancelled()); + } - private void DeleteDraft(DeleteContentDraft command) + private void CreateDraft(CreateContentDraft command, Status status) + { + Raise(command, new ContentDraftCreated { Status = status }); + } + + private void Delete(DeleteContent command) + { + Raise(command, new ContentDeleted()); + } + + private void DeleteDraft(DeleteContentDraft command) + { + Raise(command, new ContentDraftDeleted()); + } + + private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent + { + RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); + } + + private StatusChange GetChange(Status status) + { + if (status == Status.Published) { - Raise(command, new ContentDraftDeleted()); + return StatusChange.Published; } - - private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent + else if (Snapshot.IsPublished()) { - RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); + return StatusChange.Unpublished; } - - private StatusChange GetChange(Status status) + else { - if (status == Status.Published) - { - return StatusChange.Published; - } - else if (Snapshot.IsPublished()) - { - return StatusChange.Unpublished; - } - else - { - return StatusChange.Change; - } + return StatusChange.Change; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperation.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperation.cs index c77b9fbd7e..6bf9b587a0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperation.cs @@ -11,50 +11,49 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public sealed class ContentOperation : OperationContextBase<ContentCommand, IContentEntity> { - public sealed class ContentOperation : OperationContextBase<ContentCommand, IContentEntity> + public ISchemaEntity Schema { get; init; } + + public ResolvedComponents Components { get; init; } + + public Schema SchemaDef { - public ISchemaEntity Schema { get; init; } + get => Schema.SchemaDef; + } - public ResolvedComponents Components { get; init; } + public ContentOperation(IServiceProvider serviceProvider, Func<IContentEntity> snapshot) + : base(serviceProvider, snapshot) + { + } - public Schema SchemaDef + public static async Task<ContentOperation> CreateAsync(IServiceProvider services, ContentCommand command, Func<IContentEntity> snapshot) + { + var appProvider = services.GetRequiredService<IAppProvider>(); + + var (app, schema) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id); + + if (app == null) { - get => Schema.SchemaDef; + throw new DomainObjectNotFoundException(command.AppId.Id.ToString()); } - public ContentOperation(IServiceProvider serviceProvider, Func<IContentEntity> snapshot) - : base(serviceProvider, snapshot) + if (schema == null) { + throw new DomainObjectNotFoundException(command.SchemaId.Id.ToString()); } - public static async Task<ContentOperation> CreateAsync(IServiceProvider services, ContentCommand command, Func<IContentEntity> snapshot) + var components = await appProvider.GetComponentsAsync(schema); + + return new ContentOperation(services, snapshot) { - var appProvider = services.GetRequiredService<IAppProvider>(); - - var (app, schema) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id); - - if (app == null) - { - throw new DomainObjectNotFoundException(command.AppId.Id.ToString()); - } - - if (schema == null) - { - throw new DomainObjectNotFoundException(command.SchemaId.Id.ToString()); - } - - var components = await appProvider.GetComponentsAsync(schema); - - return new ContentOperation(services, snapshot) - { - App = app, - Command = command, - CommandId = command.ContentId, - Components = components, - Schema = schema - }; - } + App = app, + Command = command, + CommandId = command.ContentId, + Components = components, + Schema = schema + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentVersion.cs index e0f6b35692..4e28b6d538 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentVersion.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentVersion.cs @@ -8,31 +8,30 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public sealed class ContentVersion { - public sealed class ContentVersion - { - public Status Status { get; } + public Status Status { get; } - public ContentData Data { get; } + public ContentData Data { get; } - public ContentVersion(Status status, ContentData data) - { - Guard.NotNull(data); + public ContentVersion(Status status, ContentData data) + { + Guard.NotNull(data); - Status = status; + Status = status; - Data = data; - } + Data = data; + } - public ContentVersion WithStatus(Status status) - { - return new ContentVersion(status, Data); - } + public ContentVersion WithStatus(Status status) + { + return new ContentVersion(status, Data); + } - public ContentVersion WithData(ContentData data) - { - return new ContentVersion(Status, data); - } + public ContentVersion WithData(ContentData data) + { + return new ContentVersion(Status, data); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs index 126e09e1f7..a3eee076e7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs @@ -21,318 +21,317 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable RECS0082 // Parameter has the same name as a member and hides it -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public sealed class ContentsBulkUpdateCommandMiddleware : ICommandMiddleware { - public sealed class ContentsBulkUpdateCommandMiddleware : ICommandMiddleware + private readonly IContentQueryService contentQuery; + private readonly IContextProvider contextProvider; + private readonly ILogger<ContentsBulkUpdateCommandMiddleware> log; + + private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command, + CancellationToken CancellationToken); + + private sealed record BulkTask( + ICommandBus Bus, + NamedId<DomainId> SchemaId, + int JobIndex, + BulkUpdateJob CommandJob, + BulkUpdateContents Command, + ConcurrentBag<BulkUpdateResultItem> Results, + CancellationToken Aborted); + + public ContentsBulkUpdateCommandMiddleware( + IContentQueryService contentQuery, + IContextProvider contextProvider, + ILogger<ContentsBulkUpdateCommandMiddleware> log) { - private readonly IContentQueryService contentQuery; - private readonly IContextProvider contextProvider; - private readonly ILogger<ContentsBulkUpdateCommandMiddleware> log; - - private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command, - CancellationToken CancellationToken); - - private sealed record BulkTask( - ICommandBus Bus, - NamedId<DomainId> SchemaId, - int JobIndex, - BulkUpdateJob CommandJob, - BulkUpdateContents Command, - ConcurrentBag<BulkUpdateResultItem> Results, - CancellationToken Aborted); - - public ContentsBulkUpdateCommandMiddleware( - IContentQueryService contentQuery, - IContextProvider contextProvider, - ILogger<ContentsBulkUpdateCommandMiddleware> log) - { - this.contentQuery = contentQuery; - this.contextProvider = contextProvider; + this.contentQuery = contentQuery; + this.contextProvider = contextProvider; - this.log = log; - } + this.log = log; + } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is BulkUpdateContents bulkUpdates) { - if (context.Command is BulkUpdateContents bulkUpdates) + if (bulkUpdates.Jobs?.Length > 0) { - if (bulkUpdates.Jobs?.Length > 0) + var executionOptions = new ExecutionDataflowBlockOptions { - var executionOptions = new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) - }; + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) + }; - // Each job can create one or more commands. - var createCommandsBlock = new TransformManyBlock<BulkTask, BulkTaskCommand>(async task => + // Each job can create one or more commands. + var createCommandsBlock = new TransformManyBlock<BulkTask, BulkTaskCommand>(async task => + { + try { - try - { - return await CreateCommandsAsync(task); - } - catch (OperationCanceledException ex) - { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); - } - }, executionOptions); - - // Execute the commands in batches. - var executeCommandBlock = new ActionBlock<BulkTaskCommand>(async command => + return await CreateCommandsAsync(task); + } + catch (OperationCanceledException ex) { - try - { - await ExecuteCommandAsync(command); - } - catch (OperationCanceledException ex) - { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); - } - }, executionOptions); - - createCommandsBlock.BidirectionalLinkTo(executeCommandBlock); - - contextProvider.Context.Change(b => b - .WithoutContentEnrichment() - .WithoutCleanup() - .WithUnpublished(true) - .WithoutTotal()); - - var results = new ConcurrentBag<BulkUpdateResultItem>(); - - for (var i = 0; i < bulkUpdates.Jobs.Length; i++) + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); + } + }, executionOptions); + + // Execute the commands in batches. + var executeCommandBlock = new ActionBlock<BulkTaskCommand>(async command => + { + try + { + await ExecuteCommandAsync(command); + } + catch (OperationCanceledException ex) { - var task = new BulkTask( - context.CommandBus, - bulkUpdates.SchemaId, - i, - bulkUpdates.Jobs[i], - bulkUpdates, - results, - ct); - - if (!await createCommandsBlock.SendAsync(task, ct)) - { - break; - } + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); } + }, executionOptions); - createCommandsBlock.Complete(); + createCommandsBlock.BidirectionalLinkTo(executeCommandBlock); - // Wait for all commands to be executed. - await executeCommandBlock.Completion; + contextProvider.Context.Change(b => b + .WithoutContentEnrichment() + .WithoutCleanup() + .WithUnpublished(true) + .WithoutTotal()); - context.Complete(new BulkUpdateResult(results)); - } - else + var results = new ConcurrentBag<BulkUpdateResultItem>(); + + for (var i = 0; i < bulkUpdates.Jobs.Length; i++) { - context.Complete(new BulkUpdateResult()); + var task = new BulkTask( + context.CommandBus, + bulkUpdates.SchemaId, + i, + bulkUpdates.Jobs[i], + bulkUpdates, + results, + ct); + + if (!await createCommandsBlock.SendAsync(task, ct)) + { + break; + } } + + createCommandsBlock.Complete(); + + // Wait for all commands to be executed. + await executeCommandBlock.Completion; + + context.Complete(new BulkUpdateResult(results)); } else { - await next(context, ct); + context.Complete(new BulkUpdateResult()); } } - - private async Task ExecuteCommandAsync(BulkTaskCommand bulkCommand) + else { - var (task, id, command, ct) = bulkCommand; + await next(context, ct); + } + } - try - { - await task.Bus.PublishAsync(command, ct); + private async Task ExecuteCommandAsync(BulkTaskCommand bulkCommand) + { + var (task, id, command, ct) = bulkCommand; - task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex)); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to execute content bulk job with index {index} of type {type}.", - task.JobIndex, - task.CommandJob.Type); + try + { + await task.Bus.PublishAsync(command, ct); - task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex, ex)); - } + task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex)); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to execute content bulk job with index {index} of type {type}.", + task.JobIndex, + task.CommandJob.Type); + + task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex, ex)); } + } - private async Task<IEnumerable<BulkTaskCommand>> CreateCommandsAsync(BulkTask task) + private async Task<IEnumerable<BulkTaskCommand>> CreateCommandsAsync(BulkTask task) + { + // The task parallel pipeline does not allow async-enumerable. + var commands = new List<BulkTaskCommand>(); + try { - // The task parallel pipeline does not allow async-enumerable. - var commands = new List<BulkTaskCommand>(); - try + // Check whether another schema is defined for the current job and override the schema id if necessary. + var overridenSchema = task.CommandJob.Schema; + + if (!string.IsNullOrWhiteSpace(overridenSchema)) { - // Check whether another schema is defined for the current job and override the schema id if necessary. - var overridenSchema = task.CommandJob.Schema; + var schema = await contentQuery.GetSchemaOrThrowAsync(contextProvider.Context, overridenSchema, task.Aborted); - if (!string.IsNullOrWhiteSpace(overridenSchema)) - { - var schema = await contentQuery.GetSchemaOrThrowAsync(contextProvider.Context, overridenSchema, task.Aborted); + // Task is immutable, so we have to create a copy. + task = task with { SchemaId = schema.NamedId() }; + } - // Task is immutable, so we have to create a copy. - task = task with { SchemaId = schema.NamedId() }; - } + // The bulk command can be invoke in a schema controller or without a schema controller, therefore the name might be null. + if (task.SchemaId == null || task.SchemaId.Id == default) + { + throw new DomainObjectNotFoundException("undefined"); + } - // The bulk command can be invoke in a schema controller or without a schema controller, therefore the name might be null. - if (task.SchemaId == null || task.SchemaId.Id == default) - { - throw new DomainObjectNotFoundException("undefined"); - } + var resolvedIds = await FindIdAsync(task, task.SchemaId.Name); - var resolvedIds = await FindIdAsync(task, task.SchemaId.Name); + if (resolvedIds.Length == 0) + { + throw new DomainObjectNotFoundException("undefined"); + } - if (resolvedIds.Length == 0) + foreach (var id in resolvedIds) + { + try { - throw new DomainObjectNotFoundException("undefined"); - } + var command = CreateCommand(task); - foreach (var id in resolvedIds) + command.ContentId = id; + commands.Add(new BulkTaskCommand(task, id, command, task.Aborted)); + } + catch (Exception ex) { - try - { - var command = CreateCommand(task); + log.LogError(ex, "Failed to create content bulk job with index {index} of type {type}.", + task.JobIndex, + task.CommandJob.Type); - command.ContentId = id; - commands.Add(new BulkTaskCommand(task, id, command, task.Aborted)); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to create content bulk job with index {index} of type {type}.", - task.JobIndex, - task.CommandJob.Type); - - task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex, ex)); - } + task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex, ex)); } } - catch (Exception ex) - { - task.Results.Add(new BulkUpdateResultItem(task.CommandJob.Id, task.JobIndex, ex)); - } - - return commands; } - - private ContentCommand CreateCommand(BulkTask task) + catch (Exception ex) { - var job = task.CommandJob; - - switch (job.Type) - { - case BulkUpdateContentType.Create: - { - var command = new CreateContent(); + task.Results.Add(new BulkUpdateResultItem(task.CommandJob.Id, task.JobIndex, ex)); + } - EnrichAndCheckPermission(task, command, PermissionIds.AppContentsCreate); - return command; - } + return commands; + } - case BulkUpdateContentType.Update: - { - var command = new UpdateContent(); + private ContentCommand CreateCommand(BulkTask task) + { + var job = task.CommandJob; - EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpdateOwn); - return command; - } + switch (job.Type) + { + case BulkUpdateContentType.Create: + { + var command = new CreateContent(); - case BulkUpdateContentType.Upsert: - { - var command = new UpsertContent(); + EnrichAndCheckPermission(task, command, PermissionIds.AppContentsCreate); + return command; + } - EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpsert); - return command; - } + case BulkUpdateContentType.Update: + { + var command = new UpdateContent(); - case BulkUpdateContentType.Patch: - { - var command = new PatchContent(); + EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpdateOwn); + return command; + } - EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpdateOwn); - return command; - } + case BulkUpdateContentType.Upsert: + { + var command = new UpsertContent(); - case BulkUpdateContentType.Validate: - { - var command = new ValidateContent(); + EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpsert); + return command; + } - EnrichAndCheckPermission(task, command, PermissionIds.AppContentsReadOwn); - return command; - } + case BulkUpdateContentType.Patch: + { + var command = new PatchContent(); - case BulkUpdateContentType.ChangeStatus: - { - var command = new ChangeContentStatus { Status = job.Status ?? Status.Draft }; + EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpdateOwn); + return command; + } - EnrichAndCheckPermission(task, command, PermissionIds.AppContentsChangeStatusOwn); - return command; - } + case BulkUpdateContentType.Validate: + { + var command = new ValidateContent(); - case BulkUpdateContentType.Delete: - { - var command = new DeleteContent(); + EnrichAndCheckPermission(task, command, PermissionIds.AppContentsReadOwn); + return command; + } - EnrichAndCheckPermission(task, command, PermissionIds.AppContentsDeleteOwn); - return command; - } + case BulkUpdateContentType.ChangeStatus: + { + var command = new ChangeContentStatus { Status = job.Status ?? Status.Draft }; - default: - ThrowHelper.NotSupportedException(); - return default!; - } - } + EnrichAndCheckPermission(task, command, PermissionIds.AppContentsChangeStatusOwn); + return command; + } - private void EnrichAndCheckPermission<T>(BulkTask task, T command, string permissionId) where T : ContentCommand - { - SimpleMapper.Map(task.Command, command); - SimpleMapper.Map(task.CommandJob, command); + case BulkUpdateContentType.Delete: + { + var command = new DeleteContent(); - if (!contextProvider.Context.Allows(permissionId, command.SchemaId.Name)) - { - throw new DomainForbiddenException("Forbidden"); - } + EnrichAndCheckPermission(task, command, PermissionIds.AppContentsDeleteOwn); + return command; + } - command.SchemaId = task.SchemaId; - command.ExpectedVersion = task.Command.ExpectedVersion; + default: + ThrowHelper.NotSupportedException(); + return default!; } + } + + private void EnrichAndCheckPermission<T>(BulkTask task, T command, string permissionId) where T : ContentCommand + { + SimpleMapper.Map(task.Command, command); + SimpleMapper.Map(task.CommandJob, command); - private async Task<DomainId[]> FindIdAsync(BulkTask task, string schema) + if (!contextProvider.Context.Allows(permissionId, command.SchemaId.Name)) { - var id = task.CommandJob.Id; + throw new DomainForbiddenException("Forbidden"); + } - if (id != null) - { - return new[] { id.Value }; - } + command.SchemaId = task.SchemaId; + command.ExpectedVersion = task.Command.ExpectedVersion; + } - if (task.CommandJob.Query != null) - { - task.CommandJob.Query.Take = task.CommandJob.ExpectedCount; + private async Task<DomainId[]> FindIdAsync(BulkTask task, string schema) + { + var id = task.CommandJob.Id; - var existingQuery = Q.Empty.WithJsonQuery(task.CommandJob.Query); - var existingResult = await contentQuery.QueryAsync(contextProvider.Context, schema, existingQuery, task.Aborted); + if (id != null) + { + return new[] { id.Value }; + } - if (existingResult.Total > task.CommandJob.ExpectedCount) - { - throw new DomainException(T.Get("contents.bulkInsertQueryNotUnique")); - } + if (task.CommandJob.Query != null) + { + task.CommandJob.Query.Take = task.CommandJob.ExpectedCount; - // Upsert means that we either update the content if we find it or that we create a new one. - // Therefore we create a new ID if we cannot find the ID for the query. - if (existingResult.Count == 0 && task.CommandJob.Type == BulkUpdateContentType.Upsert) - { - return new[] { DomainId.NewGuid() }; - } + var existingQuery = Q.Empty.WithJsonQuery(task.CommandJob.Query); + var existingResult = await contentQuery.QueryAsync(contextProvider.Context, schema, existingQuery, task.Aborted); - return existingResult.Select(x => x.Id).ToArray(); + if (existingResult.Total > task.CommandJob.ExpectedCount) + { + throw new DomainException(T.Get("contents.bulkInsertQueryNotUnique")); } - if (task.CommandJob.Type is BulkUpdateContentType.Create or BulkUpdateContentType.Upsert) + // Upsert means that we either update the content if we find it or that we create a new one. + // Therefore we create a new ID if we cannot find the ID for the query. + if (existingResult.Count == 0 && task.CommandJob.Type == BulkUpdateContentType.Upsert) { return new[] { DomainId.NewGuid() }; } - return Array.Empty<DomainId>(); + return existingResult.Select(x => x.Id).ToArray(); } + + if (task.CommandJob.Type is BulkUpdateContentType.Create or BulkUpdateContentType.Upsert) + { + return new[] { DomainId.NewGuid() }; + } + + return Array.Empty<DomainId>(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs index e8b835daea..6d0234f0c5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs @@ -8,152 +8,151 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards; + +public static class ScriptingExtensions { - public static class ScriptingExtensions + private static readonly ScriptOptions Options = new ScriptOptions { - private static readonly ScriptOptions Options = new ScriptOptions - { - AsContext = true, - CanDisallow = true, - CanReject = true - }; + AsContext = true, + CanDisallow = true, + CanReject = true + }; - public static Task<ContentData> ExecuteCreateScriptAsync(this ContentOperation operation, ContentData data, Status status) - { - var script = operation.SchemaDef.Scripts.Create; - - if (string.IsNullOrWhiteSpace(script)) - { - return Task.FromResult(data); - } + public static Task<ContentData> ExecuteCreateScriptAsync(this ContentOperation operation, ContentData data, Status status) + { + var script = operation.SchemaDef.Scripts.Create; - // Script vars are just wrappers over dictionaries for better performance. - var vars = Enrich(operation, new ContentScriptVars - { - Data = data, - DataOld = null, - OldData = null, - OldStatus = default, - Operation = "Create", - Status = status, - StatusOld = default - }); - - return TransformAsync(operation, script, vars); + if (string.IsNullOrWhiteSpace(script)) + { + return Task.FromResult(data); } - public static Task<ContentData> ExecuteUpdateScriptAsync(this ContentOperation operation, ContentData data) + // Script vars are just wrappers over dictionaries for better performance. + var vars = Enrich(operation, new ContentScriptVars { - var script = operation.SchemaDef.Scripts.Update; + Data = data, + DataOld = null, + OldData = null, + OldStatus = default, + Operation = "Create", + Status = status, + StatusOld = default + }); + + return TransformAsync(operation, script, vars); + } - if (string.IsNullOrWhiteSpace(script)) - { - return Task.FromResult(data); - } + public static Task<ContentData> ExecuteUpdateScriptAsync(this ContentOperation operation, ContentData data) + { + var script = operation.SchemaDef.Scripts.Update; - // Script vars are just wrappers over dictionaries for better performance. - var vars = Enrich(operation, new ContentScriptVars - { - Data = data, - DataOld = operation.Snapshot.Data, - OldData = operation.Snapshot.Data, - OldStatus = operation.Snapshot.Status, - Operation = "Update", - Status = operation.Snapshot.EditingStatus(), - StatusOld = default - }); - - return TransformAsync(operation, script, vars); + if (string.IsNullOrWhiteSpace(script)) + { + return Task.FromResult(data); } - public static Task<ContentData> ExecuteChangeScriptAsync(this ContentOperation operation, Status status, StatusChange change) + // Script vars are just wrappers over dictionaries for better performance. + var vars = Enrich(operation, new ContentScriptVars { - var script = operation.SchemaDef.Scripts.Change; + Data = data, + DataOld = operation.Snapshot.Data, + OldData = operation.Snapshot.Data, + OldStatus = operation.Snapshot.Status, + Operation = "Update", + Status = operation.Snapshot.EditingStatus(), + StatusOld = default + }); + + return TransformAsync(operation, script, vars); + } - if (string.IsNullOrWhiteSpace(script)) - { - return Task.FromResult(operation.Snapshot.Data); - } + public static Task<ContentData> ExecuteChangeScriptAsync(this ContentOperation operation, Status status, StatusChange change) + { + var script = operation.SchemaDef.Scripts.Change; - // Script vars are just wrappers over dictionaries for better performance. - var vars = Enrich(operation, new ContentScriptVars - { - Data = operation.Snapshot.Data.Clone(), - DataOld = null, - OldData = null, - OldStatus = operation.Snapshot.EditingStatus(), - Operation = change.ToString(), - Status = status, - StatusOld = operation.Snapshot.EditingStatus(), - Validate = Validate(operation, status) - }); - - return TransformAsync(operation, script, vars); + if (string.IsNullOrWhiteSpace(script)) + { + return Task.FromResult(operation.Snapshot.Data); } - public static Task ExecuteDeleteScriptAsync(this ContentOperation operation, bool permanent) + // Script vars are just wrappers over dictionaries for better performance. + var vars = Enrich(operation, new ContentScriptVars { - var script = operation.SchemaDef.Scripts.Delete; - - if (string.IsNullOrWhiteSpace(script)) - { - return Task.CompletedTask; - } + Data = operation.Snapshot.Data.Clone(), + DataOld = null, + OldData = null, + OldStatus = operation.Snapshot.EditingStatus(), + Operation = change.ToString(), + Status = status, + StatusOld = operation.Snapshot.EditingStatus(), + Validate = Validate(operation, status) + }); + + return TransformAsync(operation, script, vars); + } - // Script vars are just wrappers over dictionaries for better performance. - var vars = Enrich(operation, new ContentScriptVars - { - Data = operation.Snapshot.Data, - DataOld = null, - OldData = null, - OldStatus = operation.Snapshot.EditingStatus(), - Operation = "Delete", - Permanent = permanent, - Status = operation.Snapshot.EditingStatus(), - StatusOld = default - }); - - return ExecuteAsync(operation, script, vars); - } + public static Task ExecuteDeleteScriptAsync(this ContentOperation operation, bool permanent) + { + var script = operation.SchemaDef.Scripts.Delete; - private static async Task<ContentData> TransformAsync(ContentOperation operation, string script, ContentScriptVars vars) + if (string.IsNullOrWhiteSpace(script)) { - return await operation.Resolve<IScriptEngine>().TransformAsync(vars, script, Options); + return Task.CompletedTask; } - private static async Task ExecuteAsync(ContentOperation operation, string script, ContentScriptVars vars) + // Script vars are just wrappers over dictionaries for better performance. + var vars = Enrich(operation, new ContentScriptVars { - await operation.Resolve<IScriptEngine>().ExecuteAsync(vars, script, Options); - } + Data = operation.Snapshot.Data, + DataOld = null, + OldData = null, + OldStatus = operation.Snapshot.EditingStatus(), + Operation = "Delete", + Permanent = permanent, + Status = operation.Snapshot.EditingStatus(), + StatusOld = default + }); + + return ExecuteAsync(operation, script, vars); + } + + private static async Task<ContentData> TransformAsync(ContentOperation operation, string script, ContentScriptVars vars) + { + return await operation.Resolve<IScriptEngine>().TransformAsync(vars, script, Options); + } + + private static async Task ExecuteAsync(ContentOperation operation, string script, ContentScriptVars vars) + { + await operation.Resolve<IScriptEngine>().ExecuteAsync(vars, script, Options); + } - private static Action Validate(ContentOperation operation, Status status) + private static Action Validate(ContentOperation operation, Status status) + { + return () => { - return () => + try { - try - { - var snapshot = operation.Snapshot; - - operation.ValidateContentAndInputAsync(snapshot.Data, false, snapshot.IsPublished() || status == Status.Published).Wait(); - } - catch (AggregateException ex) when (ex.InnerException != null) - { - throw ex.Flatten().InnerException!; - } - }; - } + var snapshot = operation.Snapshot; - private static ContentScriptVars Enrich(ContentOperation operation, ContentScriptVars vars) - { - vars.AppId = operation.App.Id; - vars.AppName = operation.App.Name; - vars.ContentId = operation.CommandId; - vars.SchemaId = operation.Schema.Id; - vars.SchemaName = operation.Schema.SchemaDef.Name; - vars.User = operation.User; - - return vars; - } + operation.ValidateContentAndInputAsync(snapshot.Data, false, snapshot.IsPublished() || status == Status.Published).Wait(); + } + catch (AggregateException ex) when (ex.InnerException != null) + { + throw ex.Flatten().InnerException!; + } + }; + } + + private static ContentScriptVars Enrich(ContentOperation operation, ContentScriptVars vars) + { + vars.AppId = operation.App.Id; + vars.AppName = operation.App.Name; + vars.ContentId = operation.CommandId; + vars.SchemaId = operation.Schema.Id; + vars.SchemaName = operation.Schema.SchemaDef.Name; + vars.User = operation.User; + + return vars; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs index 5f1e06d277..8df0edf563 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs @@ -10,32 +10,31 @@ using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards; + +public static class SecurityExtensions { - public static class SecurityExtensions + public static void MustHavePermission(this ContentOperation context, string permissionId) { - public static void MustHavePermission(this ContentOperation context, string permissionId) - { - var content = context.Snapshot; + var content = context.Snapshot; - if (Equals(content.CreatedBy, context.Actor) || context.User == null) - { - return; - } + if (Equals(content.CreatedBy, context.Actor) || context.User == null) + { + return; + } - var permissions = context.User?.Claims.Permissions(); + var permissions = context.User?.Claims.Permissions(); - if (permissions == null) - { - throw new DomainForbiddenException(T.Get("common.errorNoPermission")); - } + if (permissions == null) + { + throw new DomainForbiddenException(T.Get("common.errorNoPermission")); + } - var permission = PermissionIds.ForApp(permissionId, context.App.Name, context.Schema.SchemaDef.Name); + var permission = PermissionIds.ForApp(permissionId, context.App.Name, context.Schema.SchemaDef.Name); - if (!permissions.Allows(permission)) - { - throw new DomainForbiddenException(T.Get("common.errorNoPermission")); - } + if (!permissions.Allows(permission)) + { + throw new DomainForbiddenException(T.Get("common.errorNoPermission")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs index bb4354e288..e5ca5cd09b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs @@ -10,48 +10,47 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards; + +public static class SingletonExtensions { - public static class SingletonExtensions + public static void MustNotCreateForUnpublishedSchema(this ContentOperation operation) { - public static void MustNotCreateForUnpublishedSchema(this ContentOperation operation) + if (!operation.SchemaDef.IsPublished && operation.SchemaDef.Type != SchemaType.Singleton) { - if (!operation.SchemaDef.IsPublished && operation.SchemaDef.Type != SchemaType.Singleton) - { - throw new DomainException(T.Get("contents.schemaNotPublished")); - } + throw new DomainException(T.Get("contents.schemaNotPublished")); } + } - public static void MustNotCreateComponent(this ContentOperation operation) + public static void MustNotCreateComponent(this ContentOperation operation) + { + if (operation.SchemaDef.Type == SchemaType.Component) { - if (operation.SchemaDef.Type == SchemaType.Component) - { - throw new DomainException(T.Get("contents.componentNotCreatable")); - } + throw new DomainException(T.Get("contents.componentNotCreatable")); } + } - public static void MustNotCreateSingleton(this ContentOperation operation) + public static void MustNotCreateSingleton(this ContentOperation operation) + { + if (operation.SchemaDef.Type == SchemaType.Singleton && operation.CommandId != operation.Schema.Id) { - if (operation.SchemaDef.Type == SchemaType.Singleton && operation.CommandId != operation.Schema.Id) - { - throw new DomainException(T.Get("contents.singletonNotCreatable")); - } + throw new DomainException(T.Get("contents.singletonNotCreatable")); } + } - public static void MustNotChangeSingleton(this ContentOperation operation, Status status) + public static void MustNotChangeSingleton(this ContentOperation operation, Status status) + { + if (operation.SchemaDef.Type == SchemaType.Singleton && (operation.Snapshot.NewStatus == null || status != Status.Published)) { - if (operation.SchemaDef.Type == SchemaType.Singleton && (operation.Snapshot.NewStatus == null || status != Status.Published)) - { - throw new DomainException(T.Get("contents.singletonNotChangeable")); - } + throw new DomainException(T.Get("contents.singletonNotChangeable")); } + } - public static void MustNotDeleteSingleton(this ContentOperation operation) + public static void MustNotDeleteSingleton(this ContentOperation operation) + { + if (operation.SchemaDef.Type == SchemaType.Singleton) { - if (operation.SchemaDef.Type == SchemaType.Singleton) - { - throw new DomainException(T.Get("contents.singletonNotDeletable")); - } + throw new DomainException(T.Get("contents.singletonNotDeletable")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs index 10f793f026..b0e95bbd8f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs @@ -17,112 +17,111 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards; + +public static class ValidationExtensions { - public static class ValidationExtensions + public static void MustDeleteDraft(this ContentOperation operation) { - public static void MustDeleteDraft(this ContentOperation operation) + if (operation.Snapshot.NewStatus == null) { - if (operation.Snapshot.NewStatus == null) - { - throw new DomainException(T.Get("contents.draftToDeleteNotFound")); - } + throw new DomainException(T.Get("contents.draftToDeleteNotFound")); } + } - public static void MustCreateDraft(this ContentOperation operation) + public static void MustCreateDraft(this ContentOperation operation) + { + if (operation.Snapshot.EditingStatus() != Status.Published) { - if (operation.Snapshot.EditingStatus() != Status.Published) - { - throw new DomainException(T.Get("contents.draftNotCreateForUnpublished")); - } + throw new DomainException(T.Get("contents.draftNotCreateForUnpublished")); } + } - public static void MustHaveData(this ContentOperation operation, ContentData? data) + public static void MustHaveData(this ContentOperation operation, ContentData? data) + { + if (data == null) { - if (data == null) - { - operation.AddError(Not.Defined(nameof(data)), nameof(data)); - } - - operation.ThrowOnErrors(); + operation.AddError(Not.Defined(nameof(data)), nameof(data)); } - public static async Task ValidateInputAsync(this ContentOperation operation, ContentData data, bool optimize, bool published) - { - var validator = GetValidator(operation, optimize, published); + operation.ThrowOnErrors(); + } - await validator.ValidateInputAsync(data); + public static async Task ValidateInputAsync(this ContentOperation operation, ContentData data, bool optimize, bool published) + { + var validator = GetValidator(operation, optimize, published); - operation.AddErrors(validator.Errors).ThrowOnErrors(); - } + await validator.ValidateInputAsync(data); - public static async Task ValidateInputPartialAsync(this ContentOperation operation, ContentData data, bool optimize, bool published) - { - var validator = GetValidator(operation, optimize, published); + operation.AddErrors(validator.Errors).ThrowOnErrors(); + } - await validator.ValidateInputPartialAsync(data); + public static async Task ValidateInputPartialAsync(this ContentOperation operation, ContentData data, bool optimize, bool published) + { + var validator = GetValidator(operation, optimize, published); - operation.AddErrors(validator.Errors).ThrowOnErrors(); - } + await validator.ValidateInputPartialAsync(data); - public static async Task ValidateContentAsync(this ContentOperation operation, ContentData data, bool optimize, bool published) - { - var validator = GetValidator(operation, optimize, published); + operation.AddErrors(validator.Errors).ThrowOnErrors(); + } - await validator.ValidateContentAsync(data); + public static async Task ValidateContentAsync(this ContentOperation operation, ContentData data, bool optimize, bool published) + { + var validator = GetValidator(operation, optimize, published); - operation.AddErrors(validator.Errors).ThrowOnErrors(); - } + await validator.ValidateContentAsync(data); - public static async Task ValidateContentAndInputAsync(this ContentOperation operation, ContentData data, bool optimize, bool published) - { - var validator = GetValidator(operation, optimize, published); + operation.AddErrors(validator.Errors).ThrowOnErrors(); + } - await validator.ValidateInputAndContentAsync(data); + public static async Task ValidateContentAndInputAsync(this ContentOperation operation, ContentData data, bool optimize, bool published) + { + var validator = GetValidator(operation, optimize, published); - operation.AddErrors(validator.Errors).ThrowOnErrors(); - } + await validator.ValidateInputAndContentAsync(data); - public static void GenerateDefaultValues(this ContentOperation operation, ContentData data) - { - data.GenerateDefaultValues(operation.Schema.SchemaDef, operation.Partition()); - } + operation.AddErrors(validator.Errors).ThrowOnErrors(); + } - public static async Task CheckReferrersAsync(this ContentOperation operation) - { - var contentRepository = operation.Resolve<IContentRepository>(); + public static void GenerateDefaultValues(this ContentOperation operation, ContentData data) + { + data.GenerateDefaultValues(operation.Schema.SchemaDef, operation.Partition()); + } - var hasReferrer = await contentRepository.HasReferrersAsync(operation.App.Id, operation.CommandId, SearchScope.All, default); + public static async Task CheckReferrersAsync(this ContentOperation operation) + { + var contentRepository = operation.Resolve<IContentRepository>(); - if (hasReferrer) - { - throw new DomainException(T.Get("contents.referenced"), "OBJECT_REFERENCED"); - } - } + var hasReferrer = await contentRepository.HasReferrersAsync(operation.App.Id, operation.CommandId, SearchScope.All, default); - private static ContentValidator GetValidator(this ContentOperation operation, bool optimize, bool published) + if (hasReferrer) { - var rootContext = - new RootContext(operation.Resolve<IJsonSerializer>(), - operation.App.NamedId(), - operation.Schema.NamedId(), - operation.SchemaDef, - operation.CommandId, - operation.Components); - - var validationContext = new ValidationContext(rootContext).Optimized(optimize).AsPublishing(published); - - var validator = - new ContentValidator(operation.Partition(), - validationContext, - operation.Resolve<IEnumerable<IValidatorsFactory>>()); - - return validator; + throw new DomainException(T.Get("contents.referenced"), "OBJECT_REFERENCED"); } + } - private static PartitionResolver Partition(this ContentOperation operation) - { - return operation.App.PartitionResolver(); - } + private static ContentValidator GetValidator(this ContentOperation operation, bool optimize, bool published) + { + var rootContext = + new RootContext(operation.Resolve<IJsonSerializer>(), + operation.App.NamedId(), + operation.Schema.NamedId(), + operation.SchemaDef, + operation.CommandId, + operation.Components); + + var validationContext = new ValidationContext(rootContext).Optimized(optimize).AsPublishing(published); + + var validator = + new ContentValidator(operation.Partition(), + validationContext, + operation.Resolve<IEnumerable<IValidatorsFactory>>()); + + return validator; + } + + private static PartitionResolver Partition(this ContentOperation operation) + { + return operation.App.PartitionResolver(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs index c253b59c5a..ddb57fdfd5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs @@ -10,76 +10,75 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards; + +public static class WorkflowExtensions { - public static class WorkflowExtensions + public static ValueTask<Status> GetInitialStatusAsync(this ContentOperation operation) { - public static ValueTask<Status> GetInitialStatusAsync(this ContentOperation operation) - { - var workflow = GetWorkflow(operation); + var workflow = GetWorkflow(operation); - return workflow.GetInitialStatusAsync(operation.Schema); - } + return workflow.GetInitialStatusAsync(operation.Schema); + } - public static ValueTask<bool> ShouldValidateAsync(this ContentOperation operation, Status status) - { - var workflow = GetWorkflow(operation); + public static ValueTask<bool> ShouldValidateAsync(this ContentOperation operation, Status status) + { + var workflow = GetWorkflow(operation); - return workflow.ShouldValidateAsync(operation.Schema, status); - } + return workflow.ShouldValidateAsync(operation.Schema, status); + } - public static async Task CheckTransitionAsync(this ContentOperation operation, Status status) + public static async Task CheckTransitionAsync(this ContentOperation operation, Status status) + { + if (operation.SchemaDef.Type != SchemaType.Singleton) { - if (operation.SchemaDef.Type != SchemaType.Singleton) - { - var workflow = GetWorkflow(operation); + var workflow = GetWorkflow(operation); - var oldStatus = operation.Snapshot.EditingStatus(); + var oldStatus = operation.Snapshot.EditingStatus(); - if (!await workflow.CanMoveToAsync(operation.Snapshot, oldStatus, status, operation.User)) - { - var values = new { oldStatus, newStatus = status }; + if (!await workflow.CanMoveToAsync(operation.Snapshot, oldStatus, status, operation.User)) + { + var values = new { oldStatus, newStatus = status }; - operation.AddError(T.Get("contents.statusTransitionNotAllowed", values), nameof(status)); - operation.ThrowOnErrors(); - } + operation.AddError(T.Get("contents.statusTransitionNotAllowed", values), nameof(status)); + operation.ThrowOnErrors(); } } + } - public static async Task CheckStatusAsync(this ContentOperation operation, Status status) + public static async Task CheckStatusAsync(this ContentOperation operation, Status status) + { + if (operation.SchemaDef.Type != SchemaType.Singleton) { - if (operation.SchemaDef.Type != SchemaType.Singleton) - { - var workflow = GetWorkflow(operation); + var workflow = GetWorkflow(operation); - var statusInfo = await workflow.GetInfoAsync(operation.Snapshot, status); + var statusInfo = await workflow.GetInfoAsync(operation.Snapshot, status); - if (statusInfo == null) - { - operation.AddError(T.Get("contents.statusNotValid"), nameof(status)); - operation.ThrowOnErrors(); - } + if (statusInfo == null) + { + operation.AddError(T.Get("contents.statusNotValid"), nameof(status)); + operation.ThrowOnErrors(); } } + } - public static async Task CheckUpdateAsync(this ContentOperation operation) + public static async Task CheckUpdateAsync(this ContentOperation operation) + { + if (operation.User != null) { - if (operation.User != null) - { - var workflow = GetWorkflow(operation); + var workflow = GetWorkflow(operation); - var status = operation.Snapshot.EditingStatus(); + var status = operation.Snapshot.EditingStatus(); - if (!await workflow.CanUpdateAsync(operation.Snapshot, status, operation.User)) - { - throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status })); - } + if (!await workflow.CanUpdateAsync(operation.Snapshot, status, operation.User)) + { + throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status })); } } + } - private static IContentWorkflow GetWorkflow(ContentOperation operation) - { - return operation.Resolve<IContentWorkflow>(); - } + private static IContentWorkflow GetWorkflow(ContentOperation operation) + { + return operation.Resolve<IContentWorkflow>(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs index 744cf36878..2c5de56aa9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs @@ -11,160 +11,159 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class DynamicContentWorkflow : IContentWorkflow { - public sealed class DynamicContentWorkflow : IContentWorkflow + private readonly IScriptEngine scriptEngine; + private readonly IAppProvider appProvider; + + public DynamicContentWorkflow(IScriptEngine scriptEngine, IAppProvider appProvider) { - private readonly IScriptEngine scriptEngine; - private readonly IAppProvider appProvider; + this.scriptEngine = scriptEngine; - public DynamicContentWorkflow(IScriptEngine scriptEngine, IAppProvider appProvider) - { - this.scriptEngine = scriptEngine; + this.appProvider = appProvider; + } - this.appProvider = appProvider; - } + public async ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - public async ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); + } - return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); - } + public async ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - public async ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && IsTrue(transition, null, user); + } - return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && IsTrue(transition, null, user); - } + public async ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - public async ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, data, user); + } - return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, data, user); - } + public async ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - public async ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) - { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, content.Data, user); + } - return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, content.Data, user); - } + public async ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - public async ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + if (workflow.TryGetStep(status, out var step)) { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + return step.NoUpdate == null || !IsTrue(step.NoUpdate, content.Data, user); + } - if (workflow.TryGetStep(status, out var step)) - { - return step.NoUpdate == null || !IsTrue(step.NoUpdate, content.Data, user); - } + return true; + } + public async ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + if (workflow.TryGetStep(status, out var step) && step.Validate) + { return true; } - public async ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + return status == Status.Published && schema.SchemaDef.Properties.ValidateOnPublish; + } - if (workflow.TryGetStep(status, out var step) && step.Validate) - { - return true; - } + public async ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status) + { + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - return status == Status.Published && schema.SchemaDef.Properties.ValidateOnPublish; + if (workflow.TryGetStep(status, out var step)) + { + return new StatusInfo(status, GetColor(step)); } - public async ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status) - { - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); + return null; + } - if (workflow.TryGetStep(status, out var step)) - { - return new StatusInfo(status, GetColor(step)); - } + public async ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); - return null; - } + var (status, _) = workflow.GetInitialStep(); - public async ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema) - { - var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + return status; + } - var (status, _) = workflow.GetInitialStep(); + public async ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + { + var result = new List<StatusInfo>(); - return status; - } + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - public async ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + foreach (var (to, step, transition) in workflow.GetTransitions(status)) { - var result = new List<StatusInfo>(); - - var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - - foreach (var (to, step, transition) in workflow.GetTransitions(status)) + if (IsTrue(transition, content.Data, user)) { - if (IsTrue(transition, content.Data, user)) - { - result.Add(new StatusInfo(to, GetColor(step))); - } + result.Add(new StatusInfo(to, GetColor(step))); } - - return result.ToArray(); } - private bool IsTrue(WorkflowCondition condition, ContentData? data, ClaimsPrincipal? user) + return result.ToArray(); + } + + private bool IsTrue(WorkflowCondition condition, ContentData? data, ClaimsPrincipal? user) + { + if (condition?.Roles != null && user != null) { - if (condition?.Roles != null && user != null) + if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && condition.Roles.Contains(x.Value))) { - if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && condition.Roles.Contains(x.Value))) - { - return false; - } + return false; } + } - if (!string.IsNullOrWhiteSpace(condition?.Expression) && data != null) + if (!string.IsNullOrWhiteSpace(condition?.Expression) && data != null) + { + var vars = new DataScriptVars { - var vars = new DataScriptVars - { - Data = data - }; - - return scriptEngine.Evaluate(vars, condition.Expression); - } + Data = data + }; - return true; + return scriptEngine.Evaluate(vars, condition.Expression); } - private async ValueTask<Workflow> GetWorkflowAsync(DomainId appId, DomainId schemaId) - { - Workflow? result = null; + return true; + } - var app = await appProvider.GetAppAsync(appId, false); + private async ValueTask<Workflow> GetWorkflowAsync(DomainId appId, DomainId schemaId) + { + Workflow? result = null; - if (app != null) - { - result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId)); + var app = await appProvider.GetAppAsync(appId, false); - if (result == null) - { - result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0); - } - } + if (app != null) + { + result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId)); if (result == null) { - result = Workflow.Default; + result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0); } - - return result; } - private static string GetColor(WorkflowStep step) + if (result == null) { - return step.Color ?? StatusColors.Draft; + result = Workflow.Default; } + + return result; + } + + private static string GetColor(WorkflowStep step) + { + return step.Color ?? StatusColors.Draft; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs index d0a2fd499d..0dcf990564 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs @@ -21,84 +21,83 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable RECS0082 // Parameter has the same name as a member and hides it -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public sealed class CachingGraphQLResolver : IConfigureExecution { - public sealed class CachingGraphQLResolver : IConfigureExecution - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private readonly IBackgroundCache cache; - private readonly ISchemasHash schemasHash; - private readonly IServiceProvider serviceProvider; - private readonly GraphQLOptions options; + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private readonly IBackgroundCache cache; + private readonly ISchemasHash schemasHash; + private readonly IServiceProvider serviceProvider; + private readonly GraphQLOptions options; - private sealed record CacheEntry(GraphQLSchema Model, string Hash, Instant Created); + private sealed record CacheEntry(GraphQLSchema Model, string Hash, Instant Created); - public float SortOrder => 0; + public float SortOrder => 0; - public IServiceProvider Services - { - get => serviceProvider; - } + public IServiceProvider Services + { + get => serviceProvider; + } - public CachingGraphQLResolver(IBackgroundCache cache, ISchemasHash schemasHash, IServiceProvider serviceProvider, - IOptions<GraphQLOptions> options) - { - this.cache = cache; - this.schemasHash = schemasHash; - this.serviceProvider = serviceProvider; - this.options = options.Value; - } + public CachingGraphQLResolver(IBackgroundCache cache, ISchemasHash schemasHash, IServiceProvider serviceProvider, + IOptions<GraphQLOptions> options) + { + this.cache = cache; + this.schemasHash = schemasHash; + this.serviceProvider = serviceProvider; + this.options = options.Value; + } - public async Task<ExecutionResult> ExecuteAsync(ExecutionOptions options, ExecutionDelegate next) - { - var context = ((GraphQLExecutionContext)options.UserContext!).Context; + public async Task<ExecutionResult> ExecuteAsync(ExecutionOptions options, ExecutionDelegate next) + { + var context = ((GraphQLExecutionContext)options.UserContext!).Context; - options.Schema = await GetSchemaAsync(context.App); + options.Schema = await GetSchemaAsync(context.App); - return await next(options); - } + return await next(options); + } - public async Task<GraphQLSchema> GetSchemaAsync(IAppEntity app) - { - var entry = await GetModelEntryAsync(app); + public async Task<GraphQLSchema> GetSchemaAsync(IAppEntity app) + { + var entry = await GetModelEntryAsync(app); - return entry.Model; - } + return entry.Model; + } - private Task<CacheEntry> GetModelEntryAsync(IAppEntity app) + private Task<CacheEntry> GetModelEntryAsync(IAppEntity app) + { + if (options.CacheDuration <= 0) { - if (options.CacheDuration <= 0) - { - return CreateModelAsync(app); - } - - var cacheKey = CreateCacheKey(app.Id, app.Version.ToString(CultureInfo.InvariantCulture)); - - return cache.GetOrCreateAsync(cacheKey, CacheDuration, async entry => - { - return await CreateModelAsync(app); - }, - async entry => - { - var (created, hash) = await schemasHash.GetCurrentHashAsync(app); - - return created < entry.Created || string.Equals(hash, entry.Hash, StringComparison.OrdinalIgnoreCase); - }); + return CreateModelAsync(app); } - private async Task<CacheEntry> CreateModelAsync(IAppEntity app) + var cacheKey = CreateCacheKey(app.Id, app.Version.ToString(CultureInfo.InvariantCulture)); + + return cache.GetOrCreateAsync(cacheKey, CacheDuration, async entry => + { + return await CreateModelAsync(app); + }, + async entry => { - var schemasList = await serviceProvider.GetRequiredService<IAppProvider>().GetSchemasAsync(app.Id); - var schemasKey = await schemasHash.ComputeHashAsync(app, schemasList); + var (created, hash) = await schemasHash.GetCurrentHashAsync(app); - var now = SystemClock.Instance.GetCurrentInstant(); + return created < entry.Created || string.Equals(hash, entry.Hash, StringComparison.OrdinalIgnoreCase); + }); + } - return new CacheEntry(new Builder(app, options).BuildSchema(schemasList), schemasKey, now); - } + private async Task<CacheEntry> CreateModelAsync(IAppEntity app) + { + var schemasList = await serviceProvider.GetRequiredService<IAppProvider>().GetSchemasAsync(app.Id); + var schemasKey = await schemasHash.ComputeHashAsync(app, schemasList); - private static object CreateCacheKey(DomainId appId, string etag) - { - return $"GraphQLModel_{appId}_{etag}"; - } + var now = SystemClock.Instance.GetCurrentInstant(); + + return new CacheEntry(new Builder(app, options).BuildSchema(schemasList), schemasKey, now); + } + + private static object CreateCacheKey(DomainId appId, string etag) + { + return $"GraphQLModel_{appId}_{etag}"; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index 1fbb2a8efd..76ebc54dc9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -12,200 +12,199 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public sealed class GraphQLExecutionContext : QueryExecutionContext { - public sealed class GraphQLExecutionContext : QueryExecutionContext + private static readonly List<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>(); + private static readonly List<IEnrichedContentEntity> EmptyContents = new List<IEnrichedContentEntity>(); + private readonly IDataLoaderContextAccessor dataLoaders; + + public override Context Context { get; } + + public GraphQLExecutionContext( + IDataLoaderContextAccessor dataLoaders, + IAssetQueryService assetQuery, + IAssetCache assetCache, + IContentQueryService contentQuery, + IContentCache contentCache, + IServiceProvider serviceProvider, + Context context) + : base(assetQuery, assetCache, contentQuery, contentCache, serviceProvider) { - private static readonly List<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>(); - private static readonly List<IEnrichedContentEntity> EmptyContents = new List<IEnrichedContentEntity>(); - private readonly IDataLoaderContextAccessor dataLoaders; - - public override Context Context { get; } - - public GraphQLExecutionContext( - IDataLoaderContextAccessor dataLoaders, - IAssetQueryService assetQuery, - IAssetCache assetCache, - IContentQueryService contentQuery, - IContentCache contentCache, - IServiceProvider serviceProvider, - Context context) - : base(assetQuery, assetCache, contentQuery, contentCache, serviceProvider) - { - this.dataLoaders = dataLoaders; + this.dataLoaders = dataLoaders; - Context = context.Clone(b => b - .WithoutCleanup() - .WithoutContentEnrichment()); - } + Context = context.Clone(b => b + .WithoutCleanup() + .WithoutContentEnrichment()); + } - public async ValueTask<IUser?> FindUserAsync(RefToken refToken, - CancellationToken ct) + public async ValueTask<IUser?> FindUserAsync(RefToken refToken, + CancellationToken ct) + { + if (refToken.IsClient) { - if (refToken.IsClient) - { - return new ClientUser(refToken); - } - else - { - var dataLoader = GetUserLoader(); - - return await dataLoader.LoadAsync(refToken.Identifier).GetResultAsync(ct); - } + return new ClientUser(refToken); } - - public async Task<IEnrichedAssetEntity?> FindAssetAsync(DomainId id, - CancellationToken ct) + else { - var dataLoader = GetAssetsLoader(); + var dataLoader = GetUserLoader(); - return await dataLoader.LoadAsync(id).GetResultAsync(ct); + return await dataLoader.LoadAsync(refToken.Identifier).GetResultAsync(ct); } + } + + public async Task<IEnrichedAssetEntity?> FindAssetAsync(DomainId id, + CancellationToken ct) + { + var dataLoader = GetAssetsLoader(); + + return await dataLoader.LoadAsync(id).GetResultAsync(ct); + } + + public async Task<IContentEntity?> FindContentAsync(DomainId schemaId, DomainId id, + CancellationToken ct) + { + var dataLoader = GetContentsLoader(); - public async Task<IContentEntity?> FindContentAsync(DomainId schemaId, DomainId id, - CancellationToken ct) + var content = await dataLoader.LoadAsync(id).GetResultAsync(ct); + + if (content?.SchemaId.Id != schemaId) { - var dataLoader = GetContentsLoader(); + content = null; + } - var content = await dataLoader.LoadAsync(id).GetResultAsync(ct); + return content; + } - if (content?.SchemaId.Id != schemaId) - { - content = null; - } + public Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(JsonValue value, TimeSpan cacheDuration, + CancellationToken ct) + { + var ids = ParseIds(value); + + return GetAssetsAsync(ids, cacheDuration, ct); + } - return content; + public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetAssetsAsync(List<DomainId>? ids, TimeSpan cacheDuration, + CancellationToken ct) + { + if (ids == null || ids.Count == 0) + { + return EmptyAssets; } - public Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(JsonValue value, TimeSpan cacheDuration, - CancellationToken ct) + async Task<IReadOnlyList<IEnrichedAssetEntity>> LoadAsync(IEnumerable<DomainId> ids) { - var ids = ParseIds(value); + var result = await GetAssetsLoader().LoadAsync(ids).GetResultAsync(ct); - return GetAssetsAsync(ids, cacheDuration, ct); + return result?.NotNull().ToList() ?? EmptyAssets; } - public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetAssetsAsync(List<DomainId>? ids, TimeSpan cacheDuration, - CancellationToken ct) + if (cacheDuration > TimeSpan.Zero) { - if (ids == null || ids.Count == 0) + var assets = await AssetCache.CacheOrQueryAsync(ids, async pendingIds => { - return EmptyAssets; - } + return await LoadAsync(pendingIds); + }, cacheDuration); - async Task<IReadOnlyList<IEnrichedAssetEntity>> LoadAsync(IEnumerable<DomainId> ids) - { - var result = await GetAssetsLoader().LoadAsync(ids).GetResultAsync(ct); + return assets; + } - return result?.NotNull().ToList() ?? EmptyAssets; - } + return await LoadAsync(ids); + } - if (cacheDuration > TimeSpan.Zero) - { - var assets = await AssetCache.CacheOrQueryAsync(ids, async pendingIds => - { - return await LoadAsync(pendingIds); - }, cacheDuration); + public Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(JsonValue value, TimeSpan cacheDuration, + CancellationToken ct) + { + var ids = ParseIds(value); - return assets; - } + return GetContentsAsync(ids, cacheDuration, ct); + } - return await LoadAsync(ids); + public async Task<IReadOnlyList<IEnrichedContentEntity>> GetContentsAsync(List<DomainId>? ids, TimeSpan cacheDuration, + CancellationToken ct) + { + if (ids == null || ids.Count == 0) + { + return EmptyContents; } - public Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(JsonValue value, TimeSpan cacheDuration, - CancellationToken ct) + async Task<IReadOnlyList<IEnrichedContentEntity>> LoadAsync(IEnumerable<DomainId> ids) { - var ids = ParseIds(value); + var result = await GetContentsLoader().LoadAsync(ids).GetResultAsync(ct); - return GetContentsAsync(ids, cacheDuration, ct); + return result?.NotNull().ToList() ?? EmptyContents; } - public async Task<IReadOnlyList<IEnrichedContentEntity>> GetContentsAsync(List<DomainId>? ids, TimeSpan cacheDuration, - CancellationToken ct) + if (cacheDuration > TimeSpan.Zero) { - if (ids == null || ids.Count == 0) + var contents = await ContentCache.CacheOrQueryAsync(ids, async pendingIds => { - return EmptyContents; - } + return await LoadAsync(pendingIds); + }, cacheDuration); - async Task<IReadOnlyList<IEnrichedContentEntity>> LoadAsync(IEnumerable<DomainId> ids) - { - var result = await GetContentsLoader().LoadAsync(ids).GetResultAsync(ct); + return contents.ToList(); + } - return result?.NotNull().ToList() ?? EmptyContents; - } + return await LoadAsync(ids); + } - if (cacheDuration > TimeSpan.Zero) + private IDataLoader<DomainId, IEnrichedAssetEntity> GetAssetsLoader() + { + return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedAssetEntity>(nameof(GetAssetsLoader), + async (batch, ct) => { - var contents = await ContentCache.CacheOrQueryAsync(ids, async pendingIds => - { - return await LoadAsync(pendingIds); - }, cacheDuration); - - return contents.ToList(); - } + var result = await GetReferencedAssetsAsync(new List<DomainId>(batch), ct); - return await LoadAsync(ids); - } + return result.ToDictionary(x => x.Id); + }); + } - private IDataLoader<DomainId, IEnrichedAssetEntity> GetAssetsLoader() - { - return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedAssetEntity>(nameof(GetAssetsLoader), - async (batch, ct) => - { - var result = await GetReferencedAssetsAsync(new List<DomainId>(batch), ct); + private IDataLoader<DomainId, IEnrichedContentEntity> GetContentsLoader() + { + return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedContentEntity>(nameof(GetContentsLoader), + async (batch, ct) => + { + var result = await GetReferencedContentsAsync(new List<DomainId>(batch), ct); - return result.ToDictionary(x => x.Id); - }); - } + return result.ToDictionary(x => x.Id); + }); + } - private IDataLoader<DomainId, IEnrichedContentEntity> GetContentsLoader() - { - return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedContentEntity>(nameof(GetContentsLoader), - async (batch, ct) => - { - var result = await GetReferencedContentsAsync(new List<DomainId>(batch), ct); + private IDataLoader<string, IUser> GetUserLoader() + { + return dataLoaders.Context!.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader), + async (batch, ct) => + { + var result = await Resolve<IUserResolver>().QueryManyAsync(batch.ToArray(), ct); - return result.ToDictionary(x => x.Id); - }); - } + return result; + }); + } - private IDataLoader<string, IUser> GetUserLoader() + private static List<DomainId>? ParseIds(JsonValue value) + { + try { - return dataLoaders.Context!.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader), - async (batch, ct) => - { - var result = await Resolve<IUserResolver>().QueryManyAsync(batch.ToArray(), ct); - - return result; - }); - } + List<DomainId>? result = null; - private static List<DomainId>? ParseIds(JsonValue value) - { - try + if (value.Value is JsonArray a) { - List<DomainId>? result = null; - - if (value.Value is JsonArray a) + foreach (var item in a) { - foreach (var item in a) + if (item.Value is string id) { - if (item.Value is string id) - { - result ??= new List<DomainId>(); - result.Add(DomainId.Create(id)); - } + result ??= new List<DomainId>(); + result.Add(DomainId.Create(id)); } } - - return result; - } - catch - { - return null; } + + return result; + } + catch + { + return null; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs index fe28c574f8..566f88ea3e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public sealed class GraphQLOptions { - public sealed class GraphQLOptions - { - public int CacheDuration { get; set; } = 10 * 60; + public int CacheDuration { get; set; } = 10 * 60; - public bool EnableSubscriptions { get; set; } = true; - } + public bool EnableSubscriptions { get; set; } = true; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationMutations.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationMutations.cs index 3e7050d12b..27f80fa763 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationMutations.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationMutations.cs @@ -9,84 +9,83 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +internal sealed class ApplicationMutations : ObjectGraphType { - internal sealed class ApplicationMutations : ObjectGraphType + public ApplicationMutations(Builder builder, IEnumerable<SchemaInfo> schemas) { - public ApplicationMutations(Builder builder, IEnumerable<SchemaInfo> schemas) + foreach (var schemaInfo in schemas.Where(x => x.Fields.Count > 0)) { - foreach (var schemaInfo in schemas.Where(x => x.Fields.Count > 0)) - { - var contentType = new NonNullGraphType(builder.GetContentType(schemaInfo)); - - var inputType = new DataInputGraphType(builder, schemaInfo); + var contentType = new NonNullGraphType(builder.GetContentType(schemaInfo)); - AddField(new FieldType - { - Name = $"create{schemaInfo.TypeName}Content", - Arguments = ContentActions.Create.Arguments(inputType), - ResolvedType = contentType, - Resolver = ContentActions.Create.Resolver, - Description = $"Creates an {schemaInfo.DisplayName} content." - }).WithSchemaNamedId(schemaInfo); + var inputType = new DataInputGraphType(builder, schemaInfo); - AddField(new FieldType - { - Name = $"update{schemaInfo.TypeName}Content", - Arguments = ContentActions.Update.Arguments(inputType), - ResolvedType = contentType, - Resolver = ContentActions.Update.Resolver, - Description = $"Update an {schemaInfo.DisplayName} content by id." - }).WithSchemaNamedId(schemaInfo); + AddField(new FieldType + { + Name = $"create{schemaInfo.TypeName}Content", + Arguments = ContentActions.Create.Arguments(inputType), + ResolvedType = contentType, + Resolver = ContentActions.Create.Resolver, + Description = $"Creates an {schemaInfo.DisplayName} content." + }).WithSchemaNamedId(schemaInfo); - AddField(new FieldType - { - Name = $"upsert{schemaInfo.TypeName}Content", - Arguments = ContentActions.Upsert.Arguments(inputType), - ResolvedType = contentType, - Resolver = ContentActions.Upsert.Resolver, - Description = $"Upsert an {schemaInfo.DisplayName} content by id." - }).WithSchemaNamedId(schemaInfo); + AddField(new FieldType + { + Name = $"update{schemaInfo.TypeName}Content", + Arguments = ContentActions.Update.Arguments(inputType), + ResolvedType = contentType, + Resolver = ContentActions.Update.Resolver, + Description = $"Update an {schemaInfo.DisplayName} content by id." + }).WithSchemaNamedId(schemaInfo); - AddField(new FieldType - { - Name = $"patch{schemaInfo.TypeName}Content", - Arguments = ContentActions.Patch.Arguments(inputType), - ResolvedType = contentType, - Resolver = ContentActions.Patch.Resolver, - Description = $"Patch an {schemaInfo.DisplayName} content by id." - }).WithSchemaNamedId(schemaInfo); + AddField(new FieldType + { + Name = $"upsert{schemaInfo.TypeName}Content", + Arguments = ContentActions.Upsert.Arguments(inputType), + ResolvedType = contentType, + Resolver = ContentActions.Upsert.Resolver, + Description = $"Upsert an {schemaInfo.DisplayName} content by id." + }).WithSchemaNamedId(schemaInfo); - AddField(new FieldType - { - Name = $"change{schemaInfo.TypeName}Content", - Arguments = ContentActions.ChangeStatus.Arguments, - ResolvedType = contentType, - Resolver = ContentActions.ChangeStatus.Resolver, - Description = $"Change a {schemaInfo.DisplayName} content." - }).WithSchemaNamedId(schemaInfo); + AddField(new FieldType + { + Name = $"patch{schemaInfo.TypeName}Content", + Arguments = ContentActions.Patch.Arguments(inputType), + ResolvedType = contentType, + Resolver = ContentActions.Patch.Resolver, + Description = $"Patch an {schemaInfo.DisplayName} content by id." + }).WithSchemaNamedId(schemaInfo); - AddField(new FieldType - { - Name = $"delete{schemaInfo.TypeName}Content", - Arguments = ContentActions.Delete.Arguments, - ResolvedType = EntitySavedGraphType.NonNull, - Resolver = ContentActions.Delete.Resolver, - Description = $"Delete an {schemaInfo.DisplayName} content." - }).WithSchemaNamedId(schemaInfo); + AddField(new FieldType + { + Name = $"change{schemaInfo.TypeName}Content", + Arguments = ContentActions.ChangeStatus.Arguments, + ResolvedType = contentType, + Resolver = ContentActions.ChangeStatus.Resolver, + Description = $"Change a {schemaInfo.DisplayName} content." + }).WithSchemaNamedId(schemaInfo); - AddField(new FieldType - { - Name = $"publish{schemaInfo.TypeName}Content", - Arguments = ContentActions.ChangeStatus.Arguments, - ResolvedType = contentType, - Resolver = ContentActions.ChangeStatus.Resolver, - Description = $"Publish a {schemaInfo.DisplayName} content.", - DeprecationReason = $"Use 'change{schemaInfo.TypeName}Content' instead" - }).WithSchemaNamedId(schemaInfo); - } + AddField(new FieldType + { + Name = $"delete{schemaInfo.TypeName}Content", + Arguments = ContentActions.Delete.Arguments, + ResolvedType = EntitySavedGraphType.NonNull, + Resolver = ContentActions.Delete.Resolver, + Description = $"Delete an {schemaInfo.DisplayName} content." + }).WithSchemaNamedId(schemaInfo); - Description = "The app mutations."; + AddField(new FieldType + { + Name = $"publish{schemaInfo.TypeName}Content", + Arguments = ContentActions.ChangeStatus.Arguments, + ResolvedType = contentType, + Resolver = ContentActions.ChangeStatus.Resolver, + Description = $"Publish a {schemaInfo.DisplayName} content.", + DeprecationReason = $"Use 'change{schemaInfo.TypeName}Content' instead" + }).WithSchemaNamedId(schemaInfo); } + + Description = "The app mutations."; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs index 74b08f29b7..a7328ad94b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs @@ -8,60 +8,59 @@ using GraphQL.Types; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +internal sealed class ApplicationQueries : ObjectGraphType { - internal sealed class ApplicationQueries : ObjectGraphType + public ApplicationQueries(Builder builder, IEnumerable<SchemaInfo> schemaInfos) { - public ApplicationQueries(Builder builder, IEnumerable<SchemaInfo> schemaInfos) - { - AddField(SharedTypes.FindAsset); - AddField(SharedTypes.QueryAssets); - AddField(SharedTypes.QueryAssetsWithTotal); - - foreach (var schemaInfo in schemaInfos) - { - var contentType = builder.GetContentType(schemaInfo); + AddField(SharedTypes.FindAsset); + AddField(SharedTypes.QueryAssets); + AddField(SharedTypes.QueryAssetsWithTotal); - AddContentFind(schemaInfo, contentType); - AddContentQueries(builder, schemaInfo, contentType); - } + foreach (var schemaInfo in schemaInfos) + { + var contentType = builder.GetContentType(schemaInfo); - Description = "The app queries."; + AddContentFind(schemaInfo, contentType); + AddContentQueries(builder, schemaInfo, contentType); } - private void AddContentFind(SchemaInfo schemaInfo, IGraphType contentType) + Description = "The app queries."; + } + + private void AddContentFind(SchemaInfo schemaInfo, IGraphType contentType) + { + AddField(new FieldType { - AddField(new FieldType - { - Name = $"find{schemaInfo.TypeName}Content", - Arguments = ContentActions.Find.Arguments, - ResolvedType = contentType, - Resolver = ContentActions.Find.Resolver, - Description = $"Find an {schemaInfo.DisplayName} content by id." - }).WithSchemaId(schemaInfo); - } + Name = $"find{schemaInfo.TypeName}Content", + Arguments = ContentActions.Find.Arguments, + ResolvedType = contentType, + Resolver = ContentActions.Find.Resolver, + Description = $"Find an {schemaInfo.DisplayName} content by id." + }).WithSchemaId(schemaInfo); + } - private void AddContentQueries(Builder builder, SchemaInfo schemaInfo, IGraphType contentType) + private void AddContentQueries(Builder builder, SchemaInfo schemaInfo, IGraphType contentType) + { + AddField(new FieldType { - AddField(new FieldType - { - Name = $"query{schemaInfo.TypeName}Contents", - Arguments = ContentActions.QueryOrReferencing.Arguments, - ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), - Resolver = ContentActions.QueryOrReferencing.Query, - Description = $"Query {schemaInfo.DisplayName} content items." - }).WithSchemaId(schemaInfo); + Name = $"query{schemaInfo.TypeName}Contents", + Arguments = ContentActions.QueryOrReferencing.Arguments, + ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), + Resolver = ContentActions.QueryOrReferencing.Query, + Description = $"Query {schemaInfo.DisplayName} content items." + }).WithSchemaId(schemaInfo); - var resultType = builder.GetContentResultType(schemaInfo); + var resultType = builder.GetContentResultType(schemaInfo); - AddField(new FieldType - { - Name = $"query{schemaInfo.TypeName}ContentsWithTotal", - Arguments = ContentActions.QueryOrReferencing.Arguments, - ResolvedType = resultType, - Resolver = ContentActions.QueryOrReferencing.QueryWithTotal, - Description = $"Query {schemaInfo.DisplayName} content items with total count." - }).WithSchemaId(schemaInfo); - } + AddField(new FieldType + { + Name = $"query{schemaInfo.TypeName}ContentsWithTotal", + Arguments = ContentActions.QueryOrReferencing.Arguments, + ResolvedType = resultType, + Resolver = ContentActions.QueryOrReferencing.QueryWithTotal, + Description = $"Query {schemaInfo.DisplayName} content items with total count." + }).WithSchemaId(schemaInfo); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationSubscriptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationSubscriptions.cs index 1d5e65ef03..713589d885 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationSubscriptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationSubscriptions.cs @@ -9,31 +9,30 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +internal sealed class ApplicationSubscriptions : ObjectGraphType { - internal sealed class ApplicationSubscriptions : ObjectGraphType + public ApplicationSubscriptions() { - public ApplicationSubscriptions() + AddField(new FieldType { - AddField(new FieldType - { - Name = $"assetChanges", - Arguments = AssetActions.Subscription.Arguments, - ResolvedType = SharedTypes.EnrichedAssetEvent, - Resolver = null, - StreamResolver = AssetActions.Subscription.Resolver, - Description = "Subscribe to asset events." - }); + Name = $"assetChanges", + Arguments = AssetActions.Subscription.Arguments, + ResolvedType = SharedTypes.EnrichedAssetEvent, + Resolver = null, + StreamResolver = AssetActions.Subscription.Resolver, + Description = "Subscribe to asset events." + }); - AddField(new FieldType - { - Name = $"contentChanges", - Arguments = ContentActions.Subscription.Arguments, - ResolvedType = SharedTypes.EnrichedContentEvent, - Resolver = null, - StreamResolver = ContentActions.Subscription.Resolver, - Description = "Subscribe to content events." - }); - } + AddField(new FieldType + { + Name = $"contentChanges", + Arguments = ContentActions.Subscription.Arguments, + ResolvedType = SharedTypes.EnrichedContentEvent, + Resolver = null, + StreamResolver = ContentActions.Subscription.Resolver, + Description = "Subscribe to content events." + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs index 4634fe85f7..9769ec3568 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs @@ -15,128 +15,127 @@ using Squidex.Infrastructure; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets; + +internal static class AssetActions { - internal static class AssetActions + public static class Metadata { - public static class Metadata + public static readonly QueryArguments Arguments = new QueryArguments { - public static readonly QueryArguments Arguments = new QueryArguments + new QueryArgument(Scalars.String) { - new QueryArgument(Scalars.String) - { - Name = "path", - Description = FieldDescriptions.JsonPath, - DefaultValue = null - } - }; + Name = "path", + Description = FieldDescriptions.JsonPath, + DefaultValue = null + } + }; - public static readonly IFieldResolver Resolver = Resolvers.Sync<IEnrichedAssetEntity, object?>((source, fieldContext, _) => + public static readonly IFieldResolver Resolver = Resolvers.Sync<IEnrichedAssetEntity, object?>((source, fieldContext, _) => + { + if (fieldContext.Arguments != null && + fieldContext.Arguments.TryGetValue("path", out var path)) { - if (fieldContext.Arguments != null && - fieldContext.Arguments.TryGetValue("path", out var path)) - { - source.Metadata.TryGetByPath(path.Value as string, out var result); + source.Metadata.TryGetByPath(path.Value as string, out var result); - return result; - } + return result; + } - return source.Metadata; - }); - } + return source.Metadata; + }); + } - public static class Find + public static class Find + { + public static readonly QueryArguments Arguments = new QueryArguments { - public static readonly QueryArguments Arguments = new QueryArguments + new QueryArgument(Scalars.NonNullString) { - new QueryArgument(Scalars.NonNullString) - { - Name = "id", - Description = "The ID of the asset (usually GUID).", - DefaultValue = null - } - }; + Name = "id", + Description = "The ID of the asset (usually GUID).", + DefaultValue = null + } + }; - public static readonly IFieldResolver Resolver = Resolvers.Async<object, object?>(async (_, fieldContext, context) => - { - var assetId = fieldContext.GetArgument<DomainId>("id"); + public static readonly IFieldResolver Resolver = Resolvers.Async<object, object?>(async (_, fieldContext, context) => + { + var assetId = fieldContext.GetArgument<DomainId>("id"); - return await context.FindAssetAsync(assetId, - fieldContext.CancellationToken); - }); - } + return await context.FindAssetAsync(assetId, + fieldContext.CancellationToken); + }); + } - public static class Query + public static class Query + { + public static readonly QueryArguments Arguments = new QueryArguments { - public static readonly QueryArguments Arguments = new QueryArguments + new QueryArgument(Scalars.Int) { - new QueryArgument(Scalars.Int) - { - Name = "top", - Description = FieldDescriptions.QueryTop, - DefaultValue = null - }, - new QueryArgument(Scalars.Int) - { - Name = "skip", - Description = FieldDescriptions.QuerySkip, - DefaultValue = 0 - }, - new QueryArgument(Scalars.String) - { - Name = "filter", - Description = FieldDescriptions.QueryFilter, - DefaultValue = null - }, - new QueryArgument(Scalars.String) - { - Name = "orderby", - Description = FieldDescriptions.QueryOrderBy, - DefaultValue = null - } - }; - - public static readonly IFieldResolver Resolver = Resolvers.Async<object, object>(async (_, fieldContext, context) => + Name = "top", + Description = FieldDescriptions.QueryTop, + DefaultValue = null + }, + new QueryArgument(Scalars.Int) { - var query = fieldContext.BuildODataQuery(); + Name = "skip", + Description = FieldDescriptions.QuerySkip, + DefaultValue = 0 + }, + new QueryArgument(Scalars.String) + { + Name = "filter", + Description = FieldDescriptions.QueryFilter, + DefaultValue = null + }, + new QueryArgument(Scalars.String) + { + Name = "orderby", + Description = FieldDescriptions.QueryOrderBy, + DefaultValue = null + } + }; - var q = Q.Empty.WithODataQuery(query).WithoutTotal(); + public static readonly IFieldResolver Resolver = Resolvers.Async<object, object>(async (_, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); - return await context.QueryAssetsAsync(q, - fieldContext.CancellationToken); - }); + var q = Q.Empty.WithODataQuery(query).WithoutTotal(); - public static readonly IFieldResolver ResolverWithTotal = Resolvers.Async<object, object>(async (_, fieldContext, context) => - { - var query = fieldContext.BuildODataQuery(); + return await context.QueryAssetsAsync(q, + fieldContext.CancellationToken); + }); - var q = Q.Empty.WithODataQuery(query); + public static readonly IFieldResolver ResolverWithTotal = Resolvers.Async<object, object>(async (_, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); - return await context.QueryAssetsAsync(q, - fieldContext.CancellationToken); - }); - } + var q = Q.Empty.WithODataQuery(query); - public static class Subscription + return await context.QueryAssetsAsync(q, + fieldContext.CancellationToken); + }); + } + + public static class Subscription + { + public static readonly QueryArguments Arguments = new QueryArguments { - public static readonly QueryArguments Arguments = new QueryArguments + new QueryArgument(Scalars.EnrichedAssetEventType) { - new QueryArgument(Scalars.EnrichedAssetEventType) - { - Name = "type", - Description = FieldDescriptions.EventType, - DefaultValue = null - } - }; + Name = "type", + Description = FieldDescriptions.EventType, + DefaultValue = null + } + }; - public static readonly ISourceStreamResolver Resolver = Resolvers.Stream(PermissionIds.AppAssetsRead, c => + public static readonly ISourceStreamResolver Resolver = Resolvers.Stream(PermissionIds.AppAssetsRead, c => + { + return new AssetSubscription { - return new AssetSubscription - { - // Primary filter for the event types. - Type = c.GetArgument<EnrichedAssetEventType?>("type") - }; - }); - } + // Primary filter for the event types. + Type = c.GetArgument<EnrichedAssetEventType?>("type") + }; + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs index 197b403642..7a5587cebe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs @@ -14,267 +14,266 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets; + +internal sealed class AssetGraphType : SharedObjectGraphType<IEnrichedAssetEntity> { - internal sealed class AssetGraphType : SharedObjectGraphType<IEnrichedAssetEntity> + public AssetGraphType() { - public AssetGraphType() + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "Asset"; + + AddField(new FieldType + { + Name = "id", + ResolvedType = Scalars.NonNullString, + Resolver = EntityResolvers.Id, + Description = FieldDescriptions.EntityId + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = Scalars.NonNullInt, + Resolver = EntityResolvers.Version, + Description = FieldDescriptions.EntityVersion + }); + + AddField(new FieldType + { + Name = "created", + ResolvedType = Scalars.NonNullDateTime, + Resolver = EntityResolvers.Created, + Description = FieldDescriptions.EntityCreated + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = Scalars.NonNullString, + Resolver = EntityResolvers.CreatedBy, + Description = FieldDescriptions.EntityCreatedBy + }); + + AddField(new FieldType + { + Name = "createdByUser", + ResolvedType = UserGraphType.NonNull, + Resolver = EntityResolvers.CreatedByUser, + Description = FieldDescriptions.EntityCreatedBy + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = Scalars.NonNullDateTime, + Resolver = EntityResolvers.LastModified, + Description = FieldDescriptions.EntityLastModified + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + ResolvedType = Scalars.NonNullString, + Resolver = EntityResolvers.LastModifiedBy, + Description = FieldDescriptions.EntityLastModifiedBy + }); + + AddField(new FieldType + { + Name = "lastModifiedByUser", + ResolvedType = UserGraphType.NonNull, + Resolver = EntityResolvers.LastModifiedByUser, + Description = FieldDescriptions.EntityLastModifiedBy + }); + + AddField(new FieldType + { + Name = "mimeType", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.MimeType), + Description = FieldDescriptions.AssetMimeType + }); + + AddField(new FieldType + { + Name = "url", + ResolvedType = Scalars.NonNullString, + Resolver = Url, + Description = FieldDescriptions.AssetUrl + }); + + AddField(new FieldType + { + Name = "thumbnailUrl", + ResolvedType = Scalars.String, + Resolver = ThumbnailUrl, + Description = FieldDescriptions.AssetThumbnailUrl + }); + + AddField(new FieldType + { + Name = "fileName", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.FileName), + Description = FieldDescriptions.AssetFileName + }); + + AddField(new FieldType + { + Name = "fileHash", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.FileHash), + Description = FieldDescriptions.AssetFileHash + }); + + AddField(new FieldType { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "Asset"; - - AddField(new FieldType - { - Name = "id", - ResolvedType = Scalars.NonNullString, - Resolver = EntityResolvers.Id, - Description = FieldDescriptions.EntityId - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = Scalars.NonNullInt, - Resolver = EntityResolvers.Version, - Description = FieldDescriptions.EntityVersion - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = Scalars.NonNullDateTime, - Resolver = EntityResolvers.Created, - Description = FieldDescriptions.EntityCreated - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = Scalars.NonNullString, - Resolver = EntityResolvers.CreatedBy, - Description = FieldDescriptions.EntityCreatedBy - }); - - AddField(new FieldType - { - Name = "createdByUser", - ResolvedType = UserGraphType.NonNull, - Resolver = EntityResolvers.CreatedByUser, - Description = FieldDescriptions.EntityCreatedBy - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = Scalars.NonNullDateTime, - Resolver = EntityResolvers.LastModified, - Description = FieldDescriptions.EntityLastModified - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = Scalars.NonNullString, - Resolver = EntityResolvers.LastModifiedBy, - Description = FieldDescriptions.EntityLastModifiedBy - }); - - AddField(new FieldType - { - Name = "lastModifiedByUser", - ResolvedType = UserGraphType.NonNull, - Resolver = EntityResolvers.LastModifiedByUser, - Description = FieldDescriptions.EntityLastModifiedBy - }); - - AddField(new FieldType - { - Name = "mimeType", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.MimeType), - Description = FieldDescriptions.AssetMimeType - }); - - AddField(new FieldType - { - Name = "url", - ResolvedType = Scalars.NonNullString, - Resolver = Url, - Description = FieldDescriptions.AssetUrl - }); - - AddField(new FieldType - { - Name = "thumbnailUrl", - ResolvedType = Scalars.String, - Resolver = ThumbnailUrl, - Description = FieldDescriptions.AssetThumbnailUrl - }); - - AddField(new FieldType - { - Name = "fileName", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.FileName), - Description = FieldDescriptions.AssetFileName - }); - - AddField(new FieldType - { - Name = "fileHash", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.FileHash), - Description = FieldDescriptions.AssetFileHash - }); - - AddField(new FieldType - { - Name = "fileType", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.FileName.FileType()), - Description = FieldDescriptions.AssetFileType - }); - - AddField(new FieldType - { - Name = "fileSize", - ResolvedType = Scalars.NonNullInt, - Resolver = Resolve(x => x.FileSize), - Description = FieldDescriptions.AssetFileSize - }); - - AddField(new FieldType - { - Name = "fileVersion", - ResolvedType = Scalars.NonNullInt, - Resolver = Resolve(x => x.FileVersion), - Description = FieldDescriptions.AssetFileVersion - }); - - AddField(new FieldType - { - Name = "slug", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.Slug), - Description = FieldDescriptions.AssetSlug - }); - - AddField(new FieldType - { - Name = "isProtected", - ResolvedType = Scalars.NonNullBoolean, - Resolver = Resolve(x => x.IsProtected), - Description = FieldDescriptions.AssetIsProtected - }); - - AddField(new FieldType - { - Name = "isImage", - ResolvedType = Scalars.NonNullBoolean, - Resolver = Resolve(x => x.Type == AssetType.Image), - Description = FieldDescriptions.AssetIsImage, - DeprecationReason = "Use 'type' field instead." - }); - - AddField(new FieldType - { - Name = "pixelWidth", - ResolvedType = Scalars.Int, - Resolver = Resolve(x => x.Metadata.GetPixelWidth()), - Description = FieldDescriptions.AssetPixelWidth, - DeprecationReason = "Use 'metadata' field instead." - }); - - AddField(new FieldType - { - Name = "pixelHeight", - ResolvedType = Scalars.Int, - Resolver = Resolve(x => x.Metadata.GetPixelHeight()), - Description = FieldDescriptions.AssetPixelHeight, - DeprecationReason = "Use 'metadata' field instead." - }); - - AddField(new FieldType - { - Name = "type", - ResolvedType = Scalars.NonNullAssetType, - Resolver = Resolve(x => x.Type), - Description = FieldDescriptions.AssetType - }); - - AddField(new FieldType - { - Name = "metadataText", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.MetadataText), - Description = FieldDescriptions.AssetMetadataText - }); - - AddField(new FieldType - { - Name = "tags", - ResolvedType = Scalars.NonNullStrings, - Resolver = Resolve(x => x.TagNames), - Description = FieldDescriptions.AssetTags - }); - - AddField(new FieldType - { - Name = "metadata", - Arguments = AssetActions.Metadata.Arguments, - ResolvedType = Scalars.JsonNoop, - Resolver = AssetActions.Metadata.Resolver, - Description = FieldDescriptions.AssetMetadata - }); - - AddField(new FieldType - { - Name = "editToken", - ResolvedType = Scalars.String, - Resolver = Resolve(x => x.EditToken), - Description = FieldDescriptions.EditToken - }); - - AddField(new FieldType - { - Name = "sourceUrl", - ResolvedType = Scalars.NonNullString, - Resolver = SourceUrl, - Description = FieldDescriptions.AssetSourceUrl - }); - - Description = "An asset"; - } - - private static readonly IFieldResolver Url = Resolve((asset, _, context) => + Name = "fileType", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.FileName.FileType()), + Description = FieldDescriptions.AssetFileType + }); + + AddField(new FieldType { - var urlGenerator = context.Resolve<IUrlGenerator>(); + Name = "fileSize", + ResolvedType = Scalars.NonNullInt, + Resolver = Resolve(x => x.FileSize), + Description = FieldDescriptions.AssetFileSize + }); - return urlGenerator.AssetContent(asset.AppId, asset.Id.ToString()); + AddField(new FieldType + { + Name = "fileVersion", + ResolvedType = Scalars.NonNullInt, + Resolver = Resolve(x => x.FileVersion), + Description = FieldDescriptions.AssetFileVersion }); - private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) => + AddField(new FieldType { - var urlGenerator = context.Resolve<IUrlGenerator>(); + Name = "slug", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.Slug), + Description = FieldDescriptions.AssetSlug + }); - return urlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion); + AddField(new FieldType + { + Name = "isProtected", + ResolvedType = Scalars.NonNullBoolean, + Resolver = Resolve(x => x.IsProtected), + Description = FieldDescriptions.AssetIsProtected }); - private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) => + AddField(new FieldType { - var urlGenerator = context.Resolve<IUrlGenerator>(); + Name = "isImage", + ResolvedType = Scalars.NonNullBoolean, + Resolver = Resolve(x => x.Type == AssetType.Image), + Description = FieldDescriptions.AssetIsImage, + DeprecationReason = "Use 'type' field instead." + }); - return urlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type); + AddField(new FieldType + { + Name = "pixelWidth", + ResolvedType = Scalars.Int, + Resolver = Resolve(x => x.Metadata.GetPixelWidth()), + Description = FieldDescriptions.AssetPixelWidth, + DeprecationReason = "Use 'metadata' field instead." + }); + + AddField(new FieldType + { + Name = "pixelHeight", + ResolvedType = Scalars.Int, + Resolver = Resolve(x => x.Metadata.GetPixelHeight()), + Description = FieldDescriptions.AssetPixelHeight, + DeprecationReason = "Use 'metadata' field instead." }); - private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, IResolveFieldContext, GraphQLExecutionContext, T> resolver) + AddField(new FieldType { - return Resolvers.Sync(resolver); - } + Name = "type", + ResolvedType = Scalars.NonNullAssetType, + Resolver = Resolve(x => x.Type), + Description = FieldDescriptions.AssetType + }); + + AddField(new FieldType + { + Name = "metadataText", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.MetadataText), + Description = FieldDescriptions.AssetMetadataText + }); + + AddField(new FieldType + { + Name = "tags", + ResolvedType = Scalars.NonNullStrings, + Resolver = Resolve(x => x.TagNames), + Description = FieldDescriptions.AssetTags + }); + + AddField(new FieldType + { + Name = "metadata", + Arguments = AssetActions.Metadata.Arguments, + ResolvedType = Scalars.JsonNoop, + Resolver = AssetActions.Metadata.Resolver, + Description = FieldDescriptions.AssetMetadata + }); - private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, T> resolver) + AddField(new FieldType { - return Resolvers.Sync(resolver); - } + Name = "editToken", + ResolvedType = Scalars.String, + Resolver = Resolve(x => x.EditToken), + Description = FieldDescriptions.EditToken + }); + + AddField(new FieldType + { + Name = "sourceUrl", + ResolvedType = Scalars.NonNullString, + Resolver = SourceUrl, + Description = FieldDescriptions.AssetSourceUrl + }); + + Description = "An asset"; + } + + private static readonly IFieldResolver Url = Resolve((asset, _, context) => + { + var urlGenerator = context.Resolve<IUrlGenerator>(); + + return urlGenerator.AssetContent(asset.AppId, asset.Id.ToString()); + }); + + private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) => + { + var urlGenerator = context.Resolve<IUrlGenerator>(); + + return urlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion); + }); + + private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) => + { + var urlGenerator = context.Resolve<IUrlGenerator>(); + + return urlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type); + }); + + private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, IResolveFieldContext, GraphQLExecutionContext, T> resolver) + { + return Resolvers.Sync(resolver); + } + + private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, T> resolver) + { + return Resolvers.Sync(resolver); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs index 0b6a163639..721d49baf7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs @@ -11,37 +11,36 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets; + +internal sealed class AssetsResultGraphType : SharedObjectGraphType<IResultList<IAssetEntity>> { - internal sealed class AssetsResultGraphType : SharedObjectGraphType<IResultList<IAssetEntity>> + public AssetsResultGraphType(IGraphType assetsList) { - public AssetsResultGraphType(IGraphType assetsList) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "AssetResultDto"; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "AssetResultDto"; - AddField(new FieldType - { - Name = "total", - ResolvedType = Scalars.NonNullInt, - Resolver = ResolveList(x => x.Total), - Description = FieldDescriptions.AssetsTotal - }); + AddField(new FieldType + { + Name = "total", + ResolvedType = Scalars.NonNullInt, + Resolver = ResolveList(x => x.Total), + Description = FieldDescriptions.AssetsTotal + }); - AddField(new FieldType - { - Name = "items", - ResolvedType = new NonNullGraphType(assetsList), - Resolver = ResolveList(x => x), - Description = FieldDescriptions.AssetsItems - }); + AddField(new FieldType + { + Name = "items", + ResolvedType = new NonNullGraphType(assetsList), + Resolver = ResolveList(x => x), + Description = FieldDescriptions.AssetsItems + }); - Description = "List of assets and total count of assets."; - } + Description = "List of assets and total count of assets."; + } - private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedAssetEntity>, T> resolver) - { - return Resolvers.Sync(resolver); - } + private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedAssetEntity>, T> resolver) + { + return Resolvers.Sync(resolver); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/EnrichedAssetEventGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/EnrichedAssetEventGraphType.cs index b91334011a..28ca24d6a2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/EnrichedAssetEventGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/EnrichedAssetEventGraphType.cs @@ -13,251 +13,250 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets; + +internal sealed class EnrichedAssetEventGraphType : SharedObjectGraphType<EnrichedAssetEvent> { - internal sealed class EnrichedAssetEventGraphType : SharedObjectGraphType<EnrichedAssetEvent> + public EnrichedAssetEventGraphType() { - public EnrichedAssetEventGraphType() + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "EnrichedAssetEvent"; + + AddField(new FieldType + { + Name = "type", + ResolvedType = Scalars.EnrichedAssetEventType, + Resolver = Resolve(x => x.Type), + Description = FieldDescriptions.EventType + }); + + AddField(new FieldType + { + Name = "id", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.Id.ToString()), + Description = FieldDescriptions.EntityId + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = Scalars.NonNullInt, + Resolver = Resolve(x => x.Version), + Description = FieldDescriptions.EntityVersion + }); + + AddField(new FieldType + { + Name = "created", + ResolvedType = Scalars.NonNullDateTime, + Resolver = Resolve(x => x.Created.ToDateTimeUtc()), + Description = FieldDescriptions.EntityCreated + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.CreatedBy.ToString()), + Description = FieldDescriptions.EntityCreatedBy + }); + + AddField(new FieldType + { + Name = "createdByUser", + ResolvedType = UserGraphType.NonNull, + Resolver = Resolve(x => x.CreatedBy), + Description = FieldDescriptions.EntityCreatedBy + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = Scalars.NonNullDateTime, + Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()), + Description = FieldDescriptions.EntityLastModified + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Description = FieldDescriptions.EntityLastModifiedBy + }); + + AddField(new FieldType + { + Name = "lastModifiedByUser", + ResolvedType = UserGraphType.NonNull, + Resolver = Resolve(x => x.LastModifiedBy), + Description = FieldDescriptions.EntityLastModifiedBy + }); + + AddField(new FieldType + { + Name = "mimeType", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.MimeType), + Description = FieldDescriptions.AssetMimeType + }); + + AddField(new FieldType + { + Name = "url", + ResolvedType = Scalars.NonNullString, + Resolver = Url, + Description = FieldDescriptions.AssetUrl + }); + + AddField(new FieldType + { + Name = "thumbnailUrl", + ResolvedType = Scalars.String, + Resolver = ThumbnailUrl, + Description = FieldDescriptions.AssetThumbnailUrl + }); + + AddField(new FieldType { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "EnrichedAssetEvent"; - - AddField(new FieldType - { - Name = "type", - ResolvedType = Scalars.EnrichedAssetEventType, - Resolver = Resolve(x => x.Type), - Description = FieldDescriptions.EventType - }); - - AddField(new FieldType - { - Name = "id", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.Id.ToString()), - Description = FieldDescriptions.EntityId - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = Scalars.NonNullInt, - Resolver = Resolve(x => x.Version), - Description = FieldDescriptions.EntityVersion - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = Scalars.NonNullDateTime, - Resolver = Resolve(x => x.Created.ToDateTimeUtc()), - Description = FieldDescriptions.EntityCreated - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), - Description = FieldDescriptions.EntityCreatedBy - }); - - AddField(new FieldType - { - Name = "createdByUser", - ResolvedType = UserGraphType.NonNull, - Resolver = Resolve(x => x.CreatedBy), - Description = FieldDescriptions.EntityCreatedBy - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = Scalars.NonNullDateTime, - Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()), - Description = FieldDescriptions.EntityLastModified - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), - Description = FieldDescriptions.EntityLastModifiedBy - }); - - AddField(new FieldType - { - Name = "lastModifiedByUser", - ResolvedType = UserGraphType.NonNull, - Resolver = Resolve(x => x.LastModifiedBy), - Description = FieldDescriptions.EntityLastModifiedBy - }); - - AddField(new FieldType - { - Name = "mimeType", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.MimeType), - Description = FieldDescriptions.AssetMimeType - }); - - AddField(new FieldType - { - Name = "url", - ResolvedType = Scalars.NonNullString, - Resolver = Url, - Description = FieldDescriptions.AssetUrl - }); - - AddField(new FieldType - { - Name = "thumbnailUrl", - ResolvedType = Scalars.String, - Resolver = ThumbnailUrl, - Description = FieldDescriptions.AssetThumbnailUrl - }); - - AddField(new FieldType - { - Name = "fileName", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.FileName), - Description = FieldDescriptions.AssetFileName - }); - - AddField(new FieldType - { - Name = "fileHash", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.FileHash), - Description = FieldDescriptions.AssetFileHash - }); - - AddField(new FieldType - { - Name = "fileType", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.FileName.FileType()), - Description = FieldDescriptions.AssetFileType - }); - - AddField(new FieldType - { - Name = "fileSize", - ResolvedType = Scalars.NonNullInt, - Resolver = Resolve(x => x.FileSize), - Description = FieldDescriptions.AssetFileSize - }); - - AddField(new FieldType - { - Name = "fileVersion", - ResolvedType = Scalars.NonNullInt, - Resolver = Resolve(x => x.FileVersion), - Description = FieldDescriptions.AssetFileVersion - }); - - AddField(new FieldType - { - Name = "slug", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.Slug), - Description = FieldDescriptions.AssetSlug - }); - - AddField(new FieldType - { - Name = "isProtected", - ResolvedType = Scalars.NonNullBoolean, - Resolver = Resolve(x => x.IsProtected), - Description = FieldDescriptions.AssetIsProtected - }); - - AddField(new FieldType - { - Name = "isImage", - ResolvedType = Scalars.NonNullBoolean, - Resolver = Resolve(x => x.AssetType == AssetType.Image), - Description = FieldDescriptions.AssetIsImage, - DeprecationReason = "Use 'type' field instead." - }); - - AddField(new FieldType - { - Name = "assetType", - ResolvedType = Scalars.NonNullAssetType, - Resolver = Resolve(x => x.AssetType), - Description = FieldDescriptions.AssetType - }); - - AddField(new FieldType - { - Name = "pixelWidth", - ResolvedType = Scalars.Int, - Resolver = Resolve(x => x.Metadata.GetPixelWidth()), - Description = FieldDescriptions.AssetPixelWidth, - DeprecationReason = "Use 'metadata' field instead." - }); - - AddField(new FieldType - { - Name = "pixelHeight", - ResolvedType = Scalars.Int, - Resolver = Resolve(x => x.Metadata.GetPixelHeight()), - Description = FieldDescriptions.AssetPixelHeight, - DeprecationReason = "Use 'metadata' field instead." - }); - - AddField(new FieldType - { - Name = "metadata", - Arguments = AssetActions.Metadata.Arguments, - ResolvedType = Scalars.JsonNoop, - Resolver = AssetActions.Metadata.Resolver, - Description = FieldDescriptions.AssetMetadata - }); - - AddField(new FieldType - { - Name = "sourceUrl", - ResolvedType = Scalars.NonNullString, - Resolver = SourceUrl, - Description = FieldDescriptions.AssetSourceUrl - }); - - Description = "An asset event"; - } - - private static readonly IFieldResolver Url = Resolve((asset, _, context) => + Name = "fileName", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.FileName), + Description = FieldDescriptions.AssetFileName + }); + + AddField(new FieldType + { + Name = "fileHash", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.FileHash), + Description = FieldDescriptions.AssetFileHash + }); + + AddField(new FieldType { - var urlGenerator = context.Resolve<IUrlGenerator>(); + Name = "fileType", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.FileName.FileType()), + Description = FieldDescriptions.AssetFileType + }); - return urlGenerator.AssetContent(asset.AppId, asset.Id.ToString()); + AddField(new FieldType + { + Name = "fileSize", + ResolvedType = Scalars.NonNullInt, + Resolver = Resolve(x => x.FileSize), + Description = FieldDescriptions.AssetFileSize }); - private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) => + AddField(new FieldType { - var urlGenerator = context.Resolve<IUrlGenerator>(); + Name = "fileVersion", + ResolvedType = Scalars.NonNullInt, + Resolver = Resolve(x => x.FileVersion), + Description = FieldDescriptions.AssetFileVersion + }); - return urlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion); + AddField(new FieldType + { + Name = "slug", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.Slug), + Description = FieldDescriptions.AssetSlug }); - private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) => + AddField(new FieldType { - var urlGenerator = context.Resolve<IUrlGenerator>(); + Name = "isProtected", + ResolvedType = Scalars.NonNullBoolean, + Resolver = Resolve(x => x.IsProtected), + Description = FieldDescriptions.AssetIsProtected + }); + + AddField(new FieldType + { + Name = "isImage", + ResolvedType = Scalars.NonNullBoolean, + Resolver = Resolve(x => x.AssetType == AssetType.Image), + Description = FieldDescriptions.AssetIsImage, + DeprecationReason = "Use 'type' field instead." + }); + + AddField(new FieldType + { + Name = "assetType", + ResolvedType = Scalars.NonNullAssetType, + Resolver = Resolve(x => x.AssetType), + Description = FieldDescriptions.AssetType + }); + + AddField(new FieldType + { + Name = "pixelWidth", + ResolvedType = Scalars.Int, + Resolver = Resolve(x => x.Metadata.GetPixelWidth()), + Description = FieldDescriptions.AssetPixelWidth, + DeprecationReason = "Use 'metadata' field instead." + }); - return urlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.AssetType); + AddField(new FieldType + { + Name = "pixelHeight", + ResolvedType = Scalars.Int, + Resolver = Resolve(x => x.Metadata.GetPixelHeight()), + Description = FieldDescriptions.AssetPixelHeight, + DeprecationReason = "Use 'metadata' field instead." }); - private static IFieldResolver Resolve<T>(Func<EnrichedAssetEvent, IResolveFieldContext, GraphQLExecutionContext, T> resolver) + AddField(new FieldType { - return Resolvers.Sync(resolver); - } + Name = "metadata", + Arguments = AssetActions.Metadata.Arguments, + ResolvedType = Scalars.JsonNoop, + Resolver = AssetActions.Metadata.Resolver, + Description = FieldDescriptions.AssetMetadata + }); - private static IFieldResolver Resolve<T>(Func<EnrichedAssetEvent, T> resolver) + AddField(new FieldType { - return Resolvers.Sync(resolver); - } + Name = "sourceUrl", + ResolvedType = Scalars.NonNullString, + Resolver = SourceUrl, + Description = FieldDescriptions.AssetSourceUrl + }); + + Description = "An asset event"; + } + + private static readonly IFieldResolver Url = Resolve((asset, _, context) => + { + var urlGenerator = context.Resolve<IUrlGenerator>(); + + return urlGenerator.AssetContent(asset.AppId, asset.Id.ToString()); + }); + + private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) => + { + var urlGenerator = context.Resolve<IUrlGenerator>(); + + return urlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion); + }); + + private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) => + { + var urlGenerator = context.Resolve<IUrlGenerator>(); + + return urlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.AssetType); + }); + + private static IFieldResolver Resolve<T>(Func<EnrichedAssetEvent, IResolveFieldContext, GraphQLExecutionContext, T> resolver) + { + return Resolvers.Sync(resolver); + } + + private static IFieldResolver Resolve<T>(Func<EnrichedAssetEvent, T> resolver) + { + return Resolvers.Sync(resolver); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs index 87ba1f4048..c82b0953d6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs @@ -18,202 +18,201 @@ using Squidex.Infrastructure.Collections; using GraphQLSchema = GraphQL.Types.Schema; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - internal sealed class Builder - { - private readonly Dictionary<SchemaInfo, ComponentGraphType> componentTypes = new Dictionary<SchemaInfo, ComponentGraphType>(ReferenceEqualityComparer.Instance); - private readonly Dictionary<SchemaInfo, ContentGraphType> contentTypes = new Dictionary<SchemaInfo, ContentGraphType>(ReferenceEqualityComparer.Instance); - private readonly Dictionary<SchemaInfo, ContentResultGraphType> contentResultTypes = new Dictionary<SchemaInfo, ContentResultGraphType>(ReferenceEqualityComparer.Instance); - private readonly Dictionary<FieldInfo, EmbeddableStringGraphType> embeddableStringTypes = new Dictionary<FieldInfo, EmbeddableStringGraphType>(); - private readonly Dictionary<FieldInfo, ReferenceUnionGraphType> referenceUnionTypes = new Dictionary<FieldInfo, ReferenceUnionGraphType>(); - private readonly Dictionary<FieldInfo, ComponentUnionGraphType> componentUnionTypes = new Dictionary<FieldInfo, ComponentUnionGraphType>(); - private readonly Dictionary<FieldInfo, NestedGraphType> nestedTypes = new Dictionary<FieldInfo, NestedGraphType>(); - private readonly Dictionary<string, EnumerationGraphType?> enumTypes = new Dictionary<string, EnumerationGraphType?>(); - private readonly Dictionary<string, IGraphType[]> dynamicTypes = new Dictionary<string, IGraphType[]>(); - private readonly FieldVisitor fieldVisitor; - private readonly FieldInputVisitor fieldInputVisitor; - private readonly PartitionResolver partitionResolver; - private readonly HashSet<SchemaInfo> allSchemas = new HashSet<SchemaInfo>(); - private readonly ReservedNames typeNames = ReservedNames.ForTypes(); - private readonly GraphQLOptions options; - - static Builder() - { - ValueConverter.Register<string, DomainId>(DomainId.Create); - ValueConverter.Register<string, Status>(x => new Status(x)); - } - - public IInterfaceGraphType ContentInterface { get; } = new ContentInterfaceGraphType(); - - public IInterfaceGraphType ComponentInterface { get; } = new ComponentInterfaceGraphType(); - - public Builder(IAppEntity app, GraphQLOptions options) - { - partitionResolver = app.PartitionResolver(); +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; - fieldVisitor = new FieldVisitor(this); - fieldInputVisitor = new FieldInputVisitor(this); +internal sealed class Builder +{ + private readonly Dictionary<SchemaInfo, ComponentGraphType> componentTypes = new Dictionary<SchemaInfo, ComponentGraphType>(ReferenceEqualityComparer.Instance); + private readonly Dictionary<SchemaInfo, ContentGraphType> contentTypes = new Dictionary<SchemaInfo, ContentGraphType>(ReferenceEqualityComparer.Instance); + private readonly Dictionary<SchemaInfo, ContentResultGraphType> contentResultTypes = new Dictionary<SchemaInfo, ContentResultGraphType>(ReferenceEqualityComparer.Instance); + private readonly Dictionary<FieldInfo, EmbeddableStringGraphType> embeddableStringTypes = new Dictionary<FieldInfo, EmbeddableStringGraphType>(); + private readonly Dictionary<FieldInfo, ReferenceUnionGraphType> referenceUnionTypes = new Dictionary<FieldInfo, ReferenceUnionGraphType>(); + private readonly Dictionary<FieldInfo, ComponentUnionGraphType> componentUnionTypes = new Dictionary<FieldInfo, ComponentUnionGraphType>(); + private readonly Dictionary<FieldInfo, NestedGraphType> nestedTypes = new Dictionary<FieldInfo, NestedGraphType>(); + private readonly Dictionary<string, EnumerationGraphType?> enumTypes = new Dictionary<string, EnumerationGraphType?>(); + private readonly Dictionary<string, IGraphType[]> dynamicTypes = new Dictionary<string, IGraphType[]>(); + private readonly FieldVisitor fieldVisitor; + private readonly FieldInputVisitor fieldInputVisitor; + private readonly PartitionResolver partitionResolver; + private readonly HashSet<SchemaInfo> allSchemas = new HashSet<SchemaInfo>(); + private readonly ReservedNames typeNames = ReservedNames.ForTypes(); + private readonly GraphQLOptions options; + + static Builder() + { + ValueConverter.Register<string, DomainId>(DomainId.Create); + ValueConverter.Register<string, Status>(x => new Status(x)); + } - this.options = options; - } + public IInterfaceGraphType ContentInterface { get; } = new ContentInterfaceGraphType(); - public GraphQLSchema BuildSchema(IEnumerable<ISchemaEntity> schemas) - { - // Do not add schema without fields. - allSchemas.AddRange(SchemaInfo.Build(schemas, typeNames).Where(x => x.Fields.Count > 0)); + public IInterfaceGraphType ComponentInterface { get; } = new ComponentInterfaceGraphType(); - // Only published normal schemas (not components are used for entities). - var schemaInfos = allSchemas.Where(x => x.Schema.SchemaDef.IsPublished && x.Schema.SchemaDef.Type != SchemaType.Component).ToList(); - - foreach (var schemaInfo in schemaInfos) - { - var contentType = new ContentGraphType(schemaInfo); + public Builder(IAppEntity app, GraphQLOptions options) + { + partitionResolver = app.PartitionResolver(); - contentTypes[schemaInfo] = contentType; - contentResultTypes[schemaInfo] = new ContentResultGraphType(contentType, schemaInfo); - } + fieldVisitor = new FieldVisitor(this); + fieldInputVisitor = new FieldInputVisitor(this); - foreach (var schemaInfo in allSchemas) - { - var componentType = new ComponentGraphType(schemaInfo); + this.options = options; + } - componentTypes[schemaInfo] = componentType; - } + public GraphQLSchema BuildSchema(IEnumerable<ISchemaEntity> schemas) + { + // Do not add schema without fields. + allSchemas.AddRange(SchemaInfo.Build(schemas, typeNames).Where(x => x.Fields.Count > 0)); - var newSchema = new GraphQLSchema - { - Query = new ApplicationQueries(this, schemaInfos) - }; + // Only published normal schemas (not components are used for entities). + var schemaInfos = allSchemas.Where(x => x.Schema.SchemaDef.IsPublished && x.Schema.SchemaDef.Type != SchemaType.Component).ToList(); - newSchema.RegisterType(ComponentInterface); - newSchema.RegisterType(ContentInterface); + foreach (var schemaInfo in schemaInfos) + { + var contentType = new ContentGraphType(schemaInfo); - newSchema.Directives.Register(SharedTypes.MemoryCacheDirective); + contentTypes[schemaInfo] = contentType; + contentResultTypes[schemaInfo] = new ContentResultGraphType(contentType, schemaInfo); + } - if (schemaInfos.Any()) - { - var mutations = new ApplicationMutations(this, schemaInfos); + foreach (var schemaInfo in allSchemas) + { + var componentType = new ComponentGraphType(schemaInfo); - if (mutations.Fields.Count > 0) - { - newSchema.Mutation = mutations; - } - } + componentTypes[schemaInfo] = componentType; + } - if (options.EnableSubscriptions) - { - newSchema.Subscription = new ApplicationSubscriptions(); - } + var newSchema = new GraphQLSchema + { + Query = new ApplicationQueries(this, schemaInfos) + }; - foreach (var (schemaInfo, contentType) in contentTypes) - { - contentType.Initialize(this, schemaInfo, schemaInfos); - } + newSchema.RegisterType(ComponentInterface); + newSchema.RegisterType(ContentInterface); - foreach (var (schemaInfo, componentType) in componentTypes) - { - componentType.Initialize(this, schemaInfo); - } + newSchema.Directives.Register(SharedTypes.MemoryCacheDirective); - foreach (var contentType in contentTypes.Values) - { - newSchema.RegisterType(contentType); - } + if (schemaInfos.Any()) + { + var mutations = new ApplicationMutations(this, schemaInfos); - foreach (var customType in dynamicTypes.SelectMany(x => x.Value)) + if (mutations.Fields.Count > 0) { - newSchema.RegisterType(customType); + newSchema.Mutation = mutations; } - - newSchema.RegisterVisitor(ErrorVisitor.Instance); - newSchema.Initialize(); - - return newSchema; } - public FieldGraphSchema GetGraphType(FieldInfo fieldInfo) + if (options.EnableSubscriptions) { - return fieldInfo.Field.Accept(fieldVisitor, fieldInfo); + newSchema.Subscription = new ApplicationSubscriptions(); } - public IFieldPartitioning ResolvePartition(Partitioning key) + foreach (var (schemaInfo, contentType) in contentTypes) { - return partitionResolver(key); + contentType.Initialize(this, schemaInfo, schemaInfos); } - public IGraphType? GetInputGraphType(FieldInfo fieldInfo) + foreach (var (schemaInfo, componentType) in componentTypes) { - return fieldInfo.Field.Accept(fieldInputVisitor, fieldInfo); + componentType.Initialize(this, schemaInfo); } - public IObjectGraphType GetContentResultType(SchemaInfo schemaId) + foreach (var contentType in contentTypes.Values) { - return contentResultTypes.GetValueOrDefault(schemaId)!; + newSchema.RegisterType(contentType); } - public IObjectGraphType? GetContentType(DomainId schemaId) + foreach (var customType in dynamicTypes.SelectMany(x => x.Value)) { - return contentTypes.FirstOrDefault(x => x.Key.Schema.Id == schemaId).Value; + newSchema.RegisterType(customType); } - public IObjectGraphType GetContentType(SchemaInfo schemaId) - { - return contentTypes.GetValueOrDefault(schemaId)!; - } + newSchema.RegisterVisitor(ErrorVisitor.Instance); + newSchema.Initialize(); - public IObjectGraphType? GetComponentType(DomainId schemaId) - { - var schema = allSchemas.FirstOrDefault(x => x.Schema.Id == schemaId); + return newSchema; + } - if (schema == null) - { - return null; - } + public FieldGraphSchema GetGraphType(FieldInfo fieldInfo) + { + return fieldInfo.Field.Accept(fieldVisitor, fieldInfo); + } - return componentTypes.GetValueOrDefault(schema); - } + public IFieldPartitioning ResolvePartition(Partitioning key) + { + return partitionResolver(key); + } - public IGraphType[] GetDynamicTypes(string? schema) - { - var graphQLSchema = schema; + public IGraphType? GetInputGraphType(FieldInfo fieldInfo) + { + return fieldInfo.Field.Accept(fieldInputVisitor, fieldInfo); + } - if (string.IsNullOrWhiteSpace(graphQLSchema)) - { - return Array.Empty<GraphType>(); - } + public IObjectGraphType GetContentResultType(SchemaInfo schemaId) + { + return contentResultTypes.GetValueOrDefault(schemaId)!; + } - return dynamicTypes.GetOrAdd(graphQLSchema, x => DynamicSchemaBuilder.ParseTypes(x, typeNames)); - } + public IObjectGraphType? GetContentType(DomainId schemaId) + { + return contentTypes.FirstOrDefault(x => x.Key.Schema.Id == schemaId).Value; + } - public EmbeddableStringGraphType GetEmbeddableString(FieldInfo fieldInfo, StringFieldProperties properties) - { - return embeddableStringTypes.GetOrAdd(fieldInfo, x => new EmbeddableStringGraphType(this, x, properties)); - } + public IObjectGraphType GetContentType(SchemaInfo schemaId) + { + return contentTypes.GetValueOrDefault(schemaId)!; + } - public EnumerationGraphType? GetEnumeration(string name, IEnumerable<string> values) - { - return enumTypes.GetOrAdd(name, x => FieldEnumType.TryCreate(name, values)); - } + public IObjectGraphType? GetComponentType(DomainId schemaId) + { + var schema = allSchemas.FirstOrDefault(x => x.Schema.Id == schemaId); - public ReferenceUnionGraphType GetReferenceUnion(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + if (schema == null) { - return referenceUnionTypes.GetOrAdd(fieldInfo, x => new ReferenceUnionGraphType(this, x, schemaIds)); + return null; } - public ComponentUnionGraphType GetComponentUnion(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) - { - return componentUnionTypes.GetOrAdd(fieldInfo, x => new ComponentUnionGraphType(this, x, schemaIds)); - } + return componentTypes.GetValueOrDefault(schema); + } - public NestedGraphType GetNested(FieldInfo fieldInfo) - { - return nestedTypes.GetOrAdd(fieldInfo, x => new NestedGraphType(this, x)); - } + public IGraphType[] GetDynamicTypes(string? schema) + { + var graphQLSchema = schema; - public IEnumerable<KeyValuePair<SchemaInfo, ContentGraphType>> GetAllContentTypes() + if (string.IsNullOrWhiteSpace(graphQLSchema)) { - return contentTypes; + return Array.Empty<GraphType>(); } + + return dynamicTypes.GetOrAdd(graphQLSchema, x => DynamicSchemaBuilder.ParseTypes(x, typeNames)); + } + + public EmbeddableStringGraphType GetEmbeddableString(FieldInfo fieldInfo, StringFieldProperties properties) + { + return embeddableStringTypes.GetOrAdd(fieldInfo, x => new EmbeddableStringGraphType(this, x, properties)); + } + + public EnumerationGraphType? GetEnumeration(string name, IEnumerable<string> values) + { + return enumTypes.GetOrAdd(name, x => FieldEnumType.TryCreate(name, values)); + } + + public ReferenceUnionGraphType GetReferenceUnion(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + { + return referenceUnionTypes.GetOrAdd(fieldInfo, x => new ReferenceUnionGraphType(this, x, schemaIds)); + } + + public ComponentUnionGraphType GetComponentUnion(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + { + return componentUnionTypes.GetOrAdd(fieldInfo, x => new ComponentUnionGraphType(this, x, schemaIds)); + } + + public NestedGraphType GetNested(FieldInfo fieldInfo) + { + return nestedTypes.GetOrAdd(fieldInfo, x => new NestedGraphType(this, x)); + } + + public IEnumerable<KeyValuePair<SchemaInfo, ContentGraphType>> GetAllContentTypes() + { + return contentTypes; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs index f23bd38060..e8c063c597 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs @@ -10,67 +10,66 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class ComponentGraphType : ObjectGraphType<JsonObject> { - internal sealed class ComponentGraphType : ObjectGraphType<JsonObject> + public ComponentGraphType(SchemaInfo schemaInfo) { - public ComponentGraphType(SchemaInfo schemaInfo) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = schemaInfo.ComponentType; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = schemaInfo.ComponentType; - IsTypeOf = CheckType(schemaInfo.Schema.Id.ToString()); - } + IsTypeOf = CheckType(schemaInfo.Schema.Id.ToString()); + } - public void Initialize(Builder builder, SchemaInfo schemaInfo) - { - Description = $"The structure of the {schemaInfo.DisplayName} component schema."; + public void Initialize(Builder builder, SchemaInfo schemaInfo) + { + Description = $"The structure of the {schemaInfo.DisplayName} component schema."; - AddField(ContentFields.SchemaId); + AddField(ContentFields.SchemaId); - foreach (var fieldInfo in schemaInfo.Fields) + foreach (var fieldInfo in schemaInfo.Fields) + { + if (fieldInfo.Field.IsComponentLike()) { - if (fieldInfo.Field.IsComponentLike()) + AddField(new FieldType { - AddField(new FieldType - { - Name = fieldInfo.FieldNameDynamic, - Arguments = ContentActions.Json.Arguments, - ResolvedType = Scalars.Json, - Resolver = FieldVisitor.JsonPath, - Description = fieldInfo.Field.RawProperties.Hints - }).WithSourceName(fieldInfo); - } + Name = fieldInfo.FieldNameDynamic, + Arguments = ContentActions.Json.Arguments, + ResolvedType = Scalars.Json, + Resolver = FieldVisitor.JsonPath, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(fieldInfo); + } - var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); + var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); - if (resolvedType != null && resolver != null) + if (resolvedType != null && resolver != null) + { + AddField(new FieldType { - AddField(new FieldType - { - Name = fieldInfo.FieldName, - Arguments = args, - ResolvedType = resolvedType, - Resolver = resolver, - Description = fieldInfo.Field.RawProperties.Hints - }).WithSourceName(fieldInfo); - } + Name = fieldInfo.FieldName, + Arguments = args, + ResolvedType = resolvedType, + Resolver = resolver, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(fieldInfo); } - - AddResolvedInterface(builder.ComponentInterface); } - private static Func<object, bool> CheckType(string schemaId) + AddResolvedInterface(builder.ComponentInterface); + } + + private static Func<object, bool> CheckType(string schemaId) + { + return value => { - return value => + if (value is not JsonObject json) { - if (value is not JsonObject json) - { - return false; - } + return false; + } - return Component.IsValid(json, out var discriminator) && discriminator == schemaId; - }; - } + return Component.IsValid(json, out var discriminator) && discriminator == schemaId; + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs index cf3b9c51b0..cdffd92d3d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs @@ -8,18 +8,17 @@ using GraphQL.Types; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class ComponentInterfaceGraphType : InterfaceGraphType<JsonObject> { - internal sealed class ComponentInterfaceGraphType : InterfaceGraphType<JsonObject> + public ComponentInterfaceGraphType() { - public ComponentInterfaceGraphType() - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "Component"; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "Component"; - AddField(ContentFields.SchemaId); + AddField(ContentFields.SchemaId); - Description = "The structure of all content types."; - } + Description = "The structure of all content types."; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs index d3a34d80e7..e40824f002 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs @@ -11,49 +11,48 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class ComponentUnionGraphType : UnionGraphType { - internal sealed class ComponentUnionGraphType : UnionGraphType - { - private readonly Dictionary<string, IObjectGraphType> types = new Dictionary<string, IObjectGraphType>(); + private readonly Dictionary<string, IObjectGraphType> types = new Dictionary<string, IObjectGraphType>(); - public bool HasType => types.Count > 0; + public bool HasType => types.Count > 0; - public ComponentUnionGraphType(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.UnionComponentType; + public ComponentUnionGraphType(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + { + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = fieldInfo.UnionComponentType; - if (schemaIds?.Any() == true) + if (schemaIds?.Any() == true) + { + foreach (var schemaId in schemaIds) { - foreach (var schemaId in schemaIds) - { - var contentType = builder.GetComponentType(schemaId); + var contentType = builder.GetComponentType(schemaId); - if (contentType != null) - { - types[schemaId.ToString()] = contentType; - } + if (contentType != null) + { + types[schemaId.ToString()] = contentType; } } + } - if (HasType) + if (HasType) + { + foreach (var type in types) { - foreach (var type in types) - { - AddPossibleType(type.Value); - } + AddPossibleType(type.Value); + } - ResolveType = value => + ResolveType = value => + { + if (value is JsonObject json && Component.IsValid(json, out var schemaId)) { - if (value is JsonObject json && Component.IsValid(json, out var schemaId)) - { - return types.GetValueOrDefault(schemaId); - } + return types.GetValueOrDefault(schemaId); + } - return null; - }; - } + return null; + }; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index 6b76a5cebc..b8a0342847 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -17,38 +17,13 @@ using Squidex.Infrastructure; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal static class ContentActions { - internal static class ContentActions + public static class Json { - public static class Json - { - public static readonly QueryArguments Arguments = new QueryArguments - { - new QueryArgument(Scalars.String) - { - Name = "path", - Description = FieldDescriptions.JsonPath, - DefaultValue = null - } - }; - - public static readonly ValueResolver<object> Resolver = (value, fieldContext, context) => - { - if (fieldContext.Arguments != null && - fieldContext.Arguments.TryGetValue("path", out var contextValue) && - contextValue.Value is string path) - { - value.TryGetByPath(path, out var result); - - return result!; - } - - return value; - }; - } - - public static readonly QueryArguments JsonPath = new QueryArguments + public static readonly QueryArguments Arguments = new QueryArguments { new QueryArgument(Scalars.String) { @@ -58,345 +33,227 @@ public static class Json } }; - public static class Find + public static readonly ValueResolver<object> Resolver = (value, fieldContext, context) => { - public static readonly QueryArguments Arguments = new QueryArguments + if (fieldContext.Arguments != null && + fieldContext.Arguments.TryGetValue("path", out var contextValue) && + contextValue.Value is string path) { - new QueryArgument(Scalars.NonNullString) - { - Name = "id", - Description = FieldDescriptions.EntityId, - DefaultValue = null - }, - new QueryArgument(Scalars.Int) - { - Name = "version", - Description = FieldDescriptions.QueryVersion, - DefaultValue = null - } - }; + value.TryGetByPath(path, out var result); - public static readonly IFieldResolver Resolver = Resolvers.Async<object, object?>(async (_, fieldContext, context) => - { - var contentId = fieldContext.GetArgument<DomainId>("id"); - var contentSchema = fieldContext.FieldDefinition.SchemaId(); + return result!; + } - var version = fieldContext.GetArgument<int?>("version"); + return value; + }; + } - if (version >= 0) - { - return await context.FindContentAsync(contentSchema, contentId, version.Value, - fieldContext.CancellationToken); - } - else - { - return await context.FindContentAsync(DomainId.Create(contentSchema), contentId, - fieldContext.CancellationToken); - } - }); + public static readonly QueryArguments JsonPath = new QueryArguments + { + new QueryArgument(Scalars.String) + { + Name = "path", + Description = FieldDescriptions.JsonPath, + DefaultValue = null } + }; - public static class QueryOrReferencing + public static class Find + { + public static readonly QueryArguments Arguments = new QueryArguments { - public static readonly QueryArguments Arguments = new QueryArguments + new QueryArgument(Scalars.NonNullString) { - new QueryArgument(Scalars.Int) - { - Name = "top", - Description = FieldDescriptions.QueryTop, - DefaultValue = null - }, - new QueryArgument(Scalars.Int) - { - Name = "skip", - Description = FieldDescriptions.QuerySkip, - DefaultValue = 0 - }, - new QueryArgument(Scalars.String) - { - Name = "filter", - Description = FieldDescriptions.QueryFilter, - DefaultValue = null - }, - new QueryArgument(Scalars.String) - { - Name = "orderby", - Description = FieldDescriptions.QueryOrderBy, - DefaultValue = null - }, - new QueryArgument(Scalars.String) - { - Name = "search", - Description = FieldDescriptions.QuerySearch, - DefaultValue = null - } - }; - - public static readonly IFieldResolver Query = Resolvers.Async<object, object>(async (_, fieldContext, context) => + Name = "id", + Description = FieldDescriptions.EntityId, + DefaultValue = null + }, + new QueryArgument(Scalars.Int) { - var query = fieldContext.BuildODataQuery(); + Name = "version", + Description = FieldDescriptions.QueryVersion, + DefaultValue = null + } + }; - var q = Q.Empty.WithODataQuery(query).WithoutTotal(); + public static readonly IFieldResolver Resolver = Resolvers.Async<object, object?>(async (_, fieldContext, context) => + { + var contentId = fieldContext.GetArgument<DomainId>("id"); + var contentSchema = fieldContext.FieldDefinition.SchemaId(); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, - fieldContext.CancellationToken); - }); + var version = fieldContext.GetArgument<int?>("version"); - public static readonly IFieldResolver QueryWithTotal = Resolvers.Async<object, object>(async (_, fieldContext, context) => + if (version >= 0) { - var query = fieldContext.BuildODataQuery(); - - var q = Q.Empty.WithODataQuery(query); - - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + return await context.FindContentAsync(contentSchema, contentId, version.Value, + fieldContext.CancellationToken); + } + else + { + return await context.FindContentAsync(DomainId.Create(contentSchema), contentId, fieldContext.CancellationToken); - }); + } + }); + } - public static readonly IFieldResolver Referencing = Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) => + public static class QueryOrReferencing + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(Scalars.Int) + { + Name = "top", + Description = FieldDescriptions.QueryTop, + DefaultValue = null + }, + new QueryArgument(Scalars.Int) { - var query = fieldContext.BuildODataQuery(); + Name = "skip", + Description = FieldDescriptions.QuerySkip, + DefaultValue = 0 + }, + new QueryArgument(Scalars.String) + { + Name = "filter", + Description = FieldDescriptions.QueryFilter, + DefaultValue = null + }, + new QueryArgument(Scalars.String) + { + Name = "orderby", + Description = FieldDescriptions.QueryOrderBy, + DefaultValue = null + }, + new QueryArgument(Scalars.String) + { + Name = "search", + Description = FieldDescriptions.QuerySearch, + DefaultValue = null + } + }; - var q = Q.Empty.WithODataQuery(query).WithReference(source.Id).WithoutTotal(); + public static readonly IFieldResolver Query = Resolvers.Async<object, object>(async (_, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, - fieldContext.CancellationToken); - }); + var q = Q.Empty.WithODataQuery(query).WithoutTotal(); - public static readonly IFieldResolver ReferencingWithTotal = Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) => - { - var query = fieldContext.BuildODataQuery(); + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); + }); - var q = Q.Empty.WithODataQuery(query).WithReference(source.Id); + public static readonly IFieldResolver QueryWithTotal = Resolvers.Async<object, object>(async (_, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, - fieldContext.CancellationToken); - }); + var q = Q.Empty.WithODataQuery(query); - public static readonly IFieldResolver References = Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) => - { - var query = fieldContext.BuildODataQuery(); + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); + }); - var q = Q.Empty.WithODataQuery(query).WithReferencing(source.Id).WithoutTotal(); + public static readonly IFieldResolver Referencing = Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, - fieldContext.CancellationToken); - }); + var q = Q.Empty.WithODataQuery(query).WithReference(source.Id).WithoutTotal(); - public static readonly IFieldResolver ReferencesWithTotal = Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) => - { - var query = fieldContext.BuildODataQuery(); + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); + }); - var q = Q.Empty.WithODataQuery(query).WithReferencing(source.Id); + public static readonly IFieldResolver ReferencingWithTotal = Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, - fieldContext.CancellationToken); - }); - } + var q = Q.Empty.WithODataQuery(query).WithReference(source.Id); + + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); + }); - public static class Create + public static readonly IFieldResolver References = Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) => { - public static QueryArguments Arguments(IGraphType inputType) - { - return new QueryArguments - { - new QueryArgument(new NonNullGraphType(inputType)) - { - Name = "data", - Description = FieldDescriptions.ContentRequestData, - DefaultValue = null - }, - new QueryArgument(Scalars.Boolean) - { - Name = "publish", - Description = FieldDescriptions.ContentRequestPublish, - DefaultValue = false - }, - new QueryArgument(Scalars.String) - { - Name = "status", - Description = FieldDescriptions.ContentRequestOptionalStatus, - DefaultValue = null - }, - new QueryArgument(Scalars.String) - { - Name = "id", - Description = FieldDescriptions.ContentRequestOptionalId, - DefaultValue = null - } - }; - } + var query = fieldContext.BuildODataQuery(); - public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsCreate, c => - { - var command = new CreateContent - { - // The data is converted from input args. - Data = c.GetArgument<ContentData>("data") - }; + var q = Q.Empty.WithODataQuery(query).WithReferencing(source.Id).WithoutTotal(); - var status = c.GetArgument<string?>("status"); + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); + }); - if (!string.IsNullOrWhiteSpace(status)) - { - command.Status = new Status(status); - } - else if (c.GetArgument<bool>("publish")) - { - command.Status = Status.Published; - } + public static readonly IFieldResolver ReferencesWithTotal = Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); - return command; - }); - } + var q = Q.Empty.WithODataQuery(query).WithReferencing(source.Id); + + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); + }); + } - public static class Upsert + public static class Create + { + public static QueryArguments Arguments(IGraphType inputType) { - public static QueryArguments Arguments(IGraphType inputType) + return new QueryArguments { - return new QueryArguments + new QueryArgument(new NonNullGraphType(inputType)) { - new QueryArgument(Scalars.NonNullString) - { - Name = "id", - Description = FieldDescriptions.EntityId, - DefaultValue = null - }, - new QueryArgument(new NonNullGraphType(inputType)) - { - Name = "data", - Description = FieldDescriptions.ContentRequestData, - DefaultValue = null - }, - new QueryArgument(Scalars.Boolean) - { - Name = "publish", - Description = FieldDescriptions.ContentRequestPublish, - DefaultValue = false - }, - new QueryArgument(Scalars.Boolean) - { - Name = "patch", - Description = FieldDescriptions.ContentRequestPatch, - DefaultValue = false - }, - new QueryArgument(Scalars.String) - { - Name = "status", - Description = FieldDescriptions.ContentRequestOptionalStatus, - DefaultValue = null - }, - new QueryArgument(Scalars.Int) - { - Name = "expectedVersion", - Description = FieldDescriptions.EntityExpectedVersion, - DefaultValue = EtagVersion.Any - } - }; - } - - public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsUpsert, c => - { - var command = new UpsertContent + Name = "data", + Description = FieldDescriptions.ContentRequestData, + DefaultValue = null + }, + new QueryArgument(Scalars.Boolean) { - // The data is converted from input args. - Data = c.GetArgument<ContentData>("data"), - - // True, to make a path, if the content exits. - Patch = c.GetArgument<bool>("patch"), - }; - - var status = c.GetArgument<string?>("status"); - - if (!string.IsNullOrWhiteSpace(status)) + Name = "publish", + Description = FieldDescriptions.ContentRequestPublish, + DefaultValue = false + }, + new QueryArgument(Scalars.String) { - command.Status = new Status(status); - } - else if (c.GetArgument<bool>("publish")) + Name = "status", + Description = FieldDescriptions.ContentRequestOptionalStatus, + DefaultValue = null + }, + new QueryArgument(Scalars.String) { - command.Status = Status.Published; + Name = "id", + Description = FieldDescriptions.ContentRequestOptionalId, + DefaultValue = null } - - return command; - }); + }; } - public static class Update + public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsCreate, c => { - public static QueryArguments Arguments(IGraphType inputType) + var command = new CreateContent { - return new QueryArguments - { - new QueryArgument(Scalars.String) - { - Name = "id", - Description = FieldDescriptions.EntityId, - DefaultValue = null - }, - new QueryArgument(new NonNullGraphType(inputType)) - { - Name = "data", - Description = FieldDescriptions.ContentRequestData, - DefaultValue = null - }, - new QueryArgument(Scalars.Int) - { - Name = "expectedVersion", - Description = FieldDescriptions.EntityExpectedVersion, - DefaultValue = EtagVersion.Any - } - }; - } + // The data is converted from input args. + Data = c.GetArgument<ContentData>("data") + }; - public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsUpdateOwn, c => - { - return new PatchContent - { - // The data is converted from input args. - Data = c.GetArgument<ContentData>("data")! - }; - }); - } + var status = c.GetArgument<string?>("status"); - public static class Patch - { - public static QueryArguments Arguments(IGraphType inputType) + if (!string.IsNullOrWhiteSpace(status)) { - return new QueryArguments - { - new QueryArgument(Scalars.String) - { - Name = "id", - Description = FieldDescriptions.EntityId, - DefaultValue = null - }, - new QueryArgument(new NonNullGraphType(inputType)) - { - Name = "data", - Description = FieldDescriptions.ContentRequestData, - DefaultValue = null - }, - new QueryArgument(Scalars.Int) - { - Name = "expectedVersion", - Description = FieldDescriptions.EntityExpectedVersion, - DefaultValue = EtagVersion.Any - } - }; + command.Status = new Status(status); } - - public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsUpdateOwn, c => + else if (c.GetArgument<bool>("publish")) { - return new PatchContent - { - // The data is converted from input args. - Data = c.GetArgument<ContentData>("data")! - }; - }); - } + command.Status = Status.Published; + } + + return command; + }); + } - public static class ChangeStatus + public static class Upsert + { + public static QueryArguments Arguments(IGraphType inputType) { - public static readonly QueryArguments Arguments = new QueryArguments + return new QueryArguments { new QueryArgument(Scalars.NonNullString) { @@ -404,16 +261,28 @@ public static class ChangeStatus Description = FieldDescriptions.EntityId, DefaultValue = null }, - new QueryArgument(Scalars.NonNullString) + new QueryArgument(new NonNullGraphType(inputType)) { - Name = "status", - Description = FieldDescriptions.ContentRequestStatus, + Name = "data", + Description = FieldDescriptions.ContentRequestData, DefaultValue = null }, - new QueryArgument(Scalars.DateTime) + new QueryArgument(Scalars.Boolean) + { + Name = "publish", + Description = FieldDescriptions.ContentRequestPublish, + DefaultValue = false + }, + new QueryArgument(Scalars.Boolean) + { + Name = "patch", + Description = FieldDescriptions.ContentRequestPatch, + DefaultValue = false + }, + new QueryArgument(Scalars.String) { - Name = "dueTime", - Description = FieldDescriptions.ContentRequestDueTime, + Name = "status", + Description = FieldDescriptions.ContentRequestOptionalStatus, DefaultValue = null }, new QueryArgument(Scalars.Int) @@ -423,28 +292,50 @@ public static class ChangeStatus DefaultValue = EtagVersion.Any } }; + } - public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsChangeStatusOwn, c => + public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsUpsert, c => + { + var command = new UpsertContent { - return new ChangeContentStatus - { - // Main parameter to set the status. - Status = c.GetArgument<Status>("status"), + // The data is converted from input args. + Data = c.GetArgument<ContentData>("data"), - // This is an optional field to delay the status change. - DueTime = c.GetArgument<Instant?>("dueTime"), - }; - }); - } + // True, to make a path, if the content exits. + Patch = c.GetArgument<bool>("patch"), + }; + + var status = c.GetArgument<string?>("status"); - public static class Delete + if (!string.IsNullOrWhiteSpace(status)) + { + command.Status = new Status(status); + } + else if (c.GetArgument<bool>("publish")) + { + command.Status = Status.Published; + } + + return command; + }); + } + + public static class Update + { + public static QueryArguments Arguments(IGraphType inputType) { - public static readonly QueryArguments Arguments = new QueryArguments + return new QueryArguments { - new QueryArgument(Scalars.NonNullString) + new QueryArgument(Scalars.String) { Name = "id", - Description = "The ID of the content (usually GUID).", + Description = FieldDescriptions.EntityId, + DefaultValue = null + }, + new QueryArgument(new NonNullGraphType(inputType)) + { + Name = "data", + Description = FieldDescriptions.ContentRequestData, DefaultValue = null }, new QueryArgument(Scalars.Int) @@ -454,60 +345,168 @@ public static class Delete DefaultValue = EtagVersion.Any } }; + } - public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsDeleteOwn, c => + public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsUpdateOwn, c => + { + return new PatchContent { - return new DeleteContent(); - }); - } + // The data is converted from input args. + Data = c.GetArgument<ContentData>("data")! + }; + }); + } - public static class Subscription + public static class Patch + { + public static QueryArguments Arguments(IGraphType inputType) { - public static readonly QueryArguments Arguments = new QueryArguments + return new QueryArguments { - new QueryArgument(Scalars.EnrichedContentEventType) + new QueryArgument(Scalars.String) { - Name = "type", - Description = FieldDescriptions.EventType, + Name = "id", + Description = FieldDescriptions.EntityId, DefaultValue = null }, - new QueryArgument(Scalars.String) + new QueryArgument(new NonNullGraphType(inputType)) { - Name = "schemaName", - Description = FieldDescriptions.ContentSchemaName, + Name = "data", + Description = FieldDescriptions.ContentRequestData, DefaultValue = null + }, + new QueryArgument(Scalars.Int) + { + Name = "expectedVersion", + Description = FieldDescriptions.EntityExpectedVersion, + DefaultValue = EtagVersion.Any } }; + } - public static readonly ISourceStreamResolver Resolver = Resolvers.Stream(PermissionIds.AppContentsRead, c => + public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsUpdateOwn, c => + { + return new PatchContent { - return new ContentSubscription - { - // Primary filter for the event types. - Type = c.GetArgument<EnrichedContentEventType?>("type"), + // The data is converted from input args. + Data = c.GetArgument<ContentData>("data")! + }; + }); + } - // The name of the schema is used instead of the ID for a simpler API. - SchemaName = c.GetArgument<string?>("schemaName") - }; - }); - } + public static class ChangeStatus + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(Scalars.NonNullString) + { + Name = "id", + Description = FieldDescriptions.EntityId, + DefaultValue = null + }, + new QueryArgument(Scalars.NonNullString) + { + Name = "status", + Description = FieldDescriptions.ContentRequestStatus, + DefaultValue = null + }, + new QueryArgument(Scalars.DateTime) + { + Name = "dueTime", + Description = FieldDescriptions.ContentRequestDueTime, + DefaultValue = null + }, + new QueryArgument(Scalars.Int) + { + Name = "expectedVersion", + Description = FieldDescriptions.EntityExpectedVersion, + DefaultValue = EtagVersion.Any + } + }; - private static IFieldResolver ContentCommand(string permissionId, Func<IResolveFieldContext, ContentCommand> creator) + public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsChangeStatusOwn, c => { - return Resolvers.Command(permissionId, c => + return new ChangeContentStatus { - var command = creator(c); + // Main parameter to set the status. + Status = c.GetArgument<Status>("status"), - var contentId = c.GetArgument<string?>("id"); + // This is an optional field to delay the status change. + DueTime = c.GetArgument<Instant?>("dueTime"), + }; + }); + } - if (!string.IsNullOrWhiteSpace(contentId)) - { - // Same parameter for all commands. - command.ContentId = DomainId.Create(contentId); - } + public static class Delete + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(Scalars.NonNullString) + { + Name = "id", + Description = "The ID of the content (usually GUID).", + DefaultValue = null + }, + new QueryArgument(Scalars.Int) + { + Name = "expectedVersion", + Description = FieldDescriptions.EntityExpectedVersion, + DefaultValue = EtagVersion.Any + } + }; - return command; - }); - } + public static readonly IFieldResolver Resolver = ContentCommand(PermissionIds.AppContentsDeleteOwn, c => + { + return new DeleteContent(); + }); + } + + public static class Subscription + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(Scalars.EnrichedContentEventType) + { + Name = "type", + Description = FieldDescriptions.EventType, + DefaultValue = null + }, + new QueryArgument(Scalars.String) + { + Name = "schemaName", + Description = FieldDescriptions.ContentSchemaName, + DefaultValue = null + } + }; + + public static readonly ISourceStreamResolver Resolver = Resolvers.Stream(PermissionIds.AppContentsRead, c => + { + return new ContentSubscription + { + // Primary filter for the event types. + Type = c.GetArgument<EnrichedContentEventType?>("type"), + + // The name of the schema is used instead of the ID for a simpler API. + SchemaName = c.GetArgument<string?>("schemaName") + }; + }); + } + + private static IFieldResolver ContentCommand(string permissionId, Func<IResolveFieldContext, ContentCommand> creator) + { + return Resolvers.Command(permissionId, c => + { + var command = creator(c); + + var contentId = c.GetArgument<string?>("id"); + + if (!string.IsNullOrWhiteSpace(contentId)) + { + // Same parameter for all commands. + command.ContentId = DomainId.Create(contentId); + } + + return command; + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs index 42b8a8ddc4..72725f4128 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs @@ -13,172 +13,171 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal static class ContentFields { - internal static class ContentFields - { - public static readonly IFieldResolver ResolveStringFieldAssets = Resolvers.Async<string, object>(async (value, fieldContext, context) => - { - var cacheDuration = fieldContext.CacheDuration(); - - var ids = context.Resolve<StringReferenceExtractor>().GetEmbeddedAssetIds(value).ToList(); - - return await context.GetAssetsAsync(ids, cacheDuration, fieldContext.CancellationToken); - }); - - public static readonly IFieldResolver ResolveStringFieldContents = Resolvers.Async<string, object>(async (value, fieldContext, context) => - { - var cacheDuration = fieldContext.CacheDuration(); - - var ids = context.Resolve<StringReferenceExtractor>().GetEmbeddedContentIds(value).ToList(); - - return await context.GetContentsAsync(ids, cacheDuration, fieldContext.CancellationToken); - }); - - public static readonly FieldType Id = new FieldType - { - Name = "id", - ResolvedType = Scalars.NonNullString, - Resolver = EntityResolvers.Id, - Description = FieldDescriptions.EntityId - }; - - public static readonly FieldType Version = new FieldType - { - Name = "version", - ResolvedType = Scalars.NonNullInt, - Resolver = EntityResolvers.Version, - Description = FieldDescriptions.EntityVersion - }; - - public static readonly FieldType Created = new FieldType - { - Name = "created", - ResolvedType = Scalars.NonNullDateTime, - Resolver = EntityResolvers.Created, - Description = FieldDescriptions.EntityCreated - }; - - public static readonly FieldType CreatedBy = new FieldType - { - Name = "createdBy", - ResolvedType = Scalars.NonNullString, - Resolver = EntityResolvers.CreatedBy, - Description = FieldDescriptions.EntityCreatedBy - }; - - public static readonly FieldType CreatedByUser = new FieldType - { - Name = "createdByUser", - ResolvedType = UserGraphType.NonNull, - Resolver = EntityResolvers.CreatedByUser, - Description = FieldDescriptions.EntityCreatedBy - }; - - public static readonly FieldType LastModified = new FieldType - { - Name = "lastModified", - ResolvedType = Scalars.NonNullDateTime, - Resolver = EntityResolvers.LastModified, - Description = FieldDescriptions.EntityLastModified - }; - - public static readonly FieldType LastModifiedBy = new FieldType - { - Name = "lastModifiedBy", - ResolvedType = Scalars.NonNullString, - Resolver = EntityResolvers.LastModifiedBy, - Description = FieldDescriptions.EntityLastModifiedBy - }; - - public static readonly FieldType LastModifiedByUser = new FieldType - { - Name = "lastModifiedByUser", - ResolvedType = UserGraphType.NonNull, - Resolver = EntityResolvers.LastModifiedByUser, - Description = FieldDescriptions.EntityLastModifiedBy - }; - - public static readonly FieldType Status = new FieldType - { - Name = "status", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.Status.ToString().ToUpperInvariant()), - Description = FieldDescriptions.ContentStatus - }; - - public static readonly FieldType StatusColor = new FieldType - { - Name = "statusColor", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.StatusColor), - Description = FieldDescriptions.ContentStatusColor - }; - - public static readonly FieldType NewStatus = new FieldType - { - Name = "newStatus", - ResolvedType = Scalars.String, - Resolver = Resolve(x => x.NewStatus?.ToString().ToUpperInvariant()), - Description = FieldDescriptions.ContentNewStatus - }; - - public static readonly FieldType NewStatusColor = new FieldType - { - Name = "newStatusColor", - ResolvedType = Scalars.String, - Resolver = Resolve(x => x.NewStatusColor), - Description = FieldDescriptions.ContentStatusColor - }; - - public static readonly FieldType SchemaId = new FieldType - { - Name = "schemaId", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x[Component.Discriminator].ToString()), - Description = FieldDescriptions.ContentSchemaId - }; - - public static readonly FieldType Url = new FieldType - { - Name = "url", - ResolvedType = Scalars.NonNullString, - Resolver = ContentResolvers.Url, - Description = FieldDescriptions.ContentUrl - }; - - public static readonly FieldType EditToken = new FieldType - { - Name = "editToken", - ResolvedType = Scalars.String, - Resolver = Resolve(x => x.EditToken), - Description = FieldDescriptions.EditToken - }; - - public static readonly FieldType StringFieldText = new FieldType - { - Name = "text", - ResolvedType = Scalars.String, - Resolver = Resolvers.Sync<string, string>(x => x), - Description = FieldDescriptions.StringFieldText - }; - - public static readonly FieldType StringFieldAssets = new FieldType - { - Name = "assets", - ResolvedType = new NonNullGraphType(SharedTypes.AssetsList), - Resolver = ResolveStringFieldAssets, - Description = FieldDescriptions.StringFieldAssets - }; - - private static IFieldResolver Resolve<T>(Func<JsonObject, T> resolver) - { - return Resolvers.Sync(resolver); - } - - private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, T> resolver) - { - return Resolvers.Sync(resolver); - } + public static readonly IFieldResolver ResolveStringFieldAssets = Resolvers.Async<string, object>(async (value, fieldContext, context) => + { + var cacheDuration = fieldContext.CacheDuration(); + + var ids = context.Resolve<StringReferenceExtractor>().GetEmbeddedAssetIds(value).ToList(); + + return await context.GetAssetsAsync(ids, cacheDuration, fieldContext.CancellationToken); + }); + + public static readonly IFieldResolver ResolveStringFieldContents = Resolvers.Async<string, object>(async (value, fieldContext, context) => + { + var cacheDuration = fieldContext.CacheDuration(); + + var ids = context.Resolve<StringReferenceExtractor>().GetEmbeddedContentIds(value).ToList(); + + return await context.GetContentsAsync(ids, cacheDuration, fieldContext.CancellationToken); + }); + + public static readonly FieldType Id = new FieldType + { + Name = "id", + ResolvedType = Scalars.NonNullString, + Resolver = EntityResolvers.Id, + Description = FieldDescriptions.EntityId + }; + + public static readonly FieldType Version = new FieldType + { + Name = "version", + ResolvedType = Scalars.NonNullInt, + Resolver = EntityResolvers.Version, + Description = FieldDescriptions.EntityVersion + }; + + public static readonly FieldType Created = new FieldType + { + Name = "created", + ResolvedType = Scalars.NonNullDateTime, + Resolver = EntityResolvers.Created, + Description = FieldDescriptions.EntityCreated + }; + + public static readonly FieldType CreatedBy = new FieldType + { + Name = "createdBy", + ResolvedType = Scalars.NonNullString, + Resolver = EntityResolvers.CreatedBy, + Description = FieldDescriptions.EntityCreatedBy + }; + + public static readonly FieldType CreatedByUser = new FieldType + { + Name = "createdByUser", + ResolvedType = UserGraphType.NonNull, + Resolver = EntityResolvers.CreatedByUser, + Description = FieldDescriptions.EntityCreatedBy + }; + + public static readonly FieldType LastModified = new FieldType + { + Name = "lastModified", + ResolvedType = Scalars.NonNullDateTime, + Resolver = EntityResolvers.LastModified, + Description = FieldDescriptions.EntityLastModified + }; + + public static readonly FieldType LastModifiedBy = new FieldType + { + Name = "lastModifiedBy", + ResolvedType = Scalars.NonNullString, + Resolver = EntityResolvers.LastModifiedBy, + Description = FieldDescriptions.EntityLastModifiedBy + }; + + public static readonly FieldType LastModifiedByUser = new FieldType + { + Name = "lastModifiedByUser", + ResolvedType = UserGraphType.NonNull, + Resolver = EntityResolvers.LastModifiedByUser, + Description = FieldDescriptions.EntityLastModifiedBy + }; + + public static readonly FieldType Status = new FieldType + { + Name = "status", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.Status.ToString().ToUpperInvariant()), + Description = FieldDescriptions.ContentStatus + }; + + public static readonly FieldType StatusColor = new FieldType + { + Name = "statusColor", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.StatusColor), + Description = FieldDescriptions.ContentStatusColor + }; + + public static readonly FieldType NewStatus = new FieldType + { + Name = "newStatus", + ResolvedType = Scalars.String, + Resolver = Resolve(x => x.NewStatus?.ToString().ToUpperInvariant()), + Description = FieldDescriptions.ContentNewStatus + }; + + public static readonly FieldType NewStatusColor = new FieldType + { + Name = "newStatusColor", + ResolvedType = Scalars.String, + Resolver = Resolve(x => x.NewStatusColor), + Description = FieldDescriptions.ContentStatusColor + }; + + public static readonly FieldType SchemaId = new FieldType + { + Name = "schemaId", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x[Component.Discriminator].ToString()), + Description = FieldDescriptions.ContentSchemaId + }; + + public static readonly FieldType Url = new FieldType + { + Name = "url", + ResolvedType = Scalars.NonNullString, + Resolver = ContentResolvers.Url, + Description = FieldDescriptions.ContentUrl + }; + + public static readonly FieldType EditToken = new FieldType + { + Name = "editToken", + ResolvedType = Scalars.String, + Resolver = Resolve(x => x.EditToken), + Description = FieldDescriptions.EditToken + }; + + public static readonly FieldType StringFieldText = new FieldType + { + Name = "text", + ResolvedType = Scalars.String, + Resolver = Resolvers.Sync<string, string>(x => x), + Description = FieldDescriptions.StringFieldText + }; + + public static readonly FieldType StringFieldAssets = new FieldType + { + Name = "assets", + ResolvedType = new NonNullGraphType(SharedTypes.AssetsList), + Resolver = ResolveStringFieldAssets, + Description = FieldDescriptions.StringFieldAssets + }; + + private static IFieldResolver Resolve<T>(Func<JsonObject, T> resolver) + { + return Resolvers.Sync(resolver); + } + + private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, T> resolver) + { + return Resolvers.Sync(resolver); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs index dd35308be6..9437441cec 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs @@ -10,150 +10,149 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity> { - internal sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity> + public ContentGraphType(SchemaInfo schemaInfo) { - public ContentGraphType(SchemaInfo schemaInfo) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = schemaInfo.ContentType; - } + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = schemaInfo.ContentType; + } - public void Initialize(Builder builder, SchemaInfo schemaInfo, IEnumerable<SchemaInfo> allSchemas) - { - var schemaId = schemaInfo.Schema.Id; + public void Initialize(Builder builder, SchemaInfo schemaInfo, IEnumerable<SchemaInfo> allSchemas) + { + var schemaId = schemaInfo.Schema.Id; - IsTypeOf = value => - { - return value is IContentEntity content && content.SchemaId?.Id == schemaId; - }; - - AddField(ContentFields.Id); - AddField(ContentFields.Version); - AddField(ContentFields.Created); - AddField(ContentFields.CreatedBy); - AddField(ContentFields.CreatedByUser); - AddField(ContentFields.EditToken); - AddField(ContentFields.LastModified); - AddField(ContentFields.LastModifiedBy); - AddField(ContentFields.LastModifiedByUser); - AddField(ContentFields.Status); - AddField(ContentFields.StatusColor); - AddField(ContentFields.NewStatus); - AddField(ContentFields.NewStatusColor); - AddField(ContentFields.Url); - - var contentDataType = new DataGraphType(builder, schemaInfo); - - if (contentDataType.Fields.Any()) - { - AddField(new FieldType - { - Name = "data", - ResolvedType = new NonNullGraphType(contentDataType), - Resolver = ContentResolvers.Data, - Description = FieldDescriptions.ContentData - }); - } - - var contentDataTypeFlat = new DataFlatGraphType(builder, schemaInfo); - - if (contentDataTypeFlat.Fields.Any()) - { - AddField(new FieldType - { - Name = "flatData", - ResolvedType = new NonNullGraphType(contentDataTypeFlat), - Resolver = ContentResolvers.FlatData, - Description = FieldDescriptions.ContentFlatData - }); - } - - foreach (var other in allSchemas.Where(x => IsReference(x, schemaInfo))) + IsTypeOf = value => + { + return value is IContentEntity content && content.SchemaId?.Id == schemaId; + }; + + AddField(ContentFields.Id); + AddField(ContentFields.Version); + AddField(ContentFields.Created); + AddField(ContentFields.CreatedBy); + AddField(ContentFields.CreatedByUser); + AddField(ContentFields.EditToken); + AddField(ContentFields.LastModified); + AddField(ContentFields.LastModifiedBy); + AddField(ContentFields.LastModifiedByUser); + AddField(ContentFields.Status); + AddField(ContentFields.StatusColor); + AddField(ContentFields.NewStatus); + AddField(ContentFields.NewStatusColor); + AddField(ContentFields.Url); + + var contentDataType = new DataGraphType(builder, schemaInfo); + + if (contentDataType.Fields.Any()) + { + AddField(new FieldType { - AddReferencingQueries(builder, other); - } + Name = "data", + ResolvedType = new NonNullGraphType(contentDataType), + Resolver = ContentResolvers.Data, + Description = FieldDescriptions.ContentData + }); + } - foreach (var other in allSchemas.Where(x => IsReference(schemaInfo, x))) - { - AddReferencesQueries(builder, other); - } + var contentDataTypeFlat = new DataFlatGraphType(builder, schemaInfo); - AddResolvedInterface(builder.ContentInterface); + if (contentDataTypeFlat.Fields.Any()) + { + AddField(new FieldType + { + Name = "flatData", + ResolvedType = new NonNullGraphType(contentDataTypeFlat), + Resolver = ContentResolvers.FlatData, + Description = FieldDescriptions.ContentFlatData + }); + } - Description = $"The structure of a {schemaInfo.DisplayName} content type."; + foreach (var other in allSchemas.Where(x => IsReference(x, schemaInfo))) + { + AddReferencingQueries(builder, other); } - private void AddReferencingQueries(Builder builder, SchemaInfo referencingSchemaInfo) + foreach (var other in allSchemas.Where(x => IsReference(schemaInfo, x))) { - var contentType = builder.GetContentType(referencingSchemaInfo); + AddReferencesQueries(builder, other); + } - AddField(new FieldType - { - Name = $"referencing{referencingSchemaInfo.TypeName}Contents", - Arguments = ContentActions.QueryOrReferencing.Arguments, - ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), - Resolver = ContentActions.QueryOrReferencing.Referencing, - Description = $"Query {referencingSchemaInfo.DisplayName} content items." - }).WithSchemaId(referencingSchemaInfo); + AddResolvedInterface(builder.ContentInterface); + + Description = $"The structure of a {schemaInfo.DisplayName} content type."; + } - var contentResultsTyp = builder.GetContentResultType(referencingSchemaInfo); + private void AddReferencingQueries(Builder builder, SchemaInfo referencingSchemaInfo) + { + var contentType = builder.GetContentType(referencingSchemaInfo); - AddField(new FieldType - { - Name = $"referencing{referencingSchemaInfo.TypeName}ContentsWithTotal", - Arguments = ContentActions.QueryOrReferencing.Arguments, - ResolvedType = contentResultsTyp, - Resolver = ContentActions.QueryOrReferencing.ReferencingWithTotal, - Description = $"Query {referencingSchemaInfo.DisplayName} content items with total count." - }).WithSchemaId(referencingSchemaInfo); - } + AddField(new FieldType + { + Name = $"referencing{referencingSchemaInfo.TypeName}Contents", + Arguments = ContentActions.QueryOrReferencing.Arguments, + ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), + Resolver = ContentActions.QueryOrReferencing.Referencing, + Description = $"Query {referencingSchemaInfo.DisplayName} content items." + }).WithSchemaId(referencingSchemaInfo); - private void AddReferencesQueries(Builder builder, SchemaInfo referencesSchemaInfo) + var contentResultsTyp = builder.GetContentResultType(referencingSchemaInfo); + + AddField(new FieldType { - var contentType = builder.GetContentType(referencesSchemaInfo); + Name = $"referencing{referencingSchemaInfo.TypeName}ContentsWithTotal", + Arguments = ContentActions.QueryOrReferencing.Arguments, + ResolvedType = contentResultsTyp, + Resolver = ContentActions.QueryOrReferencing.ReferencingWithTotal, + Description = $"Query {referencingSchemaInfo.DisplayName} content items with total count." + }).WithSchemaId(referencingSchemaInfo); + } - AddField(new FieldType - { - Name = $"references{referencesSchemaInfo.TypeName}Contents", - Arguments = ContentActions.QueryOrReferencing.Arguments, - ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), - Resolver = ContentActions.QueryOrReferencing.References, - Description = $"Query {referencesSchemaInfo.DisplayName} content items." - }).WithSchemaId(referencesSchemaInfo); + private void AddReferencesQueries(Builder builder, SchemaInfo referencesSchemaInfo) + { + var contentType = builder.GetContentType(referencesSchemaInfo); - var contentResultsTyp = builder.GetContentResultType(referencesSchemaInfo); + AddField(new FieldType + { + Name = $"references{referencesSchemaInfo.TypeName}Contents", + Arguments = ContentActions.QueryOrReferencing.Arguments, + ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), + Resolver = ContentActions.QueryOrReferencing.References, + Description = $"Query {referencesSchemaInfo.DisplayName} content items." + }).WithSchemaId(referencesSchemaInfo); - AddField(new FieldType - { - Name = $"references{referencesSchemaInfo.TypeName}ContentsWithTotal", - Arguments = ContentActions.QueryOrReferencing.Arguments, - ResolvedType = contentResultsTyp, - Resolver = ContentActions.QueryOrReferencing.ReferencesWithTotal, - Description = $"Query {referencesSchemaInfo.DisplayName} content items with total count." - }).WithSchemaId(referencesSchemaInfo); - } + var contentResultsTyp = builder.GetContentResultType(referencesSchemaInfo); - private static bool IsReference(SchemaInfo from, SchemaInfo to) + AddField(new FieldType { - return from.Schema.SchemaDef.Fields.Any(x => IsReferencing(x, to.Schema.Id)); - } + Name = $"references{referencesSchemaInfo.TypeName}ContentsWithTotal", + Arguments = ContentActions.QueryOrReferencing.Arguments, + ResolvedType = contentResultsTyp, + Resolver = ContentActions.QueryOrReferencing.ReferencesWithTotal, + Description = $"Query {referencesSchemaInfo.DisplayName} content items with total count." + }).WithSchemaId(referencesSchemaInfo); + } - private static bool IsReferencing(IField field, DomainId schemaId) + private static bool IsReference(SchemaInfo from, SchemaInfo to) + { + return from.Schema.SchemaDef.Fields.Any(x => IsReferencing(x, to.Schema.Id)); + } + + private static bool IsReferencing(IField field, DomainId schemaId) + { + switch (field) { - switch (field) - { - case IField<ReferencesFieldProperties> reference: - return - reference.Properties.SchemaIds == null || - reference.Properties.SchemaIds.Count == 0 || - reference.Properties.SchemaIds.Contains(schemaId); - case IArrayField arrayField: - return arrayField.Fields.Any(x => IsReferencing(x, schemaId)); - } - - return false; + case IField<ReferencesFieldProperties> reference: + return + reference.Properties.SchemaIds == null || + reference.Properties.SchemaIds.Count == 0 || + reference.Properties.SchemaIds.Contains(schemaId); + case IArrayField arrayField: + return arrayField.Fields.Any(x => IsReferencing(x, schemaId)); } + + return false; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs index 30bbeb8b1e..90c417a29f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs @@ -7,28 +7,27 @@ using GraphQL.Types; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class ContentInterfaceGraphType : InterfaceGraphType<IEnrichedContentEntity> { - internal sealed class ContentInterfaceGraphType : InterfaceGraphType<IEnrichedContentEntity> + public ContentInterfaceGraphType() { - public ContentInterfaceGraphType() - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "Content"; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "Content"; - AddField(ContentFields.Id); - AddField(ContentFields.Version); - AddField(ContentFields.Created); - AddField(ContentFields.CreatedBy); - AddField(ContentFields.LastModified); - AddField(ContentFields.LastModifiedBy); - AddField(ContentFields.Status); - AddField(ContentFields.StatusColor); - AddField(ContentFields.NewStatus); - AddField(ContentFields.NewStatusColor); - AddField(ContentFields.Url); + AddField(ContentFields.Id); + AddField(ContentFields.Version); + AddField(ContentFields.Created); + AddField(ContentFields.CreatedBy); + AddField(ContentFields.LastModified); + AddField(ContentFields.LastModifiedBy); + AddField(ContentFields.Status); + AddField(ContentFields.StatusColor); + AddField(ContentFields.NewStatus); + AddField(ContentFields.NewStatusColor); + AddField(ContentFields.Url); - Description = "The structure of all content types."; - } + Description = "The structure of all content types."; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs index b61d8df860..9e9a317614 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs @@ -12,50 +12,49 @@ using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal static class ContentResolvers { - internal static class ContentResolvers + public static readonly IFieldResolver Field = Resolvers.Sync<ContentData, object?>((content, fieldContext, _) => { - public static readonly IFieldResolver Field = Resolvers.Sync<ContentData, object?>((content, fieldContext, _) => - { - var fieldName = fieldContext.FieldDefinition.SourceName(); + var fieldName = fieldContext.FieldDefinition.SourceName(); - return content?.GetValueOrDefault(fieldName); - }); + return content?.GetValueOrDefault(fieldName); + }); - public static readonly IFieldResolver Url = Resolve((content, _, context) => - { - var urlGenerator = context.Resolve<IUrlGenerator>(); + public static readonly IFieldResolver Url = Resolve((content, _, context) => + { + var urlGenerator = context.Resolve<IUrlGenerator>(); - return urlGenerator.ContentUI(content.AppId, content.SchemaId, content.Id); - }); + return urlGenerator.ContentUI(content.AppId, content.SchemaId, content.Id); + }); - public static readonly IFieldResolver FlatData = Resolve((content, c, context) => - { - var language = context.Context.App.Languages.Master; + public static readonly IFieldResolver FlatData = Resolve((content, c, context) => + { + var language = context.Context.App.Languages.Master; - return content.Data.ToFlatten(language); - }); + return content.Data.ToFlatten(language); + }); - public static readonly IFieldResolver Data = Resolve(x => x.Data); + public static readonly IFieldResolver Data = Resolve(x => x.Data); - public static readonly IFieldResolver ListTotal = ResolveList(x => x.Total); + public static readonly IFieldResolver ListTotal = ResolveList(x => x.Total); - public static readonly IFieldResolver ListItems = ResolveList(x => x); + public static readonly IFieldResolver ListItems = ResolveList(x => x); - private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, IResolveFieldContext, GraphQLExecutionContext, T> resolver) - { - return Resolvers.Sync(resolver); - } + private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, IResolveFieldContext, GraphQLExecutionContext, T> resolver) + { + return Resolvers.Sync(resolver); + } - private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, T> resolver) - { - return Resolvers.Sync(resolver); - } + private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, T> resolver) + { + return Resolvers.Sync(resolver); + } - private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedContentEntity>, T> resolver) - { - return Resolvers.Sync(resolver); - } + private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedContentEntity>, T> resolver) + { + return Resolvers.Sync(resolver); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResultGraphType.cs index 34843d9225..c25da48e34 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResultGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResultGraphType.cs @@ -9,32 +9,31 @@ using Squidex.Domain.Apps.Core; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class ContentResultGraphType : ObjectGraphType<IResultList<IContentEntity>> { - internal sealed class ContentResultGraphType : ObjectGraphType<IResultList<IContentEntity>> + public ContentResultGraphType(ContentGraphType contentType, SchemaInfo schemaInfo) { - public ContentResultGraphType(ContentGraphType contentType, SchemaInfo schemaInfo) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = schemaInfo.ContentResultType; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = schemaInfo.ContentResultType; - AddField(new FieldType - { - Name = "total", - ResolvedType = Scalars.NonNullInt, - Resolver = ContentResolvers.ListTotal, - Description = FieldDescriptions.ContentsTotal - }); + AddField(new FieldType + { + Name = "total", + ResolvedType = Scalars.NonNullInt, + Resolver = ContentResolvers.ListTotal, + Description = FieldDescriptions.ContentsTotal + }); - AddField(new FieldType - { - Name = "items", - ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), - Resolver = ContentResolvers.ListItems, - Description = FieldDescriptions.ContentsItems - }); + AddField(new FieldType + { + Name = "items", + ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), + Resolver = ContentResolvers.ListItems, + Description = FieldDescriptions.ContentsItems + }); - Description = $"List of {schemaInfo.DisplayName} items and total count."; - } + Description = $"List of {schemaInfo.DisplayName} items and total count."; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataFlatGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataFlatGraphType.cs index 8513197176..142ef2be28 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataFlatGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataFlatGraphType.cs @@ -9,45 +9,44 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class DataFlatGraphType : ObjectGraphType<FlatContentData> { - internal sealed class DataFlatGraphType : ObjectGraphType<FlatContentData> + public DataFlatGraphType(Builder builder, SchemaInfo schemaInfo) { - public DataFlatGraphType(Builder builder, SchemaInfo schemaInfo) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = schemaInfo.DataFlatType; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = schemaInfo.DataFlatType; - foreach (var fieldInfo in schemaInfo.Fields) + foreach (var fieldInfo in schemaInfo.Fields) + { + if (fieldInfo.Field.IsComponentLike()) { - if (fieldInfo.Field.IsComponentLike()) + AddField(new FieldType { - AddField(new FieldType - { - Name = fieldInfo.FieldNameDynamic, - Arguments = ContentActions.Json.Arguments, - ResolvedType = Scalars.Json, - Resolver = FieldVisitor.JsonPath, - Description = fieldInfo.Field.RawProperties.Hints - }).WithSourceName(fieldInfo); - } + Name = fieldInfo.FieldNameDynamic, + Arguments = ContentActions.Json.Arguments, + ResolvedType = Scalars.Json, + Resolver = FieldVisitor.JsonPath, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(fieldInfo); + } - var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); + var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); - if (resolver != null) + if (resolver != null) + { + AddField(new FieldType { - AddField(new FieldType - { - Name = fieldInfo.FieldName, - Arguments = args, - ResolvedType = resolvedType, - Resolver = resolver, - Description = fieldInfo.Field.RawProperties.Hints - }).WithSourceName(fieldInfo); - } + Name = fieldInfo.FieldName, + Arguments = args, + ResolvedType = resolvedType, + Resolver = resolver, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(fieldInfo); } - - Description = $"The structure of the flat {schemaInfo.DisplayName} data type."; } + + Description = $"The structure of the flat {schemaInfo.DisplayName} data type."; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataGraphType.cs index 26919ee9c7..0551925c11 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataGraphType.cs @@ -10,83 +10,82 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class DataGraphType : ObjectGraphType<ContentData> { - internal sealed class DataGraphType : ObjectGraphType<ContentData> + public DataGraphType(Builder builder, SchemaInfo schemaInfo) { - public DataGraphType(Builder builder, SchemaInfo schemaInfo) + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = schemaInfo.DataType; + + foreach (var fieldInfo in schemaInfo.Fields) { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = schemaInfo.DataType; + var partitioning = builder.ResolvePartition(((RootField)fieldInfo.Field).Partitioning); - foreach (var fieldInfo in schemaInfo.Fields) + if (fieldInfo.Field.IsComponentLike()) { - var partitioning = builder.ResolvePartition(((RootField)fieldInfo.Field).Partitioning); - - if (fieldInfo.Field.IsComponentLike()) + var fieldGraphType = new ObjectGraphType { - var fieldGraphType = new ObjectGraphType - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.LocalizedTypeDynamic - }; - - foreach (var partitionKey in partitioning.AllKeys) - { - fieldGraphType.AddField(new FieldType - { - Name = partitionKey.EscapePartition(), - Arguments = ContentActions.Json.Arguments, - ResolvedType = Scalars.Json, - Resolver = FieldVisitor.JsonPath, - Description = fieldInfo.Field.RawProperties.Hints - }).WithSourceName(partitionKey); - } - - fieldGraphType.Description = $"The dynamic structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content type."; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = fieldInfo.LocalizedTypeDynamic + }; - AddField(new FieldType + foreach (var partitionKey in partitioning.AllKeys) + { + fieldGraphType.AddField(new FieldType { - Name = fieldInfo.FieldNameDynamic, - ResolvedType = fieldGraphType, - Resolver = ContentResolvers.Field - }).WithSourceName(fieldInfo); + Name = partitionKey.EscapePartition(), + Arguments = ContentActions.Json.Arguments, + ResolvedType = Scalars.Json, + Resolver = FieldVisitor.JsonPath, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(partitionKey); } - var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); + fieldGraphType.Description = $"The dynamic structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content type."; - if (resolver != null) + AddField(new FieldType { - var fieldGraphType = new ObjectGraphType - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.LocalizedType - }; + Name = fieldInfo.FieldNameDynamic, + ResolvedType = fieldGraphType, + Resolver = ContentResolvers.Field + }).WithSourceName(fieldInfo); + } - foreach (var partitionKey in partitioning.AllKeys) - { - fieldGraphType.AddField(new FieldType - { - Name = partitionKey.EscapePartition(), - Arguments = args, - ResolvedType = resolvedType, - Resolver = resolver, - Description = fieldInfo.Field.RawProperties.Hints - }).WithSourceName(partitionKey); - } + var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); - fieldGraphType.Description = $"The structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content type."; + if (resolver != null) + { + var fieldGraphType = new ObjectGraphType + { + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = fieldInfo.LocalizedType + }; - AddField(new FieldType + foreach (var partitionKey in partitioning.AllKeys) + { + fieldGraphType.AddField(new FieldType { - Name = fieldInfo.FieldName, - ResolvedType = fieldGraphType, - Resolver = ContentResolvers.Field - }).WithSourceName(fieldInfo); + Name = partitionKey.EscapePartition(), + Arguments = args, + ResolvedType = resolvedType, + Resolver = resolver, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(partitionKey); } - } - Description = $"The structure of the {schemaInfo.DisplayName} data type."; + fieldGraphType.Description = $"The structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content type."; + + AddField(new FieldType + { + Name = fieldInfo.FieldName, + ResolvedType = fieldGraphType, + Resolver = ContentResolvers.Field + }).WithSourceName(fieldInfo); + } } + + Description = $"The structure of the {schemaInfo.DisplayName} data type."; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs index b8e4f03d2b..5021ad9ff3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs @@ -12,99 +12,98 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class DataInputGraphType : InputObjectGraphType { - internal sealed class DataInputGraphType : InputObjectGraphType + public DataInputGraphType(Builder builder, SchemaInfo schemaInfo) { - public DataInputGraphType(Builder builder, SchemaInfo schemaInfo) + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = schemaInfo.DataInputType; + + foreach (var fieldInfo in schemaInfo.Fields) { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = schemaInfo.DataInputType; + var resolvedType = builder.GetInputGraphType(fieldInfo); - foreach (var fieldInfo in schemaInfo.Fields) + if (resolvedType != null) { - var resolvedType = builder.GetInputGraphType(fieldInfo); - - if (resolvedType != null) + var fieldGraphType = new InputObjectGraphType { - var fieldGraphType = new InputObjectGraphType - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.LocalizedInputType - }; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = fieldInfo.LocalizedInputType + }; - var partitioning = builder.ResolvePartition(((RootField)fieldInfo.Field).Partitioning); + var partitioning = builder.ResolvePartition(((RootField)fieldInfo.Field).Partitioning); - foreach (var partitionKey in partitioning.AllKeys) + foreach (var partitionKey in partitioning.AllKeys) + { + fieldGraphType.AddField(new FieldType { - fieldGraphType.AddField(new FieldType - { - Name = partitionKey.EscapePartition(), - ResolvedType = resolvedType, - Resolver = null, - Description = fieldInfo.Field.RawProperties.Hints - }).WithSourceName(partitionKey); - } + Name = partitionKey.EscapePartition(), + ResolvedType = resolvedType, + Resolver = null, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(partitionKey); + } - fieldGraphType.Description = $"The structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content input type."; + fieldGraphType.Description = $"The structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content input type."; - AddField(new FieldType - { - Name = fieldInfo.FieldName, - ResolvedType = fieldGraphType, - Resolver = null - }).WithSourceName(fieldInfo); - } + AddField(new FieldType + { + Name = fieldInfo.FieldName, + ResolvedType = fieldGraphType, + Resolver = null + }).WithSourceName(fieldInfo); } - - Description = $"The structure of the {schemaInfo.DisplayName} data input type."; } - public override object ParseDictionary(IDictionary<string, object?> value) + Description = $"The structure of the {schemaInfo.DisplayName} data input type."; + } + + public override object ParseDictionary(IDictionary<string, object?> value) + { + var result = new ContentData(); + + static ContentFieldData ToFieldData(IDictionary<string, object> source, IComplexGraphType type) { - var result = new ContentData(); + var result = new ContentFieldData(); - static ContentFieldData ToFieldData(IDictionary<string, object> source, IComplexGraphType type) + foreach (var field in type.Fields) { - var result = new ContentFieldData(); - - foreach (var field in type.Fields) + if (source.TryGetValue(field.Name, out var value)) { - if (source.TryGetValue(field.Name, out var value)) + if (value is IEnumerable<object> list && field.ResolvedType?.Flatten() is IComplexGraphType nestedType) { - if (value is IEnumerable<object> list && field.ResolvedType?.Flatten() is IComplexGraphType nestedType) - { - var array = new JsonArray(list.Count()); + var array = new JsonArray(list.Count()); - foreach (var item in list) + foreach (var item in list) + { + if (item is JsonValue { Value: JsonObject } nested) { - if (item is JsonValue { Value: JsonObject } nested) - { - array.Add(nested); - } + array.Add(nested); } - - result[field.SourceName()] = array; - } - else - { - result[field.SourceName()] = JsonGraphType.ParseJson(value); } + + result[field.SourceName()] = array; + } + else + { + result[field.SourceName()] = JsonGraphType.ParseJson(value); } } - - return result; } - foreach (var field in Fields) + return result; + } + + foreach (var field in Fields) + { + if (field.ResolvedType is IComplexGraphType complexType && value.TryGetValue(field.Name, out var fieldValue) && fieldValue is IDictionary<string, object> nested) { - if (field.ResolvedType is IComplexGraphType complexType && value.TryGetValue(field.Name, out var fieldValue) && fieldValue is IDictionary<string, object> nested) - { - result[field.SourceName()] = ToFieldData(nested, complexType); - } + result[field.SourceName()] = ToFieldData(nested, complexType); } - - return result; } + + return result; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs index cf785f2cea..a80388f044 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs @@ -11,54 +11,53 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class EmbeddableStringGraphType : ObjectGraphType<string> { - internal sealed class EmbeddableStringGraphType : ObjectGraphType<string> + public EmbeddableStringGraphType(Builder builder, FieldInfo fieldInfo, StringFieldProperties properties) { - public EmbeddableStringGraphType(Builder builder, FieldInfo fieldInfo, StringFieldProperties properties) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.EmbeddableStringType; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = fieldInfo.EmbeddableStringType; - AddField(ContentFields.StringFieldText); - AddField(ContentFields.StringFieldAssets); + AddField(ContentFields.StringFieldText); + AddField(ContentFields.StringFieldAssets); - var referenceType = ResolveReferences(builder, fieldInfo, properties.SchemaIds); + var referenceType = ResolveReferences(builder, fieldInfo, properties.SchemaIds); - if (referenceType != null) + if (referenceType != null) + { + AddField(new FieldType { - AddField(new FieldType - { - Name = "contents", - ResolvedType = new NonNullGraphType(new ListGraphType(new NonNullGraphType(referenceType))), - Resolver = ContentFields.ResolveStringFieldContents, - Description = FieldDescriptions.StringFieldReferences - }); - } + Name = "contents", + ResolvedType = new NonNullGraphType(new ListGraphType(new NonNullGraphType(referenceType))), + Resolver = ContentFields.ResolveStringFieldContents, + Description = FieldDescriptions.StringFieldReferences + }); } + } + + private static IGraphType? ResolveReferences(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + { + IGraphType? contentType = null; - private static IGraphType? ResolveReferences(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + if (schemaIds?.Count == 1) { - IGraphType? contentType = null; + contentType = builder.GetContentType(schemaIds[0]); + } - if (schemaIds?.Count == 1) - { - contentType = builder.GetContentType(schemaIds[0]); - } + if (contentType == null) + { + var union = builder.GetReferenceUnion(fieldInfo, schemaIds); - if (contentType == null) + if (!union.HasType) { - var union = builder.GetReferenceUnion(fieldInfo, schemaIds); - - if (!union.HasType) - { - return default; - } - - contentType = union; + return default; } - return contentType; + contentType = union; } + + return contentType; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EnrichedContentEventGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EnrichedContentEventGraphType.cs index 53f8625611..66f9c028c1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EnrichedContentEventGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EnrichedContentEventGraphType.cs @@ -10,125 +10,124 @@ using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class EnrichedContentEventGraphType : SharedObjectGraphType<EnrichedContentEvent> { - internal sealed class EnrichedContentEventGraphType : SharedObjectGraphType<EnrichedContentEvent> + public EnrichedContentEventGraphType() { - public EnrichedContentEventGraphType() + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "EnrichedContentEvent"; + + AddField(new FieldType + { + Name = "type", + ResolvedType = Scalars.EnrichedContentEventType, + Resolver = Resolve(x => x.Type), + Description = FieldDescriptions.EventType + }); + + AddField(new FieldType + { + Name = "id", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.Id.ToString()), + Description = FieldDescriptions.EntityId + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = Scalars.NonNullInt, + Resolver = Resolve(x => x.Version), + Description = FieldDescriptions.EntityVersion + }); + + AddField(new FieldType { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "EnrichedContentEvent"; - - AddField(new FieldType - { - Name = "type", - ResolvedType = Scalars.EnrichedContentEventType, - Resolver = Resolve(x => x.Type), - Description = FieldDescriptions.EventType - }); - - AddField(new FieldType - { - Name = "id", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.Id.ToString()), - Description = FieldDescriptions.EntityId - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = Scalars.NonNullInt, - Resolver = Resolve(x => x.Version), - Description = FieldDescriptions.EntityVersion - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = Scalars.NonNullDateTime, - Resolver = Resolve(x => x.Created.ToDateTimeUtc()), - Description = FieldDescriptions.EntityCreated - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), - Description = FieldDescriptions.EntityCreatedBy - }); - - AddField(new FieldType - { - Name = "createdByUser", - ResolvedType = UserGraphType.NonNull, - Resolver = Resolve(x => x.CreatedBy), - Description = FieldDescriptions.EntityCreatedBy - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = Scalars.NonNullDateTime, - Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()), - Description = FieldDescriptions.EntityLastModified - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), - Description = FieldDescriptions.EntityLastModifiedBy - }); - - AddField(new FieldType - { - Name = "lastModifiedByUser", - ResolvedType = UserGraphType.NonNull, - Resolver = Resolve(x => x.LastModifiedBy), - Description = FieldDescriptions.EntityLastModifiedBy - }); - - AddField(new FieldType - { - Name = "status", - ResolvedType = Scalars.NonNullString, - Resolver = Resolve(x => x.Status.ToString()), - Description = FieldDescriptions.ContentStatus - }); - - AddField(new FieldType - { - Name = "newStatus", - ResolvedType = Scalars.String, - Resolver = Resolve(x => x.NewStatus?.ToString()), - Description = FieldDescriptions.ContentNewStatus - }); - - AddField(new FieldType - { - Name = "data", - ResolvedType = Scalars.JsonNoop, - Resolver = Resolve(x => x.Data), - Description = FieldDescriptions.ContentData - }); - - AddField(new FieldType - { - Name = "dataOld", - ResolvedType = Scalars.JsonNoop, - Resolver = Resolve(x => x.DataOld), - Description = FieldDescriptions.ContentDataOld - }); - - Description = "An content event"; - } - - private static IFieldResolver Resolve<T>(Func<EnrichedContentEvent, T> resolver) + Name = "created", + ResolvedType = Scalars.NonNullDateTime, + Resolver = Resolve(x => x.Created.ToDateTimeUtc()), + Description = FieldDescriptions.EntityCreated + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.CreatedBy.ToString()), + Description = FieldDescriptions.EntityCreatedBy + }); + + AddField(new FieldType + { + Name = "createdByUser", + ResolvedType = UserGraphType.NonNull, + Resolver = Resolve(x => x.CreatedBy), + Description = FieldDescriptions.EntityCreatedBy + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = Scalars.NonNullDateTime, + Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()), + Description = FieldDescriptions.EntityLastModified + }); + + AddField(new FieldType { - return Resolvers.Sync(resolver); - } + Name = "lastModifiedBy", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Description = FieldDescriptions.EntityLastModifiedBy + }); + + AddField(new FieldType + { + Name = "lastModifiedByUser", + ResolvedType = UserGraphType.NonNull, + Resolver = Resolve(x => x.LastModifiedBy), + Description = FieldDescriptions.EntityLastModifiedBy + }); + + AddField(new FieldType + { + Name = "status", + ResolvedType = Scalars.NonNullString, + Resolver = Resolve(x => x.Status.ToString()), + Description = FieldDescriptions.ContentStatus + }); + + AddField(new FieldType + { + Name = "newStatus", + ResolvedType = Scalars.String, + Resolver = Resolve(x => x.NewStatus?.ToString()), + Description = FieldDescriptions.ContentNewStatus + }); + + AddField(new FieldType + { + Name = "data", + ResolvedType = Scalars.JsonNoop, + Resolver = Resolve(x => x.Data), + Description = FieldDescriptions.ContentData + }); + + AddField(new FieldType + { + Name = "dataOld", + ResolvedType = Scalars.JsonNoop, + Resolver = Resolve(x => x.DataOld), + Description = FieldDescriptions.ContentDataOld + }); + + Description = "An content event"; + } + + private static IFieldResolver Resolve<T>(Func<EnrichedContentEvent, T> resolver) + { + return Resolvers.Sync(resolver); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldEnumType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldEnumType.cs index 08cd67d6cd..db0275d1c6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldEnumType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldEnumType.cs @@ -8,29 +8,28 @@ using GraphQL.Types; using GraphQL.Utilities; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +public sealed class FieldEnumType : EnumerationGraphType { - public sealed class FieldEnumType : EnumerationGraphType + public FieldEnumType(string name, IEnumerable<string> values) { - public FieldEnumType(string name, IEnumerable<string> values) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = name; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = name; - foreach (var value in values) - { - Add(value, value, value); - } + foreach (var value in values) + { + Add(value, value, value); } + } - public static FieldEnumType? TryCreate(string name, IEnumerable<string> values) + public static FieldEnumType? TryCreate(string name, IEnumerable<string> values) + { + if (!values.All(x => x.IsValidName(NamedElement.EnumValue)) || !values.Any()) { - if (!values.All(x => x.IsValidName(NamedElement.EnumValue)) || !values.Any()) - { - return null; - } - - return new FieldEnumType(name, values); + return null; } + + return new FieldEnumType(name, values); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldGraphSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldGraphSchema.cs index fe419817a5..0c040f38d9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldGraphSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldGraphSchema.cs @@ -10,7 +10,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents -{ - public record struct FieldGraphSchema(IGraphType? Type, IFieldResolver? Resolver, QueryArguments? Arguments); -} +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +public record struct FieldGraphSchema(IGraphType? Type, IFieldResolver? Resolver, QueryArguments? Arguments); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs index 4945bcb8a6..f49ecbcee7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs @@ -8,114 +8,113 @@ using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class FieldInputVisitor : IFieldVisitor<IGraphType?, FieldInfo> { - internal sealed class FieldInputVisitor : IFieldVisitor<IGraphType?, FieldInfo> + private readonly Builder builder; + + public FieldInputVisitor(Builder builder) { - private readonly Builder builder; + this.builder = builder; + } - public FieldInputVisitor(Builder builder) + public IGraphType? Visit(IArrayField field, FieldInfo args) + { + if (args.Fields.Count == 0) { - this.builder = builder; + return null; } - public IGraphType? Visit(IArrayField field, FieldInfo args) - { - if (args.Fields.Count == 0) - { - return null; - } + var schemaFieldType = + new ListGraphType( + new NonNullGraphType( + new NestedInputGraphType(builder, args))); - var schemaFieldType = - new ListGraphType( - new NonNullGraphType( - new NestedInputGraphType(builder, args))); + return schemaFieldType; + } - return schemaFieldType; - } + public IGraphType? Visit(IField<AssetsFieldProperties> field, FieldInfo args) + { + return Scalars.Strings; + } - public IGraphType? Visit(IField<AssetsFieldProperties> field, FieldInfo args) - { - return Scalars.Strings; - } + public IGraphType? Visit(IField<BooleanFieldProperties> field, FieldInfo args) + { + return Scalars.Boolean; + } - public IGraphType? Visit(IField<BooleanFieldProperties> field, FieldInfo args) - { - return Scalars.Boolean; - } + public IGraphType? Visit(IField<ComponentFieldProperties> field, FieldInfo args) + { + return Scalars.Json; + } - public IGraphType? Visit(IField<ComponentFieldProperties> field, FieldInfo args) - { - return Scalars.Json; - } + public IGraphType? Visit(IField<ComponentsFieldProperties> field, FieldInfo args) + { + return Scalars.Json; + } - public IGraphType? Visit(IField<ComponentsFieldProperties> field, FieldInfo args) - { - return Scalars.Json; - } + public IGraphType? Visit(IField<DateTimeFieldProperties> field, FieldInfo args) + { + return Scalars.DateTime; + } - public IGraphType? Visit(IField<DateTimeFieldProperties> field, FieldInfo args) - { - return Scalars.DateTime; - } + public IGraphType? Visit(IField<GeolocationFieldProperties> field, FieldInfo args) + { + return Scalars.Json; + } - public IGraphType? Visit(IField<GeolocationFieldProperties> field, FieldInfo args) - { - return Scalars.Json; - } + public IGraphType? Visit(IField<JsonFieldProperties> field, FieldInfo args) + { + return Scalars.Json; + } - public IGraphType? Visit(IField<JsonFieldProperties> field, FieldInfo args) - { - return Scalars.Json; - } + public IGraphType? Visit(IField<ReferencesFieldProperties> field, FieldInfo args) + { + return Scalars.Strings; + } - public IGraphType? Visit(IField<ReferencesFieldProperties> field, FieldInfo args) - { - return Scalars.Strings; - } + public IGraphType? Visit(IField<NumberFieldProperties> field, FieldInfo args) + { + return Scalars.Float; + } - public IGraphType? Visit(IField<NumberFieldProperties> field, FieldInfo args) - { - return Scalars.Float; - } + public IGraphType? Visit(IField<StringFieldProperties> field, FieldInfo args) + { + var type = Scalars.String; - public IGraphType? Visit(IField<StringFieldProperties> field, FieldInfo args) + if (field.Properties?.AllowedValues?.Count > 0 && field.Properties.CreateEnum) { - var type = Scalars.String; + var @enum = builder.GetEnumeration(args.EmbeddedEnumType, field.Properties.AllowedValues); - if (field.Properties?.AllowedValues?.Count > 0 && field.Properties.CreateEnum) + if (@enum != null) { - var @enum = builder.GetEnumeration(args.EmbeddedEnumType, field.Properties.AllowedValues); - - if (@enum != null) - { - type = @enum; - } + type = @enum; } - - return type; } - public IGraphType? Visit(IField<TagsFieldProperties> field, FieldInfo args) + return type; + } + + public IGraphType? Visit(IField<TagsFieldProperties> field, FieldInfo args) + { + var type = Scalars.Strings; + + if (field.Properties?.AllowedValues?.Count > 0 && field.Properties.CreateEnum) { - var type = Scalars.Strings; + var @enum = builder.GetEnumeration(args.EmbeddedEnumType, field.Properties.AllowedValues); - if (field.Properties?.AllowedValues?.Count > 0 && field.Properties.CreateEnum) + if (@enum != null) { - var @enum = builder.GetEnumeration(args.EmbeddedEnumType, field.Properties.AllowedValues); - - if (@enum != null) - { - type = new ListGraphType(new NonNullGraphType(@enum)); - } + type = new ListGraphType(new NonNullGraphType(@enum)); } - - return type; } - public IGraphType? Visit(IField<UIFieldProperties> field, FieldInfo args) - { - return null; - } + return type; + } + + public IGraphType? Visit(IField<UIFieldProperties> field, FieldInfo args) + { + return null; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs index 5c8c80eb90..33f5a3ea8e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs @@ -15,329 +15,328 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents -{ - public delegate T ValueResolver<out T>(JsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context); +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; - public delegate Task<T> AsyncValueResolver<T>(JsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context); +public delegate T ValueResolver<out T>(JsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context); - internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo> - { - public static readonly IFieldResolver JsonNoop = CreateValueResolver((value, fieldContext, contex) => value.Value); - public static readonly IFieldResolver JsonPath = CreateValueResolver(ContentActions.Json.Resolver); +public delegate Task<T> AsyncValueResolver<T>(JsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context); - private static readonly IFieldResolver JsonBoolean = CreateValueResolver((value, fieldContext, contex) => - { - switch (value.Value) - { - case bool b: - return b; - default: - ThrowHelper.NotSupportedException(); - return default!; - } - }); +internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo> +{ + public static readonly IFieldResolver JsonNoop = CreateValueResolver((value, fieldContext, contex) => value.Value); + public static readonly IFieldResolver JsonPath = CreateValueResolver(ContentActions.Json.Resolver); - private static readonly IFieldResolver JsonComponents = CreateValueResolver((value, fieldContext, contex) => + private static readonly IFieldResolver JsonBoolean = CreateValueResolver((value, fieldContext, contex) => + { + switch (value.Value) { - switch (value.Value) - { - case JsonArray a: - return a.Select(x => x.AsObject).ToList(); - default: - ThrowHelper.NotSupportedException(); - return default!; - } - }); + case bool b: + return b; + default: + ThrowHelper.NotSupportedException(); + return default!; + } + }); - private static readonly IFieldResolver JsonDateTime = CreateValueResolver((value, fieldContext, contex) => + private static readonly IFieldResolver JsonComponents = CreateValueResolver((value, fieldContext, contex) => + { + switch (value.Value) { - switch (value.Value) - { - case string s: - return s; - default: - ThrowHelper.NotSupportedException(); - return default!; - } - }); + case JsonArray a: + return a.Select(x => x.AsObject).ToList(); + default: + ThrowHelper.NotSupportedException(); + return default!; + } + }); - private static readonly IFieldResolver JsonNumber = CreateValueResolver((value, fieldContext, contex) => + private static readonly IFieldResolver JsonDateTime = CreateValueResolver((value, fieldContext, contex) => + { + switch (value.Value) { - switch (value.Value) - { - case double n: - return n; - default: - ThrowHelper.NotSupportedException(); - return default!; - } - }); + case string s: + return s; + default: + ThrowHelper.NotSupportedException(); + return default!; + } + }); - private static readonly IFieldResolver JsonString = CreateValueResolver((value, fieldContext, contex) => + private static readonly IFieldResolver JsonNumber = CreateValueResolver((value, fieldContext, contex) => + { + switch (value.Value) { - switch (value.Value) - { - case string s: - return s; - default: - ThrowHelper.NotSupportedException(); - return default!; - } - }); + case double n: + return n; + default: + ThrowHelper.NotSupportedException(); + return default!; + } + }); - private static readonly IFieldResolver JsonStrings = CreateValueResolver((value, fieldContext, contex) => + private static readonly IFieldResolver JsonString = CreateValueResolver((value, fieldContext, contex) => + { + switch (value.Value) { - switch (value.Value) - { - case JsonArray a: - return a.Select(x => x.ToString()).ToList(); - default: - ThrowHelper.NotSupportedException(); - return default!; - } - }); + case string s: + return s; + default: + ThrowHelper.NotSupportedException(); + return default!; + } + }); - private static readonly IFieldResolver Assets = CreateAsyncValueResolver((value, fieldContext, context) => + private static readonly IFieldResolver JsonStrings = CreateValueResolver((value, fieldContext, contex) => + { + switch (value.Value) { - var cacheDuration = fieldContext.CacheDuration(); + case JsonArray a: + return a.Select(x => x.ToString()).ToList(); + default: + ThrowHelper.NotSupportedException(); + return default!; + } + }); - return context.GetReferencedAssetsAsync(value, cacheDuration, fieldContext.CancellationToken); - }); + private static readonly IFieldResolver Assets = CreateAsyncValueResolver((value, fieldContext, context) => + { + var cacheDuration = fieldContext.CacheDuration(); - private static readonly IFieldResolver References = CreateAsyncValueResolver((value, fieldContext, context) => - { - var cacheDuration = fieldContext.CacheDuration(); + return context.GetReferencedAssetsAsync(value, cacheDuration, fieldContext.CancellationToken); + }); - return context.GetReferencedContentsAsync(value, cacheDuration, fieldContext.CancellationToken); - }); + private static readonly IFieldResolver References = CreateAsyncValueResolver((value, fieldContext, context) => + { + var cacheDuration = fieldContext.CacheDuration(); + + return context.GetReferencedContentsAsync(value, cacheDuration, fieldContext.CancellationToken); + }); - private readonly Builder builder; + private readonly Builder builder; - public FieldVisitor(Builder builder) + public FieldVisitor(Builder builder) + { + this.builder = builder; + } + + public FieldGraphSchema Visit(IArrayField field, FieldInfo args) + { + if (args.Fields.Count == 0) { - this.builder = builder; + return default; } - public FieldGraphSchema Visit(IArrayField field, FieldInfo args) + var type = builder.GetNested(args); + + if (type.Fields.Count == 0) { - if (args.Fields.Count == 0) - { - return default; - } + return default; + } - var type = builder.GetNested(args); + return new (new ListGraphType(new NonNullGraphType(type)), JsonComponents, null); + } - if (type.Fields.Count == 0) - { - return default; - } + public FieldGraphSchema Visit(IField<AssetsFieldProperties> field, FieldInfo args) + { + return new (SharedTypes.AssetsList, Assets, null); + } - return new (new ListGraphType(new NonNullGraphType(type)), JsonComponents, null); - } + public FieldGraphSchema Visit(IField<BooleanFieldProperties> field, FieldInfo args) + { + return new (Scalars.Boolean, JsonBoolean, null); + } - public FieldGraphSchema Visit(IField<AssetsFieldProperties> field, FieldInfo args) - { - return new (SharedTypes.AssetsList, Assets, null); - } + public FieldGraphSchema Visit(IField<ComponentFieldProperties> field, FieldInfo args) + { + var type = ResolveComponent(args, field.Properties.SchemaIds); - public FieldGraphSchema Visit(IField<BooleanFieldProperties> field, FieldInfo args) + if (type == null) { - return new (Scalars.Boolean, JsonBoolean, null); + return default; } - public FieldGraphSchema Visit(IField<ComponentFieldProperties> field, FieldInfo args) - { - var type = ResolveComponent(args, field.Properties.SchemaIds); + return new (type, JsonNoop, null); + } - if (type == null) - { - return default; - } + public FieldGraphSchema Visit(IField<ComponentsFieldProperties> field, FieldInfo args) + { + var type = ResolveComponent(args, field.Properties.SchemaIds); - return new (type, JsonNoop, null); + if (type == null) + { + return default; } - public FieldGraphSchema Visit(IField<ComponentsFieldProperties> field, FieldInfo args) - { - var type = ResolveComponent(args, field.Properties.SchemaIds); + return new (new ListGraphType(new NonNullGraphType(type)), JsonComponents, null); + } - if (type == null) - { - return default; - } + public FieldGraphSchema Visit(IField<DateTimeFieldProperties> field, FieldInfo args) + { + return new (Scalars.DateTime, JsonDateTime, null); + } - return new (new ListGraphType(new NonNullGraphType(type)), JsonComponents, null); - } + public FieldGraphSchema Visit(IField<JsonFieldProperties> field, FieldInfo args) + { + var schema = builder.GetDynamicTypes(field.Properties.GraphQLSchema); - public FieldGraphSchema Visit(IField<DateTimeFieldProperties> field, FieldInfo args) + if (schema.Length > 0) { - return new (Scalars.DateTime, JsonDateTime, null); + return new (schema[0], JsonNoop, null); } - public FieldGraphSchema Visit(IField<JsonFieldProperties> field, FieldInfo args) - { - var schema = builder.GetDynamicTypes(field.Properties.GraphQLSchema); + return new (Scalars.Json, JsonPath, ContentActions.Json.Arguments); + } - if (schema.Length > 0) - { - return new (schema[0], JsonNoop, null); - } + public FieldGraphSchema Visit(IField<GeolocationFieldProperties> field, FieldInfo args) + { + return new (Scalars.Json, JsonPath, ContentActions.Json.Arguments); + } - return new (Scalars.Json, JsonPath, ContentActions.Json.Arguments); - } + public FieldGraphSchema Visit(IField<NumberFieldProperties> field, FieldInfo args) + { + return new (Scalars.Float, JsonNumber, null); + } - public FieldGraphSchema Visit(IField<GeolocationFieldProperties> field, FieldInfo args) - { - return new (Scalars.Json, JsonPath, ContentActions.Json.Arguments); - } + public FieldGraphSchema Visit(IField<StringFieldProperties> field, FieldInfo args) + { + var type = Scalars.String; - public FieldGraphSchema Visit(IField<NumberFieldProperties> field, FieldInfo args) + if (field.Properties.IsEmbeddable) { - return new (Scalars.Float, JsonNumber, null); + type = builder.GetEmbeddableString(args, field.Properties); } - - public FieldGraphSchema Visit(IField<StringFieldProperties> field, FieldInfo args) + else if (field.Properties?.AllowedValues?.Count > 0 && field.Properties.CreateEnum) { - var type = Scalars.String; + var @enum = builder.GetEnumeration(args.EmbeddedEnumType, field.Properties.AllowedValues); - if (field.Properties.IsEmbeddable) + if (@enum != null) { - type = builder.GetEmbeddableString(args, field.Properties); + type = @enum; } - else if (field.Properties?.AllowedValues?.Count > 0 && field.Properties.CreateEnum) - { - var @enum = builder.GetEnumeration(args.EmbeddedEnumType, field.Properties.AllowedValues); + } - if (@enum != null) - { - type = @enum; - } - } + return new (type, JsonString, null); + } - return new (type, JsonString, null); - } + public FieldGraphSchema Visit(IField<TagsFieldProperties> field, FieldInfo args) + { + var type = Scalars.Strings; - public FieldGraphSchema Visit(IField<TagsFieldProperties> field, FieldInfo args) + if (field.Properties?.AllowedValues?.Count > 0 && field.Properties.CreateEnum) { - var type = Scalars.Strings; + var @enum = builder.GetEnumeration(args.EmbeddedEnumType, field.Properties.AllowedValues); - if (field.Properties?.AllowedValues?.Count > 0 && field.Properties.CreateEnum) + if (@enum != null) { - var @enum = builder.GetEnumeration(args.EmbeddedEnumType, field.Properties.AllowedValues); - - if (@enum != null) - { - type = new ListGraphType(new NonNullGraphType(@enum)); - } + type = new ListGraphType(new NonNullGraphType(@enum)); } - - return new (type, JsonStrings, null); } - public FieldGraphSchema Visit(IField<ReferencesFieldProperties> field, FieldInfo args) - { - var type = ResolveReferences(args, field.Properties.SchemaIds); + return new (type, JsonStrings, null); + } - if (type == null) - { - return default; - } + public FieldGraphSchema Visit(IField<ReferencesFieldProperties> field, FieldInfo args) + { + var type = ResolveReferences(args, field.Properties.SchemaIds); - return new (new ListGraphType(new NonNullGraphType(type)), References, null); + if (type == null) + { + return default; } - public FieldGraphSchema Visit(IField<UIFieldProperties> field, FieldInfo args) + return new (new ListGraphType(new NonNullGraphType(type)), References, null); + } + + public FieldGraphSchema Visit(IField<UIFieldProperties> field, FieldInfo args) + { + return default; + } + + private IGraphType? ResolveReferences(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + { + IGraphType? contentType = null; + + if (schemaIds?.Count == 1) { - return default; + contentType = builder.GetContentType(schemaIds[0]); } - private IGraphType? ResolveReferences(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + if (contentType == null) { - IGraphType? contentType = null; + var union = builder.GetReferenceUnion(fieldInfo, schemaIds); - if (schemaIds?.Count == 1) + if (!union.HasType) { - contentType = builder.GetContentType(schemaIds[0]); + return null; } - if (contentType == null) - { - var union = builder.GetReferenceUnion(fieldInfo, schemaIds); + contentType = union; + } - if (!union.HasType) - { - return null; - } + return contentType; + } - contentType = union; - } + private IGraphType? ResolveComponent(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + { + IGraphType? componentType = null; - return contentType; + if (schemaIds?.Count == 1) + { + componentType = builder.GetComponentType(schemaIds[0]); } - private IGraphType? ResolveComponent(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + if (componentType == null) { - IGraphType? componentType = null; + var union = builder.GetComponentUnion(fieldInfo, schemaIds); - if (schemaIds?.Count == 1) + if (!union.HasType) { - componentType = builder.GetComponentType(schemaIds[0]); + return null; } - if (componentType == null) - { - var union = builder.GetComponentUnion(fieldInfo, schemaIds); + componentType = union; + } + + return componentType; + } + + private static IFieldResolver CreateValueResolver<T>(ValueResolver<T> valueResolver) + { + return Resolvers.Sync<IReadOnlyDictionary<string, JsonValue>, object?>((source, fieldContext, context) => + { + var key = fieldContext.FieldDefinition.SourceName(); - if (!union.HasType) + if (source.TryGetValue(key, out var value)) + { + if (value == JsonValue.Null) { return null; } - componentType = union; + return valueResolver(value, fieldContext, context); } - return componentType; - } + return null; + }); + } - private static IFieldResolver CreateValueResolver<T>(ValueResolver<T> valueResolver) + private static IFieldResolver CreateAsyncValueResolver<T>(AsyncValueResolver<T> valueResolver) + { + return Resolvers.Async<IReadOnlyDictionary<string, JsonValue>, object?>(async (source, fieldContext, context) => { - return Resolvers.Sync<IReadOnlyDictionary<string, JsonValue>, object?>((source, fieldContext, context) => - { - var key = fieldContext.FieldDefinition.SourceName(); - - if (source.TryGetValue(key, out var value)) - { - if (value == JsonValue.Null) - { - return null; - } - - return valueResolver(value, fieldContext, context); - } - - return null; - }); - } + var key = fieldContext.FieldDefinition.SourceName(); - private static IFieldResolver CreateAsyncValueResolver<T>(AsyncValueResolver<T> valueResolver) - { - return Resolvers.Async<IReadOnlyDictionary<string, JsonValue>, object?>(async (source, fieldContext, context) => + if (source.TryGetValue(key, out var value)) { - var key = fieldContext.FieldDefinition.SourceName(); - - if (source.TryGetValue(key, out var value)) + if (value == JsonValue.Null) { - if (value == JsonValue.Null) - { - return null; - } - - return await valueResolver(value, fieldContext, context); + return null; } - return null; - }); - } + return await valueResolver(value, fieldContext, context); + } + + return null; + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedGraphType.cs index e1110d9e40..53cf1df32a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedGraphType.cs @@ -9,45 +9,44 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class NestedGraphType : ObjectGraphType<JsonObject> { - internal sealed class NestedGraphType : ObjectGraphType<JsonObject> + public NestedGraphType(Builder builder, FieldInfo fieldInfo) { - public NestedGraphType(Builder builder, FieldInfo fieldInfo) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.NestedType; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = fieldInfo.NestedType; - foreach (var nestedFieldInfo in fieldInfo.Fields) + foreach (var nestedFieldInfo in fieldInfo.Fields) + { + if (nestedFieldInfo.Field.IsComponentLike()) { - if (nestedFieldInfo.Field.IsComponentLike()) + AddField(new FieldType { - AddField(new FieldType - { - Name = nestedFieldInfo.FieldNameDynamic, - Arguments = ContentActions.Json.Arguments, - ResolvedType = Scalars.Json, - Resolver = FieldVisitor.JsonPath, - Description = nestedFieldInfo.Field.RawProperties.Hints - }).WithSourceName(nestedFieldInfo); - } + Name = nestedFieldInfo.FieldNameDynamic, + Arguments = ContentActions.Json.Arguments, + ResolvedType = Scalars.Json, + Resolver = FieldVisitor.JsonPath, + Description = nestedFieldInfo.Field.RawProperties.Hints + }).WithSourceName(nestedFieldInfo); + } - var (resolvedType, resolver, args) = builder.GetGraphType(nestedFieldInfo); + var (resolvedType, resolver, args) = builder.GetGraphType(nestedFieldInfo); - if (resolvedType != null && resolver != null) + if (resolvedType != null && resolver != null) + { + AddField(new FieldType { - AddField(new FieldType - { - Name = nestedFieldInfo.FieldName, - Arguments = args, - ResolvedType = resolvedType, - Resolver = resolver, - Description = nestedFieldInfo.Field.RawProperties.Hints - }).WithSourceName(nestedFieldInfo); - } + Name = nestedFieldInfo.FieldName, + Arguments = args, + ResolvedType = resolvedType, + Resolver = resolver, + Description = nestedFieldInfo.Field.RawProperties.Hints + }).WithSourceName(nestedFieldInfo); } - - Description = $"The structure of the {fieldInfo.DisplayName} nested schema."; } + + Description = $"The structure of the {fieldInfo.DisplayName} nested schema."; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedInputGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedInputGraphType.cs index b3fb8a8189..98fc0fca50 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedInputGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedInputGraphType.cs @@ -9,47 +9,46 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class NestedInputGraphType : InputObjectGraphType { - internal sealed class NestedInputGraphType : InputObjectGraphType + public NestedInputGraphType(Builder builder, FieldInfo fieldInfo) { - public NestedInputGraphType(Builder builder, FieldInfo fieldInfo) + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = fieldInfo.NestedInputType; + + foreach (var nestedFieldInfo in fieldInfo.Fields) { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.NestedInputType; + var resolvedType = builder.GetInputGraphType(nestedFieldInfo); - foreach (var nestedFieldInfo in fieldInfo.Fields) + if (resolvedType != null) { - var resolvedType = builder.GetInputGraphType(nestedFieldInfo); - - if (resolvedType != null) + AddField(new FieldType { - AddField(new FieldType - { - Name = nestedFieldInfo.FieldName, - ResolvedType = resolvedType, - Resolver = null, - Description = nestedFieldInfo.Field.RawProperties.Hints - }).WithSourceName(nestedFieldInfo); - } + Name = nestedFieldInfo.FieldName, + ResolvedType = resolvedType, + Resolver = null, + Description = nestedFieldInfo.Field.RawProperties.Hints + }).WithSourceName(nestedFieldInfo); } - - Description = $"The structure of the {fieldInfo.DisplayName} nested schema."; } - public override object ParseDictionary(IDictionary<string, object?> value) - { - var result = new JsonObject(); + Description = $"The structure of the {fieldInfo.DisplayName} nested schema."; + } - foreach (var field in Fields) + public override object ParseDictionary(IDictionary<string, object?> value) + { + var result = new JsonObject(); + + foreach (var field in Fields) + { + if (value.TryGetValue(field.Name, out var fieldValue)) { - if (value.TryGetValue(field.Name, out var fieldValue)) - { - result[field.SourceName()] = JsonGraphType.ParseJson(fieldValue); - } + result[field.SourceName()] = JsonGraphType.ParseJson(fieldValue); } - - return new JsonValue(result); } + + return new JsonValue(result); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ReferenceUnionGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ReferenceUnionGraphType.cs index a9afbba20d..04a7aa0bdb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ReferenceUnionGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ReferenceUnionGraphType.cs @@ -9,56 +9,55 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class ReferenceUnionGraphType : UnionGraphType { - internal sealed class ReferenceUnionGraphType : UnionGraphType - { - private readonly Dictionary<DomainId, IObjectGraphType> types = new Dictionary<DomainId, IObjectGraphType>(); + private readonly Dictionary<DomainId, IObjectGraphType> types = new Dictionary<DomainId, IObjectGraphType>(); - public bool HasType => types.Count > 0; + public bool HasType => types.Count > 0; - public ReferenceUnionGraphType(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.UnionReferenceType; + public ReferenceUnionGraphType(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) + { + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = fieldInfo.UnionReferenceType; - if (schemaIds?.Any() == true) + if (schemaIds?.Any() == true) + { + foreach (var schemaId in schemaIds) { - foreach (var schemaId in schemaIds) - { - var contentType = builder.GetContentType(schemaId); + var contentType = builder.GetContentType(schemaId); - if (contentType != null) - { - types[schemaId] = contentType; - } + if (contentType != null) + { + types[schemaId] = contentType; } } - else + } + else + { + foreach (var (key, value) in builder.GetAllContentTypes()) { - foreach (var (key, value) in builder.GetAllContentTypes()) - { - types[key.Schema.Id] = value; - } + types[key.Schema.Id] = value; } + } - if (HasType) + if (HasType) + { + foreach (var type in types) { - foreach (var type in types) - { - AddPossibleType(type.Value); - } + AddPossibleType(type.Value); + } - ResolveType = value => + ResolveType = value => + { + if (value is IContentEntity content) { - if (value is IContentEntity content) - { - return types.GetValueOrDefault(content.SchemaId.Id); - } + return types.GetValueOrDefault(content.SchemaId.Id); + } - return null; - }; - } + return null; + }; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs index 1d1ab297a1..92ebb11740 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs @@ -11,142 +11,141 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; + +internal sealed class SchemaInfo { - internal sealed class SchemaInfo - { - public ISchemaEntity Schema { get; } + public ISchemaEntity Schema { get; } - public string DisplayName => Schema.DisplayName(); + public string DisplayName => Schema.DisplayName(); - public string ComponentType { get; } + public string ComponentType { get; } - public string ContentType { get; } + public string ContentType { get; } - public string DataFlatType { get; } + public string DataFlatType { get; } - public string DataInputType { get; } + public string DataInputType { get; } - public string DataType { get; } + public string DataType { get; } - public string ContentResultType { get; } + public string ContentResultType { get; } - public string TypeName { get; } + public string TypeName { get; } - public IReadOnlyList<FieldInfo> Fields { get; init; } + public IReadOnlyList<FieldInfo> Fields { get; init; } - private SchemaInfo(ISchemaEntity schema, string typeName, ReservedNames typeNames) - { - Schema = schema; - - ComponentType = typeNames[$"{typeName}Component"]; - ContentResultType = typeNames[$"{typeName}ResultDto"]; - ContentType = typeName; - DataFlatType = typeNames[$"{typeName}FlatDataDto"]; - DataInputType = typeNames[$"{typeName}DataInputDto"]; - DataType = typeNames[$"{typeName}DataDto"]; - TypeName = typeName; - } + private SchemaInfo(ISchemaEntity schema, string typeName, ReservedNames typeNames) + { + Schema = schema; + + ComponentType = typeNames[$"{typeName}Component"]; + ContentResultType = typeNames[$"{typeName}ResultDto"]; + ContentType = typeName; + DataFlatType = typeNames[$"{typeName}FlatDataDto"]; + DataInputType = typeNames[$"{typeName}DataInputDto"]; + DataType = typeNames[$"{typeName}DataDto"]; + TypeName = typeName; + } - public override string ToString() - { - return TypeName; - } + public override string ToString() + { + return TypeName; + } - public static IEnumerable<SchemaInfo> Build(IEnumerable<ISchemaEntity> schemas, ReservedNames typeNames) + public static IEnumerable<SchemaInfo> Build(IEnumerable<ISchemaEntity> schemas, ReservedNames typeNames) + { + foreach (var schema in schemas.OrderBy(x => x.Created)) { - foreach (var schema in schemas.OrderBy(x => x.Created)) - { - var typeName = typeNames[schema.TypeName()]; + var typeName = typeNames[schema.TypeName()]; - yield return new SchemaInfo(schema, typeName, typeNames) - { - Fields = FieldInfo.Build(schema.SchemaDef.Fields, $"{typeName}Data", typeNames).ToList() - }; - } + yield return new SchemaInfo(schema, typeName, typeNames) + { + Fields = FieldInfo.Build(schema.SchemaDef.Fields, $"{typeName}Data", typeNames).ToList() + }; } } +} - internal sealed class FieldInfo - { - public IField Field { get; set; } +internal sealed class FieldInfo +{ + public IField Field { get; set; } - public string DisplayName => Field.DisplayName(); + public string DisplayName => Field.DisplayName(); - public string EmbeddableStringType { get; } + public string EmbeddableStringType { get; } - public string EmbeddedEnumType { get; } + public string EmbeddedEnumType { get; } - public string FieldName { get; } + public string FieldName { get; } - public string FieldNameDynamic { get; } + public string FieldNameDynamic { get; } - public string LocalizedInputType { get; } + public string LocalizedInputType { get; } - public string LocalizedType { get; } + public string LocalizedType { get; } - public string LocalizedTypeDynamic { get; } + public string LocalizedTypeDynamic { get; } - public string NestedInputType { get; } + public string NestedInputType { get; } - public string NestedType { get; } + public string NestedType { get; } - public string UnionComponentType { get; } + public string UnionComponentType { get; } - public string UnionReferenceType { get; } + public string UnionReferenceType { get; } - public IReadOnlyList<FieldInfo> Fields { get; init; } + public IReadOnlyList<FieldInfo> Fields { get; init; } - private FieldInfo(IField field, string fieldName, string typeName, ReservedNames typeNames) - { - Field = field; - - EmbeddableStringType = typeNames[$"{typeName}EmbeddableString"]; - EmbeddedEnumType = typeNames[$"{typeName}Enum"]; - FieldName = fieldName; - FieldNameDynamic = $"{fieldName}__Dynamic"; - LocalizedInputType = typeNames[$"{typeName}InputDto"]; - LocalizedType = typeNames[$"{typeName}Dto"]; - LocalizedTypeDynamic = typeNames[$"{typeName}Dto__Dynamic"]; - NestedInputType = typeNames[$"{typeName}ChildInputDto"]; - NestedType = typeNames[$"{typeName}ChildDto"]; - UnionComponentType = typeNames[$"{typeName}ComponentUnionDto"]; - UnionReferenceType = typeNames[$"{typeName}UnionDto"]; - } + private FieldInfo(IField field, string fieldName, string typeName, ReservedNames typeNames) + { + Field = field; + + EmbeddableStringType = typeNames[$"{typeName}EmbeddableString"]; + EmbeddedEnumType = typeNames[$"{typeName}Enum"]; + FieldName = fieldName; + FieldNameDynamic = $"{fieldName}__Dynamic"; + LocalizedInputType = typeNames[$"{typeName}InputDto"]; + LocalizedType = typeNames[$"{typeName}Dto"]; + LocalizedTypeDynamic = typeNames[$"{typeName}Dto__Dynamic"]; + NestedInputType = typeNames[$"{typeName}ChildInputDto"]; + NestedType = typeNames[$"{typeName}ChildDto"]; + UnionComponentType = typeNames[$"{typeName}ComponentUnionDto"]; + UnionReferenceType = typeNames[$"{typeName}UnionDto"]; + } - public override string ToString() - { - return FieldName; - } + public override string ToString() + { + return FieldName; + } + + internal static IEnumerable<FieldInfo> Build(IEnumerable<IField> fields, string typeName, ReservedNames typeNames) + { + var typeScope = ReservedNames.ForFields(); - internal static IEnumerable<FieldInfo> Build(IEnumerable<IField> fields, string typeName, ReservedNames typeNames) + foreach (var field in fields.ForApi()) { - var typeScope = ReservedNames.ForFields(); + // Field names must be unique within the scope of the parent type. + var fieldName = typeScope[field.Name.ToCamelCase()]; + + // Type names must be globally unique. + var fieldTypeName = typeNames[$"{typeName}{field.TypeName()}"]; - foreach (var field in fields.ForApi()) + var nested = new List<FieldInfo>(); + + if (field is IArrayField arrayField) { - // Field names must be unique within the scope of the parent type. - var fieldName = typeScope[field.Name.ToCamelCase()]; - - // Type names must be globally unique. - var fieldTypeName = typeNames[$"{typeName}{field.TypeName()}"]; - - var nested = new List<FieldInfo>(); - - if (field is IArrayField arrayField) - { - nested = Build(arrayField.Fields, fieldTypeName, typeNames).ToList(); - } - - yield return new FieldInfo( - field, - fieldName, - fieldTypeName, - typeNames) - { - Fields = nested - }; + nested = Build(arrayField.Fields, fieldTypeName, typeNames).ToList(); } + + yield return new FieldInfo( + field, + fieldName, + fieldTypeName, + typeNames) + { + Fields = nested + }; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/CacheDirective.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/CacheDirective.cs index 28e6ee40aa..7517a72d18 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/CacheDirective.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/CacheDirective.cs @@ -8,21 +8,20 @@ using GraphQL.Types; using GraphQLParser.AST; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives; + +public sealed class CacheDirective : Directive { - public sealed class CacheDirective : Directive + public CacheDirective() + : base("cache", DirectiveLocation.Field, DirectiveLocation.FragmentSpread, DirectiveLocation.InlineFragment) { - public CacheDirective() - : base("cache", DirectiveLocation.Field, DirectiveLocation.FragmentSpread, DirectiveLocation.InlineFragment) - { - Description = "Enable Memory Caching"; + Description = "Enable Memory Caching"; - Arguments = new QueryArguments(new QueryArgument<IntGraphType> - { - Name = "duration", - Description = "Cache duration in seconds.", - DefaultValue = 600 - }); - } + Arguments = new QueryArguments(new QueryArgument<IntGraphType> + { + Name = "duration", + Description = "Cache duration in seconds.", + DefaultValue = 600 + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Dynamic/DynamicResolver.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Dynamic/DynamicResolver.cs index 04e58019fa..67b0a7e095 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Dynamic/DynamicResolver.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Dynamic/DynamicResolver.cs @@ -11,65 +11,64 @@ #pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Dynamic +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Dynamic; + +internal sealed class DynamicResolver : IFieldResolver { - internal sealed class DynamicResolver : IFieldResolver - { - public static readonly DynamicResolver Instance = new DynamicResolver(); + public static readonly DynamicResolver Instance = new DynamicResolver(); - public async ValueTask<object?> ResolveAsync(IResolveFieldContext context) + public async ValueTask<object?> ResolveAsync(IResolveFieldContext context) + { + if (context.Source is JsonObject jsonObject) { - if (context.Source is JsonObject jsonObject) + var name = context.FieldDefinition.Name; + + if (!jsonObject.TryGetValue(name, out var jsonValue)) { - var name = context.FieldDefinition.Name; + return null; + } - if (!jsonObject.TryGetValue(name, out var jsonValue)) - { - return null; - } + var value = Convert(jsonValue); - var value = Convert(jsonValue); + return value; + } - return value; - } + var result = await NameFieldResolver.Instance.ResolveAsync(context); - var result = await NameFieldResolver.Instance.ResolveAsync(context); + return result; + } - return result; - } + private static object? Convert(JsonValue json) + { + var value = json.Value; - private static object? Convert(JsonValue json) + switch (value) { - var value = json.Value; + case double d: + { + var asInteger = (long)d; - switch (value) - { - case double d: + if (asInteger == d) { - var asInteger = (long)d; - - if (asInteger == d) - { - return asInteger; - } - - break; + return asInteger; } - case JsonArray a: - { - var result = new List<object?>(); + break; + } - foreach (var item in a) - { - result.Add(Convert(item)); - } + case JsonArray a: + { + var result = new List<object?>(); - return result; + foreach (var item in a) + { + result.Add(Convert(item)); } - } - return value; + return result; + } } + + return value; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Dynamic/DynamicSchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Dynamic/DynamicSchemaBuilder.cs index 069319baf3..b0de5b35cb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Dynamic/DynamicSchemaBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Dynamic/DynamicSchemaBuilder.cs @@ -7,64 +7,63 @@ using GraphQL.Types; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Dynamic +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Dynamic; + +internal static class DynamicSchemaBuilder { - internal static class DynamicSchemaBuilder + public static IGraphType[] ParseTypes(string? typeDefinitions, ReservedNames typeNames) { - public static IGraphType[] ParseTypes(string? typeDefinitions, ReservedNames typeNames) + if (string.IsNullOrWhiteSpace(typeDefinitions)) { - if (string.IsNullOrWhiteSpace(typeDefinitions)) - { - return Array.Empty<IGraphType>(); - } + return Array.Empty<IGraphType>(); + } - Schema schema; - try - { - schema = Schema.For(typeDefinitions); - } - catch - { - return Array.Empty<IGraphType>(); - } + Schema schema; + try + { + schema = Schema.For(typeDefinitions); + } + catch + { + return Array.Empty<IGraphType>(); + } - var map = schema.AdditionalTypeInstances.ToDictionary(x => x.Name); + var map = schema.AdditionalTypeInstances.ToDictionary(x => x.Name); - IGraphType? Convert(IGraphType? type) + IGraphType? Convert(IGraphType? type) + { + switch (type) { - switch (type) - { - case GraphQLTypeReference reference: - return map.GetValueOrDefault(reference.TypeName) ?? reference; - case NonNullGraphType nonNull: - return new NonNullGraphType(Convert(nonNull.ResolvedType)); - case ListGraphType list: - return new ListGraphType(Convert(list.ResolvedType)); - default: - return type; - } + case GraphQLTypeReference reference: + return map.GetValueOrDefault(reference.TypeName) ?? reference; + case NonNullGraphType nonNull: + return new NonNullGraphType(Convert(nonNull.ResolvedType)); + case ListGraphType list: + return new ListGraphType(Convert(list.ResolvedType)); + default: + return type; } + } - var result = new List<IGraphType>(); + var result = new List<IGraphType>(); - foreach (var type in schema.AdditionalTypeInstances) + foreach (var type in schema.AdditionalTypeInstances) + { + if (type is IComplexGraphType complexGraphType) { - if (type is IComplexGraphType complexGraphType) - { - type.Name = typeNames[type.Name]; + type.Name = typeNames[type.Name]; - foreach (var field in complexGraphType.Fields) - { - // Assign a resolver to support json values. - field.Resolver = DynamicResolver.Instance; - field.ResolvedType = Convert(field.ResolvedType); - } + foreach (var field in complexGraphType.Fields) + { + // Assign a resolver to support json values. + field.Resolver = DynamicResolver.Instance; + field.ResolvedType = Convert(field.ResolvedType); } - - result.Add(type); } - return result.ToArray(); + result.Add(type); } + + return result.ToArray(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ErrorVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ErrorVisitor.cs index 43a90e55c7..d5c4f2be2e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ErrorVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ErrorVisitor.cs @@ -14,107 +14,106 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +internal sealed class ErrorVisitor : BaseSchemaNodeVisitor { - internal sealed class ErrorVisitor : BaseSchemaNodeVisitor + public static readonly ErrorVisitor Instance = new ErrorVisitor(); + + internal sealed class ErrorResolver : IFieldResolver { - public static readonly ErrorVisitor Instance = new ErrorVisitor(); + private readonly IFieldResolver inner; - internal sealed class ErrorResolver : IFieldResolver + public ErrorResolver(IFieldResolver inner) { - private readonly IFieldResolver inner; + this.inner = inner; + } - public ErrorResolver(IFieldResolver inner) + public async ValueTask<object?> ResolveAsync(IResolveFieldContext context) + { + try { - this.inner = inner; + return await inner.ResolveAsync(context); } - - public async ValueTask<object?> ResolveAsync(IResolveFieldContext context) + catch (ValidationException ex) + { + throw new ExecutionError(ex.Message); + } + catch (DomainException ex) + { + throw new ExecutionError(ex.Message); + } + catch (Exception ex) { - try - { - return await inner.ResolveAsync(context); - } - catch (ValidationException ex) - { - throw new ExecutionError(ex.Message); - } - catch (DomainException ex) - { - throw new ExecutionError(ex.Message); - } - catch (Exception ex) - { - var logFactory = context.RequestServices!.GetRequiredService<ILoggerFactory>(); - - logFactory.CreateLogger("GraphQL").LogError(ex, "Failed to resolve field {field}.", context.FieldDefinition.Name); - throw; - } + var logFactory = context.RequestServices!.GetRequiredService<ILoggerFactory>(); + + logFactory.CreateLogger("GraphQL").LogError(ex, "Failed to resolve field {field}.", context.FieldDefinition.Name); + throw; } } + } + + internal sealed class ErrorSourceStreamResolver : ISourceStreamResolver + { + private readonly ISourceStreamResolver inner; - internal sealed class ErrorSourceStreamResolver : ISourceStreamResolver + public ErrorSourceStreamResolver(ISourceStreamResolver inner) { - private readonly ISourceStreamResolver inner; + this.inner = inner; + } - public ErrorSourceStreamResolver(ISourceStreamResolver inner) + public async ValueTask<IObservable<object?>> ResolveAsync(IResolveFieldContext context) + { + try { - this.inner = inner; + return await inner.ResolveAsync(context); } - - public async ValueTask<IObservable<object?>> ResolveAsync(IResolveFieldContext context) + catch (ValidationException ex) { - try - { - return await inner.ResolveAsync(context); - } - catch (ValidationException ex) - { - throw new ExecutionError(ex.Message); - } - catch (DomainException ex) - { - throw new ExecutionError(ex.Message); - } - catch (Exception ex) - { - var logFactory = context.RequestServices!.GetRequiredService<ILoggerFactory>(); - - logFactory.CreateLogger("GraphQL").LogError(ex, "Failed to resolve field {field}.", context.FieldDefinition.Name); - throw; - } + throw new ExecutionError(ex.Message); + } + catch (DomainException ex) + { + throw new ExecutionError(ex.Message); + } + catch (Exception ex) + { + var logFactory = context.RequestServices!.GetRequiredService<ILoggerFactory>(); + + logFactory.CreateLogger("GraphQL").LogError(ex, "Failed to resolve field {field}.", context.FieldDefinition.Name); + throw; } } + } + + private ErrorVisitor() + { + } - private ErrorVisitor() + public override void VisitObjectFieldDefinition(FieldType field, IObjectGraphType type, ISchema schema) + { + if (type.Name.StartsWith("__", StringComparison.Ordinal)) { + return; } - public override void VisitObjectFieldDefinition(FieldType field, IObjectGraphType type, ISchema schema) + if (field.StreamResolver != null) { - if (type.Name.StartsWith("__", StringComparison.Ordinal)) + if (field.StreamResolver is ErrorSourceStreamResolver) { return; } - if (field.StreamResolver != null) + field.StreamResolver = new ErrorSourceStreamResolver(field.StreamResolver); + } + else + { + if (field.Resolver is ErrorResolver) { - if (field.StreamResolver is ErrorSourceStreamResolver) - { - return; - } - - field.StreamResolver = new ErrorSourceStreamResolver(field.StreamResolver); + return; } - else - { - if (field.Resolver is ErrorResolver) - { - return; - } - field.Resolver = new ErrorResolver(field.Resolver ?? NameFieldResolver.Instance); - } + field.Resolver = new ErrorResolver(field.Resolver ?? NameFieldResolver.Instance); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs index 1671868b4d..cc8449f826 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs @@ -9,27 +9,26 @@ using Squidex.Infrastructure; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; + +internal static class EntityResolvers { - internal static class EntityResolvers - { - public static readonly IFieldResolver Id = Resolve<IEntity>(x => x.Id.ToString()); - public static readonly IFieldResolver Created = Resolve<IEntity>(x => x.Created.ToDateTimeUtc()); - public static readonly IFieldResolver CreatedBy = Resolve<IEntityWithCreatedBy>(x => x.CreatedBy.ToString()); - public static readonly IFieldResolver CreatedByUser = ResolveUser<IEntityWithCreatedBy>(x => x.CreatedBy); - public static readonly IFieldResolver LastModified = Resolve<IEntity>(x => x.LastModified.ToDateTimeUtc()); - public static readonly IFieldResolver LastModifiedBy = Resolve<IEntityWithLastModifiedBy>(x => x.LastModifiedBy.ToString()); - public static readonly IFieldResolver LastModifiedByUser = ResolveUser<IEntityWithLastModifiedBy>(x => x.LastModifiedBy); - public static readonly IFieldResolver Version = Resolve<IEntityWithVersion>(x => x.Version); + public static readonly IFieldResolver Id = Resolve<IEntity>(x => x.Id.ToString()); + public static readonly IFieldResolver Created = Resolve<IEntity>(x => x.Created.ToDateTimeUtc()); + public static readonly IFieldResolver CreatedBy = Resolve<IEntityWithCreatedBy>(x => x.CreatedBy.ToString()); + public static readonly IFieldResolver CreatedByUser = ResolveUser<IEntityWithCreatedBy>(x => x.CreatedBy); + public static readonly IFieldResolver LastModified = Resolve<IEntity>(x => x.LastModified.ToDateTimeUtc()); + public static readonly IFieldResolver LastModifiedBy = Resolve<IEntityWithLastModifiedBy>(x => x.LastModifiedBy.ToString()); + public static readonly IFieldResolver LastModifiedByUser = ResolveUser<IEntityWithLastModifiedBy>(x => x.LastModifiedBy); + public static readonly IFieldResolver Version = Resolve<IEntityWithVersion>(x => x.Version); - private static IFieldResolver Resolve<TSource>(Func<TSource, object> resolver) - { - return Resolvers.Sync(resolver); - } + private static IFieldResolver Resolve<TSource>(Func<TSource, object> resolver) + { + return Resolvers.Sync(resolver); + } - private static IFieldResolver ResolveUser<TSource>(Func<TSource, RefToken> resolver) - { - return Resolvers.Async<TSource, IUser?>((source, fieldContext, context) => context.FindUserAsync(resolver(source), fieldContext.CancellationToken)); - } + private static IFieldResolver ResolveUser<TSource>(Func<TSource, RefToken> resolver) + { + return Resolvers.Async<TSource, IUser?>((source, fieldContext, context) => context.FindUserAsync(resolver(source), fieldContext.CancellationToken)); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs index 5f551cfa4b..d29f1fa01c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs @@ -9,33 +9,32 @@ using GraphQL.Types; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; + +internal sealed class EntitySavedGraphType : SharedObjectGraphType<CommandResult> { - internal sealed class EntitySavedGraphType : SharedObjectGraphType<CommandResult> - { - public static readonly IGraphType Nullable = new EntitySavedGraphType(); + public static readonly IGraphType Nullable = new EntitySavedGraphType(); - public static readonly IGraphType NonNull = new NonNullGraphType(Nullable); + public static readonly IGraphType NonNull = new NonNullGraphType(Nullable); - private EntitySavedGraphType() - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "EntitySavedResultDto"; + private EntitySavedGraphType() + { + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "EntitySavedResultDto"; - AddField(new FieldType - { - Name = "version", - Resolver = ResolveVersion(), - ResolvedType = Scalars.NonNullLong, - Description = "The new version of the item." - }); + AddField(new FieldType + { + Name = "version", + Resolver = ResolveVersion(), + ResolvedType = Scalars.NonNullLong, + Description = "The new version of the item." + }); - Description = "The result of a mutation"; - } + Description = "The result of a mutation"; + } - private static IFieldResolver ResolveVersion() - { - return Resolvers.Sync<CommandResult, long>(x => x.NewVersion); - } + private static IFieldResolver ResolveVersion() + { + return Resolvers.Sync<CommandResult, long>(x => x.NewVersion); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs index 5b0253031a..744ae938aa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs @@ -9,29 +9,28 @@ using GraphQLParser.AST; using NodaTime.Text; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; + +public sealed class InstantGraphType : DateTimeGraphType { - public sealed class InstantGraphType : DateTimeGraphType + public override object? Serialize(object? value) { - public override object? Serialize(object? value) - { - return value; - } + return value; + } - public override object? ParseValue(object? value) - { - return InstantPattern.ExtendedIso.Parse(value?.ToString()!).Value; - } + public override object? ParseValue(object? value) + { + return InstantPattern.ExtendedIso.Parse(value?.ToString()!).Value; + } - public override object? ParseLiteral(GraphQLValue value) + public override object? ParseLiteral(GraphQLValue value) + { + switch (value) { - switch (value) - { - case GraphQLStringValue stringValue: - return ParseValue(stringValue.Value); - default: - return null; - } + case GraphQLStringValue stringValue: + return ParseValue(stringValue.Value); + default: + return null; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs index 66203771f6..0f2ac5e3e8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs @@ -9,111 +9,110 @@ using GraphQLParser.AST; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; + +public sealed class JsonGraphType : JsonNoopGraphType { - public sealed class JsonGraphType : JsonNoopGraphType + public override object? Serialize(object? value) { - public override object? Serialize(object? value) - { - return value; - } + return value; + } - public override object? ParseValue(object? value) - { - return ParseJson(value); - } + public override object? ParseValue(object? value) + { + return ParseJson(value); + } - public static JsonValue ParseJson(object? input) + public static JsonValue ParseJson(object? input) + { + switch (input) { - switch (input) - { - case GraphQLBooleanValue booleanValue: - return booleanValue.BoolValue; + case GraphQLBooleanValue booleanValue: + return booleanValue.BoolValue; - case GraphQLFloatValue floatValue: - return double.Parse((string)floatValue.Value, NumberStyles.Any, CultureInfo.InvariantCulture); + case GraphQLFloatValue floatValue: + return double.Parse((string)floatValue.Value, NumberStyles.Any, CultureInfo.InvariantCulture); - case GraphQLIntValue intValue: - return double.Parse((string)intValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture); + case GraphQLIntValue intValue: + return double.Parse((string)intValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture); - case GraphQLNullValue: - return default; + case GraphQLNullValue: + return default; - case GraphQLStringValue stringValue: - return (string)stringValue.Value; + case GraphQLStringValue stringValue: + return (string)stringValue.Value; - case GraphQLListValue listValue: - { - var json = new JsonArray(); + case GraphQLListValue listValue: + { + var json = new JsonArray(); - if (listValue.Values != null) + if (listValue.Values != null) + { + foreach (var item in listValue.Values) { - foreach (var item in listValue.Values) - { - json.Add(ParseJson(item)); - } + json.Add(ParseJson(item)); } - - return json; } - case GraphQLObjectValue objectValue: - { - var json = new JsonObject(); + return json; + } - if (objectValue.Fields != null) + case GraphQLObjectValue objectValue: + { + var json = new JsonObject(); + + if (objectValue.Fields != null) + { + foreach (var field in objectValue.Fields) { - foreach (var field in objectValue.Fields) - { - json[field.Name.ToString()] = ParseJson(field.Value); - } + json[field.Name.ToString()] = ParseJson(field.Value); } - - return json; } - case IEnumerable<object> list: - { - var json = new JsonArray(); + return json; + } - foreach (var item in list) - { - json.Add(ParseJson(item)); - } + case IEnumerable<object> list: + { + var json = new JsonArray(); - return json; + foreach (var item in list) + { + json.Add(ParseJson(item)); } - case IDictionary<string, object> obj: - { - var json = new JsonObject(); + return json; + } - foreach (var (key, value) in obj) - { - json[key] = ParseJson(value); - } + case IDictionary<string, object> obj: + { + var json = new JsonObject(); - return json; + foreach (var (key, value) in obj) + { + json[key] = ParseJson(value); } - default: - return JsonValue.Create(input); - } - } + return json; + } - public override object ParseLiteral(GraphQLValue value) - { - if (value is JsonValueNode jsonGraphType) - { - return jsonGraphType.Value; - } - - return value; + default: + return JsonValue.Create(input); } + } - public override GraphQLValue ToAST(object? value) + public override object ParseLiteral(GraphQLValue value) + { + if (value is JsonValueNode jsonGraphType) { - return new JsonValueNode(ParseJson(value)); + return jsonGraphType.Value; } + + return value; + } + + public override GraphQLValue ToAST(object? value) + { + return new JsonValueNode(ParseJson(value)); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonNoopGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonNoopGraphType.cs index f44e4a6fad..5bc4eef04b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonNoopGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonNoopGraphType.cs @@ -8,31 +8,30 @@ using GraphQL.Types; using GraphQLParser.AST; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; + +public class JsonNoopGraphType : ScalarGraphType { - public class JsonNoopGraphType : ScalarGraphType + public JsonNoopGraphType() { - public JsonNoopGraphType() - { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "JsonScalar"; + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "JsonScalar"; - Description = "Unstructured Json object"; - } + Description = "Unstructured Json object"; + } - public override object? ParseLiteral(GraphQLValue value) - { - return value; - } + public override object? ParseLiteral(GraphQLValue value) + { + return value; + } - public override object? ParseValue(object? value) - { - return value; - } + public override object? ParseValue(object? value) + { + return value; + } - public override object? Serialize(object? value) - { - return value; - } + public override object? Serialize(object? value) + { + return value; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs index 285bfe99e6..5aa475d252 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs @@ -8,17 +8,16 @@ using GraphQLParser.AST; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; + +internal sealed class JsonValueNode : GraphQLValue { - internal sealed class JsonValueNode : GraphQLValue - { - public override ASTNodeKind Kind => ASTNodeKind.ObjectValue; + public override ASTNodeKind Kind => ASTNodeKind.ObjectValue; - public JsonValue Value { get; } + public JsonValue Value { get; } - public JsonValueNode(JsonValue value) - { - Value = value; - } + public JsonValueNode(JsonValue value) + { + Value = value; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReservedNames.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReservedNames.cs index efe5c515cc..56c5dfb8c9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReservedNames.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReservedNames.cs @@ -7,74 +7,73 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +public sealed class ReservedNames { - public sealed class ReservedNames + private readonly Dictionary<string, int> takenNames; + + public string this[string name] { - private readonly Dictionary<string, int> takenNames; + get => GetName(name); + } - public string this[string name] - { - get => GetName(name); - } + private ReservedNames(Dictionary<string, int> takenNames) + { + this.takenNames = takenNames; + } - private ReservedNames(Dictionary<string, int> takenNames) - { - this.takenNames = takenNames; - } + public static ReservedNames ForFields() + { + var reserved = new Dictionary<string, int>(); + + return new ReservedNames(reserved); + } - public static ReservedNames ForFields() + public static ReservedNames ForTypes() + { + // Reserver names that are used for other GraphQL types. + var reserved = new Dictionary<string, int> { - var reserved = new Dictionary<string, int>(); + ["Asset"] = 1, + ["AssetResultDto"] = 1, + ["Content"] = 1, + ["Component"] = 1, + ["EnrichedAssetEvent"] = 1, + ["EnrichedContentEvent"] = 1, + ["EntityCreatedResultDto"] = 1, + ["EntitySavedResultDto"] = 1, + ["JsonObject"] = 1, + ["JsonScalar"] = 1, + ["JsonPrimitive"] = 1, + ["User"] = 1, + }; - return new ReservedNames(reserved); - } + return new ReservedNames(reserved); + } - public static ReservedNames ForTypes() - { - // Reserver names that are used for other GraphQL types. - var reserved = new Dictionary<string, int> - { - ["Asset"] = 1, - ["AssetResultDto"] = 1, - ["Content"] = 1, - ["Component"] = 1, - ["EnrichedAssetEvent"] = 1, - ["EnrichedContentEvent"] = 1, - ["EntityCreatedResultDto"] = 1, - ["EntitySavedResultDto"] = 1, - ["JsonObject"] = 1, - ["JsonScalar"] = 1, - ["JsonPrimitive"] = 1, - ["User"] = 1, - }; + private string GetName(string name) + { + Guard.NotNullOrEmpty(name); - return new ReservedNames(reserved); + if (!char.IsLetter(name[0])) + { + name = "gql_" + name; } - private string GetName(string name) + if (!takenNames.TryGetValue(name, out var offset)) { - Guard.NotNullOrEmpty(name); - - if (!char.IsLetter(name[0])) - { - name = "gql_" + name; - } - - if (!takenNames.TryGetValue(name, out var offset)) - { - // If the name is free, we do not add an offset. - takenNames[name] = 1; + // If the name is free, we do not add an offset. + takenNames[name] = 1; - return name; - } - else - { - // Add + 1 to all offsets for backwards-compatibility. - takenNames[name] = ++offset; + return name; + } + else + { + // Add + 1 to all offsets for backwards-compatibility. + takenNames[name] = ++offset; - return $"{name}{offset}"; - } + return $"{name}{offset}"; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs index e29520ad0d..8373469c44 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs @@ -15,80 +15,79 @@ using Squidex.Messaging.Subscriptions; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +public static class Resolvers { - public static class Resolvers + public static IFieldResolver Sync<TSource, T>(Func<TSource, T> resolver) { - public static IFieldResolver Sync<TSource, T>(Func<TSource, T> resolver) - { - return new FuncFieldResolver<TSource, T>(x => resolver(x.Source)); - } + return new FuncFieldResolver<TSource, T>(x => resolver(x.Source)); + } - public static IFieldResolver Sync<TSource, T>(Func<TSource, IResolveFieldContext, GraphQLExecutionContext, T> resolver) - { - return new FuncFieldResolver<TSource, T>(x => resolver(x.Source, x, (GraphQLExecutionContext)x.UserContext)); - } + public static IFieldResolver Sync<TSource, T>(Func<TSource, IResolveFieldContext, GraphQLExecutionContext, T> resolver) + { + return new FuncFieldResolver<TSource, T>(x => resolver(x.Source, x, (GraphQLExecutionContext)x.UserContext)); + } - public static IFieldResolver Async<TSource, T>(Func<TSource, ValueTask<T?>> resolver) - { - return new FuncFieldResolver<TSource, T>(x => resolver(x.Source)); - } + public static IFieldResolver Async<TSource, T>(Func<TSource, ValueTask<T?>> resolver) + { + return new FuncFieldResolver<TSource, T>(x => resolver(x.Source)); + } - public static IFieldResolver Async<TSource, T>(Func<TSource, IResolveFieldContext, GraphQLExecutionContext, ValueTask<T?>> resolver) - { - return new FuncFieldResolver<TSource, T>(x => resolver(x.Source, x, (GraphQLExecutionContext)x.UserContext)); - } + public static IFieldResolver Async<TSource, T>(Func<TSource, IResolveFieldContext, GraphQLExecutionContext, ValueTask<T?>> resolver) + { + return new FuncFieldResolver<TSource, T>(x => resolver(x.Source, x, (GraphQLExecutionContext)x.UserContext)); + } - public static IFieldResolver Command(string permissionId, Func<IResolveFieldContext, ICommand> action) + public static IFieldResolver Command(string permissionId, Func<IResolveFieldContext, ICommand> action) + { + return Async<object, object>(async (source, fieldContext, context) => { - return Async<object, object>(async (source, fieldContext, context) => - { - var schemaId = fieldContext.FieldDefinition.SchemaNamedId(); + var schemaId = fieldContext.FieldDefinition.SchemaNamedId(); - if (!context.Context.Allows(permissionId, schemaId?.Name ?? Permission.Any)) - { - throw new DomainForbiddenException(T.Get("common.errorNoPermission")); - } + if (!context.Context.Allows(permissionId, schemaId?.Name ?? Permission.Any)) + { + throw new DomainForbiddenException(T.Get("common.errorNoPermission")); + } - var command = action(fieldContext); + var command = action(fieldContext); - // The app identifier is set from the http context. - if (command is ISchemaCommand schemaCommand && schemaId != null) - { - schemaCommand.SchemaId = schemaId; - } + // The app identifier is set from the http context. + if (command is ISchemaCommand schemaCommand && schemaId != null) + { + schemaCommand.SchemaId = schemaId; + } - command.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any); + command.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any); - var commandContext = - await context.Resolve<ICommandBus>() - .PublishAsync(command, fieldContext.CancellationToken); + var commandContext = + await context.Resolve<ICommandBus>() + .PublishAsync(command, fieldContext.CancellationToken); - return commandContext.PlainResult!; - }); - } + return commandContext.PlainResult!; + }); + } - public static ISourceStreamResolver Stream(string permissionId, Func<IResolveFieldContext, AppSubscription> action) + public static ISourceStreamResolver Stream(string permissionId, Func<IResolveFieldContext, AppSubscription> action) + { + return new SourceStreamResolver<object>(fieldContext => { - return new SourceStreamResolver<object>(fieldContext => - { - var context = (GraphQLExecutionContext)fieldContext.UserContext; + var context = (GraphQLExecutionContext)fieldContext.UserContext; - if (!context.Context.UserPermissions.Includes(PermissionIds.ForApp(permissionId, context.Context.App.Name))) - { - throw new DomainForbiddenException(T.Get("common.errorNoPermission")); - } + if (!context.Context.UserPermissions.Includes(PermissionIds.ForApp(permissionId, context.Context.App.Name))) + { + throw new DomainForbiddenException(T.Get("common.errorNoPermission")); + } - var subscription = action(fieldContext); + var subscription = action(fieldContext); - // The app id is taken from the URL so we cannot get events from other apps. - subscription.AppId = context.Context.App.Id; + // The app id is taken from the URL so we cannot get events from other apps. + subscription.AppId = context.Context.App.Id; - // We also check the subscriptions on the source server. - subscription.Permissions = context.Context.UserPermissions; + // We also check the subscriptions on the source server. + subscription.Permissions = context.Context.UserPermissions; - return context.Resolve<ISubscriptionService>().Subscribe<object>(subscription); - }); - } + return context.Resolve<ISubscriptionService>().Subscribe<object>(subscription); + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Scalars.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Scalars.cs index 5c51f37e3a..eeec9efb3a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Scalars.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Scalars.cs @@ -10,52 +10,51 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +public static class Scalars { - public static class Scalars - { - public static readonly IGraphType Int = new IntGraphType(); + public static readonly IGraphType Int = new IntGraphType(); - public static readonly IGraphType Long = new LongGraphType(); + public static readonly IGraphType Long = new LongGraphType(); - public static readonly IGraphType Json = new JsonGraphType(); + public static readonly IGraphType Json = new JsonGraphType(); - public static readonly IGraphType JsonNoop = new JsonNoopGraphType(); + public static readonly IGraphType JsonNoop = new JsonNoopGraphType(); - public static readonly IGraphType Float = new FloatGraphType(); + public static readonly IGraphType Float = new FloatGraphType(); - public static readonly IGraphType String = new StringGraphType(); + public static readonly IGraphType String = new StringGraphType(); - public static readonly IGraphType Strings = new ListGraphType(new NonNullGraphType(new StringGraphType())); + public static readonly IGraphType Strings = new ListGraphType(new NonNullGraphType(new StringGraphType())); - public static readonly IGraphType Boolean = new BooleanGraphType(); + public static readonly IGraphType Boolean = new BooleanGraphType(); - public static readonly IGraphType DateTime = new InstantGraphType(); + public static readonly IGraphType DateTime = new InstantGraphType(); - public static readonly IGraphType AssetType = new EnumerationGraphType<AssetType>(); + public static readonly IGraphType AssetType = new EnumerationGraphType<AssetType>(); - public static readonly IGraphType EnrichedAssetEventType = new EnumerationGraphType<EnrichedAssetEventType>(); + public static readonly IGraphType EnrichedAssetEventType = new EnumerationGraphType<EnrichedAssetEventType>(); - public static readonly IGraphType EnrichedContentEventType = new EnumerationGraphType<EnrichedContentEventType>(); + public static readonly IGraphType EnrichedContentEventType = new EnumerationGraphType<EnrichedContentEventType>(); - public static readonly IGraphType NonNullInt = new NonNullGraphType(Int); + public static readonly IGraphType NonNullInt = new NonNullGraphType(Int); - public static readonly IGraphType NonNullLong = new NonNullGraphType(Long); + public static readonly IGraphType NonNullLong = new NonNullGraphType(Long); - public static readonly IGraphType NonNullFloat = new NonNullGraphType(Float); + public static readonly IGraphType NonNullFloat = new NonNullGraphType(Float); - public static readonly IGraphType NonNullString = new NonNullGraphType(String); + public static readonly IGraphType NonNullString = new NonNullGraphType(String); - public static readonly IGraphType NonNullStrings = new NonNullGraphType(Strings); + public static readonly IGraphType NonNullStrings = new NonNullGraphType(Strings); - public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean); + public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean); - public static readonly IGraphType NonNullDateTime = new NonNullGraphType(DateTime); + public static readonly IGraphType NonNullDateTime = new NonNullGraphType(DateTime); - public static readonly IGraphType NonNullAssetType = new NonNullGraphType(AssetType); + public static readonly IGraphType NonNullAssetType = new NonNullGraphType(AssetType); - public static readonly IGraphType NonNullEnrichedAssetEventType = new NonNullGraphType(EnrichedAssetEventType); + public static readonly IGraphType NonNullEnrichedAssetEventType = new NonNullGraphType(EnrichedAssetEventType); - public static readonly IGraphType NonNullEnrichedContentEventType = new NonNullGraphType(EnrichedContentEventType); - } + public static readonly IGraphType NonNullEnrichedContentEventType = new NonNullGraphType(EnrichedContentEventType); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs index 126ffff4e9..794aae4f5f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs @@ -13,127 +13,126 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.ObjectPool; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +public static class SharedExtensions { - public static class SharedExtensions + internal static string BuildODataQuery(this IResolveFieldContext context) { - internal static string BuildODataQuery(this IResolveFieldContext context) + var sb = DefaultPools.StringBuilder.Get(); + try { - var sb = DefaultPools.StringBuilder.Get(); - try - { - sb.Append('?'); + sb.Append('?'); - if (context.Arguments != null) + if (context.Arguments != null) + { + foreach (var (key, value) in context.Arguments) { - foreach (var (key, value) in context.Arguments) + var formatted = value.Value?.ToString(); + + if (!string.IsNullOrWhiteSpace(formatted)) { - var formatted = value.Value?.ToString(); + if (key == "search") + { + formatted = $"\"{formatted.Trim('"')}\""; + } - if (!string.IsNullOrWhiteSpace(formatted)) + if (sb.Length > 1) { - if (key == "search") - { - formatted = $"\"{formatted.Trim('"')}\""; - } - - if (sb.Length > 1) - { - sb.Append('&'); - } - - sb.Append('$'); - sb.Append(key); - sb.Append('='); - sb.Append(formatted); + sb.Append('&'); } + + sb.Append('$'); + sb.Append(key); + sb.Append('='); + sb.Append(formatted); } } - - return sb.ToString(); } - finally - { - DefaultPools.StringBuilder.Return(sb); - } - } - - public static bool IsValidName(this string? name, NamedElement type) - { - try - { - NameValidator.ValidateDefault(name!, type); - return true; - } - catch - { - return false; - } + return sb.ToString(); } - - internal static FieldType WithSourceName(this FieldType field, string value) + finally { - return field.WithMetadata(nameof(SourceName), value); + DefaultPools.StringBuilder.Return(sb); } + } - internal static FieldType WithSourceName(this FieldType field, FieldInfo value) + public static bool IsValidName(this string? name, NamedElement type) + { + try { - return field.WithMetadata(nameof(SourceName), value.Field.Name); - } + NameValidator.ValidateDefault(name!, type); - internal static string SourceName(this FieldType field) - { - return field.GetMetadata<string>(nameof(SourceName))!; + return true; } - - internal static FieldType WithSchemaId(this FieldType field, SchemaInfo value) + catch { - return field.WithMetadata(nameof(SchemaId), value.Schema.Id.ToString()); + return false; } + } - internal static string SchemaId(this FieldType field) - { - return field.GetMetadata<string>(nameof(SchemaId))!; - } + internal static FieldType WithSourceName(this FieldType field, string value) + { + return field.WithMetadata(nameof(SourceName), value); + } - internal static FieldType WithSchemaNamedId(this FieldType field, SchemaInfo value) - { - return field.WithMetadata(nameof(SchemaNamedId), value.Schema.NamedId()); - } + internal static FieldType WithSourceName(this FieldType field, FieldInfo value) + { + return field.WithMetadata(nameof(SourceName), value.Field.Name); + } - internal static NamedId<DomainId> SchemaNamedId(this FieldType field) - { - return field.GetMetadata<NamedId<DomainId>>(nameof(SchemaNamedId))!; - } + internal static string SourceName(this FieldType field) + { + return field.GetMetadata<string>(nameof(SourceName))!; + } - internal static IGraphType? Flatten(this QueryArgument type) - { - return type.ResolvedType?.Flatten(); - } + internal static FieldType WithSchemaId(this FieldType field, SchemaInfo value) + { + return field.WithMetadata(nameof(SchemaId), value.Schema.Id.ToString()); + } - public static IGraphType? Flatten(this IGraphType type) - { - if (type is IProvideResolvedType provider) - { - return provider.ResolvedType?.Flatten(); - } + internal static string SchemaId(this FieldType field) + { + return field.GetMetadata<string>(nameof(SchemaId))!; + } - return type; - } + internal static FieldType WithSchemaNamedId(this FieldType field, SchemaInfo value) + { + return field.WithMetadata(nameof(SchemaNamedId), value.Schema.NamedId()); + } - public static TimeSpan CacheDuration(this IResolveFieldContext context) + internal static NamedId<DomainId> SchemaNamedId(this FieldType field) + { + return field.GetMetadata<NamedId<DomainId>>(nameof(SchemaNamedId))!; + } + + internal static IGraphType? Flatten(this QueryArgument type) + { + return type.ResolvedType?.Flatten(); + } + + public static IGraphType? Flatten(this IGraphType type) + { + if (type is IProvideResolvedType provider) { - var cacheDirective = context.GetDirective("cache"); + return provider.ResolvedType?.Flatten(); + } - if (cacheDirective != null) - { - var duration = cacheDirective.GetArgument<int>("duration"); + return type; + } - return TimeSpan.FromSeconds(duration); - } + public static TimeSpan CacheDuration(this IResolveFieldContext context) + { + var cacheDirective = context.GetDirective("cache"); - return TimeSpan.Zero; + if (cacheDirective != null) + { + var duration = cacheDirective.GetArgument<int>("duration"); + + return TimeSpan.FromSeconds(duration); } + + return TimeSpan.Zero; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedObjectGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedObjectGraphType.cs index 8549d390e4..ae1f62a6e5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedObjectGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedObjectGraphType.cs @@ -7,20 +7,19 @@ using GraphQL.Types; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +internal abstract class SharedObjectGraphType<T> : ObjectGraphType<T> { - internal abstract class SharedObjectGraphType<T> : ObjectGraphType<T> + public override void Initialize(ISchema schema) { - public override void Initialize(ISchema schema) + try + { + base.Initialize(schema); + } + catch (InvalidOperationException) { - try - { - base.Initialize(schema); - } - catch (InvalidOperationException) - { - return; - } + return; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs index 964ff44811..e9b55100ed 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedTypes.cs @@ -10,47 +10,46 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +public static class SharedTypes { - public static class SharedTypes + public static readonly IGraphType Asset = new AssetGraphType(); + + public static readonly IGraphType AssetsList = new ListGraphType(new NonNullGraphType(Asset)); + + public static readonly IGraphType AssetsResult = new AssetsResultGraphType(AssetsList); + + public static readonly IGraphType EnrichedAssetEvent = new EnrichedAssetEventGraphType(); + + public static readonly IGraphType EnrichedContentEvent = new EnrichedContentEventGraphType(); + + public static readonly CacheDirective MemoryCacheDirective = new CacheDirective(); + + public static readonly FieldType FindAsset = new FieldType + { + Name = "findAsset", + Arguments = AssetActions.Find.Arguments, + ResolvedType = Asset, + Resolver = AssetActions.Find.Resolver, + Description = "Find an asset by id." + }; + + public static readonly FieldType QueryAssets = new FieldType + { + Name = "queryAssets", + Arguments = AssetActions.Query.Arguments, + ResolvedType = new NonNullGraphType(AssetsList), + Resolver = AssetActions.Query.Resolver, + Description = "Get assets." + }; + + public static readonly FieldType QueryAssetsWithTotal = new FieldType { - public static readonly IGraphType Asset = new AssetGraphType(); - - public static readonly IGraphType AssetsList = new ListGraphType(new NonNullGraphType(Asset)); - - public static readonly IGraphType AssetsResult = new AssetsResultGraphType(AssetsList); - - public static readonly IGraphType EnrichedAssetEvent = new EnrichedAssetEventGraphType(); - - public static readonly IGraphType EnrichedContentEvent = new EnrichedContentEventGraphType(); - - public static readonly CacheDirective MemoryCacheDirective = new CacheDirective(); - - public static readonly FieldType FindAsset = new FieldType - { - Name = "findAsset", - Arguments = AssetActions.Find.Arguments, - ResolvedType = Asset, - Resolver = AssetActions.Find.Resolver, - Description = "Find an asset by id." - }; - - public static readonly FieldType QueryAssets = new FieldType - { - Name = "queryAssets", - Arguments = AssetActions.Query.Arguments, - ResolvedType = new NonNullGraphType(AssetsList), - Resolver = AssetActions.Query.Resolver, - Description = "Get assets." - }; - - public static readonly FieldType QueryAssetsWithTotal = new FieldType - { - Name = "queryAssetsWithTotal", - Arguments = AssetActions.Query.Arguments, - ResolvedType = new NonNullGraphType(AssetsResult), - Resolver = AssetActions.Query.ResolverWithTotal, - Description = "Get assets and total count." - }; - } + Name = "queryAssetsWithTotal", + Arguments = AssetActions.Query.Arguments, + ResolvedType = new NonNullGraphType(AssetsResult), + Resolver = AssetActions.Query.ResolverWithTotal, + Description = "Get assets and total count." + }; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/UserGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/UserGraphType.cs index a8df74947d..1746033f7b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/UserGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/UserGraphType.cs @@ -11,49 +11,48 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; + +internal sealed class UserGraphType : SharedObjectGraphType<IUser> { - internal sealed class UserGraphType : SharedObjectGraphType<IUser> + public static readonly IGraphType Nullable = new UserGraphType(); + + public static readonly IGraphType NonNull = new NonNullGraphType(Nullable); + + public UserGraphType() { - public static readonly IGraphType Nullable = new UserGraphType(); + // The name is used for equal comparison. Therefore it is important to treat it as readonly. + Name = "User"; - public static readonly IGraphType NonNull = new NonNullGraphType(Nullable); + AddField(new FieldType + { + Name = "id", + Resolver = Resolve(x => x.Id), + ResolvedType = Scalars.NonNullString, + Description = FieldDescriptions.UserId + }); - public UserGraphType() + AddField(new FieldType { - // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = "User"; - - AddField(new FieldType - { - Name = "id", - Resolver = Resolve(x => x.Id), - ResolvedType = Scalars.NonNullString, - Description = FieldDescriptions.UserId - }); - - AddField(new FieldType - { - Name = "displayName", - Resolver = Resolve(x => x.Claims.DisplayName()), - ResolvedType = Scalars.String, - Description = FieldDescriptions.UserDisplayName - }); - - AddField(new FieldType - { - Name = "email", - Resolver = Resolve(x => x.Email), - ResolvedType = Scalars.String, - Description = FieldDescriptions.UserEmail - }); - - Description = "A user that created or modified a content or asset."; - } - - private static IFieldResolver Resolve<T>(Func<IUser, T> resolver) + Name = "displayName", + Resolver = Resolve(x => x.Claims.DisplayName()), + ResolvedType = Scalars.String, + Description = FieldDescriptions.UserDisplayName + }); + + AddField(new FieldType { - return Resolvers.Sync(resolver); - } + Name = "email", + Resolver = Resolve(x => x.Email), + ResolvedType = Scalars.String, + Description = FieldDescriptions.UserEmail + }); + + Description = "A user that created or modified a content or asset."; + } + + private static IFieldResolver Resolve<T>(Func<IUser, T> resolver) + { + return Resolvers.Sync(resolver); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentCache.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentCache.cs index 7e5085816e..bb27f67b11 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentCache.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentCache.cs @@ -8,9 +8,8 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public interface IContentCache : IQueryCache<DomainId, IEnrichedContentEntity> { - public interface IContentCache : IQueryCache<DomainId, IEnrichedContentEntity> - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs index 998bda1be9..f2fa92555b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs @@ -8,26 +8,25 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public interface IContentEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion { - public interface IContentEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - NamedId<DomainId> AppId { get; } + NamedId<DomainId> AppId { get; } - NamedId<DomainId> SchemaId { get; } + NamedId<DomainId> SchemaId { get; } - Status? NewStatus { get; } + Status? NewStatus { get; } - Status Status { get; } + Status Status { get; } - ContentData Data { get; } + ContentData Data { get; } - ScheduleJob? ScheduleJob { get; } + ScheduleJob? ScheduleJob { get; } - bool IsDeleted { get; } - } + bool IsDeleted { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs index 2667b74124..4d4b2c4581 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public interface IContentLoader { - public interface IContentLoader - { - Task<IContentEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default); - } + Task<IContentEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index a2ec2c824d..b5a9cfd3d4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -8,23 +8,22 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public interface IContentQueryService { - public interface IContentQueryService - { - Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, Q q, - CancellationToken ct = default); + Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, Q q, + CancellationToken ct = default); - Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query, - CancellationToken ct = default); + Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query, + CancellationToken ct = default); - Task<IEnrichedContentEntity?> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default); + Task<IEnrichedContentEntity?> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); - Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName, - CancellationToken ct = default); + Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName, + CancellationToken ct = default); - Task<ISchemaEntity?> GetSchemaAsync(Context context, string schemaIdOrNama, - CancellationToken ct = default); - } + Task<ISchemaEntity?> GetSchemaAsync(Context context, string schemaIdOrNama, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index ebcc62bc45..7caf8d78cb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -9,26 +9,25 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public interface IContentWorkflow { - public interface IContentWorkflow - { - ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema); + ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema); - ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user); + ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user); - ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user); + ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user); - ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user); + ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user); - ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user); + ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user); - ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status); + ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status); - ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status); + ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status); - ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user); + ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user); - ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema); - } + ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs index a26426d10c..a6638097df 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs @@ -8,28 +8,27 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public interface IEnrichedContentEntity : IContentEntity { - public interface IEnrichedContentEntity : IContentEntity - { - bool CanUpdate { get; } + bool CanUpdate { get; } - bool IsSingleton { get; } + bool IsSingleton { get; } - string StatusColor { get; } + string StatusColor { get; } - string? NewStatusColor { get; } + string? NewStatusColor { get; } - string? ScheduledStatusColor { get; } + string? ScheduledStatusColor { get; } - string SchemaDisplayName { get; } + string SchemaDisplayName { get; } - string? EditToken { get; } + string? EditToken { get; } - RootField[]? ReferenceFields { get; } + RootField[]? ReferenceFields { get; } - StatusInfo[]? NextStatuses { get; } + StatusInfo[]? NextStatuses { get; } - ContentData? ReferenceData { get; } - } + ContentData? ReferenceData { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs index 9ca8eac66c..db0b6a4b62 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs @@ -8,10 +8,9 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public interface IWorkflowsValidator { - public interface IWorkflowsValidator - { - Task<IReadOnlyList<string>> ValidateAsync(DomainId appId, Workflows workflows); - } + Task<IReadOnlyList<string>> ValidateAsync(DomainId appId, Workflows workflows); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 972e85a721..9640f2e680 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -10,106 +10,105 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public sealed class ContentEnricher : IContentEnricher { - public sealed class ContentEnricher : IContentEnricher - { - private readonly IEnumerable<IContentEnricherStep> steps; - private readonly IAppProvider appProvider; + private readonly IEnumerable<IContentEnricherStep> steps; + private readonly IAppProvider appProvider; - public ContentEnricher(IEnumerable<IContentEnricherStep> steps, IAppProvider appProvider) - { - this.steps = steps; + public ContentEnricher(IEnumerable<IContentEnricherStep> steps, IAppProvider appProvider) + { + this.steps = steps; - this.appProvider = appProvider; - } + this.appProvider = appProvider; + } - public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, bool cloneData, Context context, - CancellationToken ct) - { - Guard.NotNull(content); + public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, bool cloneData, Context context, + CancellationToken ct) + { + Guard.NotNull(content); - var enriched = await EnrichInternalAsync(Enumerable.Repeat(content, 1), cloneData, context, ct); + var enriched = await EnrichInternalAsync(Enumerable.Repeat(content, 1), cloneData, context, ct); - return enriched[0]; - } + return enriched[0]; + } - public Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, Context context, - CancellationToken ct) - { - Guard.NotNull(contents); - Guard.NotNull(context); + public Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, Context context, + CancellationToken ct) + { + Guard.NotNull(contents); + Guard.NotNull(context); - return EnrichInternalAsync(contents, false, context, ct); - } + return EnrichInternalAsync(contents, false, context, ct); + } - private async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichInternalAsync(IEnumerable<IContentEntity> contents, bool cloneData, Context context, - CancellationToken ct) + private async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichInternalAsync(IEnumerable<IContentEntity> contents, bool cloneData, Context context, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("ContentEnricher/EnrichInternalAsync")) { - using (Telemetry.Activities.StartActivity("ContentEnricher/EnrichInternalAsync")) - { - var results = new List<ContentEntity>(); + var results = new List<ContentEntity>(); - if (context.App != null) + if (context.App != null) + { + foreach (var step in steps) { - foreach (var step in steps) - { - await step.EnrichAsync(context, ct); - } + await step.EnrichAsync(context, ct); } + } - if (contents.Any()) + if (contents.Any()) + { + foreach (var content in contents) { - foreach (var content in contents) - { - var result = SimpleMapper.Map(content, new ContentEntity()); + var result = SimpleMapper.Map(content, new ContentEntity()); - if (cloneData) + if (cloneData) + { + using (Telemetry.Activities.StartActivity("ContentEnricher/CloneData")) { - using (Telemetry.Activities.StartActivity("ContentEnricher/CloneData")) - { - result.Data = result.Data.Clone(); - } + result.Data = result.Data.Clone(); } - - results.Add(result); } - if (context.App != null) - { - var schemaCache = new Dictionary<DomainId, Task<(ISchemaEntity, ResolvedComponents)>>(); + results.Add(result); + } - Task<(ISchemaEntity, ResolvedComponents)> GetSchema(DomainId id) + if (context.App != null) + { + var schemaCache = new Dictionary<DomainId, Task<(ISchemaEntity, ResolvedComponents)>>(); + + Task<(ISchemaEntity, ResolvedComponents)> GetSchema(DomainId id) + { + return schemaCache.GetOrAdd(id, async x => { - return schemaCache.GetOrAdd(id, async x => + var schema = await appProvider.GetSchemaAsync(context.App.Id, x, false, ct); + + if (schema == null) { - var schema = await appProvider.GetSchemaAsync(context.App.Id, x, false, ct); + throw new DomainObjectNotFoundException(x.ToString()); + } - if (schema == null) - { - throw new DomainObjectNotFoundException(x.ToString()); - } + var components = await appProvider.GetComponentsAsync(schema, ct); - var components = await appProvider.GetComponentsAsync(schema, ct); + return (schema, components); + }); + } - return (schema, components); - }); - } + foreach (var step in steps) + { + ct.ThrowIfCancellationRequested(); - foreach (var step in steps) + using (Telemetry.Activities.StartActivity(step.ToString()!)) { - ct.ThrowIfCancellationRequested(); - - using (Telemetry.Activities.StartActivity(step.ToString()!)) - { - await step.EnrichAsync(context, results, GetSchema, ct); - } + await step.EnrichAsync(context, results, GetSchema, ct); } } } - - return results; } + + return results; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs index 85efb1cfd4..12d0316450 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs @@ -9,57 +9,56 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public sealed class ContentLoader : IContentLoader { - public sealed class ContentLoader : IContentLoader + private readonly IDomainObjectFactory domainObjectFactory; + private readonly IDomainObjectCache domainObjectCache; + + public ContentLoader(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) + { + this.domainObjectFactory = domainObjectFactory; + this.domainObjectCache = domainObjectCache; + } + + public async Task<IContentEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default) { - private readonly IDomainObjectFactory domainObjectFactory; - private readonly IDomainObjectCache domainObjectCache; + var uniqueId = DomainId.Combine(appId, id); + + var content = await GetCachedAsync(uniqueId, version, ct); - public ContentLoader(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) + if (content == null) { - this.domainObjectFactory = domainObjectFactory; - this.domainObjectCache = domainObjectCache; + content = await GetAsync(uniqueId, version, ct); } - public async Task<IContentEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default) + if (content is not { Version: > EtagVersion.Empty } || (version > EtagVersion.Any && content.Version != version)) { - var uniqueId = DomainId.Combine(appId, id); - - var content = await GetCachedAsync(uniqueId, version, ct); - - if (content == null) - { - content = await GetAsync(uniqueId, version, ct); - } - - if (content is not { Version: > EtagVersion.Empty } || (version > EtagVersion.Any && content.Version != version)) - { - return null; - } - - return content; + return null; } - private async Task<ContentDomainObject.State?> GetCachedAsync(DomainId uniqueId, long version, - CancellationToken ct) + return content; + } + + private async Task<ContentDomainObject.State?> GetCachedAsync(DomainId uniqueId, long version, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("ContentLoader/GetCachedAsync")) { - using (Telemetry.Activities.StartActivity("ContentLoader/GetCachedAsync")) - { - return await domainObjectCache.GetAsync<ContentDomainObject.State>(uniqueId, version, ct); - } + return await domainObjectCache.GetAsync<ContentDomainObject.State>(uniqueId, version, ct); } + } - private async Task<ContentDomainObject.State> GetAsync(DomainId uniqueId, long version, - CancellationToken ct) + private async Task<ContentDomainObject.State> GetAsync(DomainId uniqueId, long version, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("ContentLoader/GetAsync")) { - using (Telemetry.Activities.StartActivity("ContentLoader/GetAsync")) - { - var contentObject = domainObjectFactory.Create<ContentDomainObject>(uniqueId); + var contentObject = domainObjectFactory.Create<ContentDomainObject>(uniqueId); - return await contentObject.GetSnapshotAsync(version, ct); - } + return await contentObject.GetSnapshotAsync(version, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs index c9aba42fde..0a1c1bb85b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -23,251 +23,250 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ContentQueryParser { - public class ContentQueryParser + private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); + private readonly IMemoryCache cache; + private readonly IJsonSerializer serializer; + private readonly IAppProvider appprovider; + private readonly ITextIndex textIndex; + private readonly ContentOptions options; + + public ContentQueryParser(IAppProvider appprovider, ITextIndex textIndex, IOptions<ContentOptions> options, + IMemoryCache cache, IJsonSerializer serializer) { - private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); - private readonly IMemoryCache cache; - private readonly IJsonSerializer serializer; - private readonly IAppProvider appprovider; - private readonly ITextIndex textIndex; - private readonly ContentOptions options; - - public ContentQueryParser(IAppProvider appprovider, ITextIndex textIndex, IOptions<ContentOptions> options, - IMemoryCache cache, IJsonSerializer serializer) - { - this.serializer = serializer; - this.appprovider = appprovider; - this.textIndex = textIndex; - this.cache = cache; - this.options = options.Value; - } - - public virtual async Task<Q> ParseAsync(Context context, Q q, ISchemaEntity? schema = null) - { - Guard.NotNull(context); - Guard.NotNull(q); - - using (Telemetry.Activities.StartActivity("ContentQueryParser/ParseAsync")) - { - var query = await ParseClrQueryAsync(context, q, schema); + this.serializer = serializer; + this.appprovider = appprovider; + this.textIndex = textIndex; + this.cache = cache; + this.options = options.Value; + } - await TransformFilterAsync(query, context, schema); + public virtual async Task<Q> ParseAsync(Context context, Q q, ISchemaEntity? schema = null) + { + Guard.NotNull(context); + Guard.NotNull(q); - WithSorting(query); - WithPaging(query, q); + using (Telemetry.Activities.StartActivity("ContentQueryParser/ParseAsync")) + { + var query = await ParseClrQueryAsync(context, q, schema); - q = q.WithQuery(query); + await TransformFilterAsync(query, context, schema); - if (context.ShouldSkipTotal()) - { - q = q.WithoutTotal(); - } - else if (context.ShouldSkipSlowTotal()) - { - q = q.WithoutSlowTotal(); - } + WithSorting(query); + WithPaging(query, q); - return q; - } - } + q = q.WithQuery(query); - private async Task TransformFilterAsync(ClrQuery query, Context context, ISchemaEntity? schema) - { - if (query.Filter != null && schema != null) + if (context.ShouldSkipTotal()) { - query.Filter = await GeoQueryTransformer.TransformAsync(query.Filter, context, schema, textIndex); + q = q.WithoutTotal(); } - - if (!string.IsNullOrWhiteSpace(query.FullText)) + else if (context.ShouldSkipSlowTotal()) { - if (schema == null) - { - ThrowHelper.InvalidOperationException(); - return; - } - - var textQuery = new TextQuery(query.FullText, 1000) - { - PreferredSchemaId = schema.Id - }; - - var fullTextIds = await textIndex.SearchAsync(context.App, textQuery, context.Scope()); - var fullTextFilter = ClrFilter.Eq("id", "__notfound__"); - - if (fullTextIds?.Any() == true) - { - fullTextFilter = ClrFilter.In("id", fullTextIds.Select(x => x.ToString()).ToList()); - } - - if (query.Filter != null) - { - query.Filter = ClrFilter.And(query.Filter, fullTextFilter); - } - else - { - query.Filter = fullTextFilter; - } - - query.FullText = null; + q = q.WithoutSlowTotal(); } + + return q; } + } - private async Task<ClrQuery> ParseClrQueryAsync(Context context, Q q, ISchemaEntity? schema) + private async Task TransformFilterAsync(ClrQuery query, Context context, ISchemaEntity? schema) + { + if (query.Filter != null && schema != null) { - var components = ResolvedComponents.Empty; + query.Filter = await GeoQueryTransformer.TransformAsync(query.Filter, context, schema, textIndex); + } - if (schema != null) + if (!string.IsNullOrWhiteSpace(query.FullText)) + { + if (schema == null) { - components = await appprovider.GetComponentsAsync(schema); + ThrowHelper.InvalidOperationException(); + return; } - var query = q.Query; + var textQuery = new TextQuery(query.FullText, 1000) + { + PreferredSchemaId = schema.Id + }; + + var fullTextIds = await textIndex.SearchAsync(context.App, textQuery, context.Scope()); + var fullTextFilter = ClrFilter.Eq("id", "__notfound__"); - if (!string.IsNullOrWhiteSpace(q.QueryAsJson)) + if (fullTextIds?.Any() == true) { - query = ParseJson(context, schema, q.QueryAsJson, components); + fullTextFilter = ClrFilter.In("id", fullTextIds.Select(x => x.ToString()).ToList()); } - else if (q?.JsonQuery != null) + + if (query.Filter != null) { - query = ParseJson(context, schema, q.JsonQuery, components); + query.Filter = ClrFilter.And(query.Filter, fullTextFilter); } - else if (!string.IsNullOrWhiteSpace(q?.QueryAsOdata)) + else { - query = ParseOData(context, schema, q.QueryAsOdata, components); + query.Filter = fullTextFilter; } - return query; + query.FullText = null; } + } + + private async Task<ClrQuery> ParseClrQueryAsync(Context context, Q q, ISchemaEntity? schema) + { + var components = ResolvedComponents.Empty; - private static void WithSorting(ClrQuery query) + if (schema != null) { - query.Sort ??= new List<SortNode>(); + components = await appprovider.GetComponentsAsync(schema); + } - if (query.Sort.Count == 0) - { - query.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending)); - } + var query = q.Query; - if (!query.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) - { - query.Sort.Add(new SortNode(new List<string> { "id" }, SortOrder.Ascending)); - } + if (!string.IsNullOrWhiteSpace(q.QueryAsJson)) + { + query = ParseJson(context, schema, q.QueryAsJson, components); } - - private void WithPaging(ClrQuery query, Q q) + else if (q?.JsonQuery != null) { - if (query.Take is <= 0 or long.MaxValue) - { - if (q.Ids is { Count: > 0 }) - { - query.Take = q.Ids.Count; - } - else - { - query.Take = options.DefaultPageSize; - } - } - else if (query.Take > options.MaxResults) - { - query.Take = options.MaxResults; - } + query = ParseJson(context, schema, q.JsonQuery, components); } - - private ClrQuery ParseJson(Context context, ISchemaEntity? schema, Query<JsonValue> query, - ResolvedComponents components) + else if (!string.IsNullOrWhiteSpace(q?.QueryAsOdata)) { - var queryModel = BuildQueryModel(context, schema, components); - - return queryModel.Convert(query); + query = ParseOData(context, schema, q.QueryAsOdata, components); } - private ClrQuery ParseJson(Context context, ISchemaEntity? schema, string json, - ResolvedComponents components) - { - var queryModel = BuildQueryModel(context, schema, components); + return query; + } - return queryModel.Parse(json, serializer); + private static void WithSorting(ClrQuery query) + { + query.Sort ??= new List<SortNode>(); + + if (query.Sort.Count == 0) + { + query.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending)); } - private ClrQuery ParseOData(Context context, ISchemaEntity? schema, string odata, - ResolvedComponents components) + if (!query.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) { - try - { - var model = BuildEdmModel(context, schema, components); + query.Sort.Add(new SortNode(new List<string> { "id" }, SortOrder.Ascending)); + } + } - return model.ParseQuery(odata).ToQuery(); - } - catch (ValidationException) - { - throw; - } - catch (NotSupportedException) + private void WithPaging(ClrQuery query, Q q) + { + if (query.Take is <= 0 or long.MaxValue) + { + if (q.Ids is { Count: > 0 }) { - throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); + query.Take = q.Ids.Count; } - catch (ODataException ex) + else { - var message = ex.Message; - - throw new ValidationException(T.Get("common.odataFailure", new { odata, message }), ex); - } - catch (Exception) - { - throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); + query.Take = options.DefaultPageSize; } } - - private QueryModel BuildQueryModel(Context context, ISchemaEntity? schema, - ResolvedComponents components) + else if (query.Take > options.MaxResults) { - var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); + query.Take = options.MaxResults; + } + } - var result = cache.GetOrCreate(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheTime; + private ClrQuery ParseJson(Context context, ISchemaEntity? schema, Query<JsonValue> query, + ResolvedComponents components) + { + var queryModel = BuildQueryModel(context, schema, components); - return ContentQueryModel.Build(schema?.SchemaDef, context.App.PartitionResolver(), components); - }); + return queryModel.Convert(query); + } + + private ClrQuery ParseJson(Context context, ISchemaEntity? schema, string json, + ResolvedComponents components) + { + var queryModel = BuildQueryModel(context, schema, components); + + return queryModel.Parse(json, serializer); + } + + private ClrQuery ParseOData(Context context, ISchemaEntity? schema, string odata, + ResolvedComponents components) + { + try + { + var model = BuildEdmModel(context, schema, components); + + return model.ParseQuery(odata).ToQuery(); + } + catch (ValidationException) + { + throw; + } + catch (NotSupportedException) + { + throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); + } + catch (ODataException ex) + { + var message = ex.Message; - return result; + throw new ValidationException(T.Get("common.odataFailure", new { odata, message }), ex); } + catch (Exception) + { + throw new ValidationException(T.Get("common.odataNotSupported", new { odata })); + } + } - private IEdmModel BuildEdmModel(Context context, ISchemaEntity? schema, - ResolvedComponents components) + private QueryModel BuildQueryModel(Context context, ISchemaEntity? schema, + ResolvedComponents components) + { + var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); + + var result = cache.GetOrCreate(cacheKey, entry => { - var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); + entry.AbsoluteExpirationRelativeToNow = CacheTime; - var result = cache.GetOrCreate<IEdmModel>(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheTime; + return ContentQueryModel.Build(schema?.SchemaDef, context.App.PartitionResolver(), components); + }); - return BuildQueryModel(context, schema, components).ConvertToEdm("Contents", schema?.SchemaDef.Name ?? "Generic"); - }); + return result; + } - return result; - } + private IEdmModel BuildEdmModel(Context context, ISchemaEntity? schema, + ResolvedComponents components) + { + var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); - private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity? schema, bool withHidden) + var result = cache.GetOrCreate<IEdmModel>(cacheKey, entry => { - if (schema == null) - { - return $"EDM/__generic"; - } + entry.AbsoluteExpirationRelativeToNow = CacheTime; - return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; - } + return BuildQueryModel(context, schema, components).ConvertToEdm("Contents", schema?.SchemaDef.Name ?? "Generic"); + }); - private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity? schema, bool withHidden) + return result; + } + + private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity? schema, bool withHidden) + { + if (schema == null) { - if (schema == null) - { - return $"JSON/__generic"; - } + return $"EDM/__generic"; + } + + return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; + } - return $"JSON/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; + private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity? schema, bool withHidden) + { + if (schema == null) + { + return $"JSON/__generic"; } + + return $"JSON/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index 01d9d48781..1896a0a7dc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -13,252 +13,251 @@ using Squidex.Infrastructure.Translations; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public sealed class ContentQueryService : IContentQueryService { - public sealed class ContentQueryService : IContentQueryService + private const string SingletonId = "_schemaId_"; + private readonly IAppProvider appProvider; + private readonly IContentEnricher contentEnricher; + private readonly IContentRepository contentRepository; + private readonly IContentLoader contentLoader; + private readonly ContentQueryParser queryParser; + private readonly ContentOptions options; + + public ContentQueryService( + IAppProvider appProvider, + IContentEnricher contentEnricher, + IContentRepository contentRepository, + IContentLoader contentLoader, + IOptions<ContentOptions> options, + ContentQueryParser queryParser) { - private const string SingletonId = "_schemaId_"; - private readonly IAppProvider appProvider; - private readonly IContentEnricher contentEnricher; - private readonly IContentRepository contentRepository; - private readonly IContentLoader contentLoader; - private readonly ContentQueryParser queryParser; - private readonly ContentOptions options; - - public ContentQueryService( - IAppProvider appProvider, - IContentEnricher contentEnricher, - IContentRepository contentRepository, - IContentLoader contentLoader, - IOptions<ContentOptions> options, - ContentQueryParser queryParser) - { - this.appProvider = appProvider; - this.contentEnricher = contentEnricher; - this.contentRepository = contentRepository; - this.contentLoader = contentLoader; - this.options = options.Value; - this.queryParser = queryParser; - } + this.appProvider = appProvider; + this.contentEnricher = contentEnricher; + this.contentRepository = contentRepository; + this.contentLoader = contentLoader; + this.options = options.Value; + this.queryParser = queryParser; + } + + public async Task<IEnrichedContentEntity?> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default) + { + Guard.NotNull(context); - public async Task<IEnrichedContentEntity?> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default) + using (Telemetry.Activities.StartActivity("ContentQueryService/FindAsync")) { - Guard.NotNull(context); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName, ct); - using (Telemetry.Activities.StartActivity("ContentQueryService/FindAsync")) + IContentEntity? content; + + if (id.ToString().Equals(SingletonId, StringComparison.Ordinal)) { - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName, ct); - - IContentEntity? content; - - if (id.ToString().Equals(SingletonId, StringComparison.Ordinal)) - { - id = schema.Id; - } - - if (version > EtagVersion.Empty) - { - content = await contentLoader.GetAsync(context.App.Id, id, version, ct); - } - else - { - content = await FindCoreAsync(context, id, schema, ct); - } - - if (content == null || content.SchemaId.Id != schema.Id) - { - return null; - } - - return await TransformAsync(context, content, ct); + id = schema.Id; } - } - - public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q q, - CancellationToken ct = default) - { - Guard.NotNull(context); - using (Telemetry.Activities.StartActivity("ContentQueryService/QueryAsync")) + if (version > EtagVersion.Empty) { - if (q == null) - { - return ResultList.Empty<IEnrichedContentEntity>(); - } - - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName, ct); - - if (!HasPermission(context, schema, PermissionIds.AppContentsRead)) - { - q = q with { CreatedBy = context.UserPrincipal.Token() }; - } - - q = await queryParser.ParseAsync(context, q, schema); - - var contents = await QueryCoreAsync(context, q, schema, ct); - - if (q.Ids is { Count: > 0 }) - { - contents = contents.SortSet(x => x.Id, q.Ids); - } + content = await contentLoader.GetAsync(context.App.Id, id, version, ct); + } + else + { + content = await FindCoreAsync(context, id, schema, ct); + } - return await TransformAsync(context, contents, ct); + if (content == null || content.SchemaId.Id != schema.Id) + { + return null; } + + return await TransformAsync(context, content, ct); } + } - public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, Q q, - CancellationToken ct = default) - { - Guard.NotNull(context); + public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q q, + CancellationToken ct = default) + { + Guard.NotNull(context); - using (Telemetry.Activities.StartActivity("ContentQueryService/QueryAsync")) + using (Telemetry.Activities.StartActivity("ContentQueryService/QueryAsync")) + { + if (q == null) { - if (q == null) - { - return ResultList.Empty<IEnrichedContentEntity>(); - } - - var schemas = await GetSchemasAsync(context, ct); + return ResultList.Empty<IEnrichedContentEntity>(); + } - if (schemas.Count == 0) - { - return ResultList.Empty<IEnrichedContentEntity>(); - } + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName, ct); - q = await queryParser.ParseAsync(context, q); + if (!HasPermission(context, schema, PermissionIds.AppContentsRead)) + { + q = q with { CreatedBy = context.UserPrincipal.Token() }; + } - var contents = await QueryCoreAsync(context, q, schemas, ct); + q = await queryParser.ParseAsync(context, q, schema); - if (q.Ids is { Count: > 0 }) - { - contents = contents.SortSet(x => x.Id, q.Ids); - } + var contents = await QueryCoreAsync(context, q, schema, ct); - return await TransformAsync(context, contents, ct); + if (q.Ids is { Count: > 0 }) + { + contents = contents.SortSet(x => x.Id, q.Ids); } - } - private async Task<IResultList<IEnrichedContentEntity>> TransformAsync(Context context, IResultList<IContentEntity> contents, - CancellationToken ct) - { - var transformed = await TransformCoreAsync(context, contents, ct); - - return ResultList.Create(contents.Total, transformed); + return await TransformAsync(context, contents, ct); } + } - private async Task<IEnrichedContentEntity> TransformAsync(Context context, IContentEntity content, - CancellationToken ct) - { - var transformed = await TransformCoreAsync(context, Enumerable.Repeat(content, 1), ct); - - return transformed[0]; - } + public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, Q q, + CancellationToken ct = default) + { + Guard.NotNull(context); - private async Task<IReadOnlyList<IEnrichedContentEntity>> TransformCoreAsync(Context context, IEnumerable<IContentEntity> contents, - CancellationToken ct) + using (Telemetry.Activities.StartActivity("ContentQueryService/QueryAsync")) { - using (Telemetry.Activities.StartActivity("ContentQueryService/TransformCoreAsync")) + if (q == null) { - return await contentEnricher.EnrichAsync(contents, context, ct); + return ResultList.Empty<IEnrichedContentEntity>(); } - } - public async Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName, - CancellationToken ct = default) - { - var schema = await GetSchemaAsync(context, schemaIdOrName, ct); + var schemas = await GetSchemasAsync(context, ct); - if (schema == null) + if (schemas.Count == 0) { - throw new DomainObjectNotFoundException(schemaIdOrName); + return ResultList.Empty<IEnrichedContentEntity>(); } - return schema; - } + q = await queryParser.ParseAsync(context, q); - public async Task<ISchemaEntity?> GetSchemaAsync(Context context, string schemaIdOrName, - CancellationToken ct = default) - { - Guard.NotNull(context); - Guard.NotNullOrEmpty(schemaIdOrName); + var contents = await QueryCoreAsync(context, q, schemas, ct); - ISchemaEntity? schema = null; + if (q.Ids is { Count: > 0 }) + { + contents = contents.SortSet(x => x.Id, q.Ids); + } - var canCache = !context.IsFrontendClient; + return await TransformAsync(context, contents, ct); + } + } - if (Guid.TryParse(schemaIdOrName, out var guid)) - { - var schemaId = DomainId.Create(guid); + private async Task<IResultList<IEnrichedContentEntity>> TransformAsync(Context context, IResultList<IContentEntity> contents, + CancellationToken ct) + { + var transformed = await TransformCoreAsync(context, contents, ct); - schema = await appProvider.GetSchemaAsync(context.App.Id, schemaId, canCache, ct); - } + return ResultList.Create(contents.Total, transformed); + } - if (schema == null) - { - schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName, canCache, ct); - } + private async Task<IEnrichedContentEntity> TransformAsync(Context context, IContentEntity content, + CancellationToken ct) + { + var transformed = await TransformCoreAsync(context, Enumerable.Repeat(content, 1), ct); - if (schema != null && !HasPermission(context, schema, PermissionIds.AppContentsReadOwn)) - { - throw new DomainForbiddenException(T.Get("schemas.noPermission")); - } + return transformed[0]; + } - return schema; + private async Task<IReadOnlyList<IEnrichedContentEntity>> TransformCoreAsync(Context context, IEnumerable<IContentEntity> contents, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("ContentQueryService/TransformCoreAsync")) + { + return await contentEnricher.EnrichAsync(contents, context, ct); } + } - private async Task<List<ISchemaEntity>> GetSchemasAsync(Context context, - CancellationToken ct) - { - var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); + public async Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName, + CancellationToken ct = default) + { + var schema = await GetSchemaAsync(context, schemaIdOrName, ct); - return schemas.Where(x => IsAccessible(x) && HasPermission(context, x, PermissionIds.AppContentsReadOwn)).ToList(); + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaIdOrName); } - private async Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, Q q, ISchemaEntity schema, - CancellationToken ct) + return schema; + } + + public async Task<ISchemaEntity?> GetSchemaAsync(Context context, string schemaIdOrName, + CancellationToken ct = default) + { + Guard.NotNull(context); + Guard.NotNullOrEmpty(schemaIdOrName); + + ISchemaEntity? schema = null; + + var canCache = !context.IsFrontendClient; + + if (Guid.TryParse(schemaIdOrName, out var guid)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutQuery); + var schemaId = DomainId.Create(guid); - return await contentRepository.QueryAsync(context.App, schema, q, context.Scope(), combined.Token); - } + schema = await appProvider.GetSchemaAsync(context.App.Id, schemaId, canCache, ct); } - private async Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, Q q, List<ISchemaEntity> schemas, - CancellationToken ct) + if (schema == null) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutQuery); + schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName, canCache, ct); + } - return await contentRepository.QueryAsync(context.App, schemas, q, context.Scope(), combined.Token); - } + if (schema != null && !HasPermission(context, schema, PermissionIds.AppContentsReadOwn)) + { + throw new DomainForbiddenException(T.Get("schemas.noPermission")); } - private async Task<IContentEntity?> FindCoreAsync(Context context, DomainId id, ISchemaEntity schema, - CancellationToken ct) + return schema; + } + + private async Task<List<ISchemaEntity>> GetSchemasAsync(Context context, + CancellationToken ct) + { + var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); + + return schemas.Where(x => IsAccessible(x) && HasPermission(context, x, PermissionIds.AppContentsReadOwn)).ToList(); + } + + private async Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, Q q, ISchemaEntity schema, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout - combined.CancelAfter(options.TimeoutFind); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutQuery); - return await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), combined.Token); - } + return await contentRepository.QueryAsync(context.App, schema, q, context.Scope(), combined.Token); } + } - private static bool IsAccessible(ISchemaEntity schema) + private async Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, Q q, List<ISchemaEntity> schemas, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - return schema.SchemaDef.IsPublished; + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutQuery); + + return await contentRepository.QueryAsync(context.App, schemas, q, context.Scope(), combined.Token); } + } - private static bool HasPermission(Context context, ISchemaEntity schema, string permissionId) + private async Task<IContentEntity?> FindCoreAsync(Context context, DomainId id, ISchemaEntity schema, + CancellationToken ct) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - return context.UserPermissions.Allows(permissionId, context.App.Name, schema.SchemaDef.Name); + // Enforce a hard timeout + combined.CancelAfter(options.TimeoutFind); + + return await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), combined.Token); } } + + private static bool IsAccessible(ISchemaEntity schema) + { + return schema.SchemaDef.IsPublished; + } + + private static bool HasPermission(Context context, ISchemaEntity schema, string permissionId) + { + return context.UserPermissions.Allows(permissionId, context.App.Name, schema.SchemaDef.Name); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs index 543afe6a87..4719e61a04 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs @@ -11,45 +11,44 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +internal sealed class GeoQueryTransformer : AsyncTransformVisitor<ClrValue, GeoQueryTransformer.Args> { - internal sealed class GeoQueryTransformer : AsyncTransformVisitor<ClrValue, GeoQueryTransformer.Args> - { - public static readonly GeoQueryTransformer Instance = new GeoQueryTransformer(); + public static readonly GeoQueryTransformer Instance = new GeoQueryTransformer(); - public record struct Args(Context Context, ISchemaEntity Schema, ITextIndex TextIndex); + public record struct Args(Context Context, ISchemaEntity Schema, ITextIndex TextIndex); - private GeoQueryTransformer() - { - } + private GeoQueryTransformer() + { + } - public static async Task<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> filter, Context context, ISchemaEntity schema, ITextIndex textIndex) - { - var args = new Args(context, schema, textIndex); + public static async Task<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> filter, Context context, ISchemaEntity schema, ITextIndex textIndex) + { + var args = new Args(context, schema, textIndex); - return await filter.Accept(Instance, args); - } + return await filter.Accept(Instance, args); + } - public override async ValueTask<FilterNode<ClrValue>?> Visit(CompareFilter<ClrValue> nodeIn, Args args) + public override async ValueTask<FilterNode<ClrValue>?> Visit(CompareFilter<ClrValue> nodeIn, Args args) + { + if (nodeIn.Value.Value is FilterSphere sphere) { - if (nodeIn.Value.Value is FilterSphere sphere) - { - var field = string.Join(".", nodeIn.Path.Skip(1)); + var field = string.Join(".", nodeIn.Path.Skip(1)); - var searchQuery = new GeoQuery(args.Schema.Id, field, sphere.Latitude, sphere.Longitude, sphere.Radius, 1000); - var searchScope = args.Context.Scope(); + var searchQuery = new GeoQuery(args.Schema.Id, field, sphere.Latitude, sphere.Longitude, sphere.Radius, 1000); + var searchScope = args.Context.Scope(); - var ids = await args.TextIndex.SearchAsync(args.Context.App, searchQuery, searchScope); + var ids = await args.TextIndex.SearchAsync(args.Context.App, searchQuery, searchScope); - if (ids == null || ids.Count == 0) - { - return ClrFilter.Eq("id", "__notfound__"); - } - - return ClrFilter.In("id", ids.Select(x => x.ToString()).ToList()); + if (ids == null || ids.Count == 0) + { + return ClrFilter.Eq("id", "__notfound__"); } - return nodeIn; + return ClrFilter.In("id", ids.Select(x => x.ToString()).ToList()); } + + return nodeIn; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs index 0a2a5d9398..c1e220db07 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public interface IContentEnricher { - public interface IContentEnricher - { - Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, bool cloneData, Context context, - CancellationToken ct); + Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, bool cloneData, Context context, + CancellationToken ct); - Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, Context context, - CancellationToken ct); - } + Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, Context context, + CancellationToken ct); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs index e93930ff9f..7e77906de3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs @@ -11,19 +11,18 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public delegate Task<(ISchemaEntity Schema, ResolvedComponents Components)> ProvideSchema(DomainId id); + +public interface IContentEnricherStep { - public delegate Task<(ISchemaEntity Schema, ResolvedComponents Components)> ProvideSchema(DomainId id); + Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct); - public interface IContentEnricherStep + Task EnrichAsync(Context context, + CancellationToken ct) { - Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct); - - Task EnrichAsync(Context context, - CancellationToken ct) - { - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index d63c134a52..b60f2bc0be 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -9,143 +9,142 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public abstract class QueryExecutionContext : Dictionary<string, object?> { - public abstract class QueryExecutionContext : Dictionary<string, object?> - { - private readonly SemaphoreSlim maxRequests = new SemaphoreSlim(10); + private readonly SemaphoreSlim maxRequests = new SemaphoreSlim(10); - public abstract Context Context { get; } + public abstract Context Context { get; } - protected IAssetQueryService AssetQuery { get; } + protected IAssetQueryService AssetQuery { get; } - protected IAssetCache AssetCache { get; } + protected IAssetCache AssetCache { get; } - protected IContentCache ContentCache { get; } + protected IContentCache ContentCache { get; } - protected IContentQueryService ContentQuery { get; } + protected IContentQueryService ContentQuery { get; } - public IServiceProvider Services { get; } + public IServiceProvider Services { get; } - protected QueryExecutionContext( - IAssetQueryService assetQuery, - IAssetCache assetCache, - IContentQueryService contentQuery, - IContentCache contentCache, - IServiceProvider serviceProvider) - { - Guard.NotNull(serviceProvider); + protected QueryExecutionContext( + IAssetQueryService assetQuery, + IAssetCache assetCache, + IContentQueryService contentQuery, + IContentCache contentCache, + IServiceProvider serviceProvider) + { + Guard.NotNull(serviceProvider); - AssetQuery = assetQuery; - AssetCache = assetCache; - ContentQuery = contentQuery; - ContentCache = contentCache; + AssetQuery = assetQuery; + AssetCache = assetCache; + ContentQuery = contentQuery; + ContentCache = contentCache; - Services = serviceProvider; - } + Services = serviceProvider; + } + + public virtual Task<IEnrichedContentEntity?> FindContentAsync(string schemaIdOrName, DomainId id, long version, + CancellationToken ct) + { + return ContentQuery.FindAsync(Context, schemaIdOrName, id, version, ct); + } + + public virtual async Task<IResultList<IEnrichedAssetEntity>> QueryAssetsAsync(Q q, + CancellationToken ct) + { + IResultList<IEnrichedAssetEntity> assets; - public virtual Task<IEnrichedContentEntity?> FindContentAsync(string schemaIdOrName, DomainId id, long version, - CancellationToken ct) + await maxRequests.WaitAsync(ct); + try { - return ContentQuery.FindAsync(Context, schemaIdOrName, id, version, ct); + assets = await AssetQuery.QueryAsync(Context, null, q, ct); } + finally + { + maxRequests.Release(); + } + + AssetCache.SetMany(assets.Select(x => (x.Id, x))!); - public virtual async Task<IResultList<IEnrichedAssetEntity>> QueryAssetsAsync(Q q, - CancellationToken ct) + return assets; + } + + public virtual async Task<IResultList<IEnrichedContentEntity>> QueryContentsAsync(string schemaIdOrName, Q q, + CancellationToken ct) + { + IResultList<IEnrichedContentEntity> contents; + + await maxRequests.WaitAsync(ct); + try { - IResultList<IEnrichedAssetEntity> assets; + contents = await ContentQuery.QueryAsync(Context, schemaIdOrName, q, ct); + } + finally + { + maxRequests.Release(); + } + ContentCache.SetMany(contents.Select(x => (x.Id, x))!); + + return contents; + } + + public virtual async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IEnumerable<DomainId> ids, + CancellationToken ct) + { + Guard.NotNull(ids); + + return await AssetCache.CacheOrQueryAsync(ids, async pendingIds => + { await maxRequests.WaitAsync(ct); try { - assets = await AssetQuery.QueryAsync(Context, null, q, ct); + var q = Q.Empty.WithIds(pendingIds).WithoutTotal(); + + return await AssetQuery.QueryAsync(Context, null, q, ct); } finally { maxRequests.Release(); } + }); + } - AssetCache.SetMany(assets.Select(x => (x.Id, x))!); - - return assets; - } + public virtual async Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(IEnumerable<DomainId> ids, + CancellationToken ct) + { + Guard.NotNull(ids); - public virtual async Task<IResultList<IEnrichedContentEntity>> QueryContentsAsync(string schemaIdOrName, Q q, - CancellationToken ct) + return await ContentCache.CacheOrQueryAsync(ids, async pendingIds => { - IResultList<IEnrichedContentEntity> contents; - await maxRequests.WaitAsync(ct); try { - contents = await ContentQuery.QueryAsync(Context, schemaIdOrName, q, ct); + var q = Q.Empty.WithIds(pendingIds).WithoutTotal(); + + return await ContentQuery.QueryAsync(Context, q, ct); } finally { maxRequests.Release(); } + }); + } - ContentCache.SetMany(contents.Select(x => (x.Id, x))!); - - return contents; - } - - public virtual async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IEnumerable<DomainId> ids, - CancellationToken ct) - { - Guard.NotNull(ids); - - return await AssetCache.CacheOrQueryAsync(ids, async pendingIds => - { - await maxRequests.WaitAsync(ct); - try - { - var q = Q.Empty.WithIds(pendingIds).WithoutTotal(); - - return await AssetQuery.QueryAsync(Context, null, q, ct); - } - finally - { - maxRequests.Release(); - } - }); - } + public T Resolve<T>() where T : class + { + var key = typeof(T).Name; - public virtual async Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(IEnumerable<DomainId> ids, - CancellationToken ct) + if (TryGetValue(key, out var stored) && stored is T typed) { - Guard.NotNull(ids); - - return await ContentCache.CacheOrQueryAsync(ids, async pendingIds => - { - await maxRequests.WaitAsync(ct); - try - { - var q = Q.Empty.WithIds(pendingIds).WithoutTotal(); - - return await ContentQuery.QueryAsync(Context, q, ct); - } - finally - { - maxRequests.Release(); - } - }); + return typed; } - public T Resolve<T>() where T : class - { - var key = typeof(T).Name; + typed = Services.GetRequiredService<T>(); - if (TryGetValue(key, out var stored) && stored is T typed) - { - return typed; - } - - typed = Services.GetRequiredService<T>(); + this[key] = typed; - this[key] = typed; - - return typed; - } + return typed; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs index 7f7535717b..f363769ea1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs @@ -9,41 +9,40 @@ using Squidex.Domain.Apps.Core; using Squidex.Infrastructure.Json; -namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; + +public sealed class CalculateTokens : IContentEnricherStep { - public sealed class CalculateTokens : IContentEnricherStep + private readonly IJsonSerializer serializer; + private readonly IUrlGenerator urlGenerator; + + public CalculateTokens(IUrlGenerator urlGenerator, IJsonSerializer serializer) { - private readonly IJsonSerializer serializer; - private readonly IUrlGenerator urlGenerator; + this.serializer = serializer; + this.urlGenerator = urlGenerator; + } - public CalculateTokens(IUrlGenerator urlGenerator, IJsonSerializer serializer) - { - this.serializer = serializer; - this.urlGenerator = urlGenerator; - } + public Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + var url = urlGenerator.Root(); - public Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) + foreach (var content in contents) { - var url = urlGenerator.Root(); - - foreach (var content in contents) + // We have to use these short names here because they are later read like this. + var token = new { - // We have to use these short names here because they are later read like this. - var token = new - { - a = content.AppId.Name, - s = content.SchemaId.Name, - i = content.Id.ToString(), - u = url - }; + a = content.AppId.Name, + s = content.SchemaId.Name, + i = content.Id.ToString(), + u = url + }; - var json = serializer.Serialize(token); + var json = serializer.Serialize(token); - content.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); - } - - return Task.CompletedTask; + content.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); } + + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs index fc769d4e3b..2413fdd6df 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs @@ -18,135 +18,134 @@ #pragma warning disable MA0073 // Avoid comparison with bool constant -namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; + +public sealed class ConvertData : IContentEnricherStep { - public sealed class ConvertData : IContentEnricherStep + private readonly IUrlGenerator urlGenerator; + private readonly IAssetRepository assetRepository; + private readonly IContentRepository contentRepository; + private readonly ExcludeChangedTypes excludeChangedTypes; + + public ConvertData(IUrlGenerator urlGenerator, IJsonSerializer serializer, + IAssetRepository assetRepository, IContentRepository contentRepository) { - private readonly IUrlGenerator urlGenerator; - private readonly IAssetRepository assetRepository; - private readonly IContentRepository contentRepository; - private readonly ExcludeChangedTypes excludeChangedTypes; + this.urlGenerator = urlGenerator; + this.assetRepository = assetRepository; + this.contentRepository = contentRepository; + + excludeChangedTypes = new ExcludeChangedTypes(serializer); + } - public ConvertData(IUrlGenerator urlGenerator, IJsonSerializer serializer, - IAssetRepository assetRepository, IContentRepository contentRepository) + public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + var referenceCleaner = await CleanReferencesAsync(context, contents, schemas, ct); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) { - this.urlGenerator = urlGenerator; - this.assetRepository = assetRepository; - this.contentRepository = contentRepository; + ct.ThrowIfCancellationRequested(); + + var (schema, components) = await schemas(group.Key); - excludeChangedTypes = new ExcludeChangedTypes(serializer); + var converter = GenerateConverter(context, components, schema.SchemaDef, referenceCleaner); + + foreach (var content in group) + { + content.Data = converter.Convert(content.Data); + } + } + } + + private async Task<ValueReferencesConverter?> CleanReferencesAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + if (context.ShouldSkipCleanup()) + { + return null; } - public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) + using (Telemetry.Activities.StartActivity("ConvertData/CleanReferencesAsync")) { - var referenceCleaner = await CleanReferencesAsync(context, contents, schemas, ct); + var ids = new HashSet<DomainId>(); foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) { - ct.ThrowIfCancellationRequested(); - var (schema, components) = await schemas(group.Key); - var converter = GenerateConverter(context, components, schema.SchemaDef, referenceCleaner); - foreach (var content in group) { - content.Data = converter.Convert(content.Data); + content.Data.AddReferencedIds(schema.SchemaDef, ids, components); } } - } - - private async Task<ValueReferencesConverter?> CleanReferencesAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) - { - if (context.ShouldSkipCleanup()) - { - return null; - } - using (Telemetry.Activities.StartActivity("ConvertData/CleanReferencesAsync")) + if (ids.Count > 0) { - var ids = new HashSet<DomainId>(); + var (assets, refContents) = await AsyncHelper.WhenAll( + QueryAssetIdsAsync(context, ids, ct), + QueryContentIdsAsync(context, ids, ct)); - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var (schema, components) = await schemas(group.Key); + var foundIds = assets.Union(refContents).ToHashSet(); - foreach (var content in group) - { - content.Data.AddReferencedIds(schema.SchemaDef, ids, components); - } - } + return new ValueReferencesConverter(foundIds); + } + } - if (ids.Count > 0) - { - var (assets, refContents) = await AsyncHelper.WhenAll( - QueryAssetIdsAsync(context, ids, ct), - QueryContentIdsAsync(context, ids, ct)); + return null; + } - var foundIds = assets.Union(refContents).ToHashSet(); + private async Task<IEnumerable<DomainId>> QueryContentIdsAsync(Context context, HashSet<DomainId> ids, + CancellationToken ct) + { + var result = await contentRepository.QueryIdsAsync(context.App.Id, ids, context.Scope(), ct); - return new ValueReferencesConverter(foundIds); - } - } + return result.Select(x => x.Id); + } - return null; - } + private async Task<IEnumerable<DomainId>> QueryAssetIdsAsync(Context context, HashSet<DomainId> ids, + CancellationToken ct) + { + var result = await assetRepository.QueryIdsAsync(context.App.Id, ids, ct); - private async Task<IEnumerable<DomainId>> QueryContentIdsAsync(Context context, HashSet<DomainId> ids, - CancellationToken ct) - { - var result = await contentRepository.QueryIdsAsync(context.App.Id, ids, context.Scope(), ct); + return result; + } - return result.Select(x => x.Id); - } + private ContentConverter GenerateConverter(Context context, ResolvedComponents components, Schema schema, ValueReferencesConverter? cleanReferences) + { + var converter = new ContentConverter(components, schema); - private async Task<IEnumerable<DomainId>> QueryAssetIdsAsync(Context context, HashSet<DomainId> ids, - CancellationToken ct) + if (!context.IsFrontendClient) { - var result = await assetRepository.QueryIdsAsync(context.App.Id, ids, ct); - - return result; + converter.Add(ExcludeHidden.Instance); } - private ContentConverter GenerateConverter(Context context, ResolvedComponents components, Schema schema, ValueReferencesConverter? cleanReferences) - { - var converter = new ContentConverter(components, schema); + converter.Add(excludeChangedTypes); - if (!context.IsFrontendClient) - { - converter.Add(ExcludeHidden.Instance); - } - - converter.Add(excludeChangedTypes); + if (cleanReferences != null) + { + converter.Add(cleanReferences); + } - if (cleanReferences != null) - { - converter.Add(cleanReferences); - } + converter.Add(new ResolveInvariant(context.App.Languages)); - converter.Add(new ResolveInvariant(context.App.Languages)); + converter.Add( + new ResolveLanguages(context.App.Languages, + context.IsFrontendClient == false && + context.ShouldResolveLanguages(), + context.Languages().ToArray())); - converter.Add( - new ResolveLanguages(context.App.Languages, - context.IsFrontendClient == false && - context.ShouldResolveLanguages(), - context.Languages().ToArray())); + if (!context.IsFrontendClient) + { + var assetUrls = context.AssetUrls().ToList(); - if (!context.IsFrontendClient) + if (assetUrls.Count > 0) { - var assetUrls = context.AssetUrls().ToList(); - - if (assetUrls.Count > 0) - { - converter.Add(new ResolveAssetUrls(context.App.NamedId(), urlGenerator, assetUrls)); - } - - converter.Add(new AddSchemaNames(components)); + converter.Add(new ResolveAssetUrls(context.App.NamedId(), urlGenerator, assetUrls)); } - return converter; + converter.Add(new AddSchemaNames(components)); } + + return converter; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs index fd69dac070..63fa30b396 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs @@ -7,42 +7,41 @@ using Squidex.Infrastructure.Caching; -namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; + +public sealed class EnrichForCaching : IContentEnricherStep { - public sealed class EnrichForCaching : IContentEnricherStep + private readonly IRequestCache requestCache; + + public EnrichForCaching(IRequestCache requestCache) { - private readonly IRequestCache requestCache; + this.requestCache = requestCache; + } - public EnrichForCaching(IRequestCache requestCache) - { - this.requestCache = requestCache; - } + public Task EnrichAsync(Context context, + CancellationToken ct) + { + context.AddCacheHeaders(requestCache); - public Task EnrichAsync(Context context, - CancellationToken ct) - { - context.AddCacheHeaders(requestCache); + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + var app = context.App; - public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) { - var app = context.App; + ct.ThrowIfCancellationRequested(); - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - ct.ThrowIfCancellationRequested(); - - var (schema, _) = await schemas(group.Key); + var (schema, _) = await schemas(group.Key); - foreach (var content in group) - { - requestCache.AddDependency(content.UniqueId, content.Version); - requestCache.AddDependency(schema.UniqueId, schema.Version); - requestCache.AddDependency(app.UniqueId, app.Version); - } + foreach (var content in group) + { + requestCache.AddDependency(content.UniqueId, content.Version); + requestCache.AddDependency(schema.UniqueId, schema.Version); + requestCache.AddDependency(app.UniqueId, app.Version); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs index 70463a152c..52114ce5a3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs @@ -7,36 +7,35 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; + +public sealed class EnrichWithSchema : IContentEnricherStep { - public sealed class EnrichWithSchema : IContentEnricherStep + public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) { - public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) { - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - ct.ThrowIfCancellationRequested(); + ct.ThrowIfCancellationRequested(); - var (schema, _) = await schemas(group.Key); + var (schema, _) = await schemas(group.Key); - var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged(); + var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged(); - foreach (var content in group) - { - content.IsSingleton = schema.SchemaDef.Type == SchemaType.Singleton; + foreach (var content in group) + { + content.IsSingleton = schema.SchemaDef.Type == SchemaType.Singleton; - content.SchemaDisplayName = schemaDisplayName; - } + content.SchemaDisplayName = schemaDisplayName; + } - if (context.IsFrontendClient) - { - var referenceFields = schema.SchemaDef.ReferenceFields().ToArray(); + if (context.IsFrontendClient) + { + var referenceFields = schema.SchemaDef.ReferenceFields().ToArray(); - foreach (var content in group) - { - content.ReferenceFields = referenceFields; - } + foreach (var content in group) + { + content.ReferenceFields = referenceFields; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs index f56c382482..1403e1d47b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs @@ -8,104 +8,103 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; + +public sealed class EnrichWithWorkflows : IContentEnricherStep { - public sealed class EnrichWithWorkflows : IContentEnricherStep + private const string DefaultColor = StatusColors.Draft; + + private readonly IContentWorkflow contentWorkflow; + + public EnrichWithWorkflows(IContentWorkflow contentWorkflow) { - private const string DefaultColor = StatusColors.Draft; + this.contentWorkflow = contentWorkflow; + } - private readonly IContentWorkflow contentWorkflow; + public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + var cache = new Dictionary<(DomainId, Status), StatusInfo>(); - public EnrichWithWorkflows(IContentWorkflow contentWorkflow) + foreach (var content in contents) { - this.contentWorkflow = contentWorkflow; - } + ct.ThrowIfCancellationRequested(); - public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) - { - var cache = new Dictionary<(DomainId, Status), StatusInfo>(); + await EnrichColorAsync(content, content, cache); - foreach (var content in contents) + if (ShouldEnrichWithStatuses(context)) { - ct.ThrowIfCancellationRequested(); - - await EnrichColorAsync(content, content, cache); - - if (ShouldEnrichWithStatuses(context)) - { - await EnrichNextsAsync(content, context); - await EnrichCanUpdateAsync(content, context); - } + await EnrichNextsAsync(content, context); + await EnrichCanUpdateAsync(content, context); } } + } - private async Task EnrichNextsAsync(ContentEntity content, Context context) - { - var editingStatus = content.NewStatus ?? content.Status; + private async Task EnrichNextsAsync(ContentEntity content, Context context) + { + var editingStatus = content.NewStatus ?? content.Status; - if (content.IsSingleton) + if (content.IsSingleton) + { + if (editingStatus == Status.Draft) { - if (editingStatus == Status.Draft) - { - content.NextStatuses = new[] - { - new StatusInfo(Status.Published, StatusColors.Published) - }; - } - else + content.NextStatuses = new[] { - content.NextStatuses = Array.Empty<StatusInfo>(); - } + new StatusInfo(Status.Published, StatusColors.Published) + }; } else { - content.NextStatuses = await contentWorkflow.GetNextAsync(content, editingStatus, context.UserPrincipal); + content.NextStatuses = Array.Empty<StatusInfo>(); } } - - private async Task EnrichCanUpdateAsync(ContentEntity content, Context context) + else { - var editingStatus = content.NewStatus ?? content.Status; - - content.CanUpdate = await contentWorkflow.CanUpdateAsync(content, editingStatus, context.UserPrincipal); + content.NextStatuses = await contentWorkflow.GetNextAsync(content, editingStatus, context.UserPrincipal); } + } - private async Task EnrichColorAsync(ContentEntity content, ContentEntity result, Dictionary<(DomainId, Status), StatusInfo> cache) - { - result.StatusColor = await GetColorAsync(content, content.Status, cache); + private async Task EnrichCanUpdateAsync(ContentEntity content, Context context) + { + var editingStatus = content.NewStatus ?? content.Status; - if (content.NewStatus != null) - { - result.NewStatusColor = await GetColorAsync(content, content.NewStatus.Value, cache); - } + content.CanUpdate = await contentWorkflow.CanUpdateAsync(content, editingStatus, context.UserPrincipal); + } - if (content.ScheduleJob != null) - { - result.ScheduledStatusColor = await GetColorAsync(content, content.ScheduleJob.Status, cache); - } + private async Task EnrichColorAsync(ContentEntity content, ContentEntity result, Dictionary<(DomainId, Status), StatusInfo> cache) + { + result.StatusColor = await GetColorAsync(content, content.Status, cache); + + if (content.NewStatus != null) + { + result.NewStatusColor = await GetColorAsync(content, content.NewStatus.Value, cache); } - private async Task<string> GetColorAsync(IContentEntity content, Status status, Dictionary<(DomainId, Status), StatusInfo> cache) + if (content.ScheduleJob != null) { - if (!cache.TryGetValue((content.SchemaId.Id, status), out var info)) - { - info = await contentWorkflow.GetInfoAsync(content, status); + result.ScheduledStatusColor = await GetColorAsync(content, content.ScheduleJob.Status, cache); + } + } - if (info == null) - { - info = new StatusInfo(status, DefaultColor); - } + private async Task<string> GetColorAsync(IContentEntity content, Status status, Dictionary<(DomainId, Status), StatusInfo> cache) + { + if (!cache.TryGetValue((content.SchemaId.Id, status), out var info)) + { + info = await contentWorkflow.GetInfoAsync(content, status); - cache[(content.SchemaId.Id, status)] = info; + if (info == null) + { + info = new StatusInfo(status, DefaultColor); } - return info.Color; + cache[(content.SchemaId.Id, status)] = info; } - private static bool ShouldEnrichWithStatuses(Context context) - { - return context.IsFrontendClient || context.ShouldResolveFlow(); - } + return info.Color; + } + + private static bool ShouldEnrichWithStatuses(Context context) + { + return context.IsFrontendClient || context.ShouldResolveFlow(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs index 7c8ed846b2..d7ff4622d4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs @@ -16,136 +16,135 @@ using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; + +public sealed class ResolveAssets : IContentEnricherStep { - public sealed class ResolveAssets : IContentEnricherStep - { - private static readonly ILookup<DomainId, IEnrichedAssetEntity> EmptyAssets = Enumerable.Empty<IEnrichedAssetEntity>().ToLookup(x => x.Id); + private static readonly ILookup<DomainId, IEnrichedAssetEntity> EmptyAssets = Enumerable.Empty<IEnrichedAssetEntity>().ToLookup(x => x.Id); - private readonly IUrlGenerator urlGenerator; - private readonly IAssetQueryService assetQuery; - private readonly IRequestCache requestCache; + private readonly IUrlGenerator urlGenerator; + private readonly IAssetQueryService assetQuery; + private readonly IRequestCache requestCache; - public ResolveAssets(IUrlGenerator urlGenerator, IAssetQueryService assetQuery, IRequestCache requestCache) - { - this.urlGenerator = urlGenerator; - this.assetQuery = assetQuery; - this.requestCache = requestCache; - } + public ResolveAssets(IUrlGenerator urlGenerator, IAssetQueryService assetQuery, IRequestCache requestCache) + { + this.urlGenerator = urlGenerator; + this.assetQuery = assetQuery; + this.requestCache = requestCache; + } - public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) + public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + if (ShouldEnrich(context)) { - if (ShouldEnrich(context)) - { - var ids = new HashSet<DomainId>(); + var ids = new HashSet<DomainId>(); - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var (schema, components) = await schemas(group.Key); + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var (schema, components) = await schemas(group.Key); - AddAssetIds(ids, schema, components, group); - } + AddAssetIds(ids, schema, components, group); + } - var assets = await GetAssetsAsync(context, ids, ct); + var assets = await GetAssetsAsync(context, ids, ct); - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var (schema, components) = await schemas(group.Key); + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var (schema, components) = await schemas(group.Key); - ResolveAssetsUrls(schema, components, group, assets); - } + ResolveAssetsUrls(schema, components, group, assets); } } + } - private void ResolveAssetsUrls(ISchemaEntity schema, ResolvedComponents components, - IGrouping<DomainId, ContentEntity> contents, ILookup<DomainId, IEnrichedAssetEntity> assets) - { - HashSet<DomainId>? fieldIds = null; + private void ResolveAssetsUrls(ISchemaEntity schema, ResolvedComponents components, + IGrouping<DomainId, ContentEntity> contents, ILookup<DomainId, IEnrichedAssetEntity> assets) + { + HashSet<DomainId>? fieldIds = null; - foreach (var field in schema.SchemaDef.ResolvingAssets()) + foreach (var field in schema.SchemaDef.ResolvingAssets()) + { + foreach (var content in contents) { - foreach (var content in contents) - { - content.ReferenceData ??= new ContentData(); + content.ReferenceData ??= new ContentData(); - var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; - if (content.Data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + if (content.Data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var (partitionKey, partitionValue) in fieldData) { - foreach (var (partitionKey, partitionValue) in fieldData) - { - fieldIds ??= new HashSet<DomainId>(); - fieldIds.Clear(); + fieldIds ??= new HashSet<DomainId>(); + fieldIds.Clear(); - partitionValue.AddReferencedIds(field, fieldIds, components); + partitionValue.AddReferencedIds(field, fieldIds, components); - var referencedAsset = - fieldIds - .Select(x => assets[x]) - .SelectMany(x => x) - .FirstOrDefault(); + var referencedAsset = + fieldIds + .Select(x => assets[x]) + .SelectMany(x => x) + .FirstOrDefault(); - if (referencedAsset != null) - { - var array = new JsonArray(); + if (referencedAsset != null) + { + var array = new JsonArray(); - if (IsImage(referencedAsset)) - { - var url = urlGenerator.AssetContent( - referencedAsset.AppId, - referencedAsset.Id.ToString()); + if (IsImage(referencedAsset)) + { + var url = urlGenerator.AssetContent( + referencedAsset.AppId, + referencedAsset.Id.ToString()); - array.Add(url); - } + array.Add(url); + } - array.Add(referencedAsset.FileName); + array.Add(referencedAsset.FileName); - requestCache.AddDependency(referencedAsset.UniqueId, referencedAsset.Version); + requestCache.AddDependency(referencedAsset.UniqueId, referencedAsset.Version); - fieldReference.AddLocalized(partitionKey, array); - } + fieldReference.AddLocalized(partitionKey, array); } } } } } + } - private static bool IsImage(IEnrichedAssetEntity asset) - { - const int PreviewLimit = 10 * 1024; + private static bool IsImage(IEnrichedAssetEntity asset) + { + const int PreviewLimit = 10 * 1024; - return asset.Type == AssetType.Image || (asset.MimeType == "image/svg+xml" && asset.FileSize < PreviewLimit); - } + return asset.Type == AssetType.Image || (asset.MimeType == "image/svg+xml" && asset.FileSize < PreviewLimit); + } - private async Task<ILookup<DomainId, IEnrichedAssetEntity>> GetAssetsAsync(Context context, HashSet<DomainId> ids, - CancellationToken ct) + private async Task<ILookup<DomainId, IEnrichedAssetEntity>> GetAssetsAsync(Context context, HashSet<DomainId> ids, + CancellationToken ct) + { + if (ids.Count == 0) { - if (ids.Count == 0) - { - return EmptyAssets; - } + return EmptyAssets; + } - var queryContext = context.Clone(b => b - .WithoutAssetEnrichment(true) - .WithoutTotal()); + var queryContext = context.Clone(b => b + .WithoutAssetEnrichment(true) + .WithoutTotal()); - var assets = await assetQuery.QueryAsync(queryContext, null, Q.Empty.WithIds(ids), ct); + var assets = await assetQuery.QueryAsync(queryContext, null, Q.Empty.WithIds(ids), ct); - return assets.ToLookup(x => x.Id); - } + return assets.ToLookup(x => x.Id); + } - private static void AddAssetIds(HashSet<DomainId> ids, ISchemaEntity schema, ResolvedComponents components, IEnumerable<ContentEntity> contents) + private static void AddAssetIds(HashSet<DomainId> ids, ISchemaEntity schema, ResolvedComponents components, IEnumerable<ContentEntity> contents) + { + foreach (var content in contents) { - foreach (var content in contents) - { - content.Data.AddReferencedIds(schema.SchemaDef.ResolvingAssets(), ids, components, 1); - } + content.Data.AddReferencedIds(schema.SchemaDef.ResolvingAssets(), ids, components, 1); } + } - private static bool ShouldEnrich(Context context) - { - return context.IsFrontendClient && !context.ShouldSkipContentEnrichment(); - } + private static bool ShouldEnrich(Context context) + { + return context.IsFrontendClient && !context.ShouldSkipContentEnrichment(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs index cd1ed5a9aa..8e1d486e63 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs @@ -14,162 +14,161 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; + +public sealed class ResolveReferences : IContentEnricherStep { - public sealed class ResolveReferences : IContentEnricherStep - { - private static readonly ILookup<DomainId, IEnrichedContentEntity> EmptyContents = Enumerable.Empty<IEnrichedContentEntity>().ToLookup(x => x.Id); - private readonly Lazy<IContentQueryService> contentQuery; - private readonly IRequestCache requestCache; + private static readonly ILookup<DomainId, IEnrichedContentEntity> EmptyContents = Enumerable.Empty<IEnrichedContentEntity>().ToLookup(x => x.Id); + private readonly Lazy<IContentQueryService> contentQuery; + private readonly IRequestCache requestCache; - private IContentQueryService ContentQuery - { - get => contentQuery.Value; - } + private IContentQueryService ContentQuery + { + get => contentQuery.Value; + } - public ResolveReferences(Lazy<IContentQueryService> contentQuery, IRequestCache requestCache) - { - this.contentQuery = contentQuery; + public ResolveReferences(Lazy<IContentQueryService> contentQuery, IRequestCache requestCache) + { + this.contentQuery = contentQuery; - this.requestCache = requestCache; - } + this.requestCache = requestCache; + } - public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) + public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + if (!ShouldEnrich(context)) { - if (!ShouldEnrich(context)) - { - return; - } + return; + } - var ids = new HashSet<DomainId>(); + var ids = new HashSet<DomainId>(); - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var (schema, components) = await schemas(group.Key); + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var (schema, components) = await schemas(group.Key); - AddReferenceIds(ids, schema, components, group); - } + AddReferenceIds(ids, schema, components, group); + } - var references = await GetReferencesAsync(context, ids, ct); + var references = await GetReferencesAsync(context, ids, ct); - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var (schema, components) = await schemas(group.Key); + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var (schema, components) = await schemas(group.Key); - await ResolveReferencesAsync(context, schema, components, group, references, schemas); - } + await ResolveReferencesAsync(context, schema, components, group, references, schemas); } + } - private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, ResolvedComponents components, - IEnumerable<ContentEntity> contents, ILookup<DomainId, IEnrichedContentEntity> references, ProvideSchema schemas) - { - HashSet<DomainId>? fieldIds = null; + private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, ResolvedComponents components, + IEnumerable<ContentEntity> contents, ILookup<DomainId, IEnrichedContentEntity> references, ProvideSchema schemas) + { + HashSet<DomainId>? fieldIds = null; - var formatted = new Dictionary<IContentEntity, JsonObject>(); + var formatted = new Dictionary<IContentEntity, JsonObject>(); - foreach (var field in schema.SchemaDef.ResolvingReferences()) + foreach (var field in schema.SchemaDef.ResolvingReferences()) + { + foreach (var content in contents) { - foreach (var content in contents) - { - content.ReferenceData ??= new ContentData(); + content.ReferenceData ??= new ContentData(); - var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; - try + try + { + if (content.Data.TryGetValue(field.Name, out var fieldData) && fieldData != null) { - if (content.Data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + foreach (var (partition, partitionValue) in fieldData) { - foreach (var (partition, partitionValue) in fieldData) - { - fieldIds ??= new HashSet<DomainId>(); - fieldIds.Clear(); + fieldIds ??= new HashSet<DomainId>(); + fieldIds.Clear(); - partitionValue.AddReferencedIds(field, fieldIds, components); + partitionValue.AddReferencedIds(field, fieldIds, components); - var referencedContents = - fieldIds - .Select(x => references[x]) - .SelectMany(x => x) - .ToList(); + var referencedContents = + fieldIds + .Select(x => references[x]) + .SelectMany(x => x) + .ToList(); - if (referencedContents.Count == 1) - { - var reference = referencedContents[0]; + if (referencedContents.Count == 1) + { + var reference = referencedContents[0]; - var (referencedSchema, _) = await schemas(reference.SchemaId.Id); + var (referencedSchema, _) = await schemas(reference.SchemaId.Id); - requestCache.AddDependency(referencedSchema.UniqueId, referencedSchema.Version); - requestCache.AddDependency(reference.UniqueId, reference.Version); + requestCache.AddDependency(referencedSchema.UniqueId, referencedSchema.Version); + requestCache.AddDependency(reference.UniqueId, reference.Version); - var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); + var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); - fieldReference.AddLocalized(partition, value); - } - else if (referencedContents.Count > 1) - { - var value = CreateFallback(context, referencedContents); + fieldReference.AddLocalized(partition, value); + } + else if (referencedContents.Count > 1) + { + var value = CreateFallback(context, referencedContents); - fieldReference.AddLocalized(partition, value); - } + fieldReference.AddLocalized(partition, value); } } } - catch (DomainObjectNotFoundException) - { - continue; - } + } + catch (DomainObjectNotFoundException) + { + continue; } } } + } - private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) - { - return content.Data.FormatReferences(referencedSchema.SchemaDef, context.App.Languages); - } - - private static JsonObject CreateFallback(Context context, List<IEnrichedContentEntity> referencedContents) - { - var text = T.Get("contents.listReferences", new { count = referencedContents.Count }); + private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) + { + return content.Data.FormatReferences(referencedSchema.SchemaDef, context.App.Languages); + } - var value = new JsonObject(); + private static JsonObject CreateFallback(Context context, List<IEnrichedContentEntity> referencedContents) + { + var text = T.Get("contents.listReferences", new { count = referencedContents.Count }); - foreach (var partitionKey in context.App.Languages.AllKeys) - { - value.Add(partitionKey, text); - } + var value = new JsonObject(); - return value; + foreach (var partitionKey in context.App.Languages.AllKeys) + { + value.Add(partitionKey, text); } - private static void AddReferenceIds(HashSet<DomainId> ids, ISchemaEntity schema, ResolvedComponents components, IEnumerable<ContentEntity> contents) + return value; + } + + private static void AddReferenceIds(HashSet<DomainId> ids, ISchemaEntity schema, ResolvedComponents components, IEnumerable<ContentEntity> contents) + { + foreach (var content in contents) { - foreach (var content in contents) - { - content.Data.AddReferencedIds(schema.SchemaDef.ResolvingReferences(), ids, components); - } + content.Data.AddReferencedIds(schema.SchemaDef.ResolvingReferences(), ids, components); } + } - private async Task<ILookup<DomainId, IEnrichedContentEntity>> GetReferencesAsync(Context context, HashSet<DomainId> ids, - CancellationToken ct) + private async Task<ILookup<DomainId, IEnrichedContentEntity>> GetReferencesAsync(Context context, HashSet<DomainId> ids, + CancellationToken ct) + { + if (ids.Count == 0) { - if (ids.Count == 0) - { - return EmptyContents; - } + return EmptyContents; + } - var queryContext = context.Clone(b => b - .WithoutContentEnrichment(true) - .WithoutTotal()); + var queryContext = context.Clone(b => b + .WithoutContentEnrichment(true) + .WithoutTotal()); - var references = await ContentQuery.QueryAsync(queryContext, Q.Empty.WithIds(ids).WithoutTotal(), ct); + var references = await ContentQuery.QueryAsync(queryContext, Q.Empty.WithIds(ids).WithoutTotal(), ct); - return references.ToLookup(x => x.Id); - } + return references.ToLookup(x => x.Id); + } - private static bool ShouldEnrich(Context context) - { - return context.IsFrontendClient && !context.ShouldSkipContentEnrichment(); - } + private static bool ShouldEnrich(Context context) + { + return context.IsFrontendClient && !context.ShouldSkipContentEnrichment(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs index 8cf20b80c0..ab8593d5cc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs @@ -7,95 +7,94 @@ using Squidex.Domain.Apps.Core.Scripting; -namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; + +public sealed class ScriptContent : IContentEnricherStep { - public sealed class ScriptContent : IContentEnricherStep + private readonly IScriptEngine scriptEngine; + + public ScriptContent(IScriptEngine scriptEngine) { - private readonly IScriptEngine scriptEngine; + this.scriptEngine = scriptEngine; + } - public ScriptContent(IScriptEngine scriptEngine) + public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + if (!ShouldEnrich(context)) { - this.scriptEngine = scriptEngine; + return; } - public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) { - if (!ShouldEnrich(context)) + var (schema, _) = await schemas(group.Key); + + var script = schema.SchemaDef.Scripts.Query; + + if (string.IsNullOrWhiteSpace(script)) { - return; + continue; } - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + var vars = new ContentScriptVars { - var (schema, _) = await schemas(group.Key); + AppId = schema.AppId.Id, + AppName = schema.AppId.Name, + SchemaId = schema.Id, + SchemaName = schema.SchemaDef.Name, + User = context.UserPrincipal + }; - var script = schema.SchemaDef.Scripts.Query; + var preScript = schema.SchemaDef.Scripts.QueryPre; - if (string.IsNullOrWhiteSpace(script)) - { - continue; - } - - var vars = new ContentScriptVars + if (!string.IsNullOrWhiteSpace(preScript)) + { + var options = new ScriptOptions { - AppId = schema.AppId.Id, - AppName = schema.AppId.Name, - SchemaId = schema.Id, - SchemaName = schema.SchemaDef.Name, - User = context.UserPrincipal + AsContext = true }; - var preScript = schema.SchemaDef.Scripts.QueryPre; - - if (!string.IsNullOrWhiteSpace(preScript)) - { - var options = new ScriptOptions - { - AsContext = true - }; - - await scriptEngine.ExecuteAsync(vars, preScript, options, ct); - } + await scriptEngine.ExecuteAsync(vars, preScript, options, ct); + } - foreach (var content in group) - { - await TransformAsync(vars, script, content, ct); - } + foreach (var content in group) + { + await TransformAsync(vars, script, content, ct); } } + } - private async Task TransformAsync(ContentScriptVars sharedVars, string script, ContentEntity content, - CancellationToken ct) + private async Task TransformAsync(ContentScriptVars sharedVars, string script, ContentEntity content, + CancellationToken ct) + { + var vars = new ContentScriptVars { - var vars = new ContentScriptVars - { - ContentId = content.Id, - Data = content.Data, - DataOld = default, - Status = content.Status, - StatusOld = default - }; - - foreach (var (key, value) in sharedVars) + ContentId = content.Id, + Data = content.Data, + DataOld = default, + Status = content.Status, + StatusOld = default + }; + + foreach (var (key, value) in sharedVars) + { + if (!vars.ContainsKey(key)) { - if (!vars.ContainsKey(key)) - { - vars[key] = value; - } + vars[key] = value; } - - var options = new ScriptOptions - { - AsContext = true - }; - - content.Data = await scriptEngine.TransformAsync(vars, script, options, ct); } - private static bool ShouldEnrich(Context context) + var options = new ScriptOptions { - return !context.IsFrontendClient; - } + AsContext = true + }; + + content.Data = await scriptEngine.TransformAsync(vars, script, options, ct); + } + + private static bool ShouldEnrich(Context context) + { + return !context.IsFrontendClient; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs index f54f6bd40b..6120c27b89 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs @@ -17,110 +17,109 @@ #pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ReferencesFluidExtension : IFluidExtension { - public sealed class ReferencesFluidExtension : IFluidExtension + private static readonly FluidValue ErrorNullReference = FluidValue.Create(null); + private readonly IServiceProvider serviceProvider; + + private sealed class ReferenceTag : ArgumentsTag { - private static readonly FluidValue ErrorNullReference = FluidValue.Create(null); private readonly IServiceProvider serviceProvider; - private sealed class ReferenceTag : ArgumentsTag + public ReferenceTag(IServiceProvider serviceProvider) { - private readonly IServiceProvider serviceProvider; - - public ReferenceTag(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } + this.serviceProvider = serviceProvider; + } - public override async ValueTask<Completion> WriteToAsync(TextWriter writer, - TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) + public override async ValueTask<Completion> WriteToAsync(TextWriter writer, + TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) + { + if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) { - if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) - { - var id = await arguments[1].Expression.EvaluateAsync(context); + var id = await arguments[1].Expression.EvaluateAsync(context); - var content = await ResolveContentAsync(serviceProvider, enrichedEvent.AppId.Id, id); + var content = await ResolveContentAsync(serviceProvider, enrichedEvent.AppId.Id, id); - if (content != null) - { - var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); + if (content != null) + { + var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); - context.SetValue(name, content); - } + context.SetValue(name, content); } - - return Completion.Normal; } - } - public ReferencesFluidExtension(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; + return Completion.Normal; } + } - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - memberAccessStrategy.Register<IContentEntity>(); - memberAccessStrategy.Register<IWithId<DomainId>>(); - memberAccessStrategy.Register<IEntity>(); - memberAccessStrategy.Register<IEntityWithCreatedBy>(); - memberAccessStrategy.Register<IEntityWithLastModifiedBy>(); - memberAccessStrategy.Register<IEntityWithVersion>(); - memberAccessStrategy.Register<IEnrichedContentEntity>(); - - AddReferenceFilter(); - } + public ReferencesFluidExtension(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } - private void AddReferenceFilter() + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + memberAccessStrategy.Register<IContentEntity>(); + memberAccessStrategy.Register<IWithId<DomainId>>(); + memberAccessStrategy.Register<IEntity>(); + memberAccessStrategy.Register<IEntityWithCreatedBy>(); + memberAccessStrategy.Register<IEntityWithLastModifiedBy>(); + memberAccessStrategy.Register<IEntityWithVersion>(); + memberAccessStrategy.Register<IEnrichedContentEntity>(); + + AddReferenceFilter(); + } + + private void AddReferenceFilter() + { + TemplateContext.GlobalFilters.AddAsyncFilter("reference", async (input, arguments, context) => { - TemplateContext.GlobalFilters.AddAsyncFilter("reference", async (input, arguments, context) => + if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) { - if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) - { - var content = await ResolveContentAsync(serviceProvider, enrichedEvent.AppId.Id, input); + var content = await ResolveContentAsync(serviceProvider, enrichedEvent.AppId.Id, input); - if (content == null) - { - return ErrorNullReference; - } - - return FluidValue.Create(content); + if (content == null) + { + return ErrorNullReference; } - return ErrorNullReference; - }); - } + return FluidValue.Create(content); + } - public void RegisterLanguageExtensions(FluidParserFactory factory) - { - factory.RegisterTag("reference", new ReferenceTag(serviceProvider)); - } + return ErrorNullReference; + }); + } - private static async Task<IContentEntity?> ResolveContentAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id) - { - var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); + public void RegisterLanguageExtensions(FluidParserFactory factory) + { + factory.RegisterTag("reference", new ReferenceTag(serviceProvider)); + } - var app = await appProvider.GetAppAsync(appId); + private static async Task<IContentEntity?> ResolveContentAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id) + { + var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); - if (app == null) - { - return null; - } + var app = await appProvider.GetAppAsync(appId); - var domainId = DomainId.Create(id.ToStringValue()); + if (app == null) + { + return null; + } - var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>(); + var domainId = DomainId.Create(id.ToStringValue()); - var requestContext = - Context.Admin(app).Clone(b => b - .WithoutContentEnrichment() - .WithUnpublished() - .WithoutTotal()); + var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>(); - var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainId)); + var requestContext = + Context.Admin(app).Clone(b => b + .WithoutContentEnrichment() + .WithUnpublished() + .WithoutTotal()); - return contents.FirstOrDefault(); - } + var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainId)); + + return contents.FirstOrDefault(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs index 386d9dd6b3..f93963a342 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs @@ -14,105 +14,104 @@ using Squidex.Domain.Apps.Entities.Properties; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor { - public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor + private delegate void GetReferencesDelegate(JsValue references, Action<JsValue> callback); + private readonly IServiceProvider serviceProvider; + + public ReferencesJintExtension(IServiceProvider serviceProvider) { - private delegate void GetReferencesDelegate(JsValue references, Action<JsValue> callback); - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public ReferencesJintExtension(IServiceProvider serviceProvider) + public void ExtendAsync(ScriptExecutionContext context) + { + if (!context.TryGetValue<DomainId>("appId", out var appId)) { - this.serviceProvider = serviceProvider; + return; } - public void ExtendAsync(ScriptExecutionContext context) + if (!context.TryGetValue<ClaimsPrincipal>("user", out var user)) { - if (!context.TryGetValue<DomainId>("appId", out var appId)) - { - return; - } + return; + } - if (!context.TryGetValue<ClaimsPrincipal>("user", out var user)) - { - return; - } + var action = new GetReferencesDelegate((references, callback) => + { + GetReferences(context, appId, user, references, callback); + }); - var action = new GetReferencesDelegate((references, callback) => - { - GetReferences(context, appId, user, references, callback); - }); + context.Engine.SetValue("getReference", action); + context.Engine.SetValue("getReferences", action); + } - context.Engine.SetValue("getReference", action); - context.Engine.SetValue("getReferences", action); + public void Describe(AddDescription describe, ScriptScope scope) + { + if (!scope.HasFlag(ScriptScope.Async)) + { + return; } - public void Describe(AddDescription describe, ScriptScope scope) - { - if (!scope.HasFlag(ScriptScope.Async)) - { - return; - } + describe(JsonType.Function, "getReferences(ids, callback)", + Resources.ScriptingGetReferences); - describe(JsonType.Function, "getReferences(ids, callback)", - Resources.ScriptingGetReferences); + describe(JsonType.Function, "getReference(ids, callback)", + Resources.ScriptingGetReference); + } - describe(JsonType.Function, "getReference(ids, callback)", - Resources.ScriptingGetReference); - } + private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback) + { + Guard.NotNull(callback); - private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback) + context.Schedule(async (scheduler, ct) => { - Guard.NotNull(callback); + var ids = references.ToIds(); - context.Schedule(async (scheduler, ct) => + if (ids.Count == 0) { - var ids = references.ToIds(); - - if (ids.Count == 0) - { - var emptyContents = Array.Empty<IEnrichedContentEntity>(); - - scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyContents)); - return; - } + var emptyContents = Array.Empty<IEnrichedContentEntity>(); - var app = await GetAppAsync(appId); + scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyContents)); + return; + } - if (app == null) - { - var emptyContents = Array.Empty<IEnrichedContentEntity>(); + var app = await GetAppAsync(appId); - scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyContents)); - return; - } + if (app == null) + { + var emptyContents = Array.Empty<IEnrichedContentEntity>(); - var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>(); + scheduler.Run(callback, JsValue.FromObject(context.Engine, emptyContents)); + return; + } - var requestContext = - new Context(user, app).Clone(b => b - .WithoutContentEnrichment() - .WithUnpublished() - .WithoutTotal()); + var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>(); - var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids), ct); + var requestContext = + new Context(user, app).Clone(b => b + .WithoutContentEnrichment() + .WithUnpublished() + .WithoutTotal()); - scheduler.Run(callback, JsValue.FromObject(context.Engine, contents.ToArray())); - }); - } + var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids), ct); - private async Task<IAppEntity> GetAppAsync(DomainId appId) - { - var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); + scheduler.Run(callback, JsValue.FromObject(context.Engine, contents.ToArray())); + }); + } - var app = await appProvider.GetAppAsync(appId); + private async Task<IAppEntity> GetAppAsync(DomainId appId) + { + var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); - if (app == null) - { - throw new JavaScriptException("App does not exist."); - } + var app = await appProvider.GetAppAsync(appId); - return app; + if (app == null) + { + throw new JavaScriptException("App does not exist."); } + + return app; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index d2b5dde5de..720ba24d2b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -12,35 +12,34 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.Contents.Repositories +namespace Squidex.Domain.Apps.Entities.Contents.Repositories; + +public interface IContentRepository { - public interface IContentRepository - { - IAsyncEnumerable<IContentEntity> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds, - CancellationToken ct = default); + IAsyncEnumerable<IContentEntity> StreamAll(DomainId appId, HashSet<DomainId>? schemaIds, + CancellationToken ct = default); - Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q, SearchScope scope, - CancellationToken ct = default); + Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q, SearchScope scope, + CancellationToken ct = default); - Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope, - CancellationToken ct = default); + Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope, + CancellationToken ct = default); - Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode, - CancellationToken ct = default); + Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode, + CancellationToken ct = default); - Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids, SearchScope scope, - CancellationToken ct = default); + Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids, SearchScope scope, + CancellationToken ct = default); - Task<IContentEntity?> FindContentAsync(IAppEntity app, ISchemaEntity schema, DomainId id, SearchScope scope, - CancellationToken ct = default); + Task<IContentEntity?> FindContentAsync(IAppEntity app, ISchemaEntity schema, DomainId id, SearchScope scope, + CancellationToken ct = default); - Task<bool> HasReferrersAsync(DomainId appId, DomainId contentId, SearchScope scope, - CancellationToken ct = default); + Task<bool> HasReferrersAsync(DomainId appId, DomainId contentId, SearchScope scope, + CancellationToken ct = default); - Task ResetScheduledAsync(DomainId documentId, - CancellationToken ct = default); + Task ResetScheduledAsync(DomainId documentId, + CancellationToken ct = default); - IAsyncEnumerable<IContentEntity> QueryScheduledWithoutDataAsync(Instant now, - CancellationToken ct = default); - } + IAsyncEnumerable<IContentEntity> QueryScheduledWithoutDataAsync(Instant now, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs index 64c33593dc..1638b29c11 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs @@ -9,29 +9,28 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class ScheduleJob { - public sealed class ScheduleJob - { - public DomainId Id { get; } + public DomainId Id { get; } - public Instant DueTime { get; } + public Instant DueTime { get; } - public Status Status { get; } + public Status Status { get; } - public RefToken ScheduledBy { get; } + public RefToken ScheduledBy { get; } - public ScheduleJob(DomainId id, Status status, RefToken scheduledBy, Instant dueTime) - { - Id = id; - ScheduledBy = scheduledBy; - Status = status; - DueTime = dueTime; - } + public ScheduleJob(DomainId id, Status status, RefToken scheduledBy, Instant dueTime) + { + Id = id; + ScheduledBy = scheduledBy; + Status = status; + DueTime = dueTime; + } - public static ScheduleJob Build(Status status, RefToken scheduledBy, Instant dueTime) - { - return new ScheduleJob(DomainId.NewGuid(), status, scheduledBy, dueTime); - } + public static ScheduleJob Build(Status status, RefToken scheduledBy, Instant dueTime) + { + return new ScheduleJob(DomainId.NewGuid(), status, scheduledBy, dueTime); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/SearchScope.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/SearchScope.cs index ed7d9a1d8c..479660ba01 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/SearchScope.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/SearchScope.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public enum SearchScope { - public enum SearchScope - { - All, - Published - } + All, + Published } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs index 0a12fac5d8..7b452bddd1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs @@ -13,37 +13,36 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public sealed class SingletonCommandMiddleware : ICommandMiddleware { - public sealed class SingletonCommandMiddleware : ICommandMiddleware + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + await next(context, ct); + + if (context.IsCompleted && context.Command is CreateSchema { Type: SchemaType.Singleton } createSchema) { - await next(context, ct); + var schemaId = NamedId.Of(createSchema.SchemaId, createSchema.Name); + + var data = new ContentData(); - if (context.IsCompleted && context.Command is CreateSchema { Type: SchemaType.Singleton } createSchema) + var contentId = schemaId.Id; + var content = new CreateContent { - var schemaId = NamedId.Of(createSchema.SchemaId, createSchema.Name); - - var data = new ContentData(); - - var contentId = schemaId.Id; - var content = new CreateContent - { - Data = data, - ContentId = contentId, - DoNotScript = true, - DoNotValidate = true, - SchemaId = schemaId, - Status = Status.Published - }; - - SimpleMapper.Map(createSchema, content); - - // Always create the corresponding content and therefore do not pass over cancellation token. - await context.CommandBus.PublishAsync(content, default); - } + Data = data, + ContentId = contentId, + DoNotScript = true, + DoNotValidate = true, + SchemaId = schemaId, + Status = Status.Published + }; + + SimpleMapper.Map(createSchema, content); + + // Always create the corresponding content and therefore do not pass over cancellation token. + await context.CommandBus.PublishAsync(content, default); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/DeleteIndexEntry.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/DeleteIndexEntry.cs index 4bdb6fec88..39728c2641 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/DeleteIndexEntry.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/DeleteIndexEntry.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class DeleteIndexEntry : IndexCommand { - public sealed class DeleteIndexEntry : IndexCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs index e0052cdb67..c91557a5d5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs @@ -13,113 +13,112 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.ObjectPool; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public static class Extensions { - public static class Extensions + public static Dictionary<string, Geometry>? ToGeo(this ContentData data, IJsonSerializer serializer) { - public static Dictionary<string, Geometry>? ToGeo(this ContentData data, IJsonSerializer serializer) - { - Dictionary<string, Geometry>? result = null; + Dictionary<string, Geometry>? result = null; - foreach (var (field, value) in data) + foreach (var (field, value) in data) + { + if (value != null) { - if (value != null) + foreach (var (key, jsonValue) in value) { - foreach (var (key, jsonValue) in value) - { - GeoJsonValue.TryParse(jsonValue, serializer, out var geoJson); + GeoJsonValue.TryParse(jsonValue, serializer, out var geoJson); - if (geoJson != null) - { - result ??= new Dictionary<string, Geometry>(); - result[$"{field}.{key}"] = geoJson; - } + if (geoJson != null) + { + result ??= new Dictionary<string, Geometry>(); + result[$"{field}.{key}"] = geoJson; } } } - - return result; } - public static Dictionary<string, string>? ToTexts(this ContentData data) - { - Dictionary<string, string>? result = null; + return result; + } - if (data != null) + public static Dictionary<string, string>? ToTexts(this ContentData data) + { + Dictionary<string, string>? result = null; + + if (data != null) + { + var languages = new Dictionary<string, StringBuilder>(); + try { - var languages = new Dictionary<string, StringBuilder>(); - try + foreach (var (_, value) in data) { - foreach (var (_, value) in data) + if (value != null) { - if (value != null) + foreach (var (key, jsonValue) in value) { - foreach (var (key, jsonValue) in value) - { - AppendJsonText(languages, key, jsonValue); - } + AppendJsonText(languages, key, jsonValue); } } + } - foreach (var (key, sb) in languages) + foreach (var (key, sb) in languages) + { + if (sb.Length > 0) { - if (sb.Length > 0) - { - result ??= new Dictionary<string, string>(); - result[key] = sb.ToString(); - } + result ??= new Dictionary<string, string>(); + result[key] = sb.ToString(); } } - finally + } + finally + { + foreach (var (_, sb) in languages) { - foreach (var (_, sb) in languages) - { - DefaultPools.StringBuilder.Return(sb); - } + DefaultPools.StringBuilder.Return(sb); } } - - return result; } - private static void AppendJsonText(Dictionary<string, StringBuilder> languages, string language, JsonValue value) + return result; + } + + private static void AppendJsonText(Dictionary<string, StringBuilder> languages, string language, JsonValue value) + { + switch (value.Value) { - switch (value.Value) - { - case string s: - AppendText(languages, language, s); - break; - case JsonArray a: - foreach (var item in a) - { - AppendJsonText(languages, language, item); - } + case string s: + AppendText(languages, language, s); + break; + case JsonArray a: + foreach (var item in a) + { + AppendJsonText(languages, language, item); + } - break; - case JsonObject o: - foreach (var (_, item) in o) - { - AppendJsonText(languages, language, item); - } + break; + case JsonObject o: + foreach (var (_, item) in o) + { + AppendJsonText(languages, language, item); + } - break; - } + break; } + } - private static void AppendText(Dictionary<string, StringBuilder> languages, string language, string text) + private static void AppendText(Dictionary<string, StringBuilder> languages, string language, string text) + { + if (!string.IsNullOrWhiteSpace(text)) { - if (!string.IsNullOrWhiteSpace(text)) + if (!languages.TryGetValue(language, out var sb)) { - if (!languages.TryGetValue(language, out var sb)) - { - sb = DefaultPools.StringBuilder.Get(); + sb = DefaultPools.StringBuilder.Get(); - languages[language] = sb; - } - - sb.AppendIfNotEmpty(' '); - sb.Append(text); + languages[language] = sb; } + + sb.AppendIfNotEmpty(' '); + sb.Append(text); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GeoQuery.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GeoQuery.cs index d448caf22a..f7673389f3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GeoQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GeoQuery.cs @@ -9,7 +9,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Contents.Text -{ - public sealed record GeoQuery(DomainId SchemaId, string Field, double Latitude, double Longitude, double Radius, int Take); -} +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed record GeoQuery(DomainId SchemaId, string Field, double Latitude, double Longitude, double Radius, int Take); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndex.cs index eb2f187445..ba14ad8f1f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndex.cs @@ -8,20 +8,19 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public interface ITextIndex { - public interface ITextIndex - { - Task<List<DomainId>?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, - CancellationToken ct = default); + Task<List<DomainId>?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, + CancellationToken ct = default); - Task<List<DomainId>?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, - CancellationToken ct = default); + Task<List<DomainId>?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, + CancellationToken ct = default); - Task ClearAsync( - CancellationToken ct = default); + Task ClearAsync( + CancellationToken ct = default); - Task ExecuteAsync(IndexCommand[] commands, - CancellationToken ct = default); - } + Task ExecuteAsync(IndexCommand[] commands, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexCommand.cs index 49a3302c70..9e5f318819 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexCommand.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public abstract class IndexCommand { - public abstract class IndexCommand - { - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public NamedId<DomainId> SchemaId { get; set; } + public NamedId<DomainId> SchemaId { get; set; } - public string DocId { get; set; } - } + public string DocId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Query.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Query.cs index 26f4985c62..28dffe9961 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Query.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Query.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class Query { - public sealed class Query - { - public string Text { get; init; } - } + public string Text { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/QueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/QueryParser.cs index 3ebfa2e941..b9f7663fcd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/QueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/QueryParser.cs @@ -7,74 +7,73 @@ using System.Text; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class QueryParser { - public sealed class QueryParser + private readonly Func<string, string> fieldProvider; + + public QueryParser(Func<string, string> fieldProvider) { - private readonly Func<string, string> fieldProvider; + this.fieldProvider = fieldProvider; + } - public QueryParser(Func<string, string> fieldProvider) + public Query? Parse(string text) + { + if (string.IsNullOrWhiteSpace(text)) { - this.fieldProvider = fieldProvider; + return null; } - public Query? Parse(string text) - { - if (string.IsNullOrWhiteSpace(text)) - { - return null; - } + text = text.Trim(); + text = ConvertFieldNames(text); - text = text.Trim(); - text = ConvertFieldNames(text); + return new Query + { + Text = text + }; + } - return new Query - { - Text = text - }; - } + private string ConvertFieldNames(string query) + { + var indexOfColon = query.IndexOf(':', StringComparison.Ordinal); - private string ConvertFieldNames(string query) + if (indexOfColon < 0) { - var indexOfColon = query.IndexOf(':', StringComparison.Ordinal); + return query; + } - if (indexOfColon < 0) - { - return query; - } + var sb = new StringBuilder(); - var sb = new StringBuilder(); + int position = 0, lastIndexOfColon = 0; - int position = 0, lastIndexOfColon = 0; + while (indexOfColon >= 0) + { + lastIndexOfColon = indexOfColon; - while (indexOfColon >= 0) + int i; + for (i = indexOfColon - 1; i >= position; i--) { - lastIndexOfColon = indexOfColon; + var c = query[i]; - int i; - for (i = indexOfColon - 1; i >= position; i--) + if (!char.IsLetter(c) && c != '-' && c != '_') { - var c = query[i]; - - if (!char.IsLetter(c) && c != '-' && c != '_') - { - break; - } + break; } + } - i++; + i++; - sb.Append(query[position..i]); - sb.Append(fieldProvider(query[i..indexOfColon])); + sb.Append(query[position..i]); + sb.Append(fieldProvider(query[i..indexOfColon])); - position = indexOfColon + 1; + position = indexOfColon + 1; - indexOfColon = query.IndexOf(':', position); - } + indexOfColon = query.IndexOf(':', position); + } - sb.Append(query[lastIndexOfColon..]); + sb.Append(query[lastIndexOfColon..]); - return sb.ToString(); - } + return sb.ToString(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs index 35c8313141..9651dd32c0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs @@ -8,90 +8,89 @@ using Squidex.Caching; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Text.State +namespace Squidex.Domain.Apps.Entities.Contents.Text.State; + +public sealed class CachingTextIndexerState : ITextIndexerState { - public sealed class CachingTextIndexerState : ITextIndexerState - { - private readonly ITextIndexerState inner; - private readonly LRUCache<DomainId, Tuple<TextContentState?>> cache = new LRUCache<DomainId, Tuple<TextContentState?>>(10000); + private readonly ITextIndexerState inner; + private readonly LRUCache<DomainId, Tuple<TextContentState?>> cache = new LRUCache<DomainId, Tuple<TextContentState?>>(10000); - public CachingTextIndexerState(ITextIndexerState inner) - { - Guard.NotNull(inner); + public CachingTextIndexerState(ITextIndexerState inner) + { + Guard.NotNull(inner); - this.inner = inner; - } + this.inner = inner; + } - public async Task ClearAsync( - CancellationToken ct = default) - { - await inner.ClearAsync(ct); + public async Task ClearAsync( + CancellationToken ct = default) + { + await inner.ClearAsync(ct); - cache.Clear(); - } + cache.Clear(); + } - public async Task<Dictionary<DomainId, TextContentState>> GetAsync(HashSet<DomainId> ids, - CancellationToken ct = default) - { - Guard.NotNull(ids); + public async Task<Dictionary<DomainId, TextContentState>> GetAsync(HashSet<DomainId> ids, + CancellationToken ct = default) + { + Guard.NotNull(ids); - var missingIds = new HashSet<DomainId>(); + var missingIds = new HashSet<DomainId>(); - var result = new Dictionary<DomainId, TextContentState>(); + var result = new Dictionary<DomainId, TextContentState>(); - foreach (var id in ids) + foreach (var id in ids) + { + if (cache.TryGetValue(id, out var state)) { - if (cache.TryGetValue(id, out var state)) + if (state.Item1 != null) { - if (state.Item1 != null) - { - result[id] = state.Item1; - } - } - else - { - missingIds.Add(id); + result[id] = state.Item1; } } - - if (missingIds.Count > 0) + else { - var fromInner = await inner.GetAsync(missingIds, ct); - - foreach (var (id, state) in fromInner) - { - result[id] = state; - } + missingIds.Add(id); + } + } - foreach (var id in missingIds) - { - var state = fromInner.GetValueOrDefault(id); + if (missingIds.Count > 0) + { + var fromInner = await inner.GetAsync(missingIds, ct); - cache.Set(id, Tuple.Create<TextContentState?>(state)); - } + foreach (var (id, state) in fromInner) + { + result[id] = state; } - return result; + foreach (var id in missingIds) + { + var state = fromInner.GetValueOrDefault(id); + + cache.Set(id, Tuple.Create<TextContentState?>(state)); + } } - public Task SetAsync(List<TextContentState> updates, - CancellationToken ct = default) - { - Guard.NotNull(updates); + return result; + } + + public Task SetAsync(List<TextContentState> updates, + CancellationToken ct = default) + { + Guard.NotNull(updates); - foreach (var update in updates) + foreach (var update in updates) + { + if (update.IsDeleted) { - if (update.IsDeleted) - { - cache.Set(update.UniqueContentId, Tuple.Create<TextContentState?>(null)); - } - else - { - cache.Set(update.UniqueContentId, Tuple.Create<TextContentState?>(update)); - } + cache.Set(update.UniqueContentId, Tuple.Create<TextContentState?>(null)); + } + else + { + cache.Set(update.UniqueContentId, Tuple.Create<TextContentState?>(update)); } - - return inner.SetAsync(updates, ct); } + + return inner.SetAsync(updates, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/ITextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/ITextIndexerState.cs index e7d68520b3..97f5fb8024 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/ITextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/ITextIndexerState.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Text.State +namespace Squidex.Domain.Apps.Entities.Contents.Text.State; + +public interface ITextIndexerState { - public interface ITextIndexerState - { - Task<Dictionary<DomainId, TextContentState>> GetAsync(HashSet<DomainId> ids, - CancellationToken ct = default); + Task<Dictionary<DomainId, TextContentState>> GetAsync(HashSet<DomainId> ids, + CancellationToken ct = default); - Task SetAsync(List<TextContentState> updates, - CancellationToken ct = default); + Task SetAsync(List<TextContentState> updates, + CancellationToken ct = default); - Task ClearAsync( - CancellationToken ct = default); - } + Task ClearAsync( + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/InMemoryTextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/InMemoryTextIndexerState.cs index f8e067041a..cb1fcd99e3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/InMemoryTextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/InMemoryTextIndexerState.cs @@ -7,56 +7,55 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Text.State +namespace Squidex.Domain.Apps.Entities.Contents.Text.State; + +public sealed class InMemoryTextIndexerState : ITextIndexerState { - public sealed class InMemoryTextIndexerState : ITextIndexerState - { - private readonly Dictionary<DomainId, TextContentState> states = new Dictionary<DomainId, TextContentState>(); + private readonly Dictionary<DomainId, TextContentState> states = new Dictionary<DomainId, TextContentState>(); - public Task ClearAsync( - CancellationToken ct = default) - { - states.Clear(); + public Task ClearAsync( + CancellationToken ct = default) + { + states.Clear(); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task<Dictionary<DomainId, TextContentState>> GetAsync(HashSet<DomainId> ids, - CancellationToken ct = default) - { - Guard.NotNull(ids); + public Task<Dictionary<DomainId, TextContentState>> GetAsync(HashSet<DomainId> ids, + CancellationToken ct = default) + { + Guard.NotNull(ids); - var result = new Dictionary<DomainId, TextContentState>(); + var result = new Dictionary<DomainId, TextContentState>(); - foreach (var id in ids) + foreach (var id in ids) + { + if (states.TryGetValue(id, out var state)) { - if (states.TryGetValue(id, out var state)) - { - result.Add(id, state); - } + result.Add(id, state); } - - return Task.FromResult(result); } - public Task SetAsync(List<TextContentState> updates, - CancellationToken ct = default) - { - Guard.NotNull(updates); + return Task.FromResult(result); + } - foreach (var update in updates) + public Task SetAsync(List<TextContentState> updates, + CancellationToken ct = default) + { + Guard.NotNull(updates); + + foreach (var update in updates) + { + if (update.IsDeleted) { - if (update.IsDeleted) - { - states.Remove(update.UniqueContentId); - } - else - { - states[update.UniqueContentId] = update; - } + states.Remove(update.UniqueContentId); + } + else + { + states[update.UniqueContentId] = update; } - - return Task.CompletedTask; } + + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/TextContentState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/TextContentState.cs index e829c0b186..0efba07c85 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/TextContentState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/TextContentState.cs @@ -7,44 +7,43 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Text.State +namespace Squidex.Domain.Apps.Entities.Contents.Text.State; + +public sealed class TextContentState { - public sealed class TextContentState - { - public DomainId AppId { get; set; } + public DomainId AppId { get; set; } - public DomainId UniqueContentId { get; set; } + public DomainId UniqueContentId { get; set; } - public string DocIdCurrent { get; set; } + public string DocIdCurrent { get; set; } - public string? DocIdNew { get; set; } + public string? DocIdNew { get; set; } - public string? DocIdForPublished { get; set; } + public string? DocIdForPublished { get; set; } - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - public void GenerateDocIdNew() + public void GenerateDocIdNew() + { + if (DocIdCurrent?.EndsWith("_2", StringComparison.Ordinal) != false) + { + DocIdNew = $"{UniqueContentId}_1"; + } + else { - if (DocIdCurrent?.EndsWith("_2", StringComparison.Ordinal) != false) - { - DocIdNew = $"{UniqueContentId}_1"; - } - else - { - DocIdNew = $"{UniqueContentId}_2"; - } + DocIdNew = $"{UniqueContentId}_2"; } + } - public void GenerateDocIdCurrent() + public void GenerateDocIdCurrent() + { + if (DocIdNew?.EndsWith("_2", StringComparison.Ordinal) != false) + { + DocIdCurrent = $"{UniqueContentId}_1"; + } + else { - if (DocIdNew?.EndsWith("_2", StringComparison.Ordinal) != false) - { - DocIdCurrent = $"{UniqueContentId}_1"; - } - else - { - DocIdCurrent = $"{UniqueContentId}_2"; - } + DocIdCurrent = $"{UniqueContentId}_2"; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs index 290c065628..754f68d348 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs @@ -12,262 +12,220 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class TextIndexingProcess : IEventConsumer { - public sealed class TextIndexingProcess : IEventConsumer + private const string NotFound = "<404>"; + private readonly IJsonSerializer serializer; + private readonly ITextIndex textIndex; + private readonly ITextIndexerState textIndexerState; + + public int BatchSize { - private const string NotFound = "<404>"; - private readonly IJsonSerializer serializer; - private readonly ITextIndex textIndex; - private readonly ITextIndexerState textIndexerState; + get => 1000; + } - public int BatchSize - { - get => 1000; - } + public int BatchDelay + { + get => 1000; + } - public int BatchDelay - { - get => 1000; - } + public string Name + { + get => "TextIndexer5"; + } - public string Name - { - get => "TextIndexer5"; - } + public string EventsFilter + { + get => "^content-"; + } - public string EventsFilter - { - get => "^content-"; - } + public ITextIndex TextIndex + { + get => textIndex; + } + + private sealed class Updates + { + private readonly Dictionary<DomainId, TextContentState> states; + private readonly IJsonSerializer serializer; + private readonly Dictionary<DomainId, TextContentState> updates = new Dictionary<DomainId, TextContentState>(); + private readonly Dictionary<string, IndexCommand> commands = new Dictionary<string, IndexCommand>(); - public ITextIndex TextIndex + public Updates(Dictionary<DomainId, TextContentState> states, IJsonSerializer serializer) { - get => textIndex; + this.states = states; + this.serializer = serializer; } - private sealed class Updates + public async Task WriteAsync(ITextIndex textIndex, ITextIndexerState textIndexerState) { - private readonly Dictionary<DomainId, TextContentState> states; - private readonly IJsonSerializer serializer; - private readonly Dictionary<DomainId, TextContentState> updates = new Dictionary<DomainId, TextContentState>(); - private readonly Dictionary<string, IndexCommand> commands = new Dictionary<string, IndexCommand>(); - - public Updates(Dictionary<DomainId, TextContentState> states, IJsonSerializer serializer) + if (commands.Count > 0) { - this.states = states; - this.serializer = serializer; + await textIndex.ExecuteAsync(commands.Values.ToArray()); } - public async Task WriteAsync(ITextIndex textIndex, ITextIndexerState textIndexerState) + if (updates.Count > 0) { - if (commands.Count > 0) - { - await textIndex.ExecuteAsync(commands.Values.ToArray()); - } - - if (updates.Count > 0) - { - await textIndexerState.SetAsync(updates.Values.ToList()); - } + await textIndexerState.SetAsync(updates.Values.ToList()); } + } - public void On(Envelope<IEvent> @event) + public void On(Envelope<IEvent> @event) + { + switch (@event.Payload) { - switch (@event.Payload) - { - case ContentCreated created: - Create(created, created.Data); - break; - case ContentUpdated updated: - Update(updated, updated.Data); - break; - case ContentStatusChanged statusChanged when statusChanged.Status == Status.Published: - Publish(statusChanged); - break; - case ContentStatusChanged statusChanged: - Unpublish(statusChanged); - break; - case ContentDraftDeleted draftDelted: - DeleteDraft(draftDelted); - break; - case ContentDeleted deleted: - Delete(deleted); - break; - case ContentDraftCreated draftCreated: - { - CreateDraft(draftCreated); + case ContentCreated created: + Create(created, created.Data); + break; + case ContentUpdated updated: + Update(updated, updated.Data); + break; + case ContentStatusChanged statusChanged when statusChanged.Status == Status.Published: + Publish(statusChanged); + break; + case ContentStatusChanged statusChanged: + Unpublish(statusChanged); + break; + case ContentDraftDeleted draftDelted: + DeleteDraft(draftDelted); + break; + case ContentDeleted deleted: + Delete(deleted); + break; + case ContentDraftCreated draftCreated: + { + CreateDraft(draftCreated); - if (draftCreated.MigratedData != null) - { - Update(draftCreated, draftCreated.MigratedData); - } + if (draftCreated.MigratedData != null) + { + Update(draftCreated, draftCreated.MigratedData); } + } - break; - } + break; } + } - private void Create(ContentEvent @event, ContentData data) + private void Create(ContentEvent @event, ContentData data) + { + var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); + + var state = new TextContentState { - var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); + AppId = @event.AppId.Id, + UniqueContentId = uniqueId + }; + + state.GenerateDocIdCurrent(); - var state = new TextContentState + Index(@event, + new UpsertIndexEntry { - AppId = @event.AppId.Id, - UniqueContentId = uniqueId - }; + ContentId = @event.ContentId, + DocId = state.DocIdCurrent, + GeoObjects = data.ToGeo(serializer), + ServeAll = true, + ServePublished = false, + Texts = data.ToTexts(), + IsNew = true + }); + + states[state.UniqueContentId] = state; + + updates[state.UniqueContentId] = state; + } + + private void CreateDraft(ContentEvent @event) + { + var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); + + if (states.TryGetValue(uniqueId, out var state)) + { + state.GenerateDocIdNew(); - state.GenerateDocIdCurrent(); + updates[state.UniqueContentId] = state; + } + } + private void Unpublish(ContentEvent @event) + { + var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); + + if (states.TryGetValue(uniqueId, out var state) && state.DocIdForPublished != null) + { Index(@event, - new UpsertIndexEntry + new UpdateIndexEntry { - ContentId = @event.ContentId, - DocId = state.DocIdCurrent, - GeoObjects = data.ToGeo(serializer), + DocId = state.DocIdForPublished, ServeAll = true, - ServePublished = false, - Texts = data.ToTexts(), - IsNew = true + ServePublished = false }); - states[state.UniqueContentId] = state; + state.DocIdForPublished = null; updates[state.UniqueContentId] = state; } + } - private void CreateDraft(ContentEvent @event) - { - var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - - if (states.TryGetValue(uniqueId, out var state)) - { - state.GenerateDocIdNew(); - - updates[state.UniqueContentId] = state; - } - } + private void Update(ContentEvent @event, ContentData data) + { + var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - private void Unpublish(ContentEvent @event) + if (states.TryGetValue(uniqueId, out var state)) { - var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - - if (states.TryGetValue(uniqueId, out var state) && state.DocIdForPublished != null) + if (state.DocIdNew != null) { Index(@event, - new UpdateIndexEntry + new UpsertIndexEntry { - DocId = state.DocIdForPublished, + ContentId = @event.ContentId, + DocId = state.DocIdNew, + GeoObjects = data.ToGeo(serializer), ServeAll = true, - ServePublished = false + ServePublished = false, + Texts = data.ToTexts() }); - state.DocIdForPublished = null; - - updates[state.UniqueContentId] = state; + Index(@event, + new UpdateIndexEntry + { + DocId = state.DocIdCurrent, + ServeAll = false, + ServePublished = true + }); } - } - - private void Update(ContentEvent @event, ContentData data) - { - var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - - if (states.TryGetValue(uniqueId, out var state)) + else { - if (state.DocIdNew != null) - { - Index(@event, - new UpsertIndexEntry - { - ContentId = @event.ContentId, - DocId = state.DocIdNew, - GeoObjects = data.ToGeo(serializer), - ServeAll = true, - ServePublished = false, - Texts = data.ToTexts() - }); - - Index(@event, - new UpdateIndexEntry - { - DocId = state.DocIdCurrent, - ServeAll = false, - ServePublished = true - }); - } - else - { - var isPublished = state.DocIdCurrent == state.DocIdForPublished; - - Index(@event, - new UpsertIndexEntry - { - ContentId = @event.ContentId, - DocId = state.DocIdCurrent, - GeoObjects = data.ToGeo(serializer), - ServeAll = true, - ServePublished = isPublished, - Texts = data.ToTexts() - }); - } - } - } + var isPublished = state.DocIdCurrent == state.DocIdForPublished; - private void Publish(ContentEvent @event) - { - var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - - if (states.TryGetValue(uniqueId, out var state)) - { - if (state.DocIdNew != null) - { - Index(@event, - new UpdateIndexEntry - { - DocId = state.DocIdNew, - ServeAll = true, - ServePublished = true - }); - - Index(@event, - new DeleteIndexEntry - { - DocId = state.DocIdCurrent - }); - - state.DocIdForPublished = state.DocIdNew; - state.DocIdCurrent = state.DocIdNew; - } - else - { - Index(@event, - new UpdateIndexEntry - { - DocId = state.DocIdCurrent, - ServeAll = true, - ServePublished = true - }); - - state.DocIdForPublished = state.DocIdCurrent; - } - - state.DocIdNew = null; - - updates[state.UniqueContentId] = state; + Index(@event, + new UpsertIndexEntry + { + ContentId = @event.ContentId, + DocId = state.DocIdCurrent, + GeoObjects = data.ToGeo(serializer), + ServeAll = true, + ServePublished = isPublished, + Texts = data.ToTexts() + }); } } + } - private void DeleteDraft(ContentEvent @event) - { - var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); + private void Publish(ContentEvent @event) + { + var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - if (states.TryGetValue(uniqueId, out var state) && state.DocIdNew != null) + if (states.TryGetValue(uniqueId, out var state)) + { + if (state.DocIdNew != null) { Index(@event, new UpdateIndexEntry { - DocId = state.DocIdCurrent, + DocId = state.DocIdNew, ServeAll = true, ServePublished = true }); @@ -275,97 +233,138 @@ private void DeleteDraft(ContentEvent @event) Index(@event, new DeleteIndexEntry { - DocId = state.DocIdNew + DocId = state.DocIdCurrent }); - state.DocIdNew = null; - - updates[state.UniqueContentId] = state; + state.DocIdForPublished = state.DocIdNew; + state.DocIdCurrent = state.DocIdNew; } - } - - private void Delete(ContentEvent @event) - { - var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - - if (states.TryGetValue(uniqueId, out var state)) + else { Index(@event, - new DeleteIndexEntry + new UpdateIndexEntry { - DocId = state.DocIdCurrent + DocId = state.DocIdCurrent, + ServeAll = true, + ServePublished = true }); - Index(@event, - new DeleteIndexEntry - { - DocId = state.DocIdNew ?? NotFound - }); + state.DocIdForPublished = state.DocIdCurrent; + } - state.IsDeleted = true; + state.DocIdNew = null; - updates[state.UniqueContentId] = state; - } + updates[state.UniqueContentId] = state; } + } + + private void DeleteDraft(ContentEvent @event) + { + var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - private void Index(ContentEvent @event, IndexCommand command) + if (states.TryGetValue(uniqueId, out var state) && state.DocIdNew != null) { - command.AppId = @event.AppId; - command.SchemaId = @event.SchemaId; + Index(@event, + new UpdateIndexEntry + { + DocId = state.DocIdCurrent, + ServeAll = true, + ServePublished = true + }); - if (command is UpdateIndexEntry update && - commands.TryGetValue(command.DocId, out var existing) && - existing is UpsertIndexEntry upsert) - { - upsert.ServeAll = update.ServeAll; - upsert.ServePublished = update.ServePublished; - } - else - { - commands[command.DocId] = command; - } + Index(@event, + new DeleteIndexEntry + { + DocId = state.DocIdNew + }); + + state.DocIdNew = null; + + updates[state.UniqueContentId] = state; } } - public TextIndexingProcess( - IJsonSerializer serializer, - ITextIndex textIndex, - ITextIndexerState textIndexerState) + private void Delete(ContentEvent @event) { - this.serializer = serializer; - this.textIndex = textIndex; - this.textIndexerState = textIndexerState; - } + var uniqueId = DomainId.Combine(@event.AppId, @event.ContentId); - public async Task ClearAsync() - { - await textIndex.ClearAsync(); - await textIndexerState.ClearAsync(); + if (states.TryGetValue(uniqueId, out var state)) + { + Index(@event, + new DeleteIndexEntry + { + DocId = state.DocIdCurrent + }); + + Index(@event, + new DeleteIndexEntry + { + DocId = state.DocIdNew ?? NotFound + }); + + state.IsDeleted = true; + + updates[state.UniqueContentId] = state; + } } - public async Task On(IEnumerable<Envelope<IEvent>> events) + private void Index(ContentEvent @event, IndexCommand command) { - var states = await QueryStatesAsync(events); - - var updates = new Updates(states, serializer); + command.AppId = @event.AppId; + command.SchemaId = @event.SchemaId; - foreach (var @event in events) + if (command is UpdateIndexEntry update && + commands.TryGetValue(command.DocId, out var existing) && + existing is UpsertIndexEntry upsert) { - updates.On(@event); + upsert.ServeAll = update.ServeAll; + upsert.ServePublished = update.ServePublished; + } + else + { + commands[command.DocId] = command; } - - await updates.WriteAsync(textIndex, textIndexerState); } + } - private Task<Dictionary<DomainId, TextContentState>> QueryStatesAsync(IEnumerable<Envelope<IEvent>> events) - { - var ids = - events - .Select(x => x.Payload).OfType<ContentEvent>() - .Select(x => DomainId.Combine(x.AppId, x.ContentId)) - .ToHashSet(); + public TextIndexingProcess( + IJsonSerializer serializer, + ITextIndex textIndex, + ITextIndexerState textIndexerState) + { + this.serializer = serializer; + this.textIndex = textIndex; + this.textIndexerState = textIndexerState; + } - return textIndexerState.GetAsync(ids); + public async Task ClearAsync() + { + await textIndex.ClearAsync(); + await textIndexerState.ClearAsync(); + } + + public async Task On(IEnumerable<Envelope<IEvent>> events) + { + var states = await QueryStatesAsync(events); + + var updates = new Updates(states, serializer); + + foreach (var @event in events) + { + updates.On(@event); } + + await updates.WriteAsync(textIndex, textIndexerState); + } + + private Task<Dictionary<DomainId, TextContentState>> QueryStatesAsync(IEnumerable<Envelope<IEvent>> events) + { + var ids = + events + .Select(x => x.Payload).OfType<ContentEvent>() + .Select(x => DomainId.Combine(x.AppId, x.ContentId)) + .ToHashSet(); + + return textIndexerState.GetAsync(ids); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextQuery.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextQuery.cs index db496e53e5..d900492930 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextQuery.cs @@ -9,12 +9,11 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed record TextQuery(string Text, int Take) { - public sealed record TextQuery(string Text, int Take) - { - public IReadOnlyList<DomainId>? RequiredSchemaIds { get; init; } + public IReadOnlyList<DomainId>? RequiredSchemaIds { get; init; } - public DomainId? PreferredSchemaId { get; init; } - } + public DomainId? PreferredSchemaId { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpdateIndexEntry.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpdateIndexEntry.cs index df0672ad98..210a1715a4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpdateIndexEntry.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpdateIndexEntry.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public class UpdateIndexEntry : IndexCommand { - public class UpdateIndexEntry : IndexCommand - { - public bool ServeAll { get; set; } + public bool ServeAll { get; set; } - public bool ServePublished { get; set; } - } + public bool ServePublished { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs index d4952cedb6..aeb4996695 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs @@ -8,20 +8,19 @@ using NetTopologySuite.Geometries; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class UpsertIndexEntry : IndexCommand { - public sealed class UpsertIndexEntry : IndexCommand - { - public Dictionary<string, Geometry>? GeoObjects { get; set; } + public Dictionary<string, Geometry>? GeoObjects { get; set; } - public Dictionary<string, string>? Texts { get; set; } + public Dictionary<string, string>? Texts { get; set; } - public bool ServeAll { get; set; } + public bool ServeAll { get; set; } - public bool ServePublished { get; set; } + public bool ServePublished { get; set; } - public DomainId ContentId { get; set; } + public DomainId ContentId { get; set; } - public bool IsNew { get; set; } - } + public bool IsNew { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs index ccc5dde9f6..c1fffae0ec 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs @@ -11,79 +11,78 @@ using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories; -namespace Squidex.Domain.Apps.Entities.Contents.Validation +namespace Squidex.Domain.Apps.Entities.Contents.Validation; + +public sealed class DependencyValidatorsFactory : IValidatorsFactory { - public sealed class DependencyValidatorsFactory : IValidatorsFactory + private readonly IAssetRepository assetRepository; + private readonly IContentRepository contentRepository; + + public DependencyValidatorsFactory(IAssetRepository assetRepository, IContentRepository contentRepository) { - private readonly IAssetRepository assetRepository; - private readonly IContentRepository contentRepository; + this.assetRepository = assetRepository; + this.contentRepository = contentRepository; + } - public DependencyValidatorsFactory(IAssetRepository assetRepository, IContentRepository contentRepository) + public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) + { + if (context.Mode == ValidationMode.Optimized) { - this.assetRepository = assetRepository; - this.contentRepository = contentRepository; + yield break; } - public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) - { - if (context.Mode == ValidationMode.Optimized) - { - yield break; - } + var isRequired = IsRequired(context, field.RawProperties); - var isRequired = IsRequired(context, field.RawProperties); - - if (field is IField<AssetsFieldProperties> assetsField) + if (field is IField<AssetsFieldProperties> assetsField) + { + var checkAssets = new CheckAssets(async ids => { - var checkAssets = new CheckAssets(async ids => - { - return await assetRepository.QueryAsync(context.Root.AppId.Id, null, Q.Empty.WithIds(ids), default); - }); + return await assetRepository.QueryAsync(context.Root.AppId.Id, null, Q.Empty.WithIds(ids), default); + }); - yield return new AssetsValidator(isRequired, assetsField.Properties, checkAssets); - } + yield return new AssetsValidator(isRequired, assetsField.Properties, checkAssets); + } - if (field is IField<ReferencesFieldProperties> referencesField) + if (field is IField<ReferencesFieldProperties> referencesField) + { + var checkReferences = new CheckContentsByIds(async ids => { - var checkReferences = new CheckContentsByIds(async ids => - { - return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, ids, SearchScope.All, default); - }); + return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, ids, SearchScope.All, default); + }); - yield return new ReferencesValidator(isRequired, referencesField.Properties, checkReferences); - } + yield return new ReferencesValidator(isRequired, referencesField.Properties, checkReferences); + } - if (field is IField<NumberFieldProperties> numberField && numberField.Properties.IsUnique) + if (field is IField<NumberFieldProperties> numberField && numberField.Properties.IsUnique) + { + var checkUniqueness = new CheckUniqueness(async filter => { - var checkUniqueness = new CheckUniqueness(async filter => - { - return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter, default); - }); + return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter, default); + }); - yield return new UniqueValidator(checkUniqueness); - } + yield return new UniqueValidator(checkUniqueness); + } - if (field is IField<StringFieldProperties> stringField && stringField.Properties.IsUnique) + if (field is IField<StringFieldProperties> stringField && stringField.Properties.IsUnique) + { + var checkUniqueness = new CheckUniqueness(async filter => { - var checkUniqueness = new CheckUniqueness(async filter => - { - return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter, default); - }); + return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter, default); + }); - yield return new UniqueValidator(checkUniqueness); - } + yield return new UniqueValidator(checkUniqueness); } + } - private static bool IsRequired(ValidationContext context, FieldProperties properties) - { - var isRequired = properties.IsRequired; - - if (context.Action == ValidationAction.Publish) - { - isRequired = isRequired || properties.IsRequiredOnPublish; - } + private static bool IsRequired(ValidationContext context, FieldProperties properties) + { + var isRequired = properties.IsRequired; - return isRequired; + if (context.Action == ValidationAction.Publish) + { + isRequired = isRequired || properties.IsRequiredOnPublish; } + + return isRequired; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Context.cs b/backend/src/Squidex.Domain.Apps.Entities/Context.cs index f64b814e3c..099c910bfd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Context.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Context.cs @@ -15,128 +15,127 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public sealed class Context { - public sealed class Context - { - private static readonly IReadOnlyDictionary<string, string> EmptyHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + private static readonly IReadOnlyDictionary<string, string> EmptyHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - public IReadOnlyDictionary<string, string> Headers { get; private set; } + public IReadOnlyDictionary<string, string> Headers { get; private set; } - public ClaimsPermissions UserPermissions { get; } + public ClaimsPermissions UserPermissions { get; } - public ClaimsPrincipal UserPrincipal { get; } + public ClaimsPrincipal UserPrincipal { get; } - public IAppEntity App { get; set; } + public IAppEntity App { get; set; } - public bool IsFrontendClient => UserPrincipal.IsInClient(DefaultClients.Frontend); + public bool IsFrontendClient => UserPrincipal.IsInClient(DefaultClients.Frontend); - public Context(ClaimsPrincipal user, IAppEntity app) - : this(app, user, user.Claims.Permissions(), EmptyHeaders) - { - Guard.NotNull(user); - } + public Context(ClaimsPrincipal user, IAppEntity app) + : this(app, user, user.Claims.Permissions(), EmptyHeaders) + { + Guard.NotNull(user); + } - private Context( - IAppEntity app, - ClaimsPrincipal userPrincipal, - ClaimsPermissions userPermissions, - IReadOnlyDictionary<string, string> headers) - { - App = app; + private Context( + IAppEntity app, + ClaimsPrincipal userPrincipal, + ClaimsPermissions userPermissions, + IReadOnlyDictionary<string, string> headers) + { + App = app; - UserPrincipal = userPrincipal; - UserPermissions = userPermissions; + UserPrincipal = userPrincipal; + UserPermissions = userPermissions; - Headers = headers; - } + Headers = headers; + } - public static Context Anonymous(IAppEntity app) - { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + public static Context Anonymous(IAppEntity app) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - return new Context(claimsPrincipal, app); - } + return new Context(claimsPrincipal, app); + } - public static Context Admin(IAppEntity app) - { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + public static Context Admin(IAppEntity app) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.All)); + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.All)); - return new Context(claimsPrincipal, app); - } + return new Context(claimsPrincipal, app); + } + + public bool Allows(string permissionId, string schema = Permission.Any) + { + return UserPermissions.Allows(permissionId, App.Name, schema); + } + + private sealed class HeaderBuilder : ICloneBuilder + { + private readonly Context context; + private Dictionary<string, string>? headers; - public bool Allows(string permissionId, string schema = Permission.Any) + public HeaderBuilder(Context context) { - return UserPermissions.Allows(permissionId, App.Name, schema); + this.context = context; } - private sealed class HeaderBuilder : ICloneBuilder + public Context Build() { - private readonly Context context; - private Dictionary<string, string>? headers; - - public HeaderBuilder(Context context) + if (headers != null) { - this.context = context; + return new Context(context.App!, context.UserPrincipal, context.UserPermissions, headers); } - public Context Build() - { - if (headers != null) - { - return new Context(context.App!, context.UserPrincipal, context.UserPermissions, headers); - } - - return context; - } - - public Context Update() - { - context.Headers = headers ?? context.Headers; - - return context; - } - - public void Remove(string key) - { - headers ??= new Dictionary<string, string>(context.Headers, StringComparer.OrdinalIgnoreCase); - headers.Remove(key); - } - - public void SetHeader(string key, string value) - { - headers ??= new Dictionary<string, string>(context.Headers, StringComparer.OrdinalIgnoreCase); - headers[key] = value; - } + return context; } - public Context Change(Action<ICloneBuilder> action) + public Context Update() { - var builder = new HeaderBuilder(this); + context.Headers = headers ?? context.Headers; - action(builder); + return context; + } - return builder.Update(); + public void Remove(string key) + { + headers ??= new Dictionary<string, string>(context.Headers, StringComparer.OrdinalIgnoreCase); + headers.Remove(key); } - public Context Clone(Action<ICloneBuilder> action) + public void SetHeader(string key, string value) { - var builder = new HeaderBuilder(this); + headers ??= new Dictionary<string, string>(context.Headers, StringComparer.OrdinalIgnoreCase); + headers[key] = value; + } + } - action(builder); + public Context Change(Action<ICloneBuilder> action) + { + var builder = new HeaderBuilder(this); - return builder.Build(); - } + action(builder); + + return builder.Update(); } - public interface ICloneBuilder + public Context Clone(Action<ICloneBuilder> action) { - void SetHeader(string key, string value); + var builder = new HeaderBuilder(this); + + action(builder); - void Remove(string key); + return builder.Build(); } } + +public interface ICloneBuilder +{ + void SetHeader(string key, string value); + + void Remove(string key); +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/ContextExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/ContextExtensions.cs index d93719b2be..d3371209df 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/ContextExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/ContextExtensions.cs @@ -5,59 +5,58 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public static class ContextExtensions { - public static class ContextExtensions + private const string HeaderNoTotal = "X-NoTotal"; + private const string HeaderNoSlowTotal = "X-NoSlowTotal"; + + public static bool ShouldSkipTotal(this Context context) { - private const string HeaderNoTotal = "X-NoTotal"; - private const string HeaderNoSlowTotal = "X-NoSlowTotal"; + return context.Headers.ContainsKey(HeaderNoTotal); + } - public static bool ShouldSkipTotal(this Context context) - { - return context.Headers.ContainsKey(HeaderNoTotal); - } + public static ICloneBuilder WithoutTotal(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderNoTotal, value); + } - public static ICloneBuilder WithoutTotal(this ICloneBuilder builder, bool value = true) - { - return builder.WithBoolean(HeaderNoTotal, value); - } + public static bool ShouldSkipSlowTotal(this Context context) + { + return context.Headers.ContainsKey(HeaderNoSlowTotal); + } - public static bool ShouldSkipSlowTotal(this Context context) + public static ICloneBuilder WithoutSlowTotal(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(HeaderNoSlowTotal, value); + } + + public static ICloneBuilder WithBoolean(this ICloneBuilder builder, string key, bool value) + { + if (value) { - return context.Headers.ContainsKey(HeaderNoSlowTotal); + builder.SetHeader(key, "1"); } - - public static ICloneBuilder WithoutSlowTotal(this ICloneBuilder builder, bool value = true) + else { - return builder.WithBoolean(HeaderNoSlowTotal, value); + builder.Remove(key); } - public static ICloneBuilder WithBoolean(this ICloneBuilder builder, string key, bool value) + return builder; + } + + public static ICloneBuilder WithStrings(this ICloneBuilder builder, string key, IEnumerable<string>? values) + { + if (values?.Any() == true) { - if (value) - { - builder.SetHeader(key, "1"); - } - else - { - builder.Remove(key); - } - - return builder; + builder.SetHeader(key, string.Join(",", values)); } - - public static ICloneBuilder WithStrings(this ICloneBuilder builder, string key, IEnumerable<string>? values) + else { - if (values?.Any() == true) - { - builder.SetHeader(key, string.Join(",", values)); - } - else - { - builder.Remove(key); - } - - return builder; + builder.Remove(key); } + + return builder; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 5dcae143fa..1206adc53d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -11,72 +11,71 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public abstract class DomainObjectState<T> : IDomainState<T> where T : class { - public abstract class DomainObjectState<T> : IDomainState<T> where T : class - { - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public RefToken CreatedBy { get; set; } + public RefToken CreatedBy { get; set; } - public RefToken LastModifiedBy { get; set; } + public RefToken LastModifiedBy { get; set; } - public Instant Created { get; set; } + public Instant Created { get; set; } - public Instant LastModified { get; set; } + public Instant LastModified { get; set; } - public long Version { get; set; } + public long Version { get; set; } - protected DomainObjectState() - { - Version = EtagVersion.Empty; - } + protected DomainObjectState() + { + Version = EtagVersion.Empty; + } - public virtual bool ApplyEvent(IEvent @event, EnvelopeHeaders headers) - { - return ApplyEvent(@event); - } + public virtual bool ApplyEvent(IEvent @event, EnvelopeHeaders headers) + { + return ApplyEvent(@event); + } - public virtual bool ApplyEvent(IEvent @event) - { - return false; - } + public virtual bool ApplyEvent(IEvent @event) + { + return false; + } - public T Copy() - { - return (T)MemberwiseClone(); - } + public T Copy() + { + return (T)MemberwiseClone(); + } - public T Apply(Envelope<IEvent> @event) - { - var payload = (SquidexEvent)@event.Payload; + public T Apply(Envelope<IEvent> @event) + { + var payload = (SquidexEvent)@event.Payload; - var clone = (DomainObjectState<T>)MemberwiseClone(); + var clone = (DomainObjectState<T>)MemberwiseClone(); - if (!clone.ApplyEvent(@event.Payload, @event.Headers)) - { - return (this as T)!; - } + if (!clone.ApplyEvent(@event.Payload, @event.Headers)) + { + return (this as T)!; + } - var headers = @event.Headers; + var headers = @event.Headers; - if (clone.Id == DomainId.Empty) - { - clone.Id = headers.AggregateId(); - } + if (clone.Id == DomainId.Empty) + { + clone.Id = headers.AggregateId(); + } - var timestamp = headers.Timestamp(); + var timestamp = headers.Timestamp(); - if (clone.CreatedBy == null) - { - clone.Created = timestamp; - clone.CreatedBy = payload.Actor; - } + if (clone.CreatedBy == null) + { + clone.Created = timestamp; + clone.CreatedBy = payload.Actor; + } - clone.LastModified = timestamp; - clone.LastModifiedBy = payload.Actor; + clone.LastModified = timestamp; + clone.LastModifiedBy = payload.Actor; - return (clone as T)!; - } + return (clone as T)!; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs index f9ae6ef909..2bb309e18d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs @@ -8,50 +8,49 @@ using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.History -{ - public sealed class HistoryEvent - { - public DomainId Id { get; set; } = DomainId.NewGuid(); +namespace Squidex.Domain.Apps.Entities.History; - public DomainId OwnerId { get; set; } +public sealed class HistoryEvent +{ + public DomainId Id { get; set; } = DomainId.NewGuid(); - public Instant Created { get; set; } + public DomainId OwnerId { get; set; } - public RefToken Actor { get; set; } + public Instant Created { get; set; } - public long Version { get; set; } + public RefToken Actor { get; set; } - public string Channel { get; set; } + public long Version { get; set; } - public string EventType { get; set; } + public string Channel { get; set; } - public Dictionary<string, string> Parameters { get; set; } = new Dictionary<string, string>(); + public string EventType { get; set; } - public HistoryEvent() - { - } + public Dictionary<string, string> Parameters { get; set; } = new Dictionary<string, string>(); - public HistoryEvent(string channel, string eventType) - { - Guard.NotNullOrEmpty(channel); - Guard.NotNullOrEmpty(eventType); + public HistoryEvent() + { + } - Channel = channel; + public HistoryEvent(string channel, string eventType) + { + Guard.NotNullOrEmpty(channel); + Guard.NotNullOrEmpty(eventType); - EventType = eventType; - } + Channel = channel; - public HistoryEvent Param(string key, object? value) - { - var formatted = value?.ToString(); + EventType = eventType; + } - if (formatted != null) - { - Parameters[key] = formatted; - } + public HistoryEvent Param(string key, object? value) + { + var formatted = value?.ToString(); - return this; + if (formatted != null) + { + Parameters[key] = formatted; } + + return this; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs index 4a0c0d2bdf..b5168cb0c7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs @@ -9,56 +9,55 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.History +namespace Squidex.Domain.Apps.Entities.History; + +public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator { - public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator - { - private readonly Dictionary<string, string> texts = new Dictionary<string, string>(); - private readonly TypeNameRegistry typeNameRegistry; + private readonly Dictionary<string, string> texts = new Dictionary<string, string>(); + private readonly TypeNameRegistry typeNameRegistry; - public IReadOnlyDictionary<string, string> Texts - { - get => texts; - } + public IReadOnlyDictionary<string, string> Texts + { + get => texts; + } - protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) - { - Guard.NotNull(typeNameRegistry); + protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(typeNameRegistry); - this.typeNameRegistry = typeNameRegistry; - } + this.typeNameRegistry = typeNameRegistry; + } - protected void AddEventMessage<TEvent>(string message) where TEvent : IEvent - { - Guard.NotNullOrEmpty(message); + protected void AddEventMessage<TEvent>(string message) where TEvent : IEvent + { + Guard.NotNullOrEmpty(message); - texts[typeNameRegistry.GetName<TEvent>()] = message; - } + texts[typeNameRegistry.GetName<TEvent>()] = message; + } - protected bool HasEventText(IEvent @event) - { - var message = typeNameRegistry.GetName(@event.GetType()); + protected bool HasEventText(IEvent @event) + { + var message = typeNameRegistry.GetName(@event.GetType()); - return texts.ContainsKey(message); - } + return texts.ContainsKey(message); + } - protected HistoryEvent ForEvent(IEvent @event, string channel) - { - var message = typeNameRegistry.GetName(@event.GetType()); + protected HistoryEvent ForEvent(IEvent @event, string channel) + { + var message = typeNameRegistry.GetName(@event.GetType()); - return new HistoryEvent(channel, message); - } + return new HistoryEvent(channel, message); + } - public Task<HistoryEvent?> CreateEventAsync(Envelope<IEvent> @event) + public Task<HistoryEvent?> CreateEventAsync(Envelope<IEvent> @event) + { + if (HasEventText(@event.Payload)) { - if (HasEventText(@event.Payload)) - { - return CreateEventCoreAsync(@event); - } - - return Task.FromResult<HistoryEvent?>(null); + return CreateEventCoreAsync(@event); } - protected abstract Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event); + return Task.FromResult<HistoryEvent?>(null); } + + protected abstract Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs index 09fa7fae55..0b54e63f6b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -11,118 +11,117 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.History +namespace Squidex.Domain.Apps.Entities.History; + +public sealed class HistoryService : IHistoryService, IEventConsumer { - public sealed class HistoryService : IHistoryService, IEventConsumer + private readonly Dictionary<string, string> texts = new Dictionary<string, string>(); + private readonly List<IHistoryEventsCreator> creators; + private readonly IHistoryEventRepository repository; + private readonly NotifoService notifo; + + public int BatchSize { - private readonly Dictionary<string, string> texts = new Dictionary<string, string>(); - private readonly List<IHistoryEventsCreator> creators; - private readonly IHistoryEventRepository repository; - private readonly NotifoService notifo; + get => 1000; + } - public int BatchSize - { - get => 1000; - } + public int BatchDelay + { + get => 1000; + } - public int BatchDelay - { - get => 1000; - } + public string Name + { + get => GetType().Name; + } - public string Name - { - get => GetType().Name; - } + public HistoryService(IHistoryEventRepository repository, IEnumerable<IHistoryEventsCreator> creators, + NotifoService notifo) + { + this.creators = creators.ToList(); + this.repository = repository; + this.notifo = notifo; - public HistoryService(IHistoryEventRepository repository, IEnumerable<IHistoryEventsCreator> creators, - NotifoService notifo) + foreach (var creator in this.creators) { - this.creators = creators.ToList(); - this.repository = repository; - this.notifo = notifo; - - foreach (var creator in this.creators) + foreach (var (key, value) in creator.Texts) { - foreach (var (key, value) in creator.Texts) - { - texts[key] = value; - } + texts[key] = value; } } + } - public Task ClearAsync() - { - return repository.ClearAsync(); - } + public Task ClearAsync() + { + return repository.ClearAsync(); + } - public async Task On(IEnumerable<Envelope<IEvent>> events) - { - var targets = new List<(Envelope<IEvent> Event, HistoryEvent? HistoryEvent)>(); + public async Task On(IEnumerable<Envelope<IEvent>> events) + { + var targets = new List<(Envelope<IEvent> Event, HistoryEvent? HistoryEvent)>(); - foreach (var @event in events) + foreach (var @event in events) + { + switch (@event.Payload) { - switch (@event.Payload) - { - case AppEvent appEvent: - { - var historyEvent = await CreateEvent(appEvent.AppId.Id, appEvent.Actor, @event); + case AppEvent appEvent: + { + var historyEvent = await CreateEvent(appEvent.AppId.Id, appEvent.Actor, @event); - if (historyEvent != null) - { - targets.Add((@event, historyEvent)); - } - - break; + if (historyEvent != null) + { + targets.Add((@event, historyEvent)); } - case TeamEvent teamEvent: - { - var historyEvent = await CreateEvent(teamEvent.TeamId, teamEvent.Actor, @event); + break; + } - if (historyEvent != null) - { - targets.Add((@event, historyEvent)); - } + case TeamEvent teamEvent: + { + var historyEvent = await CreateEvent(teamEvent.TeamId, teamEvent.Actor, @event); - break; + if (historyEvent != null) + { + targets.Add((@event, historyEvent)); } - } - } - if (targets.Count > 0) - { - await notifo.HandleEventsAsync(targets); - - await repository.InsertManyAsync(targets.NotNull(x => x.HistoryEvent)); + break; + } } } - private async Task<HistoryEvent?> CreateEvent(DomainId ownerId, RefToken actor, Envelope<IEvent> @event) + if (targets.Count > 0) { - foreach (var creator in creators) - { - var historyEvent = await creator.CreateEventAsync(@event); - - if (historyEvent != null) - { - historyEvent.Actor = actor; - historyEvent.OwnerId = ownerId; - historyEvent.Created = @event.Headers.Timestamp(); - historyEvent.Version = @event.Headers.EventStreamNumber(); - return historyEvent; - } - } + await notifo.HandleEventsAsync(targets); - return null; + await repository.InsertManyAsync(targets.NotNull(x => x.HistoryEvent)); } + } - public async Task<IReadOnlyList<ParsedHistoryEvent>> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, - CancellationToken ct = default) + private async Task<HistoryEvent?> CreateEvent(DomainId ownerId, RefToken actor, Envelope<IEvent> @event) + { + foreach (var creator in creators) { - var items = await repository.QueryByChannelAsync(ownerId, channelPrefix, count, ct); + var historyEvent = await creator.CreateEventAsync(@event); - return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); + if (historyEvent != null) + { + historyEvent.Actor = actor; + historyEvent.OwnerId = ownerId; + historyEvent.Created = @event.Headers.Timestamp(); + historyEvent.Version = @event.Headers.EventStreamNumber(); + return historyEvent; + } } + + return null; + } + + public async Task<IReadOnlyList<ParsedHistoryEvent>> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, + CancellationToken ct = default) + { + var items = await repository.QueryByChannelAsync(ownerId, channelPrefix, count, ct); + + return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs index 9f6b0a86de..17a84be5fc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs @@ -7,12 +7,11 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.History +namespace Squidex.Domain.Apps.Entities.History; + +public interface IHistoryEventsCreator { - public interface IHistoryEventsCreator - { - IReadOnlyDictionary<string, string> Texts { get; } + IReadOnlyDictionary<string, string> Texts { get; } - Task<HistoryEvent?> CreateEventAsync(Envelope<IEvent> @event); - } + Task<HistoryEvent?> CreateEventAsync(Envelope<IEvent> @event); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs index 48f90ef675..81be7cb833 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.History +namespace Squidex.Domain.Apps.Entities.History; + +public interface IHistoryService { - public interface IHistoryService - { - Task<IReadOnlyList<ParsedHistoryEvent>> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, - CancellationToken ct = default); - } + Task<IReadOnlyList<ParsedHistoryEvent>> QueryByChannelAsync(DomainId ownerId, string channelPrefix, int count, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoOptions.cs index fb6e7b82be..83d0f0dd60 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoOptions.cs @@ -5,24 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.History +namespace Squidex.Domain.Apps.Entities.History; + +public sealed class NotifoOptions { - public sealed class NotifoOptions - { - public string AppId { get; set; } + public string AppId { get; set; } - public string ApiKey { get; set; } + public string ApiKey { get; set; } - public string ApiUrl { get; set; } = "https://app.notifo.io"; + public string ApiUrl { get; set; } = "https://app.notifo.io"; - public bool Debug { get; set; } + public bool Debug { get; set; } - public bool IsConfigured() - { - return - !string.IsNullOrWhiteSpace(ApiKey) && - !string.IsNullOrWhiteSpace(ApiUrl) && - !string.IsNullOrWhiteSpace(AppId); - } + public bool IsConfigured() + { + return + !string.IsNullOrWhiteSpace(ApiKey) && + !string.IsNullOrWhiteSpace(ApiUrl) && + !string.IsNullOrWhiteSpace(AppId); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs index d69bb7c3f3..7bbac20658 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs @@ -23,331 +23,330 @@ #pragma warning disable MA0073 // Avoid comparison with bool constant -namespace Squidex.Domain.Apps.Entities.History +namespace Squidex.Domain.Apps.Entities.History; + +public class NotifoService : IUserEvents { - public class NotifoService : IUserEvents + private static readonly Duration MaxAge = Duration.FromHours(12); + private readonly NotifoOptions options; + private readonly IUrlGenerator urlGenerator; + private readonly IUserResolver userResolver; + private readonly ILogger<NotifoService> log; + private readonly INotifoClient? client; + + public IClock Clock { get; set; } = SystemClock.Instance; + + public NotifoService(IOptions<NotifoOptions> options, + IUrlGenerator urlGenerator, + IUserResolver userResolver, + ILogger<NotifoService> log) { - private static readonly Duration MaxAge = Duration.FromHours(12); - private readonly NotifoOptions options; - private readonly IUrlGenerator urlGenerator; - private readonly IUserResolver userResolver; - private readonly ILogger<NotifoService> log; - private readonly INotifoClient? client; - - public IClock Clock { get; set; } = SystemClock.Instance; - - public NotifoService(IOptions<NotifoOptions> options, - IUrlGenerator urlGenerator, - IUserResolver userResolver, - ILogger<NotifoService> log) + this.options = options.Value; + this.urlGenerator = urlGenerator; + this.userResolver = userResolver; + this.log = log; + + if (options.Value.IsConfigured()) { - this.options = options.Value; - this.urlGenerator = urlGenerator; - this.userResolver = userResolver; - this.log = log; + var builder = + NotifoClientBuilder.Create() + .SetApiKey(options.Value.ApiKey); - if (options.Value.IsConfigured()) + if (!string.IsNullOrWhiteSpace(options.Value.ApiUrl)) { - var builder = - NotifoClientBuilder.Create() - .SetApiKey(options.Value.ApiKey); - - if (!string.IsNullOrWhiteSpace(options.Value.ApiUrl)) - { - builder = builder.SetApiUrl(options.Value.ApiUrl); - } - - if (options.Value.Debug) - { - builder = builder.ReadResponseAsString(true); - } + builder = builder.SetApiUrl(options.Value.ApiUrl); + } - client = builder.Build(); + if (options.Value.Debug) + { + builder = builder.ReadResponseAsString(true); } + + client = builder.Build(); } + } - public async Task OnUserCreatedAsync(IUser user) + public async Task OnUserCreatedAsync(IUser user) + { + if (!string.IsNullOrWhiteSpace(user.Email)) { - if (!string.IsNullOrWhiteSpace(user.Email)) - { - await UpsertUserAsync(user); - } + await UpsertUserAsync(user); } + } - public async Task OnUserUpdatedAsync(IUser user, IUser previous) + public async Task OnUserUpdatedAsync(IUser user, IUser previous) + { + if (!string.Equals(user.Email, previous?.Email, StringComparison.OrdinalIgnoreCase)) { - if (!string.Equals(user.Email, previous?.Email, StringComparison.OrdinalIgnoreCase)) - { - await UpsertUserAsync(user); - } + await UpsertUserAsync(user); } + } - private async Task UpsertUserAsync(IUser user) + private async Task UpsertUserAsync(IUser user) + { + if (client == null) { - if (client == null) - { - return; - } + return; + } - try + try + { + var settings = new Dictionary<string, ChannelSettingDto> { - var settings = new Dictionary<string, ChannelSettingDto> + [Providers.WebPush] = new ChannelSettingDto { - [Providers.WebPush] = new ChannelSettingDto - { - Send = ChannelSend.Send, - DelayInSeconds = null - }, - - [Providers.Email] = new ChannelSettingDto - { - Send = ChannelSend.Send, - DelayInSeconds = 5 * 60 - } - }; - - var userRequest = new UpsertUserDto - { - Id = user.Id, - FullName = user.Claims.DisplayName(), - PreferredLanguage = "en", - PreferredTimezone = null, - Settings = settings - }; + Send = ChannelSend.Send, + DelayInSeconds = null + }, - if (user.Email.IsEmail()) + [Providers.Email] = new ChannelSettingDto { - userRequest.EmailAddress = user.Email; + Send = ChannelSend.Send, + DelayInSeconds = 5 * 60 } + }; - var response = await client.Users.PostUsersAsync(options.AppId, new UpsertUsersDto - { - Requests = new List<UpsertUserDto> - { - userRequest - } - }); - - var apiKey = response.First().ApiKey; - - await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.NotifoKey, response.First().ApiKey, true); - } - catch (NotifoException ex) + var userRequest = new UpsertUserDto { - log.LogError(ex, "Failed to register user in notifo: {details}.", ex.ToString()); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to register user in notifo."); - } - } - - public async Task HandleEventsAsync(IEnumerable<(Envelope<IEvent> AppEvent, HistoryEvent? HistoryEvent)> events) - { - Guard.NotNull(events); + Id = user.Id, + FullName = user.Claims.DisplayName(), + PreferredLanguage = "en", + PreferredTimezone = null, + Settings = settings + }; - if (client == null) + if (user.Email.IsEmail()) { - return; + userRequest.EmailAddress = user.Email; } - try + var response = await client.Users.PostUsersAsync(options.AppId, new UpsertUsersDto { - var now = Clock.GetCurrentInstant(); - - var maxAge = now - MaxAge; + Requests = new List<UpsertUserDto> + { + userRequest + } + }); - var batches = events - .Where(x => x.AppEvent.Headers.Restored() == false) - .Where(x => x.AppEvent.Headers.Timestamp() > maxAge) - .SelectMany(x => CreateRequests(x.AppEvent, x.HistoryEvent)) - .Batch(50); + var apiKey = response.First().ApiKey; - foreach (var batch in batches) - { - var request = new PublishManyDto - { - Requests = batch.ToList() - }; + await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.NotifoKey, response.First().ApiKey, true); + } + catch (NotifoException ex) + { + log.LogError(ex, "Failed to register user in notifo: {details}.", ex.ToString()); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to register user in notifo."); + } + } - await client.Events.PostEventsAsync(options.AppId, request); - } + public async Task HandleEventsAsync(IEnumerable<(Envelope<IEvent> AppEvent, HistoryEvent? HistoryEvent)> events) + { + Guard.NotNull(events); - foreach (var @event in events) - { - switch (@event.AppEvent.Payload) - { - case AppContributorAssigned assigned: - await AssignContributorAsync(client, assigned.ContributorId, GetAppPrefix(assigned)); - break; - - case AppContributorRemoved removed: - await RemoveContributorAsync(client, removed.ContributorId, GetAppPrefix(removed)); - break; - - case TeamContributorAssigned assigned: - await AssignContributorAsync(client, assigned.ContributorId, GetTeamPrefix(assigned)); - break; - - case TeamContributorRemoved removed: - await RemoveContributorAsync(client, removed.ContributorId, GetTeamPrefix(removed)); - break; - } - } - } - catch (NotifoException ex) - { - log.LogError(ex, "Failed to push user to notifo: {details}.", ex.ToString()); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to push user to notifo."); - } + if (client == null) + { + return; } - private async Task AssignContributorAsync(INotifoClient actualClient, string userId, string prefix) + try { - var user = await userResolver.FindByIdAsync(userId); + var now = Clock.GetCurrentInstant(); - if (user != null) - { - await UpsertUserAsync(user); - } + var maxAge = now - MaxAge; + + var batches = events + .Where(x => x.AppEvent.Headers.Restored() == false) + .Where(x => x.AppEvent.Headers.Timestamp() > maxAge) + .SelectMany(x => CreateRequests(x.AppEvent, x.HistoryEvent)) + .Batch(50); - try + foreach (var batch in batches) { - var request = new AddAllowedTopicDto + var request = new PublishManyDto { - Prefix = prefix + Requests = batch.ToList() }; - await actualClient.Users.PostAllowedTopicAsync(options.AppId, userId, request); + await client.Events.PostEventsAsync(options.AppId, request); } - catch (NotifoException ex) when (ex.StatusCode != 404) + + foreach (var @event in events) { - throw; + switch (@event.AppEvent.Payload) + { + case AppContributorAssigned assigned: + await AssignContributorAsync(client, assigned.ContributorId, GetAppPrefix(assigned)); + break; + + case AppContributorRemoved removed: + await RemoveContributorAsync(client, removed.ContributorId, GetAppPrefix(removed)); + break; + + case TeamContributorAssigned assigned: + await AssignContributorAsync(client, assigned.ContributorId, GetTeamPrefix(assigned)); + break; + + case TeamContributorRemoved removed: + await RemoveContributorAsync(client, removed.ContributorId, GetTeamPrefix(removed)); + break; + } } } - - private async Task RemoveContributorAsync(INotifoClient actualClient, string userId, string prefix) + catch (NotifoException ex) { - try - { - await actualClient.Users.DeleteAllowedTopicAsync(options.ApiKey, userId, prefix); - } - catch (NotifoException ex) when (ex.StatusCode != 404) - { - throw; - } + log.LogError(ex, "Failed to push user to notifo: {details}.", ex.ToString()); } + catch (Exception ex) + { + log.LogError(ex, "Failed to push user to notifo."); + } + } - private IEnumerable<PublishDto> CreateRequests(Envelope<IEvent> @event, HistoryEvent? historyEvent) + private async Task AssignContributorAsync(INotifoClient actualClient, string userId, string prefix) + { + var user = await userResolver.FindByIdAsync(userId); + + if (user != null) { - if (@event.Payload is CommentCreated { Mentions.Length: > 0 } comment) - { - foreach (var userId in comment.Mentions) - { - yield return CreateMentionRequest(comment, userId); - } - } - else if (historyEvent != null && @event.Payload is AppEvent appEvent) - { - yield return CreateHistoryRequest(historyEvent, appEvent); - } + await UpsertUserAsync(user); } - private PublishDto CreateHistoryRequest(HistoryEvent historyEvent, IEvent payload) + try { - var publishRequest = new PublishDto + var request = new AddAllowedTopicDto { - Properties = new NotificationProperties() + Prefix = prefix }; - foreach (var (key, value) in historyEvent.Parameters) - { - publishRequest.Properties.Add(key, value); - } + await actualClient.Users.PostAllowedTopicAsync(options.AppId, userId, request); + } + catch (NotifoException ex) when (ex.StatusCode != 404) + { + throw; + } + } - if (payload is AppEvent appEvent) - { - publishRequest.Properties["SquidexApp"] = appEvent.AppId.Name; - } + private async Task RemoveContributorAsync(INotifoClient actualClient, string userId, string prefix) + { + try + { + await actualClient.Users.DeleteAllowedTopicAsync(options.ApiKey, userId, prefix); + } + catch (NotifoException ex) when (ex.StatusCode != 404) + { + throw; + } + } - if (payload is SquidexEvent squidexEvent) + private IEnumerable<PublishDto> CreateRequests(Envelope<IEvent> @event, HistoryEvent? historyEvent) + { + if (@event.Payload is CommentCreated { Mentions.Length: > 0 } comment) + { + foreach (var userId in comment.Mentions) { - SetUser(squidexEvent, publishRequest); + yield return CreateMentionRequest(comment, userId); } + } + else if (historyEvent != null && @event.Payload is AppEvent appEvent) + { + yield return CreateHistoryRequest(historyEvent, appEvent); + } + } - if (payload is AppEvent appEvent2) - { - publishRequest.Topic = BuildTopic(GetAppPrefix(appEvent2), historyEvent); - } + private PublishDto CreateHistoryRequest(HistoryEvent historyEvent, IEvent payload) + { + var publishRequest = new PublishDto + { + Properties = new NotificationProperties() + }; - if (payload is TeamEvent teamEvent) - { - publishRequest.Topic = BuildTopic(GetTeamPrefix(teamEvent), historyEvent); - } + foreach (var (key, value) in historyEvent.Parameters) + { + publishRequest.Properties.Add(key, value); + } - if (payload is ContentEvent @event and not ContentDeleted) - { - var url = urlGenerator.ContentUI(@event.AppId, @event.SchemaId, @event.ContentId); + if (payload is AppEvent appEvent) + { + publishRequest.Properties["SquidexApp"] = appEvent.AppId.Name; + } - publishRequest.Properties["SquidexUrl"] = url; - } + if (payload is SquidexEvent squidexEvent) + { + SetUser(squidexEvent, publishRequest); + } - publishRequest.TemplateCode = historyEvent.EventType; + if (payload is AppEvent appEvent2) + { + publishRequest.Topic = BuildTopic(GetAppPrefix(appEvent2), historyEvent); + } - return publishRequest; + if (payload is TeamEvent teamEvent) + { + publishRequest.Topic = BuildTopic(GetTeamPrefix(teamEvent), historyEvent); } - private static PublishDto CreateMentionRequest(CommentCreated comment, string userId) + if (payload is ContentEvent @event and not ContentDeleted) { - var publishRequest = new PublishDto - { - Topic = $"users/{userId}" - }; + var url = urlGenerator.ContentUI(@event.AppId, @event.SchemaId, @event.ContentId); - publishRequest.Properties["SquidexApp"] = comment.AppId.Name; + publishRequest.Properties["SquidexUrl"] = url; + } - publishRequest.Preformatted = new NotificationFormattingDto - { - Subject = - { - ["en"] = comment.Text - } - }; + publishRequest.TemplateCode = historyEvent.EventType; - if (comment.Url?.IsAbsoluteUri == true) - { - publishRequest.Preformatted.LinkUrl["en"] = comment.Url.ToString(); - } + return publishRequest; + } - SetUser(comment, publishRequest); + private static PublishDto CreateMentionRequest(CommentCreated comment, string userId) + { + var publishRequest = new PublishDto + { + Topic = $"users/{userId}" + }; - return publishRequest; - } + publishRequest.Properties["SquidexApp"] = comment.AppId.Name; - private static void SetUser(SquidexEvent @event, PublishDto publishRequest) + publishRequest.Preformatted = new NotificationFormattingDto { - if (@event.Actor.IsUser) + Subject = { - publishRequest.CreatorId = @event.Actor.Identifier; + ["en"] = comment.Text } - } + }; - private static string BuildTopic(string prefix, HistoryEvent @event) + if (comment.Url?.IsAbsoluteUri == true) { - return $"{prefix}/{@event.Channel.Replace('.', '/').Trim()}"; + publishRequest.Preformatted.LinkUrl["en"] = comment.Url.ToString(); } - private static string GetAppPrefix(AppEvent appEvent) - { - return $"apps/{appEvent.AppId.Id}"; - } + SetUser(comment, publishRequest); + + return publishRequest; + } - private static string GetTeamPrefix(TeamEvent teamEvent) + private static void SetUser(SquidexEvent @event, PublishDto publishRequest) + { + if (@event.Actor.IsUser) { - return $"apps/{teamEvent.TeamId}"; + publishRequest.CreatorId = @event.Actor.Identifier; } } + + private static string BuildTopic(string prefix, HistoryEvent @event) + { + return $"{prefix}/{@event.Channel.Replace('.', '/').Trim()}"; + } + + private static string GetAppPrefix(AppEvent appEvent) + { + return $"apps/{appEvent.AppId.Id}"; + } + + private static string GetTeamPrefix(TeamEvent teamEvent) + { + return $"apps/{teamEvent.TeamId}"; + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs index 8b57ab2887..8ecfe0dd7d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs @@ -9,68 +9,67 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.History +namespace Squidex.Domain.Apps.Entities.History; + +public sealed class ParsedHistoryEvent { - public sealed class ParsedHistoryEvent + private readonly HistoryEvent item; + private readonly Lazy<string?> message; + + public DomainId Id { - private readonly HistoryEvent item; - private readonly Lazy<string?> message; + get => item.Id; + } - public DomainId Id - { - get => item.Id; - } + public Instant Created + { + get => item.Created; + } - public Instant Created - { - get => item.Created; - } + public RefToken Actor + { + get => item.Actor; + } - public RefToken Actor - { - get => item.Actor; - } + public long Version + { + get => item.Version; + } - public long Version - { - get => item.Version; - } + public string Channel + { + get => item.Channel; + } - public string Channel - { - get => item.Channel; - } + public string EventType + { + get => item.EventType; + } - public string EventType - { - get => item.EventType; - } + public string? Message + { + get => message.Value; + } - public string? Message - { - get => message.Value; - } + public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary<string, string> texts) + { + this.item = item; - public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary<string, string> texts) + message = new Lazy<string?>(() => { - this.item = item; - - message = new Lazy<string?>(() => + if (texts.TryGetValue(item.EventType, out var translationKey)) { - if (texts.TryGetValue(item.EventType, out var translationKey)) - { - var result = T.Get(translationKey); + var result = T.Get(translationKey); - foreach (var (key, value) in item.Parameters) - { - result = result.Replace("[" + key + "]", value, StringComparison.Ordinal); - } - - return result; + foreach (var (key, value) in item.Parameters) + { + result = result.Replace("[" + key + "]", value, StringComparison.Ordinal); } - return null; - }); - } + return result; + } + + return null; + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs index 9819e4865c..93b32f606c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.History.Repositories +namespace Squidex.Domain.Apps.Entities.History.Repositories; + +public interface IHistoryEventRepository { - public interface IHistoryEventRepository - { - Task<IReadOnlyList<HistoryEvent>> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, - CancellationToken ct = default); + Task<IReadOnlyList<HistoryEvent>> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, + CancellationToken ct = default); - Task InsertManyAsync(IEnumerable<HistoryEvent> historyEvents, - CancellationToken ct = default); + Task InsertManyAsync(IEnumerable<HistoryEvent> historyEvents, + CancellationToken ct = default); - Task ClearAsync( - CancellationToken ct = default); - } + Task ClearAsync( + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IAppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppCommand.cs index fdb2e14599..410072d616 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IAppCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IAppCommand.cs @@ -8,10 +8,9 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IAppCommand : ICommand { - public interface IAppCommand : ICommand - { - NamedId<DomainId> AppId { get; set; } - } + NamedId<DomainId> AppId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs index db6b2283a8..52d07c161d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -12,44 +12,43 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IAppProvider { - public interface IAppProvider - { - Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false, - CancellationToken ct = default); + Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false, + CancellationToken ct = default); - Task<ITeamEntity?> GetTeamAsync(DomainId teamId, - CancellationToken ct = default); + Task<ITeamEntity?> GetTeamAsync(DomainId teamId, + CancellationToken ct = default); - Task<List<ITeamEntity>> GetUserTeamsAsync(string userId, - CancellationToken ct = default); + Task<List<ITeamEntity>> GetUserTeamsAsync(string userId, + CancellationToken ct = default); - Task<IAppEntity?> GetAppAsync(DomainId appId, bool canCache = false, - CancellationToken ct = default); + Task<IAppEntity?> GetAppAsync(DomainId appId, bool canCache = false, + CancellationToken ct = default); - Task<IAppEntity?> GetAppAsync(string appName, bool canCache = false, - CancellationToken ct = default); + Task<IAppEntity?> GetAppAsync(string appName, bool canCache = false, + CancellationToken ct = default); - Task<List<IAppEntity>> GetUserAppsAsync(string userId, PermissionSet permissions, - CancellationToken ct = default); + Task<List<IAppEntity>> GetUserAppsAsync(string userId, PermissionSet permissions, + CancellationToken ct = default); - Task<List<IAppEntity>> GetTeamAppsAsync(DomainId teamId, - CancellationToken ct = default); + Task<List<IAppEntity>> GetTeamAppsAsync(DomainId teamId, + CancellationToken ct = default); - Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false, - CancellationToken ct = default); + Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false, + CancellationToken ct = default); - Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, string name, bool canCache = false, - CancellationToken ct = default); + Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, string name, bool canCache = false, + CancellationToken ct = default); - Task<List<ISchemaEntity>> GetSchemasAsync(DomainId appId, - CancellationToken ct = default); + Task<List<ISchemaEntity>> GetSchemasAsync(DomainId appId, + CancellationToken ct = default); - Task<List<IRuleEntity>> GetRulesAsync(DomainId appId, - CancellationToken ct = default); + Task<List<IRuleEntity>> GetRulesAsync(DomainId appId, + CancellationToken ct = default); - Task<IRuleEntity?> GetRuleAsync(DomainId appId, DomainId id, - CancellationToken ct = default); - } + Task<IRuleEntity?> GetRuleAsync(DomainId appId, DomainId id, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IContextProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IContextProvider.cs index e6e2c67cfb..12a6b23c1a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IContextProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IContextProvider.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IContextProvider { - public interface IContextProvider - { - Context Context { get; } - } + Context Context { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/IDeleter.cs index ad912860e5..f4cd2ac2f5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IDeleter.cs @@ -8,19 +8,18 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IDeleter { - public interface IDeleter - { - int Order => 0; + int Order => 0; - Task DeleteAppAsync(IAppEntity app, - CancellationToken ct); + Task DeleteAppAsync(IAppEntity app, + CancellationToken ct); - Task DeleteContributorAsync(DomainId appId, string contributorId, - CancellationToken ct) - { - return Task.CompletedTask; - } + Task DeleteContributorAsync(DomainId appId, string contributorId, + CancellationToken ct) + { + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs index 62d7de0ec1..fbbdde95a2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs @@ -8,14 +8,13 @@ using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IEntity : IWithId<DomainId> { - public interface IEntity : IWithId<DomainId> - { - Instant Created { get; } + Instant Created { get; } - Instant LastModified { get; } + Instant LastModified { get; } - DomainId UniqueId { get; } - } + DomainId UniqueId { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs index b54b9dedd9..b246de09f6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithCreatedBy.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IEntityWithCreatedBy { - public interface IEntityWithCreatedBy - { - RefToken CreatedBy { get; } - } + RefToken CreatedBy { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs index e32ca46bfb..31b0c5b9f9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithLastModifiedBy.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IEntityWithLastModifiedBy { - public interface IEntityWithLastModifiedBy - { - RefToken LastModifiedBy { get; set; } - } + RefToken LastModifiedBy { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs index 7f89a375a2..db596274cf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IEntityWithTags { - public interface IEntityWithTags - { - HashSet<string> Tags { get; } - } + HashSet<string> Tags { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs index 167115ce60..2ac3b0c5e0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IEntityWithVersion.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface IEntityWithVersion { - public interface IEntityWithVersion - { - long Version { get; } - } + long Version { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs index 41ddd2610f..04586afb1e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/ISchemaCommand.cs @@ -8,10 +8,9 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface ISchemaCommand : ICommand { - public interface ISchemaCommand : ICommand - { - NamedId<DomainId> SchemaId { get; set; } - } + NamedId<DomainId> SchemaId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/ITeamCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/ITeamCommand.cs index 07cfdf6723..8520183f68 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/ITeamCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/ITeamCommand.cs @@ -8,10 +8,9 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public interface ITeamCommand : ICommand { - public interface ITeamCommand : ICommand - { - DomainId TeamId { get; set; } - } + DomainId TeamId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs index 215c150dd2..faaa7b4064 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs @@ -14,128 +14,127 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Invitation +namespace Squidex.Domain.Apps.Entities.Invitation; + +public sealed class InvitationEventConsumer : IEventConsumer { - public sealed class InvitationEventConsumer : IEventConsumer + private static readonly Duration MaxAge = Duration.FromDays(2); + private readonly IUserNotifications userNotifications; + private readonly IUserResolver userResolver; + private readonly IAppProvider appProvider; + private readonly ILogger<InvitationEventConsumer> log; + + public string Name { - private static readonly Duration MaxAge = Duration.FromDays(2); - private readonly IUserNotifications userNotifications; - private readonly IUserResolver userResolver; - private readonly IAppProvider appProvider; - private readonly ILogger<InvitationEventConsumer> log; + get => "NotificationEmailSender"; + } - public string Name - { - get => "NotificationEmailSender"; - } + public string EventsFilter + { + get { return "^app-|^app-"; } + } - public string EventsFilter - { - get { return "^app-|^app-"; } - } + public InvitationEventConsumer( + IAppProvider appProvider, + IUserNotifications userNotifications, + IUserResolver userResolver, + ILogger<InvitationEventConsumer> log) + { + this.appProvider = appProvider; + this.userNotifications = userNotifications; + this.userResolver = userResolver; + this.log = log; + } - public InvitationEventConsumer( - IAppProvider appProvider, - IUserNotifications userNotifications, - IUserResolver userResolver, - ILogger<InvitationEventConsumer> log) + public async Task On(Envelope<IEvent> @event) + { + if (!userNotifications.IsActive) { - this.appProvider = appProvider; - this.userNotifications = userNotifications; - this.userResolver = userResolver; - this.log = log; + return; } - public async Task On(Envelope<IEvent> @event) + if (@event.Headers.EventStreamNumber() <= 1) { - if (!userNotifications.IsActive) - { - return; - } + return; + } - if (@event.Headers.EventStreamNumber() <= 1) - { - return; - } + var now = SystemClock.Instance.GetCurrentInstant(); - var now = SystemClock.Instance.GetCurrentInstant(); + var timestamp = @event.Headers.Timestamp(); - var timestamp = @event.Headers.Timestamp(); + if (now - timestamp > MaxAge) + { + return; + } - if (now - timestamp > MaxAge) - { - return; - } + switch (@event.Payload) + { + case AppContributorAssigned assigned when assigned.IsAdded: + { + var (assigner, assignee) = await ResolveUsersAsync(assigned.Actor, assigned.ContributorId, default); - switch (@event.Payload) - { - case AppContributorAssigned assigned when assigned.IsAdded: + if (assigner == null || assignee == null) { - var (assigner, assignee) = await ResolveUsersAsync(assigned.Actor, assigned.ContributorId, default); - - if (assigner == null || assignee == null) - { - return; - } - - var app = await appProvider.GetAppAsync(assigned.AppId.Id, true); + return; + } - if (app == null) - { - return; - } + var app = await appProvider.GetAppAsync(assigned.AppId.Id, true); - await userNotifications.SendInviteAsync(assigner, assignee, app); + if (app == null) + { return; } - case TeamContributorAssigned assigned when assigned.IsAdded: - { - var (assigner, assignee) = await ResolveUsersAsync(assigned.Actor, assigned.ContributorId, default); + await userNotifications.SendInviteAsync(assigner, assignee, app); + return; + } - if (assigner == null || assignee == null) - { - return; - } + case TeamContributorAssigned assigned when assigned.IsAdded: + { + var (assigner, assignee) = await ResolveUsersAsync(assigned.Actor, assigned.ContributorId, default); - var team = await appProvider.GetTeamAsync(assigned.TeamId); + if (assigner == null || assignee == null) + { + return; + } - if (team == null) - { - return; - } + var team = await appProvider.GetTeamAsync(assigned.TeamId); - await userNotifications.SendInviteAsync(assigner, assignee, team); - break; + if (team == null) + { + return; } - } + + await userNotifications.SendInviteAsync(assigner, assignee, team); + break; + } } + } - private async Task<(IUser? Assignee, IUser? Assigner)> ResolveUsersAsync(RefToken assignerId, string assigneeId, - CancellationToken ct) + private async Task<(IUser? Assignee, IUser? Assigner)> ResolveUsersAsync(RefToken assignerId, string assigneeId, + CancellationToken ct) + { + if (!assignerId.IsUser) { - if (!assignerId.IsUser) - { - return default; - } - - var assigner = await userResolver.FindByIdAsync(assignerId.Identifier, ct); + return default; + } - if (assigner == null) - { - log.LogWarning("Failed to invite user: Assigner {assignerId} not found.", assignerId); - return default; - } + var assigner = await userResolver.FindByIdAsync(assignerId.Identifier, ct); - var assignee = await userResolver.FindByIdAsync(assigneeId, ct); + if (assigner == null) + { + log.LogWarning("Failed to invite user: Assigner {assignerId} not found.", assignerId); + return default; + } - if (assignee == null) - { - log.LogWarning("Failed to invite user: Assignee {assigneeId} not found.", assigneeId); - return default; - } + var assignee = await userResolver.FindByIdAsync(assigneeId, ct); - return (assigner, assignee); + if (assignee == null) + { + log.LogWarning("Failed to invite user: Assignee {assigneeId} not found.", assigneeId); + return default; } + + return (assigner, assignee); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InviteUserCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InviteUserCommandMiddleware.cs index 8d4065a2bd..7a0afcf026 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InviteUserCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InviteUserCommandMiddleware.cs @@ -13,78 +13,77 @@ using AssignAppContributor = Squidex.Domain.Apps.Entities.Apps.Commands.AssignContributor; using AssignTeamContributor = Squidex.Domain.Apps.Entities.Teams.Commands.AssignContributor; -namespace Squidex.Domain.Apps.Entities.Invitation +namespace Squidex.Domain.Apps.Entities.Invitation; + +public sealed class InviteUserCommandMiddleware : ICommandMiddleware { - public sealed class InviteUserCommandMiddleware : ICommandMiddleware + private readonly IUserResolver userResolver; + + public InviteUserCommandMiddleware(IUserResolver userResolver) { - private readonly IUserResolver userResolver; + this.userResolver = userResolver; + } - public InviteUserCommandMiddleware(IUserResolver userResolver) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is AssignAppContributor assignAppContributor) { - this.userResolver = userResolver; - } + var (userId, created) = + await ResolveUserAsync( + assignAppContributor.ContributorId, + assignAppContributor.Invite, + ct); - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - if (context.Command is AssignAppContributor assignAppContributor) - { - var (userId, created) = - await ResolveUserAsync( - assignAppContributor.ContributorId, - assignAppContributor.Invite, - ct); + assignAppContributor.ContributorId = userId; - assignAppContributor.ContributorId = userId; + await next(context, ct); - await next(context, ct); - - if (created && context.PlainResult is IAppEntity app) - { - context.Complete(new InvitedResult<IAppEntity> { Entity = app }); - } - } - else if (context.Command is AssignTeamContributor assignTeamContributor) + if (created && context.PlainResult is IAppEntity app) { - var (userId, created) = - await ResolveUserAsync( - assignTeamContributor.ContributorId, - assignTeamContributor.Invite, - ct); + context.Complete(new InvitedResult<IAppEntity> { Entity = app }); + } + } + else if (context.Command is AssignTeamContributor assignTeamContributor) + { + var (userId, created) = + await ResolveUserAsync( + assignTeamContributor.ContributorId, + assignTeamContributor.Invite, + ct); - assignTeamContributor.ContributorId = userId; + assignTeamContributor.ContributorId = userId; - await next(context, ct); + await next(context, ct); - if (created && context.PlainResult is ITeamEntity team) - { - context.Complete(new InvitedResult<ITeamEntity> { Entity = team }); - } - } - else + if (created && context.PlainResult is ITeamEntity team) { - await next(context, ct); + context.Complete(new InvitedResult<ITeamEntity> { Entity = team }); } } + else + { + await next(context, ct); + } + } - private async Task<(string Id, bool)> ResolveUserAsync(string id, bool invite, - CancellationToken ct) + private async Task<(string Id, bool)> ResolveUserAsync(string id, bool invite, + CancellationToken ct) + { + if (!id.IsEmail()) { - if (!id.IsEmail()) - { - return (id, false); - } + return (id, false); + } - if (invite) - { - var (createdUser, created) = await userResolver.CreateUserIfNotExistsAsync(id, true, ct); + if (invite) + { + var (createdUser, created) = await userResolver.CreateUserIfNotExistsAsync(id, true, ct); - return (createdUser?.Id ?? id, created); - } + return (createdUser?.Id ?? id, created); + } - var user = await userResolver.FindByIdOrEmailAsync(id, ct); + var user = await userResolver.FindByIdOrEmailAsync(id, ct); - return (user?.Id ?? id, false); - } + return (user?.Id ?? id, false); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitedResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitedResult.cs index 120dc10b71..ab8a01cdb8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitedResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitedResult.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Invitation +namespace Squidex.Domain.Apps.Entities.Invitation; + +public sealed class InvitedResult<T> { - public sealed class InvitedResult<T> - { - public T Entity { get; set; } - } + public T Entity { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotificationOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotificationOptions.cs index 810b1b310e..084779b288 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotificationOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotificationOptions.cs @@ -5,28 +5,27 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Notifications +namespace Squidex.Domain.Apps.Entities.Notifications; + +public sealed class EmailUserNotificationOptions { - public sealed class EmailUserNotificationOptions - { - public string UsageSubject { get; set; } + public string UsageSubject { get; set; } - public string UsageBody { get; set; } + public string UsageBody { get; set; } - public string NewUserSubject { get; set; } + public string NewUserSubject { get; set; } - public string NewUserBody { get; set; } + public string NewUserBody { get; set; } - public string ExistingUserSubject { get; set; } + public string ExistingUserSubject { get; set; } - public string ExistingUserBody { get; set; } + public string ExistingUserBody { get; set; } - public string NewTeamUserSubject { get; set; } + public string NewTeamUserSubject { get; set; } - public string NewTeamUserBody { get; set; } + public string NewTeamUserBody { get; set; } - public string ExistingTeamUserSubject { get; set; } + public string ExistingTeamUserSubject { get; set; } - public string ExistingTeamUserBody { get; set; } - } + public string ExistingTeamUserBody { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs index b5a098803e..35ca3456ad 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs @@ -15,189 +15,188 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Notifications +namespace Squidex.Domain.Apps.Entities.Notifications; + +public sealed class EmailUserNotifications : IUserNotifications { - public sealed class EmailUserNotifications : IUserNotifications + private readonly IEmailSender emailSender; + private readonly IUrlGenerator urlGenerator; + private readonly ILogger<EmailUserNotifications> log; + private readonly EmailUserNotificationOptions texts; + + private sealed class TemplatesVars { - private readonly IEmailSender emailSender; - private readonly IUrlGenerator urlGenerator; - private readonly ILogger<EmailUserNotifications> log; - private readonly EmailUserNotificationOptions texts; + public IUser? User { get; set; } + + public IUser? Assigner { get; init; } + + public string? AppName { get; init; } + + public string? TeamName { get; init; } + + public long? ApiCalls { get; init; } + + public long? ApiCallsLimit { get; init; } - private sealed class TemplatesVars + public string URL { get; set; } + } + + public bool IsActive + { + get => true; + } + + public EmailUserNotifications( + IOptions<EmailUserNotificationOptions> texts, + IEmailSender emailSender, + IUrlGenerator urlGenerator, + ILogger<EmailUserNotifications> log) + { + this.texts = texts.Value; + this.emailSender = emailSender; + this.urlGenerator = urlGenerator; + + this.log = log; + } + + public Task SendUsageAsync(IUser user, IAppEntity app, long usage, long usageLimit, + CancellationToken ct = default) + { + Guard.NotNull(user); + Guard.NotNull(app); + + var vars = new TemplatesVars { - public IUser? User { get; set; } + ApiCalls = usage, + ApiCallsLimit = usageLimit, + AppName = app.DisplayName() + }; + + return SendEmailAsync("Usage", + texts.UsageSubject, + texts.UsageBody, + user, vars, ct); + } - public IUser? Assigner { get; init; } + public Task SendInviteAsync(IUser assigner, IUser user, IAppEntity app, + CancellationToken ct = default) + { + Guard.NotNull(assigner); + Guard.NotNull(user); + Guard.NotNull(app); + + var vars = new TemplatesVars { Assigner = assigner, AppName = app.DisplayName() }; - public string? AppName { get; init; } + if (user.Claims.HasConsent()) + { + return SendEmailAsync("ExistingUser", + texts.ExistingUserSubject, + texts.ExistingUserBody, + user, vars, ct); + } + else + { + return SendEmailAsync("NewUser", + texts.NewUserSubject, + texts.NewUserBody, + user, vars, ct); + } + } - public string? TeamName { get; init; } + public Task SendInviteAsync(IUser assigner, IUser user, ITeamEntity team, + CancellationToken ct = default) + { + Guard.NotNull(assigner); + Guard.NotNull(user); + Guard.NotNull(team); - public long? ApiCalls { get; init; } + var vars = new TemplatesVars { Assigner = assigner, TeamName = team.Name }; - public long? ApiCallsLimit { get; init; } + if (user.Claims.HasConsent()) + { + return SendEmailAsync("ExistingUser", + texts.ExistingTeamUserSubject, + texts.ExistingTeamUserBody, + user, vars, ct); + } + else + { + return SendEmailAsync("NewUser", + texts.NewTeamUserSubject, + texts.NewTeamUserBody, + user, vars, ct); + } + } - public string URL { get; set; } + private async Task SendEmailAsync(string template, string emailSubj, string emailBody, IUser user, TemplatesVars vars, + CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(emailBody)) + { + log.LogWarning("Cannot send email to {email}: No email subject configured for template {template}.", template, user.Email); + return; } - public bool IsActive + if (string.IsNullOrWhiteSpace(emailSubj)) { - get => true; + log.LogWarning("Cannot send email to {email}: No email body configured for template {template}.", template, user.Email); + return; } - public EmailUserNotifications( - IOptions<EmailUserNotificationOptions> texts, - IEmailSender emailSender, - IUrlGenerator urlGenerator, - ILogger<EmailUserNotifications> log) + vars.URL = urlGenerator.UI(); + + vars.User = user; + + emailSubj = Format(emailSubj, vars); + emailBody = Format(emailBody, vars); + + try { - this.texts = texts.Value; - this.emailSender = emailSender; - this.urlGenerator = urlGenerator; + await emailSender.SendAsync(user.Email, emailSubj, emailBody, ct); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to send notification to {email}.", user.Email); + throw; + } + } - this.log = log; + private static string Format(string text, TemplatesVars vars) + { + if (!string.IsNullOrWhiteSpace(vars.AppName)) + { + text = text.Replace("$APP_NAME", vars.AppName, StringComparison.Ordinal); } - public Task SendUsageAsync(IUser user, IAppEntity app, long usage, long usageLimit, - CancellationToken ct = default) + if (!string.IsNullOrWhiteSpace(vars.TeamName)) { - Guard.NotNull(user); - Guard.NotNull(app); - - var vars = new TemplatesVars - { - ApiCalls = usage, - ApiCallsLimit = usageLimit, - AppName = app.DisplayName() - }; - - return SendEmailAsync("Usage", - texts.UsageSubject, - texts.UsageBody, - user, vars, ct); + text = text.Replace("$TEAM_NAME", vars.AppName, StringComparison.Ordinal); } - public Task SendInviteAsync(IUser assigner, IUser user, IAppEntity app, - CancellationToken ct = default) + if (vars.Assigner != null) { - Guard.NotNull(assigner); - Guard.NotNull(user); - Guard.NotNull(app); - - var vars = new TemplatesVars { Assigner = assigner, AppName = app.DisplayName() }; - - if (user.Claims.HasConsent()) - { - return SendEmailAsync("ExistingUser", - texts.ExistingUserSubject, - texts.ExistingUserBody, - user, vars, ct); - } - else - { - return SendEmailAsync("NewUser", - texts.NewUserSubject, - texts.NewUserBody, - user, vars, ct); - } + text = text.Replace("$ASSIGNER_EMAIL", vars.Assigner.Email, StringComparison.Ordinal); + text = text.Replace("$ASSIGNER_NAME", vars.Assigner.Claims.DisplayName(), StringComparison.Ordinal); } - public Task SendInviteAsync(IUser assigner, IUser user, ITeamEntity team, - CancellationToken ct = default) + if (vars.User != null) { - Guard.NotNull(assigner); - Guard.NotNull(user); - Guard.NotNull(team); - - var vars = new TemplatesVars { Assigner = assigner, TeamName = team.Name }; - - if (user.Claims.HasConsent()) - { - return SendEmailAsync("ExistingUser", - texts.ExistingTeamUserSubject, - texts.ExistingTeamUserBody, - user, vars, ct); - } - else - { - return SendEmailAsync("NewUser", - texts.NewTeamUserSubject, - texts.NewTeamUserBody, - user, vars, ct); - } + text = text.Replace("$USER_EMAIL", vars.User.Email, StringComparison.Ordinal); + text = text.Replace("$USER_NAME", vars.User.Claims.DisplayName(), StringComparison.Ordinal); } - private async Task SendEmailAsync(string template, string emailSubj, string emailBody, IUser user, TemplatesVars vars, - CancellationToken ct) + if (vars.ApiCallsLimit != null) { - if (string.IsNullOrWhiteSpace(emailBody)) - { - log.LogWarning("Cannot send email to {email}: No email subject configured for template {template}.", template, user.Email); - return; - } - - if (string.IsNullOrWhiteSpace(emailSubj)) - { - log.LogWarning("Cannot send email to {email}: No email body configured for template {template}.", template, user.Email); - return; - } - - vars.URL = urlGenerator.UI(); - - vars.User = user; - - emailSubj = Format(emailSubj, vars); - emailBody = Format(emailBody, vars); - - try - { - await emailSender.SendAsync(user.Email, emailSubj, emailBody, ct); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to send notification to {email}.", user.Email); - throw; - } + text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.ToString(), StringComparison.Ordinal); } - private static string Format(string text, TemplatesVars vars) + if (vars.ApiCalls != null) { - if (!string.IsNullOrWhiteSpace(vars.AppName)) - { - text = text.Replace("$APP_NAME", vars.AppName, StringComparison.Ordinal); - } - - if (!string.IsNullOrWhiteSpace(vars.TeamName)) - { - text = text.Replace("$TEAM_NAME", vars.AppName, StringComparison.Ordinal); - } - - if (vars.Assigner != null) - { - text = text.Replace("$ASSIGNER_EMAIL", vars.Assigner.Email, StringComparison.Ordinal); - text = text.Replace("$ASSIGNER_NAME", vars.Assigner.Claims.DisplayName(), StringComparison.Ordinal); - } - - if (vars.User != null) - { - text = text.Replace("$USER_EMAIL", vars.User.Email, StringComparison.Ordinal); - text = text.Replace("$USER_NAME", vars.User.Claims.DisplayName(), StringComparison.Ordinal); - } - - if (vars.ApiCallsLimit != null) - { - text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.ToString(), StringComparison.Ordinal); - } - - if (vars.ApiCalls != null) - { - text = text.Replace("$API_CALLS", vars.ApiCalls.ToString(), StringComparison.Ordinal); - } - - text = text.Replace("$UI_URL", vars.URL, StringComparison.Ordinal); - - return text; + text = text.Replace("$API_CALLS", vars.ApiCalls.ToString(), StringComparison.Ordinal); } + + text = text.Replace("$UI_URL", vars.URL, StringComparison.Ordinal); + + return text; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/IUserNotifications.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/IUserNotifications.cs index 306b2bc81b..c66ddf8a4b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/IUserNotifications.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/IUserNotifications.cs @@ -9,19 +9,18 @@ using Squidex.Domain.Apps.Entities.Teams; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Notifications +namespace Squidex.Domain.Apps.Entities.Notifications; + +public interface IUserNotifications { - public interface IUserNotifications - { - bool IsActive { get; } + bool IsActive { get; } - Task SendUsageAsync(IUser user, IAppEntity app, long usage, long usageLimit, - CancellationToken ct = default); + Task SendUsageAsync(IUser user, IAppEntity app, long usage, long usageLimit, + CancellationToken ct = default); - Task SendInviteAsync(IUser assigner, IUser user, IAppEntity app, - CancellationToken ct = default); + Task SendInviteAsync(IUser assigner, IUser user, IAppEntity app, + CancellationToken ct = default); - Task SendInviteAsync(IUser assigner, IUser user, ITeamEntity team, - CancellationToken ct = default); - } + Task SendInviteAsync(IUser assigner, IUser user, ITeamEntity team, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopUserNotifications.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopUserNotifications.cs index b1c629d85a..19d61f2106 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopUserNotifications.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopUserNotifications.cs @@ -9,31 +9,30 @@ using Squidex.Domain.Apps.Entities.Teams; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Notifications +namespace Squidex.Domain.Apps.Entities.Notifications; + +public sealed class NoopUserNotifications : IUserNotifications { - public sealed class NoopUserNotifications : IUserNotifications + public bool IsActive { - public bool IsActive - { - get => false; - } + get => false; + } - public Task SendInviteAsync(IUser assigner, IUser user, IAppEntity app, - CancellationToken ct = default) - { - return Task.CompletedTask; - } + public Task SendInviteAsync(IUser assigner, IUser user, IAppEntity app, + CancellationToken ct = default) + { + return Task.CompletedTask; + } - public Task SendInviteAsync(IUser assigner, IUser user, ITeamEntity team, - CancellationToken ct = default) - { - return Task.CompletedTask; - } + public Task SendInviteAsync(IUser assigner, IUser user, ITeamEntity team, + CancellationToken ct = default) + { + return Task.CompletedTask; + } - public Task SendUsageAsync(IUser user, IAppEntity app, long usage, long limit, - CancellationToken ct = default) - { - return Task.CompletedTask; - } + public Task SendUsageAsync(IUser user, IAppEntity app, long usage, long limit, + CancellationToken ct = default) + { + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/OperationContextBase.cs b/backend/src/Squidex.Domain.Apps.Entities/OperationContextBase.cs index b89d4e8746..d7be0a091b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/OperationContextBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/OperationContextBase.cs @@ -12,78 +12,77 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public abstract class OperationContextBase<TCommand, TSnapShot> where TCommand : SquidexCommand, IAggregateCommand { - public abstract class OperationContextBase<TCommand, TSnapShot> where TCommand : SquidexCommand, IAggregateCommand - { - private readonly List<ValidationError> errors = new List<ValidationError>(); - private readonly IServiceProvider serviceProvider; - private readonly Func<TSnapShot> snapshotProvider; - private readonly TSnapShot snapshotInitial; + private readonly List<ValidationError> errors = new List<ValidationError>(); + private readonly IServiceProvider serviceProvider; + private readonly Func<TSnapShot> snapshotProvider; + private readonly TSnapShot snapshotInitial; - public RefToken Actor => Command.Actor; + public RefToken Actor => Command.Actor; - public IAppEntity App { get; init; } + public IAppEntity App { get; init; } - public DomainId CommandId { get; init; } + public DomainId CommandId { get; init; } - public TCommand Command { get; init; } + public TCommand Command { get; init; } - public TSnapShot Snapshot => snapshotProvider(); + public TSnapShot Snapshot => snapshotProvider(); - public TSnapShot SnapshotInitial => snapshotInitial; + public TSnapShot SnapshotInitial => snapshotInitial; - public ClaimsPrincipal? User => Command.User; + public ClaimsPrincipal? User => Command.User; - public Dictionary<string, object> Context { get; } = new Dictionary<string, object>(); + public Dictionary<string, object> Context { get; } = new Dictionary<string, object>(); - protected OperationContextBase(IServiceProvider serviceProvider, Func<TSnapShot> snapshotProvider) - { - Guard.NotNull(serviceProvider); - Guard.NotNull(snapshotProvider); + protected OperationContextBase(IServiceProvider serviceProvider, Func<TSnapShot> snapshotProvider) + { + Guard.NotNull(serviceProvider); + Guard.NotNull(snapshotProvider); - this.serviceProvider = serviceProvider; - this.snapshotProvider = snapshotProvider; - this.snapshotInitial = snapshotProvider(); - } + this.serviceProvider = serviceProvider; + this.snapshotProvider = snapshotProvider; + this.snapshotInitial = snapshotProvider(); + } - public T Resolve<T>() where T : notnull - { - return serviceProvider.GetRequiredService<T>(); - } + public T Resolve<T>() where T : notnull + { + return serviceProvider.GetRequiredService<T>(); + } - public T? ResolveOptional<T>() where T : class - { - return serviceProvider.GetService(typeof(T)) as T; - } + public T? ResolveOptional<T>() where T : class + { + return serviceProvider.GetService(typeof(T)) as T; + } - public OperationContextBase<TCommand, TSnapShot> AddError(string message, params string[] propertyNames) - { - errors.Add(new ValidationError(message, propertyNames)); + public OperationContextBase<TCommand, TSnapShot> AddError(string message, params string[] propertyNames) + { + errors.Add(new ValidationError(message, propertyNames)); - return this; - } + return this; + } - public OperationContextBase<TCommand, TSnapShot> AddError(ValidationError newError) - { - errors.Add(newError); + public OperationContextBase<TCommand, TSnapShot> AddError(ValidationError newError) + { + errors.Add(newError); - return this; - } + return this; + } - public OperationContextBase<TCommand, TSnapShot> AddErrors(IEnumerable<ValidationError> newErrors) - { - errors.AddRange(newErrors); + public OperationContextBase<TCommand, TSnapShot> AddErrors(IEnumerable<ValidationError> newErrors) + { + errors.AddRange(newErrors); - return this; - } + return this; + } - public void ThrowOnErrors() + public void ThrowOnErrors() + { + if (errors.Count > 0) { - if (errors.Count > 0) - { - throw new ValidationException(errors); - } + throw new ValidationException(errors); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs index 5264ca68e2..6dbddd8814 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs @@ -8,146 +8,145 @@ // </auto-generated> //------------------------------------------------------------------------------ -namespace Squidex.Domain.Apps.Entities.Properties { - using System; +namespace Squidex.Domain.Apps.Entities.Properties; +using System; + + +/// <summary> +/// A strongly-typed resource class, for looking up localized strings, etc. +/// </summary> +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } /// <summary> - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Returns the cached ResourceManager instance used by this class. /// </summary> - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// <summary> - /// Returns the cached ResourceManager instance used by this class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Entities.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Entities.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// <summary> - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// <summary> - /// Looks up a localized string similar to Queries the asset with the specified ID and invokes the callback with an array of assets.. - /// </summary> - internal static string ScriptingGetAsset { - get { - return ResourceManager.GetString("ScriptingGetAsset", resourceCulture); - } + set { + resourceCulture = value; } - - /// <summary> - /// Looks up a localized string similar to Queries the assets with the specified IDs and invokes the callback with an array of assets.. - /// </summary> - internal static string ScriptingGetAssets { - get { - return ResourceManager.GetString("ScriptingGetAssets", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Queries the asset with the specified ID and invokes the callback with an array of assets.. + /// </summary> + internal static string ScriptingGetAsset { + get { + return ResourceManager.GetString("ScriptingGetAsset", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Get the text of an asset. Encodings: base64,ascii,unicode,utf8. - /// </summary> - internal static string ScriptingGetAssetText { - get { - return ResourceManager.GetString("ScriptingGetAssetText", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Queries the assets with the specified IDs and invokes the callback with an array of assets.. + /// </summary> + internal static string ScriptingGetAssets { + get { + return ResourceManager.GetString("ScriptingGetAssets", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Gets the blur hash of an asset if it is an image or null otherwise.. - /// </summary> - internal static string ScriptingGetBlurHash { - get { - return ResourceManager.GetString("ScriptingGetBlurHash", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Get the text of an asset. Encodings: base64,ascii,unicode,utf8. + /// </summary> + internal static string ScriptingGetAssetText { + get { + return ResourceManager.GetString("ScriptingGetAssetText", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Queries the content item with the specified ID and invokes the callback with an array of contents.. - /// </summary> - internal static string ScriptingGetReference { - get { - return ResourceManager.GetString("ScriptingGetReference", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Gets the blur hash of an asset if it is an image or null otherwise.. + /// </summary> + internal static string ScriptingGetBlurHash { + get { + return ResourceManager.GetString("ScriptingGetBlurHash", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Queries the content items with the specified IDs and invokes the callback with an array of contents.. - /// </summary> - internal static string ScriptingGetReferences { - get { - return ResourceManager.GetString("ScriptingGetReferences", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Queries the content item with the specified ID and invokes the callback with an array of contents.. + /// </summary> + internal static string ScriptingGetReference { + get { + return ResourceManager.GetString("ScriptingGetReference", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Increments the counter with the given name and returns the value (OBSOLETE).. - /// </summary> - internal static string ScriptingIncrementCounter { - get { - return ResourceManager.GetString("ScriptingIncrementCounter", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Queries the content items with the specified IDs and invokes the callback with an array of contents.. + /// </summary> + internal static string ScriptingGetReferences { + get { + return ResourceManager.GetString("ScriptingGetReferences", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Increments the counter with the given name and returns the value.. - /// </summary> - internal static string ScriptingIncrementCounterV2 { - get { - return ResourceManager.GetString("ScriptingIncrementCounterV2", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Increments the counter with the given name and returns the value (OBSOLETE).. + /// </summary> + internal static string ScriptingIncrementCounter { + get { + return ResourceManager.GetString("ScriptingIncrementCounter", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Resets the counter with the given name to zero (OBSOLETE).. - /// </summary> - internal static string ScriptingResetCounter { - get { - return ResourceManager.GetString("ScriptingResetCounter", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Increments the counter with the given name and returns the value.. + /// </summary> + internal static string ScriptingIncrementCounterV2 { + get { + return ResourceManager.GetString("ScriptingIncrementCounterV2", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Resets the counter with the given name to zero.. - /// </summary> - internal static string ScriptingResetCounterV2 { - get { - return ResourceManager.GetString("ScriptingResetCounterV2", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Resets the counter with the given name to zero (OBSOLETE).. + /// </summary> + internal static string ScriptingResetCounter { + get { + return ResourceManager.GetString("ScriptingResetCounter", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Resets the counter with the given name to zero.. + /// </summary> + internal static string ScriptingResetCounterV2 { + get { + return ResourceManager.GetString("ScriptingResetCounterV2", resourceCulture); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Q.cs b/backend/src/Squidex.Domain.Apps.Entities/Q.cs index 52f12cd66b..d7f3007ddf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Q.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Q.cs @@ -10,115 +10,114 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public record Q { - public record Q - { - public static Q Empty => new Q(); + public static Q Empty => new Q(); - public ClrQuery Query { get; init; } = new ClrQuery(); + public ClrQuery Query { get; init; } = new ClrQuery(); - public IReadOnlyList<DomainId>? Ids { get; init; } + public IReadOnlyList<DomainId>? Ids { get; init; } - public DomainId Referencing { get; init; } + public DomainId Referencing { get; init; } - public DomainId Reference { get; init; } + public DomainId Reference { get; init; } - public string? QueryAsOdata { get; init; } + public string? QueryAsOdata { get; init; } - public string? QueryAsJson { get; init; } + public string? QueryAsJson { get; init; } - public Instant? ScheduledFrom { get; init; } + public Instant? ScheduledFrom { get; init; } - public Instant? ScheduledTo { get; init; } + public Instant? ScheduledTo { get; init; } - public Query<JsonValue>? JsonQuery { get; init; } + public Query<JsonValue>? JsonQuery { get; init; } - public RefToken? CreatedBy { get; init; } + public RefToken? CreatedBy { get; init; } - public bool NoTotal { get; init; } + public bool NoTotal { get; init; } - public bool NoSlowTotal { get; init; } + public bool NoSlowTotal { get; init; } - private Q() - { - } + private Q() + { + } - public Q WithQuery(ClrQuery query) - { - Guard.NotNull(query); + public Q WithQuery(ClrQuery query) + { + Guard.NotNull(query); - return this with { Query = query }; - } + return this with { Query = query }; + } - public Q WithoutTotal(bool value = true) - { - return this with { NoTotal = value }; - } + public Q WithoutTotal(bool value = true) + { + return this with { NoTotal = value }; + } - public Q WithoutSlowTotal(bool value = true) - { - return this with { NoSlowTotal = value }; - } + public Q WithoutSlowTotal(bool value = true) + { + return this with { NoSlowTotal = value }; + } - public Q WithODataQuery(string? query) - { - return this with { QueryAsOdata = query }; - } + public Q WithODataQuery(string? query) + { + return this with { QueryAsOdata = query }; + } - public Q WithJsonQuery(string? query) - { - return this with { QueryAsJson = query }; - } + public Q WithJsonQuery(string? query) + { + return this with { QueryAsJson = query }; + } - public Q WithJsonQuery(Query<JsonValue>? query) - { - return this with { JsonQuery = query }; - } + public Q WithJsonQuery(Query<JsonValue>? query) + { + return this with { JsonQuery = query }; + } - public Q WithReferencing(DomainId id) - { - return this with { Referencing = id }; - } + public Q WithReferencing(DomainId id) + { + return this with { Referencing = id }; + } - public Q WithReference(DomainId id) - { - return this with { Reference = id }; - } + public Q WithReference(DomainId id) + { + return this with { Reference = id }; + } - public Q WithIds(params DomainId[] ids) - { - return this with { Ids = ids?.ToList() }; - } + public Q WithIds(params DomainId[] ids) + { + return this with { Ids = ids?.ToList() }; + } - public Q WithIds(IEnumerable<DomainId> ids) - { - return this with { Ids = ids?.ToList() }; - } + public Q WithIds(IEnumerable<DomainId> ids) + { + return this with { Ids = ids?.ToList() }; + } - public Q WithSchedule(Instant from, Instant to) - { - return this with { ScheduledFrom = from, ScheduledTo = to }; - } + public Q WithSchedule(Instant from, Instant to) + { + return this with { ScheduledFrom = from, ScheduledTo = to }; + } - public Q WithIds(string? ids) + public Q WithIds(string? ids) + { + if (string.IsNullOrWhiteSpace(ids)) { - if (string.IsNullOrWhiteSpace(ids)) - { - return this with { Ids = null }; - } + return this with { Ids = null }; + } - var idsList = new List<DomainId>(); + var idsList = new List<DomainId>(); - if (!string.IsNullOrEmpty(ids)) + if (!string.IsNullOrEmpty(ids)) + { + foreach (var id in ids.Split(',')) { - foreach (var id in ids.Split(',')) - { - idsList.Add(DomainId.Create(id)); - } + idsList.Add(DomainId.Create(id)); } - - return this with { Ids = idsList }; } + + return this with { Ids = idsList }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs index e5bc08b65e..18f5455b20 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs @@ -12,44 +12,43 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class BackupRules : IBackupHandler { - public sealed class BackupRules : IBackupHandler - { - private const int BatchSize = 100; - private readonly HashSet<DomainId> ruleIds = new HashSet<DomainId>(); - private readonly Rebuilder rebuilder; + private const int BatchSize = 100; + private readonly HashSet<DomainId> ruleIds = new HashSet<DomainId>(); + private readonly Rebuilder rebuilder; - public string Name { get; } = "Rules"; + public string Name { get; } = "Rules"; - public BackupRules(Rebuilder rebuilder) - { - this.rebuilder = rebuilder; - } + public BackupRules(Rebuilder rebuilder) + { + this.rebuilder = rebuilder; + } - public Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, - CancellationToken ct) + public Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, + CancellationToken ct) + { + switch (@event.Payload) { - switch (@event.Payload) - { - case RuleCreated: - ruleIds.Add(@event.Headers.AggregateId()); - break; - case RuleDeleted: - ruleIds.Remove(@event.Headers.AggregateId()); - break; - } - - return Task.FromResult(true); + case RuleCreated: + ruleIds.Add(@event.Headers.AggregateId()); + break; + case RuleDeleted: + ruleIds.Remove(@event.Headers.AggregateId()); + break; } - public async Task RestoreAsync(RestoreContext context, - CancellationToken ct) + return Task.FromResult(true); + } + + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) + { + if (ruleIds.Count > 0) { - if (ruleIds.Count > 0) - { - await rebuilder.InsertManyAsync<RuleDomainObject, RuleDomainObject.State>(ruleIds, BatchSize, ct); - } + await rebuilder.InsertManyAsync<RuleDomainObject, RuleDomainObject.State>(ruleIds, BatchSize, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs index 8f0fc974d3..58f9da26e3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/CreateRule.cs @@ -7,13 +7,12 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules.Commands +namespace Squidex.Domain.Apps.Entities.Rules.Commands; + +public sealed class CreateRule : RuleEditCommand { - public sealed class CreateRule : RuleEditCommand + public CreateRule() { - public CreateRule() - { - RuleId = DomainId.NewGuid(); - } + RuleId = DomainId.NewGuid(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs index af81dc3275..8267ff8b6c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DeleteRule.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules.Commands +namespace Squidex.Domain.Apps.Entities.Rules.Commands; + +public sealed class DeleteRule : RuleCommand { - public sealed class DeleteRule : RuleCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs index 6ce4134b41..b42577d39b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/DisableRule.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules.Commands +namespace Squidex.Domain.Apps.Entities.Rules.Commands; + +public sealed class DisableRule : RuleCommand { - public sealed class DisableRule : RuleCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs index e09ef01eb5..2a97f5fd2a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/EnableRule.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules.Commands +namespace Squidex.Domain.Apps.Entities.Rules.Commands; + +public sealed class EnableRule : RuleCommand { - public sealed class EnableRule : RuleCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs index f327300fe8..41ed8c9184 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/RuleEditCommand.cs @@ -7,16 +7,15 @@ using Squidex.Domain.Apps.Core.Rules; -namespace Squidex.Domain.Apps.Entities.Rules.Commands +namespace Squidex.Domain.Apps.Entities.Rules.Commands; + +public abstract class RuleEditCommand : RuleCommand { - public abstract class RuleEditCommand : RuleCommand - { - public string? Name { get; set; } + public string? Name { get; set; } - public RuleTrigger? Trigger { get; set; } + public RuleTrigger? Trigger { get; set; } - public RuleAction? Action { get; set; } + public RuleAction? Action { get; set; } - public bool? IsEnabled { get; set; } - } + public bool? IsEnabled { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs index c0e71188e3..1b0151d5d2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/TriggerRule.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules.Commands +namespace Squidex.Domain.Apps.Entities.Rules.Commands; + +public sealed class TriggerRule : RuleCommand { - public sealed class TriggerRule : RuleCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs index 76b9ac96b3..6098e3467a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/UpdateRule.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules.Commands +namespace Squidex.Domain.Apps.Entities.Rules.Commands; + +public sealed class UpdateRule : RuleEditCommand { - public sealed class UpdateRule : RuleEditCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/_RuleCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/_RuleCommand.cs index a655647159..0cbbcf67b1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/_RuleCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Commands/_RuleCommand.cs @@ -10,23 +10,22 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Rules.Commands +namespace Squidex.Domain.Apps.Entities.Rules.Commands; + +public abstract class RuleCommand : RuleCommandBase { - public abstract class RuleCommand : RuleCommandBase - { - public DomainId RuleId { get; set; } + public DomainId RuleId { get; set; } - public override DomainId AggregateId - { - get => DomainId.Combine(AppId, RuleId); - } + public override DomainId AggregateId + { + get => DomainId.Combine(AppId, RuleId); } +} - // This command is needed as marker for middlewares. - public abstract class RuleCommandBase : SquidexCommand, IAppCommand, IAggregateCommand - { - public NamedId<DomainId> AppId { get; set; } +// This command is needed as marker for middlewares. +public abstract class RuleCommandBase : SquidexCommand, IAppCommand, IAggregateCommand +{ + public NamedId<DomainId> AppId { get; set; } - public abstract DomainId AggregateId { get; } - } + public abstract DomainId AggregateId { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/GuardRule.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/GuardRule.cs index 7a2cd0083e..dd99e6520c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/GuardRule.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/GuardRule.cs @@ -9,60 +9,59 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards; + +public static class GuardRule { - public static class GuardRule + public static Task CanCreate(CreateRule command, IAppProvider appProvider) { - public static Task CanCreate(CreateRule command, IAppProvider appProvider) - { - Guard.NotNull(command); + Guard.NotNull(command); - return Validate.It(async e => + return Validate.It(async e => + { + if (command.Trigger == null) { - if (command.Trigger == null) - { - e(Not.Defined(nameof(command.Trigger)), nameof(command.Trigger)); - } - else - { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); + e(Not.Defined(nameof(command.Trigger)), nameof(command.Trigger)); + } + else + { + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); - errors.Foreach((x, _) => x.AddTo(e)); - } + errors.Foreach((x, _) => x.AddTo(e)); + } - if (command.Action == null) - { - e(Not.Defined(nameof(command.Action)), nameof(command.Action)); - } - else - { - var errors = command.Action.Validate(); + if (command.Action == null) + { + e(Not.Defined(nameof(command.Action)), nameof(command.Action)); + } + else + { + var errors = command.Action.Validate(); - errors.Foreach((x, _) => x.AddTo(e)); - } - }); - } + errors.Foreach((x, _) => x.AddTo(e)); + } + }); + } - public static Task CanUpdate(UpdateRule command, IRuleEntity rule, IAppProvider appProvider) - { - Guard.NotNull(command); + public static Task CanUpdate(UpdateRule command, IRuleEntity rule, IAppProvider appProvider) + { + Guard.NotNull(command); - return Validate.It(async e => + return Validate.It(async e => + { + if (command.Trigger != null) { - if (command.Trigger != null) - { - var errors = await RuleTriggerValidator.ValidateAsync(rule.AppId.Id, command.Trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(rule.AppId.Id, command.Trigger, appProvider); - errors.Foreach((x, _) => x.AddTo(e)); - } + errors.Foreach((x, _) => x.AddTo(e)); + } - if (command.Action != null) - { - var errors = command.Action.Validate(); + if (command.Action != null) + { + var errors = command.Action.Validate(); - errors.Foreach((x, _) => x.AddTo(e)); - } - }); - } + errors.Foreach((x, _) => x.AddTo(e)); + } + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/RuleTriggerValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/RuleTriggerValidator.cs index c63579393c..3a01aee542 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/RuleTriggerValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/RuleTriggerValidator.cs @@ -12,96 +12,95 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards; + +public sealed class RuleTriggerValidator : IRuleTriggerVisitor<Task<IEnumerable<ValidationError>>> { - public sealed class RuleTriggerValidator : IRuleTriggerVisitor<Task<IEnumerable<ValidationError>>> + public Func<DomainId, Task<ISchemaEntity?>> SchemaProvider { get; } + + public RuleTriggerValidator(Func<DomainId, Task<ISchemaEntity?>> schemaProvider) { - public Func<DomainId, Task<ISchemaEntity?>> SchemaProvider { get; } + SchemaProvider = schemaProvider; + } - public RuleTriggerValidator(Func<DomainId, Task<ISchemaEntity?>> schemaProvider) - { - SchemaProvider = schemaProvider; - } + public static Task<IEnumerable<ValidationError>> ValidateAsync(DomainId appId, RuleTrigger trigger, IAppProvider appProvider) + { + Guard.NotNull(trigger); + Guard.NotNull(appProvider); - public static Task<IEnumerable<ValidationError>> ValidateAsync(DomainId appId, RuleTrigger trigger, IAppProvider appProvider) - { - Guard.NotNull(trigger); - Guard.NotNull(appProvider); + var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x)); - var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x)); + return trigger.Accept(visitor); + } - return trigger.Accept(visitor); - } + public Task<IEnumerable<ValidationError>> Visit(CommentTrigger trigger) + { + return Task.FromResult(Enumerable.Empty<ValidationError>()); + } - public Task<IEnumerable<ValidationError>> Visit(CommentTrigger trigger) - { - return Task.FromResult(Enumerable.Empty<ValidationError>()); - } + public Task<IEnumerable<ValidationError>> Visit(AssetChangedTriggerV2 trigger) + { + return Task.FromResult(Enumerable.Empty<ValidationError>()); + } - public Task<IEnumerable<ValidationError>> Visit(AssetChangedTriggerV2 trigger) - { - return Task.FromResult(Enumerable.Empty<ValidationError>()); - } + public Task<IEnumerable<ValidationError>> Visit(ManualTrigger trigger) + { + return Task.FromResult(Enumerable.Empty<ValidationError>()); + } - public Task<IEnumerable<ValidationError>> Visit(ManualTrigger trigger) - { - return Task.FromResult(Enumerable.Empty<ValidationError>()); - } + public Task<IEnumerable<ValidationError>> Visit(SchemaChangedTrigger trigger) + { + return Task.FromResult(Enumerable.Empty<ValidationError>()); + } - public Task<IEnumerable<ValidationError>> Visit(SchemaChangedTrigger trigger) - { - return Task.FromResult(Enumerable.Empty<ValidationError>()); - } + public Task<IEnumerable<ValidationError>> Visit(UsageTrigger trigger) + { + var errors = new List<ValidationError>(); - public Task<IEnumerable<ValidationError>> Visit(UsageTrigger trigger) + if (trigger.NumDays is < 1 or > 30) { - var errors = new List<ValidationError>(); + errors.Add(new ValidationError(Not.Between(nameof(trigger.NumDays), 1, 30), nameof(trigger.NumDays))); + } - if (trigger.NumDays is < 1 or > 30) - { - errors.Add(new ValidationError(Not.Between(nameof(trigger.NumDays), 1, 30), nameof(trigger.NumDays))); - } + return Task.FromResult<IEnumerable<ValidationError>>(errors); + } - return Task.FromResult<IEnumerable<ValidationError>>(errors); - } + public async Task<IEnumerable<ValidationError>> Visit(ContentChangedTriggerV2 trigger) + { + var errors = new List<ValidationError>(); - public async Task<IEnumerable<ValidationError>> Visit(ContentChangedTriggerV2 trigger) + if (trigger.Schemas != null) { - var errors = new List<ValidationError>(); + var tasks = new List<Task<ValidationError?>>(); - if (trigger.Schemas != null) + foreach (var schema in trigger.Schemas) { - var tasks = new List<Task<ValidationError?>>(); - - foreach (var schema in trigger.Schemas) + if (schema.SchemaId == DomainId.Empty) { - if (schema.SchemaId == DomainId.Empty) - { - errors.Add(new ValidationError(Not.Defined("SchemaId"), nameof(trigger.Schemas))); - } - else - { - tasks.Add(CheckSchemaAsync(schema)); - } + errors.Add(new ValidationError(Not.Defined("SchemaId"), nameof(trigger.Schemas))); + } + else + { + tasks.Add(CheckSchemaAsync(schema)); } - - var checkErrors = await Task.WhenAll(tasks); - - errors.AddRange(checkErrors.NotNull()); } - return errors; + var checkErrors = await Task.WhenAll(tasks); + + errors.AddRange(checkErrors.NotNull()); } - private async Task<ValidationError?> CheckSchemaAsync(ContentChangedTriggerSchemaV2 schema) - { - if (await SchemaProvider(schema.SchemaId) == null) - { - return new ValidationError(T.Get("schemas.notFoundId", new { id = schema.SchemaId }), - nameof(ContentChangedTriggerV2.Schemas)); - } + return errors; + } - return null; + private async Task<ValidationError?> CheckSchemaAsync(ContentChangedTriggerSchemaV2 schema) + { + if (await SchemaProvider(schema.SchemaId) == null) + { + return new ValidationError(T.Get("schemas.notFoundId", new { id = schema.SchemaId }), + nameof(ContentChangedTriggerV2.Schemas)); } + + return null; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs index e7ed68744f..31763114d1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs @@ -12,92 +12,91 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject; + +public partial class RuleDomainObject { - public partial class RuleDomainObject + [CollectionName("Rules")] + public sealed class State : DomainObjectState<State>, IRuleEntity { - [CollectionName("Rules")] - public sealed class State : DomainObjectState<State>, IRuleEntity - { - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public Rule RuleDef { get; set; } + public Rule RuleDef { get; set; } - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - [JsonIgnore] - public DomainId UniqueId - { - get => DomainId.Combine(AppId, Id); - } + [JsonIgnore] + public DomainId UniqueId + { + get => DomainId.Combine(AppId, Id); + } + + public override bool ApplyEvent(IEvent @event) + { + var previousRule = RuleDef; - public override bool ApplyEvent(IEvent @event) + switch (@event) { - var previousRule = RuleDef; + case RuleCreated e: + { + Id = e.RuleId; - switch (@event) - { - case RuleCreated e: - { - Id = e.RuleId; + RuleDef = new Rule(e.Trigger, e.Action); + RuleDef = RuleDef.Rename(e.Name); - RuleDef = new Rule(e.Trigger, e.Action); - RuleDef = RuleDef.Rename(e.Name); + AppId = e.AppId; + return true; + } - AppId = e.AppId; - return true; + case RuleUpdated e: + { + if (e.Trigger != null) + { + RuleDef = RuleDef.Update(e.Trigger); } - case RuleUpdated e: + if (e.Action != null) { - if (e.Trigger != null) - { - RuleDef = RuleDef.Update(e.Trigger); - } - - if (e.Action != null) - { - RuleDef = RuleDef.Update(e.Action); - } - - if (e.Name != null) - { - RuleDef = RuleDef.Rename(e.Name); - } - - if (e.IsEnabled == true) - { - RuleDef = RuleDef.Enable(); - } - else if (e.IsEnabled == false) - { - RuleDef = RuleDef.Disable(); - } - - break; + RuleDef = RuleDef.Update(e.Action); } - case RuleEnabled: + if (e.Name != null) { - RuleDef = RuleDef.Enable(); - break; + RuleDef = RuleDef.Rename(e.Name); } - case RuleDisabled: + if (e.IsEnabled == true) { - RuleDef = RuleDef.Disable(); - break; + RuleDef = RuleDef.Enable(); } - - case RuleDeleted: + else if (e.IsEnabled == false) { - IsDeleted = true; - return true; + RuleDef = RuleDef.Disable(); } - } - return !ReferenceEquals(previousRule, RuleDef); + break; + } + + case RuleEnabled: + { + RuleDef = RuleDef.Enable(); + break; + } + + case RuleDisabled: + { + RuleDef = RuleDef.Disable(); + break; + } + + case RuleDeleted: + { + IsDeleted = true; + return true; + } } + + return !ReferenceEquals(previousRule, RuleDef); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs index 87b390b25e..8dc7297ee5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs @@ -19,145 +19,144 @@ #pragma warning disable MA0022 // Return Task.FromResult instead of returning null -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject; + +public partial class RuleDomainObject : DomainObject<RuleDomainObject.State> { - public partial class RuleDomainObject : DomainObject<RuleDomainObject.State> + private readonly IServiceProvider serviceProvider; + + public RuleDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<RuleDomainObject> log, + IServiceProvider serviceProvider) + : base(id, persistence, log) { - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public RuleDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<RuleDomainObject> log, - IServiceProvider serviceProvider) - : base(id, persistence, log) - { - this.serviceProvider = serviceProvider; - } + protected override bool IsDeleted(State snapshot) + { + return snapshot.IsDeleted; + } - protected override bool IsDeleted(State snapshot) - { - return snapshot.IsDeleted; - } + protected override bool CanAcceptCreation(ICommand command) + { + return command is RuleCommandBase; + } - protected override bool CanAcceptCreation(ICommand command) - { - return command is RuleCommandBase; - } + protected override bool CanAccept(ICommand command) + { + return command is RuleCommand ruleCommand && + ruleCommand.AppId.Equals(Snapshot.AppId) && + ruleCommand.RuleId.Equals(Snapshot.Id); + } - protected override bool CanAccept(ICommand command) + public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + switch (command) { - return command is RuleCommand ruleCommand && - ruleCommand.AppId.Equals(Snapshot.AppId) && - ruleCommand.RuleId.Equals(Snapshot.Id); - } + case CreateRule createRule: + return CreateReturnAsync(createRule, async (c, ct) => + { + await GuardRule.CanCreate(c, AppProvider()); - public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct) - { - switch (command) - { - case CreateRule createRule: - return CreateReturnAsync(createRule, async (c, ct) => - { - await GuardRule.CanCreate(c, AppProvider()); - - Create(c); - - return Snapshot; - }, ct); - - case UpdateRule updateRule: - return UpdateReturnAsync(updateRule, async (c, ct) => - { - await GuardRule.CanUpdate(c, Snapshot, AppProvider()); - - Update(c); - - return Snapshot; - }, ct); - - case EnableRule enable: - return UpdateReturn(enable, c => - { - Enable(c); - - return Snapshot; - }, ct); - - case DisableRule disable: - return UpdateReturn(disable, c => - { - Disable(c); - - return Snapshot; - }, ct); - - case DeleteRule delete: - return Update(delete, c => - { - Delete(c); - }, ct); - - case TriggerRule triggerRule: - return UpdateReturnAsync(triggerRule, async (c, ct) => - { - await Trigger(triggerRule); - - return true; - }, ct); - - default: - ThrowHelper.NotSupportedException(); - return default!; - } - } + Create(c); - private async Task Trigger(TriggerRule command) - { - var @event = new RuleManuallyTriggered(); + return Snapshot; + }, ct); - SimpleMapper.Map(command, @event); - SimpleMapper.Map(Snapshot, @event); + case UpdateRule updateRule: + return UpdateReturnAsync(updateRule, async (c, ct) => + { + await GuardRule.CanUpdate(c, Snapshot, AppProvider()); - await RuleEnqueuer().EnqueueAsync(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); - } + Update(c); - private IRuleEnqueuer RuleEnqueuer() - { - return serviceProvider.GetRequiredService<IRuleEnqueuer>(); - } + return Snapshot; + }, ct); - private void Create(CreateRule command) - { - Raise(command, new RuleCreated()); - } + case EnableRule enable: + return UpdateReturn(enable, c => + { + Enable(c); - private void Update(UpdateRule command) - { - Raise(command, new RuleUpdated()); - } + return Snapshot; + }, ct); - private void Enable(EnableRule command) - { - Raise(command, new RuleEnabled()); - } + case DisableRule disable: + return UpdateReturn(disable, c => + { + Disable(c); - private void Disable(DisableRule command) - { - Raise(command, new RuleDisabled()); - } + return Snapshot; + }, ct); - private void Delete(DeleteRule command) - { - Raise(command, new RuleDeleted()); - } + case DeleteRule delete: + return Update(delete, c => + { + Delete(c); + }, ct); - private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent - { - RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); - } + case TriggerRule triggerRule: + return UpdateReturnAsync(triggerRule, async (c, ct) => + { + await Trigger(triggerRule); - private IAppProvider AppProvider() - { - return serviceProvider.GetRequiredService<IAppProvider>(); + return true; + }, ct); + + default: + ThrowHelper.NotSupportedException(); + return default!; } } + + private async Task Trigger(TriggerRule command) + { + var @event = new RuleManuallyTriggered(); + + SimpleMapper.Map(command, @event); + SimpleMapper.Map(Snapshot, @event); + + await RuleEnqueuer().EnqueueAsync(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); + } + + private IRuleEnqueuer RuleEnqueuer() + { + return serviceProvider.GetRequiredService<IRuleEnqueuer>(); + } + + private void Create(CreateRule command) + { + Raise(command, new RuleCreated()); + } + + private void Update(UpdateRule command) + { + Raise(command, new RuleUpdated()); + } + + private void Enable(EnableRule command) + { + Raise(command, new RuleEnabled()); + } + + private void Disable(DisableRule command) + { + Raise(command, new RuleDisabled()); + } + + private void Delete(DeleteRule command) + { + Raise(command, new RuleDeleted()); + } + + private void Raise<T, TEvent>(T command, TEvent @event) where T : class where TEvent : AppEvent + { + RaiseEvent(Envelope.Create(SimpleMapper.Map(command, @event))); + } + + private IAppProvider AppProvider() + { + return serviceProvider.GetRequiredService<IAppProvider>(); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs index 0ec1af8681..4d6343a871 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs @@ -7,14 +7,13 @@ using NodaTime; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public interface IEnrichedRuleEntity : IRuleEntity { - public interface IEnrichedRuleEntity : IRuleEntity - { - int NumSucceeded { get; } + int NumSucceeded { get; } - int NumFailed { get; } + int NumFailed { get; } - Instant? LastExecuted { get; set; } - } + Instant? LastExecuted { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs index a5032150fd..ea6ba645ea 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs @@ -9,10 +9,9 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public interface IRuleEnqueuer { - public interface IRuleEnqueuer - { - Task EnqueueAsync(Rule rule, DomainId ruleId, Envelope<IEvent> @event); - } + Task EnqueueAsync(Rule rule, DomainId ruleId, Envelope<IEvent> @event); } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs index 3ed2e7e525..879ec8af1c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public interface IRuleEnricher { - public interface IRuleEnricher - { - Task<IEnrichedRuleEntity> EnrichAsync(IRuleEntity rule, Context context, - CancellationToken ct); + Task<IEnrichedRuleEntity> EnrichAsync(IRuleEntity rule, Context context, + CancellationToken ct); - Task<IReadOnlyList<IEnrichedRuleEntity>> EnrichAsync(IEnumerable<IRuleEntity> rules, Context context, - CancellationToken ct); - } + Task<IReadOnlyList<IEnrichedRuleEntity>> EnrichAsync(IEnumerable<IRuleEntity> rules, Context context, + CancellationToken ct); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs index 1961f66a99..7d1c9e2090 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs @@ -8,18 +8,17 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public interface IRuleEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion { - public interface IRuleEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - NamedId<DomainId> AppId { get; set; } + NamedId<DomainId> AppId { get; set; } - Rule RuleDef { get; } + Rule RuleDef { get; } - bool IsDeleted { get; } - } + bool IsDeleted { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs index 74c9668911..44101a8ce9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEventEntity.cs @@ -9,20 +9,19 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public interface IRuleEventEntity : IEntity { - public interface IRuleEventEntity : IEntity - { - RuleJob Job { get; } + RuleJob Job { get; } - Instant? NextAttempt { get; } + Instant? NextAttempt { get; } - RuleJobResult JobResult { get; } + RuleJobResult JobResult { get; } - RuleResult Result { get; } + RuleResult Result { get; } - int NumCalls { get; } + int NumCalls { get; } - string? LastDump { get; } - } + string? LastDump { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs index e0723a6590..6a02ccfaa2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public interface IRuleQueryService { - public interface IRuleQueryService - { - Task<IReadOnlyList<IEnrichedRuleEntity>> QueryAsync(Context context, - CancellationToken ct = default); - } + Task<IReadOnlyList<IEnrichedRuleEntity>> QueryAsync(Context context, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs index e2bbc93920..dda080b0b9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules.Indexes +namespace Squidex.Domain.Apps.Entities.Rules.Indexes; + +public interface IRulesIndex { - public interface IRulesIndex - { - Task<List<IRuleEntity>> GetRulesAsync(DomainId appId, - CancellationToken ct = default); - } + Task<List<IRuleEntity>> GetRulesAsync(DomainId appId, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs index 50b5dd4bb7..730bccfbe7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs @@ -8,31 +8,30 @@ using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules.Indexes +namespace Squidex.Domain.Apps.Entities.Rules.Indexes; + +public sealed class RulesIndex : IRulesIndex { - public sealed class RulesIndex : IRulesIndex - { - private readonly IRuleRepository ruleRepository; + private readonly IRuleRepository ruleRepository; - public RulesIndex(IRuleRepository ruleRepository) - { - this.ruleRepository = ruleRepository; - } + public RulesIndex(IRuleRepository ruleRepository) + { + this.ruleRepository = ruleRepository; + } - public async Task<List<IRuleEntity>> GetRulesAsync(DomainId appId, - CancellationToken ct = default) + public async Task<List<IRuleEntity>> GetRulesAsync(DomainId appId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("RulesIndex/GetRulesAsync")) { - using (Telemetry.Activities.StartActivity("RulesIndex/GetRulesAsync")) - { - var rules = await ruleRepository.QueryAllAsync(appId, ct); + var rules = await ruleRepository.QueryAllAsync(appId, ct); - return rules.Where(IsValid).ToList(); - } + return rules.Where(IsValid).ToList(); } + } - private static bool IsValid(IRuleEntity? rule) - { - return rule is { Version: > EtagVersion.Empty, IsDeleted: false }; - } + private static bool IsValid(IRuleEntity? rule) + { + return rule is { Version: > EtagVersion.Empty, IsDeleted: false }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs index 7ef0181c08..7620986737 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/ManualTriggerHandler.cs @@ -14,33 +14,32 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class ManualTriggerHandler : IRuleTriggerHandler { - public sealed class ManualTriggerHandler : IRuleTriggerHandler - { - public Type TriggerType => typeof(ManualTrigger); + public Type TriggerType => typeof(ManualTrigger); - public bool Handles(AppEvent appEvent) - { - return appEvent is RuleManuallyTriggered; - } + public bool Handles(AppEvent appEvent) + { + return appEvent is RuleManuallyTriggered; + } - public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct) - { - var result = new EnrichedManualEvent(); + public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, + [EnumeratorCancellation] CancellationToken ct) + { + var result = new EnrichedManualEvent(); - // Use the concrete event to map properties that are not part of app event. - SimpleMapper.Map((RuleManuallyTriggered)@event.Payload, result); + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map((RuleManuallyTriggered)@event.Payload, result); - await Task.Yield(); + await Task.Yield(); - yield return result; - } + yield return result; + } - public string? GetName(AppEvent @event) - { - return "Manual"; - } + public string? GetName(AppEvent @event) + { + return "Manual"; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs index bbc1e58916..19473236bb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs @@ -10,70 +10,69 @@ using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Rules.Queries +namespace Squidex.Domain.Apps.Entities.Rules.Queries; + +public sealed class RuleEnricher : IRuleEnricher { - public sealed class RuleEnricher : IRuleEnricher + private readonly IRuleEventRepository ruleEventRepository; + private readonly IRequestCache requestCache; + + public RuleEnricher(IRuleEventRepository ruleEventRepository, IRequestCache requestCache) { - private readonly IRuleEventRepository ruleEventRepository; - private readonly IRequestCache requestCache; + this.ruleEventRepository = ruleEventRepository; - public RuleEnricher(IRuleEventRepository ruleEventRepository, IRequestCache requestCache) - { - this.ruleEventRepository = ruleEventRepository; + this.requestCache = requestCache; + } - this.requestCache = requestCache; - } + public async Task<IEnrichedRuleEntity> EnrichAsync(IRuleEntity rule, Context context, + CancellationToken ct) + { + Guard.NotNull(rule); - public async Task<IEnrichedRuleEntity> EnrichAsync(IRuleEntity rule, Context context, - CancellationToken ct) - { - Guard.NotNull(rule); + var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context, ct); - var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context, ct); + return enriched[0]; + } - return enriched[0]; - } + public async Task<IReadOnlyList<IEnrichedRuleEntity>> EnrichAsync(IEnumerable<IRuleEntity> rules, Context context, + CancellationToken ct) + { + Guard.NotNull(rules); + Guard.NotNull(context); - public async Task<IReadOnlyList<IEnrichedRuleEntity>> EnrichAsync(IEnumerable<IRuleEntity> rules, Context context, - CancellationToken ct) + using (Telemetry.Activities.StartActivity("RuleEnricher/EnrichAsync")) { - Guard.NotNull(rules); - Guard.NotNull(context); + var results = new List<RuleEntity>(); - using (Telemetry.Activities.StartActivity("RuleEnricher/EnrichAsync")) + foreach (var rule in rules) { - var results = new List<RuleEntity>(); + var result = SimpleMapper.Map(rule, new RuleEntity()); - foreach (var rule in rules) - { - var result = SimpleMapper.Map(rule, new RuleEntity()); + results.Add(result); + } - results.Add(result); - } + foreach (var group in results.GroupBy(x => x.AppId.Id)) + { + var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key, ct); - foreach (var group in results.GroupBy(x => x.AppId.Id)) + foreach (var rule in group) { - var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key, ct); + requestCache.AddDependency(rule.UniqueId, rule.Version); - foreach (var rule in group) - { - requestCache.AddDependency(rule.UniqueId, rule.Version); + var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id); - var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id); - - if (statistic != null) - { - rule.LastExecuted = statistic.LastExecuted; - rule.NumFailed = statistic.NumFailed; - rule.NumSucceeded = statistic.NumSucceeded; + if (statistic != null) + { + rule.LastExecuted = statistic.LastExecuted; + rule.NumFailed = statistic.NumFailed; + rule.NumSucceeded = statistic.NumSucceeded; - requestCache.AddDependency(rule.LastExecuted); - } + requestCache.AddDependency(rule.LastExecuted); } } - - return results; } + + return results; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs index c8082d4622..50bc9cfbd9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs @@ -7,33 +7,32 @@ using Squidex.Domain.Apps.Entities.Rules.Indexes; -namespace Squidex.Domain.Apps.Entities.Rules.Queries +namespace Squidex.Domain.Apps.Entities.Rules.Queries; + +public sealed class RuleQueryService : IRuleQueryService { - public sealed class RuleQueryService : IRuleQueryService + private static readonly List<IEnrichedRuleEntity> EmptyResults = new List<IEnrichedRuleEntity>(); + private readonly IRulesIndex rulesIndex; + private readonly IRuleEnricher ruleEnricher; + + public RuleQueryService(IRulesIndex rulesIndex, IRuleEnricher ruleEnricher) { - private static readonly List<IEnrichedRuleEntity> EmptyResults = new List<IEnrichedRuleEntity>(); - private readonly IRulesIndex rulesIndex; - private readonly IRuleEnricher ruleEnricher; + this.rulesIndex = rulesIndex; + this.ruleEnricher = ruleEnricher; + } - public RuleQueryService(IRulesIndex rulesIndex, IRuleEnricher ruleEnricher) - { - this.rulesIndex = rulesIndex; - this.ruleEnricher = ruleEnricher; - } + public async Task<IReadOnlyList<IEnrichedRuleEntity>> QueryAsync(Context context, + CancellationToken ct = default) + { + var rules = await rulesIndex.GetRulesAsync(context.App.Id, ct); - public async Task<IReadOnlyList<IEnrichedRuleEntity>> QueryAsync(Context context, - CancellationToken ct = default) + if (rules.Count > 0) { - var rules = await rulesIndex.GetRulesAsync(context.App.Id, ct); - - if (rules.Count > 0) - { - var enriched = await ruleEnricher.EnrichAsync(rules, context, ct); + var enriched = await ruleEnricher.EnrichAsync(rules, context, ct); - return enriched; - } - - return EmptyResults; + return enriched; } + + return EmptyResults; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs index f376f58862..4790c9647b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs @@ -10,59 +10,58 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules.Repositories +namespace Squidex.Domain.Apps.Entities.Rules.Repositories; + +public interface IRuleEventRepository { - public interface IRuleEventRepository + async Task EnqueueAsync(RuleJob job, Exception? ex, + CancellationToken ct = default) { - async Task EnqueueAsync(RuleJob job, Exception? ex, - CancellationToken ct = default) + if (ex != null) { - if (ex != null) - { - await EnqueueAsync(job, (Instant?)null, ct); + await EnqueueAsync(job, (Instant?)null, ct); - await UpdateAsync(job, new RuleJobUpdate - { - JobResult = RuleJobResult.Failed, - ExecutionResult = RuleResult.Failed, - ExecutionDump = ex.ToString(), - Finished = job.Created - }, ct); - } - else + await UpdateAsync(job, new RuleJobUpdate { - await EnqueueAsync(job, job.Created, ct); - } + JobResult = RuleJobResult.Failed, + ExecutionResult = RuleResult.Failed, + ExecutionDump = ex.ToString(), + Finished = job.Created + }, ct); + } + else + { + await EnqueueAsync(job, job.Created, ct); } + } - Task UpdateAsync(RuleJob job, RuleJobUpdate update, - CancellationToken ct = default); + Task UpdateAsync(RuleJob job, RuleJobUpdate update, + CancellationToken ct = default); - Task EnqueueAsync(RuleJob job, Instant? nextAttempt, - CancellationToken ct = default); + Task EnqueueAsync(RuleJob job, Instant? nextAttempt, + CancellationToken ct = default); - Task EnqueueAsync(DomainId id, Instant nextAttempt, - CancellationToken ct = default); + Task EnqueueAsync(DomainId id, Instant nextAttempt, + CancellationToken ct = default); - Task CancelByEventAsync(DomainId eventId, - CancellationToken ct = default); + Task CancelByEventAsync(DomainId eventId, + CancellationToken ct = default); - Task CancelByRuleAsync(DomainId ruleId, - CancellationToken ct = default); + Task CancelByRuleAsync(DomainId ruleId, + CancellationToken ct = default); - Task CancelByAppAsync(DomainId appId, - CancellationToken ct = default); + Task CancelByAppAsync(DomainId appId, + CancellationToken ct = default); - Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, - CancellationToken ct = default); + Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, + CancellationToken ct = default); - Task<IReadOnlyList<RuleStatistics>> QueryStatisticsByAppAsync(DomainId appId, - CancellationToken ct = default); + Task<IReadOnlyList<RuleStatistics>> QueryStatisticsByAppAsync(DomainId appId, + CancellationToken ct = default); - Task<IResultList<IRuleEventEntity>> QueryByAppAsync(DomainId appId, DomainId? ruleId = null, int skip = 0, int take = 20, - CancellationToken ct = default); + Task<IResultList<IRuleEventEntity>> QueryByAppAsync(DomainId appId, DomainId? ruleId = null, int skip = 0, int take = 20, + CancellationToken ct = default); - Task<IRuleEventEntity> FindAsync(DomainId id, - CancellationToken ct = default); - } + Task<IRuleEventEntity> FindAsync(DomainId id, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs index ef0e3c5386..186ee465de 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules.Repositories +namespace Squidex.Domain.Apps.Entities.Rules.Repositories; + +public interface IRuleRepository { - public interface IRuleRepository - { - Task<List<IRuleEntity>> QueryAllAsync(DomainId appId, - CancellationToken ct = default); - } + Task<List<IRuleEntity>> QueryAllAsync(DomainId appId, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs index f9cf7bfb1b..89f17d2a43 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/RuleStatistics.cs @@ -8,18 +8,17 @@ using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules.Repositories +namespace Squidex.Domain.Apps.Entities.Rules.Repositories; + +public sealed class RuleStatistics { - public sealed class RuleStatistics - { - public DomainId AppId { get; set; } + public DomainId AppId { get; set; } - public DomainId RuleId { get; set; } + public DomainId RuleId { get; set; } - public int NumSucceeded { get; set; } + public int NumSucceeded { get; set; } - public int NumFailed { get; set; } + public int NumFailed { get; set; } - public Instant? LastExecuted { get; set; } - } + public Instant? LastExecuted { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index 910f99b07a..f03ca28fdf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -9,32 +9,31 @@ using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class RuleCommandMiddleware : AggregateCommandMiddleware<RuleCommandBase, RuleDomainObject> { - public sealed class RuleCommandMiddleware : AggregateCommandMiddleware<RuleCommandBase, RuleDomainObject> + private readonly IRuleEnricher ruleEnricher; + private readonly IContextProvider contextProvider; + + public RuleCommandMiddleware(IDomainObjectFactory domainObjectFactory, + IRuleEnricher ruleEnricher, IContextProvider contextProvider) + : base(domainObjectFactory) { - private readonly IRuleEnricher ruleEnricher; - private readonly IContextProvider contextProvider; + this.ruleEnricher = ruleEnricher; + this.contextProvider = contextProvider; + } - public RuleCommandMiddleware(IDomainObjectFactory domainObjectFactory, - IRuleEnricher ruleEnricher, IContextProvider contextProvider) - : base(domainObjectFactory) - { - this.ruleEnricher = ruleEnricher; - this.contextProvider = contextProvider; - } + protected override async Task<object> EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) + { + var payload = await base.EnrichResultAsync(context, result, ct); - protected override async Task<object> EnrichResultAsync(CommandContext context, CommandResult result, - CancellationToken ct) + if (payload is IRuleEntity rule and not IEnrichedRuleEntity) { - var payload = await base.EnrichResultAsync(context, result, ct); - - if (payload is IRuleEntity rule and not IEnrichedRuleEntity) - { - payload = await ruleEnricher.EnrichAsync(rule, contextProvider.Context, ct); - } - - return payload; + payload = await ruleEnricher.EnrichAsync(rule, contextProvider.Context, ct); } + + return payload; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerWorker.cs index 14a2e469e7..258607af6c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerWorker.cs @@ -17,147 +17,146 @@ using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Timers; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class RuleDequeuerWorker : IBackgroundProcess { - public sealed class RuleDequeuerWorker : IBackgroundProcess + private readonly ConcurrentDictionary<DomainId, bool> executing = new ConcurrentDictionary<DomainId, bool>(); + private readonly ITargetBlock<IRuleEventEntity> requestBlock; + private readonly IRuleEventRepository ruleEventRepository; + private readonly IRuleService ruleService; + private readonly ILogger<RuleDequeuerWorker> log; + private CompletionTimer timer; + + public IClock Clock { get; set; } = SystemClock.Instance; + + public RuleDequeuerWorker( + IRuleService ruleService, + IRuleEventRepository ruleEventRepository, + ILogger<RuleDequeuerWorker> log) { - private readonly ConcurrentDictionary<DomainId, bool> executing = new ConcurrentDictionary<DomainId, bool>(); - private readonly ITargetBlock<IRuleEventEntity> requestBlock; - private readonly IRuleEventRepository ruleEventRepository; - private readonly IRuleService ruleService; - private readonly ILogger<RuleDequeuerWorker> log; - private CompletionTimer timer; - - public IClock Clock { get; set; } = SystemClock.Instance; - - public RuleDequeuerWorker( - IRuleService ruleService, - IRuleEventRepository ruleEventRepository, - ILogger<RuleDequeuerWorker> log) - { - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - this.log = log; + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + this.log = log; - requestBlock = - new PartitionedActionBlock<IRuleEventEntity>(HandleAsync, x => x.Job.ExecutionPartition, - new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); - } + requestBlock = + new PartitionedActionBlock<IRuleEventEntity>(HandleAsync, x => x.Job.ExecutionPartition, + new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); + } - public Task StartAsync( - CancellationToken ct) - { - timer = new CompletionTimer((int)TimeSpan.FromSeconds(10).TotalMilliseconds, QueryAsync); + public Task StartAsync( + CancellationToken ct) + { + timer = new CompletionTimer((int)TimeSpan.FromSeconds(10).TotalMilliseconds, QueryAsync); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public async Task StopAsync( - CancellationToken ct) - { - await (timer?.StopAsync() ?? Task.CompletedTask); + public async Task StopAsync( + CancellationToken ct) + { + await (timer?.StopAsync() ?? Task.CompletedTask); - requestBlock.Complete(); + requestBlock.Complete(); - await requestBlock.Completion; - } + await requestBlock.Completion; + } - public async Task QueryAsync( - CancellationToken ct = default) + public async Task QueryAsync( + CancellationToken ct = default) + { + try { - try - { - var now = Clock.GetCurrentInstant(); + var now = Clock.GetCurrentInstant(); - await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, ct); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to query rule events."); - } + await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, ct); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to query rule events."); } + } - public async Task HandleAsync(IRuleEventEntity @event) + public async Task HandleAsync(IRuleEventEntity @event) + { + if (!executing.TryAdd(@event.Id, false)) { - if (!executing.TryAdd(@event.Id, false)) - { - return; - } + return; + } - try - { - var job = @event.Job; + try + { + var job = @event.Job; - var (response, elapsed) = await ruleService.InvokeAsync(job.ActionName, job.ActionData); + var (response, elapsed) = await ruleService.InvokeAsync(job.ActionName, job.ActionData); - var jobDelay = ComputeJobDelay(response.Status, @event, job); - var jobResult = ComputeJobResult(response.Status, jobDelay); + var jobDelay = ComputeJobDelay(response.Status, @event, job); + var jobResult = ComputeJobResult(response.Status, jobDelay); - var now = Clock.GetCurrentInstant(); + var now = Clock.GetCurrentInstant(); - var update = new RuleJobUpdate - { - Elapsed = elapsed, - ExecutionDump = response.Dump, - ExecutionResult = response.Status, - Finished = now, - JobNext = jobDelay, - JobResult = jobResult - }; + var update = new RuleJobUpdate + { + Elapsed = elapsed, + ExecutionDump = response.Dump, + ExecutionResult = response.Status, + Finished = now, + JobNext = jobDelay, + JobResult = jobResult + }; - await ruleEventRepository.UpdateAsync(@event.Job, update); + await ruleEventRepository.UpdateAsync(@event.Job, update); - if (response.Status == RuleResult.Failed) - { - log.LogWarning(response.Exception, "Failed to execute rule event with rule id {ruleId}/{description}.", - @event.Job.RuleId, - @event.Job.Description); - } - } - catch (Exception ex) + if (response.Status == RuleResult.Failed) { - log.LogError(ex, "Failed to execute rule event with internal error."); - } - finally - { - executing.TryRemove(@event.Id, out _); + log.LogWarning(response.Exception, "Failed to execute rule event with rule id {ruleId}/{description}.", + @event.Job.RuleId, + @event.Job.Description); } } + catch (Exception ex) + { + log.LogError(ex, "Failed to execute rule event with internal error."); + } + finally + { + executing.TryRemove(@event.Id, out _); + } + } - private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) + private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) + { + if (result != RuleResult.Success && nextCall == null) { - if (result != RuleResult.Success && nextCall == null) - { - return RuleJobResult.Failed; - } - else if (result != RuleResult.Success && nextCall != null) - { - return RuleJobResult.Retry; - } - else - { - return RuleJobResult.Success; - } + return RuleJobResult.Failed; + } + else if (result != RuleResult.Success && nextCall != null) + { + return RuleJobResult.Retry; } + else + { + return RuleJobResult.Success; + } + } - private static Instant? ComputeJobDelay(RuleResult result, IRuleEventEntity @event, RuleJob job) + private static Instant? ComputeJobDelay(RuleResult result, IRuleEventEntity @event, RuleJob job) + { + if (result != RuleResult.Success) { - if (result != RuleResult.Success) + switch (@event.NumCalls) { - switch (@event.NumCalls) - { - case 0: - return job.Created.Plus(Duration.FromMinutes(5)); - case 1: - return job.Created.Plus(Duration.FromHours(1)); - case 2: - return job.Created.Plus(Duration.FromHours(6)); - case 3: - return job.Created.Plus(Duration.FromHours(12)); - } + case 0: + return job.Created.Plus(Duration.FromMinutes(5)); + case 1: + return job.Created.Plus(Duration.FromHours(1)); + case 2: + return job.Created.Plus(Duration.FromHours(6)); + case 3: + return job.Created.Plus(Duration.FromHours(12)); } - - return null; } + + return null; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index f06ce51089..e902099f14 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -16,102 +16,101 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer { - public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer + private readonly IMemoryCache cache; + private readonly IRuleEventRepository ruleEventRepository; + private readonly IRuleService ruleService; + private readonly ILogger<RuleEnqueuer> log; + private readonly IAppProvider appProvider; + private readonly ILocalCache localCache; + private readonly TimeSpan cacheDuration; + + public string Name { - private readonly IMemoryCache cache; - private readonly IRuleEventRepository ruleEventRepository; - private readonly IRuleService ruleService; - private readonly ILogger<RuleEnqueuer> log; - private readonly IAppProvider appProvider; - private readonly ILocalCache localCache; - private readonly TimeSpan cacheDuration; - - public string Name - { - get => GetType().Name; - } + get => GetType().Name; + } - public RuleEnqueuer(IMemoryCache cache, ILocalCache localCache, - IAppProvider appProvider, - IRuleEventRepository ruleEventRepository, - IRuleService ruleService, - IOptions<RuleOptions> options, - ILogger<RuleEnqueuer> log) - { - this.appProvider = appProvider; - this.cache = cache; - this.cacheDuration = options.Value.RulesCacheDuration; - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - this.log = log; - this.localCache = localCache; - } + public RuleEnqueuer(IMemoryCache cache, ILocalCache localCache, + IAppProvider appProvider, + IRuleEventRepository ruleEventRepository, + IRuleService ruleService, + IOptions<RuleOptions> options, + ILogger<RuleEnqueuer> log) + { + this.appProvider = appProvider; + this.cache = cache; + this.cacheDuration = options.Value.RulesCacheDuration; + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + this.log = log; + this.localCache = localCache; + } - public async Task EnqueueAsync(Rule rule, DomainId ruleId, Envelope<IEvent> @event) - { - Guard.NotNull(rule); - Guard.NotNull(@event, nameof(@event)); + public async Task EnqueueAsync(Rule rule, DomainId ruleId, Envelope<IEvent> @event) + { + Guard.NotNull(rule); + Guard.NotNull(@event, nameof(@event)); - var ruleContext = new RuleContext - { - Rule = rule, - RuleId = ruleId - }; + var ruleContext = new RuleContext + { + Rule = rule, + RuleId = ruleId + }; - var jobs = ruleService.CreateJobsAsync(@event, ruleContext); + var jobs = ruleService.CreateJobsAsync(@event, ruleContext); - await foreach (var job in jobs) + await foreach (var job in jobs) + { + // We do not want to handle disabled rules in the normal flow. + if (job.Job != null && job.SkipReason is SkipReason.None or SkipReason.Failed) { - // We do not want to handle disabled rules in the normal flow. - if (job.Job != null && job.SkipReason is SkipReason.None or SkipReason.Failed) - { - log.LogInformation("Adding rule job {jobId} for Rule(action={ruleAction}, trigger={ruleTrigger})", job.Job.Id, - rule.Action.GetType().Name, rule.Trigger.GetType().Name); + log.LogInformation("Adding rule job {jobId} for Rule(action={ruleAction}, trigger={ruleTrigger})", job.Job.Id, + rule.Action.GetType().Name, rule.Trigger.GetType().Name); - await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError); - } + await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError); } } + } - public async Task On(Envelope<IEvent> @event) + public async Task On(Envelope<IEvent> @event) + { + if (@event.Headers.Restored()) { - if (@event.Headers.Restored()) - { - return; - } + return; + } - if (@event.Payload is AppEvent appEvent) + if (@event.Payload is AppEvent appEvent) + { + using (localCache.StartContext()) { - using (localCache.StartContext()) - { - var rules = await GetRulesAsync(appEvent.AppId.Id); + var rules = await GetRulesAsync(appEvent.AppId.Id); - foreach (var ruleEntity in rules) - { - await EnqueueAsync(ruleEntity.RuleDef, ruleEntity.Id, @event); - } + foreach (var ruleEntity in rules) + { + await EnqueueAsync(ruleEntity.RuleDef, ruleEntity.Id, @event); } } } + } - private Task<List<IRuleEntity>> GetRulesAsync(DomainId appId) + private Task<List<IRuleEntity>> GetRulesAsync(DomainId appId) + { + if (cacheDuration <= TimeSpan.Zero || cacheDuration == TimeSpan.MaxValue) { - if (cacheDuration <= TimeSpan.Zero || cacheDuration == TimeSpan.MaxValue) - { - return appProvider.GetRulesAsync(appId); - } + return appProvider.GetRulesAsync(appId); + } - var cacheKey = $"{typeof(RuleEnqueuer)}_Rules_{appId}"; + var cacheKey = $"{typeof(RuleEnqueuer)}_Rules_{appId}"; - // Cache the rules for performance reasons for a short period of time (usually 10 sec). - return cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = cacheDuration; + // Cache the rules for performance reasons for a short period of time (usually 10 sec). + return cache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = cacheDuration; - return appProvider.GetRulesAsync(appId); - }); - } + return appProvider.GetRulesAsync(appId); + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs index efd1170e2f..1e017544b6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs @@ -9,39 +9,38 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class RuleEntity : IEnrichedRuleEntity { - public sealed class RuleEntity : IEnrichedRuleEntity - { - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public NamedId<DomainId> AppId { get; set; } + public NamedId<DomainId> AppId { get; set; } - public NamedId<DomainId> SchemaId { get; set; } + public NamedId<DomainId> SchemaId { get; set; } - public long Version { get; set; } + public long Version { get; set; } - public Instant Created { get; set; } + public Instant Created { get; set; } - public Instant LastModified { get; set; } + public Instant LastModified { get; set; } - public RefToken CreatedBy { get; set; } + public RefToken CreatedBy { get; set; } - public RefToken LastModifiedBy { get; set; } + public RefToken LastModifiedBy { get; set; } - public Rule RuleDef { get; set; } + public Rule RuleDef { get; set; } - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - public int NumSucceeded { get; set; } + public int NumSucceeded { get; set; } - public int NumFailed { get; set; } + public int NumFailed { get; set; } - public Instant? LastExecuted { get; set; } + public Instant? LastExecuted { get; set; } - public DomainId UniqueId - { - get => DomainId.Combine(AppId, Id); - } + public DomainId UniqueId + { + get => DomainId.Combine(AppId, Id); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs index 6d8b40ad8f..d8ade9d57f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobResult.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public enum RuleJobResult { - public enum RuleJobResult - { - Pending, - Success, - Retry, - Failed, - Cancelled - } + Pending, + Success, + Retry, + Failed, + Cancelled } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobUpdate.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobUpdate.cs index f9971ed963..c841f6d37e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobUpdate.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleJobUpdate.cs @@ -8,20 +8,19 @@ using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public sealed class RuleJobUpdate { - public sealed class RuleJobUpdate - { - public string? ExecutionDump { get; set; } + public string? ExecutionDump { get; set; } - public RuleResult ExecutionResult { get; set; } + public RuleResult ExecutionResult { get; set; } - public RuleJobResult JobResult { get; set; } + public RuleJobResult JobResult { get; set; } - public TimeSpan Elapsed { get; set; } + public TimeSpan Elapsed { get; set; } - public Instant Finished { get; set; } + public Instant Finished { get; set; } - public Instant? JobNext { get; set; } - } + public Instant? JobNext { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs index cfa9a0d8ca..1cb19af595 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs @@ -15,141 +15,140 @@ using Squidex.Infrastructure.States; using Squidex.Messaging; -namespace Squidex.Domain.Apps.Entities.Rules.Runner +namespace Squidex.Domain.Apps.Entities.Rules.Runner; + +public sealed class DefaultRuleRunnerService : IRuleRunnerService { - public sealed class DefaultRuleRunnerService : IRuleRunnerService + private const int MaxSimulatedEvents = 100; + private readonly IPersistenceFactory<RuleRunnerState> persistenceFactory; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly IRuleService ruleService; + private readonly IMessageBus messaging; + + public DefaultRuleRunnerService( + IPersistenceFactory<RuleRunnerState> persistenceFactory, + IEventFormatter eventFormatter, + IEventStore eventStore, + IRuleService ruleService, + IMessageBus messaging) { - private const int MaxSimulatedEvents = 100; - private readonly IPersistenceFactory<RuleRunnerState> persistenceFactory; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly IRuleService ruleService; - private readonly IMessageBus messaging; - - public DefaultRuleRunnerService( - IPersistenceFactory<RuleRunnerState> persistenceFactory, - IEventFormatter eventFormatter, - IEventStore eventStore, - IRuleService ruleService, - IMessageBus messaging) - { - this.eventFormatter = eventFormatter; - this.persistenceFactory = persistenceFactory; - this.eventStore = eventStore; - this.ruleService = ruleService; - this.messaging = messaging; - } + this.eventFormatter = eventFormatter; + this.persistenceFactory = persistenceFactory; + this.eventStore = eventStore; + this.ruleService = ruleService; + this.messaging = messaging; + } - public Task<List<SimulatedRuleEvent>> SimulateAsync(IRuleEntity rule, - CancellationToken ct = default) - { - return SimulateAsync(rule.AppId, rule.Id, rule.RuleDef, ct); - } + public Task<List<SimulatedRuleEvent>> SimulateAsync(IRuleEntity rule, + CancellationToken ct = default) + { + return SimulateAsync(rule.AppId, rule.Id, rule.RuleDef, ct); + } + + public async Task<List<SimulatedRuleEvent>> SimulateAsync(NamedId<DomainId> appId, DomainId ruleId, Rule rule, + CancellationToken ct = default) + { + Guard.NotNull(rule); - public async Task<List<SimulatedRuleEvent>> SimulateAsync(NamedId<DomainId> appId, DomainId ruleId, Rule rule, - CancellationToken ct = default) + var context = new RuleContext { - Guard.NotNull(rule); + AppId = appId, + Rule = rule, + RuleId = ruleId, + IncludeSkipped = true, + IncludeStale = true + }; - var context = new RuleContext - { - AppId = appId, - Rule = rule, - RuleId = ruleId, - IncludeSkipped = true, - IncludeStale = true - }; + var simulatedEvents = new List<SimulatedRuleEvent>(MaxSimulatedEvents); - var simulatedEvents = new List<SimulatedRuleEvent>(MaxSimulatedEvents); + var fromNow = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(7)); - var fromNow = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(7)); + await foreach (var storedEvent in eventStore.QueryAllReverseAsync($"^([a-zA-Z0-9]+)\\-{appId.Id}", fromNow, MaxSimulatedEvents, ct)) + { + var @event = eventFormatter.ParseIfKnown(storedEvent); - await foreach (var storedEvent in eventStore.QueryAllReverseAsync($"^([a-zA-Z0-9]+)\\-{appId.Id}", fromNow, MaxSimulatedEvents, ct)) + if (@event?.Payload is AppEvent appEvent) { - var @event = eventFormatter.ParseIfKnown(storedEvent); - - if (@event?.Payload is AppEvent appEvent) + // Also create jobs for rules with failing conditions because we want to show them in th table. + await foreach (var result in ruleService.CreateJobsAsync(@event, context, ct).WithCancellation(ct)) { - // Also create jobs for rules with failing conditions because we want to show them in th table. - await foreach (var result in ruleService.CreateJobsAsync(@event, context, ct).WithCancellation(ct)) + var eventName = result.Job?.EventName; + + if (string.IsNullOrWhiteSpace(eventName)) { - var eventName = result.Job?.EventName; - - if (string.IsNullOrWhiteSpace(eventName)) - { - eventName = ruleService.GetName(appEvent); - } - - simulatedEvents.Add(new SimulatedRuleEvent - { - ActionData = result.Job?.ActionData, - ActionName = result.Job?.ActionName, - EnrichedEvent = result.EnrichedEvent, - Error = result.EnrichmentError?.Message, - Event = @event.Payload, - EventId = @event.Headers.EventId(), - EventName = eventName, - SkipReason = result.SkipReason - }); + eventName = ruleService.GetName(appEvent); } + + simulatedEvents.Add(new SimulatedRuleEvent + { + ActionData = result.Job?.ActionData, + ActionName = result.Job?.ActionName, + EnrichedEvent = result.EnrichedEvent, + Error = result.EnrichmentError?.Message, + Event = @event.Payload, + EventId = @event.Headers.EventId(), + EventName = eventName, + SkipReason = result.SkipReason + }); } } - - return simulatedEvents; } - public bool CanRunRule(IRuleEntity rule) - { - var context = GetContext(rule); + return simulatedEvents; + } - return context.Rule.Trigger is not ManualTrigger; - } + public bool CanRunRule(IRuleEntity rule) + { + var context = GetContext(rule); - public bool CanRunFromSnapshots(IRuleEntity rule) - { - var context = GetContext(rule); + return context.Rule.Trigger is not ManualTrigger; + } - return CanRunRule(rule) && ruleService.CanCreateSnapshotEvents(context); - } + public bool CanRunFromSnapshots(IRuleEntity rule) + { + var context = GetContext(rule); - public Task CancelAsync(DomainId appId, - CancellationToken ct = default) - { - return messaging.PublishAsync(new RuleRunnerCancel(appId), ct: ct); - } + return CanRunRule(rule) && ruleService.CanCreateSnapshotEvents(context); + } - public Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false, - CancellationToken ct = default) - { - return messaging.PublishAsync(new RuleRunnerRun(appId, ruleId, fromSnapshots), ct: ct); - } + public Task CancelAsync(DomainId appId, + CancellationToken ct = default) + { + return messaging.PublishAsync(new RuleRunnerCancel(appId), ct: ct); + } - public async Task<DomainId?> GetRunningRuleIdAsync(DomainId appId, - CancellationToken ct = default) - { - var state = await GetStateAsync(appId, ct); + public Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false, + CancellationToken ct = default) + { + return messaging.PublishAsync(new RuleRunnerRun(appId, ruleId, fromSnapshots), ct: ct); + } - return state.Value.RuleId; - } + public async Task<DomainId?> GetRunningRuleIdAsync(DomainId appId, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, ct); - private async Task<SimpleState<RuleRunnerState>> GetStateAsync(DomainId appId, - CancellationToken ct) - { - var state = new SimpleState<RuleRunnerState>(persistenceFactory, GetType(), appId); + return state.Value.RuleId; + } - await state.LoadAsync(ct); + private async Task<SimpleState<RuleRunnerState>> GetStateAsync(DomainId appId, + CancellationToken ct) + { + var state = new SimpleState<RuleRunnerState>(persistenceFactory, GetType(), appId); - return state; - } + await state.LoadAsync(ct); - private static RuleContext GetContext(IRuleEntity rule) + return state; + } + + private static RuleContext GetContext(IRuleEntity rule) + { + return new RuleContext { - return new RuleContext - { - AppId = rule.AppId, - Rule = rule.RuleDef, - RuleId = rule.Id - }; - } + AppId = rule.AppId, + Rule = rule.RuleDef, + RuleId = rule.Id + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs index 742f313045..77beaea7aa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs @@ -8,27 +8,26 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules.Runner +namespace Squidex.Domain.Apps.Entities.Rules.Runner; + +public interface IRuleRunnerService { - public interface IRuleRunnerService - { - Task<List<SimulatedRuleEvent>> SimulateAsync(NamedId<DomainId> appId, DomainId ruleId, Rule rule, - CancellationToken ct = default); + Task<List<SimulatedRuleEvent>> SimulateAsync(NamedId<DomainId> appId, DomainId ruleId, Rule rule, + CancellationToken ct = default); - Task<List<SimulatedRuleEvent>> SimulateAsync(IRuleEntity rule, - CancellationToken ct = default); + Task<List<SimulatedRuleEvent>> SimulateAsync(IRuleEntity rule, + CancellationToken ct = default); - Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false, - CancellationToken ct = default); + Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false, + CancellationToken ct = default); - Task CancelAsync(DomainId appId, - CancellationToken ct = default); + Task CancelAsync(DomainId appId, + CancellationToken ct = default); - Task<DomainId?> GetRunningRuleIdAsync(DomainId appId, - CancellationToken ct = default); + Task<DomainId?> GetRunningRuleIdAsync(DomainId appId, + CancellationToken ct = default); - bool CanRunRule(IRuleEntity rule); + bool CanRunRule(IRuleEntity rule); - bool CanRunFromSnapshots(IRuleEntity rule); - } + bool CanRunFromSnapshots(IRuleEntity rule); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/Messages.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/Messages.cs index c8296df128..7f5575708e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/Messages.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/Messages.cs @@ -10,9 +10,8 @@ #pragma warning disable MA0048 // File name must match type name #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Rules.Runner -{ - public sealed record RuleRunnerRun(DomainId AppId, DomainId RuleId, bool FromSnapshots); +namespace Squidex.Domain.Apps.Entities.Rules.Runner; - public sealed record RuleRunnerCancel(DomainId AppId); -} +public sealed record RuleRunnerRun(DomainId AppId, DomainId RuleId, bool FromSnapshots); + +public sealed record RuleRunnerCancel(DomainId AppId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerProcessor.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerProcessor.cs index b33b29dbea..95bdb69218 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerProcessor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerProcessor.cs @@ -16,275 +16,274 @@ using Squidex.Infrastructure.Translations; using TaskHelper = Squidex.Infrastructure.Tasks.TaskExtensions; -namespace Squidex.Domain.Apps.Entities.Rules.Runner +namespace Squidex.Domain.Apps.Entities.Rules.Runner; + +public sealed class RuleRunnerProcessor { - public sealed class RuleRunnerProcessor + private const int MaxErrors = 10; + private readonly IAppProvider appProvider; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly ILocalCache localCache; + private readonly IRuleEventRepository ruleEventRepository; + private readonly IRuleService ruleService; + private readonly ILogger<RuleRunnerProcessor> log; + private readonly SimpleState<RuleRunnerState> state; + private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); + private readonly DomainId appId; + private Run? currentRun; + + // Use a run to store all state that is necessary for a single run. + private sealed class Run : IDisposable { - private const int MaxErrors = 10; - private readonly IAppProvider appProvider; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly ILocalCache localCache; - private readonly IRuleEventRepository ruleEventRepository; - private readonly IRuleService ruleService; - private readonly ILogger<RuleRunnerProcessor> log; - private readonly SimpleState<RuleRunnerState> state; - private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); - private readonly DomainId appId; - private Run? currentRun; - - // Use a run to store all state that is necessary for a single run. - private sealed class Run : IDisposable - { - private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); - private readonly CancellationTokenSource cancellationLinked; + private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly CancellationTokenSource cancellationLinked; - public RuleRunnerState Job { get; init; } + public RuleRunnerState Job { get; init; } - public RuleContext Context { get; set; } + public RuleContext Context { get; set; } - public CancellationToken CancellationToken => cancellationLinked.Token; - - public Run(CancellationToken ct) - { - cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); - } - - public void Dispose() - { - cancellationSource.Dispose(); - cancellationLinked.Dispose(); - } + public CancellationToken CancellationToken => cancellationLinked.Token; - public void Cancel() - { - try - { - cancellationSource.Cancel(); - } - catch (ObjectDisposedException) - { - // Cancellation token might have been disposed, if the run is completed. - } - } + public Run(CancellationToken ct) + { + cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); } - public RuleRunnerProcessor( - DomainId appId, - IAppProvider appProvider, - IEventFormatter eventFormatter, - IEventStore eventStore, - ILocalCache localCache, - IPersistenceFactory<RuleRunnerState> persistenceFactory, - IRuleEventRepository ruleEventRepository, - IRuleService ruleService, - ILogger<RuleRunnerProcessor> log) + public void Dispose() { - this.appId = appId; - this.appProvider = appProvider; - this.localCache = localCache; - this.eventStore = eventStore; - this.eventFormatter = eventFormatter; - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - this.log = log; - - state = new SimpleState<RuleRunnerState>(persistenceFactory, GetType(), appId); + cancellationSource.Dispose(); + cancellationLinked.Dispose(); } - public async Task LoadAsync( - CancellationToken ct = default) + public void Cancel() { - await state.LoadAsync(ct); - - if (!state.Value.RunFromSnapshots && state.Value.RuleId != default) + try { - TaskHelper.Forget(RunAsync(state.Value.RuleId, false, default)); + cancellationSource.Cancel(); } - else + catch (ObjectDisposedException) { - await state.ClearAsync(ct); + // Cancellation token might have been disposed, if the run is completed. } } + } + + public RuleRunnerProcessor( + DomainId appId, + IAppProvider appProvider, + IEventFormatter eventFormatter, + IEventStore eventStore, + ILocalCache localCache, + IPersistenceFactory<RuleRunnerState> persistenceFactory, + IRuleEventRepository ruleEventRepository, + IRuleService ruleService, + ILogger<RuleRunnerProcessor> log) + { + this.appId = appId; + this.appProvider = appProvider; + this.localCache = localCache; + this.eventStore = eventStore; + this.eventFormatter = eventFormatter; + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + this.log = log; + + state = new SimpleState<RuleRunnerState>(persistenceFactory, GetType(), appId); + } - public Task CancelAsync() + public async Task LoadAsync( + CancellationToken ct = default) + { + await state.LoadAsync(ct); + + if (!state.Value.RunFromSnapshots && state.Value.RuleId != default) { - // Ensure that only one thread is accessing the current state at a time. - return scheduler.Schedule(() => - { - currentRun?.Cancel(); - }); + TaskHelper.Forget(RunAsync(state.Value.RuleId, false, default)); } - - public Task RunAsync(DomainId ruleId, bool fromSnapshots, - CancellationToken ct) + else { - return scheduler.ScheduleAsync(async ct => - { - // There is no continuation token for snapshots, therefore we cannot continue with the run. - if (currentRun?.Job.RunFromSnapshots == true) - { - throw new DomainException(T.Get("rules.ruleAlreadyRunning")); - } + await state.ClearAsync(ct); + } + } - var previousJob = state.Value; + public Task CancelAsync() + { + // Ensure that only one thread is accessing the current state at a time. + return scheduler.Schedule(() => + { + currentRun?.Cancel(); + }); + } - // If we have not removed the state, we have not completed the previous run and can therefore just continue. - var position = - previousJob.RuleId == ruleId && !previousJob.RunFromSnapshots ? - previousJob.Position : - null; + public Task RunAsync(DomainId ruleId, bool fromSnapshots, + CancellationToken ct) + { + return scheduler.ScheduleAsync(async ct => + { + // There is no continuation token for snapshots, therefore we cannot continue with the run. + if (currentRun?.Job.RunFromSnapshots == true) + { + throw new DomainException(T.Get("rules.ruleAlreadyRunning")); + } - // Set the current run first to indicate that we are running a rule at the moment. - var run = currentRun = new Run(ct) - { - Job = new RuleRunnerState - { - RuleId = ruleId, - RunId = DomainId.NewGuid(), - RunFromSnapshots = fromSnapshots, - Position = position - } - }; + var previousJob = state.Value; - state.Value = run.Job; - try - { - await state.WriteAsync(run.CancellationToken); + // If we have not removed the state, we have not completed the previous run and can therefore just continue. + var position = + previousJob.RuleId == ruleId && !previousJob.RunFromSnapshots ? + previousJob.Position : + null; - await ProcessAsync(run, run.CancellationToken); - } - finally + // Set the current run first to indicate that we are running a rule at the moment. + var run = currentRun = new Run(ct) + { + Job = new RuleRunnerState { - // Unset the run to indicate that we are done. - currentRun.Dispose(); - currentRun = null; + RuleId = ruleId, + RunId = DomainId.NewGuid(), + RunFromSnapshots = fromSnapshots, + Position = position } - }, ct); - } + }; - private async Task ProcessAsync(Run run, - CancellationToken ct) - { + state.Value = run.Job; try { - var rule = await appProvider.GetRuleAsync(appId, run.Job.RuleId, ct); - - // The rule might have been deleted in the meantime. - if (rule == null) - { - throw new DomainObjectNotFoundException(run.Job.RuleId.ToString()!); - } + await state.WriteAsync(run.CancellationToken); - using (localCache.StartContext()) - { - // Also run disabled rules, because we want to enable rules to be only used with manual trigger. - run.Context = new RuleContext - { - AppId = rule.AppId, - Rule = rule.RuleDef, - RuleId = rule.Id, - IncludeStale = true, - IncludeSkipped = true - }; - - if (run.Job.RunFromSnapshots && ruleService.CanCreateSnapshotEvents(run.Context)) - { - await EnqueueFromSnapshotsAsync(run, ct); - } - else - { - await EnqueueFromEventsAsync(run, ct); - } - } - } - catch (OperationCanceledException) - { - return; - } - catch (Exception ex) - { - log.LogError(ex, "Failed to run rule with ID {ruleId}.", run.Job.RuleId); + await ProcessAsync(run, run.CancellationToken); } finally { - // Remove the state to indicate that the run has been completed. - await state.ClearAsync(default); + // Unset the run to indicate that we are done. + currentRun.Dispose(); + currentRun = null; } - } + }, ct); + } - private async Task EnqueueFromSnapshotsAsync(Run run, - CancellationToken ct) + private async Task ProcessAsync(Run run, + CancellationToken ct) + { + try { - // We collect errors and allow a few erors before we throw an exception. - var errors = 0; + var rule = await appProvider.GetRuleAsync(appId, run.Job.RuleId, ct); - await foreach (var job in ruleService.CreateSnapshotJobsAsync(run.Context, ct)) + // The rule might have been deleted in the meantime. + if (rule == null) { - if (job.Job != null && job.SkipReason is SkipReason.None or SkipReason.Disabled) + throw new DomainObjectNotFoundException(run.Job.RuleId.ToString()!); + } + + using (localCache.StartContext()) + { + // Also run disabled rules, because we want to enable rules to be only used with manual trigger. + run.Context = new RuleContext { - await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); + AppId = rule.AppId, + Rule = rule.RuleDef, + RuleId = rule.Id, + IncludeStale = true, + IncludeSkipped = true + }; + + if (run.Job.RunFromSnapshots && ruleService.CanCreateSnapshotEvents(run.Context)) + { + await EnqueueFromSnapshotsAsync(run, ct); } - else if (job.EnrichmentError != null) + else { - errors++; + await EnqueueFromEventsAsync(run, ct); + } + } + } + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) + { + log.LogError(ex, "Failed to run rule with ID {ruleId}.", run.Job.RuleId); + } + finally + { + // Remove the state to indicate that the run has been completed. + await state.ClearAsync(default); + } + } - if (errors >= MaxErrors) - { - throw job.EnrichmentError; - } + private async Task EnqueueFromSnapshotsAsync(Run run, + CancellationToken ct) + { + // We collect errors and allow a few erors before we throw an exception. + var errors = 0; - log.LogWarning(job.EnrichmentError, "Failed to run rule with ID {ruleId}, continue with next job.", run.Context.RuleId); + await foreach (var job in ruleService.CreateSnapshotJobsAsync(run.Context, ct)) + { + if (job.Job != null && job.SkipReason is SkipReason.None or SkipReason.Disabled) + { + await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); + } + else if (job.EnrichmentError != null) + { + errors++; + + if (errors >= MaxErrors) + { + throw job.EnrichmentError; } + + log.LogWarning(job.EnrichmentError, "Failed to run rule with ID {ruleId}, continue with next job.", run.Context.RuleId); } } + } - private async Task EnqueueFromEventsAsync(Run run, - CancellationToken ct) - { - // We collect errors and allow a few erors before we throw an exception. - var errors = 0; + private async Task EnqueueFromEventsAsync(Run run, + CancellationToken ct) + { + // We collect errors and allow a few erors before we throw an exception. + var errors = 0; - // Use a prefix query so that the storage can use an index for the query. - var filter = $"^([a-z]+)\\-{appId}"; + // Use a prefix query so that the storage can use an index for the query. + var filter = $"^([a-z]+)\\-{appId}"; - await foreach (var storedEvent in eventStore.QueryAllAsync(filter, run.Job.Position, ct: ct)) + await foreach (var storedEvent in eventStore.QueryAllAsync(filter, run.Job.Position, ct: ct)) + { + try { - try + var @event = eventFormatter.ParseIfKnown(storedEvent); + + if (@event != null) { - var @event = eventFormatter.ParseIfKnown(storedEvent); + var jobs = ruleService.CreateJobsAsync(@event, run.Context, ct); - if (@event != null) + await foreach (var job in jobs.WithCancellation(ct)) { - var jobs = ruleService.CreateJobsAsync(@event, run.Context, ct); - - await foreach (var job in jobs.WithCancellation(ct)) + if (job.Job != null && job.SkipReason is SkipReason.None or SkipReason.Disabled) { - if (job.Job != null && job.SkipReason is SkipReason.None or SkipReason.Disabled) - { - await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); - } + await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); } } } - catch (Exception ex) - { - errors++; - - if (errors >= MaxErrors) - { - throw; - } + } + catch (Exception ex) + { + errors++; - log.LogWarning(ex, "Failed to run rule with ID {ruleId}, continue with next job.", run.Context.RuleId); - } - finally + if (errors >= MaxErrors) { - run.Job.Position = storedEvent.EventPosition; + throw; } - await state.WriteAsync(ct); + log.LogWarning(ex, "Failed to run rule with ID {ruleId}, continue with next job.", run.Context.RuleId); + } + finally + { + run.Job.Position = storedEvent.EventPosition; } + + await state.WriteAsync(ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerState.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerState.cs index 2a9f1fdcd3..b26fae1beb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerState.cs @@ -8,17 +8,16 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Rules.Runner +namespace Squidex.Domain.Apps.Entities.Rules.Runner; + +[CollectionName("Rules_Runner")] +public sealed class RuleRunnerState { - [CollectionName("Rules_Runner")] - public sealed class RuleRunnerState - { - public DomainId RuleId { get; set; } + public DomainId RuleId { get; set; } - public DomainId RunId { get; set; } + public DomainId RunId { get; set; } - public string? Position { get; set; } + public string? Position { get; set; } - public bool RunFromSnapshots { get; set; } - } + public bool RunFromSnapshots { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerWorker.cs index ff01968921..6edbcd749b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerWorker.cs @@ -11,69 +11,68 @@ using Squidex.Infrastructure.States; using Squidex.Messaging; -namespace Squidex.Domain.Apps.Entities.Rules.Runner +namespace Squidex.Domain.Apps.Entities.Rules.Runner; + +public sealed class RuleRunnerWorker : + IBackgroundProcess, + IMessageHandler<RuleRunnerRun>, + IMessageHandler<RuleRunnerCancel> { - public sealed class RuleRunnerWorker : - IBackgroundProcess, - IMessageHandler<RuleRunnerRun>, - IMessageHandler<RuleRunnerCancel> + private readonly Dictionary<DomainId, Task<RuleRunnerProcessor>> processors = new Dictionary<DomainId, Task<RuleRunnerProcessor>>(); + private readonly Func<DomainId, RuleRunnerProcessor> processorFactory; + private readonly ISnapshotStore<RuleRunnerState> snapshotStore; + + public RuleRunnerWorker(IServiceProvider serviceProvider, ISnapshotStore<RuleRunnerState> snapshotStore) { - private readonly Dictionary<DomainId, Task<RuleRunnerProcessor>> processors = new Dictionary<DomainId, Task<RuleRunnerProcessor>>(); - private readonly Func<DomainId, RuleRunnerProcessor> processorFactory; - private readonly ISnapshotStore<RuleRunnerState> snapshotStore; + var objectFactory = ActivatorUtilities.CreateFactory(typeof(RuleRunnerProcessor), new[] { typeof(DomainId) }); - public RuleRunnerWorker(IServiceProvider serviceProvider, ISnapshotStore<RuleRunnerState> snapshotStore) + processorFactory = key => { - var objectFactory = ActivatorUtilities.CreateFactory(typeof(RuleRunnerProcessor), new[] { typeof(DomainId) }); + return (RuleRunnerProcessor)objectFactory(serviceProvider, new object[] { key }); + }; - processorFactory = key => - { - return (RuleRunnerProcessor)objectFactory(serviceProvider, new object[] { key }); - }; - - this.snapshotStore = snapshotStore; - } + this.snapshotStore = snapshotStore; + } - public async Task StartAsync( - CancellationToken ct) + public async Task StartAsync( + CancellationToken ct) + { + await foreach (var snapshot in snapshotStore.ReadAllAsync(ct)) { - await foreach (var snapshot in snapshotStore.ReadAllAsync(ct)) - { - await GetProcessorAsync(snapshot.Key, ct); - } + await GetProcessorAsync(snapshot.Key, ct); } + } - public async Task HandleAsync(RuleRunnerRun message, - CancellationToken ct) - { - var processor = await GetProcessorAsync(message.AppId, ct); + public async Task HandleAsync(RuleRunnerRun message, + CancellationToken ct) + { + var processor = await GetProcessorAsync(message.AppId, ct); - await processor.RunAsync(message.RuleId, message.FromSnapshots, ct); - } + await processor.RunAsync(message.RuleId, message.FromSnapshots, ct); + } - public async Task HandleAsync(RuleRunnerCancel message, - CancellationToken ct) - { - var processor = await GetProcessorAsync(message.AppId, ct); + public async Task HandleAsync(RuleRunnerCancel message, + CancellationToken ct) + { + var processor = await GetProcessorAsync(message.AppId, ct); - await processor.CancelAsync(); - } + await processor.CancelAsync(); + } - private Task<RuleRunnerProcessor> GetProcessorAsync(DomainId appId, - CancellationToken ct) + private Task<RuleRunnerProcessor> GetProcessorAsync(DomainId appId, + CancellationToken ct) + { + // Use a normal dictionary to avoid double creations. + lock (processors) { - // Use a normal dictionary to avoid double creations. - lock (processors) + return processors.GetOrAdd(appId, async key => { - return processors.GetOrAdd(appId, async key => - { - var processor = processorFactory(key); + var processor = processorFactory(key); - await processor.LoadAsync(ct); + await processor.LoadAsync(ct); - return processor; - }); - } + return processor; + }); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs index 31ac6b9ba0..a2e262e170 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs @@ -7,24 +7,23 @@ using Squidex.Domain.Apps.Core.HandleRules; -namespace Squidex.Domain.Apps.Entities.Rules.Runner +namespace Squidex.Domain.Apps.Entities.Rules.Runner; + +public sealed record SimulatedRuleEvent { - public sealed record SimulatedRuleEvent - { - public Guid EventId { get; init; } + public Guid EventId { get; init; } - public string EventName { get; init; } + public string EventName { get; init; } - public object Event { get; init; } + public object Event { get; init; } - public object? EnrichedEvent { get; init; } + public object? EnrichedEvent { get; init; } - public string? ActionName { get; init; } + public string? ActionName { get; init; } - public string? ActionData { get; init; } + public string? ActionData { get; init; } - public string? Error { get; init; } + public string? Error { get; init; } - public SkipReason SkipReason { get; init; } - } + public SkipReason SkipReason { get; init; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/Messages.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/Messages.cs index 00f5c70bef..417de57a83 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/Messages.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/Messages.cs @@ -10,16 +10,15 @@ #pragma warning disable MA0048 // File name must match type name #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public sealed record UsageTrackingAdd(DomainId RuleId, NamedId<DomainId> AppId, int Limits, int? NumDays) - : UsageTrackingMessage(RuleId); +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking; - public sealed record UsageTrackingRemove(DomainId RuleId) - : UsageTrackingMessage(RuleId); +public sealed record UsageTrackingAdd(DomainId RuleId, NamedId<DomainId> AppId, int Limits, int? NumDays) + : UsageTrackingMessage(RuleId); - public sealed record UsageTrackingUpdate(DomainId RuleId, int Limits, int? NumDays) - : UsageTrackingMessage(RuleId); +public sealed record UsageTrackingRemove(DomainId RuleId) + : UsageTrackingMessage(RuleId); - public abstract record UsageTrackingMessage(DomainId RuleId); -} +public sealed record UsageTrackingUpdate(DomainId RuleId, int Limits, int? NumDays) + : UsageTrackingMessage(RuleId); + +public abstract record UsageTrackingMessage(DomainId RuleId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs index 8537a5bebf..e5482a14b4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -10,34 +10,33 @@ using Squidex.Infrastructure.Commands; using Squidex.Messaging; -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking; + +public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware { - public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware + private readonly IMessageBus messaging; + + public UsageTrackerCommandMiddleware(IMessageBus messaging) { - private readonly IMessageBus messaging; + this.messaging = messaging; + } - public UsageTrackerCommandMiddleware(IMessageBus messaging) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + switch (context.Command) { - this.messaging = messaging; + case DeleteRule deleteRule: + await messaging.PublishAsync(new UsageTrackingRemove(deleteRule.RuleId), ct: default); + break; + case CreateRule { Trigger: UsageTrigger usage } createRule: + await messaging.PublishAsync(new UsageTrackingAdd(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays), ct: default); + break; + case UpdateRule { Trigger: UsageTrigger usage } ruleUpdated: + await messaging.PublishAsync(new UsageTrackingUpdate(ruleUpdated.RuleId, usage.Limit, usage.NumDays), ct: default); + break; } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - switch (context.Command) - { - case DeleteRule deleteRule: - await messaging.PublishAsync(new UsageTrackingRemove(deleteRule.RuleId), ct: default); - break; - case CreateRule { Trigger: UsageTrigger usage } createRule: - await messaging.PublishAsync(new UsageTrackingAdd(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays), ct: default); - break; - case UpdateRule { Trigger: UsageTrigger usage } ruleUpdated: - await messaging.PublishAsync(new UsageTrackingUpdate(ruleUpdated.RuleId, usage.Limit, usage.NumDays), ct: default); - break; - } - - await next(context, ct); - } + await next(context, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerWorker.cs index 7e8f3dc9c3..94813b8e92 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerWorker.cs @@ -14,162 +14,161 @@ using Squidex.Infrastructure.UsageTracking; using Squidex.Messaging; -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public sealed class UsageTrackerWorker : IMessageHandler<UsageTrackingMessage>, IBackgroundProcess - { - private readonly SimpleState<State> state; - private readonly IApiUsageTracker usageTracker; - private CompletionTimer timer; +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking; - public sealed class Target - { - public NamedId<DomainId> AppId { get; set; } - - public int Limits { get; set; } +public sealed class UsageTrackerWorker : IMessageHandler<UsageTrackingMessage>, IBackgroundProcess +{ + private readonly SimpleState<State> state; + private readonly IApiUsageTracker usageTracker; + private CompletionTimer timer; - public int? NumDays { get; set; } + public sealed class Target + { + public NamedId<DomainId> AppId { get; set; } - public DateTime? Triggered { get; set; } + public int Limits { get; set; } - public Target SetApp(NamedId<DomainId> appId) - { - AppId = appId; + public int? NumDays { get; set; } - return this; - } + public DateTime? Triggered { get; set; } - public Target SetLimit(int value) - { - Limits = value; - - return this; - } - - public Target SetNumDays(int? value) - { - NumDays = value; + public Target SetApp(NamedId<DomainId> appId) + { + AppId = appId; - return this; - } + return this; } - [CollectionName("UsageTracker")] - public sealed class State + public Target SetLimit(int value) { - public Dictionary<DomainId, Target> Targets { get; set; } = new Dictionary<DomainId, Target>(); + Limits = value; + + return this; } - public UsageTrackerWorker(IPersistenceFactory<State> persistenceFactory, IApiUsageTracker usageTracker) + public Target SetNumDays(int? value) { - this.usageTracker = usageTracker; + NumDays = value; - state = new SimpleState<State>(persistenceFactory, GetType(), "Default"); + return this; } + } - public async Task StartAsync( - CancellationToken ct) - { - await state.LoadAsync(ct); + [CollectionName("UsageTracker")] + public sealed class State + { + public Dictionary<DomainId, Target> Targets { get; set; } = new Dictionary<DomainId, Target>(); + } - timer = new CompletionTimer((int)TimeSpan.FromMinutes(10).TotalMilliseconds, _ => CheckUsagesAsync()); - } + public UsageTrackerWorker(IPersistenceFactory<State> persistenceFactory, IApiUsageTracker usageTracker) + { + this.usageTracker = usageTracker; - public Task StopAsync( - CancellationToken ct) - { - return timer?.StopAsync() ?? Task.CompletedTask; - } + state = new SimpleState<State>(persistenceFactory, GetType(), "Default"); + } + + public async Task StartAsync( + CancellationToken ct) + { + await state.LoadAsync(ct); + + timer = new CompletionTimer((int)TimeSpan.FromMinutes(10).TotalMilliseconds, _ => CheckUsagesAsync()); + } + + public Task StopAsync( + CancellationToken ct) + { + return timer?.StopAsync() ?? Task.CompletedTask; + } + + public async Task CheckUsagesAsync() + { + var today = DateTime.Today; - public async Task CheckUsagesAsync() + foreach (var (key, target) in state.Value.Targets) { - var today = DateTime.Today; + var from = GetFromDate(today, target.NumDays); - foreach (var (key, target) in state.Value.Targets) + if (target.Triggered == null || target.Triggered < from) { - var from = GetFromDate(today, target.NumDays); + var costs = await usageTracker.GetMonthCallsAsync(target.AppId.Id.ToString(), today, null); - if (target.Triggered == null || target.Triggered < from) - { - var costs = await usageTracker.GetMonthCallsAsync(target.AppId.Id.ToString(), today, null); + var limit = target.Limits; - var limit = target.Limits; + if (costs > limit) + { + target.Triggered = today; - if (costs > limit) + var @event = new AppUsageExceeded { - target.Triggered = today; - - var @event = new AppUsageExceeded - { - AppId = target.AppId, - CallsCurrent = costs, - CallsLimit = limit, - RuleId = key - }; - - await state.WriteEventAsync(Envelope.Create<IEvent>(@event)); - } + AppId = target.AppId, + CallsCurrent = costs, + CallsLimit = limit, + RuleId = key + }; + + await state.WriteEventAsync(Envelope.Create<IEvent>(@event)); } } - - await state.WriteAsync(); } - private static DateTime GetFromDate(DateTime today, int? numDays) + await state.WriteAsync(); + } + + private static DateTime GetFromDate(DateTime today, int? numDays) + { + if (numDays != null) { - if (numDays != null) - { - return today.AddDays(-numDays.Value).AddDays(1); - } - else - { - return new DateTime(today.Year, today.Month, 1); - } + return today.AddDays(-numDays.Value).AddDays(1); } - - public Task HandleAsync(UsageTrackingMessage message, - CancellationToken ct) + else { - switch (message) - { - case UsageTrackingAdd add: - return HandleAsync(add, ct); - case UsageTrackingRemove remove: - return HandleAsync(remove, ct); - case UsageTrackingUpdate update: - return HandleAsync(update, ct); - default: - return Task.CompletedTask; - } + return new DateTime(today.Year, today.Month, 1); } + } - public Task HandleAsync(UsageTrackingAdd message, - CancellationToken ct) + public Task HandleAsync(UsageTrackingMessage message, + CancellationToken ct) + { + switch (message) { - UpdateTarget(message.RuleId, t => t.SetApp(message.AppId).SetLimit(message.Limits).SetNumDays(message.NumDays)); - - return state.WriteAsync(ct); + case UsageTrackingAdd add: + return HandleAsync(add, ct); + case UsageTrackingRemove remove: + return HandleAsync(remove, ct); + case UsageTrackingUpdate update: + return HandleAsync(update, ct); + default: + return Task.CompletedTask; } + } - public Task HandleAsync(UsageTrackingUpdate message, - CancellationToken ct) - { - UpdateTarget(message.RuleId, t => t.SetLimit(message.Limits).SetNumDays(message.NumDays)); + public Task HandleAsync(UsageTrackingAdd message, + CancellationToken ct) + { + UpdateTarget(message.RuleId, t => t.SetApp(message.AppId).SetLimit(message.Limits).SetNumDays(message.NumDays)); - return state.WriteAsync(ct); - } + return state.WriteAsync(ct); + } - public Task HandleAsync(UsageTrackingRemove message, - CancellationToken ct) - { - state.Value.Targets.Remove(message.RuleId); + public Task HandleAsync(UsageTrackingUpdate message, + CancellationToken ct) + { + UpdateTarget(message.RuleId, t => t.SetLimit(message.Limits).SetNumDays(message.NumDays)); - return state.WriteAsync(ct); - } + return state.WriteAsync(ct); + } - private void UpdateTarget(DomainId ruleId, Action<Target> updater) - { - updater(state.Value.Targets.GetOrAddNew(ruleId)); - } + public Task HandleAsync(UsageTrackingRemove message, + CancellationToken ct) + { + state.Value.Targets.Remove(message.RuleId); + + return state.WriteAsync(ct); + } + + private void UpdateTarget(DomainId ruleId, Action<Target> updater) + { + updater(state.Value.Targets.GetOrAddNew(ruleId)); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs index f1ae588fb9..76c646ea1c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -12,43 +12,42 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking; + +public sealed class UsageTriggerHandler : IRuleTriggerHandler { - public sealed class UsageTriggerHandler : IRuleTriggerHandler - { - private const string EventName = "Usage exceeded"; + private const string EventName = "Usage exceeded"; - public Type TriggerType => typeof(UsageTrigger); + public Type TriggerType => typeof(UsageTrigger); - public bool Handles(AppEvent appEvent) - { - return appEvent is AppUsageExceeded; - } + public bool Handles(AppEvent appEvent) + { + return appEvent is AppUsageExceeded; + } - public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct) - { - var usageEvent = (AppUsageExceeded)@event.Payload; + public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, + [EnumeratorCancellation] CancellationToken ct) + { + var usageEvent = (AppUsageExceeded)@event.Payload; - var result = new EnrichedUsageExceededEvent - { - CallsCurrent = usageEvent.CallsCurrent, - CallsLimit = usageEvent.CallsLimit, - Name = EventName - }; + var result = new EnrichedUsageExceededEvent + { + CallsCurrent = usageEvent.CallsCurrent, + CallsLimit = usageEvent.CallsLimit, + Name = EventName + }; - await Task.Yield(); + await Task.Yield(); - yield return result; - } + yield return result; + } - public bool Trigger(Envelope<AppEvent> @event, RuleContext context) - { - var trigger = (UsageTrigger)context.Rule.Trigger; + public bool Trigger(Envelope<AppEvent> @event, RuleContext context) + { + var trigger = (UsageTrigger)context.Rule.Trigger; - var usageEvent = (AppUsageExceeded)@event.Payload; + var usageEvent = (AppUsageExceeded)@event.Payload; - return usageEvent.CallsLimit >= trigger.Limit; - } + return usageEvent.CallsLimit >= trigger.Limit; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs index 0ccefbf265..36672c6bee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs @@ -12,44 +12,43 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public sealed class BackupSchemas : IBackupHandler { - public sealed class BackupSchemas : IBackupHandler - { - private const int BatchSize = 100; - private readonly HashSet<DomainId> schemaIds = new HashSet<DomainId>(); - private readonly Rebuilder rebuilder; + private const int BatchSize = 100; + private readonly HashSet<DomainId> schemaIds = new HashSet<DomainId>(); + private readonly Rebuilder rebuilder; - public string Name { get; } = "Schemas"; + public string Name { get; } = "Schemas"; - public BackupSchemas(Rebuilder rebuilder) - { - this.rebuilder = rebuilder; - } + public BackupSchemas(Rebuilder rebuilder) + { + this.rebuilder = rebuilder; + } - public Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, - CancellationToken ct) + public Task<bool> RestoreEventAsync(Envelope<IEvent> @event, RestoreContext context, + CancellationToken ct) + { + switch (@event.Payload) { - switch (@event.Payload) - { - case SchemaCreated: - schemaIds.Add(@event.Headers.AggregateId()); - break; - case SchemaDeleted: - schemaIds.Remove(@event.Headers.AggregateId()); - break; - } - - return Task.FromResult(true); + case SchemaCreated: + schemaIds.Add(@event.Headers.AggregateId()); + break; + case SchemaDeleted: + schemaIds.Remove(@event.Headers.AggregateId()); + break; } - public async Task RestoreAsync(RestoreContext context, - CancellationToken ct) + return Task.FromResult(true); + } + + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) + { + if (schemaIds.Count > 0) { - if (schemaIds.Count > 0) - { - await rebuilder.InsertManyAsync<SchemaDomainObject, SchemaDomainObject.State>(schemaIds, BatchSize, ct); - } + await rebuilder.InsertManyAsync<SchemaDomainObject, SchemaDomainObject.State>(schemaIds, BatchSize, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs index 12b7c9f9cc..7643260861 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs @@ -7,14 +7,13 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class AddField : ParentFieldCommand { - public sealed class AddField : ParentFieldCommand - { - public string Name { get; set; } + public string Name { get; set; } - public string Partitioning { get; set; } + public string Partitioning { get; set; } - public FieldProperties Properties { get; set; } - } + public FieldProperties Properties { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs index 5f13caed30..0aa5ea1822 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class ChangeCategory : SchemaCommand { - public sealed class ChangeCategory : SchemaCommand - { - public string? Name { get; set; } - } + public string? Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs index b21fe6764d..d538ee8675 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs @@ -7,22 +7,21 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class ConfigureFieldRules : SchemaCommand { - public sealed class ConfigureFieldRules : SchemaCommand - { - public FieldRuleCommand[]? FieldRules { get; set; } + public FieldRuleCommand[]? FieldRules { get; set; } - public FieldRules ToFieldRules() + public FieldRules ToFieldRules() + { + if (FieldRules?.Length > 0) + { + return new FieldRules(FieldRules.Select(x => x.ToFieldRule()).ToList()); + } + else { - if (FieldRules?.Length > 0) - { - return new FieldRules(FieldRules.Select(x => x.ToFieldRule()).ToList()); - } - else - { - return Core.Schemas.FieldRules.Empty; - } + return Core.Schemas.FieldRules.Empty; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs index 3316dd897c..038f013617 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class ConfigurePreviewUrls : SchemaCommand { - public sealed class ConfigurePreviewUrls : SchemaCommand - { - public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } - } + public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs index 0c7c1ad87b..8288975488 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class ConfigureScripts : SchemaCommand { - public sealed class ConfigureScripts : SchemaCommand - { - public SchemaScripts? Scripts { get; set; } - } + public SchemaScripts? Scripts { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureUIFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureUIFields.cs index 9655cdab0f..36da65471a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureUIFields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureUIFields.cs @@ -7,12 +7,11 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class ConfigureUIFields : SchemaCommand { - public sealed class ConfigureUIFields : SchemaCommand - { - public FieldNames? FieldsInLists { get; set; } + public FieldNames? FieldsInLists { get; set; } - public FieldNames? FieldsInReferences { get; set; } - } + public FieldNames? FieldsInReferences { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs index 9856b9f5dc..0c98bc882f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs @@ -10,47 +10,46 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class CreateSchema : SchemaCommandBase, IUpsertCommand, IAggregateCommand { - public sealed class CreateSchema : SchemaCommandBase, IUpsertCommand, IAggregateCommand - { - public DomainId SchemaId { get; set; } + public DomainId SchemaId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string Category { get; set; } + public string Category { get; set; } - public bool IsPublished { get; set; } + public bool IsPublished { get; set; } - public SchemaType Type { get; set; } + public SchemaType Type { get; set; } - public UpsertSchemaField[]? Fields { get; set; } + public UpsertSchemaField[]? Fields { get; set; } - public FieldNames? FieldsInReferences { get; set; } + public FieldNames? FieldsInReferences { get; set; } - public FieldNames? FieldsInLists { get; set; } + public FieldNames? FieldsInLists { get; set; } - public FieldRuleCommand[]? FieldRules { get; set; } + public FieldRuleCommand[]? FieldRules { get; set; } - public SchemaScripts? Scripts { get; set; } + public SchemaScripts? Scripts { get; set; } - public SchemaProperties Properties { get; set; } + public SchemaProperties Properties { get; set; } - public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } + public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } - public override DomainId AggregateId - { - get => DomainId.Combine(AppId, SchemaId); - } + public override DomainId AggregateId + { + get => DomainId.Combine(AppId, SchemaId); + } - public CreateSchema() - { - SchemaId = DomainId.NewGuid(); - } + public CreateSchema() + { + SchemaId = DomainId.NewGuid(); + } - public Schema BuildSchema() - { - return ((IUpsertCommand)this).ToSchema(Name, Type); - } + public Schema BuildSchema() + { + return ((IUpsertCommand)this).ToSchema(Name, Type); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs index 34d5abffd9..168da25e8c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class DeleteField : FieldCommand { - public sealed class DeleteField : FieldCommand - { - } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs index 522dd95153..c1f634f2ce 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DeleteSchema.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class DeleteSchema : SchemaCommand { - public sealed class DeleteSchema : SchemaCommand - { - } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs index 92f472cf4f..569b862042 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/DisableField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class DisableField : FieldCommand { - public sealed class DisableField : FieldCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs index ac9774d3e2..e7c6acb48d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/EnableField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class EnableField : FieldCommand { - public sealed class EnableField : FieldCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs index 4b87d861c1..7b96a7197d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public class FieldCommand : ParentFieldCommand { - public class FieldCommand : ParentFieldCommand - { - public long FieldId { get; set; } - } + public long FieldId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs index 2cd06ba354..6283c7556f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs @@ -7,22 +7,21 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class FieldRuleCommand { - public sealed class FieldRuleCommand - { - public FieldRuleAction Action { get; set; } + public FieldRuleAction Action { get; set; } - public string Field { get; set; } + public string Field { get; set; } - public string? Condition { get; set; } + public string? Condition { get; set; } - public FieldRule ToFieldRule() + public FieldRule ToFieldRule() + { + return new FieldRule(Action, Field) { - return new FieldRule(Action, Field) - { - Condition = Condition - }; - } + Condition = Condition + }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs index 8d5582b599..0ea9fcc3fa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/HideField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class HideField : FieldCommand { - public sealed class HideField : FieldCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs index 8687817dd2..c0244c351b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs @@ -10,106 +10,105 @@ using Squidex.Infrastructure.Collections; using SchemaField = Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public interface IUpsertCommand { - public interface IUpsertCommand - { - bool IsPublished { get; set; } + bool IsPublished { get; set; } - string Category { get; set; } + string Category { get; set; } - SchemaField[]? Fields { get; set; } + SchemaField[]? Fields { get; set; } - SchemaScripts? Scripts { get; set; } + SchemaScripts? Scripts { get; set; } - SchemaProperties Properties { get; set; } + SchemaProperties Properties { get; set; } - FieldNames? FieldsInReferences { get; set; } + FieldNames? FieldsInReferences { get; set; } - FieldNames? FieldsInLists { get; set; } + FieldNames? FieldsInLists { get; set; } - FieldRuleCommand[]? FieldRules { get; set; } + FieldRuleCommand[]? FieldRules { get; set; } - ReadonlyDictionary<string, string>? PreviewUrls { get; set; } + ReadonlyDictionary<string, string>? PreviewUrls { get; set; } - Schema ToSchema(string name, SchemaType type) - { - var schema = new Schema(name, Properties, type); + Schema ToSchema(string name, SchemaType type) + { + var schema = new Schema(name, Properties, type); - if (IsPublished) - { - schema = schema.Publish(); - } + if (IsPublished) + { + schema = schema.Publish(); + } - if (Scripts != null) - { - schema = schema.SetScripts(Scripts); - } + if (Scripts != null) + { + schema = schema.SetScripts(Scripts); + } - if (PreviewUrls != null) - { - schema = schema.SetPreviewUrls(PreviewUrls); - } + if (PreviewUrls != null) + { + schema = schema.SetPreviewUrls(PreviewUrls); + } - if (FieldsInLists != null) - { - schema = schema.SetFieldsInLists(FieldsInLists); - } + if (FieldsInLists != null) + { + schema = schema.SetFieldsInLists(FieldsInLists); + } - if (FieldsInReferences != null) - { - schema = schema.SetFieldsInReferences(FieldsInReferences); - } + if (FieldsInReferences != null) + { + schema = schema.SetFieldsInReferences(FieldsInReferences); + } - if (FieldRules != null) - { - schema = schema.SetFieldRules(FieldRules.Select(x => x.ToFieldRule()).ToArray()); - } + if (FieldRules != null) + { + schema = schema.SetFieldRules(FieldRules.Select(x => x.ToFieldRule()).ToArray()); + } - if (!string.IsNullOrWhiteSpace(Category)) - { - schema = schema.ChangeCategory(Category); - } + if (!string.IsNullOrWhiteSpace(Category)) + { + schema = schema.ChangeCategory(Category); + } - var totalFields = 0; + var totalFields = 0; - if (Fields != null) + if (Fields != null) + { + foreach (var eventField in Fields) { - foreach (var eventField in Fields) - { - totalFields++; + totalFields++; - var partitioning = Partitioning.FromString(eventField.Partitioning); + var partitioning = Partitioning.FromString(eventField.Partitioning); - var field = - eventField.Properties.CreateRootField( - totalFields, - eventField.Name, partitioning, - eventField); + var field = + eventField.Properties.CreateRootField( + totalFields, + eventField.Name, partitioning, + eventField); - if (field is ArrayField arrayField && eventField.Nested?.Length > 0) + if (field is ArrayField arrayField && eventField.Nested?.Length > 0) + { + foreach (var nestedEventField in eventField.Nested) { - foreach (var nestedEventField in eventField.Nested) - { - totalFields++; + totalFields++; - var nestedField = - nestedEventField.Properties.CreateNestedField( - totalFields, - nestedEventField.Name, - nestedEventField); + var nestedField = + nestedEventField.Properties.CreateNestedField( + totalFields, + nestedEventField.Name, + nestedEventField); - arrayField = arrayField.AddField(nestedField); - } - - field = arrayField; + arrayField = arrayField.AddField(nestedField); } - schema = schema.AddField(field); + field = arrayField; } - } - return schema; + schema = schema.AddField(field); + } } + + return schema; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs index 15a9c0b1c0..75b53fb7f1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/LockField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class LockField : FieldCommand { - public sealed class LockField : FieldCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs index 91f067fb16..ad775f2b61 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public abstract class ParentFieldCommand : SchemaCommand { - public abstract class ParentFieldCommand : SchemaCommand - { - public long? ParentFieldId { get; set; } - } + public long? ParentFieldId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs index bde23a6bc7..31ff699468 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/PublishSchema.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class PublishSchema : SchemaCommand { - public sealed class PublishSchema : SchemaCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs index 452c3230ae..0d530f794e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class ReorderFields : ParentFieldCommand { - public sealed class ReorderFields : ParentFieldCommand - { - public long[] FieldIds { get; set; } - } + public long[] FieldIds { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs index 5330ac9c7d..2bd243c8ad 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ShowField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class ShowField : FieldCommand { - public sealed class ShowField : FieldCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs index 182196876e..ad984f9c1a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs @@ -10,37 +10,36 @@ using Squidex.Infrastructure.Commands; using SchemaField = Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class SynchronizeSchema : SchemaCommand, IUpsertCommand, IAggregateCommand, ISchemaCommand { - public sealed class SynchronizeSchema : SchemaCommand, IUpsertCommand, IAggregateCommand, ISchemaCommand - { - public bool NoFieldDeletion { get; set; } + public bool NoFieldDeletion { get; set; } - public bool NoFieldRecreation { get; set; } + public bool NoFieldRecreation { get; set; } - public bool IsPublished { get; set; } + public bool IsPublished { get; set; } - public string Category { get; set; } + public string Category { get; set; } - public SchemaField[]? Fields { get; set; } + public SchemaField[]? Fields { get; set; } - public FieldNames? FieldsInReferences { get; set; } + public FieldNames? FieldsInReferences { get; set; } - public FieldNames? FieldsInLists { get; set; } + public FieldNames? FieldsInLists { get; set; } - public FieldRuleCommand[]? FieldRules { get; set; } + public FieldRuleCommand[]? FieldRules { get; set; } - public SchemaScripts? Scripts { get; set; } + public SchemaScripts? Scripts { get; set; } - public SchemaProperties Properties { get; set; } + public SchemaProperties Properties { get; set; } - public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } + public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } - public Schema BuildSchema(string name, SchemaType type) - { - IUpsertCommand self = this; + public Schema BuildSchema(string name, SchemaType type) + { + IUpsertCommand self = this; - return self.ToSchema(name, type); - } + return self.ToSchema(name, type); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs index 3652b963b4..d41b37dd52 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UnpublishSchema.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class UnpublishSchema : SchemaCommand { - public sealed class UnpublishSchema : SchemaCommand - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs index f86bfb4b60..cee32d4bee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateField.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class UpdateField : FieldCommand { - public sealed class UpdateField : FieldCommand - { - public FieldProperties Properties { get; set; } - } + public FieldProperties Properties { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs index 37cf412632..3a753881e2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpdateSchema.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class UpdateSchema : SchemaCommand { - public sealed class UpdateSchema : SchemaCommand - { - public SchemaProperties Properties { get; set; } - } + public SchemaProperties Properties { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs index 2497b5be1f..c4fb744268 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class UpsertSchemaField : UpsertSchemaFieldBase { - public sealed class UpsertSchemaField : UpsertSchemaFieldBase - { - public string Partitioning { get; set; } + public string Partitioning { get; set; } - public UpsertSchemaNestedField[]? Nested { get; set; } - } + public UpsertSchemaNestedField[]? Nested { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs index 558eb8b378..f4ee30de63 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs @@ -7,18 +7,17 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public abstract class UpsertSchemaFieldBase : IFieldSettings { - public abstract class UpsertSchemaFieldBase : IFieldSettings - { - public string Name { get; set; } + public string Name { get; set; } - public bool IsLocked { get; set; } + public bool IsLocked { get; set; } - public bool IsHidden { get; set; } + public bool IsHidden { get; set; } - public bool IsDisabled { get; set; } + public bool IsDisabled { get; set; } - public FieldProperties Properties { get; set; } - } + public FieldProperties Properties { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs index e514fb4cdf..70afc9f301 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public sealed class UpsertSchemaNestedField : UpsertSchemaFieldBase { - public sealed class UpsertSchemaNestedField : UpsertSchemaFieldBase - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/_SchemaCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/_SchemaCommand.cs index 247f6d5fb1..975bbad479 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/_SchemaCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/_SchemaCommand.cs @@ -10,23 +10,22 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Schemas.Commands +namespace Squidex.Domain.Apps.Entities.Schemas.Commands; + +public abstract class SchemaCommand : SchemaCommandBase, ISchemaCommand { - public abstract class SchemaCommand : SchemaCommandBase, ISchemaCommand - { - public NamedId<DomainId> SchemaId { get; set; } + public NamedId<DomainId> SchemaId { get; set; } - public override DomainId AggregateId - { - get => DomainId.Combine(AppId, SchemaId.Id); - } + public override DomainId AggregateId + { + get => DomainId.Combine(AppId, SchemaId.Id); } +} - // This command is needed as marker for middlewares. - public abstract class SchemaCommandBase : SquidexCommand, IAppCommand, IAggregateCommand - { - public NamedId<DomainId> AppId { get; set; } +// This command is needed as marker for middlewares. +public abstract class SchemaCommandBase : SquidexCommand, IAppCommand, IAggregateCommand +{ + public NamedId<DomainId> AppId { get; set; } - public abstract DomainId AggregateId { get; } - } + public abstract DomainId AggregateId { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/FieldPropertiesValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/FieldPropertiesValidator.cs index 4fff24efbc..7a5c0e9c30 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/FieldPropertiesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/FieldPropertiesValidator.cs @@ -11,308 +11,307 @@ using Squidex.Infrastructure.Validation; using GraphQLSchema = GraphQL.Types.Schema; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards; + +public sealed class FieldPropertiesValidator : IFieldPropertiesVisitor<IEnumerable<ValidationError>, None> { - public sealed class FieldPropertiesValidator : IFieldPropertiesVisitor<IEnumerable<ValidationError>, None> - { - private static readonly FieldPropertiesValidator Instance = new FieldPropertiesValidator(); + private static readonly FieldPropertiesValidator Instance = new FieldPropertiesValidator(); - private FieldPropertiesValidator() - { - } + private FieldPropertiesValidator() + { + } - public static IEnumerable<ValidationError> Validate(FieldProperties properties) + public static IEnumerable<ValidationError> Validate(FieldProperties properties) + { + if (properties != null) { - if (properties != null) + foreach (var error in properties.Accept(Instance, None.Value)) { - foreach (var error in properties.Accept(Instance, None.Value)) - { - yield return error; - } + yield return error; } } + } - public IEnumerable<ValidationError> Visit(ArrayFieldProperties properties, None args) + public IEnumerable<ValidationError> Visit(ArrayFieldProperties properties, None args) + { + if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) { - if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), - nameof(properties.MinItems), - nameof(properties.MaxItems)); - } + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), + nameof(properties.MinItems), + nameof(properties.MaxItems)); } + } - public IEnumerable<ValidationError> Visit(AssetsFieldProperties properties, None args) + public IEnumerable<ValidationError> Visit(AssetsFieldProperties properties, None args) + { + if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) { - if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), - nameof(properties.MinItems), - nameof(properties.MaxItems)); - } + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), + nameof(properties.MinItems), + nameof(properties.MaxItems)); + } - if (IsMaxGreaterThanMin(properties.MaxHeight, properties.MinHeight)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxHeight), nameof(properties.MinHeight)), - nameof(properties.MaxHeight), - nameof(properties.MinHeight)); - } + if (IsMaxGreaterThanMin(properties.MaxHeight, properties.MinHeight)) + { + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxHeight), nameof(properties.MinHeight)), + nameof(properties.MaxHeight), + nameof(properties.MinHeight)); + } - if (IsMaxGreaterThanMin(properties.MaxWidth, properties.MinWidth)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWidth), nameof(properties.MinWidth)), - nameof(properties.MaxWidth), - nameof(properties.MinWidth)); - } + if (IsMaxGreaterThanMin(properties.MaxWidth, properties.MinWidth)) + { + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWidth), nameof(properties.MinWidth)), + nameof(properties.MaxWidth), + nameof(properties.MinWidth)); + } - if (IsMaxGreaterThanMin(properties.MaxSize, properties.MinSize)) - { - yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxSize), nameof(properties.MinSize)), - nameof(properties.MaxSize), - nameof(properties.MinSize)); - } + if (IsMaxGreaterThanMin(properties.MaxSize, properties.MinSize)) + { + yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxSize), nameof(properties.MinSize)), + nameof(properties.MaxSize), + nameof(properties.MinSize)); + } - if (properties.AspectWidth.HasValue != properties.AspectHeight.HasValue) - { - yield return new ValidationError(Not.BothDefined(nameof(properties.AspectWidth), nameof(properties.AspectHeight)), - nameof(properties.AspectWidth), - nameof(properties.AspectHeight)); - } + if (properties.AspectWidth.HasValue != properties.AspectHeight.HasValue) + { + yield return new ValidationError(Not.BothDefined(nameof(properties.AspectWidth), nameof(properties.AspectHeight)), + nameof(properties.AspectWidth), + nameof(properties.AspectHeight)); } + } - public IEnumerable<ValidationError> Visit(BooleanFieldProperties properties, None args) + public IEnumerable<ValidationError> Visit(BooleanFieldProperties properties, None args) + { + if (!properties.Editor.IsEnumValue()) { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Editor)), - nameof(properties.Editor)); - } + yield return new ValidationError(Not.Valid(nameof(properties.Editor)), + nameof(properties.Editor)); } + } - public IEnumerable<ValidationError> Visit(ComponentFieldProperties properties, None args) + public IEnumerable<ValidationError> Visit(ComponentFieldProperties properties, None args) + { + yield break; + } + + public IEnumerable<ValidationError> Visit(ComponentsFieldProperties properties, None args) + { + if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) { - yield break; + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), + nameof(properties.MinItems), + nameof(properties.MaxItems)); } + } - public IEnumerable<ValidationError> Visit(ComponentsFieldProperties properties, None args) + public IEnumerable<ValidationError> Visit(DateTimeFieldProperties properties, None args) + { + if (!properties.Editor.IsEnumValue()) { - if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), - nameof(properties.MinItems), - nameof(properties.MaxItems)); - } + yield return new ValidationError(Not.Valid(nameof(properties.Editor)), + nameof(properties.Editor)); } - public IEnumerable<ValidationError> Visit(DateTimeFieldProperties properties, None args) + if (IsMaxGreaterThanMin(properties.MaxValue, properties.MinValue)) { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Editor)), - nameof(properties.Editor)); - } + yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)), + nameof(properties.MinValue), + nameof(properties.MaxValue)); + } - if (IsMaxGreaterThanMin(properties.MaxValue, properties.MinValue)) + if (properties.CalculatedDefaultValue.HasValue) + { + if (!properties.CalculatedDefaultValue.Value.IsEnumValue()) { - yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)), - nameof(properties.MinValue), - nameof(properties.MaxValue)); + yield return new ValidationError(Not.Valid(nameof(properties.CalculatedDefaultValue)), + nameof(properties.CalculatedDefaultValue)); } - if (properties.CalculatedDefaultValue.HasValue) + if (properties.DefaultValue.HasValue) { - if (!properties.CalculatedDefaultValue.Value.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.CalculatedDefaultValue)), - nameof(properties.CalculatedDefaultValue)); - } - - if (properties.DefaultValue.HasValue) - { - yield return new ValidationError(T.Get("schemas.dateTimeCalculatedDefaultAndDefaultError"), - nameof(properties.CalculatedDefaultValue), - nameof(properties.DefaultValue)); - } + yield return new ValidationError(T.Get("schemas.dateTimeCalculatedDefaultAndDefaultError"), + nameof(properties.CalculatedDefaultValue), + nameof(properties.DefaultValue)); } } + } - public IEnumerable<ValidationError> Visit(GeolocationFieldProperties properties, None args) + public IEnumerable<ValidationError> Visit(GeolocationFieldProperties properties, None args) + { + if (!properties.Editor.IsEnumValue()) { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Editor)), - nameof(properties.Editor)); - } + yield return new ValidationError(Not.Valid(nameof(properties.Editor)), + nameof(properties.Editor)); } + } - public IEnumerable<ValidationError> Visit(JsonFieldProperties properties, None args) + public IEnumerable<ValidationError> Visit(JsonFieldProperties properties, None args) + { + if (!IsValidGraphQLSchema(properties.GraphQLSchema)) { - if (!IsValidGraphQLSchema(properties.GraphQLSchema)) - { - yield return new ValidationError(Not.Valid(nameof(properties.GraphQLSchema)), - nameof(properties.GraphQLSchema)); - } + yield return new ValidationError(Not.Valid(nameof(properties.GraphQLSchema)), + nameof(properties.GraphQLSchema)); } + } - public IEnumerable<ValidationError> Visit(NumberFieldProperties properties, None args) + public IEnumerable<ValidationError> Visit(NumberFieldProperties properties, None args) + { + if (!properties.Editor.IsEnumValue()) { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Editor)), - nameof(properties.Editor)); - } - - if (properties.Editor is NumberFieldEditor.Radio or NumberFieldEditor.Dropdown && properties.AllowedValues?.Any() != true) - { - yield return new ValidationError(T.Get("schemas.stringEditorsNeedAllowedValuesError"), - nameof(properties.AllowedValues)); - } + yield return new ValidationError(Not.Valid(nameof(properties.Editor)), + nameof(properties.Editor)); + } - if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue >= properties.MaxValue) - { - yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)), - nameof(properties.MinValue), - nameof(properties.MaxValue)); - } + if (properties.Editor is NumberFieldEditor.Radio or NumberFieldEditor.Dropdown && properties.AllowedValues?.Any() != true) + { + yield return new ValidationError(T.Get("schemas.stringEditorsNeedAllowedValuesError"), + nameof(properties.AllowedValues)); + } - if (properties.InlineEditable && properties.Editor == NumberFieldEditor.Radio) - { - yield return new ValidationError(T.Get("schemas.number.inlineEditorError"), - nameof(properties.InlineEditable), - nameof(properties.Editor)); - } + if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue >= properties.MaxValue) + { + yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)), + nameof(properties.MinValue), + nameof(properties.MaxValue)); } - public IEnumerable<ValidationError> Visit(ReferencesFieldProperties properties, None args) + if (properties.InlineEditable && properties.Editor == NumberFieldEditor.Radio) { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Editor)), - nameof(properties.Editor)); - } + yield return new ValidationError(T.Get("schemas.number.inlineEditorError"), + nameof(properties.InlineEditable), + nameof(properties.Editor)); + } + } - if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems > properties.MaxItems) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), - nameof(properties.MinItems), - nameof(properties.MaxItems)); - } + public IEnumerable<ValidationError> Visit(ReferencesFieldProperties properties, None args) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError(Not.Valid(nameof(properties.Editor)), + nameof(properties.Editor)); + } - if (properties.ResolveReference && properties.MaxItems != 1) - { - yield return new ValidationError(T.Get("schemas.references.resolveError"), - nameof(properties.ResolveReference), - nameof(properties.MaxItems)); - } + if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems > properties.MaxItems) + { + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), + nameof(properties.MinItems), + nameof(properties.MaxItems)); } - public IEnumerable<ValidationError> Visit(StringFieldProperties properties, None args) + if (properties.ResolveReference && properties.MaxItems != 1) { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Editor)), - nameof(properties.Editor)); - } + yield return new ValidationError(T.Get("schemas.references.resolveError"), + nameof(properties.ResolveReference), + nameof(properties.MaxItems)); + } + } - if (!properties.ContentType.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.ContentType)), - nameof(properties.ContentType)); - } + public IEnumerable<ValidationError> Visit(StringFieldProperties properties, None args) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError(Not.Valid(nameof(properties.Editor)), + nameof(properties.Editor)); + } - if (properties.Editor is StringFieldEditor.Radio or StringFieldEditor.Dropdown && properties.AllowedValues?.Any() != true) - { - yield return new ValidationError(T.Get("schemas.stringEditorsNeedAllowedValuesError"), - nameof(properties.AllowedValues)); - } + if (!properties.ContentType.IsEnumValue()) + { + yield return new ValidationError(Not.Valid(nameof(properties.ContentType)), + nameof(properties.ContentType)); + } - if (properties.Pattern != null && !properties.Pattern.IsValidRegex()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Pattern)), - nameof(properties.Pattern)); - } + if (properties.Editor is StringFieldEditor.Radio or StringFieldEditor.Dropdown && properties.AllowedValues?.Any() != true) + { + yield return new ValidationError(T.Get("schemas.stringEditorsNeedAllowedValuesError"), + nameof(properties.AllowedValues)); + } - if (IsMaxGreaterThanMin(properties.MaxLength, properties.MinLength)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxLength), nameof(properties.MinLength)), - nameof(properties.MinLength), - nameof(properties.MaxLength)); - } + if (properties.Pattern != null && !properties.Pattern.IsValidRegex()) + { + yield return new ValidationError(Not.Valid(nameof(properties.Pattern)), + nameof(properties.Pattern)); + } - if (IsMaxGreaterThanMin(properties.MaxWords, properties.MinWords)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWords), nameof(properties.MinWords)), - nameof(properties.MinWords), - nameof(properties.MaxWords)); - } + if (IsMaxGreaterThanMin(properties.MaxLength, properties.MinLength)) + { + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxLength), nameof(properties.MinLength)), + nameof(properties.MinLength), + nameof(properties.MaxLength)); + } - if (IsMaxGreaterThanMin(properties.MaxCharacters, properties.MinCharacters)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxCharacters), nameof(properties.MinCharacters)), - nameof(properties.MinCharacters), - nameof(properties.MaxCharacters)); - } + if (IsMaxGreaterThanMin(properties.MaxWords, properties.MinWords)) + { + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWords), nameof(properties.MinWords)), + nameof(properties.MinWords), + nameof(properties.MaxWords)); + } - if (properties.InlineEditable && properties.Editor != StringFieldEditor.Dropdown && properties.Editor != StringFieldEditor.Input && properties.Editor != StringFieldEditor.Slug) - { - yield return new ValidationError(T.Get("schemas.string.inlineEditorError"), - nameof(properties.InlineEditable), - nameof(properties.Editor)); - } + if (IsMaxGreaterThanMin(properties.MaxCharacters, properties.MinCharacters)) + { + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxCharacters), nameof(properties.MinCharacters)), + nameof(properties.MinCharacters), + nameof(properties.MaxCharacters)); } - public IEnumerable<ValidationError> Visit(TagsFieldProperties properties, None args) + if (properties.InlineEditable && properties.Editor != StringFieldEditor.Dropdown && properties.Editor != StringFieldEditor.Input && properties.Editor != StringFieldEditor.Slug) { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Editor)), - nameof(properties.Editor)); - } + yield return new ValidationError(T.Get("schemas.string.inlineEditorError"), + nameof(properties.InlineEditable), + nameof(properties.Editor)); + } + } - if (properties.Editor is TagsFieldEditor.Checkboxes or TagsFieldEditor.Dropdown && properties.AllowedValues?.Any() != true) - { - yield return new ValidationError(T.Get("schemas.tags.editorNeedsAllowedValues"), - nameof(properties.AllowedValues)); - } + public IEnumerable<ValidationError> Visit(TagsFieldProperties properties, None args) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError(Not.Valid(nameof(properties.Editor)), + nameof(properties.Editor)); + } - if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), - nameof(properties.MinItems), - nameof(properties.MaxItems)); - } + if (properties.Editor is TagsFieldEditor.Checkboxes or TagsFieldEditor.Dropdown && properties.AllowedValues?.Any() != true) + { + yield return new ValidationError(T.Get("schemas.tags.editorNeedsAllowedValues"), + nameof(properties.AllowedValues)); } - public IEnumerable<ValidationError> Visit(UIFieldProperties properties, None args) + if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) { - if (!properties.Editor.IsEnumValue()) - { - yield return new ValidationError(Not.Valid(nameof(properties.Editor)), - nameof(properties.Editor)); - } + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), + nameof(properties.MinItems), + nameof(properties.MaxItems)); } + } - private static bool IsMaxGreaterThanMin<T>(T? min, T? max) where T : struct, IComparable + public IEnumerable<ValidationError> Visit(UIFieldProperties properties, None args) + { + if (!properties.Editor.IsEnumValue()) { - return max.HasValue && min.HasValue && min.Value.CompareTo(max.Value) < 0; + yield return new ValidationError(Not.Valid(nameof(properties.Editor)), + nameof(properties.Editor)); } + } - private static bool IsValidGraphQLSchema(string? schema) + private static bool IsMaxGreaterThanMin<T>(T? min, T? max) where T : struct, IComparable + { + return max.HasValue && min.HasValue && min.Value.CompareTo(max.Value) < 0; + } + + private static bool IsValidGraphQLSchema(string? schema) + { + if (string.IsNullOrWhiteSpace(schema)) { - if (string.IsNullOrWhiteSpace(schema)) - { - return true; - } + return true; + } - try - { - GraphQLSchema.For(schema); - return true; - } - catch - { - return false; - } + try + { + GraphQLSchema.For(schema); + return true; + } + catch + { + return false; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs index c9e9630bd2..c999e09d3e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs @@ -10,58 +10,57 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards; + +public static class GuardHelper { - public static class GuardHelper + public static IArrayField GetArrayFieldOrThrow(Schema schema, long parentId, bool allowLocked) { - public static IArrayField GetArrayFieldOrThrow(Schema schema, long parentId, bool allowLocked) + if (!schema.FieldsById.TryGetValue(parentId, out var rootField) || rootField is not IArrayField arrayField) { - if (!schema.FieldsById.TryGetValue(parentId, out var rootField) || rootField is not IArrayField arrayField) - { - throw new DomainObjectNotFoundException(parentId.ToString(CultureInfo.InvariantCulture)); - } - - if (!allowLocked) - { - EnsureNotLocked(arrayField); - } - - return arrayField; + throw new DomainObjectNotFoundException(parentId.ToString(CultureInfo.InvariantCulture)); } - public static IField GetFieldOrThrow(Schema schema, long fieldId, long? parentId, bool allowLocked) + if (!allowLocked) { - if (parentId != null) - { - var arrayField = GetArrayFieldOrThrow(schema, parentId.Value, allowLocked); + EnsureNotLocked(arrayField); + } - if (!arrayField.FieldsById.TryGetValue(fieldId, out var nestedField)) - { - throw new DomainObjectNotFoundException(fieldId.ToString(CultureInfo.InvariantCulture)); - } + return arrayField; + } - return nestedField; - } + public static IField GetFieldOrThrow(Schema schema, long fieldId, long? parentId, bool allowLocked) + { + if (parentId != null) + { + var arrayField = GetArrayFieldOrThrow(schema, parentId.Value, allowLocked); - if (!schema.FieldsById.TryGetValue(fieldId, out var field)) + if (!arrayField.FieldsById.TryGetValue(fieldId, out var nestedField)) { throw new DomainObjectNotFoundException(fieldId.ToString(CultureInfo.InvariantCulture)); } - if (!allowLocked) - { - EnsureNotLocked(field); - } + return nestedField; + } - return field; + if (!schema.FieldsById.TryGetValue(fieldId, out var field)) + { + throw new DomainObjectNotFoundException(fieldId.ToString(CultureInfo.InvariantCulture)); } - private static void EnsureNotLocked(IField field) + if (!allowLocked) { - if (field.IsLocked) - { - throw new DomainException(T.Get("schemas.fieldIsLocked")); - } + EnsureNotLocked(field); + } + + return field; + } + + private static void EnsureNotLocked(IField field) + { + if (field.IsLocked) + { + throw new DomainException(T.Get("schemas.fieldIsLocked")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs index 68be5cf914..a4dd427acc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs @@ -13,317 +13,316 @@ using Squidex.Infrastructure.Validation; using Squidex.Text; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards; + +public static class GuardSchema { - public static class GuardSchema + public static void CanCreate(CreateSchema command) { - public static void CanCreate(CreateSchema command) - { - Guard.NotNull(command); + Guard.NotNull(command); - Validate.It(e => + Validate.It(e => + { + if (!command.Name.IsSlug()) { - if (!command.Name.IsSlug()) - { - e(Not.ValidSlug(nameof(command.Name)), nameof(command.Name)); - } + e(Not.ValidSlug(nameof(command.Name)), nameof(command.Name)); + } - ValidateUpsert(command, e); - }); - } + ValidateUpsert(command, e); + }); + } + + public static void CanSynchronize(SynchronizeSchema command) + { + Guard.NotNull(command); - public static void CanSynchronize(SynchronizeSchema command) + Validate.It(e => { - Guard.NotNull(command); + ValidateUpsert(command, e); + }); + } - Validate.It(e => - { - ValidateUpsert(command, e); - }); - } + public static void CanReorder(ReorderFields command, Schema schema) + { + Guard.NotNull(command); - public static void CanReorder(ReorderFields command, Schema schema) - { - Guard.NotNull(command); + IArrayField? arrayField = null; - IArrayField? arrayField = null; + if (command.ParentFieldId != null) + { + arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); + } - if (command.ParentFieldId != null) + Validate.It(e => + { + if (command.FieldIds == null) { - arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); + e(Not.Defined(nameof(command.FieldIds)), nameof(command.FieldIds)); } - Validate.It(e => + if (arrayField == null) { - if (command.FieldIds == null) - { - e(Not.Defined(nameof(command.FieldIds)), nameof(command.FieldIds)); - } + ValidateFieldIds(command, schema.FieldsById, e); + } + else + { + ValidateFieldIds(command, arrayField.FieldsById, e); + } + }); + } - if (arrayField == null) - { - ValidateFieldIds(command, schema.FieldsById, e); - } - else - { - ValidateFieldIds(command, arrayField.FieldsById, e); - } - }); - } + public static void CanConfigurePreviewUrls(ConfigurePreviewUrls command) + { + Guard.NotNull(command); - public static void CanConfigurePreviewUrls(ConfigurePreviewUrls command) + Validate.It(e => { - Guard.NotNull(command); - - Validate.It(e => + if (command.PreviewUrls == null) { - if (command.PreviewUrls == null) - { - e(Not.Defined(nameof(command.PreviewUrls)), nameof(command.PreviewUrls)); - } - }); - } + e(Not.Defined(nameof(command.PreviewUrls)), nameof(command.PreviewUrls)); + } + }); + } + + public static void CanConfigureUIFields(ConfigureUIFields command, Schema schema) + { + Guard.NotNull(command); - public static void CanConfigureUIFields(ConfigureUIFields command, Schema schema) + Validate.It(e => { - Guard.NotNull(command); + ValidateFieldNames(schema, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField); + ValidateFieldNames(schema, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed); + }); + } - Validate.It(e => - { - ValidateFieldNames(schema, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField); - ValidateFieldNames(schema, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed); - }); - } + public static void CanConfigureFieldRules(ConfigureFieldRules command) + { + Guard.NotNull(command); - public static void CanConfigureFieldRules(ConfigureFieldRules command) + Validate.It(e => { - Guard.NotNull(command); - - Validate.It(e => - { - ValidateFieldRules(command.FieldRules, nameof(command.FieldRules), e); - }); - } + ValidateFieldRules(command.FieldRules, nameof(command.FieldRules), e); + }); + } - private static void ValidateUpsert(IUpsertCommand command, AddValidation e) + private static void ValidateUpsert(IUpsertCommand command, AddValidation e) + { + if (command.Fields?.Length > 0) { - if (command.Fields?.Length > 0) + command.Fields.Foreach((field, fieldIndex) => { - command.Fields.Foreach((field, fieldIndex) => - { - var fieldPrefix = $"Fields[{fieldIndex}]"; + var fieldPrefix = $"Fields[{fieldIndex}]"; - ValidateRootField(field, fieldPrefix, e); - }); + ValidateRootField(field, fieldPrefix, e); + }); - foreach (var fieldName in command.Fields.Duplicates(x => x.Name)) + foreach (var fieldName in command.Fields.Duplicates(x => x.Name)) + { + if (fieldName.IsPropertyName()) { - if (fieldName.IsPropertyName()) - { - e(T.Get("schemas.duplicateFieldName", new { field = fieldName }), nameof(command.Fields)); - } + e(T.Get("schemas.duplicateFieldName", new { field = fieldName }), nameof(command.Fields)); } } + } - ValidateFieldNames(command, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField); - ValidateFieldNames(command, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed); + ValidateFieldNames(command, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField); + ValidateFieldNames(command, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed); - ValidateFieldRules(command.FieldRules, nameof(command.FieldRules), e); - } + ValidateFieldRules(command.FieldRules, nameof(command.FieldRules), e); + } - private static void ValidateRootField(UpsertSchemaField field, string prefix, AddValidation e) + private static void ValidateRootField(UpsertSchemaField field, string prefix, AddValidation e) + { + if (field == null) + { + e(Not.Defined("Field"), prefix); + } + else { - if (field == null) + if (!field.Partitioning.IsValidPartitioning()) { - e(Not.Defined("Field"), prefix); + e(Not.Valid(nameof(field.Partitioning)), $"{prefix}.{nameof(field.Partitioning)}"); } - else - { - if (!field.Partitioning.IsValidPartitioning()) - { - e(Not.Valid(nameof(field.Partitioning)), $"{prefix}.{nameof(field.Partitioning)}"); - } - ValidateField(field, prefix, e); + ValidateField(field, prefix, e); - if (field.Nested?.Length > 0) + if (field.Nested?.Length > 0) + { + if (field.Properties is ArrayFieldProperties) { - if (field.Properties is ArrayFieldProperties) + field.Nested.Foreach((nestedField, nestedIndex) => { - field.Nested.Foreach((nestedField, nestedIndex) => - { - var nestedPrefix = $"{prefix}.Nested[{nestedIndex}]"; + var nestedPrefix = $"{prefix}.Nested[{nestedIndex}]"; - ValidateNestedField(nestedField, nestedPrefix, e); - }); - } - else if (field.Nested.Length > 0) - { - e(T.Get("schemas.onlyArraysHaveNested"), $"{prefix}.{nameof(field.Partitioning)}"); - } + ValidateNestedField(nestedField, nestedPrefix, e); + }); + } + else if (field.Nested.Length > 0) + { + e(T.Get("schemas.onlyArraysHaveNested"), $"{prefix}.{nameof(field.Partitioning)}"); + } - foreach (var fieldName in field.Nested.Duplicates(x => x.Name)) + foreach (var fieldName in field.Nested.Duplicates(x => x.Name)) + { + if (fieldName.IsPropertyName()) { - if (fieldName.IsPropertyName()) - { - e(T.Get("schemas.duplicateFieldName", new { field = fieldName }), $"{prefix}.Nested"); - } + e(T.Get("schemas.duplicateFieldName", new { field = fieldName }), $"{prefix}.Nested"); } } } } + } - private static void ValidateNestedField(UpsertSchemaNestedField nestedField, string prefix, AddValidation e) + private static void ValidateNestedField(UpsertSchemaNestedField nestedField, string prefix, AddValidation e) + { + if (nestedField == null) + { + e(Not.Defined("Field"), prefix); + } + else { - if (nestedField == null) + if (nestedField.Properties is ArrayFieldProperties) { - e(Not.Defined("Field"), prefix); + e(T.Get("schemas.onylArraysInRoot"), $"{prefix}.{nameof(nestedField.Properties)}"); } - else - { - if (nestedField.Properties is ArrayFieldProperties) - { - e(T.Get("schemas.onylArraysInRoot"), $"{prefix}.{nameof(nestedField.Properties)}"); - } - ValidateField(nestedField, prefix, e); - } + ValidateField(nestedField, prefix, e); } + } - private static void ValidateField(UpsertSchemaFieldBase field, string prefix, AddValidation e) + private static void ValidateField(UpsertSchemaFieldBase field, string prefix, AddValidation e) + { + if (!field.Name.IsPropertyName()) { - if (!field.Name.IsPropertyName()) - { - e(Not.ValidJavascriptName(nameof(field.Name)), $"{prefix}.{nameof(field.Name)}"); - } + e(Not.ValidJavascriptName(nameof(field.Name)), $"{prefix}.{nameof(field.Name)}"); + } - if (field.Properties == null) - { - e(Not.Defined(nameof(field.Properties)), $"{prefix}.{nameof(field.Properties)}"); - } - else + if (field.Properties == null) + { + e(Not.Defined(nameof(field.Properties)), $"{prefix}.{nameof(field.Properties)}"); + } + else + { + if (field.Properties.IsUIProperty()) { - if (field.Properties.IsUIProperty()) + if (field.IsHidden) { - if (field.IsHidden) - { - e(T.Get("schemas.uiFieldCannotBeHidden"), $"{prefix}.{nameof(field.IsHidden)}"); - } + e(T.Get("schemas.uiFieldCannotBeHidden"), $"{prefix}.{nameof(field.IsHidden)}"); + } - if (field.IsDisabled) - { - e(T.Get("schemas.uiFieldCannotBeDisabled"), $"{prefix}.{nameof(field.IsDisabled)}"); - } + if (field.IsDisabled) + { + e(T.Get("schemas.uiFieldCannotBeDisabled"), $"{prefix}.{nameof(field.IsDisabled)}"); } + } - var errors = FieldPropertiesValidator.Validate(field.Properties); + var errors = FieldPropertiesValidator.Validate(field.Properties); - errors.Foreach((x, _) => x.WithPrefix($"{prefix}.{nameof(field.Properties)}").AddTo(e)); - } + errors.Foreach((x, _) => x.WithPrefix($"{prefix}.{nameof(field.Properties)}").AddTo(e)); } + } - private static void ValidateFieldNames(Schema schema, FieldNames? fields, string path, AddValidation e, Func<string, bool> isAllowed) + private static void ValidateFieldNames(Schema schema, FieldNames? fields, string path, AddValidation e, Func<string, bool> isAllowed) + { + if (fields != null) { - if (fields != null) + fields.Foreach((fieldName, fieldIndex) => { - fields.Foreach((fieldName, fieldIndex) => - { - var fieldPrefix = $"{path}[{fieldIndex}]"; + var fieldPrefix = $"{path}[{fieldIndex}]"; - var field = schema.FieldsByName.GetValueOrDefault(fieldName ?? string.Empty); + var field = schema.FieldsByName.GetValueOrDefault(fieldName ?? string.Empty); - if (string.IsNullOrWhiteSpace(fieldName)) - { - e(Not.Defined("Field"), fieldPrefix); - } - else if (field == null && !isAllowed(fieldName)) - { - e(T.Get("schemas.fieldNotInSchema"), fieldPrefix); - } - else if (field?.IsUI() == true) - { - e(T.Get("schemas.fieldCannotBeUIField"), fieldPrefix); - } - }); + if (string.IsNullOrWhiteSpace(fieldName)) + { + e(Not.Defined("Field"), fieldPrefix); + } + else if (field == null && !isAllowed(fieldName)) + { + e(T.Get("schemas.fieldNotInSchema"), fieldPrefix); + } + else if (field?.IsUI() == true) + { + e(T.Get("schemas.fieldCannotBeUIField"), fieldPrefix); + } + }); - foreach (var duplicate in fields.Duplicates()) + foreach (var duplicate in fields.Duplicates()) + { + if (!string.IsNullOrWhiteSpace(duplicate)) { - if (!string.IsNullOrWhiteSpace(duplicate)) - { - e(T.Get("schemas.duplicateFieldName", new { field = duplicate }), path); - } + e(T.Get("schemas.duplicateFieldName", new { field = duplicate }), path); } } } + } - private static void ValidateFieldRules(FieldRuleCommand[]? fieldRules, string path, AddValidation e) + private static void ValidateFieldRules(FieldRuleCommand[]? fieldRules, string path, AddValidation e) + { + fieldRules?.Foreach((rule, ruleIndex) => { - fieldRules?.Foreach((rule, ruleIndex) => + var rulePrefix = $"{path}[{ruleIndex}]"; + + if (string.IsNullOrWhiteSpace(rule.Field)) { - var rulePrefix = $"{path}[{ruleIndex}]"; + e(Not.Defined(nameof(rule.Field)), $"{rulePrefix}.{nameof(rule.Field)}"); + } - if (string.IsNullOrWhiteSpace(rule.Field)) + if (!rule.Action.IsEnumValue()) + { + e(Not.Valid(nameof(rule.Action)), $"{rulePrefix}.{nameof(rule.Action)}"); + } + }); + } + + private static void ValidateFieldNames(IUpsertCommand command, FieldNames? fields, string path, AddValidation e, Func<string, bool> isAllowed) + { + if (fields != null) + { + fields.Foreach((fieldName, fieldIndex) => + { + var fieldPrefix = $"{path}[{fieldIndex}]"; + + var field = command?.Fields?.FirstOrDefault(x => x.Name == fieldName); + + if (string.IsNullOrWhiteSpace(fieldName)) { - e(Not.Defined(nameof(rule.Field)), $"{rulePrefix}.{nameof(rule.Field)}"); + e(Not.Defined("Field"), fieldPrefix); } - - if (!rule.Action.IsEnumValue()) + else if (field == null && !isAllowed(fieldName)) + { + e(T.Get("schemas.fieldNotInSchema"), fieldPrefix); + } + else if (field?.Properties?.IsUIProperty() == true) { - e(Not.Valid(nameof(rule.Action)), $"{rulePrefix}.{nameof(rule.Action)}"); + e(T.Get("schemas.fieldCannotBeUIField"), fieldPrefix); } }); - } - private static void ValidateFieldNames(IUpsertCommand command, FieldNames? fields, string path, AddValidation e, Func<string, bool> isAllowed) - { - if (fields != null) + foreach (var duplicate in fields.Duplicates()) { - fields.Foreach((fieldName, fieldIndex) => - { - var fieldPrefix = $"{path}[{fieldIndex}]"; - - var field = command?.Fields?.FirstOrDefault(x => x.Name == fieldName); - - if (string.IsNullOrWhiteSpace(fieldName)) - { - e(Not.Defined("Field"), fieldPrefix); - } - else if (field == null && !isAllowed(fieldName)) - { - e(T.Get("schemas.fieldNotInSchema"), fieldPrefix); - } - else if (field?.Properties?.IsUIProperty() == true) - { - e(T.Get("schemas.fieldCannotBeUIField"), fieldPrefix); - } - }); - - foreach (var duplicate in fields.Duplicates()) + if (!string.IsNullOrWhiteSpace(duplicate)) { - if (!string.IsNullOrWhiteSpace(duplicate)) - { - e(T.Get("schemas.duplicateFieldName", new { field = duplicate }), path); - } + e(T.Get("schemas.duplicateFieldName", new { field = duplicate }), path); } } } + } - private static bool IsMetaField(string field) - { - return field.StartsWith("meta.", StringComparison.Ordinal); - } + private static bool IsMetaField(string field) + { + return field.StartsWith("meta.", StringComparison.Ordinal); + } - private static bool IsNotAllowed(string field) - { - return false; - } + private static bool IsNotAllowed(string field) + { + return false; + } - private static void ValidateFieldIds<TField>(ReorderFields c, IReadOnlyDictionary<long, TField> fields, AddValidation e) + private static void ValidateFieldIds<TField>(ReorderFields c, IReadOnlyDictionary<long, TField> fields, AddValidation e) + { + if (c.FieldIds != null && (c.FieldIds.Length != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x)))) { - if (c.FieldIds != null && (c.FieldIds.Length != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x)))) - { - e(T.Get("schemas.fieldsNotCovered"), nameof(c.FieldIds)); - } + e(T.Get("schemas.fieldsNotCovered"), nameof(c.FieldIds)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchemaField.cs index ebd75073a6..1491aca200 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchemaField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchemaField.cs @@ -12,137 +12,136 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards; + +public static class GuardSchemaField { - public static class GuardSchemaField + public static void CanAdd(AddField command, Schema schema) { - public static void CanAdd(AddField command, Schema schema) + Guard.NotNull(command); + + Validate.It(e => { - Guard.NotNull(command); + if (!command.Name.IsPropertyName()) + { + e(Not.ValidJavascriptName(nameof(command.Name)), nameof(command.Name)); + } - Validate.It(e => + if (command.Properties == null) { - if (!command.Name.IsPropertyName()) - { - e(Not.ValidJavascriptName(nameof(command.Name)), nameof(command.Name)); - } + e(Not.Defined(nameof(command.Properties)), nameof(command.Properties)); + } + else + { + var errors = FieldPropertiesValidator.Validate(command.Properties); - if (command.Properties == null) + errors.Foreach((x, _) => x.WithPrefix(nameof(command.Properties)).AddTo(e)); + } + + if (command.ParentFieldId != null) + { + var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); + + if (arrayField.FieldsByName.ContainsKey(command.Name)) { - e(Not.Defined(nameof(command.Properties)), nameof(command.Properties)); + e(T.Get("schemas.fieldNameAlreadyExists")); } - else + } + else + { + if (command.ParentFieldId == null && !command.Partitioning.IsValidPartitioning()) { - var errors = FieldPropertiesValidator.Validate(command.Properties); - - errors.Foreach((x, _) => x.WithPrefix(nameof(command.Properties)).AddTo(e)); + e(Not.Valid("Partitioning"), nameof(command.Partitioning)); } - if (command.ParentFieldId != null) - { - var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false); - - if (arrayField.FieldsByName.ContainsKey(command.Name)) - { - e(T.Get("schemas.fieldNameAlreadyExists")); - } - } - else + if (schema.FieldsByName.ContainsKey(command.Name)) { - if (command.ParentFieldId == null && !command.Partitioning.IsValidPartitioning()) - { - e(Not.Valid("Partitioning"), nameof(command.Partitioning)); - } - - if (schema.FieldsByName.ContainsKey(command.Name)) - { - e(T.Get("schemas.fieldNameAlreadyExists")); - } + e(T.Get("schemas.fieldNameAlreadyExists")); } - }); - } + } + }); + } - public static void CanUpdate(UpdateField command, Schema schema) - { - Guard.NotNull(command); + public static void CanUpdate(UpdateField command, Schema schema) + { + Guard.NotNull(command); - GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - Validate.It(e => + Validate.It(e => + { + if (command.Properties == null) { - if (command.Properties == null) - { - e(Not.Defined("Properties"), nameof(command.Properties)); - } - else - { - var errors = FieldPropertiesValidator.Validate(command.Properties); + e(Not.Defined("Properties"), nameof(command.Properties)); + } + else + { + var errors = FieldPropertiesValidator.Validate(command.Properties); - errors.Foreach((x, _) => x.WithPrefix(nameof(command.Properties)).AddTo(e)); - } - }); - } + errors.Foreach((x, _) => x.WithPrefix(nameof(command.Properties)).AddTo(e)); + } + }); + } - public static void CanHide(HideField command, Schema schema) - { - Guard.NotNull(command); + public static void CanHide(HideField command, Schema schema) + { + Guard.NotNull(command); - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - if (!field.IsForApi(true)) - { - throw new DomainException(T.Get("schemas.uiFieldCannotBeHidden")); - } + if (!field.IsForApi(true)) + { + throw new DomainException(T.Get("schemas.uiFieldCannotBeHidden")); } + } - public static void CanDisable(DisableField command, Schema schema) - { - Guard.NotNull(command); + public static void CanDisable(DisableField command, Schema schema) + { + Guard.NotNull(command); - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - if (!field.IsForApi(true)) - { - throw new DomainException(T.Get("schemas.uiFieldCannotBeDisabled")); - } + if (!field.IsForApi(true)) + { + throw new DomainException(T.Get("schemas.uiFieldCannotBeDisabled")); } + } - public static void CanShow(ShowField command, Schema schema) - { - Guard.NotNull(command); + public static void CanShow(ShowField command, Schema schema) + { + Guard.NotNull(command); - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - if (!field.IsForApi(true)) - { - throw new DomainException(T.Get("schemas.uiFieldCannotBeShown")); - } + if (!field.IsForApi(true)) + { + throw new DomainException(T.Get("schemas.uiFieldCannotBeShown")); } + } - public static void CanEnable(EnableField command, Schema schema) - { - Guard.NotNull(command); + public static void CanEnable(EnableField command, Schema schema) + { + Guard.NotNull(command); - var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - if (!field.IsForApi(true)) - { - throw new DomainException(T.Get("schemas.uiFieldCannotBeEnabled")); - } + if (!field.IsForApi(true)) + { + throw new DomainException(T.Get("schemas.uiFieldCannotBeEnabled")); } + } - public static void CanDelete(DeleteField command, Schema schema) - { - Guard.NotNull(command); + public static void CanDelete(DeleteField command, Schema schema) + { + Guard.NotNull(command); - GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); - } + GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false); + } - public static void CanLock(LockField command, Schema schema) - { - Guard.NotNull(command); + public static void CanLock(LockField command, Schema schema) + { + Guard.NotNull(command); - GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, true); - } + GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, true); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs index f482a6b5a3..0477231d34 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs @@ -13,180 +13,179 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject; + +public sealed partial class SchemaDomainObject { - public sealed partial class SchemaDomainObject + [CollectionName("Schemas")] + public sealed class State : DomainObjectState<State>, ISchemaEntity { - [CollectionName("Schemas")] - public sealed class State : DomainObjectState<State>, ISchemaEntity - { - public NamedId<DomainId> AppId { get; set; } - - public Schema SchemaDef { get; set; } - - public long SchemaFieldsTotal { get; set; } - - public bool IsDeleted { get; set; } - - [JsonIgnore] - public DomainId UniqueId - { - get => DomainId.Combine(AppId, Id); - } + public NamedId<DomainId> AppId { get; set; } - public override bool ApplyEvent(IEvent @event) - { - var previousSchema = SchemaDef; - - switch (@event) - { - case SchemaCreated e: - { - Id = e.SchemaId.Id; - - SchemaDef = e.Schema; - SchemaFieldsTotal = e.Schema.MaxId(); - - AppId = e.AppId; - return true; - } - - case FieldAdded e: - { - if (e.ParentFieldId != null) - { - var field = e.Properties.CreateNestedField(e.FieldId.Id, e.Name); - - SchemaDef = SchemaDef.UpdateField(e.ParentFieldId.Id, x => ((ArrayField)x).AddField(field)); - } - else - { - var partitioning = Partitioning.FromString(e.Partitioning); - - var field = e.Properties.CreateRootField(e.FieldId.Id, e.Name, partitioning); - - SchemaDef = SchemaDef.DeleteField(e.FieldId.Id); - SchemaDef = SchemaDef.AddField(field); - } - - SchemaFieldsTotal = Math.Max(SchemaFieldsTotal, e.FieldId.Id); - break; - } - - case SchemaUIFieldsConfigured e: - { - if (e.FieldsInLists != null) - { - SchemaDef = SchemaDef.SetFieldsInLists(e.FieldsInLists); - } - - if (e.FieldsInReferences != null) - { - SchemaDef = SchemaDef.SetFieldsInReferences(e.FieldsInReferences); - } - - break; - } - - case SchemaCategoryChanged e: - { - SchemaDef = SchemaDef.ChangeCategory(e.Name); - break; - } - - case SchemaPreviewUrlsConfigured e: - { - SchemaDef = SchemaDef.SetPreviewUrls(e.PreviewUrls); - break; - } + public Schema SchemaDef { get; set; } - case SchemaScriptsConfigured e: - { - SchemaDef = SchemaDef.SetScripts(e.Scripts); - break; - } + public long SchemaFieldsTotal { get; set; } - case SchemaFieldRulesConfigured e: - { - SchemaDef = SchemaDef.SetFieldRules(e.FieldRules); - break; - } + public bool IsDeleted { get; set; } - case SchemaPublished: - { - SchemaDef = SchemaDef.Publish(); - break; - } + [JsonIgnore] + public DomainId UniqueId + { + get => DomainId.Combine(AppId, Id); + } - case SchemaUnpublished: - { - SchemaDef = SchemaDef.Unpublish(); - break; - } + public override bool ApplyEvent(IEvent @event) + { + var previousSchema = SchemaDef; - case SchemaUpdated e: - { - SchemaDef = SchemaDef.Update(e.Properties); - break; - } + switch (@event) + { + case SchemaCreated e: + { + Id = e.SchemaId.Id; - case SchemaFieldsReordered e: - { - SchemaDef = SchemaDef.ReorderFields(e.FieldIds.ToList(), e.ParentFieldId?.Id); - break; - } + SchemaDef = e.Schema; + SchemaFieldsTotal = e.Schema.MaxId(); - case FieldUpdated e: - { - SchemaDef = SchemaDef.UpdateField(e.FieldId.Id, e.Properties, e.ParentFieldId?.Id); - break; - } + AppId = e.AppId; + return true; + } - case FieldLocked e: + case FieldAdded e: + { + if (e.ParentFieldId != null) { - SchemaDef = SchemaDef.LockField(e.FieldId.Id, e.ParentFieldId?.Id); - break; - } + var field = e.Properties.CreateNestedField(e.FieldId.Id, e.Name); - case FieldDisabled e: - { - SchemaDef = SchemaDef.DisableField(e.FieldId.Id, e.ParentFieldId?.Id); - break; + SchemaDef = SchemaDef.UpdateField(e.ParentFieldId.Id, x => ((ArrayField)x).AddField(field)); } - - case FieldEnabled e: + else { - SchemaDef = SchemaDef.EnableField(e.FieldId.Id, e.ParentFieldId?.Id); - break; - } + var partitioning = Partitioning.FromString(e.Partitioning); - case FieldHidden e: - { - SchemaDef = SchemaDef.HideField(e.FieldId.Id, e.ParentFieldId?.Id); - break; - } + var field = e.Properties.CreateRootField(e.FieldId.Id, e.Name, partitioning); - case FieldShown e: - { - SchemaDef = SchemaDef.ShowField(e.FieldId.Id, e.ParentFieldId?.Id); - break; + SchemaDef = SchemaDef.DeleteField(e.FieldId.Id); + SchemaDef = SchemaDef.AddField(field); } - case FieldDeleted e: - { - SchemaDef = SchemaDef.DeleteField(e.FieldId.Id, e.ParentFieldId?.Id); - break; - } + SchemaFieldsTotal = Math.Max(SchemaFieldsTotal, e.FieldId.Id); + break; + } - case SchemaDeleted: - { - IsDeleted = true; - return true; + case SchemaUIFieldsConfigured e: + { + if (e.FieldsInLists != null) + { + SchemaDef = SchemaDef.SetFieldsInLists(e.FieldsInLists); } - } - - return !ReferenceEquals(previousSchema, SchemaDef); + + if (e.FieldsInReferences != null) + { + SchemaDef = SchemaDef.SetFieldsInReferences(e.FieldsInReferences); + } + + break; + } + + case SchemaCategoryChanged e: + { + SchemaDef = SchemaDef.ChangeCategory(e.Name); + break; + } + + case SchemaPreviewUrlsConfigured e: + { + SchemaDef = SchemaDef.SetPreviewUrls(e.PreviewUrls); + break; + } + + case SchemaScriptsConfigured e: + { + SchemaDef = SchemaDef.SetScripts(e.Scripts); + break; + } + + case SchemaFieldRulesConfigured e: + { + SchemaDef = SchemaDef.SetFieldRules(e.FieldRules); + break; + } + + case SchemaPublished: + { + SchemaDef = SchemaDef.Publish(); + break; + } + + case SchemaUnpublished: + { + SchemaDef = SchemaDef.Unpublish(); + break; + } + + case SchemaUpdated e: + { + SchemaDef = SchemaDef.Update(e.Properties); + break; + } + + case SchemaFieldsReordered e: + { + SchemaDef = SchemaDef.ReorderFields(e.FieldIds.ToList(), e.ParentFieldId?.Id); + break; + } + + case FieldUpdated e: + { + SchemaDef = SchemaDef.UpdateField(e.FieldId.Id, e.Properties, e.ParentFieldId?.Id); + break; + } + + case FieldLocked e: + { + SchemaDef = SchemaDef.LockField(e.FieldId.Id, e.ParentFieldId?.Id); + break; + } + + case FieldDisabled e: + { + SchemaDef = SchemaDef.DisableField(e.FieldId.Id, e.ParentFieldId?.Id); + break; + } + + case FieldEnabled e: + { + SchemaDef = SchemaDef.EnableField(e.FieldId.Id, e.ParentFieldId?.Id); + break; + } + + case FieldHidden e: + { + SchemaDef = SchemaDef.HideField(e.FieldId.Id, e.ParentFieldId?.Id); + break; + } + + case FieldShown e: + { + SchemaDef = SchemaDef.ShowField(e.FieldId.Id, e.ParentFieldId?.Id); + break; + } + + case FieldDeleted e: + { + SchemaDef = SchemaDef.DeleteField(e.FieldId.Id, e.ParentFieldId?.Id); + break; + } + + case SchemaDeleted: + { + IsDeleted = true; + return true; + } } + + return !ReferenceEquals(previousSchema, SchemaDef); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs index ff88414647..cab04a50c3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs @@ -20,396 +20,395 @@ #pragma warning disable MA0022 // Return Task.FromResult instead of returning null -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject; + +public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject.State> { - public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject.State> + public SchemaDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<SchemaDomainObject> log) + : base(id, persistence, log) { - public SchemaDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<SchemaDomainObject> log) - : base(id, persistence, log) - { - } + } - protected override bool IsDeleted(State snapshot) - { - return snapshot.IsDeleted; - } + protected override bool IsDeleted(State snapshot) + { + return snapshot.IsDeleted; + } - protected override bool CanAcceptCreation(ICommand command) - { - return command is SchemaCommandBase; - } + protected override bool CanAcceptCreation(ICommand command) + { + return command is SchemaCommandBase; + } - protected override bool CanAccept(ICommand command) - { - return command is SchemaCommand schemaCommand && - Equals(schemaCommand.AppId, Snapshot.AppId) && - Equals(schemaCommand.SchemaId?.Id, Snapshot.Id); - } + protected override bool CanAccept(ICommand command) + { + return command is SchemaCommand schemaCommand && + Equals(schemaCommand.AppId, Snapshot.AppId) && + Equals(schemaCommand.SchemaId?.Id, Snapshot.Id); + } - public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct) + public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + switch (command) { - switch (command) - { - case AddField addField: - return UpdateReturn(addField, c => - { - GuardSchemaField.CanAdd(c, Snapshot.SchemaDef); + case AddField addField: + return UpdateReturn(addField, c => + { + GuardSchemaField.CanAdd(c, Snapshot.SchemaDef); - AddField(c); + AddField(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case CreateSchema createSchema: - return CreateReturn(createSchema, c => - { - GuardSchema.CanCreate(c); + case CreateSchema createSchema: + return CreateReturn(createSchema, c => + { + GuardSchema.CanCreate(c); - Create(c); + Create(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case SynchronizeSchema synchronize: - return UpdateReturn(synchronize, c => - { - GuardSchema.CanSynchronize(c); + case SynchronizeSchema synchronize: + return UpdateReturn(synchronize, c => + { + GuardSchema.CanSynchronize(c); - Synchronize(c); + Synchronize(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case DeleteField deleteField: - return UpdateReturn(deleteField, c => - { - GuardSchemaField.CanDelete(deleteField, Snapshot.SchemaDef); + case DeleteField deleteField: + return UpdateReturn(deleteField, c => + { + GuardSchemaField.CanDelete(deleteField, Snapshot.SchemaDef); - DeleteField(c); + DeleteField(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case LockField lockField: - return UpdateReturn(lockField, c => - { - GuardSchemaField.CanLock(lockField, Snapshot.SchemaDef); + case LockField lockField: + return UpdateReturn(lockField, c => + { + GuardSchemaField.CanLock(lockField, Snapshot.SchemaDef); - LockField(c); + LockField(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case HideField hideField: - return UpdateReturn(hideField, c => - { - GuardSchemaField.CanHide(c, Snapshot.SchemaDef); + case HideField hideField: + return UpdateReturn(hideField, c => + { + GuardSchemaField.CanHide(c, Snapshot.SchemaDef); - HideField(c); + HideField(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case ShowField showField: - return UpdateReturn(showField, c => - { - GuardSchemaField.CanShow(c, Snapshot.SchemaDef); + case ShowField showField: + return UpdateReturn(showField, c => + { + GuardSchemaField.CanShow(c, Snapshot.SchemaDef); - ShowField(c); + ShowField(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case DisableField disableField: - return UpdateReturn(disableField, c => - { - GuardSchemaField.CanDisable(c, Snapshot.SchemaDef); + case DisableField disableField: + return UpdateReturn(disableField, c => + { + GuardSchemaField.CanDisable(c, Snapshot.SchemaDef); - DisableField(c); + DisableField(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case EnableField enableField: - return UpdateReturn(enableField, c => - { - GuardSchemaField.CanEnable(c, Snapshot.SchemaDef); + case EnableField enableField: + return UpdateReturn(enableField, c => + { + GuardSchemaField.CanEnable(c, Snapshot.SchemaDef); - EnableField(c); + EnableField(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case UpdateField updateField: - return UpdateReturn(updateField, c => - { - GuardSchemaField.CanUpdate(c, Snapshot.SchemaDef); + case UpdateField updateField: + return UpdateReturn(updateField, c => + { + GuardSchemaField.CanUpdate(c, Snapshot.SchemaDef); - UpdateField(c); + UpdateField(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case ReorderFields reorderFields: - return UpdateReturn(reorderFields, c => - { - GuardSchema.CanReorder(c, Snapshot.SchemaDef); + case ReorderFields reorderFields: + return UpdateReturn(reorderFields, c => + { + GuardSchema.CanReorder(c, Snapshot.SchemaDef); - Reorder(c); + Reorder(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case ConfigureFieldRules configureFieldRules: - return UpdateReturn(configureFieldRules, c => - { - GuardSchema.CanConfigureFieldRules(c); + case ConfigureFieldRules configureFieldRules: + return UpdateReturn(configureFieldRules, c => + { + GuardSchema.CanConfigureFieldRules(c); - ConfigureFieldRules(c); + ConfigureFieldRules(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case ConfigurePreviewUrls configurePreviewUrls: - return UpdateReturn(configurePreviewUrls, c => - { - GuardSchema.CanConfigurePreviewUrls(c); + case ConfigurePreviewUrls configurePreviewUrls: + return UpdateReturn(configurePreviewUrls, c => + { + GuardSchema.CanConfigurePreviewUrls(c); - ConfigurePreviewUrls(c); + ConfigurePreviewUrls(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case ConfigureUIFields configureUIFields: - return UpdateReturn(configureUIFields, c => - { - GuardSchema.CanConfigureUIFields(c, Snapshot.SchemaDef); + case ConfigureUIFields configureUIFields: + return UpdateReturn(configureUIFields, c => + { + GuardSchema.CanConfigureUIFields(c, Snapshot.SchemaDef); - ConfigureUIFields(c); + ConfigureUIFields(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case ChangeCategory changeCategory: - return UpdateReturn(changeCategory, c => - { - ChangeCategory(c); + case ChangeCategory changeCategory: + return UpdateReturn(changeCategory, c => + { + ChangeCategory(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case UpdateSchema update: - return UpdateReturn(update, c => - { - Update(c); + case UpdateSchema update: + return UpdateReturn(update, c => + { + Update(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case PublishSchema publish: - return UpdateReturn(publish, c => - { - Publish(c); + case PublishSchema publish: + return UpdateReturn(publish, c => + { + Publish(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case UnpublishSchema unpublish: - return UpdateReturn(unpublish, c => - { - Unpublish(c); + case UnpublishSchema unpublish: + return UpdateReturn(unpublish, c => + { + Unpublish(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case ConfigureScripts configureScripts: - return UpdateReturn(configureScripts, c => - { - ConfigureScripts(c); + case ConfigureScripts configureScripts: + return UpdateReturn(configureScripts, c => + { + ConfigureScripts(c); - return Snapshot; - }, ct); + return Snapshot; + }, ct); - case DeleteSchema deleteSchema: - return Update(deleteSchema, c => - { - Delete(c); - }, ct); + case DeleteSchema deleteSchema: + return Update(deleteSchema, c => + { + Delete(c); + }, ct); - default: - ThrowHelper.NotSupportedException(); - return default!; - } + default: + ThrowHelper.NotSupportedException(); + return default!; } + } - private void Synchronize(SynchronizeSchema command) + private void Synchronize(SynchronizeSchema command) + { + var options = new SchemaSynchronizationOptions { - var options = new SchemaSynchronizationOptions - { - NoFieldDeletion = command.NoFieldDeletion, - NoFieldRecreation = command.NoFieldRecreation - }; - - var schemaSource = Snapshot.SchemaDef; - var schemaTarget = command.BuildSchema(schemaSource.Name, schemaSource.Type); + NoFieldDeletion = command.NoFieldDeletion, + NoFieldRecreation = command.NoFieldRecreation + }; - var events = schemaSource.Synchronize(schemaTarget, () => Snapshot.SchemaFieldsTotal + 1, options); + var schemaSource = Snapshot.SchemaDef; + var schemaTarget = command.BuildSchema(schemaSource.Name, schemaSource.Type); - foreach (var @event in events) - { - Raise(command, @event); - } - } + var events = schemaSource.Synchronize(schemaTarget, () => Snapshot.SchemaFieldsTotal + 1, options); - private void Create(CreateSchema command) + foreach (var @event in events) { - Raise(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.BuildSchema() }); + Raise(command, @event); } + } - private void AddField(AddField command) - { - Raise(command, new FieldAdded { FieldId = CreateFieldId(command) }); - } + private void Create(CreateSchema command) + { + Raise(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.BuildSchema() }); + } - private void UpdateField(UpdateField command) - { - Raise(command, new FieldUpdated()); - } + private void AddField(AddField command) + { + Raise(command, new FieldAdded { FieldId = CreateFieldId(command) }); + } - private void LockField(LockField command) - { - Raise(command, new FieldLocked()); - } + private void UpdateField(UpdateField command) + { + Raise(command, new FieldUpdated()); + } - private void HideField(HideField command) - { - Raise(command, new FieldHidden()); - } + private void LockField(LockField command) + { + Raise(command, new FieldLocked()); + } - private void ShowField(ShowField command) - { - Raise(command, new FieldShown()); - } + private void HideField(HideField command) + { + Raise(command, new FieldHidden()); + } - private void DisableField(DisableField command) - { - Raise(command, new FieldDisabled()); - } + private void ShowField(ShowField command) + { + Raise(command, new FieldShown()); + } - private void EnableField(EnableField command) - { - Raise(command, new FieldEnabled()); - } + private void DisableField(DisableField command) + { + Raise(command, new FieldDisabled()); + } - private void DeleteField(DeleteField command) - { - Raise(command, new FieldDeleted()); - } + private void EnableField(EnableField command) + { + Raise(command, new FieldEnabled()); + } - private void Reorder(ReorderFields command) - { - Raise(command, new SchemaFieldsReordered()); - } + private void DeleteField(DeleteField command) + { + Raise(command, new FieldDeleted()); + } - private void Publish(PublishSchema command) - { - Raise(command, new SchemaPublished()); - } + private void Reorder(ReorderFields command) + { + Raise(command, new SchemaFieldsReordered()); + } - private void Unpublish(UnpublishSchema command) - { - Raise(command, new SchemaUnpublished()); - } + private void Publish(PublishSchema command) + { + Raise(command, new SchemaPublished()); + } - private void ConfigureScripts(ConfigureScripts command) - { - Raise(command, new SchemaScriptsConfigured()); - } + private void Unpublish(UnpublishSchema command) + { + Raise(command, new SchemaUnpublished()); + } - private void ConfigureFieldRules(ConfigureFieldRules command) - { - Raise(command, new SchemaFieldRulesConfigured { FieldRules = command.ToFieldRules() }); - } + private void ConfigureScripts(ConfigureScripts command) + { + Raise(command, new SchemaScriptsConfigured()); + } - private void ChangeCategory(ChangeCategory command) - { - Raise(command, new SchemaCategoryChanged()); - } + private void ConfigureFieldRules(ConfigureFieldRules command) + { + Raise(command, new SchemaFieldRulesConfigured { FieldRules = command.ToFieldRules() }); + } - private void ConfigurePreviewUrls(ConfigurePreviewUrls command) - { - Raise(command, new SchemaPreviewUrlsConfigured()); - } + private void ChangeCategory(ChangeCategory command) + { + Raise(command, new SchemaCategoryChanged()); + } - private void ConfigureUIFields(ConfigureUIFields command) - { - Raise(command, new SchemaUIFieldsConfigured()); - } + private void ConfigurePreviewUrls(ConfigurePreviewUrls command) + { + Raise(command, new SchemaPreviewUrlsConfigured()); + } - private void Update(UpdateSchema command) - { - Raise(command, new SchemaUpdated()); - } + private void ConfigureUIFields(ConfigureUIFields command) + { + Raise(command, new SchemaUIFieldsConfigured()); + } - private void Delete(DeleteSchema command) - { - Raise(command, new SchemaDeleted()); - } + private void Update(UpdateSchema command) + { + Raise(command, new SchemaUpdated()); + } - private void Raise<T, TEvent>(T command, TEvent @event) where TEvent : SchemaEvent where T : class - { - SimpleMapper.Map(command, @event); + private void Delete(DeleteSchema command) + { + Raise(command, new SchemaDeleted()); + } - NamedId<long>? GetFieldId(long? id) - { - if (id != null && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) - { - return field.NamedId(); - } + private void Raise<T, TEvent>(T command, TEvent @event) where TEvent : SchemaEvent where T : class + { + SimpleMapper.Map(command, @event); - return null; + NamedId<long>? GetFieldId(long? id) + { + if (id != null && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field)) + { + return field.NamedId(); } - if (command is ParentFieldCommand parentField && @event is ParentFieldEvent parentFieldEvent) + return null; + } + + if (command is ParentFieldCommand parentField && @event is ParentFieldEvent parentFieldEvent) + { + if (parentField.ParentFieldId != null) { - if (parentField.ParentFieldId != null) + if (Snapshot.SchemaDef.FieldsById.TryGetValue(parentField.ParentFieldId.Value, out var field)) { - if (Snapshot.SchemaDef.FieldsById.TryGetValue(parentField.ParentFieldId.Value, out var field)) - { - parentFieldEvent.ParentFieldId = field.NamedId(); + parentFieldEvent.ParentFieldId = field.NamedId(); - if (command is FieldCommand fc && @event is FieldEvent fe) + if (command is FieldCommand fc && @event is FieldEvent fe) + { + if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fc.FieldId, out var nestedField)) { - if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fc.FieldId, out var nestedField)) - { - fe.FieldId = nestedField.NamedId(); - } + fe.FieldId = nestedField.NamedId(); } } } - else if (command is FieldCommand fc && @event is FieldEvent fe) - { - fe.FieldId = GetFieldId(fc.FieldId)!; - } } + else if (command is FieldCommand fc && @event is FieldEvent fe) + { + fe.FieldId = GetFieldId(fc.FieldId)!; + } + } - SimpleMapper.Map(command, @event); + SimpleMapper.Map(command, @event); - @event.AppId ??= Snapshot.AppId; - @event.SchemaId ??= Snapshot.NamedId(); + @event.AppId ??= Snapshot.AppId; + @event.SchemaId ??= Snapshot.NamedId(); - RaiseEvent(Envelope.Create(@event)); - } + RaiseEvent(Envelope.Create(@event)); + } - private NamedId<long> CreateFieldId(AddField command) - { - return NamedId.Of(Snapshot.SchemaFieldsTotal + 1, command.Name); - } + private NamedId<long> CreateFieldId(AddField command) + { + return NamedId.Of(Snapshot.SchemaFieldsTotal + 1, command.Name); + } - public Task<ISchemaEntity> GetStateAsync() - { - return Task.FromResult<ISchemaEntity>(Snapshot); - } + public Task<ISchemaEntity> GetStateAsync() + { + return Task.FromResult<ISchemaEntity>(Snapshot); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs index c2889b6ad1..00e625ca9f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs @@ -8,18 +8,17 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public interface ISchemaEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion { - public interface ISchemaEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - NamedId<DomainId> AppId { get; } + NamedId<DomainId> AppId { get; } - bool IsDeleted { get; } + bool IsDeleted { get; } - Schema SchemaDef { get; } - } + Schema SchemaDef { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs index 5b1c744d67..a88abee869 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs @@ -8,14 +8,13 @@ using NodaTime; using Squidex.Domain.Apps.Entities.Apps; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public interface ISchemasHash { - public interface ISchemasHash - { - Task<(Instant Create, string Hash)> GetCurrentHashAsync(IAppEntity app, - CancellationToken ct = default); + Task<(Instant Create, string Hash)> GetCurrentHashAsync(IAppEntity app, + CancellationToken ct = default); - ValueTask<string> ComputeHashAsync(IAppEntity app, IEnumerable<ISchemaEntity> schemas, - CancellationToken ct = default); - } + ValueTask<string> ComputeHashAsync(IAppEntity app, IEnumerable<ISchemaEntity> schemas, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs index 1488b3511e..17cd52742f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes; + +public interface ISchemasIndex { - public interface ISchemasIndex - { - Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, DomainId id, bool canCache, - CancellationToken ct = default); + Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, DomainId id, bool canCache, + CancellationToken ct = default); - Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, string name, bool canCache, - CancellationToken ct = default); + Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, string name, bool canCache, + CancellationToken ct = default); - Task<List<ISchemaEntity>> GetSchemasAsync(DomainId appId, - CancellationToken ct = default); - } + Task<List<ISchemaEntity>> GetSchemasAsync(DomainId appId, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs index 320f8aaea5..ebc866a07b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -14,215 +14,214 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes; + +public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex { - public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); + private readonly ISchemaRepository schemaRepository; + private readonly IReplicatedCache schemaCache; + private readonly IPersistenceFactory<NameReservationState.State> persistenceFactory; + + public SchemasIndex(ISchemaRepository schemaRepository, IReplicatedCache schemaCache, + IPersistenceFactory<NameReservationState.State> persistenceFactory) { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); - private readonly ISchemaRepository schemaRepository; - private readonly IReplicatedCache schemaCache; - private readonly IPersistenceFactory<NameReservationState.State> persistenceFactory; + this.schemaRepository = schemaRepository; + this.schemaCache = schemaCache; + this.persistenceFactory = persistenceFactory; + } - public SchemasIndex(ISchemaRepository schemaRepository, IReplicatedCache schemaCache, - IPersistenceFactory<NameReservationState.State> persistenceFactory) + public async Task<List<ISchemaEntity>> GetSchemasAsync(DomainId appId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemasAsync")) { - this.schemaRepository = schemaRepository; - this.schemaCache = schemaCache; - this.persistenceFactory = persistenceFactory; - } + var schemas = await schemaRepository.QueryAllAsync(appId, ct); - public async Task<List<ISchemaEntity>> GetSchemasAsync(DomainId appId, - CancellationToken ct = default) - { - using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemasAsync")) + foreach (var schema in schemas.Where(IsValid)) { - var schemas = await schemaRepository.QueryAllAsync(appId, ct); - - foreach (var schema in schemas.Where(IsValid)) - { - await InvalidateItAsync(appId, schema.Id, schema.SchemaDef.Name); - } - - return schemas.Where(IsValid).ToList(); + await InvalidateItAsync(appId, schema.Id, schema.SchemaDef.Name); } + + return schemas.Where(IsValid).ToList(); } + } - public async Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, string name, bool canCache, - CancellationToken ct = default) + public async Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, string name, bool canCache, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaByNameAsync")) { - using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaByNameAsync")) - { - var cacheKey = GetCacheKey(appId, name); + var cacheKey = GetCacheKey(appId, name); - if (canCache) + if (canCache) + { + if (schemaCache.TryGetValue(cacheKey, out var value) && value is ISchemaEntity cachedSchema) { - if (schemaCache.TryGetValue(cacheKey, out var value) && value is ISchemaEntity cachedSchema) - { - return cachedSchema; - } + return cachedSchema; } + } - var schema = await schemaRepository.FindAsync(appId, name, ct); - - if (!IsValid(schema)) - { - schema = null; - } + var schema = await schemaRepository.FindAsync(appId, name, ct); - if (schema != null) - { - await CacheItAsync(schema); - } + if (!IsValid(schema)) + { + schema = null; + } - return schema; + if (schema != null) + { + await CacheItAsync(schema); } + + return schema; } + } - public async Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, DomainId id, bool canCache, - CancellationToken ct = default) + public async Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, DomainId id, bool canCache, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaAsync")) { - using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaAsync")) - { - var cacheKey = GetCacheKey(appId, id); + var cacheKey = GetCacheKey(appId, id); - if (canCache) + if (canCache) + { + if (schemaCache.TryGetValue(cacheKey, out var v) && v is ISchemaEntity cachedSchema) { - if (schemaCache.TryGetValue(cacheKey, out var v) && v is ISchemaEntity cachedSchema) - { - return cachedSchema; - } + return cachedSchema; } + } - var schema = await schemaRepository.FindAsync(appId, id, ct); + var schema = await schemaRepository.FindAsync(appId, id, ct); - if (!IsValid(schema)) - { - schema = null; - } - - if (schema != null) - { - await CacheItAsync(schema); - } + if (!IsValid(schema)) + { + schema = null; + } - return schema; + if (schema != null) + { + await CacheItAsync(schema); } + + return schema; } + } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - var command = context.Command; + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + var command = context.Command; - if (command is CreateSchema createSchema) - { - var names = await GetNamesAsync(createSchema.AppId.Id, ct); + if (command is CreateSchema createSchema) + { + var names = await GetNamesAsync(createSchema.AppId.Id, ct); - var token = await CheckSchemaAsync(createSchema, names, ct); - try - { - await next(context, ct); - } - finally - { - // Always remove the reservation and therefore do not pass over cancellation token. - await names.RemoveReservationAsync(token, default); - } - } - else + var token = await CheckSchemaAsync(createSchema, names, ct); + try { await next(context, ct); } - - if (context.IsCompleted) + finally { - switch (command) - { - case CreateSchema create: - await OnCreateAsync(create); - break; - case DeleteSchema delete: - await OnDeleteAsync(delete); - break; - case SchemaCommand update: - await OnUpdateAsync(update); - break; - } + // Always remove the reservation and therefore do not pass over cancellation token. + await names.RemoveReservationAsync(token, default); } } - - private Task OnCreateAsync(CreateSchema create) + else { - return InvalidateItAsync(create.AppId.Id, create.SchemaId, create.Name); + await next(context, ct); } - private Task OnDeleteAsync(DeleteSchema delete) + if (context.IsCompleted) { - return InvalidateItAsync(delete.AppId.Id, delete.SchemaId.Id, delete.SchemaId.Name); - } - - private Task OnUpdateAsync(SchemaCommand update) - { - return InvalidateItAsync(update.AppId.Id, update.SchemaId.Id, update.SchemaId.Name); + switch (command) + { + case CreateSchema create: + await OnCreateAsync(create); + break; + case DeleteSchema delete: + await OnDeleteAsync(delete); + break; + case SchemaCommand update: + await OnUpdateAsync(update); + break; + } } + } - private async Task<string> CheckSchemaAsync(CreateSchema command, NameReservationState names, - CancellationToken ct) - { - var existing = await schemaRepository.FindAsync(command.AppId.Id, command.Name, ct); + private Task OnCreateAsync(CreateSchema create) + { + return InvalidateItAsync(create.AppId.Id, create.SchemaId, create.Name); + } - if (existing is { IsDeleted: false }) - { - throw new ValidationException(T.Get("schemas.nameAlreadyExists")); - } + private Task OnDeleteAsync(DeleteSchema delete) + { + return InvalidateItAsync(delete.AppId.Id, delete.SchemaId.Id, delete.SchemaId.Name); + } - var token = await names.ReserveAsync(command.SchemaId, command.Name, ct); + private Task OnUpdateAsync(SchemaCommand update) + { + return InvalidateItAsync(update.AppId.Id, update.SchemaId.Id, update.SchemaId.Name); + } - if (token == null) - { - throw new ValidationException(T.Get("schemas.nameAlreadyExists")); - } + private async Task<string> CheckSchemaAsync(CreateSchema command, NameReservationState names, + CancellationToken ct) + { + var existing = await schemaRepository.FindAsync(command.AppId.Id, command.Name, ct); - return token; + if (existing is { IsDeleted: false }) + { + throw new ValidationException(T.Get("schemas.nameAlreadyExists")); } - private async Task<NameReservationState> GetNamesAsync(DomainId appId, - CancellationToken ct) + var token = await names.ReserveAsync(command.SchemaId, command.Name, ct); + + if (token == null) { - var state = new NameReservationState(persistenceFactory, $"{appId}_Schemas"); + throw new ValidationException(T.Get("schemas.nameAlreadyExists")); + } - await state.LoadAsync(ct); + return token; + } - return state; - } + private async Task<NameReservationState> GetNamesAsync(DomainId appId, + CancellationToken ct) + { + var state = new NameReservationState(persistenceFactory, $"{appId}_Schemas"); - private static string GetCacheKey(DomainId appId, string name) - { - return $"{typeof(SchemasIndex)}_Schemas_Name_{appId}_{name}"; - } + await state.LoadAsync(ct); - private static string GetCacheKey(DomainId appId, DomainId id) - { - return $"{typeof(SchemasIndex)}_Schemas_Id_{appId}_{id}"; - } + return state; + } - private static bool IsValid(ISchemaEntity? schema) - { - return schema is { Version: > EtagVersion.Empty, IsDeleted: false }; - } + private static string GetCacheKey(DomainId appId, string name) + { + return $"{typeof(SchemasIndex)}_Schemas_Name_{appId}_{name}"; + } - private Task InvalidateItAsync(DomainId appId, DomainId id, string name) - { - return schemaCache.RemoveAsync( - GetCacheKey(appId, id), - GetCacheKey(appId, name)); - } + private static string GetCacheKey(DomainId appId, DomainId id) + { + return $"{typeof(SchemasIndex)}_Schemas_Id_{appId}_{id}"; + } - private Task CacheItAsync(ISchemaEntity schema) - { - return Task.WhenAll( - schemaCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration), - schemaCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration)); - } + private static bool IsValid(ISchemaEntity? schema) + { + return schema is { Version: > EtagVersion.Empty, IsDeleted: false }; + } + + private Task InvalidateItAsync(DomainId appId, DomainId id, string name) + { + return schemaCache.RemoveAsync( + GetCacheKey(appId, id), + GetCacheKey(appId, name)); + } + + private Task CacheItAsync(ISchemaEntity schema) + { + return Task.WhenAll( + schemaCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration), + schemaCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration)); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs index dfbab4bbb9..fa09944b5a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Schemas.Repositories +namespace Squidex.Domain.Apps.Entities.Schemas.Repositories; + +public interface ISchemaRepository { - public interface ISchemaRepository - { - Task<List<ISchemaEntity>> QueryAllAsync(DomainId appId, - CancellationToken ct = default); + Task<List<ISchemaEntity>> QueryAllAsync(DomainId appId, + CancellationToken ct = default); - Task<ISchemaEntity?> FindAsync(DomainId appId, DomainId id, - CancellationToken ct = default); + Task<ISchemaEntity?> FindAsync(DomainId appId, DomainId id, + CancellationToken ct = default); - Task<ISchemaEntity?> FindAsync(DomainId appId, string name, - CancellationToken ct = default); - } + Task<ISchemaEntity?> FindAsync(DomainId appId, string name, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs index 2eda50aa0f..7d8ddfdf3a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs @@ -15,78 +15,77 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public sealed class SchemaChangedTriggerHandler : IRuleTriggerHandler { - public sealed class SchemaChangedTriggerHandler : IRuleTriggerHandler + private readonly IScriptEngine scriptEngine; + + public Type TriggerType => typeof(SchemaChangedTrigger); + + public SchemaChangedTriggerHandler(IScriptEngine scriptEngine) { - private readonly IScriptEngine scriptEngine; + this.scriptEngine = scriptEngine; + } - public Type TriggerType => typeof(SchemaChangedTrigger); + public bool Handles(AppEvent appEvent) + { + return appEvent is SchemaEvent; + } - public SchemaChangedTriggerHandler(IScriptEngine scriptEngine) - { - this.scriptEngine = scriptEngine; - } + public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, + [EnumeratorCancellation] CancellationToken ct) + { + var result = new EnrichedSchemaEvent(); - public bool Handles(AppEvent appEvent) - { - return appEvent is SchemaEvent; - } + // Use the concrete event to map properties that are not part of app event. + SimpleMapper.Map((SchemaEvent)@event.Payload, result); - public async IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct) + switch (@event.Payload) { - var result = new EnrichedSchemaEvent(); + case FieldEvent: + case SchemaPreviewUrlsConfigured: + case SchemaScriptsConfigured: + case SchemaUpdated: + case ParentFieldEvent: + result.Type = EnrichedSchemaEventType.Updated; + break; + case SchemaCreated: + result.Type = EnrichedSchemaEventType.Created; + break; + case SchemaPublished: + result.Type = EnrichedSchemaEventType.Published; + break; + case SchemaUnpublished: + result.Type = EnrichedSchemaEventType.Unpublished; + break; + case SchemaDeleted: + result.Type = EnrichedSchemaEventType.Deleted; + break; + default: + yield break; + } - // Use the concrete event to map properties that are not part of app event. - SimpleMapper.Map((SchemaEvent)@event.Payload, result); + await Task.Yield(); - switch (@event.Payload) - { - case FieldEvent: - case SchemaPreviewUrlsConfigured: - case SchemaScriptsConfigured: - case SchemaUpdated: - case ParentFieldEvent: - result.Type = EnrichedSchemaEventType.Updated; - break; - case SchemaCreated: - result.Type = EnrichedSchemaEventType.Created; - break; - case SchemaPublished: - result.Type = EnrichedSchemaEventType.Published; - break; - case SchemaUnpublished: - result.Type = EnrichedSchemaEventType.Unpublished; - break; - case SchemaDeleted: - result.Type = EnrichedSchemaEventType.Deleted; - break; - default: - yield break; - } + yield return result; + } - await Task.Yield(); + public bool Trigger(EnrichedEvent @event, RuleContext context) + { + var trigger = (SchemaChangedTrigger)context.Rule.Trigger; - yield return result; + if (string.IsNullOrWhiteSpace(trigger.Condition)) + { + return true; } - public bool Trigger(EnrichedEvent @event, RuleContext context) + // Script vars are just wrappers over dictionaries for better performance. + var vars = new EventScriptVars { - var trigger = (SchemaChangedTrigger)context.Rule.Trigger; - - if (string.IsNullOrWhiteSpace(trigger.Condition)) - { - return true; - } + ["event"] = @event + }; - // Script vars are just wrappers over dictionaries for better performance. - var vars = new EventScriptVars - { - ["event"] = @event - }; - - return scriptEngine.Evaluate(vars, trigger.Condition); - } + return scriptEngine.Evaluate(vars, trigger.Condition); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs index 43000d19c5..22c32357ba 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs @@ -9,28 +9,27 @@ using Squidex.Infrastructure; using StaticNamedId = Squidex.Infrastructure.NamedId; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public static class SchemaExtensions { - public static class SchemaExtensions + public static NamedId<DomainId> NamedId(this ISchemaEntity schema) { - public static NamedId<DomainId> NamedId(this ISchemaEntity schema) - { - return StaticNamedId.Of(schema.Id, schema.SchemaDef.Name); - } + return StaticNamedId.Of(schema.Id, schema.SchemaDef.Name); + } - public static string EscapePartition(this string value) - { - return value.Replace('-', '_'); - } + public static string EscapePartition(this string value) + { + return value.Replace('-', '_'); + } - public static string TypeName(this ISchemaEntity schema) - { - return schema.SchemaDef.TypeName(); - } + public static string TypeName(this ISchemaEntity schema) + { + return schema.SchemaDef.TypeName(); + } - public static string DisplayName(this ISchemaEntity schema) - { - return schema.SchemaDef.DisplayName(); - } + public static string DisplayName(this ISchemaEntity schema) + { + return schema.SchemaDef.DisplayName(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs index a5e9ba47b3..faaa02fb7d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs @@ -11,82 +11,81 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase { - public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase + public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) { - public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage<SchemaFieldsReordered>( - "history.schemas.fieldsReordered"); + AddEventMessage<SchemaFieldsReordered>( + "history.schemas.fieldsReordered"); - AddEventMessage<SchemaCreated>( - "history.schemas.created"); + AddEventMessage<SchemaCreated>( + "history.schemas.created"); - AddEventMessage<SchemaUpdated>( - "history.schemas.updated"); + AddEventMessage<SchemaUpdated>( + "history.schemas.updated"); - AddEventMessage<SchemaDeleted>( - "history.schemas.deleted"); + AddEventMessage<SchemaDeleted>( + "history.schemas.deleted"); - AddEventMessage<SchemaPublished>( - "history.schemas.published"); + AddEventMessage<SchemaPublished>( + "history.schemas.published"); - AddEventMessage<SchemaUnpublished>( - "history.schemas.unpublished"); + AddEventMessage<SchemaUnpublished>( + "history.schemas.unpublished"); - AddEventMessage<SchemaFieldsReordered>( - "history.schemas.fieldsReordered"); + AddEventMessage<SchemaFieldsReordered>( + "history.schemas.fieldsReordered"); - AddEventMessage<SchemaScriptsConfigured>( - "history.schemas.scriptsConfigured"); + AddEventMessage<SchemaScriptsConfigured>( + "history.schemas.scriptsConfigured"); - AddEventMessage<FieldAdded>( - "history.schemas.fieldAdded"); + AddEventMessage<FieldAdded>( + "history.schemas.fieldAdded"); - AddEventMessage<FieldDeleted>( - "history.schemas.fieldDeleted"); + AddEventMessage<FieldDeleted>( + "history.schemas.fieldDeleted"); - AddEventMessage<FieldLocked>( - "history.schemas.fieldLocked"); + AddEventMessage<FieldLocked>( + "history.schemas.fieldLocked"); - AddEventMessage<FieldHidden>( - "history.schemas.fieldHidden"); + AddEventMessage<FieldHidden>( + "history.schemas.fieldHidden"); - AddEventMessage<FieldShown>( - "history.schemas.fieldShown"); + AddEventMessage<FieldShown>( + "history.schemas.fieldShown"); - AddEventMessage<FieldDisabled>( - "history.schemas.fieldDisabled"); + AddEventMessage<FieldDisabled>( + "history.schemas.fieldDisabled"); - AddEventMessage<FieldEnabled>( - "history.schemas.fieldDisabled"); + AddEventMessage<FieldEnabled>( + "history.schemas.fieldDisabled"); - AddEventMessage<FieldUpdated>( - "history.schemas.fieldUpdated"); + AddEventMessage<FieldUpdated>( + "history.schemas.fieldUpdated"); - AddEventMessage<FieldDeleted>( - "history.schemas.fieldDeleted"); - } + AddEventMessage<FieldDeleted>( + "history.schemas.fieldDeleted"); + } - protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event) - { - HistoryEvent? result = null; + protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event) + { + HistoryEvent? result = null; - if (@event.Payload is SchemaEvent schemaEvent) - { - var channel = $"schemas.{schemaEvent.SchemaId.Id}"; + if (@event.Payload is SchemaEvent schemaEvent) + { + var channel = $"schemas.{schemaEvent.SchemaId.Id}"; - result = ForEvent(@event.Payload, channel).Param("Name", schemaEvent.SchemaId.Name); + result = ForEvent(@event.Payload, channel).Param("Name", schemaEvent.SchemaId.Name); - if (schemaEvent is FieldEvent fieldEvent) - { - result.Param("Field", fieldEvent.FieldId.Name); - } + if (schemaEvent is FieldEvent fieldEvent) + { + result.Param("Field", fieldEvent.FieldId.Name); } - - return Task.FromResult(result); } + + return Task.FromResult(result); } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs index 31162e847c..759dbaafa8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs @@ -13,78 +13,77 @@ using Squidex.Infrastructure.Translations; using Squidex.Shared; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public sealed class SchemasSearchSource : ISearchSource { - public sealed class SchemasSearchSource : ISearchSource + private readonly IAppProvider appProvider; + private readonly IUrlGenerator urlGenerator; + + public SchemasSearchSource(IAppProvider appProvider, IUrlGenerator urlGenerator) { - private readonly IAppProvider appProvider; - private readonly IUrlGenerator urlGenerator; + this.appProvider = appProvider; - public SchemasSearchSource(IAppProvider appProvider, IUrlGenerator urlGenerator) - { - this.appProvider = appProvider; + this.urlGenerator = urlGenerator; + } - this.urlGenerator = urlGenerator; - } + public async Task<SearchResults> SearchAsync(string query, Context context, + CancellationToken ct) + { + var result = new SearchResults(); - public async Task<SearchResults> SearchAsync(string query, Context context, - CancellationToken ct) - { - var result = new SearchResults(); + var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); - var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); + if (schemas.Count > 0) + { + var appId = context.App.NamedId(); - if (schemas.Count > 0) + foreach (var schema in schemas) { - var appId = context.App.NamedId(); + var schemaId = schema.NamedId(); - foreach (var schema in schemas) - { - var schemaId = schema.NamedId(); + var name = schema.SchemaDef.DisplayNameUnchanged(); - var name = schema.SchemaDef.DisplayNameUnchanged(); + if (name.Contains(query, StringComparison.OrdinalIgnoreCase)) + { + AddSchemaUrl(result, appId, schemaId, name); - if (name.Contains(query, StringComparison.OrdinalIgnoreCase)) + if (schema.SchemaDef.Type != SchemaType.Component && HasPermission(context, schemaId)) { - AddSchemaUrl(result, appId, schemaId, name); - - if (schema.SchemaDef.Type != SchemaType.Component && HasPermission(context, schemaId)) - { - AddContentsUrl(result, appId, schema, schemaId, name); - } + AddContentsUrl(result, appId, schema, schemaId, name); } } } - - return result; } - private void AddSchemaUrl(SearchResults result, NamedId<DomainId> appId, NamedId<DomainId> schemaId, string name) - { - var schemaUrl = urlGenerator.SchemaUI(appId, schemaId); + return result; + } - result.Add(T.Get("search.schemaResult", new { name }), SearchResultType.Schema, schemaUrl); - } + private void AddSchemaUrl(SearchResults result, NamedId<DomainId> appId, NamedId<DomainId> schemaId, string name) + { + var schemaUrl = urlGenerator.SchemaUI(appId, schemaId); - private void AddContentsUrl(SearchResults result, NamedId<DomainId> appId, ISchemaEntity schema, NamedId<DomainId> schemaId, string name) - { - if (schema.SchemaDef.Type == SchemaType.Singleton) - { - var contentUrl = urlGenerator.ContentUI(appId, schemaId, schemaId.Id); + result.Add(T.Get("search.schemaResult", new { name }), SearchResultType.Schema, schemaUrl); + } - result.Add(T.Get("search.contentResult", new { name }), SearchResultType.Content, contentUrl, name); - } - else - { - var contentUrl = urlGenerator.ContentsUI(appId, schemaId); + private void AddContentsUrl(SearchResults result, NamedId<DomainId> appId, ISchemaEntity schema, NamedId<DomainId> schemaId, string name) + { + if (schema.SchemaDef.Type == SchemaType.Singleton) + { + var contentUrl = urlGenerator.ContentUI(appId, schemaId, schemaId.Id); - result.Add(T.Get("search.contentsResult", new { name }), SearchResultType.Content, contentUrl, name); - } + result.Add(T.Get("search.contentResult", new { name }), SearchResultType.Content, contentUrl, name); } - - private static bool HasPermission(Context context, NamedId<DomainId> schemaId) + else { - return context.Allows(PermissionIds.AppContentsReadOwn, schemaId.Name); + var contentUrl = urlGenerator.ContentsUI(appId, schemaId); + + result.Add(T.Get("search.contentsResult", new { name }), SearchResultType.Content, contentUrl, name); } } + + private static bool HasPermission(Context context, NamedId<DomainId> schemaId) + { + return context.Allows(PermissionIds.AppContentsReadOwn, schemaId.Name); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchManager.cs index e237bdee59..8b60ff9d30 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchManager.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Search +namespace Squidex.Domain.Apps.Entities.Search; + +public interface ISearchManager { - public interface ISearchManager - { - Task<SearchResults> SearchAsync(string? query, Context context, - CancellationToken ct = default); - } + Task<SearchResults> SearchAsync(string? query, Context context, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchSource.cs index 3259852653..b33d76ad5a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchSource.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Search +namespace Squidex.Domain.Apps.Entities.Search; + +public interface ISearchSource { - public interface ISearchSource - { - Task<SearchResults> SearchAsync(string query, Context context, - CancellationToken ct); - } + Task<SearchResults> SearchAsync(string query, Context context, + CancellationToken ct); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Search/SearchManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Search/SearchManager.cs index 565058be17..1d7ec82333 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Search/SearchManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Search/SearchManager.cs @@ -7,48 +7,47 @@ using Microsoft.Extensions.Logging; -namespace Squidex.Domain.Apps.Entities.Search +namespace Squidex.Domain.Apps.Entities.Search; + +public sealed class SearchManager : ISearchManager { - public sealed class SearchManager : ISearchManager + private static readonly SearchResults Empty = new SearchResults(); + private readonly IEnumerable<ISearchSource> searchSources; + private readonly ILogger<SearchManager> log; + + public SearchManager(IEnumerable<ISearchSource> searchSources, ILogger<SearchManager> log) { - private static readonly SearchResults Empty = new SearchResults(); - private readonly IEnumerable<ISearchSource> searchSources; - private readonly ILogger<SearchManager> log; + this.searchSources = searchSources; - public SearchManager(IEnumerable<ISearchSource> searchSources, ILogger<SearchManager> log) - { - this.searchSources = searchSources; + this.log = log; + } - this.log = log; + public async Task<SearchResults> SearchAsync(string? query, Context context, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(query) || query.Length < 3) + { + return new SearchResults(); } - public async Task<SearchResults> SearchAsync(string? query, Context context, - CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(query) || query.Length < 3) - { - return new SearchResults(); - } + var tasks = searchSources.Select(x => SearchAsync(x, query, context, ct)); - var tasks = searchSources.Select(x => SearchAsync(x, query, context, ct)); + var results = await Task.WhenAll(tasks); - var results = await Task.WhenAll(tasks); + return new SearchResults(results.SelectMany(x => x)); + } - return new SearchResults(results.SelectMany(x => x)); + private async Task<SearchResults> SearchAsync(ISearchSource source, string query, Context context, + CancellationToken ct) + { + try + { + return await source.SearchAsync(query, context, ct); } - - private async Task<SearchResults> SearchAsync(ISearchSource source, string query, Context context, - CancellationToken ct) + catch (Exception ex) { - try - { - return await source.SearchAsync(query, context, ct); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to execute search from source {source} with query '{query}'.", source, query); - return Empty; - } + log.LogError(ex, "Failed to execute search from source {source} with query '{query}'.", source, query); + return Empty; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResult.cs index 558fbf6ff8..2f966b13a8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResult.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResult.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Search +namespace Squidex.Domain.Apps.Entities.Search; + +public sealed class SearchResult { - public sealed class SearchResult - { - public string Name { get; set; } + public string Name { get; set; } - public string? Label { get; set; } + public string? Label { get; set; } - public string Url { get; set; } + public string Url { get; set; } - public SearchResultType Type { get; set; } - } + public SearchResultType Type { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResultType.cs b/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResultType.cs index 89b168e1af..d6ce6f1c33 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResultType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResultType.cs @@ -5,15 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Search +namespace Squidex.Domain.Apps.Entities.Search; + +public enum SearchResultType { - public enum SearchResultType - { - Asset, - Content, - Dashboard, - Setting, - Rule, - Schema - } + Asset, + Content, + Dashboard, + Setting, + Rule, + Schema } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResults.cs b/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResults.cs index a301c10ec0..07b38548e2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResults.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Search/SearchResults.cs @@ -5,24 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Search +namespace Squidex.Domain.Apps.Entities.Search; + +public sealed class SearchResults : List<SearchResult> { - public sealed class SearchResults : List<SearchResult> + public SearchResults() { - public SearchResults() - { - } + } - public SearchResults(IEnumerable<SearchResult> source) - : base(source) - { - } + public SearchResults(IEnumerable<SearchResult> source) + : base(source) + { + } - public SearchResults Add(string name, SearchResultType type, string url, string? label = null) - { - Add(new SearchResult { Name = name, Type = type, Label = label, Url = url }); + public SearchResults Add(string name, SearchResultType type, string url, string? label = null) + { + Add(new SearchResult { Name = name, Type = type, Label = label, Url = url }); - return this; - } + return this; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs index 4e06603e8e..198004e4d0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs @@ -9,16 +9,15 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public abstract class SquidexCommand : ICommand { - public abstract class SquidexCommand : ICommand - { - public RefToken Actor { get; set; } + public RefToken Actor { get; set; } - public ClaimsPrincipal? User { get; set; } + public ClaimsPrincipal? User { get; set; } - public bool FromRule { get; set; } + public bool FromRule { get; set; } - public long ExpectedVersion { get; set; } = EtagVersion.Auto; - } + public long ExpectedVersion { get; set; } = EtagVersion.Auto; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs index 8d82fce421..636f3b38be 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs @@ -7,10 +7,9 @@ using System.Reflection; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public static class SquidexEntities { - public static class SquidexEntities - { - public static readonly Assembly Assembly = typeof(SquidexEntities).Assembly; - } + public static readonly Assembly Assembly = typeof(SquidexEntities).Assembly; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs index 79432f6424..20e06f27b0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGenerator.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Tags +namespace Squidex.Domain.Apps.Entities.Tags; + +public interface ITagGenerator<in T> { - public interface ITagGenerator<in T> - { - void GenerateTags(T source, HashSet<string> tags); - } + void GenerateTags(T source, HashSet<string> tags); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs index 4242c58f97..6f7b2adef1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs @@ -11,350 +11,349 @@ using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; -namespace Squidex.Domain.Apps.Entities.Tags +namespace Squidex.Domain.Apps.Entities.Tags; + +public sealed class TagService : ITagService { - public sealed class TagService : ITagService - { - private readonly IPersistenceFactory<State> persistenceFactory; + private readonly IPersistenceFactory<State> persistenceFactory; - [CollectionName("Index_Tags")] - public sealed class State : TagsExport, IOnRead + [CollectionName("Index_Tags")] + public sealed class State : TagsExport, IOnRead + { + public ValueTask OnReadAsync() { - public ValueTask OnReadAsync() - { - // Tags should never be null, but it might happen due of bugs. - Tags ??= new Dictionary<string, Tag>(); + // Tags should never be null, but it might happen due of bugs. + Tags ??= new Dictionary<string, Tag>(); - // Alias can be null, because it was not part of the initial release. - Alias ??= new Dictionary<string, string>(); + // Alias can be null, because it was not part of the initial release. + Alias ??= new Dictionary<string, string>(); - return default; - } + return default; + } - public bool Rebuild(TagsExport export) + public bool Rebuild(TagsExport export) + { + // Tags should never be null, but it might happen due of bugs. + if (export.Tags != null) { - // Tags should never be null, but it might happen due of bugs. - if (export.Tags != null) - { - Tags = export.Tags; - } - - // Alias can be null, because it was not part of the initial release. - if (export.Alias != null) - { - Alias = export.Alias; - } - - return true; + Tags = export.Tags; } - public bool Clear() + // Alias can be null, because it was not part of the initial release. + if (export.Alias != null) { - var isChanged = false; - - // Clear only resets the counts to zero, because we have no other source for tag names. - foreach (var (_, tag) in Tags) - { - isChanged = tag.Count > 0; + Alias = export.Alias; + } - tag.Count = 0; - } + return true; + } - return isChanged; - } + public bool Clear() + { + var isChanged = false; - public bool Rename(string name, string newName) + // Clear only resets the counts to zero, because we have no other source for tag names. + foreach (var (_, tag) in Tags) { - name = NormalizeName(name); + isChanged = tag.Count > 0; - if (!TryGetTag(name, out var tag, false)) - { - return false; - } - - // Avoid the normalization of the new name, if the old name does not exist. - newName = NormalizeName(newName); + tag.Count = 0; + } - if (string.Equals(name, newName, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + return isChanged; + } - if (TryGetTag(newName, out var newTag, false)) - { - // Merge both tags by adding up the count. - newTag.Info.Count += tag.Info.Count; + public bool Rename(string name, string newName) + { + name = NormalizeName(name); - // Remove one of the tags. - Tags.Remove(tag.Id); - } - else - { - tag.Info.Name = newName; - } + if (!TryGetTag(name, out var tag, false)) + { + return false; + } - foreach (var alias in Alias.Where(x => x.Value == name).ToList()) - { - // Remove the mapping to the old name. - Alias.Remove(alias.Key); + // Avoid the normalization of the new name, if the old name does not exist. + newName = NormalizeName(newName); - // If the tag has been named back to the original name, we do not need the mapping anymore. - if (alias.Key != newName) - { - // Create a new mapping to the new name. - Alias[alias.Key] = newName; - } - } + if (string.Equals(name, newName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } - // Create a new mapping to the new name. - Alias[name] = newName; + if (TryGetTag(newName, out var newTag, false)) + { + // Merge both tags by adding up the count. + newTag.Info.Count += tag.Info.Count; - return true; + // Remove one of the tags. + Tags.Remove(tag.Id); + } + else + { + tag.Info.Name = newName; } - public bool Update(Dictionary<string, int> updates) + foreach (var alias in Alias.Where(x => x.Value == name).ToList()) { - var isChanged = false; + // Remove the mapping to the old name. + Alias.Remove(alias.Key); - foreach (var (id, update) in updates) + // If the tag has been named back to the original name, we do not need the mapping anymore. + if (alias.Key != newName) { - if (update != 0 && Tags.TryGetValue(id, out var tag)) - { - var newCount = Math.Max(0, tag.Count + update); - - if (newCount != tag.Count) - { - tag.Count = newCount; - - isChanged = true; - } - } + // Create a new mapping to the new name. + Alias[alias.Key] = newName; } - - return isChanged; } - public (bool, Dictionary<string, string>) GetIds(HashSet<string> names) - { - var tagIds = new Dictionary<string, string>(); + // Create a new mapping to the new name. + Alias[name] = newName; + + return true; + } - var isChanged = false; + public bool Update(Dictionary<string, int> updates) + { + var isChanged = false; - foreach (var name in names.Select(NormalizeName)) + foreach (var (id, update) in updates) + { + if (update != 0 && Tags.TryGetValue(id, out var tag)) { - if (TryGetTag(name, out var tag)) - { - // If the tag exists, return the ID. - tagIds[name] = tag.Id; - } - else - { - // If the tag does not exist create a new one with a random ID. - var id = Guid.NewGuid().ToString(); + var newCount = Math.Max(0, tag.Count + update); - Tags[id] = new Tag { Name = name }; - tagIds[name] = id; + if (newCount != tag.Count) + { + tag.Count = newCount; - // Track that something has changed and the state needs to be written. isChanged = true; } } - - return (isChanged, tagIds); } - public Dictionary<string, string> GetNames(HashSet<string> ids) - { - var tagNames = new Dictionary<string, string>(); + return isChanged; + } - foreach (var id in ids) - { - if (Tags.TryGetValue(id, out var tag)) - { - tagNames[id] = tag.Name; - } - } + public (bool, Dictionary<string, string>) GetIds(HashSet<string> names) + { + var tagIds = new Dictionary<string, string>(); - return tagNames; - } + var isChanged = false; - public TagsSet GetTags(long version) + foreach (var name in names.Select(NormalizeName)) { - var result = new Dictionary<string, int>(); - - foreach (var tag in Tags.Values) + if (TryGetTag(name, out var tag)) { - // We have changed the normalization logic, therefore some names are not up to date. - var name = NormalizeName(tag.Name); - - // An old bug could have produced duplicate names. - result[name] = result.GetValueOrDefault(name) + tag.Count; + // If the tag exists, return the ID. + tagIds[name] = tag.Id; } + else + { + // If the tag does not exist create a new one with a random ID. + var id = Guid.NewGuid().ToString(); - return new TagsSet(result, version); - } + Tags[id] = new Tag { Name = name }; + tagIds[name] = id; - private static string NormalizeName(string name) - { - return name.TrimNonLetterOrDigit().ToLowerInvariant(); + // Track that something has changed and the state needs to be written. + isChanged = true; + } } - private bool TryGetTag(string name, out (string Id, Tag Info)result, bool useAlias = true) - { - result = default!; + return (isChanged, tagIds); + } + + public Dictionary<string, string> GetNames(HashSet<string> ids) + { + var tagNames = new Dictionary<string, string>(); - // If the tag has been renamed we create a mapping from the old name to the new name. - if (useAlias && Alias.TryGetValue(name, out var newName)) + foreach (var id in ids) + { + if (Tags.TryGetValue(id, out var tag)) { - name = newName; + tagNames[id] = tag.Name; } + } - var found = Tags.FirstOrDefault(x => x.Value.Name == name); + return tagNames; + } - if (found.Value != null) - { - result = (found.Key, found.Value); - return true; - } + public TagsSet GetTags(long version) + { + var result = new Dictionary<string, int>(); - return false; + foreach (var tag in Tags.Values) + { + // We have changed the normalization logic, therefore some names are not up to date. + var name = NormalizeName(tag.Name); + + // An old bug could have produced duplicate names. + result[name] = result.GetValueOrDefault(name) + tag.Count; } + + return new TagsSet(result, version); } - public TagService(IPersistenceFactory<State> persistenceFactory) + private static string NormalizeName(string name) { - this.persistenceFactory = persistenceFactory; + return name.TrimNonLetterOrDigit().ToLowerInvariant(); } - public async Task RenameTagAsync(DomainId id, string group, string name, string newName, - CancellationToken ct = default) + private bool TryGetTag(string name, out (string Id, Tag Info)result, bool useAlias = true) { - Guard.NotNullOrEmpty(name); - Guard.NotNullOrEmpty(newName); + result = default!; + + // If the tag has been renamed we create a mapping from the old name to the new name. + if (useAlias && Alias.TryGetValue(name, out var newName)) + { + name = newName; + } + + var found = Tags.FirstOrDefault(x => x.Value.Name == name); - var state = await GetStateAsync(id, group, ct); + if (found.Value != null) + { + result = (found.Key, found.Value); + return true; + } - await state.UpdateAsync(s => s.Rename(name, newName), ct: ct); + return false; } + } - public async Task RebuildTagsAsync(DomainId id, string group, TagsExport export, - CancellationToken ct = default) - { - Guard.NotNull(export); + public TagService(IPersistenceFactory<State> persistenceFactory) + { + this.persistenceFactory = persistenceFactory; + } - var state = await GetStateAsync(id, group, ct); + public async Task RenameTagAsync(DomainId id, string group, string name, string newName, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(name); + Guard.NotNullOrEmpty(newName); - await state.UpdateAsync(s => s.Rebuild(export), ct: ct); - } + var state = await GetStateAsync(id, group, ct); - public async Task<Dictionary<string, string>> GetTagIdsAsync(DomainId id, string group, HashSet<string> names, - CancellationToken ct = default) - { - Guard.NotNull(names); + await state.UpdateAsync(s => s.Rename(name, newName), ct: ct); + } - var state = await GetStateAsync(id, group, ct); + public async Task RebuildTagsAsync(DomainId id, string group, TagsExport export, + CancellationToken ct = default) + { + Guard.NotNull(export); - return await state.UpdateAsync(s => s.GetIds(names), ct: ct); - } + var state = await GetStateAsync(id, group, ct); - public async Task<Dictionary<string, string>> GetTagNamesAsync(DomainId id, string group, HashSet<string> ids, - CancellationToken ct = default) - { - Guard.NotNull(ids); + await state.UpdateAsync(s => s.Rebuild(export), ct: ct); + } - var state = await GetStateAsync(id, group, ct); + public async Task<Dictionary<string, string>> GetTagIdsAsync(DomainId id, string group, HashSet<string> names, + CancellationToken ct = default) + { + Guard.NotNull(names); - return state.Value.GetNames(ids); - } + var state = await GetStateAsync(id, group, ct); - public async Task UpdateAsync(DomainId id, string group, Dictionary<string, int> update, - CancellationToken ct = default) - { - var state = await GetStateAsync(id, group, ct); + return await state.UpdateAsync(s => s.GetIds(names), ct: ct); + } - await state.UpdateAsync(s => s.Update(update), ct: ct); - } + public async Task<Dictionary<string, string>> GetTagNamesAsync(DomainId id, string group, HashSet<string> ids, + CancellationToken ct = default) + { + Guard.NotNull(ids); - public async Task<TagsSet> GetTagsAsync(DomainId id, string group, - CancellationToken ct = default) - { - var state = await GetStateAsync(id, group, ct); + var state = await GetStateAsync(id, group, ct); - return state.Value.GetTags(state.Version); - } + return state.Value.GetNames(ids); + } - public async Task<TagsExport> GetExportableTagsAsync(DomainId id, string group, - CancellationToken ct = default) - { - var state = await GetStateAsync(id, group, ct); + public async Task UpdateAsync(DomainId id, string group, Dictionary<string, int> update, + CancellationToken ct = default) + { + var state = await GetStateAsync(id, group, ct); - return state.Value; - } + await state.UpdateAsync(s => s.Update(update), ct: ct); + } - public async Task ClearAsync(DomainId id, string group, - CancellationToken ct = default) - { - var state = await GetStateAsync(id, group, ct); + public async Task<TagsSet> GetTagsAsync(DomainId id, string group, + CancellationToken ct = default) + { + var state = await GetStateAsync(id, group, ct); - await state.ClearAsync(ct); - } + return state.Value.GetTags(state.Version); + } - private async Task<SimpleState<State>> GetStateAsync(DomainId id, string group, - CancellationToken ct) - { - var state = new SimpleState<State>(persistenceFactory, GetType(), $"{id}_{group}"); + public async Task<TagsExport> GetExportableTagsAsync(DomainId id, string group, + CancellationToken ct = default) + { + var state = await GetStateAsync(id, group, ct); - await state.LoadAsync(ct); + return state.Value; + } - return state; - } + public async Task ClearAsync(DomainId id, string group, + CancellationToken ct = default) + { + var state = await GetStateAsync(id, group, ct); - public async Task ClearAsync( - CancellationToken ct = default) - { - var writerBlock = new ActionBlock<SnapshotResult<State>[]>(async batch => - { - try - { - var isChanged = !batch.All(x => !x.Value.Clear()); + await state.ClearAsync(ct); + } - if (isChanged) - { - var jobs = batch.Select(x => new SnapshotWriteJob<State>(x.Key, x.Value, x.Version)); + private async Task<SimpleState<State>> GetStateAsync(DomainId id, string group, + CancellationToken ct) + { + var state = new SimpleState<State>(persistenceFactory, GetType(), $"{id}_{group}"); - await persistenceFactory.Snapshots.WriteManyAsync(jobs, ct); - } - } - catch (OperationCanceledException ex) - { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); - } - }, - new ExecutionDataflowBlockOptions - { - BoundedCapacity = 2, - MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1, - }); + await state.LoadAsync(ct); + + return state; + } - // Create batches of 500 items to clear the tag count for better performance. - var batchBlock = new BatchBlock<SnapshotResult<State>>(500, new GroupingDataflowBlockOptions + public async Task ClearAsync( + CancellationToken ct = default) + { + var writerBlock = new ActionBlock<SnapshotResult<State>[]>(async batch => + { + try { - BoundedCapacity = 500 - }); + var isChanged = !batch.All(x => !x.Value.Clear()); - batchBlock.BidirectionalLinkTo(writerBlock); + if (isChanged) + { + var jobs = batch.Select(x => new SnapshotWriteJob<State>(x.Key, x.Value, x.Version)); - await foreach (var state in persistenceFactory.Snapshots.ReadAllAsync(ct)) + await persistenceFactory.Snapshots.WriteManyAsync(jobs, ct); + } + } + catch (OperationCanceledException ex) { - // Uses back-propagation to not query additional items from the database, when queue is full. - await batchBlock.SendAsync(state, ct); + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); } + }, + new ExecutionDataflowBlockOptions + { + BoundedCapacity = 2, + MaxDegreeOfParallelism = 1, + MaxMessagesPerTask = 1, + }); - batchBlock.Complete(); + // Create batches of 500 items to clear the tag count for better performance. + var batchBlock = new BatchBlock<SnapshotResult<State>>(500, new GroupingDataflowBlockOptions + { + BoundedCapacity = 500 + }); - await writerBlock.Completion; + batchBlock.BidirectionalLinkTo(writerBlock); + + await foreach (var state in persistenceFactory.Snapshots.ReadAllAsync(ct)) + { + // Uses back-propagation to not query additional items from the database, when queue is full. + await batchBlock.SendAsync(state, ct); } + + batchBlock.Complete(); + + await writerBlock.Completion; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/AssignContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/AssignContributor.cs index bab461dd43..b4681a32c0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/AssignContributor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/AssignContributor.cs @@ -7,18 +7,17 @@ using Roles = Squidex.Domain.Apps.Core.Apps.Role; -namespace Squidex.Domain.Apps.Entities.Teams.Commands +namespace Squidex.Domain.Apps.Entities.Teams.Commands; + +public sealed class AssignContributor : TeamCommand { - public sealed class AssignContributor : TeamCommand - { - public string ContributorId { get; set; } + public string ContributorId { get; set; } - public string Role { get; set; } = Roles.Owner; + public string Role { get; set; } = Roles.Owner; - public bool IgnoreActor { get; set; } + public bool IgnoreActor { get; set; } - public bool IgnorePlans { get; set; } + public bool IgnorePlans { get; set; } - public bool Invite { get; set; } - } + public bool Invite { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/ChangePlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/ChangePlan.cs index 6a738d4164..9910d1acc8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/ChangePlan.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/ChangePlan.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Teams.Commands +namespace Squidex.Domain.Apps.Entities.Teams.Commands; + +public sealed class ChangePlan : TeamCommand { - public sealed class ChangePlan : TeamCommand - { - public bool FromCallback { get; set; } + public bool FromCallback { get; set; } - public string PlanId { get; set; } - } + public string PlanId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/CreateTeam.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/CreateTeam.cs index 6525ff913c..02c259c494 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/CreateTeam.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/CreateTeam.cs @@ -7,15 +7,14 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Teams.Commands +namespace Squidex.Domain.Apps.Entities.Teams.Commands; + +public sealed class CreateTeam : TeamCommand { - public sealed class CreateTeam : TeamCommand - { - public string Name { get; set; } + public string Name { get; set; } - public CreateTeam() - { - TeamId = DomainId.NewGuid(); - } + public CreateTeam() + { + TeamId = DomainId.NewGuid(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/RemoveContributor.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/RemoveContributor.cs index ac0b57a17b..a4597e930e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/RemoveContributor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/RemoveContributor.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Teams.Commands +namespace Squidex.Domain.Apps.Entities.Teams.Commands; + +public sealed class RemoveContributor : TeamCommand { - public sealed class RemoveContributor : TeamCommand - { - public string ContributorId { get; set; } - } + public string ContributorId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/UpdateTeam.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/UpdateTeam.cs index 4ec1383fe3..96f1c9b986 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/UpdateTeam.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/UpdateTeam.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Teams.Commands +namespace Squidex.Domain.Apps.Entities.Teams.Commands; + +public sealed class UpdateTeam : TeamCommand { - public sealed class UpdateTeam : TeamCommand - { - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/_TeamCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/_TeamCommand.cs index 8a08e530af..5cd29cfe07 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/_TeamCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Commands/_TeamCommand.cs @@ -10,21 +10,20 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Teams.Commands -{ - public abstract class TeamCommand : TeamCommandBase, ITeamCommand - { - public DomainId TeamId { get; set; } +namespace Squidex.Domain.Apps.Entities.Teams.Commands; - public override DomainId AggregateId - { - get => TeamId; - } - } +public abstract class TeamCommand : TeamCommandBase, ITeamCommand +{ + public DomainId TeamId { get; set; } - // This command is needed as marker for middlewares. - public abstract class TeamCommandBase : SquidexCommand, IAggregateCommand + public override DomainId AggregateId { - public abstract DomainId AggregateId { get; } + get => TeamId; } } + +// This command is needed as marker for middlewares. +public abstract class TeamCommandBase : SquidexCommand, IAggregateCommand +{ + public abstract DomainId AggregateId { get; } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeam.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeam.cs index e61ed19789..5bc2be6b44 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeam.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeam.cs @@ -11,53 +11,52 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards; + +public static class GuardTeam { - public static class GuardTeam + public static void CanCreate(CreateTeam command) { - public static void CanCreate(CreateTeam command) - { - Guard.NotNull(command); + Guard.NotNull(command); - Validate.It(e => - { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined(nameof(command.Name)), nameof(command.Name)); - } - }); - } - - public static void CanUpdate(UpdateTeam command) + Validate.It(e => { - Guard.NotNull(command); + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined(nameof(command.Name)), nameof(command.Name)); + } + }); + } + + public static void CanUpdate(UpdateTeam command) + { + Guard.NotNull(command); - Validate.It(e => + Validate.It(e => + { + if (string.IsNullOrWhiteSpace(command.Name)) { - if (string.IsNullOrWhiteSpace(command.Name)) - { - e(Not.Defined(nameof(command.Name)), nameof(command.Name)); - } - }); - } - - public static void CanChangePlan(ChangePlan command, IBillingPlans billingPlans) + e(Not.Defined(nameof(command.Name)), nameof(command.Name)); + } + }); + } + + public static void CanChangePlan(ChangePlan command, IBillingPlans billingPlans) + { + Guard.NotNull(command); + + Validate.It(e => { - Guard.NotNull(command); + if (string.IsNullOrWhiteSpace(command.PlanId)) + { + e(Not.Defined(nameof(command.PlanId)), nameof(command.PlanId)); + return; + } - Validate.It(e => + if (billingPlans.GetPlan(command.PlanId) == null) { - if (string.IsNullOrWhiteSpace(command.PlanId)) - { - e(Not.Defined(nameof(command.PlanId)), nameof(command.PlanId)); - return; - } - - if (billingPlans.GetPlan(command.PlanId) == null) - { - e(T.Get("apps.plans.notFound"), nameof(command.PlanId)); - } - }); - } + e(T.Get("apps.plans.notFound"), nameof(command.PlanId)); + } + }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeamContributors.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeamContributors.cs index 24b9d45a21..ca9f66f6ab 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeamContributors.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/Guards/GuardTeamContributors.cs @@ -12,69 +12,68 @@ using Squidex.Infrastructure.Validation; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards; + +public static class GuardTeamContributors { - public static class GuardTeamContributors + public static Task CanAssign(AssignContributor command, ITeamEntity team, IUserResolver users) { - public static Task CanAssign(AssignContributor command, ITeamEntity team, IUserResolver users) - { - Guard.NotNull(command); + Guard.NotNull(command); - var contributors = team.Contributors; + var contributors = team.Contributors; - return Validate.It(async e => + return Validate.It(async e => + { + if (command.Role != Role.Owner) { - if (command.Role != Role.Owner) + e(Not.Valid(nameof(command.Role)), nameof(command.Role)); + } + + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); + } + else + { + var user = await users.FindByIdAsync(command.ContributorId); + + if (user == null) { - e(Not.Valid(nameof(command.Role)), nameof(command.Role)); + throw new DomainObjectNotFoundException(command.ContributorId); } - if (string.IsNullOrWhiteSpace(command.ContributorId)) + if (!command.IgnoreActor && string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) { - e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); + throw new DomainForbiddenException(T.Get("apps.contributors.cannotChangeYourself")); } - else - { - var user = await users.FindByIdAsync(command.ContributorId); + } + }); + } - if (user == null) - { - throw new DomainObjectNotFoundException(command.ContributorId); - } + public static void CanRemove(RemoveContributor command, ITeamEntity team) + { + Guard.NotNull(command); - if (!command.IgnoreActor && string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase)) - { - throw new DomainForbiddenException(T.Get("apps.contributors.cannotChangeYourself")); - } - } - }); - } + var contributors = team.Contributors; - public static void CanRemove(RemoveContributor command, ITeamEntity team) + Validate.It(e => { - Guard.NotNull(command); - - var contributors = team.Contributors; - - Validate.It(e => + if (string.IsNullOrWhiteSpace(command.ContributorId)) { - if (string.IsNullOrWhiteSpace(command.ContributorId)) - { - e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); - } - - var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); + e(Not.Defined(nameof(command.ContributorId)), nameof(command.ContributorId)); + } - if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) - { - e(T.Get("apps.contributors.onlyOneOwner")); - } - }); + var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList(); - if (!contributors.ContainsKey(command.ContributorId)) + if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) { - throw new DomainObjectNotFoundException(command.ContributorId); + e(T.Get("apps.contributors.onlyOneOwner")); } + }); + + if (!contributors.ContainsKey(command.ContributorId)) + { + throw new DomainObjectNotFoundException(command.ContributorId); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.State.cs index 7d536bbd68..b059d7dc13 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.State.cs @@ -13,73 +13,72 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Entities.Teams.DomainObject +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject; + +public partial class TeamDomainObject { - public partial class TeamDomainObject + public sealed class State : DomainObjectState<State>, ITeamEntity { - public sealed class State : DomainObjectState<State>, ITeamEntity - { - public string Name { get; set; } + public string Name { get; set; } - public Contributors Contributors { get; set; } = Contributors.Empty; + public Contributors Contributors { get; set; } = Contributors.Empty; - public AssignedPlan? Plan { get; set; } + public AssignedPlan? Plan { get; set; } - [JsonIgnore] - public DomainId UniqueId - { - get => Id; - } + [JsonIgnore] + public DomainId UniqueId + { + get => Id; + } - public override bool ApplyEvent(IEvent @event) + public override bool ApplyEvent(IEvent @event) + { + switch (@event) { - switch (@event) - { - case TeamCreated e: - { - Id = e.TeamId; + case TeamCreated e: + { + Id = e.TeamId; - SimpleMapper.Map(e, this); - return true; - } + SimpleMapper.Map(e, this); + return true; + } - case TeamUpdated e when Is.Change(Name, e.Name): - { - SimpleMapper.Map(e, this); - return true; - } + case TeamUpdated e when Is.Change(Name, e.Name): + { + SimpleMapper.Map(e, this); + return true; + } - case TeamPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): - return UpdatePlan(e.ToPlan()); + case TeamPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): + return UpdatePlan(e.ToPlan()); - case TeamPlanReset e when Plan != null: - return UpdatePlan(null); + case TeamPlanReset e when Plan != null: + return UpdatePlan(null); - case TeamContributorAssigned e: - return UpdateContributors(e, (e, c) => c.Assign(e.ContributorId, e.Role)); + case TeamContributorAssigned e: + return UpdateContributors(e, (e, c) => c.Assign(e.ContributorId, e.Role)); - case TeamContributorRemoved e: - return UpdateContributors(e, (e, c) => c.Remove(e.ContributorId)); - } - - return false; + case TeamContributorRemoved e: + return UpdateContributors(e, (e, c) => c.Remove(e.ContributorId)); } - private bool UpdateContributors<T>(T @event, Func<T, Contributors, Contributors> update) - { - var previous = Contributors; + return false; + } - Contributors = update(@event, previous); + private bool UpdateContributors<T>(T @event, Func<T, Contributors, Contributors> update) + { + var previous = Contributors; - return !ReferenceEquals(previous, Contributors); - } + Contributors = update(@event, previous); - private bool UpdatePlan(AssignedPlan? plan) - { - Plan = plan; + return !ReferenceEquals(previous, Contributors); + } - return true; - } + private bool UpdatePlan(AssignedPlan? plan) + { + Plan = plan; + + return true; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs index f50a6b3ccb..451c97264d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs @@ -21,190 +21,189 @@ #pragma warning disable MA0022 // Return Task.FromResult instead of returning null -namespace Squidex.Domain.Apps.Entities.Teams.DomainObject +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject; + +public partial class TeamDomainObject : DomainObject<TeamDomainObject.State> { - public partial class TeamDomainObject : DomainObject<TeamDomainObject.State> + private readonly IServiceProvider serviceProvider; + + public TeamDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<TeamDomainObject> log, + IServiceProvider serviceProvider) + : base(id, persistence, log) { - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public TeamDomainObject(DomainId id, IPersistenceFactory<State> persistence, ILogger<TeamDomainObject> log, - IServiceProvider serviceProvider) - : base(id, persistence, log) - { - this.serviceProvider = serviceProvider; - } + protected override bool IsDeleted(State snapshot) + { + return false; + } - protected override bool IsDeleted(State snapshot) - { - return false; - } + protected override bool CanAcceptCreation(ICommand command) + { + return command is TeamCommandBase; + } - protected override bool CanAcceptCreation(ICommand command) - { - return command is TeamCommandBase; - } + protected override bool CanAccept(ICommand command) + { + return command is TeamCommand update && Equals(update?.TeamId, Snapshot.Id); + } - protected override bool CanAccept(ICommand command) + public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct) + { + switch (command) { - return command is TeamCommand update && Equals(update?.TeamId, Snapshot.Id); - } + case CreateTeam create: + return CreateReturn(create, c => + { + GuardTeam.CanCreate(c); - public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct) - { - switch (command) - { - case CreateTeam create: - return CreateReturn(create, c => - { - GuardTeam.CanCreate(c); + Create(c); - Create(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case UpdateTeam update: + return UpdateReturn(update, c => + { + GuardTeam.CanUpdate(c); - case UpdateTeam update: - return UpdateReturn(update, c => - { - GuardTeam.CanUpdate(c); + Update(c); - Update(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case AssignContributor assignContributor: + return UpdateReturnAsync(assignContributor, async (c, ct) => + { + await GuardTeamContributors.CanAssign(c, Snapshot, Users); - case AssignContributor assignContributor: - return UpdateReturnAsync(assignContributor, async (c, ct) => - { - await GuardTeamContributors.CanAssign(c, Snapshot, Users); + AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); - AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case RemoveContributor removeContributor: + return UpdateReturn(removeContributor, c => + { + GuardTeamContributors.CanRemove(c, Snapshot); - case RemoveContributor removeContributor: - return UpdateReturn(removeContributor, c => - { - GuardTeamContributors.CanRemove(c, Snapshot); + RemoveContributor(c); - RemoveContributor(c); + return Snapshot; + }, ct); - return Snapshot; - }, ct); + case ChangePlan changePlan: + return UpdateReturnAsync(changePlan, async (c, ct) => + { + GuardTeam.CanChangePlan(c, BillingPlans); - case ChangePlan changePlan: - return UpdateReturnAsync(changePlan, async (c, ct) => + if (string.Equals(FreePlan?.Id, c.PlanId, StringComparison.Ordinal)) { - GuardTeam.CanChangePlan(c, BillingPlans); - - if (string.Equals(FreePlan?.Id, c.PlanId, StringComparison.Ordinal)) + if (!c.FromCallback) { - if (!c.FromCallback) - { - await BillingManager.UnsubscribeAsync(c.Actor.Identifier, Snapshot, default); - } + await BillingManager.UnsubscribeAsync(c.Actor.Identifier, Snapshot, default); + } - ResetPlan(c); + ResetPlan(c); - return new PlanChangedResult(c.PlanId, true, null); - } - else + return new PlanChangedResult(c.PlanId, true, null); + } + else + { + if (!c.FromCallback) { - if (!c.FromCallback) - { - var redirectUri = await BillingManager.MustRedirectToPortalAsync(c.Actor.Identifier, Snapshot, c.PlanId, ct); - - if (redirectUri != null) - { - return new PlanChangedResult(c.PlanId, false, redirectUri); - } + var redirectUri = await BillingManager.MustRedirectToPortalAsync(c.Actor.Identifier, Snapshot, c.PlanId, ct); - await BillingManager.SubscribeAsync(c.Actor.Identifier, Snapshot, changePlan.PlanId, default); + if (redirectUri != null) + { + return new PlanChangedResult(c.PlanId, false, redirectUri); } - ChangePlan(c); - - return new PlanChangedResult(c.PlanId); + await BillingManager.SubscribeAsync(c.Actor.Identifier, Snapshot, changePlan.PlanId, default); } - }, ct); - default: - ThrowHelper.NotSupportedException(); - return default!; - } - } - - private void Create(CreateTeam command) - { - void RaiseInitial<T>(T @event) where T : TeamEvent - { - Raise(command, @event, command.TeamId); - } + ChangePlan(c); - RaiseInitial(new TeamCreated()); + return new PlanChangedResult(c.PlanId); + } + }, ct); - var actor = command.Actor; - - if (actor.IsUser) - { - RaiseInitial(new TeamContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }); - } + default: + ThrowHelper.NotSupportedException(); + return default!; } + } - private void ChangePlan(ChangePlan command) + private void Create(CreateTeam command) + { + void RaiseInitial<T>(T @event) where T : TeamEvent { - Raise(command, new TeamPlanChanged()); + Raise(command, @event, command.TeamId); } - private void ResetPlan(ChangePlan command) - { - Raise(command, new TeamPlanReset()); - } + RaiseInitial(new TeamCreated()); - private void Update(UpdateTeam command) - { - Raise(command, new TeamUpdated()); - } + var actor = command.Actor; - private void AssignContributor(AssignContributor command, bool isAdded) + if (actor.IsUser) { - Raise(command, new TeamContributorAssigned { IsAdded = isAdded }); + RaiseInitial(new TeamContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }); } + } - private void RemoveContributor(RemoveContributor command) - { - Raise(command, new TeamContributorRemoved()); - } + private void ChangePlan(ChangePlan command) + { + Raise(command, new TeamPlanChanged()); + } - private void Raise<T, TEvent>(T command, TEvent @event, DomainId? id = null) where T : class where TEvent : TeamEvent - { - SimpleMapper.Map(command, @event); + private void ResetPlan(ChangePlan command) + { + Raise(command, new TeamPlanReset()); + } - @event.TeamId = id ?? Snapshot.Id; + private void Update(UpdateTeam command) + { + Raise(command, new TeamUpdated()); + } - RaiseEvent(Envelope.Create(@event)); - } + private void AssignContributor(AssignContributor command, bool isAdded) + { + Raise(command, new TeamContributorAssigned { IsAdded = isAdded }); + } - private IBillingPlans BillingPlans - { - get => serviceProvider.GetRequiredService<IBillingPlans>(); - } + private void RemoveContributor(RemoveContributor command) + { + Raise(command, new TeamContributorRemoved()); + } - private IBillingManager BillingManager - { - get => serviceProvider.GetRequiredService<IBillingManager>(); - } + private void Raise<T, TEvent>(T command, TEvent @event, DomainId? id = null) where T : class where TEvent : TeamEvent + { + SimpleMapper.Map(command, @event); - private IUserResolver Users - { - get => serviceProvider.GetRequiredService<IUserResolver>(); - } + @event.TeamId = id ?? Snapshot.Id; - private Plan FreePlan - { - get => BillingPlans.GetFreePlan(); - } + RaiseEvent(Envelope.Create(@event)); + } + + private IBillingPlans BillingPlans + { + get => serviceProvider.GetRequiredService<IBillingPlans>(); + } + + private IBillingManager BillingManager + { + get => serviceProvider.GetRequiredService<IBillingManager>(); + } + + private IUserResolver Users + { + get => serviceProvider.GetRequiredService<IUserResolver>(); + } + + private Plan FreePlan + { + get => BillingPlans.GetFreePlan(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/ITeamEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/ITeamEntity.cs index 8913918529..e7b6358659 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/ITeamEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/ITeamEntity.cs @@ -7,18 +7,17 @@ using Squidex.Domain.Apps.Core; -namespace Squidex.Domain.Apps.Entities.Teams +namespace Squidex.Domain.Apps.Entities.Teams; + +public interface ITeamEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion { - public interface ITeamEntity : - IEntity, - IEntityWithCreatedBy, - IEntityWithLastModifiedBy, - IEntityWithVersion - { - string Name { get; } + string Name { get; } - Contributors Contributors { get; } + Contributors Contributors { get; } - AssignedPlan? Plan { get; } - } + AssignedPlan? Plan { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/ITeamsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/ITeamsIndex.cs index b8ea6f19f3..d910ea4011 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/ITeamsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/ITeamsIndex.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Teams.Indexes +namespace Squidex.Domain.Apps.Entities.Teams.Indexes; + +public interface ITeamsIndex { - public interface ITeamsIndex - { - Task<ITeamEntity?> GetTeamAsync(DomainId id, - CancellationToken ct = default); + Task<ITeamEntity?> GetTeamAsync(DomainId id, + CancellationToken ct = default); - Task<List<ITeamEntity>> GetTeamsAsync(string userId, - CancellationToken ct = default); - } + Task<List<ITeamEntity>> GetTeamsAsync(string userId, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/TeamsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/TeamsIndex.cs index c64393b8e2..ea5823362d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/TeamsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Indexes/TeamsIndex.cs @@ -8,42 +8,41 @@ using Squidex.Domain.Apps.Entities.Teams.Repositories; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Teams.Indexes +namespace Squidex.Domain.Apps.Entities.Teams.Indexes; + +public sealed class TeamsIndex : ITeamsIndex { - public sealed class TeamsIndex : ITeamsIndex - { - private readonly ITeamRepository teamRepository; + private readonly ITeamRepository teamRepository; - public TeamsIndex(ITeamRepository teamRepository) - { - this.teamRepository = teamRepository; - } + public TeamsIndex(ITeamRepository teamRepository) + { + this.teamRepository = teamRepository; + } - public async Task<ITeamEntity?> GetTeamAsync(DomainId id, - CancellationToken ct = default) + public async Task<ITeamEntity?> GetTeamAsync(DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("TeamsIndex/GetTeamAsync")) { - using (Telemetry.Activities.StartActivity("TeamsIndex/GetTeamAsync")) - { - var team = await teamRepository.FindAsync(id, ct); + var team = await teamRepository.FindAsync(id, ct); - return IsValid(team) ? team : null; - } + return IsValid(team) ? team : null; } + } - public async Task<List<ITeamEntity>> GetTeamsAsync(string userId, - CancellationToken ct = default) + public async Task<List<ITeamEntity>> GetTeamsAsync(string userId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("TeamsIndex/GetTeamsAsync")) { - using (Telemetry.Activities.StartActivity("TeamsIndex/GetTeamsAsync")) - { - var teams = await teamRepository.QueryAllAsync(userId, ct); + var teams = await teamRepository.QueryAllAsync(userId, ct); - return teams.Where(IsValid).ToList(); - } + return teams.Where(IsValid).ToList(); } + } - private static bool IsValid(ITeamEntity? rule) - { - return rule is { Version: > EtagVersion.Empty }; - } + private static bool IsValid(ITeamEntity? rule) + { + return rule is { Version: > EtagVersion.Empty }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/Repositories/ITeamRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/Repositories/ITeamRepository.cs index afe19510ef..6ca1d731d2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/Repositories/ITeamRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/Repositories/ITeamRepository.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Teams.Repositories +namespace Squidex.Domain.Apps.Entities.Teams.Repositories; + +public interface ITeamRepository { - public interface ITeamRepository - { - Task<List<ITeamEntity>> QueryAllAsync(string contributorId, - CancellationToken ct = default); + Task<List<ITeamEntity>> QueryAllAsync(string contributorId, + CancellationToken ct = default); - Task<ITeamEntity?> FindAsync(DomainId id, - CancellationToken ct = default); - } + Task<ITeamEntity?> FindAsync(DomainId id, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamExtensions.cs index d424621dc1..10f835eb72 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamExtensions.cs @@ -7,13 +7,12 @@ using System.Diagnostics.CodeAnalysis; -namespace Squidex.Domain.Apps.Entities.Teams +namespace Squidex.Domain.Apps.Entities.Teams; + +public static class TeamExtensions { - public static class TeamExtensions + public static bool TryGetContributorRole(this ITeamEntity app, string id, [MaybeNullWhen(false)] out string role) { - public static bool TryGetContributorRole(this ITeamEntity app, string id, [MaybeNullWhen(false)] out string role) - { - return app.Contributors.TryGetValue(id, out role); - } + return app.Contributors.TryGetValue(id, out role); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs index e5f4d06b22..b4191fd943 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs @@ -10,71 +10,70 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Teams.Entities.Teams +namespace Squidex.Domain.Teams.Entities.Teams; + +public sealed class TeamHistoryEventsCreator : HistoryEventsCreatorBase { - public sealed class TeamHistoryEventsCreator : HistoryEventsCreatorBase + public TeamHistoryEventsCreator(TypeNameRegistry typeNameRegistry) + : base(typeNameRegistry) { - public TeamHistoryEventsCreator(TypeNameRegistry typeNameRegistry) - : base(typeNameRegistry) - { - AddEventMessage<TeamCreated>( - "history.teams.created"); + AddEventMessage<TeamCreated>( + "history.teams.created"); - AddEventMessage<TeamContributorAssigned>( - "history.teams.contributoreAssigned"); + AddEventMessage<TeamContributorAssigned>( + "history.teams.contributoreAssigned"); - AddEventMessage<TeamContributorRemoved>( - "history.teams.contributoreRemoved"); + AddEventMessage<TeamContributorRemoved>( + "history.teams.contributoreRemoved"); - AddEventMessage<TeamPlanChanged>( - "history.teams.planChanged"); + AddEventMessage<TeamPlanChanged>( + "history.teams.planChanged"); - AddEventMessage<TeamPlanReset>( - "history.teams.planReset"); + AddEventMessage<TeamPlanReset>( + "history.teams.planReset"); - AddEventMessage<TeamPlanReset>( - "history.teams.updated"); - } + AddEventMessage<TeamPlanReset>( + "history.teams.updated"); + } - private HistoryEvent? CreateEvent(IEvent @event) + private HistoryEvent? CreateEvent(IEvent @event) + { + switch (@event) { - switch (@event) - { - case TeamCreated e: - return CreateGeneralEvent(e, e.Name); - case TeamContributorAssigned e: - return CreateContributorsEvent(e, e.ContributorId, e.Role); - case TeamContributorRemoved e: - return CreateContributorsEvent(e, e.ContributorId); - case TeamPlanChanged e: - return CreatePlansEvent(e, e.PlanId); - case TeamPlanReset e: - return CreatePlansEvent(e); - case TeamUpdated e: - return CreateGeneralEvent(e, e.Name); - } - - return null; + case TeamCreated e: + return CreateGeneralEvent(e, e.Name); + case TeamContributorAssigned e: + return CreateContributorsEvent(e, e.ContributorId, e.Role); + case TeamContributorRemoved e: + return CreateContributorsEvent(e, e.ContributorId); + case TeamPlanChanged e: + return CreatePlansEvent(e, e.PlanId); + case TeamPlanReset e: + return CreatePlansEvent(e); + case TeamUpdated e: + return CreateGeneralEvent(e, e.Name); } - private HistoryEvent CreateGeneralEvent(IEvent e, string? name = null) - { - return ForEvent(e, "settings.general").Param("Name", name); - } + return null; + } - private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null) - { - return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); - } + private HistoryEvent CreateGeneralEvent(IEvent e, string? name = null) + { + return ForEvent(e, "settings.general").Param("Name", name); + } - private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) - { - return ForEvent(e, "settings.plan").Param("Plan", plan); - } + private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null) + { + return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); + } - protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event) - { - return Task.FromResult(CreateEvent(@event.Payload)); - } + private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) + { + return ForEvent(e, "settings.plan").Param("Plan", plan); + } + + protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event) + { + return Task.FromResult(CreateEvent(@event.Payload)); } } diff --git a/backend/src/Squidex.Domain.Apps.Events/AppEvent.cs b/backend/src/Squidex.Domain.Apps.Events/AppEvent.cs index 30ab1bd87c..d56b2e4d21 100644 --- a/backend/src/Squidex.Domain.Apps.Events/AppEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/AppEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events +namespace Squidex.Domain.Apps.Events; + +public abstract class AppEvent : SquidexEvent { - public abstract class AppEvent : SquidexEvent - { - public NamedId<DomainId> AppId { get; set; } - } + public NamedId<DomainId> AppId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs b/backend/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs index e6415c660a..c4da25354e 100644 --- a/backend/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs +++ b/backend/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs @@ -8,15 +8,14 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events +namespace Squidex.Domain.Apps.Events; + +[EventType(nameof(AppUsageExceeded))] +public sealed class AppUsageExceeded : AppEvent { - [EventType(nameof(AppUsageExceeded))] - public sealed class AppUsageExceeded : AppEvent - { - public long CallsCurrent { get; set; } + public long CallsCurrent { get; set; } - public long CallsLimit { get; set; } + public long CallsLimit { get; set; } - public DomainId RuleId { get; set; } - } + public DomainId RuleId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppAssetsScriptsConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppAssetsScriptsConfigured.cs index c11c17adc0..31ae1941ac 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppAssetsScriptsConfigured.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppAssetsScriptsConfigured.cs @@ -8,11 +8,10 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppAssetsScriptsConfigured))] +public sealed class AppAssetsScriptsConfigured : AppEvent { - [EventType(nameof(AppAssetsScriptsConfigured))] - public sealed class AppAssetsScriptsConfigured : AppEvent - { - public AssetScripts? Scripts { get; set; } - } + public AssetScripts? Scripts { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs index e265a33d16..570eeaf9a5 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientAttached.cs @@ -7,15 +7,14 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppClientAttached))] +public sealed class AppClientAttached : AppEvent { - [EventType(nameof(AppClientAttached))] - public sealed class AppClientAttached : AppEvent - { - public string Id { get; set; } + public string Id { get; set; } - public string Secret { get; set; } + public string Secret { get; set; } - public string? Role { get; set; } - } + public string? Role { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs index 218571b44f..dd4fa72aa0 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRevoked.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppClientRevoked))] +public sealed class AppClientRevoked : AppEvent { - [EventType(nameof(AppClientRevoked))] - public sealed class AppClientRevoked : AppEvent - { - public string Id { get; set; } - } + public string Id { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs index 1cee830817..05c591be5f 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs @@ -7,21 +7,20 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppClientUpdated), 2)] +public sealed class AppClientUpdated : AppEvent { - [EventType(nameof(AppClientUpdated), 2)] - public sealed class AppClientUpdated : AppEvent - { - public string Id { get; set; } + public string Id { get; set; } - public string? Name { get; set; } + public string? Name { get; set; } - public string? Role { get; set; } + public string? Role { get; set; } - public long? ApiCallsLimit { get; set; } + public long? ApiCallsLimit { get; set; } - public long? ApiTrafficLimit { get; set; } + public long? ApiTrafficLimit { get; set; } - public bool? AllowAnonymous { get; set; } - } + public bool? AllowAnonymous { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs index 204529bb7d..fbdb2b460f 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppContributorAssigned), 2)] +public sealed class AppContributorAssigned : AppEvent { - [EventType(nameof(AppContributorAssigned), 2)] - public sealed class AppContributorAssigned : AppEvent - { - public string ContributorId { get; set; } + public string ContributorId { get; set; } - public string Role { get; set; } + public string Role { get; set; } - public bool IsCreated { get; set; } + public bool IsCreated { get; set; } - public bool IsAdded { get; set; } - } + public bool IsAdded { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs index 69ec57bc42..c0535aff3f 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppContributorRemoved))] +public sealed class AppContributorRemoved : AppEvent { - [EventType(nameof(AppContributorRemoved))] - public sealed class AppContributorRemoved : AppEvent - { - public string ContributorId { get; set; } - } + public string ContributorId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs index b10664e6a0..7887ca6f2e 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppCreated))] +public sealed class AppCreated : AppEvent { - [EventType(nameof(AppCreated))] - public sealed class AppCreated : AppEvent - { - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppDeleted.cs index 4963f8d894..58b7c4f76f 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppDeleted.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppDeleted))] +public sealed class AppDeleted : AppEvent { - [EventType(nameof(AppDeleted))] - public sealed class AppDeleted : AppEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs index 1835d2941e..940662d9c6 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageRemoved.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppImageRemoved))] +public sealed class AppImageRemoved : AppEvent { - [EventType(nameof(AppImageRemoved))] - public sealed class AppImageRemoved : AppEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs index 9a237e6d8f..6b7e115f40 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppImageUploaded.cs @@ -8,11 +8,10 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppImageUploaded))] +public sealed class AppImageUploaded : AppEvent { - [EventType(nameof(AppImageUploaded))] - public sealed class AppImageUploaded : AppEvent - { - public AppImage Image { get; set; } - } + public AppImage Image { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs index 4ba3f28908..67f1e3341a 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageAdded.cs @@ -8,11 +8,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppLanguageAdded))] +public sealed class AppLanguageAdded : AppEvent { - [EventType(nameof(AppLanguageAdded))] - public sealed class AppLanguageAdded : AppEvent - { - public Language Language { get; set; } - } + public Language Language { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs index a5fd2fc681..faaabfe2e7 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageRemoved.cs @@ -8,11 +8,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppLanguageRemoved))] +public sealed class AppLanguageRemoved : AppEvent { - [EventType(nameof(AppLanguageRemoved))] - public sealed class AppLanguageRemoved : AppEvent - { - public Language Language { get; set; } - } + public Language Language { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs index addea9eb95..9be5e16cfb 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs @@ -8,17 +8,16 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[TypeName("AppLanguageUpdated")] +public sealed class AppLanguageUpdated : AppEvent { - [TypeName("AppLanguageUpdated")] - public sealed class AppLanguageUpdated : AppEvent - { - public Language Language { get; set; } + public Language Language { get; set; } - public bool IsOptional { get; set; } + public bool IsOptional { get; set; } - public bool IsMaster { get; set; } + public bool IsMaster { get; set; } - public Language[]? Fallback { get; set; } - } + public Language[]? Fallback { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs index bf22af3cd2..1e4a6f902a 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppMasterLanguageSet.cs @@ -8,11 +8,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppMasterLanguageSet))] +public sealed class AppMasterLanguageSet : AppEvent { - [EventType(nameof(AppMasterLanguageSet))] - public sealed class AppMasterLanguageSet : AppEvent - { - public Language Language { get; set; } - } + public Language Language { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs index 946c9ffcce..e7286bd868 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs @@ -8,16 +8,15 @@ using Squidex.Domain.Apps.Core; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppPlanChanged))] +public sealed class AppPlanChanged : AppEvent { - [EventType(nameof(AppPlanChanged))] - public sealed class AppPlanChanged : AppEvent - { - public string PlanId { get; set; } + public string PlanId { get; set; } - public AssignedPlan ToPlan() - { - return new AssignedPlan(Actor, PlanId); - } + public AssignedPlan ToPlan() + { + return new AssignedPlan(Actor, PlanId); } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs index 7941a2748e..9a58079f93 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanReset.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppPlanReset))] +public sealed class AppPlanReset : AppEvent { - [EventType(nameof(AppPlanReset))] - public sealed class AppPlanReset : AppEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs index 0bd58efebc..863c5f8919 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppRoleAdded))] +public sealed class AppRoleAdded : AppEvent { - [EventType(nameof(AppRoleAdded))] - public sealed class AppRoleAdded : AppEvent - { - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs index 227d980feb..8c50a48e1e 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppRoleDeleted))] +public sealed class AppRoleDeleted : AppEvent { - [EventType(nameof(AppRoleDeleted))] - public sealed class AppRoleDeleted : AppEvent - { - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs index 0d16c45a82..ecc83fa229 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs @@ -9,20 +9,19 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Security; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppRoleUpdated))] +public sealed class AppRoleUpdated : AppEvent { - [EventType(nameof(AppRoleUpdated))] - public sealed class AppRoleUpdated : AppEvent - { - public string Name { get; set; } + public string Name { get; set; } - public string[] Permissions { get; set; } + public string[] Permissions { get; set; } - public JsonObject? Properties { get; set; } + public JsonObject? Properties { get; set; } - public PermissionSet ToPermissions() - { - return new PermissionSet(Permissions); - } + public PermissionSet ToPermissions() + { + return new PermissionSet(Permissions); } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppSettingsUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppSettingsUpdated.cs index b79d4ef551..e27768bccd 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppSettingsUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppSettingsUpdated.cs @@ -8,11 +8,10 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppSettingsUpdated))] +public sealed class AppSettingsUpdated : AppEvent { - [EventType(nameof(AppSettingsUpdated))] - public sealed class AppSettingsUpdated : AppEvent - { - public AppSettings Settings { get; set; } - } + public AppSettings Settings { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppTransfered.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppTransfered.cs index f00e74a0b8..1976a6def6 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppTransfered.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppTransfered.cs @@ -8,11 +8,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppTransfered))] +public sealed class AppTransfered : AppEvent { - [EventType(nameof(AppTransfered))] - public sealed class AppTransfered : AppEvent - { - public DomainId? TeamId { get; set; } - } + public DomainId? TeamId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs index bcc4f8896a..8e04a53bd3 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppUpdated.cs @@ -7,13 +7,12 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppUpdated))] +public sealed class AppUpdated : AppEvent { - [EventType(nameof(AppUpdated))] - public sealed class AppUpdated : AppEvent - { - public string Label { get; set; } + public string Label { get; set; } - public string Description { get; set; } - } + public string Description { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs index 03ca4f6fc4..4ba923aa5a 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs @@ -8,13 +8,12 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppWorkflowAdded))] +public sealed class AppWorkflowAdded : AppEvent { - [EventType(nameof(AppWorkflowAdded))] - public sealed class AppWorkflowAdded : AppEvent - { - public DomainId WorkflowId { get; set; } + public DomainId WorkflowId { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs index c5a97051cc..a8fc5b7ee4 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs @@ -8,11 +8,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppWorkflowDeleted))] +public sealed class AppWorkflowDeleted : AppEvent { - [EventType(nameof(AppWorkflowDeleted))] - public sealed class AppWorkflowDeleted : AppEvent - { - public DomainId WorkflowId { get; set; } - } + public DomainId WorkflowId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs index 72fc4296ee..18d6236b6f 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs @@ -9,13 +9,12 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Apps +namespace Squidex.Domain.Apps.Events.Apps; + +[EventType(nameof(AppWorkflowUpdated))] +public sealed class AppWorkflowUpdated : AppEvent { - [EventType(nameof(AppWorkflowUpdated))] - public sealed class AppWorkflowUpdated : AppEvent - { - public DomainId WorkflowId { get; set; } + public DomainId WorkflowId { get; set; } - public Workflow Workflow { get; set; } - } + public Workflow Workflow { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs index 1b503677ca..16fe45a901 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs @@ -8,19 +8,18 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetAnnotated))] +public sealed class AssetAnnotated : AssetEvent { - [EventType(nameof(AssetAnnotated))] - public sealed class AssetAnnotated : AssetEvent - { - public string? FileName { get; set; } + public string? FileName { get; set; } - public string? Slug { get; set; } + public string? Slug { get; set; } - public bool? IsProtected { get; set; } + public bool? IsProtected { get; set; } - public AssetMetadata? Metadata { get; set; } + public AssetMetadata? Metadata { get; set; } - public HashSet<string>? Tags { get; set; } - } + public HashSet<string>? Tags { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs index 473ebf87ef..c9336e2783 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs @@ -9,29 +9,28 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetCreated), 2)] +public sealed class AssetCreated : AssetEvent { - [EventType(nameof(AssetCreated), 2)] - public sealed class AssetCreated : AssetEvent - { - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public string FileName { get; set; } + public string FileName { get; set; } - public string FileHash { get; set; } + public string FileHash { get; set; } - public string MimeType { get; set; } + public string MimeType { get; set; } - public string Slug { get; set; } + public string Slug { get; set; } - public long FileVersion { get; set; } + public long FileVersion { get; set; } - public long FileSize { get; set; } + public long FileSize { get; set; } - public AssetType Type { get; set; } + public AssetType Type { get; set; } - public AssetMetadata Metadata { get; set; } + public AssetMetadata Metadata { get; set; } - public HashSet<string>? Tags { get; set; } - } + public HashSet<string>? Tags { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs index aaeb5e7865..f3dce20226 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetDeleted.cs @@ -7,13 +7,12 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetDeleted))] +public sealed class AssetDeleted : AssetEvent { - [EventType(nameof(AssetDeleted))] - public sealed class AssetDeleted : AssetEvent - { - public long DeletedSize { get; set; } + public long DeletedSize { get; set; } - public HashSet<string>? OldTags { get; set; } - } + public HashSet<string>? OldTags { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs index 24ec47f989..6c23a4534d 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +public abstract class AssetEvent : AppEvent { - public abstract class AssetEvent : AppEvent - { - public DomainId AssetId { get; set; } - } + public DomainId AssetId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs index ffd53a1abc..dfda368faf 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs @@ -8,13 +8,12 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetFolderCreated))] +public sealed class AssetFolderCreated : AssetFolderEvent { - [EventType(nameof(AssetFolderCreated))] - public sealed class AssetFolderCreated : AssetFolderEvent - { - public DomainId ParentId { get; set; } + public DomainId ParentId { get; set; } - public string FolderName { get; set; } - } + public string FolderName { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs index 31e58756dd..6f10446888 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetFolderDeleted))] +public sealed class AssetFolderDeleted : AssetFolderEvent { - [EventType(nameof(AssetFolderDeleted))] - public sealed class AssetFolderDeleted : AssetFolderEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs index 2977261332..8de4c7b433 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +public abstract class AssetFolderEvent : AppEvent { - public abstract class AssetFolderEvent : AppEvent - { - public DomainId AssetFolderId { get; set; } - } + public DomainId AssetFolderId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs index 3be426a763..3ae3b8adc8 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs @@ -8,11 +8,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetFolderMoved))] +public sealed class AssetFolderMoved : AssetFolderEvent { - [EventType(nameof(AssetFolderMoved))] - public sealed class AssetFolderMoved : AssetFolderEvent - { - public DomainId ParentId { get; set; } - } + public DomainId ParentId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs index 8016098b11..c789374a14 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetFolderRenamed))] +public sealed class AssetFolderRenamed : AssetFolderEvent { - [EventType(nameof(AssetFolderRenamed))] - public sealed class AssetFolderRenamed : AssetFolderEvent - { - public string FolderName { get; set; } - } + public string FolderName { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs index 96250ff596..bad771c62d 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs @@ -8,11 +8,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetMoved))] +public sealed class AssetMoved : AssetEvent { - [EventType(nameof(AssetMoved))] - public sealed class AssetMoved : AssetEvent - { - public DomainId ParentId { get; set; } - } + public DomainId ParentId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs index b0dd55e201..e60700b5c4 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs @@ -8,21 +8,20 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Assets +namespace Squidex.Domain.Apps.Events.Assets; + +[EventType(nameof(AssetUpdated), 2)] +public sealed class AssetUpdated : AssetEvent { - [EventType(nameof(AssetUpdated), 2)] - public sealed class AssetUpdated : AssetEvent - { - public string MimeType { get; set; } + public string MimeType { get; set; } - public string FileHash { get; set; } + public string FileHash { get; set; } - public long FileSize { get; set; } + public long FileSize { get; set; } - public long FileVersion { get; set; } + public long FileVersion { get; set; } - public AssetType Type { get; set; } + public AssetType Type { get; set; } - public AssetMetadata Metadata { get; set; } - } + public AssetMetadata Metadata { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs index 72aa429508..79b202dedd 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs @@ -7,15 +7,14 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Comments +namespace Squidex.Domain.Apps.Events.Comments; + +[EventType(nameof(CommentCreated))] +public sealed class CommentCreated : CommentsEvent { - [EventType(nameof(CommentCreated))] - public sealed class CommentCreated : CommentsEvent - { - public string Text { get; set; } + public string Text { get; set; } - public string[]? Mentions { get; set; } + public string[]? Mentions { get; set; } - public Uri? Url { get; set; } - } + public Uri? Url { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs index a18b984c8e..f6282e2e0d 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Comments +namespace Squidex.Domain.Apps.Events.Comments; + +[EventType(nameof(CommentDeleted))] +public sealed class CommentDeleted : CommentsEvent { - [EventType(nameof(CommentDeleted))] - public sealed class CommentDeleted : CommentsEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs index fbfc57c638..dc314d772f 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Comments +namespace Squidex.Domain.Apps.Events.Comments; + +[EventType(nameof(CommentUpdated))] +public sealed class CommentUpdated : CommentsEvent { - [EventType(nameof(CommentUpdated))] - public sealed class CommentUpdated : CommentsEvent - { - public string Text { get; set; } - } + public string Text { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs index cf8ef7e998..652358c85f 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs @@ -7,12 +7,11 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events.Comments +namespace Squidex.Domain.Apps.Events.Comments; + +public abstract class CommentsEvent : AppEvent { - public abstract class CommentsEvent : AppEvent - { - public DomainId CommentsId { get; set; } + public DomainId CommentsId { get; set; } - public DomainId CommentId { get; set; } - } + public DomainId CommentId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs index 53cbf44984..69cc4c5f8b 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs @@ -8,13 +8,12 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +[EventType(nameof(ContentCreated), 2)] +public sealed class ContentCreated : ContentEvent { - [EventType(nameof(ContentCreated), 2)] - public sealed class ContentCreated : ContentEvent - { - public Status Status { get; set; } + public Status Status { get; set; } - public ContentData Data { get; set; } - } + public ContentData Data { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs index a209c0dc3f..d28a4ee3e0 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +[EventType(nameof(ContentDeleted))] +public sealed class ContentDeleted : ContentEvent { - [EventType(nameof(ContentDeleted))] - public sealed class ContentDeleted : ContentEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDraftCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDraftCreated.cs index 5009228343..4494bb231a 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDraftCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDraftCreated.cs @@ -8,13 +8,12 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +[EventType(nameof(ContentDraftCreated))] +public sealed class ContentDraftCreated : ContentEvent { - [EventType(nameof(ContentDraftCreated))] - public sealed class ContentDraftCreated : ContentEvent - { - public ContentData? MigratedData { get; set; } + public ContentData? MigratedData { get; set; } - public Status Status { get; set; } - } + public Status Status { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDraftDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDraftDeleted.cs index 9b49b52288..7a9894873c 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDraftDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentDraftDeleted.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +[EventType(nameof(ContentDraftDeleted))] +public sealed class ContentDraftDeleted : ContentEvent { - [EventType(nameof(ContentDraftDeleted))] - public sealed class ContentDraftDeleted : ContentEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs index 146d5b26a3..825f615cfc 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +public abstract class ContentEvent : SchemaEvent { - public abstract class ContentEvent : SchemaEvent - { - public DomainId ContentId { get; set; } - } + public DomainId ContentId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs index e585a64e18..7721b290aa 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentSchedulingCancelled.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +[EventType(nameof(ContentSchedulingCancelled))] +public sealed class ContentSchedulingCancelled : ContentEvent { - [EventType(nameof(ContentSchedulingCancelled))] - public sealed class ContentSchedulingCancelled : ContentEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs index 4addd8d14f..e684f92adc 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs @@ -8,15 +8,14 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +[EventType(nameof(ContentStatusChanged), 2)] +public sealed class ContentStatusChanged : ContentEvent { - [EventType(nameof(ContentStatusChanged), 2)] - public sealed class ContentStatusChanged : ContentEvent - { - public StatusChange Change { get; set; } + public StatusChange Change { get; set; } - public Status Status { get; set; } + public Status Status { get; set; } - public bool NewVersion { get; set; } - } + public bool NewVersion { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs index ffc322d0e8..af331ec5b8 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs @@ -9,13 +9,12 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +[EventType(nameof(ContentStatusScheduled))] +public sealed class ContentStatusScheduled : ContentEvent { - [EventType(nameof(ContentStatusScheduled))] - public sealed class ContentStatusScheduled : ContentEvent - { - public Status Status { get; set; } + public Status Status { get; set; } - public Instant DueTime { get; set; } - } + public Instant DueTime { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs index 83ab343326..9af4203f19 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs @@ -8,13 +8,12 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Contents +namespace Squidex.Domain.Apps.Events.Contents; + +[EventType(nameof(ContentUpdated))] +public sealed class ContentUpdated : ContentEvent { - [EventType(nameof(ContentUpdated))] - public sealed class ContentUpdated : ContentEvent - { - public ContentData Data { get; set; } + public ContentData Data { get; set; } - public bool NewVersion { get; set; } - } + public bool NewVersion { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs index ae4d0fd12b..5af774c959 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs @@ -9,25 +9,24 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; -namespace Squidex.Domain.Apps.Events.Rules +namespace Squidex.Domain.Apps.Events.Rules; + +[EventType(nameof(RuleCreated))] +public sealed class RuleCreated : RuleEvent, IMigrated<IEvent> { - [EventType(nameof(RuleCreated))] - public sealed class RuleCreated : RuleEvent, IMigrated<IEvent> - { - public RuleTrigger Trigger { get; set; } + public RuleTrigger Trigger { get; set; } - public RuleAction Action { get; set; } + public RuleAction Action { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public IEvent Migrate() + public IEvent Migrate() + { + if (Trigger is IMigrated<RuleTrigger> migrated) { - if (Trigger is IMigrated<RuleTrigger> migrated) - { - Trigger = migrated.Migrate(); - } - - return this; + Trigger = migrated.Migrate(); } + + return this; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs index 8532bc8212..19c4233a5a 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Rules +namespace Squidex.Domain.Apps.Events.Rules; + +[EventType(nameof(RuleDeleted))] +public sealed class RuleDeleted : RuleEvent { - [EventType(nameof(RuleDeleted))] - public sealed class RuleDeleted : RuleEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs index 1510990327..971081adf7 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Rules +namespace Squidex.Domain.Apps.Events.Rules; + +[EventType(nameof(RuleDisabled))] +public sealed class RuleDisabled : RuleEvent { - [EventType(nameof(RuleDisabled))] - public sealed class RuleDisabled : RuleEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs index e9c4c6c023..1e08e44fa5 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Rules +namespace Squidex.Domain.Apps.Events.Rules; + +[EventType(nameof(RuleEnabled))] +public sealed class RuleEnabled : RuleEvent { - [EventType(nameof(RuleEnabled))] - public sealed class RuleEnabled : RuleEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs index cc49b34543..599616fba2 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events.Rules +namespace Squidex.Domain.Apps.Events.Rules; + +public abstract class RuleEvent : AppEvent { - public abstract class RuleEvent : AppEvent - { - public DomainId RuleId { get; set; } - } + public DomainId RuleId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs index 2d455a2305..124c3fddff 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleManuallyTriggered.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Events.Rules +namespace Squidex.Domain.Apps.Events.Rules; + +public sealed class RuleManuallyTriggered : RuleEvent { - public sealed class RuleManuallyTriggered : RuleEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs index 1f3c5d72cf..bdb1c3711e 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs @@ -9,31 +9,30 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; -namespace Squidex.Domain.Apps.Events.Rules +namespace Squidex.Domain.Apps.Events.Rules; + +[EventType(nameof(RuleUpdated))] +public sealed class RuleUpdated : RuleEvent, IMigrated<IEvent> { - [EventType(nameof(RuleUpdated))] - public sealed class RuleUpdated : RuleEvent, IMigrated<IEvent> - { - public string? Name { get; set; } + public string? Name { get; set; } - public RuleTrigger? Trigger { get; set; } + public RuleTrigger? Trigger { get; set; } - public RuleAction? Action { get; set; } + public RuleAction? Action { get; set; } - public bool? IsEnabled { get; set; } + public bool? IsEnabled { get; set; } - public IEvent Migrate() + public IEvent Migrate() + { + if (Trigger is IMigrated<RuleTrigger> migrated) { - if (Trigger is IMigrated<RuleTrigger> migrated) - { - var clone = (RuleUpdated)MemberwiseClone(); + var clone = (RuleUpdated)MemberwiseClone(); - clone.Trigger = migrated.Migrate(); + clone.Trigger = migrated.Migrate(); - return clone; - } - - return this; + return clone; } + + return this; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/SchemaEvent.cs b/backend/src/Squidex.Domain.Apps.Events/SchemaEvent.cs index f4965af981..1f7f74b17b 100644 --- a/backend/src/Squidex.Domain.Apps.Events/SchemaEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/SchemaEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events +namespace Squidex.Domain.Apps.Events; + +public abstract class SchemaEvent : AppEvent { - public abstract class SchemaEvent : AppEvent - { - public NamedId<DomainId> SchemaId { get; set; } - } + public NamedId<DomainId> SchemaId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs index aec0de6170..edf78d8247 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs @@ -8,15 +8,14 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(FieldAdded))] +public sealed class FieldAdded : FieldEvent { - [EventType(nameof(FieldAdded))] - public sealed class FieldAdded : FieldEvent - { - public string Name { get; set; } + public string Name { get; set; } - public string? Partitioning { get; set; } + public string? Partitioning { get; set; } - public FieldProperties Properties { get; set; } - } + public FieldProperties Properties { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs index c7b7e91e0b..f772ad3659 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(FieldDeleted))] +public sealed class FieldDeleted : FieldEvent { - [EventType(nameof(FieldDeleted))] - public sealed class FieldDeleted : FieldEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs index 9bae251d79..c35ae0876b 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(FieldDisabled))] +public sealed class FieldDisabled : FieldEvent { - [EventType(nameof(FieldDisabled))] - public sealed class FieldDisabled : FieldEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs index 9618f210bb..79cc665bfe 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(FieldEnabled))] +public sealed class FieldEnabled : FieldEvent { - [EventType(nameof(FieldEnabled))] - public sealed class FieldEnabled : FieldEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs index 51c1af837f..0742b66935 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +public abstract class FieldEvent : ParentFieldEvent { - public abstract class FieldEvent : ParentFieldEvent - { - public NamedId<long> FieldId { get; set; } - } + public NamedId<long> FieldId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs index 52b4496346..d56c32cb06 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(FieldHidden))] +public sealed class FieldHidden : FieldEvent { - [EventType(nameof(FieldHidden))] - public sealed class FieldHidden : FieldEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs index 5522536020..dcfba166e0 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(FieldLocked))] +public sealed class FieldLocked : FieldEvent { - [EventType(nameof(FieldLocked))] - public sealed class FieldLocked : FieldEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs index d13adbecc4..71514e67b1 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(FieldShown))] +public sealed class FieldShown : FieldEvent { - [EventType(nameof(FieldShown))] - public sealed class FieldShown : FieldEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs index b1340cd8ef..c7cd2b3e74 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs @@ -8,11 +8,10 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(FieldUpdated))] +public sealed class FieldUpdated : FieldEvent { - [EventType(nameof(FieldUpdated))] - public sealed class FieldUpdated : FieldEvent - { - public FieldProperties Properties { get; set; } - } + public FieldProperties Properties { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs index 08f3746810..959bcf239c 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +public abstract class ParentFieldEvent : SchemaEvent { - public abstract class ParentFieldEvent : SchemaEvent - { - public NamedId<long>? ParentFieldId { get; set; } - } + public NamedId<long>? ParentFieldId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs index 17d0561fd3..0d4b648806 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaCategoryChanged))] +public sealed class SchemaCategoryChanged : SchemaEvent { - [EventType(nameof(SchemaCategoryChanged))] - public sealed class SchemaCategoryChanged : SchemaEvent - { - public string? Name { get; set; } - } + public string? Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs index e9b5478004..5e9b114ea6 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs @@ -8,11 +8,10 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaCreated), 2)] +public sealed class SchemaCreated : SchemaEvent { - [EventType(nameof(SchemaCreated), 2)] - public sealed class SchemaCreated : SchemaEvent - { - public Schema Schema { get; set; } - } + public Schema Schema { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs index 65de93f84b..a0829f63af 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +public sealed class SchemaCreatedField : SchemaCreatedFieldBase { - public sealed class SchemaCreatedField : SchemaCreatedFieldBase - { - public string Partitioning { get; set; } + public string Partitioning { get; set; } - public SchemaCreatedNestedField[] Nested { get; set; } - } + public SchemaCreatedNestedField[] Nested { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs index da27beec54..241046be6e 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs @@ -7,18 +7,17 @@ using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +public abstract class SchemaCreatedFieldBase : IFieldSettings { - public abstract class SchemaCreatedFieldBase : IFieldSettings - { - public string Name { get; set; } + public string Name { get; set; } - public bool IsHidden { get; set; } + public bool IsHidden { get; set; } - public bool IsLocked { get; set; } + public bool IsLocked { get; set; } - public bool IsDisabled { get; set; } + public bool IsDisabled { get; set; } - public FieldProperties Properties { get; set; } - } + public FieldProperties Properties { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs index bb2c22c505..6dad13de40 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedNestedField.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +public sealed class SchemaCreatedNestedField : SchemaCreatedFieldBase { - public sealed class SchemaCreatedNestedField : SchemaCreatedFieldBase - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs index 44f3746ce9..59be4592c3 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaDeleted))] +public sealed class SchemaDeleted : SchemaEvent { - [EventType(nameof(SchemaDeleted))] - public sealed class SchemaDeleted : SchemaEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs index bb22cefc21..a91d9d2cfe 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs @@ -8,11 +8,10 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaFieldRulesConfigured))] +public sealed class SchemaFieldRulesConfigured : SchemaEvent { - [EventType(nameof(SchemaFieldRulesConfigured))] - public sealed class SchemaFieldRulesConfigured : SchemaEvent - { - public FieldRules? FieldRules { get; set; } - } + public FieldRules? FieldRules { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs index 83d5da9faa..dc31526cc0 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaFieldsReordered))] +public sealed class SchemaFieldsReordered : ParentFieldEvent { - [EventType(nameof(SchemaFieldsReordered))] - public sealed class SchemaFieldsReordered : ParentFieldEvent - { - public long[] FieldIds { get; set; } - } + public long[] FieldIds { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs index 849e27d98b..b536f22e15 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs @@ -8,11 +8,10 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaPreviewUrlsConfigured))] +public sealed class SchemaPreviewUrlsConfigured : SchemaEvent { - [EventType(nameof(SchemaPreviewUrlsConfigured))] - public sealed class SchemaPreviewUrlsConfigured : SchemaEvent - { - public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } - } + public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs index d727eef22d..1b54481bbb 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaPublished))] +public sealed class SchemaPublished : SchemaEvent { - [EventType(nameof(SchemaPublished))] - public sealed class SchemaPublished : SchemaEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs index 86e0c61db0..e262d89e99 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs @@ -8,11 +8,10 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaScriptsConfigured))] +public sealed class SchemaScriptsConfigured : SchemaEvent { - [EventType(nameof(SchemaScriptsConfigured))] - public sealed class SchemaScriptsConfigured : SchemaEvent - { - public SchemaScripts? Scripts { get; set; } - } + public SchemaScripts? Scripts { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUIFieldsConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUIFieldsConfigured.cs index 364fb93971..ccc30da759 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUIFieldsConfigured.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUIFieldsConfigured.cs @@ -8,13 +8,12 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaUIFieldsConfigured))] +public sealed class SchemaUIFieldsConfigured : SchemaEvent { - [EventType(nameof(SchemaUIFieldsConfigured))] - public sealed class SchemaUIFieldsConfigured : SchemaEvent - { - public FieldNames? FieldsInLists { get; set; } + public FieldNames? FieldsInLists { get; set; } - public FieldNames? FieldsInReferences { get; set; } - } + public FieldNames? FieldsInReferences { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs index d5b61a6538..19f7f61e3b 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaUnpublished))] +public sealed class SchemaUnpublished : SchemaEvent { - [EventType(nameof(SchemaUnpublished))] - public sealed class SchemaUnpublished : SchemaEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs index ab7d8f9766..fa136e3d9b 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs @@ -8,11 +8,10 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Schemas +namespace Squidex.Domain.Apps.Events.Schemas; + +[EventType(nameof(SchemaUpdated))] +public sealed class SchemaUpdated : SchemaEvent { - [EventType(nameof(SchemaUpdated))] - public sealed class SchemaUpdated : SchemaEvent - { - public SchemaProperties Properties { get; set; } - } + public SchemaProperties Properties { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs index cf8ea7e65d..ac8ede1a85 100644 --- a/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs @@ -8,12 +8,11 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events +namespace Squidex.Domain.Apps.Events; + +public abstract class SquidexEvent : IEvent { - public abstract class SquidexEvent : IEvent - { - public RefToken Actor { get; set; } + public RefToken Actor { get; set; } - public bool FromRule { get; set; } - } + public bool FromRule { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/SquidexEvents.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexEvents.cs index 75eb77f049..bc1c1680db 100644 --- a/backend/src/Squidex.Domain.Apps.Events/SquidexEvents.cs +++ b/backend/src/Squidex.Domain.Apps.Events/SquidexEvents.cs @@ -9,10 +9,9 @@ #pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. -namespace Squidex.Domain.Apps.Events +namespace Squidex.Domain.Apps.Events; + +public sealed class SquidexEvents { - public sealed class SquidexEvents - { - public static readonly Assembly Assembly = typeof(SquidexEvents).Assembly; - } + public static readonly Assembly Assembly = typeof(SquidexEvents).Assembly; } diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorAssigned.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorAssigned.cs index a1b9f510a4..5eadbd467c 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorAssigned.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorAssigned.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Teams +namespace Squidex.Domain.Apps.Events.Teams; + +[EventType(nameof(TeamContributorAssigned))] +public sealed class TeamContributorAssigned : TeamEvent { - [EventType(nameof(TeamContributorAssigned))] - public sealed class TeamContributorAssigned : TeamEvent - { - public string ContributorId { get; set; } + public string ContributorId { get; set; } - public string Role { get; set; } + public string Role { get; set; } - public bool IsCreated { get; set; } + public bool IsCreated { get; set; } - public bool IsAdded { get; set; } - } + public bool IsAdded { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorRemoved.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorRemoved.cs index 6ebcf4264f..049b8620e9 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorRemoved.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamContributorRemoved.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Teams +namespace Squidex.Domain.Apps.Events.Teams; + +[EventType(nameof(TeamContributorRemoved))] +public sealed class TeamContributorRemoved : TeamEvent { - [EventType(nameof(TeamContributorRemoved))] - public sealed class TeamContributorRemoved : TeamEvent - { - public string ContributorId { get; set; } - } + public string ContributorId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamCreated.cs index 5c87b4dc00..e2c232e748 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamCreated.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Teams +namespace Squidex.Domain.Apps.Events.Teams; + +[EventType(nameof(TeamCreated))] +public sealed class TeamCreated : TeamEvent { - [EventType(nameof(TeamCreated))] - public sealed class TeamCreated : TeamEvent - { - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamEvent.cs index 271600d81b..3bb3c0fd17 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Events.Teams +namespace Squidex.Domain.Apps.Events.Teams; + +public abstract class TeamEvent : SquidexEvent { - public abstract class TeamEvent : SquidexEvent - { - public DomainId TeamId { get; set; } - } + public DomainId TeamId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanChanged.cs index 7db4d6553f..fe09e111a1 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanChanged.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanChanged.cs @@ -8,16 +8,15 @@ using Squidex.Domain.Apps.Core; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Teams +namespace Squidex.Domain.Apps.Events.Teams; + +[EventType(nameof(TeamPlanChanged))] +public sealed class TeamPlanChanged : TeamEvent { - [EventType(nameof(TeamPlanChanged))] - public sealed class TeamPlanChanged : TeamEvent - { - public string PlanId { get; set; } + public string PlanId { get; set; } - public AssignedPlan ToPlan() - { - return new AssignedPlan(Actor, PlanId); - } + public AssignedPlan ToPlan() + { + return new AssignedPlan(Actor, PlanId); } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanReset.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanReset.cs index ba44673c98..0283fc9a5d 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanReset.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamPlanReset.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Teams +namespace Squidex.Domain.Apps.Events.Teams; + +[EventType(nameof(TeamPlanReset))] +public sealed class TeamPlanReset : TeamEvent { - [EventType(nameof(TeamPlanReset))] - public sealed class TeamPlanReset : TeamEvent - { - } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamUpdated.cs index ddb8c3c7ba..a810a06976 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Teams/TeamUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Teams/TeamUpdated.cs @@ -7,11 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Teams +namespace Squidex.Domain.Apps.Events.Teams; + +[EventType(nameof(TeamUpdated))] +public sealed class TeamUpdated : TeamEvent { - [EventType(nameof(TeamUpdated))] - public sealed class TeamUpdated : TeamEvent - { - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs index a412a4b754..7e3ddd7210 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs @@ -12,124 +12,123 @@ using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Users.MongoDb +namespace Squidex.Domain.Users.MongoDb; + +public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleStore<IdentityRole> { - public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleStore<IdentityRole> + static MongoRoleStore() { - static MongoRoleStore() + BsonClassMap.RegisterClassMap<IdentityRole<string>>(cm => { - BsonClassMap.RegisterClassMap<IdentityRole<string>>(cm => - { - cm.AutoMap(); + cm.AutoMap(); - cm.MapMember(x => x.Id) - .SetSerializer(new StringSerializer(BsonType.ObjectId)); + cm.MapMember(x => x.Id) + .SetSerializer(new StringSerializer(BsonType.ObjectId)); - cm.UnmapMember(x => x.ConcurrencyStamp); - }); - } + cm.UnmapMember(x => x.ConcurrencyStamp); + }); + } - public MongoRoleStore(IMongoDatabase database) - : base(database) - { - } + public MongoRoleStore(IMongoDatabase database) + : base(database) + { + } - protected override string CollectionName() - { - return "Identity_Roles"; - } + protected override string CollectionName() + { + return "Identity_Roles"; + } - protected override Task SetupCollectionAsync(IMongoCollection<IdentityRole> collection, - CancellationToken ct) - { - return collection.Indexes.CreateOneAsync( - new CreateIndexModel<IdentityRole>( - Index - .Ascending(x => x.NormalizedName), - new CreateIndexOptions - { - Unique = true - }), - cancellationToken: ct); - } - - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; - } + protected override Task SetupCollectionAsync(IMongoCollection<IdentityRole> collection, + CancellationToken ct) + { + return collection.Indexes.CreateOneAsync( + new CreateIndexModel<IdentityRole>( + Index + .Ascending(x => x.NormalizedName), + new CreateIndexOptions + { + Unique = true + }), + cancellationToken: ct); + } - public void Dispose() - { - } + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; + } - public async Task<IdentityRole> FindByIdAsync(string roleId, - CancellationToken cancellationToken) - { - return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken); - } + public void Dispose() + { + } - public async Task<IdentityRole> FindByNameAsync(string normalizedRoleName, - CancellationToken cancellationToken) - { - return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken); - } + public async Task<IdentityRole> FindByIdAsync(string roleId, + CancellationToken cancellationToken) + { + return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken); + } - public async Task<IdentityResult> CreateAsync(IdentityRole role, - CancellationToken cancellationToken) - { - await Collection.InsertOneAsync(role, null, cancellationToken); + public async Task<IdentityRole> FindByNameAsync(string normalizedRoleName, + CancellationToken cancellationToken) + { + return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken); + } - return IdentityResult.Success; - } + public async Task<IdentityResult> CreateAsync(IdentityRole role, + CancellationToken cancellationToken) + { + await Collection.InsertOneAsync(role, null, cancellationToken); - public async Task<IdentityResult> UpdateAsync(IdentityRole role, - CancellationToken cancellationToken) - { - await Collection.ReplaceOneAsync(x => x.Id == role.Id, role, cancellationToken: cancellationToken); + return IdentityResult.Success; + } - return IdentityResult.Success; - } + public async Task<IdentityResult> UpdateAsync(IdentityRole role, + CancellationToken cancellationToken) + { + await Collection.ReplaceOneAsync(x => x.Id == role.Id, role, cancellationToken: cancellationToken); - public async Task<IdentityResult> DeleteAsync(IdentityRole role, - CancellationToken cancellationToken) - { - await Collection.DeleteOneAsync(x => x.Id == role.Id, null, cancellationToken); + return IdentityResult.Success; + } - return IdentityResult.Success; - } + public async Task<IdentityResult> DeleteAsync(IdentityRole role, + CancellationToken cancellationToken) + { + await Collection.DeleteOneAsync(x => x.Id == role.Id, null, cancellationToken); - public Task<string> GetRoleIdAsync(IdentityRole role, - CancellationToken cancellationToken) - { - return Task.FromResult(role.Id); - } + return IdentityResult.Success; + } - public Task<string> GetRoleNameAsync(IdentityRole role, - CancellationToken cancellationToken) - { - return Task.FromResult(role.Name); - } + public Task<string> GetRoleIdAsync(IdentityRole role, + CancellationToken cancellationToken) + { + return Task.FromResult(role.Id); + } - public Task<string> GetNormalizedRoleNameAsync(IdentityRole role, - CancellationToken cancellationToken) - { - return Task.FromResult(role.NormalizedName); - } + public Task<string> GetRoleNameAsync(IdentityRole role, + CancellationToken cancellationToken) + { + return Task.FromResult(role.Name); + } - public Task SetRoleNameAsync(IdentityRole role, string roleName, - CancellationToken cancellationToken) - { - role.Name = roleName; + public Task<string> GetNormalizedRoleNameAsync(IdentityRole role, + CancellationToken cancellationToken) + { + return Task.FromResult(role.NormalizedName); + } - return Task.CompletedTask; - } + public Task SetRoleNameAsync(IdentityRole role, string roleName, + CancellationToken cancellationToken) + { + role.Name = roleName; - public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, - CancellationToken cancellationToken) - { - role.NormalizedName = normalizedName; + return Task.CompletedTask; + } + + public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, + CancellationToken cancellationToken) + { + role.NormalizedName = normalizedName; - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs index bf48a508c1..3b1381f5f3 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs @@ -12,110 +12,109 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Users.MongoDb +namespace Squidex.Domain.Users.MongoDb; + +public sealed class MongoUser : IdentityUser { - public sealed class MongoUser : IdentityUser + [BsonRequired] + [BsonElement] + public List<Claim> Claims { get; set; } = new List<Claim>(); + + [BsonRequired] + [BsonElement] + public List<UserTokenInfo> Tokens { get; set; } = new List<UserTokenInfo>(); + + [BsonRequired] + [BsonElement] + public List<UserLogin> Logins { get; set; } = new List<UserLogin>(); + + [BsonRequired] + [BsonElement] + public HashSet<string> Roles { get; set; } = new HashSet<string>(); + + internal string? GetToken(string provider, string name) + { + return Tokens.Find(x => x.LoginProvider == provider && x.Name == name)?.Value; + } + + internal void AddLogin(UserLoginInfo login) + { + Logins.Add(new UserLogin(login)); + } + + internal void AddRole(string role) + { + Roles.Add(role); + } + + internal void AddClaim(Claim claim) + { + Claims.Add(claim); + } + + internal void AddClaims(IEnumerable<Claim> claims) + { + claims.Foreach(AddClaim); + } + + internal void AddToken(string provider, string name, string value) + { + Tokens.Add(new UserTokenInfo { LoginProvider = provider, Name = name, Value = value }); + } + + internal void RemoveLogin(string provider, string providerKey) + { + Logins.RemoveAll(x => x.LoginProvider == provider && x.ProviderKey == providerKey); + } + + internal void RemoveClaim(Claim claim) + { + Claims.RemoveAll(x => x.Type == claim.Type && x.Value == claim.Value); + } + + internal void RemoveToken(string provider, string name) + { + Tokens.RemoveAll(x => x.LoginProvider == provider && x.Name == name); + } + + internal void RemoveRole(string role) + { + Roles.Remove(role); + } + + internal void RemoveClaims(IEnumerable<Claim> claims) + { + claims.Foreach(RemoveClaim); + } + + internal void ReplaceClaim(Claim existingClaim, Claim newClaim) + { + RemoveClaim(existingClaim); + + AddClaim(newClaim); + } + + internal void ReplaceToken(string provider, string name, string value) { - [BsonRequired] - [BsonElement] - public List<Claim> Claims { get; set; } = new List<Claim>(); - - [BsonRequired] - [BsonElement] - public List<UserTokenInfo> Tokens { get; set; } = new List<UserTokenInfo>(); - - [BsonRequired] - [BsonElement] - public List<UserLogin> Logins { get; set; } = new List<UserLogin>(); - - [BsonRequired] - [BsonElement] - public HashSet<string> Roles { get; set; } = new HashSet<string>(); - - internal string? GetToken(string provider, string name) - { - return Tokens.Find(x => x.LoginProvider == provider && x.Name == name)?.Value; - } - - internal void AddLogin(UserLoginInfo login) - { - Logins.Add(new UserLogin(login)); - } - - internal void AddRole(string role) - { - Roles.Add(role); - } - - internal void AddClaim(Claim claim) - { - Claims.Add(claim); - } - - internal void AddClaims(IEnumerable<Claim> claims) - { - claims.Foreach(AddClaim); - } - - internal void AddToken(string provider, string name, string value) - { - Tokens.Add(new UserTokenInfo { LoginProvider = provider, Name = name, Value = value }); - } - - internal void RemoveLogin(string provider, string providerKey) - { - Logins.RemoveAll(x => x.LoginProvider == provider && x.ProviderKey == providerKey); - } - - internal void RemoveClaim(Claim claim) - { - Claims.RemoveAll(x => x.Type == claim.Type && x.Value == claim.Value); - } - - internal void RemoveToken(string provider, string name) - { - Tokens.RemoveAll(x => x.LoginProvider == provider && x.Name == name); - } - - internal void RemoveRole(string role) - { - Roles.Remove(role); - } - - internal void RemoveClaims(IEnumerable<Claim> claims) - { - claims.Foreach(RemoveClaim); - } - - internal void ReplaceClaim(Claim existingClaim, Claim newClaim) - { - RemoveClaim(existingClaim); - - AddClaim(newClaim); - } - - internal void ReplaceToken(string provider, string name, string value) - { - RemoveToken(provider, name); - - AddToken(provider, name, value); - } + RemoveToken(provider, name); + + AddToken(provider, name, value); } +} + +public sealed class UserTokenInfo : IdentityUserToken<string> +{ +} - public sealed class UserTokenInfo : IdentityUserToken<string> +public sealed class UserLogin : UserLoginInfo +{ + public UserLogin(string provider, string providerKey, string displayName) + : base(provider, providerKey, displayName) { } - public sealed class UserLogin : UserLoginInfo + public UserLogin(UserLoginInfo source) + : base(source.LoginProvider, source.ProviderKey, source.ProviderDisplayName) { - public UserLogin(string provider, string providerKey, string displayName) - : base(provider, providerKey, displayName) - { - } - - public UserLogin(UserLoginInfo source) - : base(source.LoginProvider, source.ProviderKey, source.ProviderDisplayName) - { - } } } diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs index b0860198be..c90f92e5fb 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs @@ -13,645 +13,644 @@ using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Users.MongoDb +namespace Squidex.Domain.Users.MongoDb; + +public sealed class MongoUserStore : + MongoRepositoryBase<MongoUser>, + IUserAuthenticationTokenStore<IdentityUser>, + IUserAuthenticatorKeyStore<IdentityUser>, + IUserClaimStore<IdentityUser>, + IUserEmailStore<IdentityUser>, + IUserFactory, + IUserLockoutStore<IdentityUser>, + IUserLoginStore<IdentityUser>, + IUserPasswordStore<IdentityUser>, + IUserPhoneNumberStore<IdentityUser>, + IUserRoleStore<IdentityUser>, + IUserSecurityStampStore<IdentityUser>, + IUserTwoFactorStore<IdentityUser>, + IUserTwoFactorRecoveryCodeStore<IdentityUser>, + IQueryableUserStore<IdentityUser> { - public sealed class MongoUserStore : - MongoRepositoryBase<MongoUser>, - IUserAuthenticationTokenStore<IdentityUser>, - IUserAuthenticatorKeyStore<IdentityUser>, - IUserClaimStore<IdentityUser>, - IUserEmailStore<IdentityUser>, - IUserFactory, - IUserLockoutStore<IdentityUser>, - IUserLoginStore<IdentityUser>, - IUserPasswordStore<IdentityUser>, - IUserPhoneNumberStore<IdentityUser>, - IUserRoleStore<IdentityUser>, - IUserSecurityStampStore<IdentityUser>, - IUserTwoFactorStore<IdentityUser>, - IUserTwoFactorRecoveryCodeStore<IdentityUser>, - IQueryableUserStore<IdentityUser> - { - private const string InternalLoginProvider = "[AspNetUserStore]"; - private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; - private const string RecoveryCodeTokenName = "RecoveryCodes"; - - static MongoUserStore() - { - BsonClassMap.RegisterClassMap<Claim>(cm => - { - cm.MapConstructor(typeof(Claim).GetConstructors() - .First(x => - { - var parameters = x.GetParameters(); - - return parameters.Length == 2 && - parameters[0].Name == "type" && - parameters[0].ParameterType == typeof(string) && - parameters[1].Name == "value" && - parameters[1].ParameterType == typeof(string); - })) - .SetArguments(new[] - { - nameof(Claim.Type), - nameof(Claim.Value) - }); - - cm.MapMember(x => x.Type); - cm.MapMember(x => x.Value); - }); - - BsonClassMap.RegisterClassMap<UserLogin>(cm => - { - cm.MapConstructor(typeof(UserLogin).GetConstructors() - .First(x => - { - var parameters = x.GetParameters(); - - return parameters.Length == 3; - })) - .SetArguments(new[] - { - nameof(UserLogin.LoginProvider), - nameof(UserLogin.ProviderKey), - nameof(UserLogin.ProviderDisplayName) - }); - - cm.AutoMap(); - }); - - BsonClassMap.RegisterClassMap<IdentityUserToken<string>>(cm => - { - cm.AutoMap(); - - cm.UnmapMember(x => x.UserId); - }); - - BsonClassMap.RegisterClassMap<IdentityUser<string>>(cm => - { - cm.AutoMap(); - - cm.MapMember(x => x.Id) - .SetSerializer(new StringSerializer(BsonType.ObjectId)); - - cm.MapMember(x => x.AccessFailedCount) - .SetIgnoreIfDefault(true); - - cm.MapMember(x => x.EmailConfirmed) - .SetIgnoreIfDefault(true); - - cm.MapMember(x => x.LockoutEnd) - .SetElementName("LockoutEndDateUtc").SetIgnoreIfNull(true); - - cm.MapMember(x => x.LockoutEnabled) - .SetIgnoreIfDefault(true); - - cm.MapMember(x => x.PasswordHash) - .SetIgnoreIfNull(true); - - cm.MapMember(x => x.PhoneNumber) - .SetIgnoreIfNull(true); - - cm.MapMember(x => x.PhoneNumberConfirmed) - .SetIgnoreIfDefault(true); - - cm.MapMember(x => x.SecurityStamp) - .SetIgnoreIfNull(true); - - cm.MapMember(x => x.TwoFactorEnabled) - .SetIgnoreIfDefault(true); - }); - } + private const string InternalLoginProvider = "[AspNetUserStore]"; + private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; + private const string RecoveryCodeTokenName = "RecoveryCodes"; - public MongoUserStore(IMongoDatabase database) - : base(database) + static MongoUserStore() + { + BsonClassMap.RegisterClassMap<Claim>(cm => { - } + cm.MapConstructor(typeof(Claim).GetConstructors() + .First(x => + { + var parameters = x.GetParameters(); - protected override string CollectionName() - { - return "Identity_Users"; - } + return parameters.Length == 2 && + parameters[0].Name == "type" && + parameters[0].ParameterType == typeof(string) && + parameters[1].Name == "value" && + parameters[1].ParameterType == typeof(string); + })) + .SetArguments(new[] + { + nameof(Claim.Type), + nameof(Claim.Value) + }); - protected override Task SetupCollectionAsync(IMongoCollection<MongoUser> collection, - CancellationToken ct) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel<MongoUser>( - Index - .Ascending("Logins.LoginProvider") - .Ascending("Logins.ProviderKey")), - new CreateIndexModel<MongoUser>( - Index - .Ascending(x => x.NormalizedUserName), - new CreateIndexOptions - { - Unique = true - }), - new CreateIndexModel<MongoUser>( - Index - .Ascending(x => x.NormalizedEmail), - new CreateIndexOptions - { - Unique = true - }) - }, ct); - } + cm.MapMember(x => x.Type); + cm.MapMember(x => x.Value); + }); - protected override MongoCollectionSettings CollectionSettings() + BsonClassMap.RegisterClassMap<UserLogin>(cm => { - return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; - } + cm.MapConstructor(typeof(UserLogin).GetConstructors() + .First(x => + { + var parameters = x.GetParameters(); - public void Dispose() - { - } + return parameters.Length == 3; + })) + .SetArguments(new[] + { + nameof(UserLogin.LoginProvider), + nameof(UserLogin.ProviderKey), + nameof(UserLogin.ProviderDisplayName) + }); - public IQueryable<IdentityUser> Users - { - get => Collection.AsQueryable(); - } + cm.AutoMap(); + }); - public bool IsId(string id) + BsonClassMap.RegisterClassMap<IdentityUserToken<string>>(cm => { - return ObjectId.TryParse(id, out _); - } + cm.AutoMap(); - public IdentityUser Create(string email) - { - return new MongoUser { Email = email, UserName = email }; - } + cm.UnmapMember(x => x.UserId); + }); - public async Task<IdentityUser> FindByIdAsync(string userId, - CancellationToken cancellationToken) + BsonClassMap.RegisterClassMap<IdentityUser<string>>(cm => { - if (!IsId(userId)) - { - return null!; - } + cm.AutoMap(); - return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); - } + cm.MapMember(x => x.Id) + .SetSerializer(new StringSerializer(BsonType.ObjectId)); - public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail, - CancellationToken cancellationToken) - { - var result = await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); + cm.MapMember(x => x.AccessFailedCount) + .SetIgnoreIfDefault(true); - return result; - } + cm.MapMember(x => x.EmailConfirmed) + .SetIgnoreIfDefault(true); - public async Task<IdentityUser> FindByNameAsync(string normalizedUserName, - CancellationToken cancellationToken) - { - var result = await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); + cm.MapMember(x => x.LockoutEnd) + .SetElementName("LockoutEndDateUtc").SetIgnoreIfNull(true); - return result; - } + cm.MapMember(x => x.LockoutEnabled) + .SetIgnoreIfDefault(true); - public async Task<IdentityUser> FindByLoginAsync(string loginProvider, string providerKey, - CancellationToken cancellationToken) - { - var result = await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); + cm.MapMember(x => x.PasswordHash) + .SetIgnoreIfNull(true); - return result; - } + cm.MapMember(x => x.PhoneNumber) + .SetIgnoreIfNull(true); - public async Task<IList<IdentityUser>> GetUsersForClaimAsync(Claim claim, - CancellationToken cancellationToken) - { - var result = await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken); + cm.MapMember(x => x.PhoneNumberConfirmed) + .SetIgnoreIfDefault(true); - return result.OfType<IdentityUser>().ToList(); - } + cm.MapMember(x => x.SecurityStamp) + .SetIgnoreIfNull(true); - public async Task<IList<IdentityUser>> GetUsersInRoleAsync(string roleName, - CancellationToken cancellationToken) - { - var result = await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken); + cm.MapMember(x => x.TwoFactorEnabled) + .SetIgnoreIfDefault(true); + }); + } - return result.OfType<IdentityUser>().ToList(); - } + public MongoUserStore(IMongoDatabase database) + : base(database) + { + } - public async Task<IdentityResult> CreateAsync(IdentityUser user, - CancellationToken cancellationToken) - { - user.Id = ObjectId.GenerateNewId().ToString(); + protected override string CollectionName() + { + return "Identity_Users"; + } - await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); + protected override Task SetupCollectionAsync(IMongoCollection<MongoUser> collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel<MongoUser>( + Index + .Ascending("Logins.LoginProvider") + .Ascending("Logins.ProviderKey")), + new CreateIndexModel<MongoUser>( + Index + .Ascending(x => x.NormalizedUserName), + new CreateIndexOptions + { + Unique = true + }), + new CreateIndexModel<MongoUser>( + Index + .Ascending(x => x.NormalizedEmail), + new CreateIndexOptions + { + Unique = true + }) + }, ct); + } - return IdentityResult.Success; - } + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; + } - public async Task<IdentityResult> UpdateAsync(IdentityUser user, - CancellationToken cancellationToken) - { - await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, cancellationToken: cancellationToken); + public void Dispose() + { + } - return IdentityResult.Success; - } + public IQueryable<IdentityUser> Users + { + get => Collection.AsQueryable(); + } - public async Task<IdentityResult> DeleteAsync(IdentityUser user, - CancellationToken cancellationToken) - { - await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); + public bool IsId(string id) + { + return ObjectId.TryParse(id, out _); + } - return IdentityResult.Success; - } + public IdentityUser Create(string email) + { + return new MongoUser { Email = email, UserName = email }; + } - public Task<string> GetUserIdAsync(IdentityUser user, - CancellationToken cancellationToken) + public async Task<IdentityUser> FindByIdAsync(string userId, + CancellationToken cancellationToken) + { + if (!IsId(userId)) { - var result = user.Id; - - return Task.FromResult(result); + return null!; } - public Task<string> GetUserNameAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.UserName; + return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); + } - return Task.FromResult(result); - } + public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail, + CancellationToken cancellationToken) + { + var result = await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); - public Task<string> GetNormalizedUserNameAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.NormalizedUserName; + return result; + } - return Task.FromResult(result); - } + public async Task<IdentityUser> FindByNameAsync(string normalizedUserName, + CancellationToken cancellationToken) + { + var result = await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); - public Task<string> GetPasswordHashAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.PasswordHash; + return result; + } - return Task.FromResult(result); - } + public async Task<IdentityUser> FindByLoginAsync(string loginProvider, string providerKey, + CancellationToken cancellationToken) + { + var result = await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); - public Task<IList<string>> GetRolesAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = ((MongoUser)user).Roles.ToList(); + return result; + } - return Task.FromResult<IList<string>>(result); - } + public async Task<IList<IdentityUser>> GetUsersForClaimAsync(Claim claim, + CancellationToken cancellationToken) + { + var result = await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken); - public Task<bool> IsInRoleAsync(IdentityUser user, string roleName, - CancellationToken cancellationToken) - { - var result = ((MongoUser)user).Roles.Contains(roleName); + return result.OfType<IdentityUser>().ToList(); + } - return Task.FromResult(result); - } + public async Task<IList<IdentityUser>> GetUsersInRoleAsync(string roleName, + CancellationToken cancellationToken) + { + var result = await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken); - public Task<IList<UserLoginInfo>> GetLoginsAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = ((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); + return result.OfType<IdentityUser>().ToList(); + } - return Task.FromResult<IList<UserLoginInfo>>(result); - } + public async Task<IdentityResult> CreateAsync(IdentityUser user, + CancellationToken cancellationToken) + { + user.Id = ObjectId.GenerateNewId().ToString(); - public Task<string> GetSecurityStampAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.SecurityStamp; + await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); - return Task.FromResult(result); - } + return IdentityResult.Success; + } - public Task<string> GetEmailAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.Email; + public async Task<IdentityResult> UpdateAsync(IdentityUser user, + CancellationToken cancellationToken) + { + await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, cancellationToken: cancellationToken); - return Task.FromResult(result); - } + return IdentityResult.Success; + } - public Task<bool> GetEmailConfirmedAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.EmailConfirmed; + public async Task<IdentityResult> DeleteAsync(IdentityUser user, + CancellationToken cancellationToken) + { + await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); - return Task.FromResult(result); - } + return IdentityResult.Success; + } - public Task<string> GetNormalizedEmailAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.NormalizedEmail; + public Task<string> GetUserIdAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.Id; - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task<IList<Claim>> GetClaimsAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = ((MongoUser)user).Claims; + public Task<string> GetUserNameAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.UserName; - return Task.FromResult<IList<Claim>>(result); - } + return Task.FromResult(result); + } - public Task<string> GetPhoneNumberAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.PhoneNumber; + public Task<string> GetNormalizedUserNameAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.NormalizedUserName; - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task<bool> GetPhoneNumberConfirmedAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.PhoneNumberConfirmed; + public Task<string> GetPasswordHashAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.PasswordHash; - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task<bool> GetTwoFactorEnabledAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.TwoFactorEnabled; + public Task<IList<string>> GetRolesAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = ((MongoUser)user).Roles.ToList(); - return Task.FromResult(result); - } + return Task.FromResult<IList<string>>(result); + } - public Task<DateTimeOffset?> GetLockoutEndDateAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.LockoutEnd; + public Task<bool> IsInRoleAsync(IdentityUser user, string roleName, + CancellationToken cancellationToken) + { + var result = ((MongoUser)user).Roles.Contains(roleName); - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task<int> GetAccessFailedCountAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.AccessFailedCount; + public Task<IList<UserLoginInfo>> GetLoginsAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = ((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); - return Task.FromResult(result); - } + return Task.FromResult<IList<UserLoginInfo>>(result); + } - public Task<bool> GetLockoutEnabledAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = user.LockoutEnabled; + public Task<string> GetSecurityStampAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.SecurityStamp; - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task<string> GetTokenAsync(IdentityUser user, string loginProvider, string name, - CancellationToken cancellationToken) - { - var result = ((MongoUser)user).GetToken(loginProvider, name)!; + public Task<string> GetEmailAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.Email; - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task<string> GetAuthenticatorKeyAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = ((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!; + public Task<bool> GetEmailConfirmedAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.EmailConfirmed; - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task<bool> HasPasswordAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = !string.IsNullOrWhiteSpace(user.PasswordHash); + public Task<string> GetNormalizedEmailAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.NormalizedEmail; - return Task.FromResult(result); - } + return Task.FromResult(result); + } - public Task<int> CountCodesAsync(IdentityUser user, - CancellationToken cancellationToken) - { - var result = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0; + public Task<IList<Claim>> GetClaimsAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = ((MongoUser)user).Claims; - return Task.FromResult(result); - } + return Task.FromResult<IList<Claim>>(result); + } - public Task SetUserNameAsync(IdentityUser user, string userName, - CancellationToken cancellationToken) - { - ((MongoUser)user).UserName = userName; + public Task<string> GetPhoneNumberAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.PhoneNumber; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, - CancellationToken cancellationToken) - { - ((MongoUser)user).NormalizedUserName = normalizedName; + public Task<bool> GetPhoneNumberConfirmedAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.PhoneNumberConfirmed; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, - CancellationToken cancellationToken) - { - ((MongoUser)user).PasswordHash = passwordHash; + public Task<bool> GetTwoFactorEnabledAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.TwoFactorEnabled; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task AddToRoleAsync(IdentityUser user, string roleName, - CancellationToken cancellationToken) - { - ((MongoUser)user).AddRole(roleName); + public Task<DateTimeOffset?> GetLockoutEndDateAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.LockoutEnd; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task RemoveFromRoleAsync(IdentityUser user, string roleName, - CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveRole(roleName); + public Task<int> GetAccessFailedCountAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.AccessFailedCount; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, - CancellationToken cancellationToken) - { - ((MongoUser)user).AddLogin(login); + public Task<bool> GetLockoutEnabledAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = user.LockoutEnabled; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, - CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveLogin(loginProvider, providerKey); + public Task<string> GetTokenAsync(IdentityUser user, string loginProvider, string name, + CancellationToken cancellationToken) + { + var result = ((MongoUser)user).GetToken(loginProvider, name)!; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task SetSecurityStampAsync(IdentityUser user, string stamp, - CancellationToken cancellationToken) - { - ((MongoUser)user).SecurityStamp = stamp; + public Task<string> GetAuthenticatorKeyAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = ((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task SetEmailAsync(IdentityUser user, string email, - CancellationToken cancellationToken) - { - ((MongoUser)user).Email = email; + public Task<bool> HasPasswordAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = !string.IsNullOrWhiteSpace(user.PasswordHash); - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, - CancellationToken cancellationToken) - { - ((MongoUser)user).EmailConfirmed = confirmed; + public Task<int> CountCodesAsync(IdentityUser user, + CancellationToken cancellationToken) + { + var result = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0; - return Task.CompletedTask; - } + return Task.FromResult(result); + } - public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, - CancellationToken cancellationToken) - { - ((MongoUser)user).NormalizedEmail = normalizedEmail; + public Task SetUserNameAsync(IdentityUser user, string userName, + CancellationToken cancellationToken) + { + ((MongoUser)user).UserName = userName; - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task AddClaimsAsync(IdentityUser user, IEnumerable<Claim> claims, - CancellationToken cancellationToken) - { - ((MongoUser)user).AddClaims(claims); + public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, + CancellationToken cancellationToken) + { + ((MongoUser)user).NormalizedUserName = normalizedName; - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, - CancellationToken cancellationToken) - { - ((MongoUser)user).ReplaceClaim(claim, newClaim); + public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, + CancellationToken cancellationToken) + { + ((MongoUser)user).PasswordHash = passwordHash; - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task RemoveClaimsAsync(IdentityUser user, IEnumerable<Claim> claims, - CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveClaims(claims); + public Task AddToRoleAsync(IdentityUser user, string roleName, + CancellationToken cancellationToken) + { + ((MongoUser)user).AddRole(roleName); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, - CancellationToken cancellationToken) - { - ((MongoUser)user).PhoneNumber = phoneNumber; + public Task RemoveFromRoleAsync(IdentityUser user, string roleName, + CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveRole(roleName); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, - CancellationToken cancellationToken) - { - ((MongoUser)user).PhoneNumberConfirmed = confirmed; + public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, + CancellationToken cancellationToken) + { + ((MongoUser)user).AddLogin(login); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, - CancellationToken cancellationToken) - { - ((MongoUser)user).TwoFactorEnabled = enabled; + public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, + CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveLogin(loginProvider, providerKey); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, - CancellationToken cancellationToken) - { - ((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime; + public Task SetSecurityStampAsync(IdentityUser user, string stamp, + CancellationToken cancellationToken) + { + ((MongoUser)user).SecurityStamp = stamp; - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task<int> IncrementAccessFailedCountAsync(IdentityUser user, - CancellationToken cancellationToken) - { - ((MongoUser)user).AccessFailedCount++; + public Task SetEmailAsync(IdentityUser user, string email, + CancellationToken cancellationToken) + { + ((MongoUser)user).Email = email; - return Task.FromResult(((MongoUser)user).AccessFailedCount); - } + return Task.CompletedTask; + } - public Task ResetAccessFailedCountAsync(IdentityUser user, - CancellationToken cancellationToken) - { - ((MongoUser)user).AccessFailedCount = 0; + public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, + CancellationToken cancellationToken) + { + ((MongoUser)user).EmailConfirmed = confirmed; - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, - CancellationToken cancellationToken) - { - ((MongoUser)user).LockoutEnabled = enabled; + public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, + CancellationToken cancellationToken) + { + ((MongoUser)user).NormalizedEmail = normalizedEmail; - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, - CancellationToken cancellationToken) - { - ((MongoUser)user).ReplaceToken(loginProvider, name, value); + public Task AddClaimsAsync(IdentityUser user, IEnumerable<Claim> claims, + CancellationToken cancellationToken) + { + ((MongoUser)user).AddClaims(claims); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, - CancellationToken cancellationToken) - { - ((MongoUser)user).RemoveToken(loginProvider, name); + public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, + CancellationToken cancellationToken) + { + ((MongoUser)user).ReplaceClaim(claim, newClaim); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, - CancellationToken cancellationToken) - { - ((MongoUser)user).ReplaceToken(InternalLoginProvider, AuthenticatorKeyTokenName, key); + public Task RemoveClaimsAsync(IdentityUser user, IEnumerable<Claim> claims, + CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveClaims(claims); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task ReplaceCodesAsync(IdentityUser user, IEnumerable<string> recoveryCodes, - CancellationToken cancellationToken) - { - ((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes)); + public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, + CancellationToken cancellationToken) + { + ((MongoUser)user).PhoneNumber = phoneNumber; - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task<bool> RedeemCodeAsync(IdentityUser user, string code, - CancellationToken cancellationToken) - { - var mergedCodes = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName) ?? string.Empty; + public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, + CancellationToken cancellationToken) + { + ((MongoUser)user).PhoneNumberConfirmed = confirmed; + + return Task.CompletedTask; + } + + public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, + CancellationToken cancellationToken) + { + ((MongoUser)user).TwoFactorEnabled = enabled; - var splitCodes = mergedCodes.Split(';'); - if (splitCodes.Contains(code)) - { - var updatedCodes = new List<string>(splitCodes.Where(s => s != code)); + return Task.CompletedTask; + } - ((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", updatedCodes)); + public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, + CancellationToken cancellationToken) + { + ((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime; - return Task.FromResult(true); - } + return Task.CompletedTask; + } - return Task.FromResult(false); + public Task<int> IncrementAccessFailedCountAsync(IdentityUser user, + CancellationToken cancellationToken) + { + ((MongoUser)user).AccessFailedCount++; + + return Task.FromResult(((MongoUser)user).AccessFailedCount); + } + + public Task ResetAccessFailedCountAsync(IdentityUser user, + CancellationToken cancellationToken) + { + ((MongoUser)user).AccessFailedCount = 0; + + return Task.CompletedTask; + } + + public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, + CancellationToken cancellationToken) + { + ((MongoUser)user).LockoutEnabled = enabled; + + return Task.CompletedTask; + } + + public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, + CancellationToken cancellationToken) + { + ((MongoUser)user).ReplaceToken(loginProvider, name, value); + + return Task.CompletedTask; + } + + public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, + CancellationToken cancellationToken) + { + ((MongoUser)user).RemoveToken(loginProvider, name); + + return Task.CompletedTask; + } + + public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, + CancellationToken cancellationToken) + { + ((MongoUser)user).ReplaceToken(InternalLoginProvider, AuthenticatorKeyTokenName, key); + + return Task.CompletedTask; + } + + public Task ReplaceCodesAsync(IdentityUser user, IEnumerable<string> recoveryCodes, + CancellationToken cancellationToken) + { + ((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes)); + + return Task.CompletedTask; + } + + public Task<bool> RedeemCodeAsync(IdentityUser user, string code, + CancellationToken cancellationToken) + { + var mergedCodes = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName) ?? string.Empty; + + var splitCodes = mergedCodes.Split(';'); + if (splitCodes.Contains(code)) + { + var updatedCodes = new List<string>(splitCodes.Where(s => s != code)); + + ((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", updatedCodes)); + + return Task.FromResult(true); } + + return Task.FromResult(false); } } diff --git a/backend/src/Squidex.Domain.Users/DefaultKeyStore.cs b/backend/src/Squidex.Domain.Users/DefaultKeyStore.cs index b2fb0e4f16..4d7c442b39 100644 --- a/backend/src/Squidex.Domain.Users/DefaultKeyStore.cs +++ b/backend/src/Squidex.Domain.Users/DefaultKeyStore.cs @@ -13,90 +13,89 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public sealed class DefaultKeyStore : IConfigureOptions<OpenIddictServerOptions> { - public sealed class DefaultKeyStore : IConfigureOptions<OpenIddictServerOptions> + private readonly ISnapshotStore<State> store; + + [CollectionName("Identity_Keys")] + public sealed class State { - private readonly ISnapshotStore<State> store; + public string Key { get; set; } - [CollectionName("Identity_Keys")] - public sealed class State - { - public string Key { get; set; } + public RSAParameters Parameters { get; set; } + } - public RSAParameters Parameters { get; set; } - } + public DefaultKeyStore(ISnapshotStore<State> store) + { + this.store = store; + } - public DefaultKeyStore(ISnapshotStore<State> store) - { - this.store = store; - } + public void Configure(OpenIddictServerOptions options) + { + var securityKey = GetOrCreateKeyAsync().Result; - public void Configure(OpenIddictServerOptions options) - { - var securityKey = GetOrCreateKeyAsync().Result; + options.SigningCredentials.Add( + new SigningCredentials(securityKey, + SecurityAlgorithms.RsaSha256)); - options.SigningCredentials.Add( - new SigningCredentials(securityKey, - SecurityAlgorithms.RsaSha256)); + options.EncryptionCredentials.Add(new EncryptingCredentials(securityKey, + SecurityAlgorithms.RsaOAEP, + SecurityAlgorithms.Aes256CbcHmacSha512)); + } - options.EncryptionCredentials.Add(new EncryptingCredentials(securityKey, - SecurityAlgorithms.RsaOAEP, - SecurityAlgorithms.Aes256CbcHmacSha512)); - } + private async Task<RsaSecurityKey> GetOrCreateKeyAsync() + { + var (_, state, _, _) = await store.ReadAsync(default); + + RsaSecurityKey securityKey; - private async Task<RsaSecurityKey> GetOrCreateKeyAsync() + var attempts = 0; + + while (state == null && attempts < 10) { - var (_, state, _, _) = await store.ReadAsync(default); + securityKey = new RsaSecurityKey(RSA.Create(2048)) + { + KeyId = CryptoRandom.CreateUniqueId(16) + }; - RsaSecurityKey securityKey; + state = new State { Key = securityKey.KeyId }; - var attempts = 0; + if (securityKey.Rsa != null) + { + var parameters = securityKey.Rsa.ExportParameters(true); - while (state == null && attempts < 10) + state.Parameters = parameters; + } + else { - securityKey = new RsaSecurityKey(RSA.Create(2048)) - { - KeyId = CryptoRandom.CreateUniqueId(16) - }; - - state = new State { Key = securityKey.KeyId }; - - if (securityKey.Rsa != null) - { - var parameters = securityKey.Rsa.ExportParameters(true); - - state.Parameters = parameters; - } - else - { - state.Parameters = securityKey.Parameters; - } - - try - { - await store.WriteAsync(new SnapshotWriteJob<State>(default, state, 0)); - - return securityKey; - } - catch (InconsistentStateException) - { - (_, state, _, _) = await store.ReadAsync(default); - } + state.Parameters = securityKey.Parameters; } - if (state == null) + try { - ThrowHelper.InvalidOperationException("Cannot read key."); - return default!; - } + await store.WriteAsync(new SnapshotWriteJob<State>(default, state, 0)); - securityKey = new RsaSecurityKey(state.Parameters) + return securityKey; + } + catch (InconsistentStateException) { - KeyId = state.Key - }; + (_, state, _, _) = await store.ReadAsync(default); + } + } - return securityKey; + if (state == null) + { + ThrowHelper.InvalidOperationException("Cannot read key."); + return default!; } + + securityKey = new RsaSecurityKey(state.Parameters) + { + KeyId = state.Key + }; + + return securityKey; } } diff --git a/backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs b/backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs index 8cd1f87743..e009a83290 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs @@ -7,36 +7,35 @@ using Squidex.Assets; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public sealed class DefaultUserPictureStore : IUserPictureStore { - public sealed class DefaultUserPictureStore : IUserPictureStore - { - private readonly IAssetStore assetStore; + private readonly IAssetStore assetStore; - public DefaultUserPictureStore(IAssetStore assetStore) - { - this.assetStore = assetStore; - } + public DefaultUserPictureStore(IAssetStore assetStore) + { + this.assetStore = assetStore; + } - public Task UploadAsync(string userId, Stream stream, - CancellationToken ct = default) - { - var fileName = GetFileName(userId); + public Task UploadAsync(string userId, Stream stream, + CancellationToken ct = default) + { + var fileName = GetFileName(userId); - return assetStore.UploadAsync(fileName, stream, true, ct); - } + return assetStore.UploadAsync(fileName, stream, true, ct); + } - public Task DownloadAsync(string userId, Stream stream, - CancellationToken ct = default) - { - var fileName = GetFileName(userId); + public Task DownloadAsync(string userId, Stream stream, + CancellationToken ct = default) + { + var fileName = GetFileName(userId); - return assetStore.DownloadAsync(fileName, stream, default, ct); - } + return assetStore.DownloadAsync(fileName, stream, default, ct); + } - private static string GetFileName(string userId) - { - return $"{userId}_0_picture"; - } + private static string GetFileName(string userId) + { + return $"{userId}_0_picture"; } } diff --git a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs index 55edc96a48..57d065afa3 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs @@ -13,142 +13,141 @@ #pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public sealed class DefaultUserResolver : IUserResolver { - public sealed class DefaultUserResolver : IUserResolver + private readonly IServiceProvider serviceProvider; + + public DefaultUserResolver(IServiceProvider serviceProvider) { - private readonly IServiceProvider serviceProvider; + this.serviceProvider = serviceProvider; + } - public DefaultUserResolver(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } + public async Task<(IUser? User, bool Created)> CreateUserIfNotExistsAsync(string email, bool invited = false, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(email); - public async Task<(IUser? User, bool Created)> CreateUserIfNotExistsAsync(string email, bool invited = false, - CancellationToken ct = default) + await using (var scope = serviceProvider.CreateAsyncScope()) { - Guard.NotNullOrEmpty(email); + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - await using (var scope = serviceProvider.CreateAsyncScope()) + try { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - - try + var values = new UserValues { - var values = new UserValues - { - Invited = invited - }; + Invited = invited + }; - var user = await userService.CreateAsync(email, values, ct: ct); + var user = await userService.CreateAsync(email, values, ct: ct); - return (user, true); - } - catch - { - } + return (user, true); + } + catch + { + } - var found = await FindByIdOrEmailAsync(email, ct); + var found = await FindByIdOrEmailAsync(email, ct); - return (found, false); - } + return (found, false); } + } - public async Task SetClaimAsync(string id, string type, string value, bool silent = false, - CancellationToken ct = default) + public async Task SetClaimAsync(string id, string type, string value, bool silent = false, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); + Guard.NotNullOrEmpty(type); + Guard.NotNullOrEmpty(value); + + await using (var scope = serviceProvider.CreateAsyncScope()) { - Guard.NotNullOrEmpty(id); - Guard.NotNullOrEmpty(type); - Guard.NotNullOrEmpty(value); + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - await using (var scope = serviceProvider.CreateAsyncScope()) + var values = new UserValues { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - - var values = new UserValues + CustomClaims = new List<Claim> { - CustomClaims = new List<Claim> - { - new Claim(type, value) - } - }; + new Claim(type, value) + } + }; - await userService.UpdateAsync(id, values, silent, ct); - } + await userService.UpdateAsync(id, values, silent, ct); } + } - public async Task<IUser?> FindByIdAsync(string id, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(id); + public async Task<IUser?> FindByIdAsync(string id, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); - await using (var scope = serviceProvider.CreateAsyncScope()) - { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); + await using (var scope = serviceProvider.CreateAsyncScope()) + { + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - return await userService.FindByIdAsync(id, ct); - } + return await userService.FindByIdAsync(id, ct); } + } - public async Task<IUser?> FindByIdOrEmailAsync(string idOrEmail, - CancellationToken ct = default) + public async Task<IUser?> FindByIdOrEmailAsync(string idOrEmail, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(idOrEmail); + + await using (var scope = serviceProvider.CreateAsyncScope()) { - Guard.NotNullOrEmpty(idOrEmail); + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - await using (var scope = serviceProvider.CreateAsyncScope()) + if (idOrEmail.Contains('@', StringComparison.Ordinal)) { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - - if (idOrEmail.Contains('@', StringComparison.Ordinal)) - { - return await userService.FindByEmailAsync(idOrEmail, ct); - } - else - { - return await userService.FindByIdAsync(idOrEmail, ct); - } + return await userService.FindByEmailAsync(idOrEmail, ct); + } + else + { + return await userService.FindByIdAsync(idOrEmail, ct); } } + } - public async Task<List<IUser>> QueryAllAsync( - CancellationToken ct = default) + public async Task<List<IUser>> QueryAllAsync( + CancellationToken ct = default) + { + await using (var scope = serviceProvider.CreateAsyncScope()) { - await using (var scope = serviceProvider.CreateAsyncScope()) - { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - var result = await userService.QueryAsync(take: int.MaxValue, ct: ct); + var result = await userService.QueryAsync(take: int.MaxValue, ct: ct); - return result.ToList(); - } + return result.ToList(); } + } - public async Task<List<IUser>> QueryByEmailAsync(string email, - CancellationToken ct = default) + public async Task<List<IUser>> QueryByEmailAsync(string email, + CancellationToken ct = default) + { + await using (var scope = serviceProvider.CreateAsyncScope()) { - await using (var scope = serviceProvider.CreateAsyncScope()) - { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - var result = await userService.QueryAsync(email, ct: ct); + var result = await userService.QueryAsync(email, ct: ct); - return result.Where(x => !x.Claims.IsHidden()).ToList(); - } + return result.Where(x => !x.Claims.IsHidden()).ToList(); } + } - public async Task<Dictionary<string, IUser>> QueryManyAsync(string[] ids, - CancellationToken ct = default) - { - Guard.NotNull(ids); + public async Task<Dictionary<string, IUser>> QueryManyAsync(string[] ids, + CancellationToken ct = default) + { + Guard.NotNull(ids); - await using (var scope = serviceProvider.CreateAsyncScope()) - { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); + await using (var scope = serviceProvider.CreateAsyncScope()) + { + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - var result = await userService.QueryAsync(ids, ct); + var result = await userService.QueryAsync(ids, ct); - return result.OfType<IUser>().ToDictionary(x => x.Id); - } + return result.OfType<IUser>().ToDictionary(x => x.Id); } } } diff --git a/backend/src/Squidex.Domain.Users/DefaultUserService.cs b/backend/src/Squidex.Domain.Users/DefaultUserService.cs index d05071bcfe..10f4afe8ea 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserService.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserService.cs @@ -14,408 +14,407 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public sealed class DefaultUserService : IUserService { - public sealed class DefaultUserService : IUserService + private readonly UserManager<IdentityUser> userManager; + private readonly IUserFactory userFactory; + private readonly IEnumerable<IUserEvents> userEvents; + private readonly ILogger<DefaultUserService> log; + + public DefaultUserService(UserManager<IdentityUser> userManager, IUserFactory userFactory, + IEnumerable<IUserEvents> userEvents, ILogger<DefaultUserService> log) { - private readonly UserManager<IdentityUser> userManager; - private readonly IUserFactory userFactory; - private readonly IEnumerable<IUserEvents> userEvents; - private readonly ILogger<DefaultUserService> log; + this.userManager = userManager; + this.userFactory = userFactory; + this.userEvents = userEvents; - public DefaultUserService(UserManager<IdentityUser> userManager, IUserFactory userFactory, - IEnumerable<IUserEvents> userEvents, ILogger<DefaultUserService> log) - { - this.userManager = userManager; - this.userFactory = userFactory; - this.userEvents = userEvents; + this.log = log; + } - this.log = log; - } + public async Task<bool> IsEmptyAsync( + CancellationToken ct = default) + { + var result = await QueryAsync(null, 1, 0, ct); - public async Task<bool> IsEmptyAsync( - CancellationToken ct = default) - { - var result = await QueryAsync(null, 1, 0, ct); + return result.Total == 0; + } - return result.Total == 0; - } + public string GetUserId(ClaimsPrincipal user, + CancellationToken ct = default) + { + Guard.NotNull(user); - public string GetUserId(ClaimsPrincipal user, - CancellationToken ct = default) - { - Guard.NotNull(user); + return userManager.GetUserId(user); + } - return userManager.GetUserId(user); - } + public async Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids, + CancellationToken ct = default) + { + Guard.NotNull(ids); - public async Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids, - CancellationToken ct = default) - { - Guard.NotNull(ids); + ids = ids.Where(userFactory.IsId); - ids = ids.Where(userFactory.IsId); + if (!ids.Any()) + { + return ResultList.Empty<IUser>(); + } - if (!ids.Any()) - { - return ResultList.Empty<IUser>(); - } + var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); - var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); + var resolved = await ResolveAsync(users); - var resolved = await ResolveAsync(users); + return ResultList.Create(users.Count, resolved); + } - return ResultList.Create(users.Count, resolved); - } + public async Task<IResultList<IUser>> QueryAsync(string? query = null, int take = 10, int skip = 0, + CancellationToken ct = default) + { + Guard.GreaterThan(take, 0); + Guard.GreaterEquals(skip, 0); - public async Task<IResultList<IUser>> QueryAsync(string? query = null, int take = 10, int skip = 0, - CancellationToken ct = default) + IQueryable<IdentityUser> QueryUsers(string? email = null) { - Guard.GreaterThan(take, 0); - Guard.GreaterEquals(skip, 0); + var result = userManager.Users; - IQueryable<IdentityUser> QueryUsers(string? email = null) + if (!string.IsNullOrWhiteSpace(email)) { - var result = userManager.Users; - - if (!string.IsNullOrWhiteSpace(email)) - { - var normalizedEmail = userManager.NormalizeEmail(email); - - result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); - } + var normalizedEmail = userManager.NormalizeEmail(email); - return result; + result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); } - var userItems = QueryUsers(query).Skip(skip).Take(take).ToList(); - var userTotal = QueryUsers(query).LongCount(); - - var resolved = await ResolveAsync(userItems); - - return ResultList.Create(userTotal, resolved); + return result; } - public Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user, - CancellationToken ct = default) - { - Guard.NotNull(user); + var userItems = QueryUsers(query).Skip(skip).Take(take).ToList(); + var userTotal = QueryUsers(query).LongCount(); - return userManager.GetLoginsAsync((IdentityUser)user.Identity); - } + var resolved = await ResolveAsync(userItems); - public Task<bool> HasPasswordAsync(IUser user, - CancellationToken ct = default) - { - Guard.NotNull(user); + return ResultList.Create(userTotal, resolved); + } - return userManager.HasPasswordAsync((IdentityUser)user.Identity); - } + public Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user, + CancellationToken ct = default) + { + Guard.NotNull(user); - public async Task<IUser?> FindByLoginAsync(string provider, string key, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(provider); + return userManager.GetLoginsAsync((IdentityUser)user.Identity); + } - var user = await userManager.FindByLoginAsync(provider, key); + public Task<bool> HasPasswordAsync(IUser user, + CancellationToken ct = default) + { + Guard.NotNull(user); - return await ResolveOptionalAsync(user); - } + return userManager.HasPasswordAsync((IdentityUser)user.Identity); + } - public async Task<IUser?> FindByEmailAsync(string email, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(email); + public async Task<IUser?> FindByLoginAsync(string provider, string key, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(provider); - var user = await userManager.FindByEmailAsync(email); + var user = await userManager.FindByLoginAsync(provider, key); - return await ResolveOptionalAsync(user); - } + return await ResolveOptionalAsync(user); + } - public async Task<IUser?> GetAsync(ClaimsPrincipal principal, - CancellationToken ct = default) - { - Guard.NotNull(principal); + public async Task<IUser?> FindByEmailAsync(string email, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(email); - var user = await userManager.GetUserAsync(principal); + var user = await userManager.FindByEmailAsync(email); - return await ResolveOptionalAsync(user); - } + return await ResolveOptionalAsync(user); + } - public async Task<IUser?> FindByIdAsync(string id, - CancellationToken ct = default) - { - if (!userFactory.IsId(id)) - { - return null; - } + public async Task<IUser?> GetAsync(ClaimsPrincipal principal, + CancellationToken ct = default) + { + Guard.NotNull(principal); - var user = await userManager.FindByIdAsync(id); + var user = await userManager.GetUserAsync(principal); - return await ResolveOptionalAsync(user); - } + return await ResolveOptionalAsync(user); + } - public async Task<IUser> CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false, - CancellationToken ct = default) + public async Task<IUser?> FindByIdAsync(string id, + CancellationToken ct = default) + { + if (!userFactory.IsId(id)) { - Guard.NotNullOrEmpty(email); - - var isFirst = !userManager.Users.Any(); - - var user = userFactory.Create(email); - - try - { - await userManager.CreateAsync(user).Throw(log); + return null; + } - values ??= new UserValues(); + var user = await userManager.FindByIdAsync(id); - if (string.IsNullOrWhiteSpace(values.DisplayName)) - { - values.DisplayName = email; - } + return await ResolveOptionalAsync(user); + } - if (isFirst) - { - var permissions = values.Permissions?.ToIds().ToList() ?? new List<string>(); + public async Task<IUser> CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(email); - permissions.Add(PermissionIds.Admin); + var isFirst = !userManager.Users.Any(); - values.Permissions = new PermissionSet(permissions); - } + var user = userFactory.Create(email); - await userManager.SyncClaims(user, values).Throw(log); + try + { + await userManager.CreateAsync(user).Throw(log); - if (!string.IsNullOrWhiteSpace(values.Password)) - { - await userManager.AddPasswordAsync(user, values.Password).Throw(log); - } + values ??= new UserValues(); - if (!isFirst && lockAutomatically) - { - await userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log); - } + if (string.IsNullOrWhiteSpace(values.DisplayName)) + { + values.DisplayName = email; } - catch (Exception) + + if (isFirst) { - try - { - if (userFactory.IsId(user.Id)) - { - await userManager.DeleteAsync(user); - } - } - catch (Exception ex2) - { - log.LogError(ex2, "Failed to cleanup user after creation failed."); - } + var permissions = values.Permissions?.ToIds().ToList() ?? new List<string>(); + + permissions.Add(PermissionIds.Admin); - throw; + values.Permissions = new PermissionSet(permissions); } - var resolved = await ResolveAsync(user); + await userManager.SyncClaims(user, values).Throw(log); - foreach (var events in userEvents) + if (!string.IsNullOrWhiteSpace(values.Password)) { - await events.OnUserRegisteredAsync(resolved); + await userManager.AddPasswordAsync(user, values.Password).Throw(log); } - if (HasConsentGiven(values, null!)) + if (!isFirst && lockAutomatically) { - foreach (var events in userEvents) + await userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log); + } + } + catch (Exception) + { + try + { + if (userFactory.IsId(user.Id)) { - await events.OnConsentGivenAsync(resolved); + await userManager.DeleteAsync(user); } } + catch (Exception ex2) + { + log.LogError(ex2, "Failed to cleanup user after creation failed."); + } - return resolved; + throw; } - public Task<IUser> SetPasswordAsync(string id, string password, string? oldPassword = null, - CancellationToken ct = default) + var resolved = await ResolveAsync(user); + + foreach (var events in userEvents) { - Guard.NotNullOrEmpty(id); + await events.OnUserRegisteredAsync(resolved); + } - return ForUserAsync(id, async user => + if (HasConsentGiven(values, null!)) + { + foreach (var events in userEvents) { - if (await userManager.HasPasswordAsync(user)) - { - await userManager.ChangePasswordAsync(user, oldPassword!, password).Throw(log); - } - else - { - await userManager.AddPasswordAsync(user, password).Throw(log); - } - }); + await events.OnConsentGivenAsync(resolved); + } } - public async Task<IUser> UpdateAsync(string id, UserValues values, bool silent = false, - CancellationToken ct = default) + return resolved; + } + + public Task<IUser> SetPasswordAsync(string id, string password, string? oldPassword = null, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); + + return ForUserAsync(id, async user => { - Guard.NotNullOrEmpty(id); - Guard.NotNull(values); + if (await userManager.HasPasswordAsync(user)) + { + await userManager.ChangePasswordAsync(user, oldPassword!, password).Throw(log); + } + else + { + await userManager.AddPasswordAsync(user, password).Throw(log); + } + }); + } - var user = await GetUserAsync(id); + public async Task<IUser> UpdateAsync(string id, UserValues values, bool silent = false, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); + Guard.NotNull(values); + + var user = await GetUserAsync(id); - var oldUser = await ResolveAsync(user); + var oldUser = await ResolveAsync(user); - if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) + if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) + { + await userManager.SetEmailAsync(user, values.Email).Throw(log); + await userManager.SetUserNameAsync(user, values.Email).Throw(log); + } + + await userManager.SyncClaims(user, values).Throw(log); + + if (!string.IsNullOrWhiteSpace(values.Password)) + { + if (await userManager.HasPasswordAsync(user)) { - await userManager.SetEmailAsync(user, values.Email).Throw(log); - await userManager.SetUserNameAsync(user, values.Email).Throw(log); + await userManager.RemovePasswordAsync(user).Throw(log); } - await userManager.SyncClaims(user, values).Throw(log); + await userManager.AddPasswordAsync(user, values.Password).Throw(log); + } - if (!string.IsNullOrWhiteSpace(values.Password)) - { - if (await userManager.HasPasswordAsync(user)) - { - await userManager.RemovePasswordAsync(user).Throw(log); - } + var resolved = await ResolveAsync(user); - await userManager.AddPasswordAsync(user, values.Password).Throw(log); + if (!silent) + { + foreach (var events in userEvents) + { + await events.OnUserUpdatedAsync(resolved, oldUser); } - var resolved = await ResolveAsync(user); - - if (!silent) + if (HasConsentGiven(values, oldUser)) { foreach (var events in userEvents) { - await events.OnUserUpdatedAsync(resolved, oldUser); - } - - if (HasConsentGiven(values, oldUser)) - { - foreach (var events in userEvents) - { - await events.OnConsentGivenAsync(resolved); - } + await events.OnConsentGivenAsync(resolved); } } - - return resolved; } - public Task<IUser> LockAsync(string id, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(id); + return resolved; + } - return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log)); - } + public Task<IUser> LockAsync(string id, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); - public Task<IUser> UnlockAsync(string id, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(id); + return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log)); + } - return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, null).Throw(log)); - } + public Task<IUser> UnlockAsync(string id, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); - public Task<IUser> AddLoginAsync(string id, ExternalLoginInfo externalLogin, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(id); + return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, null).Throw(log)); + } - return ForUserAsync(id, user => userManager.AddLoginAsync(user, externalLogin).Throw(log)); - } + public Task<IUser> AddLoginAsync(string id, ExternalLoginInfo externalLogin, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); - public Task<IUser> RemoveLoginAsync(string id, string loginProvider, string providerKey, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(id); + return ForUserAsync(id, user => userManager.AddLoginAsync(user, externalLogin).Throw(log)); + } - return ForUserAsync(id, user => userManager.RemoveLoginAsync(user, loginProvider, providerKey).Throw(log)); - } + public Task<IUser> RemoveLoginAsync(string id, string loginProvider, string providerKey, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); - public async Task DeleteAsync(string id, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(id); + return ForUserAsync(id, user => userManager.RemoveLoginAsync(user, loginProvider, providerKey).Throw(log)); + } - var user = await GetUserAsync(id); + public async Task DeleteAsync(string id, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id); - var resolved = await ResolveAsync(user); + var user = await GetUserAsync(id); - await userManager.DeleteAsync(user).Throw(log); + var resolved = await ResolveAsync(user); - foreach (var events in userEvents) - { - await events.OnUserDeletedAsync(resolved); - } - } + await userManager.DeleteAsync(user).Throw(log); - private async Task<IUser> ForUserAsync(string id, Func<IdentityUser, Task> action) + foreach (var events in userEvents) { - var user = await GetUserAsync(id); - - await action(user); - - return await ResolveAsync(user); + await events.OnUserDeletedAsync(resolved); } + } - private async Task<IdentityUser> GetUserAsync(string id) - { - if (!userFactory.IsId(id)) - { - throw new DomainObjectNotFoundException(id); - } + private async Task<IUser> ForUserAsync(string id, Func<IdentityUser, Task> action) + { + var user = await GetUserAsync(id); - var user = await userManager.FindByIdAsync(id); + await action(user); - if (user == null) - { - throw new DomainObjectNotFoundException(id); - } + return await ResolveAsync(user); + } - return user; + private async Task<IdentityUser> GetUserAsync(string id) + { + if (!userFactory.IsId(id)) + { + throw new DomainObjectNotFoundException(id); } - private Task<IUser[]> ResolveAsync(IEnumerable<IdentityUser> users) + var user = await userManager.FindByIdAsync(id); + + if (user == null) { - return Task.WhenAll(users.Select(async user => - { - return await ResolveAsync(user); - })); + throw new DomainObjectNotFoundException(id); } - private async Task<IUser> ResolveAsync(IdentityUser user) - { - var claims = await userManager.GetClaimsAsync(user); + return user; + } - if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase))) - { - claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email)); - } + private Task<IUser[]> ResolveAsync(IEnumerable<IdentityUser> users) + { + return Task.WhenAll(users.Select(async user => + { + return await ResolveAsync(user); + })); + } - return new UserWithClaims(user, claims.ToList()); - } + private async Task<IUser> ResolveAsync(IdentityUser user) + { + var claims = await userManager.GetClaimsAsync(user); - private async Task<IUser?> ResolveOptionalAsync(IdentityUser? user) + if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase))) { - if (user == null) - { - return null; - } - - return await ResolveAsync(user); + claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email)); } - private static bool HasConsentGiven(UserValues values, IUser? oldUser) - { - if (values.Consent == true && oldUser?.Claims.HasConsent() != true) - { - return true; - } + return new UserWithClaims(user, claims.ToList()); + } - return values.ConsentForEmails == true && oldUser?.Claims.HasConsentForEmails() != true; + private async Task<IUser?> ResolveOptionalAsync(IdentityUser? user) + { + if (user == null) + { + return null; } - private static DateTimeOffset LockoutDate() + return await ResolveAsync(user); + } + + private static bool HasConsentGiven(UserValues values, IUser? oldUser) + { + if (values.Consent == true && oldUser?.Claims.HasConsent() != true) { - return DateTimeOffset.UtcNow.AddYears(100); + return true; } + + return values.ConsentForEmails == true && oldUser?.Claims.HasConsentForEmails() != true; + } + + private static DateTimeOffset LockoutDate() + { + return DateTimeOffset.UtcNow.AddYears(100); } } diff --git a/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs index 52fec96049..607e938c0b 100644 --- a/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs +++ b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs @@ -10,52 +10,51 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.States; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public sealed class DefaultXmlRepository : IXmlRepository { - public sealed class DefaultXmlRepository : IXmlRepository + private readonly ISnapshotStore<State> store; + + [CollectionName("Identity_Xml")] + public sealed class State { - private readonly ISnapshotStore<State> store; + public string Xml { get; set; } - [CollectionName("Identity_Xml")] - public sealed class State + public State() { - public string Xml { get; set; } - - public State() - { - } - - public State(XElement xml) - { - Xml = xml.ToString(); - } - - public XElement ToXml() - { - return XElement.Parse(Xml); - } } - public DefaultXmlRepository(ISnapshotStore<State> store) + public State(XElement xml) { - this.store = store; + Xml = xml.ToString(); } - public IReadOnlyCollection<XElement> GetAllElements() + public XElement ToXml() { - return GetAllElementsAsync().Result; + return XElement.Parse(Xml); } + } - public void StoreElement(XElement element, string friendlyName) - { - var state = new State(element); + public DefaultXmlRepository(ISnapshotStore<State> store) + { + this.store = store; + } - store.WriteAsync(new SnapshotWriteJob<State>(DomainId.Create(friendlyName), state, 0)); - } + public IReadOnlyCollection<XElement> GetAllElements() + { + return GetAllElementsAsync().Result; + } - private async Task<IReadOnlyCollection<XElement>> GetAllElementsAsync() - { - return await store.ReadAllAsync().Select(x => x.Value.ToXml()).ToListAsync(); - } + public void StoreElement(XElement element, string friendlyName) + { + var state = new State(element); + + store.WriteAsync(new SnapshotWriteJob<State>(DomainId.Create(friendlyName), state, 0)); + } + + private async Task<IReadOnlyCollection<XElement>> GetAllElementsAsync() + { + return await store.ReadAllAsync().Select(x => x.Value.ToXml()).ToListAsync(); } } diff --git a/backend/src/Squidex.Domain.Users/IUserEvents.cs b/backend/src/Squidex.Domain.Users/IUserEvents.cs index 01deaebf45..fbfc38752a 100644 --- a/backend/src/Squidex.Domain.Users/IUserEvents.cs +++ b/backend/src/Squidex.Domain.Users/IUserEvents.cs @@ -7,28 +7,27 @@ using Squidex.Shared.Users; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public interface IUserEvents { - public interface IUserEvents + Task OnUserRegisteredAsync(IUser user) { - Task OnUserRegisteredAsync(IUser user) - { - return Task.CompletedTask; - } + return Task.CompletedTask; + } - Task OnUserUpdatedAsync(IUser user, IUser previous) - { - return Task.CompletedTask; - } + Task OnUserUpdatedAsync(IUser user, IUser previous) + { + return Task.CompletedTask; + } - Task OnUserDeletedAsync(IUser user) - { - return Task.CompletedTask; - } + Task OnUserDeletedAsync(IUser user) + { + return Task.CompletedTask; + } - Task OnConsentGivenAsync(IUser user) - { - return Task.CompletedTask; - } + Task OnConsentGivenAsync(IUser user) + { + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Users/IUserFactory.cs b/backend/src/Squidex.Domain.Users/IUserFactory.cs index c9addd16da..bf0836bf1a 100644 --- a/backend/src/Squidex.Domain.Users/IUserFactory.cs +++ b/backend/src/Squidex.Domain.Users/IUserFactory.cs @@ -7,12 +7,11 @@ using Microsoft.AspNetCore.Identity; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public interface IUserFactory { - public interface IUserFactory - { - IdentityUser Create(string email); + IdentityUser Create(string email); - bool IsId(string id); - } + bool IsId(string id); } diff --git a/backend/src/Squidex.Domain.Users/IUserPictureStore.cs b/backend/src/Squidex.Domain.Users/IUserPictureStore.cs index 885130441c..e3e05f9c35 100644 --- a/backend/src/Squidex.Domain.Users/IUserPictureStore.cs +++ b/backend/src/Squidex.Domain.Users/IUserPictureStore.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public interface IUserPictureStore { - public interface IUserPictureStore - { - Task UploadAsync(string userId, Stream stream, - CancellationToken ct = default); + Task UploadAsync(string userId, Stream stream, + CancellationToken ct = default); - Task DownloadAsync(string userId, Stream stream, - CancellationToken ct = default); - } + Task DownloadAsync(string userId, Stream stream, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Users/IUserService.cs b/backend/src/Squidex.Domain.Users/IUserService.cs index d8974b818b..37430efcf9 100644 --- a/backend/src/Squidex.Domain.Users/IUserService.cs +++ b/backend/src/Squidex.Domain.Users/IUserService.cs @@ -10,62 +10,61 @@ using Squidex.Infrastructure; using Squidex.Shared.Users; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public interface IUserService { - public interface IUserService - { - Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids, - CancellationToken ct = default); + Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids, + CancellationToken ct = default); - Task<IResultList<IUser>> QueryAsync(string? query = null, int take = 10, int skip = 0, - CancellationToken ct = default); + Task<IResultList<IUser>> QueryAsync(string? query = null, int take = 10, int skip = 0, + CancellationToken ct = default); - string GetUserId(ClaimsPrincipal user, - CancellationToken ct = default); + string GetUserId(ClaimsPrincipal user, + CancellationToken ct = default); - Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user, - CancellationToken ct = default); + Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user, + CancellationToken ct = default); - Task<bool> HasPasswordAsync(IUser user, - CancellationToken ct = default); + Task<bool> HasPasswordAsync(IUser user, + CancellationToken ct = default); - Task<bool> IsEmptyAsync( - CancellationToken ct = default); + Task<bool> IsEmptyAsync( + CancellationToken ct = default); - Task<IUser> CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false, - CancellationToken ct = default); + Task<IUser> CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false, + CancellationToken ct = default); - Task<IUser?> GetAsync(ClaimsPrincipal principal, - CancellationToken ct = default); + Task<IUser?> GetAsync(ClaimsPrincipal principal, + CancellationToken ct = default); - Task<IUser?> FindByEmailAsync(string email, - CancellationToken ct = default); + Task<IUser?> FindByEmailAsync(string email, + CancellationToken ct = default); - Task<IUser?> FindByIdAsync(string id, - CancellationToken ct = default); + Task<IUser?> FindByIdAsync(string id, + CancellationToken ct = default); - Task<IUser?> FindByLoginAsync(string provider, string key, - CancellationToken ct = default); + Task<IUser?> FindByLoginAsync(string provider, string key, + CancellationToken ct = default); - Task<IUser> SetPasswordAsync(string id, string password, string? oldPassword = null, - CancellationToken ct = default); + Task<IUser> SetPasswordAsync(string id, string password, string? oldPassword = null, + CancellationToken ct = default); - Task<IUser> AddLoginAsync(string id, ExternalLoginInfo externalLogin, - CancellationToken ct = default); + Task<IUser> AddLoginAsync(string id, ExternalLoginInfo externalLogin, + CancellationToken ct = default); - Task<IUser> RemoveLoginAsync(string id, string loginProvider, string providerKey, - CancellationToken ct = default); + Task<IUser> RemoveLoginAsync(string id, string loginProvider, string providerKey, + CancellationToken ct = default); - Task<IUser> LockAsync(string id, - CancellationToken ct = default); + Task<IUser> LockAsync(string id, + CancellationToken ct = default); - Task<IUser> UnlockAsync(string id, - CancellationToken ct = default); + Task<IUser> UnlockAsync(string id, + CancellationToken ct = default); - Task<IUser> UpdateAsync(string id, UserValues values, bool silent = false, - CancellationToken ct = default); + Task<IUser> UpdateAsync(string id, UserValues values, bool silent = false, + CancellationToken ct = default); - Task DeleteAsync(string id, - CancellationToken ct = default); - } + Task DeleteAsync(string id, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Users/InMemory/Extensions.cs b/backend/src/Squidex.Domain.Users/InMemory/Extensions.cs index 6b48cd8b9c..bd2ee7b584 100644 --- a/backend/src/Squidex.Domain.Users/InMemory/Extensions.cs +++ b/backend/src/Squidex.Domain.Users/InMemory/Extensions.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Users.InMemory +namespace Squidex.Domain.Users.InMemory; + +public static class Extensions { - public static class Extensions + public static ValueTask<T> AsValueTask<T>(this T value) { - public static ValueTask<T> AsValueTask<T>(this T value) - { - return new ValueTask<T>(value); - } + return new ValueTask<T>(value); } } diff --git a/backend/src/Squidex.Domain.Users/InMemory/ImmutableApplication.cs b/backend/src/Squidex.Domain.Users/InMemory/ImmutableApplication.cs index 6b0c670738..3ce0cdcadb 100644 --- a/backend/src/Squidex.Domain.Users/InMemory/ImmutableApplication.cs +++ b/backend/src/Squidex.Domain.Users/InMemory/ImmutableApplication.cs @@ -10,48 +10,47 @@ using System.Text.Json; using OpenIddict.Abstractions; -namespace Squidex.Domain.Users.InMemory +namespace Squidex.Domain.Users.InMemory; + +public sealed class ImmutableApplication { - public sealed class ImmutableApplication - { - public string Id { get; } + public string Id { get; } - public string? ClientId { get; } + public string? ClientId { get; } - public string? ClientSecret { get; } + public string? ClientSecret { get; } - public string? ConsentType { get; } + public string? ConsentType { get; } - public string? DisplayName { get; } + public string? DisplayName { get; } - public string? Type { get; } + public string? Type { get; } - public ImmutableDictionary<CultureInfo, string> DisplayNames { get; } + public ImmutableDictionary<CultureInfo, string> DisplayNames { get; } - public ImmutableArray<string> Permissions { get; } + public ImmutableArray<string> Permissions { get; } - public ImmutableArray<string> PostLogoutRedirectUris { get; } + public ImmutableArray<string> PostLogoutRedirectUris { get; } - public ImmutableArray<string> RedirectUris { get; } + public ImmutableArray<string> RedirectUris { get; } - public ImmutableArray<string> Requirements { get; } + public ImmutableArray<string> Requirements { get; } - public ImmutableDictionary<string, JsonElement> Properties { get; } + public ImmutableDictionary<string, JsonElement> Properties { get; } - public ImmutableApplication(string id, OpenIddictApplicationDescriptor descriptor) - { - Id = id; - ClientId = descriptor.ClientId; - ClientSecret = descriptor.ClientSecret; - ConsentType = descriptor.ConsentType; - DisplayName = descriptor.DisplayName; - DisplayNames = descriptor.DisplayNames.ToImmutableDictionary(); - Permissions = descriptor.Permissions.ToImmutableArray(); - PostLogoutRedirectUris = descriptor.PostLogoutRedirectUris.Select(x => x.ToString()).ToImmutableArray(); - Properties = descriptor.Properties.ToImmutableDictionary(); - RedirectUris = descriptor.RedirectUris.Select(x => x.ToString()).ToImmutableArray(); - Requirements = descriptor.Requirements.ToImmutableArray(); - Type = descriptor.Type; - } + public ImmutableApplication(string id, OpenIddictApplicationDescriptor descriptor) + { + Id = id; + ClientId = descriptor.ClientId; + ClientSecret = descriptor.ClientSecret; + ConsentType = descriptor.ConsentType; + DisplayName = descriptor.DisplayName; + DisplayNames = descriptor.DisplayNames.ToImmutableDictionary(); + Permissions = descriptor.Permissions.ToImmutableArray(); + PostLogoutRedirectUris = descriptor.PostLogoutRedirectUris.Select(x => x.ToString()).ToImmutableArray(); + Properties = descriptor.Properties.ToImmutableDictionary(); + RedirectUris = descriptor.RedirectUris.Select(x => x.ToString()).ToImmutableArray(); + Requirements = descriptor.Requirements.ToImmutableArray(); + Type = descriptor.Type; } } diff --git a/backend/src/Squidex.Domain.Users/InMemory/ImmutableScope.cs b/backend/src/Squidex.Domain.Users/InMemory/ImmutableScope.cs index c6c8236895..1e5020d9e0 100644 --- a/backend/src/Squidex.Domain.Users/InMemory/ImmutableScope.cs +++ b/backend/src/Squidex.Domain.Users/InMemory/ImmutableScope.cs @@ -10,36 +10,35 @@ using System.Text.Json; using OpenIddict.Abstractions; -namespace Squidex.Domain.Users.InMemory +namespace Squidex.Domain.Users.InMemory; + +public sealed class ImmutableScope { - public sealed class ImmutableScope - { - public string Id { get; } + public string Id { get; } - public string? Name { get; } + public string? Name { get; } - public string? Description { get; } + public string? Description { get; } - public string? DisplayName { get; } + public string? DisplayName { get; } - public ImmutableDictionary<CultureInfo, string> Descriptions { get; } + public ImmutableDictionary<CultureInfo, string> Descriptions { get; } - public ImmutableDictionary<CultureInfo, string> DisplayNames { get; } + public ImmutableDictionary<CultureInfo, string> DisplayNames { get; } - public ImmutableDictionary<string, JsonElement> Properties { get; } + public ImmutableDictionary<string, JsonElement> Properties { get; } - public ImmutableArray<string> Resources { get; } + public ImmutableArray<string> Resources { get; } - public ImmutableScope(string id, OpenIddictScopeDescriptor descriptor) - { - Id = id; - Description = descriptor.Description; - Descriptions = descriptor.Descriptions.ToImmutableDictionary(); - Name = descriptor.Name; - DisplayName = descriptor.DisplayName; - DisplayNames = descriptor.DisplayNames.ToImmutableDictionary(); - Properties = descriptor.Properties.ToImmutableDictionary(); - Resources = descriptor.Resources.ToImmutableArray(); - } + public ImmutableScope(string id, OpenIddictScopeDescriptor descriptor) + { + Id = id; + Description = descriptor.Description; + Descriptions = descriptor.Descriptions.ToImmutableDictionary(); + Name = descriptor.Name; + DisplayName = descriptor.DisplayName; + DisplayNames = descriptor.DisplayNames.ToImmutableDictionary(); + Properties = descriptor.Properties.ToImmutableDictionary(); + Resources = descriptor.Resources.ToImmutableArray(); } } diff --git a/backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs b/backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs index d1f3db0a0b..a05292c6fe 100644 --- a/backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs +++ b/backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs @@ -11,262 +11,261 @@ using System.Text.Json; using OpenIddict.Abstractions; -namespace Squidex.Domain.Users.InMemory +namespace Squidex.Domain.Users.InMemory; + +public class InMemoryApplicationStore : IOpenIddictApplicationStore<ImmutableApplication> { - public class InMemoryApplicationStore : IOpenIddictApplicationStore<ImmutableApplication> + private readonly List<ImmutableApplication> applications; + + public InMemoryApplicationStore(params (string Id, OpenIddictApplicationDescriptor Descriptor)[] applications) { - private readonly List<ImmutableApplication> applications; + this.applications = applications.Select(x => new ImmutableApplication(x.Id, x.Descriptor)).ToList(); + } - public InMemoryApplicationStore(params (string Id, OpenIddictApplicationDescriptor Descriptor)[] applications) - { - this.applications = applications.Select(x => new ImmutableApplication(x.Id, x.Descriptor)).ToList(); - } + public InMemoryApplicationStore(IEnumerable<(string Id, OpenIddictApplicationDescriptor Descriptor)> applications) + { + this.applications = applications.Select(x => new ImmutableApplication(x.Id, x.Descriptor)).ToList(); + } - public InMemoryApplicationStore(IEnumerable<(string Id, OpenIddictApplicationDescriptor Descriptor)> applications) - { - this.applications = applications.Select(x => new ImmutableApplication(x.Id, x.Descriptor)).ToList(); - } + public virtual ValueTask<long> CountAsync( + CancellationToken cancellationToken) + { + return new ValueTask<long>(applications.Count); + } - public virtual ValueTask<long> CountAsync( - CancellationToken cancellationToken) - { - return new ValueTask<long>(applications.Count); - } + public virtual ValueTask<long> CountAsync<TResult>(Func<IQueryable<ImmutableApplication>, IQueryable<TResult>> query, + CancellationToken cancellationToken) + { + return query(applications.AsQueryable()).LongCount().AsValueTask(); + } - public virtual ValueTask<long> CountAsync<TResult>(Func<IQueryable<ImmutableApplication>, IQueryable<TResult>> query, - CancellationToken cancellationToken) - { - return query(applications.AsQueryable()).LongCount().AsValueTask(); - } + public virtual ValueTask<TResult> GetAsync<TState, TResult>(Func<IQueryable<ImmutableApplication>, TState, IQueryable<TResult>> query, TState state, + CancellationToken cancellationToken) + { + var result = query(applications.AsQueryable(), state).First(); - public virtual ValueTask<TResult> GetAsync<TState, TResult>(Func<IQueryable<ImmutableApplication>, TState, IQueryable<TResult>> query, TState state, - CancellationToken cancellationToken) - { - var result = query(applications.AsQueryable(), state).First(); + return result.AsValueTask(); + } - return result.AsValueTask(); - } + public virtual ValueTask<ImmutableApplication?> FindByIdAsync(string identifier, + CancellationToken cancellationToken) + { + var result = applications.Find(x => x.Id == identifier); - public virtual ValueTask<ImmutableApplication?> FindByIdAsync(string identifier, - CancellationToken cancellationToken) - { - var result = applications.Find(x => x.Id == identifier); + return result.AsValueTask(); + } - return result.AsValueTask(); - } + public virtual ValueTask<ImmutableApplication?> FindByClientIdAsync(string identifier, + CancellationToken cancellationToken) + { + var result = applications.Find(x => x.ClientId == identifier); - public virtual ValueTask<ImmutableApplication?> FindByClientIdAsync(string identifier, - CancellationToken cancellationToken) - { - var result = applications.Find(x => x.ClientId == identifier); + return result.AsValueTask(); + } - return result.AsValueTask(); - } + public virtual async IAsyncEnumerable<ImmutableApplication> FindByPostLogoutRedirectUriAsync(string address, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var result = applications.Where(x => x.PostLogoutRedirectUris.Contains(address)); - public virtual async IAsyncEnumerable<ImmutableApplication> FindByPostLogoutRedirectUriAsync(string address, - [EnumeratorCancellation] CancellationToken cancellationToken) + foreach (var item in result) { - var result = applications.Where(x => x.PostLogoutRedirectUris.Contains(address)); - - foreach (var item in result) - { - yield return await Task.FromResult(item); - } + yield return await Task.FromResult(item); } + } - public virtual async IAsyncEnumerable<ImmutableApplication> FindByRedirectUriAsync(string address, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var result = applications.Where(x => x.RedirectUris.Contains(address)); - - foreach (var item in result) - { - yield return await Task.FromResult(item); - } - } + public virtual async IAsyncEnumerable<ImmutableApplication> FindByRedirectUriAsync(string address, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var result = applications.Where(x => x.RedirectUris.Contains(address)); - public virtual async IAsyncEnumerable<ImmutableApplication> ListAsync(int? count, int? offset, - [EnumeratorCancellation] CancellationToken cancellationToken) + foreach (var item in result) { - var result = applications; - - foreach (var item in result) - { - yield return await Task.FromResult(item); - } + yield return await Task.FromResult(item); } + } - public virtual async IAsyncEnumerable<TResult> ListAsync<TState, TResult>(Func<IQueryable<ImmutableApplication>, TState, IQueryable<TResult>> query, TState state, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var result = query(applications.AsQueryable(), state); - - foreach (var item in result) - { - yield return await Task.FromResult(item); - } - } + public virtual async IAsyncEnumerable<ImmutableApplication> ListAsync(int? count, int? offset, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var result = applications; - public virtual ValueTask<string?> GetIdAsync(ImmutableApplication application, - CancellationToken cancellationToken) + foreach (var item in result) { - return new ValueTask<string?>(application.Id); + yield return await Task.FromResult(item); } + } - public virtual ValueTask<string?> GetClientIdAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.ClientId.AsValueTask(); - } + public virtual async IAsyncEnumerable<TResult> ListAsync<TState, TResult>(Func<IQueryable<ImmutableApplication>, TState, IQueryable<TResult>> query, TState state, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var result = query(applications.AsQueryable(), state); - public virtual ValueTask<string?> GetClientSecretAsync(ImmutableApplication application, - CancellationToken cancellationToken) + foreach (var item in result) { - return application.ClientSecret.AsValueTask(); + yield return await Task.FromResult(item); } + } - public virtual ValueTask<string?> GetClientTypeAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.Type.AsValueTask(); - } + public virtual ValueTask<string?> GetIdAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return new ValueTask<string?>(application.Id); + } - public virtual ValueTask<string?> GetConsentTypeAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.ConsentType.AsValueTask(); - } + public virtual ValueTask<string?> GetClientIdAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.ClientId.AsValueTask(); + } - public virtual ValueTask<string?> GetDisplayNameAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.DisplayName.AsValueTask(); - } + public virtual ValueTask<string?> GetClientSecretAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.ClientSecret.AsValueTask(); + } - public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.DisplayNames.AsValueTask(); - } + public virtual ValueTask<string?> GetClientTypeAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.Type.AsValueTask(); + } - public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.Permissions.AsValueTask(); - } + public virtual ValueTask<string?> GetConsentTypeAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.ConsentType.AsValueTask(); + } - public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.PostLogoutRedirectUris.AsValueTask(); - } + public virtual ValueTask<string?> GetDisplayNameAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.DisplayName.AsValueTask(); + } - public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.RedirectUris.AsValueTask(); - } + public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.DisplayNames.AsValueTask(); + } - public virtual ValueTask<ImmutableArray<string>> GetRequirementsAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.Requirements.AsValueTask(); - } + public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.Permissions.AsValueTask(); + } - public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - return application.Properties.AsValueTask(); - } + public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.PostLogoutRedirectUris.AsValueTask(); + } - public virtual ValueTask CreateAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.RedirectUris.AsValueTask(); + } - public virtual ValueTask UpdateAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask<ImmutableArray<string>> GetRequirementsAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.Requirements.AsValueTask(); + } - public virtual ValueTask DeleteAsync(ImmutableApplication application, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + return application.Properties.AsValueTask(); + } - public virtual ValueTask<ImmutableApplication> InstantiateAsync( - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask CreateAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetClientIdAsync(ImmutableApplication application, string? identifier, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask UpdateAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetClientSecretAsync(ImmutableApplication application, string? secret, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask DeleteAsync(ImmutableApplication application, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetClientTypeAsync(ImmutableApplication application, string? type, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask<ImmutableApplication> InstantiateAsync( + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetConsentTypeAsync(ImmutableApplication application, string? type, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetClientIdAsync(ImmutableApplication application, string? identifier, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetDisplayNameAsync(ImmutableApplication application, string? name, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetClientSecretAsync(ImmutableApplication application, string? secret, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetDisplayNamesAsync(ImmutableApplication application, ImmutableDictionary<CultureInfo, string> names, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetClientTypeAsync(ImmutableApplication application, string? type, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetPermissionsAsync(ImmutableApplication application, ImmutableArray<string> permissions, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetConsentTypeAsync(ImmutableApplication application, string? type, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetPostLogoutRedirectUrisAsync(ImmutableApplication application, ImmutableArray<string> addresses, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetDisplayNameAsync(ImmutableApplication application, string? name, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetRedirectUrisAsync(ImmutableApplication application, ImmutableArray<string> addresses, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetDisplayNamesAsync(ImmutableApplication application, ImmutableDictionary<CultureInfo, string> names, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetPropertiesAsync(ImmutableApplication application, ImmutableDictionary<string, JsonElement> properties, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetPermissionsAsync(ImmutableApplication application, ImmutableArray<string> permissions, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetRequirementsAsync(ImmutableApplication application, ImmutableArray<string> requirements, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetPostLogoutRedirectUrisAsync(ImmutableApplication application, ImmutableArray<string> addresses, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public virtual ValueTask SetRedirectUrisAsync(ImmutableApplication application, ImmutableArray<string> addresses, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public virtual ValueTask SetPropertiesAsync(ImmutableApplication application, ImmutableDictionary<string, JsonElement> properties, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public virtual ValueTask SetRequirementsAsync(ImmutableApplication application, ImmutableArray<string> requirements, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); } } diff --git a/backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs b/backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs index fa2dbe30d6..5e5deaf272 100644 --- a/backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs +++ b/backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs @@ -11,214 +11,213 @@ using System.Text.Json; using OpenIddict.Abstractions; -namespace Squidex.Domain.Users.InMemory +namespace Squidex.Domain.Users.InMemory; + +public class InMemoryScopeStore : IOpenIddictScopeStore<ImmutableScope> { - public class InMemoryScopeStore : IOpenIddictScopeStore<ImmutableScope> + private readonly List<ImmutableScope> scopes; + + public InMemoryScopeStore(params (string Id, OpenIddictScopeDescriptor Descriptor)[] scopes) { - private readonly List<ImmutableScope> scopes; + this.scopes = scopes.Select(x => new ImmutableScope(x.Id, x.Descriptor)).ToList(); + } - public InMemoryScopeStore(params (string Id, OpenIddictScopeDescriptor Descriptor)[] scopes) - { - this.scopes = scopes.Select(x => new ImmutableScope(x.Id, x.Descriptor)).ToList(); - } + public InMemoryScopeStore(IEnumerable<(string Id, OpenIddictScopeDescriptor Descriptor)> scopes) + { + this.scopes = scopes.Select(x => new ImmutableScope(x.Id, x.Descriptor)).ToList(); + } - public InMemoryScopeStore(IEnumerable<(string Id, OpenIddictScopeDescriptor Descriptor)> scopes) - { - this.scopes = scopes.Select(x => new ImmutableScope(x.Id, x.Descriptor)).ToList(); - } + public virtual ValueTask<long> CountAsync( + CancellationToken cancellationToken) + { + return new ValueTask<long>(scopes.Count); + } - public virtual ValueTask<long> CountAsync( - CancellationToken cancellationToken) - { - return new ValueTask<long>(scopes.Count); - } + public virtual ValueTask<long> CountAsync<TResult>(Func<IQueryable<ImmutableScope>, IQueryable<TResult>> query, + CancellationToken cancellationToken) + { + return query(scopes.AsQueryable()).LongCount().AsValueTask(); + } - public virtual ValueTask<long> CountAsync<TResult>(Func<IQueryable<ImmutableScope>, IQueryable<TResult>> query, - CancellationToken cancellationToken) - { - return query(scopes.AsQueryable()).LongCount().AsValueTask(); - } + public virtual ValueTask<TResult> GetAsync<TState, TResult>(Func<IQueryable<ImmutableScope>, TState, IQueryable<TResult>> query, TState state, + CancellationToken cancellationToken) + { + var result = query(scopes.AsQueryable(), state).First(); - public virtual ValueTask<TResult> GetAsync<TState, TResult>(Func<IQueryable<ImmutableScope>, TState, IQueryable<TResult>> query, TState state, - CancellationToken cancellationToken) - { - var result = query(scopes.AsQueryable(), state).First(); + return result.AsValueTask(); + } - return result.AsValueTask(); - } + public virtual ValueTask<ImmutableScope?> FindByIdAsync(string identifier, + CancellationToken cancellationToken) + { + var result = scopes.Find(x => x.Id == identifier); - public virtual ValueTask<ImmutableScope?> FindByIdAsync(string identifier, - CancellationToken cancellationToken) - { - var result = scopes.Find(x => x.Id == identifier); + return result.AsValueTask(); + } - return result.AsValueTask(); - } + public virtual ValueTask<ImmutableScope?> FindByNameAsync(string name, + CancellationToken cancellationToken) + { + var result = scopes.Find(x => x.Name == name); - public virtual ValueTask<ImmutableScope?> FindByNameAsync(string name, - CancellationToken cancellationToken) - { - var result = scopes.Find(x => x.Name == name); + return result.AsValueTask(); + } - return result.AsValueTask(); - } + public virtual async IAsyncEnumerable<ImmutableScope> FindByNamesAsync(ImmutableArray<string> names, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var result = scopes.Where(x => x.Name != null && names.Contains(x.Name)); - public virtual async IAsyncEnumerable<ImmutableScope> FindByNamesAsync(ImmutableArray<string> names, - [EnumeratorCancellation] CancellationToken cancellationToken) + foreach (var item in result) { - var result = scopes.Where(x => x.Name != null && names.Contains(x.Name)); - - foreach (var item in result) - { - yield return await Task.FromResult(item); - } + yield return await Task.FromResult(item); } + } - public virtual async IAsyncEnumerable<ImmutableScope> FindByResourceAsync(string resource, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var result = scopes.Where(x => x.Resources.Contains(resource)); - - foreach (var item in result) - { - yield return await Task.FromResult(item); - } - } + public virtual async IAsyncEnumerable<ImmutableScope> FindByResourceAsync(string resource, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var result = scopes.Where(x => x.Resources.Contains(resource)); - public virtual async IAsyncEnumerable<ImmutableScope> ListAsync(int? count, int? offset, - [EnumeratorCancellation] CancellationToken cancellationToken) + foreach (var item in result) { - var result = scopes; - - foreach (var item in result) - { - yield return await Task.FromResult(item); - } + yield return await Task.FromResult(item); } + } - public virtual async IAsyncEnumerable<TResult> ListAsync<TState, TResult>(Func<IQueryable<ImmutableScope>, TState, IQueryable<TResult>> query, TState state, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var result = query(scopes.AsQueryable(), state); - - foreach (var item in result) - { - yield return await Task.FromResult(item); - } - } + public virtual async IAsyncEnumerable<ImmutableScope> ListAsync(int? count, int? offset, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var result = scopes; - public virtual ValueTask<string?> GetIdAsync(ImmutableScope scope, - CancellationToken cancellationToken) + foreach (var item in result) { - return new ValueTask<string?>(scope.Id); + yield return await Task.FromResult(item); } + } - public virtual ValueTask<string?> GetNameAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - return scope.Name.AsValueTask(); - } + public virtual async IAsyncEnumerable<TResult> ListAsync<TState, TResult>(Func<IQueryable<ImmutableScope>, TState, IQueryable<TResult>> query, TState state, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var result = query(scopes.AsQueryable(), state); - public virtual ValueTask<string?> GetDescriptionAsync(ImmutableScope scope, - CancellationToken cancellationToken) + foreach (var item in result) { - return scope.Description.AsValueTask(); + yield return await Task.FromResult(item); } + } - public virtual ValueTask<string?> GetDisplayNameAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - return scope.DisplayName.AsValueTask(); - } + public virtual ValueTask<string?> GetIdAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + return new ValueTask<string?>(scope.Id); + } - public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - return scope.Descriptions.AsValueTask(); - } + public virtual ValueTask<string?> GetNameAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + return scope.Name.AsValueTask(); + } - public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - return scope.DisplayNames.AsValueTask(); - } + public virtual ValueTask<string?> GetDescriptionAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + return scope.Description.AsValueTask(); + } - public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - return scope.Properties.AsValueTask(); - } + public virtual ValueTask<string?> GetDisplayNameAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + return scope.DisplayName.AsValueTask(); + } - public virtual ValueTask<ImmutableArray<string>> GetResourcesAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - return scope.Resources.AsValueTask(); - } + public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + return scope.Descriptions.AsValueTask(); + } - public virtual ValueTask CreateAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + return scope.DisplayNames.AsValueTask(); + } - public virtual ValueTask UpdateAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + return scope.Properties.AsValueTask(); + } - public virtual ValueTask DeleteAsync(ImmutableScope scope, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask<ImmutableArray<string>> GetResourcesAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + return scope.Resources.AsValueTask(); + } - public virtual ValueTask<ImmutableScope> InstantiateAsync( - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask CreateAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetDescriptionAsync(ImmutableScope scope, string? description, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask UpdateAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetDescriptionsAsync(ImmutableScope scope, ImmutableDictionary<CultureInfo, string> descriptions, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask DeleteAsync(ImmutableScope scope, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetDisplayNameAsync(ImmutableScope scope, string? name, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask<ImmutableScope> InstantiateAsync( + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetDisplayNamesAsync(ImmutableScope scope, ImmutableDictionary<CultureInfo, string> names, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetDescriptionAsync(ImmutableScope scope, string? description, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetNameAsync(ImmutableScope scope, string? name, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetDescriptionsAsync(ImmutableScope scope, ImmutableDictionary<CultureInfo, string> descriptions, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetPropertiesAsync(ImmutableScope scope, ImmutableDictionary<string, JsonElement> properties, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetDisplayNameAsync(ImmutableScope scope, string? name, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } - public virtual ValueTask SetResourcesAsync(ImmutableScope scope, ImmutableArray<string> resources, - CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } + public virtual ValueTask SetDisplayNamesAsync(ImmutableScope scope, ImmutableDictionary<CultureInfo, string> names, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public virtual ValueTask SetNameAsync(ImmutableScope scope, string? name, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public virtual ValueTask SetPropertiesAsync(ImmutableScope scope, ImmutableDictionary<string, JsonElement> properties, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public virtual ValueTask SetResourcesAsync(ImmutableScope scope, ImmutableArray<string> resources, + CancellationToken cancellationToken) + { + throw new NotSupportedException(); } } diff --git a/backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs b/backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs index 062872181a..57ba073047 100644 --- a/backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs +++ b/backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs @@ -12,31 +12,30 @@ using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Domain.Users -{ - public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory<IdentityUser, IdentityRole> - { - private const string AdministratorRole = "ADMINISTRATOR"; +namespace Squidex.Domain.Users; - public UserClaimsPrincipalFactoryWithEmail(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) - : base(userManager, roleManager, optionsAccessor) - { - } +public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory<IdentityUser, IdentityRole> +{ + private const string AdministratorRole = "ADMINISTRATOR"; - public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user) - { - var principal = await base.CreateAsync(user); + public UserClaimsPrincipalFactoryWithEmail(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) + : base(userManager, roleManager, optionsAccessor) + { + } - var identity = principal.Identities.First(); + public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user) + { + var principal = await base.CreateAsync(user); - identity.AddClaim(new Claim(OpenIdClaims.Email, await UserManager.GetEmailAsync(user))); + var identity = principal.Identities.First(); - if (await UserManager.IsInRoleAsync(user, AdministratorRole)) - { - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.Admin)); - } + identity.AddClaim(new Claim(OpenIdClaims.Email, await UserManager.GetEmailAsync(user))); - return principal; + if (await UserManager.IsInRoleAsync(user, AdministratorRole)) + { + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.Admin)); } + + return principal; } } diff --git a/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs b/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs index c0d6bc859a..4e7aa33712 100644 --- a/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -13,147 +13,146 @@ using Squidex.Infrastructure.Validation; using Squidex.Shared.Identity; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +internal static class UserManagerExtensions { - internal static class UserManagerExtensions + public static async Task Throw(this Task<IdentityResult> task, ILogger log) { - public static async Task Throw(this Task<IdentityResult> task, ILogger log) - { - var result = await task; + var result = await task; - static string Localize(IdentityError error) + static string Localize(IdentityError error) + { + if (!string.IsNullOrWhiteSpace(error.Code)) { - if (!string.IsNullOrWhiteSpace(error.Code)) - { - return T.Get($"dotnet_identity_{error.Code}", error.Description); - } - else - { - return error.Description; - } + return T.Get($"dotnet_identity_{error.Code}", error.Description); } - - if (!result.Succeeded) + else { - var errorMessageBuilder = new StringBuilder(); - - foreach (var error in result.Errors) - { - errorMessageBuilder.Append(error.Code); - errorMessageBuilder.Append(": "); - errorMessageBuilder.AppendLine(error.Description); - } - - var errorMessage = errorMessageBuilder.ToString(); - - log.LogError("Identity operation failed: {errorMessage}.", errorMessage); - - throw new ValidationException(result.Errors.Select(x => new ValidationError(Localize(x))).ToList()); + return error.Description; } } - public static async Task<IdentityResult> SyncClaims(this UserManager<IdentityUser> userManager, IdentityUser user, UserValues values) + if (!result.Succeeded) { - var current = await userManager.GetClaimsAsync(user); + var errorMessageBuilder = new StringBuilder(); - var claimsToRemove = new List<Claim>(); - var claimsToAdd = new List<Claim>(); - - void RemoveClaims(Func<Claim, bool> predicate) + foreach (var error in result.Errors) { - claimsToAdd.RemoveAll(x => predicate(x)); - claimsToRemove.AddRange(current.Where(predicate)); + errorMessageBuilder.Append(error.Code); + errorMessageBuilder.Append(": "); + errorMessageBuilder.AppendLine(error.Description); } - void AddClaim(string type, string value) - { - claimsToAdd.Add(new Claim(type, value)); - } + var errorMessage = errorMessageBuilder.ToString(); - void SyncString(string type, string? value) - { - if (value != null) - { - RemoveClaims(x => x.Type == type); + log.LogError("Identity operation failed: {errorMessage}.", errorMessage); - if (!string.IsNullOrWhiteSpace(value)) - { - AddClaim(type, value); - } - } - } + throw new ValidationException(result.Errors.Select(x => new ValidationError(Localize(x))).ToList()); + } + } - void SyncBoolean(string type, bool? value) - { - if (value != null) - { - RemoveClaims(x => x.Type == type); + public static async Task<IdentityResult> SyncClaims(this UserManager<IdentityUser> userManager, IdentityUser user, UserValues values) + { + var current = await userManager.GetClaimsAsync(user); - if (value == true) - { - AddClaim(type, value.ToString()!); - } - } - } + var claimsToRemove = new List<Claim>(); + var claimsToAdd = new List<Claim>(); - SyncString(SquidexClaimTypes.ClientSecret, values.ClientSecret); - SyncString(SquidexClaimTypes.DisplayName, values.DisplayName); - SyncString(SquidexClaimTypes.PictureUrl, values.PictureUrl); + void RemoveClaims(Func<Claim, bool> predicate) + { + claimsToAdd.RemoveAll(x => predicate(x)); + claimsToRemove.AddRange(current.Where(predicate)); + } - SyncBoolean(SquidexClaimTypes.Hidden, values.Hidden); - SyncBoolean(SquidexClaimTypes.Invited, values.Invited); - SyncBoolean(SquidexClaimTypes.Consent, values.Consent); - SyncBoolean(SquidexClaimTypes.ConsentForEmails, values.ConsentForEmails); + void AddClaim(string type, string value) + { + claimsToAdd.Add(new Claim(type, value)); + } - if (values.Permissions != null) + void SyncString(string type, string? value) + { + if (value != null) { - RemoveClaims(x => x.Type == SquidexClaimTypes.Permissions); + RemoveClaims(x => x.Type == type); - foreach (var permission in values.Permissions) + if (!string.IsNullOrWhiteSpace(value)) { - AddClaim(SquidexClaimTypes.Permissions, permission.Id); + AddClaim(type, value); } } + } - if (values.Properties != null) + void SyncBoolean(string type, bool? value) + { + if (value != null) { - RemoveClaims(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)); + RemoveClaims(x => x.Type == type); - foreach (var (name, value) in values.Properties) + if (value == true) { - AddClaim($"{SquidexClaimTypes.CustomPrefix}:{name}", value); + AddClaim(type, value.ToString()!); } } + } + + SyncString(SquidexClaimTypes.ClientSecret, values.ClientSecret); + SyncString(SquidexClaimTypes.DisplayName, values.DisplayName); + SyncString(SquidexClaimTypes.PictureUrl, values.PictureUrl); - if (values.CustomClaims != null) + SyncBoolean(SquidexClaimTypes.Hidden, values.Hidden); + SyncBoolean(SquidexClaimTypes.Invited, values.Invited); + SyncBoolean(SquidexClaimTypes.Consent, values.Consent); + SyncBoolean(SquidexClaimTypes.ConsentForEmails, values.ConsentForEmails); + + if (values.Permissions != null) + { + RemoveClaims(x => x.Type == SquidexClaimTypes.Permissions); + + foreach (var permission in values.Permissions) { - foreach (var group in values.CustomClaims.GroupBy(x => x.Type)) - { - RemoveClaims(x => x.Type == group.Key); + AddClaim(SquidexClaimTypes.Permissions, permission.Id); + } + } - foreach (var claim in group) - { - AddClaim(claim.Type, claim.Value); - } - } + if (values.Properties != null) + { + RemoveClaims(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)); + + foreach (var (name, value) in values.Properties) + { + AddClaim($"{SquidexClaimTypes.CustomPrefix}:{name}", value); } + } - if (claimsToRemove.Count > 0) + if (values.CustomClaims != null) + { + foreach (var group in values.CustomClaims.GroupBy(x => x.Type)) { - var result = await userManager.RemoveClaimsAsync(user, claimsToRemove); + RemoveClaims(x => x.Type == group.Key); - if (!result.Succeeded) + foreach (var claim in group) { - return result; + AddClaim(claim.Type, claim.Value); } } + } + + if (claimsToRemove.Count > 0) + { + var result = await userManager.RemoveClaimsAsync(user, claimsToRemove); - if (claimsToAdd.Count > 0) + if (!result.Succeeded) { - return await userManager.AddClaimsAsync(user, claimsToAdd); + return result; } + } - return IdentityResult.Success; + if (claimsToAdd.Count > 0) + { + return await userManager.AddClaimsAsync(user, claimsToAdd); } + + return IdentityResult.Success; } } diff --git a/backend/src/Squidex.Domain.Users/UserValues.cs b/backend/src/Squidex.Domain.Users/UserValues.cs index de04cf3799..93677e1b7e 100644 --- a/backend/src/Squidex.Domain.Users/UserValues.cs +++ b/backend/src/Squidex.Domain.Users/UserValues.cs @@ -8,32 +8,31 @@ using System.Security.Claims; using Squidex.Infrastructure.Security; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public sealed class UserValues { - public sealed class UserValues - { - public string? DisplayName { get; set; } + public string? DisplayName { get; set; } - public string? PictureUrl { get; set; } + public string? PictureUrl { get; set; } - public string? Password { get; set; } + public string? Password { get; set; } - public string? ClientSecret { get; set; } + public string? ClientSecret { get; set; } - public string Email { get; set; } + public string Email { get; set; } - public bool? Hidden { get; set; } + public bool? Hidden { get; set; } - public bool? Invited { get; set; } + public bool? Invited { get; set; } - public bool? Consent { get; set; } + public bool? Consent { get; set; } - public bool? ConsentForEmails { get; set; } + public bool? ConsentForEmails { get; set; } - public PermissionSet? Permissions { get; set; } + public PermissionSet? Permissions { get; set; } - public List<Claim>? CustomClaims { get; set; } + public List<Claim>? CustomClaims { get; set; } - public List<(string Name, string Value)>? Properties { get; set; } - } + public List<(string Name, string Value)>? Properties { get; set; } } diff --git a/backend/src/Squidex.Domain.Users/UserWithClaims.cs b/backend/src/Squidex.Domain.Users/UserWithClaims.cs index 64c8e58373..045c9f8974 100644 --- a/backend/src/Squidex.Domain.Users/UserWithClaims.cs +++ b/backend/src/Squidex.Domain.Users/UserWithClaims.cs @@ -9,36 +9,35 @@ using Microsoft.AspNetCore.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +internal sealed class UserWithClaims : IUser { - internal sealed class UserWithClaims : IUser - { - public IdentityUser Identity { get; } + public IdentityUser Identity { get; } - public string Id - { - get => Identity.Id; - } + public string Id + { + get => Identity.Id; + } - public string Email - { - get => Identity.Email; - } + public string Email + { + get => Identity.Email; + } - public bool IsLocked - { - get => Identity.LockoutEnd > DateTime.UtcNow; - } + public bool IsLocked + { + get => Identity.LockoutEnd > DateTime.UtcNow; + } - public IReadOnlyList<Claim> Claims { get; } + public IReadOnlyList<Claim> Claims { get; } - object IUser.Identity => Identity; + object IUser.Identity => Identity; - public UserWithClaims(IdentityUser user, IReadOnlyList<Claim> claims) - { - Identity = user; + public UserWithClaims(IdentityUser user, IReadOnlyList<Claim> claims) + { + Identity = user; - Claims = claims; - } + Claims = claims; } } diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs b/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs index 9102277d12..497f304815 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs @@ -8,24 +8,23 @@ using EventStore.Client; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Squidex.Infrastructure.Diagnostics +namespace Squidex.Infrastructure.Diagnostics; + +public sealed class GetEventStoreHealthCheck : IHealthCheck { - public sealed class GetEventStoreHealthCheck : IHealthCheck - { - private readonly EventStoreClient client; + private readonly EventStoreClient client; - public GetEventStoreHealthCheck(EventStoreClientSettings settings) - { - client = new EventStoreClient(settings); - } + public GetEventStoreHealthCheck(EventStoreClientSettings settings) + { + client = new EventStoreClient(settings); + } - public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, - CancellationToken cancellationToken = default) - { - await client.ReadStreamAsync(Direction.Forwards, "test", default, cancellationToken: cancellationToken) - .FirstOrDefaultAsync(cancellationToken); + public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) + { + await client.ReadStreamAsync(Direction.Forwards, "test", default, cancellationToken: cancellationToken) + .FirstOrDefaultAsync(cancellationToken); - return HealthCheckResult.Healthy("Application must query data from EventStore."); - } + return HealthCheckResult.Healthy("Application must query data from EventStore."); } } diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/EventStoreProjectionClient.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/EventStoreProjectionClient.cs index 0ab8f8050b..f40798b0ea 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/EventStoreProjectionClient.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/EventStoreProjectionClient.cs @@ -9,39 +9,39 @@ using EventStore.Client; using Squidex.Text; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class EventStoreProjectionClient { - public sealed class EventStoreProjectionClient + private readonly ConcurrentDictionary<string, bool> projections = new ConcurrentDictionary<string, bool>(); + private readonly string projectionPrefix; + private readonly EventStoreProjectionManagementClient client; + + public EventStoreProjectionClient(EventStoreClientSettings settings, string projectionPrefix) { - private readonly ConcurrentDictionary<string, bool> projections = new ConcurrentDictionary<string, bool>(); - private readonly string projectionPrefix; - private readonly EventStoreProjectionManagementClient client; + client = new EventStoreProjectionManagementClient(settings); - public EventStoreProjectionClient(EventStoreClientSettings settings, string projectionPrefix) - { - client = new EventStoreProjectionManagementClient(settings); + this.projectionPrefix = projectionPrefix; + } - this.projectionPrefix = projectionPrefix; - } + private string CreateFilterProjectionName(string filter) + { + return $"by-{projectionPrefix.Slugify()}-{filter.Slugify()}"; + } - private string CreateFilterProjectionName(string filter) + public async Task<string> CreateProjectionAsync(string? streamFilter = null) + { + if (!string.IsNullOrWhiteSpace(streamFilter) && streamFilter[0] != '^') { - return $"by-{projectionPrefix.Slugify()}-{filter.Slugify()}"; + return $"{projectionPrefix}-{streamFilter}"; } - public async Task<string> CreateProjectionAsync(string? streamFilter = null) - { - if (!string.IsNullOrWhiteSpace(streamFilter) && streamFilter[0] != '^') - { - return $"{projectionPrefix}-{streamFilter}"; - } - - streamFilter ??= ".*"; + streamFilter ??= ".*"; - var name = CreateFilterProjectionName(streamFilter); + var name = CreateFilterProjectionName(streamFilter); - var query = - $@"fromAll() + var query = + $@"fromAll() .when({{ $any: function (s, e) {{ if (e.streamId.indexOf('{projectionPrefix}') === 0 && /{streamFilter}/.test(e.streamId.substring({projectionPrefix.Length + 1}))) {{ @@ -50,26 +50,25 @@ public async Task<string> CreateProjectionAsync(string? streamFilter = null) }} }});"; - await CreateProjectionAsync(name, query); + await CreateProjectionAsync(name, query); - return name; - } + return name; + } - private async Task CreateProjectionAsync(string name, string query) + private async Task CreateProjectionAsync(string name, string query) + { + if (projections.TryAdd(name, true)) { - if (projections.TryAdd(name, true)) + try { - try - { - await client.CreateContinuousAsync(name, "fromAll().when()"); - await client.UpdateAsync(name, query, true); - } - catch (Exception ex) + await client.CreateContinuousAsync(name, "fromAll().when()"); + await client.UpdateAsync(name, query, true); + } + catch (Exception ex) + { + if (!ex.Is<InvalidOperationException>()) { - if (!ex.Is<InvalidOperationException>()) - { - throw; - } + throw; } } } diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs index 82f4f1d5db..6058d21d54 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs @@ -11,72 +11,71 @@ using Squidex.Infrastructure.Json; using EventStoreData = EventStore.Client.EventData; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public static class Formatter { - public static class Formatter + private static readonly HashSet<string> PrivateHeaders = new HashSet<string> { "$v", "$p", "$c", "$causedBy" }; + + public static StoredEvent Read(ResolvedEvent resolvedEvent, string? prefix, IJsonSerializer serializer) { - private static readonly HashSet<string> PrivateHeaders = new HashSet<string> { "$v", "$p", "$c", "$causedBy" }; + var @event = resolvedEvent.Event; - public static StoredEvent Read(ResolvedEvent resolvedEvent, string? prefix, IJsonSerializer serializer) - { - var @event = resolvedEvent.Event; + var eventPayload = Encoding.UTF8.GetString(@event.Data.Span); + var eventHeaders = GetHeaders(serializer, @event); - var eventPayload = Encoding.UTF8.GetString(@event.Data.Span); - var eventHeaders = GetHeaders(serializer, @event); + var eventData = new EventData(@event.EventType, eventHeaders, eventPayload); - var eventData = new EventData(@event.EventType, eventHeaders, eventPayload); + var streamName = GetStreamName(prefix, @event); - var streamName = GetStreamName(prefix, @event); + return new StoredEvent( + streamName, + resolvedEvent.OriginalEventNumber.ToInt64().ToString(CultureInfo.InvariantCulture), + resolvedEvent.Event.EventNumber.ToInt64(), + eventData); + } - return new StoredEvent( - streamName, - resolvedEvent.OriginalEventNumber.ToInt64().ToString(CultureInfo.InvariantCulture), - resolvedEvent.Event.EventNumber.ToInt64(), - eventData); - } + private static string GetStreamName(string? prefix, EventRecord @event) + { + var streamName = @event.EventStreamId; - private static string GetStreamName(string? prefix, EventRecord @event) + if (prefix != null && streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { - var streamName = @event.EventStreamId; + streamName = streamName[(prefix.Length + 1)..]; + } - if (prefix != null && streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - streamName = streamName[(prefix.Length + 1)..]; - } + return streamName; + } - return streamName; - } + private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, EventRecord @event) + { + var headers = Deserialize<EnvelopeHeaders>(serializer, @event.Metadata); - private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, EventRecord @event) + foreach (var key in headers.Keys.ToList()) { - var headers = Deserialize<EnvelopeHeaders>(serializer, @event.Metadata); - - foreach (var key in headers.Keys.ToList()) + if (PrivateHeaders.Contains(key)) { - if (PrivateHeaders.Contains(key)) - { - headers.Remove(key); - } + headers.Remove(key); } - - return headers; } - private static T Deserialize<T>(IJsonSerializer serializer, ReadOnlyMemory<byte> source) - { - var json = Encoding.UTF8.GetString(source.Span); + return headers; + } - return serializer.Deserialize<T>(json); - } + private static T Deserialize<T>(IJsonSerializer serializer, ReadOnlyMemory<byte> source) + { + var json = Encoding.UTF8.GetString(source.Span); - public static EventStoreData Write(EventData eventData, IJsonSerializer serializer) - { - var payload = Encoding.UTF8.GetBytes(eventData.Payload); + return serializer.Deserialize<T>(json); + } - var headersJson = serializer.Serialize(eventData.Headers); - var headersBytes = Encoding.UTF8.GetBytes(headersJson); + public static EventStoreData Write(EventData eventData, IJsonSerializer serializer) + { + var payload = Encoding.UTF8.GetBytes(eventData.Payload); - return new EventStoreData(Uuid.FromGuid(Guid.NewGuid()), eventData.Type, payload, headersBytes); - } + var headersJson = serializer.Serialize(eventData.Headers); + var headersBytes = Encoding.UTF8.GetBytes(headersJson); + + return new EventStoreData(Uuid.FromGuid(Guid.NewGuid()), eventData.Type, payload, headersBytes); } } diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs index 52190a8f7a..88bf9eeae4 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -12,245 +12,244 @@ using Squidex.Hosting.Configuration; using Squidex.Infrastructure.Json; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class GetEventStore : IEventStore, IInitializable { - public sealed class GetEventStore : IEventStore, IInitializable + private const string StreamPrefix = "squidex"; + private static readonly IReadOnlyList<StoredEvent> EmptyEvents = new List<StoredEvent>(); + private readonly EventStoreClient client; + private readonly EventStoreProjectionClient projectionClient; + private readonly IJsonSerializer serializer; + + public GetEventStore(EventStoreClientSettings settings, IJsonSerializer serializer) { - private const string StreamPrefix = "squidex"; - private static readonly IReadOnlyList<StoredEvent> EmptyEvents = new List<StoredEvent>(); - private readonly EventStoreClient client; - private readonly EventStoreProjectionClient projectionClient; - private readonly IJsonSerializer serializer; + this.serializer = serializer; - public GetEventStore(EventStoreClientSettings settings, IJsonSerializer serializer) - { - this.serializer = serializer; + client = new EventStoreClient(settings); - client = new EventStoreClient(settings); + projectionClient = new EventStoreProjectionClient(settings, StreamPrefix); + } - projectionClient = new EventStoreProjectionClient(settings, StreamPrefix); + public async Task InitializeAsync( + CancellationToken ct) + { + try + { + await client.DeleteAsync(Guid.NewGuid().ToString(), StreamState.Any, cancellationToken: ct); } - - public async Task InitializeAsync( - CancellationToken ct) + catch (Exception ex) { - try - { - await client.DeleteAsync(Guid.NewGuid().ToString(), StreamState.Any, cancellationToken: ct); - } - catch (Exception ex) - { - var error = new ConfigurationError("GetEventStore cannot connect to event store."); + var error = new ConfigurationError("GetEventStore cannot connect to event store."); - throw new ConfigurationException(error, ex); - } + throw new ConfigurationException(error, ex); } + } - public IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> subscriber, string? streamFilter = null, string? position = null) - { - Guard.NotNull(streamFilter); + public IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> subscriber, string? streamFilter = null, string? position = null) + { + Guard.NotNull(streamFilter); - return new GetEventStoreSubscription(subscriber, client, projectionClient, serializer, position, StreamPrefix, streamFilter); - } + return new GetEventStoreSubscription(subscriber, client, projectionClient, serializer, position, StreamPrefix, streamFilter); + } - public async IAsyncEnumerable<StoredEvent> QueryAllAsync(string? streamFilter = null, string? position = null, int take = int.MaxValue, - [EnumeratorCancellation] CancellationToken ct = default) + public async IAsyncEnumerable<StoredEvent> QueryAllAsync(string? streamFilter = null, string? position = null, int take = int.MaxValue, + [EnumeratorCancellation] CancellationToken ct = default) + { + if (take <= 0) { - if (take <= 0) - { - yield break; - } + yield break; + } - var streamName = await projectionClient.CreateProjectionAsync(streamFilter); + var streamName = await projectionClient.CreateProjectionAsync(streamFilter); - var stream = QueryAsync(streamName, position.ToPosition(false), take, ct); + var stream = QueryAsync(streamName, position.ToPosition(false), take, ct); - await foreach (var storedEvent in stream.IgnoreNotFound(ct)) - { - yield return storedEvent; - } + await foreach (var storedEvent in stream.IgnoreNotFound(ct)) + { + yield return storedEvent; } + } - public async IAsyncEnumerable<StoredEvent> QueryAllReverseAsync(string? streamFilter = null, Instant timestamp = default, int take = int.MaxValue, - [EnumeratorCancellation] CancellationToken ct = default) + public async IAsyncEnumerable<StoredEvent> QueryAllReverseAsync(string? streamFilter = null, Instant timestamp = default, int take = int.MaxValue, + [EnumeratorCancellation] CancellationToken ct = default) + { + if (take <= 0) { - if (take <= 0) - { - yield break; - } - - var streamName = await projectionClient.CreateProjectionAsync(streamFilter); + yield break; + } - var stream = QueryReverseAsync(streamName, StreamPosition.End, take, ct); + var streamName = await projectionClient.CreateProjectionAsync(streamFilter); - await foreach (var storedEvent in stream.IgnoreNotFound(ct).TakeWhile(x => x.Data.Headers.Timestamp() >= timestamp).WithCancellation(ct)) - { - yield return storedEvent; - } - } + var stream = QueryReverseAsync(streamName, StreamPosition.End, take, ct); - public async Task<IReadOnlyList<StoredEvent>> QueryReverseAsync(string streamName, int count = int.MaxValue, - CancellationToken ct = default) + await foreach (var storedEvent in stream.IgnoreNotFound(ct).TakeWhile(x => x.Data.Headers.Timestamp() >= timestamp).WithCancellation(ct)) { - Guard.NotNullOrEmpty(streamName); + yield return storedEvent; + } + } - if (count <= 0) - { - return EmptyEvents; - } + public async Task<IReadOnlyList<StoredEvent>> QueryReverseAsync(string streamName, int count = int.MaxValue, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(streamName); - using (Telemetry.Activities.StartActivity("GetEventStore/GetEventStore")) - { - var result = new List<StoredEvent>(); + if (count <= 0) + { + return EmptyEvents; + } - var stream = QueryReverseAsync(GetStreamName(streamName), StreamPosition.End, count, ct); + using (Telemetry.Activities.StartActivity("GetEventStore/GetEventStore")) + { + var result = new List<StoredEvent>(); - await foreach (var storedEvent in stream.IgnoreNotFound(ct)) - { - result.Add(storedEvent); - } + var stream = QueryReverseAsync(GetStreamName(streamName), StreamPosition.End, count, ct); - return result.ToList(); + await foreach (var storedEvent in stream.IgnoreNotFound(ct)) + { + result.Add(storedEvent); } - } - public async Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(streamName); + return result.ToList(); + } + } - using (Telemetry.Activities.StartActivity("GetEventStore/QueryAsync")) - { - var result = new List<StoredEvent>(); + public async Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(streamName); - var stream = QueryAsync(GetStreamName(streamName), streamPosition.ToPosition(), int.MaxValue, ct); + using (Telemetry.Activities.StartActivity("GetEventStore/QueryAsync")) + { + var result = new List<StoredEvent>(); - await foreach (var storedEvent in stream.IgnoreNotFound(ct)) - { - result.Add(storedEvent); - } + var stream = QueryAsync(GetStreamName(streamName), streamPosition.ToPosition(), int.MaxValue, ct); - return result.ToList(); + await foreach (var storedEvent in stream.IgnoreNotFound(ct)) + { + result.Add(storedEvent); } - } - private IAsyncEnumerable<StoredEvent> QueryAsync(string streamName, StreamPosition start, long count, - CancellationToken ct = default) - { - var result = client.ReadStreamAsync( - Direction.Forwards, - streamName, - start, - count, - true, - cancellationToken: ct); - - return result.Select(x => Formatter.Read(x, StreamPrefix, serializer)); + return result.ToList(); } + } - private IAsyncEnumerable<StoredEvent> QueryReverseAsync(string streamName, StreamPosition start, long count, - CancellationToken ct = default) - { - var result = client.ReadStreamAsync( - Direction.Backwards, - streamName, - start, - count, - resolveLinkTos: true, - cancellationToken: ct); - - return result.Select(x => Formatter.Read(x, StreamPrefix, serializer)); - } + private IAsyncEnumerable<StoredEvent> QueryAsync(string streamName, StreamPosition start, long count, + CancellationToken ct = default) + { + var result = client.ReadStreamAsync( + Direction.Forwards, + streamName, + start, + count, + true, + cancellationToken: ct); + + return result.Select(x => Formatter.Read(x, StreamPrefix, serializer)); + } - public async Task DeleteStreamAsync(string streamName, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(streamName); + private IAsyncEnumerable<StoredEvent> QueryReverseAsync(string streamName, StreamPosition start, long count, + CancellationToken ct = default) + { + var result = client.ReadStreamAsync( + Direction.Backwards, + streamName, + start, + count, + resolveLinkTos: true, + cancellationToken: ct); + + return result.Select(x => Formatter.Read(x, StreamPrefix, serializer)); + } - await client.DeleteAsync(GetStreamName(streamName), StreamState.Any, cancellationToken: ct); - } + public async Task DeleteStreamAsync(string streamName, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(streamName); - public Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events, - CancellationToken ct = default) - { - return AppendEventsInternalAsync(streamName, EtagVersion.Any, events, ct); - } + await client.DeleteAsync(GetStreamName(streamName), StreamState.Any, cancellationToken: ct); + } - public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events, - CancellationToken ct = default) - { - Guard.GreaterEquals(expectedVersion, -1); + public Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events, + CancellationToken ct = default) + { + return AppendEventsInternalAsync(streamName, EtagVersion.Any, events, ct); + } - return AppendEventsInternalAsync(streamName, expectedVersion, events, ct); - } + public Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events, + CancellationToken ct = default) + { + Guard.GreaterEquals(expectedVersion, -1); + + return AppendEventsInternalAsync(streamName, expectedVersion, events, ct); + } + + private async Task AppendEventsInternalAsync(string streamName, long expectedVersion, ICollection<EventData> events, + CancellationToken ct) + { + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); - private async Task AppendEventsInternalAsync(string streamName, long expectedVersion, ICollection<EventData> events, - CancellationToken ct) + using (Telemetry.Activities.StartActivity("GetEventStore/AppendEventsInternalAsync")) { - Guard.NotNullOrEmpty(streamName); - Guard.NotNull(events); + if (events.Count == 0) + { + return; + } - using (Telemetry.Activities.StartActivity("GetEventStore/AppendEventsInternalAsync")) + try { - if (events.Count == 0) + var eventData = events.Select(x => Formatter.Write(x, serializer)); + + streamName = GetStreamName(streamName); + + if (expectedVersion == -1) { - return; + await client.AppendToStreamAsync(streamName, StreamState.NoStream, eventData, cancellationToken: ct); } - - try + else if (expectedVersion < -1) { - var eventData = events.Select(x => Formatter.Write(x, serializer)); - - streamName = GetStreamName(streamName); - - if (expectedVersion == -1) - { - await client.AppendToStreamAsync(streamName, StreamState.NoStream, eventData, cancellationToken: ct); - } - else if (expectedVersion < -1) - { - await client.AppendToStreamAsync(streamName, StreamState.Any, eventData, cancellationToken: ct); - } - else - { - await client.AppendToStreamAsync(streamName, expectedVersion.ToRevision(), eventData, cancellationToken: ct); - } + await client.AppendToStreamAsync(streamName, StreamState.Any, eventData, cancellationToken: ct); } - catch (WrongExpectedVersionException ex) + else { - throw new WrongEventVersionException(ex.ActualVersion ?? 0, expectedVersion); + await client.AppendToStreamAsync(streamName, expectedVersion.ToRevision(), eventData, cancellationToken: ct); } } + catch (WrongExpectedVersionException ex) + { + throw new WrongEventVersionException(ex.ActualVersion ?? 0, expectedVersion); + } } + } - public async Task DeleteAsync(string streamFilter, - CancellationToken ct = default) - { - var streamName = await projectionClient.CreateProjectionAsync(streamFilter); + public async Task DeleteAsync(string streamFilter, + CancellationToken ct = default) + { + var streamName = await projectionClient.CreateProjectionAsync(streamFilter); - var events = client.ReadStreamAsync(Direction.Forwards, streamName, StreamPosition.Start, resolveLinkTos: true, cancellationToken: ct); + var events = client.ReadStreamAsync(Direction.Forwards, streamName, StreamPosition.Start, resolveLinkTos: true, cancellationToken: ct); - if (await events.ReadState == ReadState.StreamNotFound) - { - return; - } + if (await events.ReadState == ReadState.StreamNotFound) + { + return; + } - var deleted = new HashSet<string>(); + var deleted = new HashSet<string>(); - await foreach (var storedEvent in TaskAsyncEnumerableExtensions.WithCancellation(events, ct)) - { - var streamToDelete = storedEvent.Event.EventStreamId; + await foreach (var storedEvent in TaskAsyncEnumerableExtensions.WithCancellation(events, ct)) + { + var streamToDelete = storedEvent.Event.EventStreamId; - if (deleted.Add(streamToDelete)) - { - await client.DeleteAsync(streamToDelete, StreamState.Any, cancellationToken: ct); - } + if (deleted.Add(streamToDelete)) + { + await client.DeleteAsync(streamToDelete, StreamState.Any, cancellationToken: ct); } } + } - private static string GetStreamName(string streamName) - { - return $"{StreamPrefix}-{streamName}"; - } + private static string GetStreamName(string streamName) + { + return $"{StreamPrefix}-{streamName}"; } } diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs index 2192521a6c..d387aceb36 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -9,82 +9,81 @@ using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Tasks; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +internal sealed class GetEventStoreSubscription : IEventSubscription { - internal sealed class GetEventStoreSubscription : IEventSubscription + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private StreamSubscription subscription; + + public GetEventStoreSubscription( + IEventSubscriber<StoredEvent> eventSubscriber, + EventStoreClient client, + EventStoreProjectionClient projectionClient, + IJsonSerializer serializer, + string? position, + string? prefix, + string? streamFilter) { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private StreamSubscription subscription; - - public GetEventStoreSubscription( - IEventSubscriber<StoredEvent> eventSubscriber, - EventStoreClient client, - EventStoreProjectionClient projectionClient, - IJsonSerializer serializer, - string? position, - string? prefix, - string? streamFilter) + Task.Run(async () => { - Task.Run(async () => - { - var ct = cts.Token; + var ct = cts.Token; - var streamName = await projectionClient.CreateProjectionAsync(streamFilter); + var streamName = await projectionClient.CreateProjectionAsync(streamFilter); - async Task OnEvent(StreamSubscription subscription, ResolvedEvent @event, - CancellationToken ct) - { - var storedEvent = Formatter.Read(@event, prefix, serializer); + async Task OnEvent(StreamSubscription subscription, ResolvedEvent @event, + CancellationToken ct) + { + var storedEvent = Formatter.Read(@event, prefix, serializer); - await eventSubscriber.OnNextAsync(this, storedEvent); - } + await eventSubscriber.OnNextAsync(this, storedEvent); + } - void OnError(StreamSubscription subscription, SubscriptionDroppedReason reason, Exception? ex) + void OnError(StreamSubscription subscription, SubscriptionDroppedReason reason, Exception? ex) + { + if (reason != SubscriptionDroppedReason.Disposed && + reason != SubscriptionDroppedReason.SubscriberError) { - if (reason != SubscriptionDroppedReason.Disposed && - reason != SubscriptionDroppedReason.SubscriberError) - { - ex ??= new InvalidOperationException($"Subscription closed with reason {reason}."); + ex ??= new InvalidOperationException($"Subscription closed with reason {reason}."); - eventSubscriber.OnErrorAsync(this, ex).AsTask().Forget(); - } + eventSubscriber.OnErrorAsync(this, ex).AsTask().Forget(); } + } - if (!string.IsNullOrWhiteSpace(position)) - { - var from = FromStream.After(position.ToPosition(true)); + if (!string.IsNullOrWhiteSpace(position)) + { + var from = FromStream.After(position.ToPosition(true)); - subscription = await client.SubscribeToStreamAsync(streamName, from, - OnEvent, true, - OnError, - cancellationToken: ct); - } - else - { - var from = FromStream.Start; + subscription = await client.SubscribeToStreamAsync(streamName, from, + OnEvent, true, + OnError, + cancellationToken: ct); + } + else + { + var from = FromStream.Start; - subscription = await client.SubscribeToStreamAsync(streamName, from, - OnEvent, true, - OnError, - cancellationToken: ct); - } - }, cts.Token); - } + subscription = await client.SubscribeToStreamAsync(streamName, from, + OnEvent, true, + OnError, + cancellationToken: ct); + } + }, cts.Token); + } - public void Dispose() - { - subscription?.Dispose(); + public void Dispose() + { + subscription?.Dispose(); - cts.Cancel(); - } + cts.Cancel(); + } - public ValueTask CompleteAsync() - { - return default; - } + public ValueTask CompleteAsync() + { + return default; + } - public void WakeUp() - { - } + public void WakeUp() + { } } diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Utils.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Utils.cs index cbbba3e3e4..9723298a63 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Utils.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Utils.cs @@ -9,73 +9,72 @@ using System.Runtime.CompilerServices; using EventStore.Client; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public static class Utils { - public static class Utils + public static StreamRevision ToRevision(this long version) { - public static StreamRevision ToRevision(this long version) + return StreamRevision.FromInt64(version); + } + + public static StreamPosition ToPosition(this long version) + { + if (version <= 0) { - return StreamRevision.FromInt64(version); + return StreamPosition.Start; } - public static StreamPosition ToPosition(this long version) - { - if (version <= 0) - { - return StreamPosition.Start; - } + return StreamPosition.FromInt64(version); + } - return StreamPosition.FromInt64(version); + public static StreamPosition ToPosition(this string? position, bool inclusive) + { + if (string.IsNullOrWhiteSpace(position)) + { + return StreamPosition.Start; } - public static StreamPosition ToPosition(this string? position, bool inclusive) + if (long.TryParse(position, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedPosition)) { - if (string.IsNullOrWhiteSpace(position)) + if (!inclusive) { - return StreamPosition.Start; + parsedPosition++; } - if (long.TryParse(position, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedPosition)) - { - if (!inclusive) - { - parsedPosition++; - } + return StreamPosition.FromInt64(parsedPosition); + } - return StreamPosition.FromInt64(parsedPosition); - } + return StreamPosition.Start; + } - return StreamPosition.Start; + public static async IAsyncEnumerable<StoredEvent> IgnoreNotFound(this IAsyncEnumerable<StoredEvent> source, + [EnumeratorCancellation] CancellationToken ct = default) + { + var enumerator = source.GetAsyncEnumerator(ct); + + bool resultFound; + try + { + resultFound = await enumerator.MoveNextAsync(ct); + } + catch (StreamNotFoundException) + { + resultFound = false; } - public static async IAsyncEnumerable<StoredEvent> IgnoreNotFound(this IAsyncEnumerable<StoredEvent> source, - [EnumeratorCancellation] CancellationToken ct = default) + if (!resultFound) { - var enumerator = source.GetAsyncEnumerator(ct); + yield break; + } - bool resultFound; - try - { - resultFound = await enumerator.MoveNextAsync(ct); - } - catch (StreamNotFoundException) - { - resultFound = false; - } + yield return enumerator.Current; - if (!resultFound) - { - yield break; - } + while (await enumerator.MoveNextAsync(ct)) + { + ct.ThrowIfCancellationRequested(); yield return enumerator.Current; - - while (await enumerator.MoveNextAsync(ct)) - { - ct.ThrowIfCancellationRequested(); - - yield return enumerator.Current; - } } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoCacheEntry.cs b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoCacheEntry.cs index 07c2927f4d..a802e915f3 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoCacheEntry.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoCacheEntry.cs @@ -7,20 +7,19 @@ using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Infrastructure.Caching +namespace Squidex.Infrastructure.Caching; + +public sealed class MongoCacheEntry { - public sealed class MongoCacheEntry - { - [BsonId] - [BsonElement("_id")] - public string Key { get; set; } + [BsonId] + [BsonElement("_id")] + public string Key { get; set; } - [BsonRequired] - [BsonElement(nameof(Expires))] - public DateTime Expires { get; set; } + [BsonRequired] + [BsonElement(nameof(Expires))] + public DateTime Expires { get; set; } - [BsonRequired] - [BsonElement(nameof(Value))] - public byte[] Value { get; set; } - } + [BsonRequired] + [BsonElement(nameof(Value))] + public byte[] Value { get; set; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs index b6253e5729..1500da7ec1 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs @@ -9,101 +9,100 @@ using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Infrastructure.Caching +namespace Squidex.Infrastructure.Caching; + +public sealed class MongoDistributedCache : MongoRepositoryBase<MongoCacheEntry>, IDistributedCache { - public sealed class MongoDistributedCache : MongoRepositoryBase<MongoCacheEntry>, IDistributedCache + public MongoDistributedCache(IMongoDatabase database) + : base(database) { - public MongoDistributedCache(IMongoDatabase database) - : base(database) - { - } + } - protected override string CollectionName() - { - return "Cache"; - } + protected override string CollectionName() + { + return "Cache"; + } - protected override Task SetupCollectionAsync(IMongoCollection<MongoCacheEntry> collection, - CancellationToken ct) - { - return Collection.Indexes.CreateOneAsync( - new CreateIndexModel<MongoCacheEntry>( - Index.Ascending(x => x.Expires), - new CreateIndexOptions - { - ExpireAfter = TimeSpan.Zero - }), - null, ct); - } + protected override Task SetupCollectionAsync(IMongoCollection<MongoCacheEntry> collection, + CancellationToken ct) + { + return Collection.Indexes.CreateOneAsync( + new CreateIndexModel<MongoCacheEntry>( + Index.Ascending(x => x.Expires), + new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero + }), + null, ct); + } - public byte[] Get(string key) - { - throw new NotSupportedException(); - } + public byte[] Get(string key) + { + throw new NotSupportedException(); + } - public void Refresh(string key) - { - throw new NotSupportedException(); - } + public void Refresh(string key) + { + throw new NotSupportedException(); + } - public void Remove(string key) - { - throw new NotSupportedException(); - } + public void Remove(string key) + { + throw new NotSupportedException(); + } - public void Set(string key, byte[] value, DistributedCacheEntryOptions options) - { - throw new NotSupportedException(); - } + public void Set(string key, byte[] value, DistributedCacheEntryOptions options) + { + throw new NotSupportedException(); + } - public Task RefreshAsync(string key, - CancellationToken token = default) - { - return Task.CompletedTask; - } + public Task RefreshAsync(string key, + CancellationToken token = default) + { + return Task.CompletedTask; + } + + public Task RemoveAsync(string key, + CancellationToken token = default) + { + return Collection.DeleteOneAsync(x => x.Key == key, token); + } + + public async Task<byte[]?> GetAsync(string key, + CancellationToken token = default) + { + var entry = await Collection.Find(x => x.Key == key).FirstOrDefaultAsync(token); - public Task RemoveAsync(string key, - CancellationToken token = default) + if (entry != null && entry.Expires > DateTime.UtcNow) { - return Collection.DeleteOneAsync(x => x.Key == key, token); + return entry.Value; } - public async Task<byte[]?> GetAsync(string key, - CancellationToken token = default) - { - var entry = await Collection.Find(x => x.Key == key).FirstOrDefaultAsync(token); + return null; + } - if (entry != null && entry.Expires > DateTime.UtcNow) - { - return entry.Value; - } + public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, + CancellationToken token = default) + { + var expires = DateTime.UtcNow; - return null; + if (options.AbsoluteExpiration.HasValue) + { + expires = options.AbsoluteExpiration.Value.UtcDateTime; } - - public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, - CancellationToken token = default) + else if (options.AbsoluteExpirationRelativeToNow.HasValue) { - var expires = DateTime.UtcNow; - - if (options.AbsoluteExpiration.HasValue) - { - expires = options.AbsoluteExpiration.Value.UtcDateTime; - } - else if (options.AbsoluteExpirationRelativeToNow.HasValue) - { - expires += options.AbsoluteExpirationRelativeToNow.Value; - } - else if (options.SlidingExpiration.HasValue) - { - expires += options.SlidingExpiration.Value; - } - - return Collection.UpdateOneAsync(x => x.Key == key, - Update - .Set(x => x.Value, value) - .Set(x => x.Expires, expires), - Upsert, token); + expires += options.AbsoluteExpirationRelativeToNow.Value; } + else if (options.SlidingExpiration.HasValue) + { + expires += options.SlidingExpiration.Value; + } + + return Collection.UpdateOneAsync(x => x.Key == key, + Update + .Set(x => x.Value, value) + .Set(x => x.Expires, expires), + Upsert, token); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs index 43c9485f35..b6f3062607 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs @@ -8,29 +8,28 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using MongoDB.Driver; -namespace Squidex.Infrastructure.Diagnostics +namespace Squidex.Infrastructure.Diagnostics; + +public sealed class MongoHealthCheck : IHealthCheck { - public sealed class MongoHealthCheck : IHealthCheck - { - private readonly IMongoDatabase mongoDatabase; + private readonly IMongoDatabase mongoDatabase; - public MongoHealthCheck(IMongoDatabase mongoDatabase) - { - this.mongoDatabase = mongoDatabase; - } + public MongoHealthCheck(IMongoDatabase mongoDatabase) + { + this.mongoDatabase = mongoDatabase; + } - public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, - CancellationToken cancellationToken = default) - { - var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken); + public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) + { + var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken); - var result = await collectionNames.AnyAsync(cancellationToken); + var result = await collectionNames.AnyAsync(cancellationToken); - var status = result ? - HealthStatus.Healthy : - HealthStatus.Unhealthy; + var status = result ? + HealthStatus.Healthy : + HealthStatus.Unhealthy; - return new HealthCheckResult(status, "Application must query data from MongoDB"); - } + return new HealthCheckResult(status, "Application must query data from MongoDB"); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs index 33df4ae0a6..1a54f612f5 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs @@ -7,100 +7,99 @@ using MongoDB.Driver; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +internal static class FilterExtensions { - internal static class FilterExtensions + public static FilterDefinition<MongoEventCommit> ByPosition(StreamPosition streamPosition) { - public static FilterDefinition<MongoEventCommit> ByPosition(StreamPosition streamPosition) + if (streamPosition.IsEndOfCommit) { - if (streamPosition.IsEndOfCommit) - { - return Builders<MongoEventCommit>.Filter.Gt(x => x.Timestamp, streamPosition.Timestamp); - } - else - { - return Builders<MongoEventCommit>.Filter.Gte(x => x.Timestamp, streamPosition.Timestamp); - } + return Builders<MongoEventCommit>.Filter.Gt(x => x.Timestamp, streamPosition.Timestamp); } - - public static FilterDefinition<MongoEventCommit>? ByStream(string? streamFilter) + else { - if (StreamFilter.IsAll(streamFilter)) - { - return null; - } + return Builders<MongoEventCommit>.Filter.Gte(x => x.Timestamp, streamPosition.Timestamp); + } + } - if (streamFilter.Contains('^', StringComparison.Ordinal)) - { - return Builders<MongoEventCommit>.Filter.Regex(x => x.EventStream, streamFilter); - } - else - { - return Builders<MongoEventCommit>.Filter.Eq(x => x.EventStream, streamFilter); - } + public static FilterDefinition<MongoEventCommit>? ByStream(string? streamFilter) + { + if (StreamFilter.IsAll(streamFilter)) + { + return null; } - public static FilterDefinition<ChangeStreamDocument<MongoEventCommit>>? ByChangeInStream(string? streamFilter) + if (streamFilter.Contains('^', StringComparison.Ordinal)) { - if (StreamFilter.IsAll(streamFilter)) - { - return null; - } + return Builders<MongoEventCommit>.Filter.Regex(x => x.EventStream, streamFilter); + } + else + { + return Builders<MongoEventCommit>.Filter.Eq(x => x.EventStream, streamFilter); + } + } - if (streamFilter.Contains('^', StringComparison.Ordinal)) - { - return Builders<ChangeStreamDocument<MongoEventCommit>>.Filter.Regex(x => x.FullDocument.EventStream, streamFilter); - } - else - { - return Builders<ChangeStreamDocument<MongoEventCommit>>.Filter.Eq(x => x.FullDocument.EventStream, streamFilter); - } + public static FilterDefinition<ChangeStreamDocument<MongoEventCommit>>? ByChangeInStream(string? streamFilter) + { + if (StreamFilter.IsAll(streamFilter)) + { + return null; } - public static IEnumerable<StoredEvent> Filtered(this MongoEventCommit commit, StreamPosition lastPosition) + if (streamFilter.Contains('^', StringComparison.Ordinal)) { - var eventStreamOffset = commit.EventStreamOffset; + return Builders<ChangeStreamDocument<MongoEventCommit>>.Filter.Regex(x => x.FullDocument.EventStream, streamFilter); + } + else + { + return Builders<ChangeStreamDocument<MongoEventCommit>>.Filter.Eq(x => x.FullDocument.EventStream, streamFilter); + } + } - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; + public static IEnumerable<StoredEvent> Filtered(this MongoEventCommit commit, StreamPosition lastPosition) + { + var eventStreamOffset = commit.EventStreamOffset; - foreach (var @event in commit.Events) - { - eventStreamOffset++; + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; - if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) - { - var eventData = @event.ToEventData(); - var eventPosition = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + foreach (var @event in commit.Events) + { + eventStreamOffset++; - yield return new StoredEvent(commit.EventStream, eventPosition, eventStreamOffset, eventData); - } + if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) + { + var eventData = @event.ToEventData(); + var eventPosition = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - commitOffset++; + yield return new StoredEvent(commit.EventStream, eventPosition, eventStreamOffset, eventData); } - } - public static IEnumerable<StoredEvent> Filtered(this MongoEventCommit commit, long streamPosition = EtagVersion.Empty) - { - var eventStreamOffset = commit.EventStreamOffset; + commitOffset++; + } + } - var commitTimestamp = commit.Timestamp; - var commitOffset = 0; + public static IEnumerable<StoredEvent> Filtered(this MongoEventCommit commit, long streamPosition = EtagVersion.Empty) + { + var eventStreamOffset = commit.EventStreamOffset; - foreach (var @event in commit.Events) - { - eventStreamOffset++; + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; - if (eventStreamOffset >= streamPosition) - { - var eventData = @event.ToEventData(); - var eventPosition = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + foreach (var @event in commit.Events) + { + eventStreamOffset++; - yield return new StoredEvent(commit.EventStream, eventPosition, eventStreamOffset, eventData); - } + if (eventStreamOffset >= streamPosition) + { + var eventData = @event.ToEventData(); + var eventPosition = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - commitOffset++; + yield return new StoredEvent(commit.EventStream, eventPosition, eventStreamOffset, eventData); } + + commitOffset++; } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs index 2e23e60b82..e416330b76 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs @@ -7,30 +7,29 @@ using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class MongoEvent { - public sealed class MongoEvent - { - [BsonElement] - [BsonRequired] - public string Type { get; set; } + [BsonElement] + [BsonRequired] + public string Type { get; set; } - [BsonRequired] - [BsonElement(nameof(Payload))] - public string Payload { get; set; } + [BsonRequired] + [BsonElement(nameof(Payload))] + public string Payload { get; set; } - [BsonRequired] - [BsonElement("Metadata")] - public EnvelopeHeaders Headers { get; set; } + [BsonRequired] + [BsonElement("Metadata")] + public EnvelopeHeaders Headers { get; set; } - public static MongoEvent FromEventData(EventData data) - { - return new MongoEvent { Type = data.Type, Headers = data.Headers, Payload = data.Payload }; - } + public static MongoEvent FromEventData(EventData data) + { + return new MongoEvent { Type = data.Type, Headers = data.Headers, Payload = data.Payload }; + } - public EventData ToEventData() - { - return new EventData(Type, Headers, Payload); - } + public EventData ToEventData() + { + return new EventData(Type, Headers, Payload); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs index 487c1b2e21..2fd580d426 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventCommit.cs @@ -8,33 +8,32 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class MongoEventCommit { - public sealed class MongoEventCommit - { - [BsonId] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } - - [BsonRequired] - [BsonElement(nameof(Timestamp))] - public BsonTimestamp Timestamp { get; set; } - - [BsonRequired] - [BsonElement(nameof(Events))] - public MongoEvent[] Events { get; set; } - - [BsonRequired] - [BsonElement(nameof(EventStreamOffset))] - public long EventStreamOffset { get; set; } - - [BsonRequired] - [BsonElement(nameof(EventsCount))] - public long EventsCount { get; set; } - - [BsonRequired] - [BsonElement(nameof(EventStream))] - public string EventStream { get; set; } - } + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + [BsonRequired] + [BsonElement(nameof(Timestamp))] + public BsonTimestamp Timestamp { get; set; } + + [BsonRequired] + [BsonElement(nameof(Events))] + public MongoEvent[] Events { get; set; } + + [BsonRequired] + [BsonElement(nameof(EventStreamOffset))] + public long EventStreamOffset { get; set; } + + [BsonRequired] + [BsonElement(nameof(EventsCount))] + public long EventsCount { get; set; } + + [BsonRequired] + [BsonElement(nameof(EventStream))] + public string EventStream { get; set; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index bcc7111884..4e4ba386bf 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -10,70 +10,69 @@ using MongoDB.Driver.Core.Clusters; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore { - public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore - { - private readonly IEventNotifier notifier; + private readonly IEventNotifier notifier; - public IMongoCollection<BsonDocument> RawCollection - { - get => Database.GetCollection<BsonDocument>(CollectionName()); - } + public IMongoCollection<BsonDocument> RawCollection + { + get => Database.GetCollection<BsonDocument>(CollectionName()); + } - public IMongoCollection<MongoEventCommit> TypedCollection - { - get => Collection; - } + public IMongoCollection<MongoEventCommit> TypedCollection + { + get => Collection; + } - public bool CanUseChangeStreams { get; private set; } + public bool CanUseChangeStreams { get; private set; } - public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) - : base(database) - { - this.notifier = notifier; - } + public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) + : base(database) + { + this.notifier = notifier; + } - protected override string CollectionName() - { - return "Events2"; - } + protected override string CollectionName() + { + return "Events2"; + } - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; - } + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; + } - protected override async Task SetupCollectionAsync(IMongoCollection<MongoEventCommit> collection, - CancellationToken ct) + protected override async Task SetupCollectionAsync(IMongoCollection<MongoEventCommit> collection, + CancellationToken ct) + { + await collection.Indexes.CreateManyAsync(new[] { - await collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel<MongoEventCommit>( - Index - .Ascending(x => x.Timestamp)), - new CreateIndexModel<MongoEventCommit>( - Index - .Ascending(x => x.Timestamp) - .Ascending(x => x.EventStream)), - new CreateIndexModel<MongoEventCommit>( - Index - .Descending(x => x.Timestamp) - .Ascending(x => x.EventStream)), - new CreateIndexModel<MongoEventCommit>( - Index - .Ascending(x => x.EventStream) - .Descending(x => x.EventStreamOffset), - new CreateIndexOptions - { - Unique = true - }) - }, ct); + new CreateIndexModel<MongoEventCommit>( + Index + .Ascending(x => x.Timestamp)), + new CreateIndexModel<MongoEventCommit>( + Index + .Ascending(x => x.Timestamp) + .Ascending(x => x.EventStream)), + new CreateIndexModel<MongoEventCommit>( + Index + .Descending(x => x.Timestamp) + .Ascending(x => x.EventStream)), + new CreateIndexModel<MongoEventCommit>( + Index + .Ascending(x => x.EventStream) + .Descending(x => x.EventStreamOffset), + new CreateIndexOptions + { + Unique = true + }) + }, ct); - var clusterVersion = await Database.GetMajorVersionAsync(ct); - var clusteredAsReplica = Database.Client.Cluster.Description.Type == ClusterType.ReplicaSet; + var clusterVersion = await Database.GetMajorVersionAsync(ct); + var clusteredAsReplica = Database.Client.Cluster.Description.Type == ClusterType.ReplicaSet; - CanUseChangeStreams = clusteredAsReplica && clusterVersion >= 4; - } + CanUseChangeStreams = clusteredAsReplica && clusterVersion >= 4; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs index 839515a8c5..ebf2977ddb 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs @@ -10,160 +10,159 @@ using NodaTime; using Squidex.Infrastructure.Tasks; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class MongoEventStoreSubscription : IEventSubscription { - public sealed class MongoEventStoreSubscription : IEventSubscription - { - private readonly MongoEventStore eventStore; - private readonly IEventSubscriber<StoredEvent> eventSubscriber; - private readonly CancellationTokenSource stopToken = new CancellationTokenSource(); + private readonly MongoEventStore eventStore; + private readonly IEventSubscriber<StoredEvent> eventSubscriber; + private readonly CancellationTokenSource stopToken = new CancellationTokenSource(); - public MongoEventStoreSubscription(MongoEventStore eventStore, IEventSubscriber<StoredEvent> eventSubscriber, string? streamFilter, string? position) - { - this.eventStore = eventStore; - this.eventSubscriber = eventSubscriber; + public MongoEventStoreSubscription(MongoEventStore eventStore, IEventSubscriber<StoredEvent> eventSubscriber, string? streamFilter, string? position) + { + this.eventStore = eventStore; + this.eventSubscriber = eventSubscriber; - QueryAsync(streamFilter, position).Forget(); - } + QueryAsync(streamFilter, position).Forget(); + } - private async Task QueryAsync(string? streamFilter, string? position) + private async Task QueryAsync(string? streamFilter, string? position) + { + try { + string? lastRawPosition = null; + try { - string? lastRawPosition = null; - - try - { - lastRawPosition = await QueryOldAsync(streamFilter, position); - } - catch (OperationCanceledException) - { - } - - if (!stopToken.IsCancellationRequested) - { - await QueryCurrentAsync(streamFilter, lastRawPosition); - } + lastRawPosition = await QueryOldAsync(streamFilter, position); } - catch (Exception ex) + catch (OperationCanceledException) { - await eventSubscriber.OnErrorAsync(this, ex); } - } - private async Task QueryCurrentAsync(string? streamFilter, StreamPosition lastPosition) + if (!stopToken.IsCancellationRequested) + { + await QueryCurrentAsync(streamFilter, lastRawPosition); + } + } + catch (Exception ex) { - BsonDocument? resumeToken = null; + await eventSubscriber.OnErrorAsync(this, ex); + } + } + + private async Task QueryCurrentAsync(string? streamFilter, StreamPosition lastPosition) + { + BsonDocument? resumeToken = null; + + var start = + lastPosition.Timestamp.Timestamp > 0 ? + lastPosition.Timestamp.Timestamp - 30 : + SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromSeconds(30)).ToUnixTimeSeconds(); - var start = - lastPosition.Timestamp.Timestamp > 0 ? - lastPosition.Timestamp.Timestamp - 30 : - SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromSeconds(30)).ToUnixTimeSeconds(); + var changePipeline = Match(streamFilter); + var changeStart = new BsonTimestamp((int)start, 0); - var changePipeline = Match(streamFilter); - var changeStart = new BsonTimestamp((int)start, 0); + while (!stopToken.IsCancellationRequested) + { + var changeOptions = new ChangeStreamOptions(); - while (!stopToken.IsCancellationRequested) + if (resumeToken != null) + { + changeOptions.StartAfter = resumeToken; + } + else { - var changeOptions = new ChangeStreamOptions(); + changeOptions.StartAtOperationTime = changeStart; + } - if (resumeToken != null) - { - changeOptions.StartAfter = resumeToken; - } - else - { - changeOptions.StartAtOperationTime = changeStart; - } + using (var cursor = eventStore.TypedCollection.Watch(changePipeline, changeOptions, stopToken.Token)) + { + var isRead = false; - using (var cursor = eventStore.TypedCollection.Watch(changePipeline, changeOptions, stopToken.Token)) + await cursor.ForEachAsync(async change => { - var isRead = false; - - await cursor.ForEachAsync(async change => + if (change.OperationType == ChangeStreamOperationType.Insert) { - if (change.OperationType == ChangeStreamOperationType.Insert) + foreach (var storedEvent in change.FullDocument.Filtered(lastPosition)) { - foreach (var storedEvent in change.FullDocument.Filtered(lastPosition)) - { - await eventSubscriber.OnNextAsync(this, storedEvent); - } + await eventSubscriber.OnNextAsync(this, storedEvent); } + } - isRead = true; - }, stopToken.Token); + isRead = true; + }, stopToken.Token); - resumeToken = cursor.GetResumeToken(); + resumeToken = cursor.GetResumeToken(); - if (!isRead) - { - await Task.Delay(1000, stopToken.Token); - } + if (!isRead) + { + await Task.Delay(1000, stopToken.Token); } } } + } - private async Task<string?> QueryOldAsync(string? streamFilter, string? position) - { - string? lastRawPosition = null; + private async Task<string?> QueryOldAsync(string? streamFilter, string? position) + { + string? lastRawPosition = null; - using (var cts = new CancellationTokenSource()) + using (var cts = new CancellationTokenSource()) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, stopToken.Token)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, stopToken.Token)) + await foreach (var storedEvent in eventStore.QueryAllAsync(streamFilter, position, ct: combined.Token)) { - await foreach (var storedEvent in eventStore.QueryAllAsync(streamFilter, position, ct: combined.Token)) - { - var now = SystemClock.Instance.GetCurrentInstant(); + var now = SystemClock.Instance.GetCurrentInstant(); - var timeToNow = now - storedEvent.Data.Headers.Timestamp(); + var timeToNow = now - storedEvent.Data.Headers.Timestamp(); - if (timeToNow <= Duration.FromMinutes(5)) - { - cts.Cancel(); - } - else - { - await eventSubscriber.OnNextAsync(this, storedEvent); + if (timeToNow <= Duration.FromMinutes(5)) + { + cts.Cancel(); + } + else + { + await eventSubscriber.OnNextAsync(this, storedEvent); - lastRawPosition = storedEvent.EventPosition; - } + lastRawPosition = storedEvent.EventPosition; } } } - - return lastRawPosition; } - private static PipelineDefinition<ChangeStreamDocument<MongoEventCommit>, ChangeStreamDocument<MongoEventCommit>>? Match(string? streamFilter) - { - var result = new EmptyPipelineDefinition<ChangeStreamDocument<MongoEventCommit>>(); + return lastRawPosition; + } - var byStream = FilterExtensions.ByChangeInStream(streamFilter); + private static PipelineDefinition<ChangeStreamDocument<MongoEventCommit>, ChangeStreamDocument<MongoEventCommit>>? Match(string? streamFilter) + { + var result = new EmptyPipelineDefinition<ChangeStreamDocument<MongoEventCommit>>(); - if (byStream != null) - { - var filterBuilder = Builders<ChangeStreamDocument<MongoEventCommit>>.Filter; + var byStream = FilterExtensions.ByChangeInStream(streamFilter); - var filter = filterBuilder.Or(filterBuilder.Ne(x => x.OperationType, ChangeStreamOperationType.Insert), byStream); + if (byStream != null) + { + var filterBuilder = Builders<ChangeStreamDocument<MongoEventCommit>>.Filter; - return result.Match(filter); - } + var filter = filterBuilder.Or(filterBuilder.Ne(x => x.OperationType, ChangeStreamOperationType.Insert), byStream); - return result; + return result.Match(filter); } - public void Dispose() - { - stopToken.Cancel(); - } + return result; + } - public ValueTask CompleteAsync() - { - return default; - } + public void Dispose() + { + stopToken.Cancel(); + } - public void WakeUp() - { - } + public ValueTask CompleteAsync() + { + return default; + } + + public void WakeUp() + { } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs index 1c6904e178..0e746f15a6 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -13,186 +13,185 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public delegate bool EventPredicate(MongoEvent data); + +public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore { - public delegate bool EventPredicate(MongoEvent data); + private static readonly List<StoredEvent> EmptyEvents = new List<StoredEvent>(); - public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore + public IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> subscriber, string? streamFilter = null, string? position = null) { - private static readonly List<StoredEvent> EmptyEvents = new List<StoredEvent>(); + Guard.NotNull(subscriber); - public IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> subscriber, string? streamFilter = null, string? position = null) + if (CanUseChangeStreams) { - Guard.NotNull(subscriber); - - if (CanUseChangeStreams) - { - return new MongoEventStoreSubscription(this, subscriber, streamFilter, position); - } - else - { - return new PollingSubscription(this, subscriber, streamFilter, position); - } + return new MongoEventStoreSubscription(this, subscriber, streamFilter, position); } - - public async Task<IReadOnlyList<StoredEvent>> QueryReverseAsync(string streamName, int count = int.MaxValue, - CancellationToken ct = default) + else { - Guard.NotNullOrEmpty(streamName); + return new PollingSubscription(this, subscriber, streamFilter, position); + } + } - if (count <= 0) - { - return EmptyEvents; - } + public async Task<IReadOnlyList<StoredEvent>> QueryReverseAsync(string streamName, int count = int.MaxValue, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(streamName); - using (Telemetry.Activities.StartActivity("MongoEventStore/QueryLatestAsync")) - { - var filter = Filter.Eq(x => x.EventStream, streamName); + if (count <= 0) + { + return EmptyEvents; + } - var commits = - await Collection.Find(filter).Sort(Sort.Descending(x => x.Timestamp)).Limit(count) - .ToListAsync(ct); + using (Telemetry.Activities.StartActivity("MongoEventStore/QueryLatestAsync")) + { + var filter = Filter.Eq(x => x.EventStream, streamName); - var result = commits.Select(x => x.Filtered()).Reverse().SelectMany(x => x).TakeLast(count).ToList(); + var commits = + await Collection.Find(filter).Sort(Sort.Descending(x => x.Timestamp)).Limit(count) + .ToListAsync(ct); - return result; - } + var result = commits.Select(x => x.Filtered()).Reverse().SelectMany(x => x).TakeLast(count).ToList(); + + return result; } + } - public async Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(streamName); + public async Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(streamName); - using (Telemetry.Activities.StartActivity("MongoEventStore/QueryAsync")) - { - var filter = - Filter.And( - Filter.Eq(x => x.EventStream, streamName), - Filter.Gte(x => x.EventStreamOffset, streamPosition - MaxCommitSize)); + using (Telemetry.Activities.StartActivity("MongoEventStore/QueryAsync")) + { + var filter = + Filter.And( + Filter.Eq(x => x.EventStream, streamName), + Filter.Gte(x => x.EventStreamOffset, streamPosition - MaxCommitSize)); - var commits = - await Collection.Find(filter).Sort(Sort.Ascending(x => x.Timestamp)) - .ToListAsync(ct); + var commits = + await Collection.Find(filter).Sort(Sort.Ascending(x => x.Timestamp)) + .ToListAsync(ct); - var result = commits.SelectMany(x => x.Filtered(streamPosition)).ToList(); + var result = commits.SelectMany(x => x.Filtered(streamPosition)).ToList(); - return result; - } + return result; } + } - public async Task<IReadOnlyDictionary<string, IReadOnlyList<StoredEvent>>> QueryManyAsync(IEnumerable<string> streamNames, - CancellationToken ct = default) - { - Guard.NotNull(streamNames); + public async Task<IReadOnlyDictionary<string, IReadOnlyList<StoredEvent>>> QueryManyAsync(IEnumerable<string> streamNames, + CancellationToken ct = default) + { + Guard.NotNull(streamNames); - using (Telemetry.Activities.StartActivity("MongoEventStore/QueryManyAsync")) - { - var position = EtagVersion.Empty; + using (Telemetry.Activities.StartActivity("MongoEventStore/QueryManyAsync")) + { + var position = EtagVersion.Empty; - var filter = - Filter.And( - Filter.In(x => x.EventStream, streamNames), - Filter.Gte(x => x.EventStreamOffset, position)); + var filter = + Filter.And( + Filter.In(x => x.EventStream, streamNames), + Filter.Gte(x => x.EventStreamOffset, position)); - var commits = - await Collection.Find(filter).Sort(Sort.Ascending(x => x.Timestamp)) - .ToListAsync(ct); + var commits = + await Collection.Find(filter).Sort(Sort.Ascending(x => x.Timestamp)) + .ToListAsync(ct); - var result = commits.GroupBy(x => x.EventStream) - .ToDictionary( - x => x.Key, - x => (IReadOnlyList<StoredEvent>)x.SelectMany(y => y.Filtered(position)).ToList()); + var result = commits.GroupBy(x => x.EventStream) + .ToDictionary( + x => x.Key, + x => (IReadOnlyList<StoredEvent>)x.SelectMany(y => y.Filtered(position)).ToList()); - return result; - } + return result; } + } - public async IAsyncEnumerable<StoredEvent> QueryAllReverseAsync(string? streamFilter = null, Instant timestamp = default, int take = int.MaxValue, - [EnumeratorCancellation] CancellationToken ct = default) + public async IAsyncEnumerable<StoredEvent> QueryAllReverseAsync(string? streamFilter = null, Instant timestamp = default, int take = int.MaxValue, + [EnumeratorCancellation] CancellationToken ct = default) + { + if (take <= 0) { - if (take <= 0) - { - yield break; - } + yield break; + } - StreamPosition lastPosition = timestamp; + StreamPosition lastPosition = timestamp; - var filterDefinition = CreateFilter(streamFilter, lastPosition); + var filterDefinition = CreateFilter(streamFilter, lastPosition); - var find = - Collection.Find(filterDefinition, Batching.Options) - .Limit(take).Sort(Sort.Descending(x => x.Timestamp).Ascending(x => x.EventStream)); + var find = + Collection.Find(filterDefinition, Batching.Options) + .Limit(take).Sort(Sort.Descending(x => x.Timestamp).Ascending(x => x.EventStream)); - var taken = 0; + var taken = 0; - using (var cursor = await find.ToCursorAsync(ct)) + using (var cursor = await find.ToCursorAsync(ct)) + { + while (taken < take && await cursor.MoveNextAsync(ct)) { - while (taken < take && await cursor.MoveNextAsync(ct)) + foreach (var current in cursor.Current) { - foreach (var current in cursor.Current) + foreach (var @event in current.Filtered(lastPosition).Reverse()) { - foreach (var @event in current.Filtered(lastPosition).Reverse()) - { - yield return @event; + yield return @event; - taken++; - - if (taken == take) - { - break; - } - } + taken++; if (taken == take) { break; } } + + if (taken == take) + { + break; + } } } } + } - public async IAsyncEnumerable<StoredEvent> QueryAllAsync(string? streamFilter = null, string? position = null, int take = int.MaxValue, - [EnumeratorCancellation] CancellationToken ct = default) - { - StreamPosition lastPosition = position; + public async IAsyncEnumerable<StoredEvent> QueryAllAsync(string? streamFilter = null, string? position = null, int take = int.MaxValue, + [EnumeratorCancellation] CancellationToken ct = default) + { + StreamPosition lastPosition = position; - var filterDefinition = CreateFilter(streamFilter, lastPosition); + var filterDefinition = CreateFilter(streamFilter, lastPosition); - var find = - Collection.Find(filterDefinition) - .Limit(take).Sort(Sort.Ascending(x => x.Timestamp).Ascending(x => x.EventStream)); + var find = + Collection.Find(filterDefinition) + .Limit(take).Sort(Sort.Ascending(x => x.Timestamp).Ascending(x => x.EventStream)); - var taken = 0; + var taken = 0; - await foreach (var current in find.ToAsyncEnumerable(ct)) + await foreach (var current in find.ToAsyncEnumerable(ct)) + { + foreach (var @event in current.Filtered(lastPosition)) { - foreach (var @event in current.Filtered(lastPosition)) - { - yield return @event; + yield return @event; - taken++; + taken++; - if (taken == take) - { - break; - } + if (taken == take) + { + break; } } } + } - private static EventFilter CreateFilter(string? streamFilter, StreamPosition streamPosition) - { - var byPosition = FilterExtensions.ByPosition(streamPosition); - var byStream = FilterExtensions.ByStream(streamFilter); - - if (byStream != null) - { - return Filter.And(byPosition, byStream); - } + private static EventFilter CreateFilter(string? streamFilter, StreamPosition streamPosition) + { + var byPosition = FilterExtensions.ByPosition(streamPosition); + var byStream = FilterExtensions.ByStream(streamFilter); - return byPosition; + if (byStream != null) + { + return Filter.And(byPosition, byStream); } + + return byPosition; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs index f58c2d2f88..dadf04be74 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -10,164 +10,163 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public partial class MongoEventStore { - public partial class MongoEventStore + private const int MaxCommitSize = 100; + private const int MaxWriteAttempts = 20; + private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); + + public Task DeleteStreamAsync(string streamName, + CancellationToken ct = default) { - private const int MaxCommitSize = 100; - private const int MaxWriteAttempts = 20; - private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); + Guard.NotNullOrEmpty(streamName); - public Task DeleteStreamAsync(string streamName, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(streamName); + return Collection.DeleteManyAsync(x => x.EventStream == streamName, ct); + } - return Collection.DeleteManyAsync(x => x.EventStream == streamName, ct); - } + public Task DeleteAsync(string streamFilter, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(streamFilter); - public Task DeleteAsync(string streamFilter, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(streamFilter); + return Collection.DeleteManyAsync(FilterExtensions.ByStream(streamFilter), ct); + } - return Collection.DeleteManyAsync(FilterExtensions.ByStream(streamFilter), ct); - } + public Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events, + CancellationToken ct = default) + { + return AppendAsync(commitId, streamName, EtagVersion.Any, events, ct); + } - public Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events, - CancellationToken ct = default) - { - return AppendAsync(commitId, streamName, EtagVersion.Any, events, ct); - } + public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events, + CancellationToken ct = default) + { + Guard.NotEmpty(commitId); + Guard.NotNullOrEmpty(streamName); + Guard.NotNull(events); + Guard.GreaterEquals(expectedVersion, EtagVersion.Any); - public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events, - CancellationToken ct = default) + using (Telemetry.Activities.StartActivity("MongoEventStore/AppendAsync")) { - Guard.NotEmpty(commitId); - Guard.NotNullOrEmpty(streamName); - Guard.NotNull(events); - Guard.GreaterEquals(expectedVersion, EtagVersion.Any); + if (events.Count == 0) + { + return; + } + + var currentVersion = await GetEventStreamOffsetAsync(streamName, ct); - using (Telemetry.Activities.StartActivity("MongoEventStore/AppendAsync")) + if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) { - if (events.Count == 0) - { - return; - } + throw new WrongEventVersionException(currentVersion, expectedVersion); + } - var currentVersion = await GetEventStreamOffsetAsync(streamName, ct); + var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); - if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) + for (var attempt = 1; attempt <= MaxWriteAttempts; attempt++) + { + try { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } + await Collection.InsertOneAsync(commit, cancellationToken: ct); - var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); + if (!CanUseChangeStreams) + { + notifier.NotifyEventsStored(streamName); + } - for (var attempt = 1; attempt <= MaxWriteAttempts; attempt++) + return; + } + catch (MongoWriteException ex) { - try + if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { - await Collection.InsertOneAsync(commit, cancellationToken: ct); + currentVersion = await GetEventStreamOffsetAsync(streamName, ct); - if (!CanUseChangeStreams) + if (expectedVersion > EtagVersion.Any) { - notifier.NotifyEventsStored(streamName); + throw new WrongEventVersionException(currentVersion, expectedVersion); } - return; - } - catch (MongoWriteException ex) - { - if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + if (attempt >= MaxWriteAttempts) { - currentVersion = await GetEventStreamOffsetAsync(streamName, ct); - - if (expectedVersion > EtagVersion.Any) - { - throw new WrongEventVersionException(currentVersion, expectedVersion); - } - - if (attempt >= MaxWriteAttempts) - { - throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); - } - } - else - { - throw; + throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); } } + else + { + throw; + } } } } + } + + public async Task AppendUnsafeAsync(IEnumerable<EventCommit> commits, + CancellationToken ct = default) + { + Guard.NotNull(commits); - public async Task AppendUnsafeAsync(IEnumerable<EventCommit> commits, - CancellationToken ct = default) + using (Telemetry.Activities.StartActivity("MongoEventStore/AppendUnsafeAsync")) { - Guard.NotNull(commits); + var writes = new List<WriteModel<MongoEventCommit>>(); - using (Telemetry.Activities.StartActivity("MongoEventStore/AppendUnsafeAsync")) + foreach (var commit in commits) { - var writes = new List<WriteModel<MongoEventCommit>>(); + var document = BuildCommit(commit.Id, commit.StreamName, commit.Offset, commit.Events); - foreach (var commit in commits) - { - var document = BuildCommit(commit.Id, commit.StreamName, commit.Offset, commit.Events); - - writes.Add(new InsertOneModel<MongoEventCommit>(document)); - } - - if (writes.Count > 0) - { - await Collection.BulkWriteAsync(writes, BulkUnordered, ct); - } + writes.Add(new InsertOneModel<MongoEventCommit>(document)); } - } - private async Task<long> GetEventStreamOffsetAsync(string streamName, - CancellationToken ct = default) - { - var document = - await Collection.Find(Filter.Eq(x => x.EventStream, streamName)) - .Project<BsonDocument>(Projection - .Include(x => x.EventStreamOffset) - .Include(x => x.EventsCount)) - .Sort(Sort.Descending(x => x.EventStreamOffset)).Limit(1) - .FirstOrDefaultAsync(ct); - - if (document != null) + if (writes.Count > 0) { - return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); + await Collection.BulkWriteAsync(writes, BulkUnordered, ct); } - - return EtagVersion.Empty; } + } - private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events) + private async Task<long> GetEventStreamOffsetAsync(string streamName, + CancellationToken ct = default) + { + var document = + await Collection.Find(Filter.Eq(x => x.EventStream, streamName)) + .Project<BsonDocument>(Projection + .Include(x => x.EventStreamOffset) + .Include(x => x.EventsCount)) + .Sort(Sort.Descending(x => x.EventStreamOffset)).Limit(1) + .FirstOrDefaultAsync(ct); + + if (document != null) { - var commitEvents = new MongoEvent[events.Count]; + return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); + } - var i = 0; + return EtagVersion.Empty; + } - foreach (var e in events) - { - var mongoEvent = MongoEvent.FromEventData(e); + private static MongoEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events) + { + var commitEvents = new MongoEvent[events.Count]; - commitEvents[i++] = mongoEvent; - } + var i = 0; - var mongoCommit = new MongoEventCommit - { - Id = commitId, - Events = commitEvents, - EventsCount = events.Count, - EventStream = streamName, - EventStreamOffset = expectedVersion, - Timestamp = EmptyTimestamp - }; - - return mongoCommit; + foreach (var e in events) + { + var mongoEvent = MongoEvent.FromEventData(e); + + commitEvents[i++] = mongoEvent; } + + var mongoCommit = new MongoEventCommit + { + Id = commitId, + Events = commitEvents, + EventsCount = events.Count, + EventStream = streamName, + EventStreamOffset = expectedVersion, + Timestamp = EmptyTimestamp + }; + + return mongoCommit; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs index c21a9cbbc7..3b4c7e54d9 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs @@ -10,84 +10,83 @@ using NodaTime; using Squidex.Infrastructure.ObjectPool; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +internal sealed class StreamPosition { - internal sealed class StreamPosition - { - public static readonly StreamPosition Empty = new StreamPosition(new BsonTimestamp(0, 0), -1, -1); + public static readonly StreamPosition Empty = new StreamPosition(new BsonTimestamp(0, 0), -1, -1); - public BsonTimestamp Timestamp { get; } + public BsonTimestamp Timestamp { get; } - public long CommitOffset { get; } + public long CommitOffset { get; } - public long CommitSize { get; } + public long CommitSize { get; } - public bool IsEndOfCommit { get; } + public bool IsEndOfCommit { get; } - public StreamPosition(BsonTimestamp timestamp, long commitOffset, long commitSize) - { - Timestamp = timestamp; + public StreamPosition(BsonTimestamp timestamp, long commitOffset, long commitSize) + { + Timestamp = timestamp; - CommitOffset = commitOffset; - CommitSize = commitSize; + CommitOffset = commitOffset; + CommitSize = commitSize; - IsEndOfCommit = CommitOffset == CommitSize - 1; - } + IsEndOfCommit = CommitOffset == CommitSize - 1; + } - public static implicit operator string(StreamPosition position) + public static implicit operator string(StreamPosition position) + { + var sb = DefaultPools.StringBuilder.Get(); + try { - var sb = DefaultPools.StringBuilder.Get(); - try - { - sb.Append(position.Timestamp.Timestamp); - sb.Append('-'); - sb.Append(position.Timestamp.Increment); - sb.Append('-'); - sb.Append(position.CommitOffset); - sb.Append('-'); - sb.Append(position.CommitSize); - - return sb.ToString(); - } - finally - { - DefaultPools.StringBuilder.Return(sb); - } + sb.Append(position.Timestamp.Timestamp); + sb.Append('-'); + sb.Append(position.Timestamp.Increment); + sb.Append('-'); + sb.Append(position.CommitOffset); + sb.Append('-'); + sb.Append(position.CommitSize); + + return sb.ToString(); } - - public static implicit operator StreamPosition(string? position) + finally { - if (!string.IsNullOrWhiteSpace(position)) - { - var parts = position.Split('-'); - - if (parts.Length == 4) - { - var culture = CultureInfo.InvariantCulture; - - return new StreamPosition( - new BsonTimestamp( - int.Parse(parts[0], NumberStyles.Integer, culture), - int.Parse(parts[1], NumberStyles.Integer, culture)), - long.Parse(parts[2], NumberStyles.Integer, culture), - long.Parse(parts[3], NumberStyles.Integer, culture)); - } - } - - return Empty; + DefaultPools.StringBuilder.Return(sb); } + } - public static implicit operator StreamPosition(Instant timestamp) + public static implicit operator StreamPosition(string? position) + { + if (!string.IsNullOrWhiteSpace(position)) { - if (timestamp != default) + var parts = position.Split('-'); + + if (parts.Length == 4) { + var culture = CultureInfo.InvariantCulture; + return new StreamPosition( - new BsonTimestamp((int)timestamp.ToUnixTimeSeconds(), 0), - 0, - 0); + new BsonTimestamp( + int.Parse(parts[0], NumberStyles.Integer, culture), + int.Parse(parts[1], NumberStyles.Integer, culture)), + long.Parse(parts[2], NumberStyles.Integer, culture), + long.Parse(parts[3], NumberStyles.Integer, culture)); } + } + + return Empty; + } - return Empty; + public static implicit operator StreamPosition(Instant timestamp) + { + if (timestamp != default) + { + return new StreamPosition( + new BsonTimestamp((int)timestamp.ToUnixTimeSeconds(), 0), + 0, + 0); } + + return Empty; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequest.cs b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequest.cs index aa08e5a40b..6d5926c376 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequest.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequest.cs @@ -9,34 +9,33 @@ using MongoDB.Bson.Serialization.Attributes; using NodaTime; -namespace Squidex.Infrastructure.Log +namespace Squidex.Infrastructure.Log; + +public sealed class MongoRequest { - public sealed class MongoRequest + [BsonId] + [BsonElement("_id")] + public ObjectId Id { get; set; } + + [BsonRequired] + [BsonElement(nameof(Key))] + public string Key { get; set; } + + [BsonRequired] + [BsonElement(nameof(Timestamp))] + public Instant Timestamp { get; set; } + + [BsonRequired] + [BsonElement(nameof(Properties))] + public Dictionary<string, string> Properties { get; set; } + + public static MongoRequest FromRequest(Request request) + { + return new MongoRequest { Key = request.Key, Timestamp = request.Timestamp, Properties = request.Properties }; + } + + public Request ToRequest() { - [BsonId] - [BsonElement("_id")] - public ObjectId Id { get; set; } - - [BsonRequired] - [BsonElement(nameof(Key))] - public string Key { get; set; } - - [BsonRequired] - [BsonElement(nameof(Timestamp))] - public Instant Timestamp { get; set; } - - [BsonRequired] - [BsonElement(nameof(Properties))] - public Dictionary<string, string> Properties { get; set; } - - public static MongoRequest FromRequest(Request request) - { - return new MongoRequest { Key = request.Key, Timestamp = request.Timestamp, Properties = request.Properties }; - } - - public Request ToRequest() - { - return new Request { Key = Key, Timestamp = Timestamp, Properties = Properties }; - } + return new Request { Key = Key, Timestamp = Timestamp, Properties = Properties }; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs index e92ec84890..9f25c01418 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs @@ -10,78 +10,77 @@ using NodaTime; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Infrastructure.Log -{ - public sealed class MongoRequestLogRepository : MongoRepositoryBase<MongoRequest>, IRequestLogRepository - { - private readonly RequestLogStoreOptions options; +namespace Squidex.Infrastructure.Log; - public MongoRequestLogRepository(IMongoDatabase database, IOptions<RequestLogStoreOptions> options) - : base(database) - { - Guard.NotNull(options); +public sealed class MongoRequestLogRepository : MongoRepositoryBase<MongoRequest>, IRequestLogRepository +{ + private readonly RequestLogStoreOptions options; - this.options = options.Value; - } + public MongoRequestLogRepository(IMongoDatabase database, IOptions<RequestLogStoreOptions> options) + : base(database) + { + Guard.NotNull(options); - protected override string CollectionName() - { - return "RequestLog"; - } + this.options = options.Value; + } - protected override Task SetupCollectionAsync(IMongoCollection<MongoRequest> collection, - CancellationToken ct) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel<MongoRequest>( - Index - .Ascending(x => x.Key) - .Ascending(x => x.Timestamp)), - new CreateIndexModel<MongoRequest>( - Index - .Ascending(x => x.Timestamp), - new CreateIndexOptions - { - ExpireAfter = TimeSpan.FromDays(options.StoreRetentionInDays) - }) - }, ct); - } + protected override string CollectionName() + { + return "RequestLog"; + } - public Task InsertManyAsync(IEnumerable<Request> items, - CancellationToken ct = default) + protected override Task SetupCollectionAsync(IMongoCollection<MongoRequest> collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] { - Guard.NotNull(items); + new CreateIndexModel<MongoRequest>( + Index + .Ascending(x => x.Key) + .Ascending(x => x.Timestamp)), + new CreateIndexModel<MongoRequest>( + Index + .Ascending(x => x.Timestamp), + new CreateIndexOptions + { + ExpireAfter = TimeSpan.FromDays(options.StoreRetentionInDays) + }) + }, ct); + } - var entities = items.Select(MongoRequest.FromRequest).ToList(); + public Task InsertManyAsync(IEnumerable<Request> items, + CancellationToken ct = default) + { + Guard.NotNull(items); - if (entities.Count == 0) - { - return Task.CompletedTask; - } + var entities = items.Select(MongoRequest.FromRequest).ToList(); - return Collection.InsertManyAsync(entities, InsertUnordered, ct); + if (entities.Count == 0) + { + return Task.CompletedTask; } - public Task DeleteAsync(string key, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(key); + return Collection.InsertManyAsync(entities, InsertUnordered, ct); + } - return Collection.DeleteManyAsync(Filter.Eq(x => x.Key, key), ct); - } + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(key); - public IAsyncEnumerable<Request> QueryAllAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(key); + return Collection.DeleteManyAsync(Filter.Eq(x => x.Key, key), ct); + } - var timestampStart = Instant.FromDateTimeUtc(fromDate); - var timestampEnd = Instant.FromDateTimeUtc(toDate.AddDays(1)); + public IAsyncEnumerable<Request> QueryAllAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(key); - var find = Collection.Find(x => x.Key == key && x.Timestamp >= timestampStart && x.Timestamp < timestampEnd); + var timestampStart = Instant.FromDateTimeUtc(fromDate); + var timestampEnd = Instant.FromDateTimeUtc(toDate.AddDays(1)); - return find.ToAsyncEnumerable(ct).Select(x => x.ToRequest()); - } + var find = Collection.Find(x => x.Key == key && x.Timestamp >= timestampStart && x.Timestamp < timestampEnd); + + return find.ToAsyncEnumerable(ct).Select(x => x.ToRequest()); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs index 9ab4560167..50a0a59eaa 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationEntity.cs @@ -8,20 +8,19 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +public sealed class MongoMigrationEntity { - public sealed class MongoMigrationEntity - { - [BsonId] - [BsonElement("_id")] - public string Id { get; set; } + [BsonId] + [BsonElement("_id")] + public string Id { get; set; } - [BsonRequired] - [BsonElement(nameof(IsLocked))] - public bool IsLocked { get; set; } + [BsonRequired] + [BsonElement(nameof(IsLocked))] + public bool IsLocked { get; set; } - [BsonElement] - [BsonRequired] - public int Version { get; set; } - } + [BsonElement] + [BsonRequired] + public int Version { get; set; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs index add92fd9a8..708489d3a2 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs @@ -8,61 +8,60 @@ using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +public sealed class MongoMigrationStatus : MongoRepositoryBase<MongoMigrationEntity>, IMigrationStatus { - public sealed class MongoMigrationStatus : MongoRepositoryBase<MongoMigrationEntity>, IMigrationStatus - { - private const string DefaultId = "Default"; - private static readonly FindOneAndUpdateOptions<MongoMigrationEntity> UpsertFind = new FindOneAndUpdateOptions<MongoMigrationEntity> { IsUpsert = true }; + private const string DefaultId = "Default"; + private static readonly FindOneAndUpdateOptions<MongoMigrationEntity> UpsertFind = new FindOneAndUpdateOptions<MongoMigrationEntity> { IsUpsert = true }; - public MongoMigrationStatus(IMongoDatabase database) - : base(database) - { - } + public MongoMigrationStatus(IMongoDatabase database) + : base(database) + { + } - protected override string CollectionName() - { - return "Migration"; - } + protected override string CollectionName() + { + return "Migration"; + } - public async Task<int> GetVersionAsync( - CancellationToken ct = default) - { - var entity = await Collection.Find(x => x.Id == DefaultId).FirstOrDefaultAsync(ct); + public async Task<int> GetVersionAsync( + CancellationToken ct = default) + { + var entity = await Collection.Find(x => x.Id == DefaultId).FirstOrDefaultAsync(ct); - return entity.Version; - } + return entity.Version; + } - public async Task<bool> TryLockAsync( - CancellationToken ct = default) - { - var entity = - await Collection.FindOneAndUpdateAsync<MongoMigrationEntity>(x => x.Id == DefaultId, - Update - .Set(x => x.IsLocked, true) - .SetOnInsert(x => x.Version, 0), - UpsertFind, - ct); + public async Task<bool> TryLockAsync( + CancellationToken ct = default) + { + var entity = + await Collection.FindOneAndUpdateAsync<MongoMigrationEntity>(x => x.Id == DefaultId, + Update + .Set(x => x.IsLocked, true) + .SetOnInsert(x => x.Version, 0), + UpsertFind, + ct); - return entity is not { IsLocked: true }; - } + return entity is not { IsLocked: true }; + } - public Task CompleteAsync(int newVersion, - CancellationToken ct = default) - { - return Collection.UpdateOneAsync(x => x.Id == DefaultId, - Update - .Set(x => x.Version, newVersion), - cancellationToken: ct); - } + public Task CompleteAsync(int newVersion, + CancellationToken ct = default) + { + return Collection.UpdateOneAsync(x => x.Id == DefaultId, + Update + .Set(x => x.Version, newVersion), + cancellationToken: ct); + } - public Task UnlockAsync( - CancellationToken ct = default) - { - return Collection.UpdateOneAsync(x => x.Id == DefaultId, - Update - .Set(x => x.IsLocked, false), - cancellationToken: ct); - } + public Task UnlockAsync( + CancellationToken ct = default) + { + return Collection.UpdateOneAsync(x => x.Id == DefaultId, + Update + .Set(x => x.IsLocked, false), + cancellationToken: ct); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs index 8b34ffa404..cd98d8fa48 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs @@ -7,13 +7,12 @@ using MongoDB.Driver; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public static class Batching { - public static class Batching + public static readonly FindOptions Options = new FindOptions { - public static readonly FindOptions Options = new FindOptions - { - BatchSize = 200 - }; - } + BatchSize = 200 + }; } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDefaultConventions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDefaultConventions.cs index c7d6fb0901..6e9591c5ce 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDefaultConventions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDefaultConventions.cs @@ -8,32 +8,31 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Conventions; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public static class BsonDefaultConventions { - public static class BsonDefaultConventions - { - private static bool isRegistered; + private static bool isRegistered; - public static void Register() + public static void Register() + { + try { - try - { - if (isRegistered) - { - return; - } - - ConventionRegistry.Register("IgnoreExtraElements", new ConventionPack - { - new IgnoreExtraElementsConvention(true) - }, t => true); - - isRegistered = true; - } - catch (BsonSerializationException) + if (isRegistered) { return; } + + ConventionRegistry.Register("IgnoreExtraElements", new ConventionPack + { + new IgnoreExtraElementsConvention(true) + }, t => true); + + isRegistered = true; + } + catch (BsonSerializationException) + { + return; } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs index a10b118bbc..59df7d3140 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs @@ -9,69 +9,68 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public sealed class BsonDomainIdSerializer : SerializerBase<DomainId>, IBsonPolymorphicSerializer, IRepresentationConfigurable<BsonDomainIdSerializer> { - public sealed class BsonDomainIdSerializer : SerializerBase<DomainId>, IBsonPolymorphicSerializer, IRepresentationConfigurable<BsonDomainIdSerializer> + public static void Register() { - public static void Register() + try { - try - { - BsonSerializer.RegisterSerializer(new BsonDomainIdSerializer()); - } - catch (BsonSerializationException) - { - return; - } + BsonSerializer.RegisterSerializer(new BsonDomainIdSerializer()); } - - public bool IsDiscriminatorCompatibleWithObjectSerializer + catch (BsonSerializationException) { - get => true; + return; } + } - public BsonType Representation { get; } = BsonType.String; - - public override DomainId Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) - { - switch (context.Reader.CurrentBsonType) - { - case BsonType.String: - return DomainId.Create(context.Reader.ReadString()); - case BsonType.Binary: - var binary = context.Reader.ReadBinaryData(); - - if (binary.SubType is BsonBinarySubType.UuidLegacy or BsonBinarySubType.UuidStandard) - { - return DomainId.Create(binary.ToGuid()); - } + public bool IsDiscriminatorCompatibleWithObjectSerializer + { + get => true; + } - return DomainId.Create(binary.ToString()); - default: - ThrowHelper.NotSupportedException(); - return default!; - } - } + public BsonType Representation { get; } = BsonType.String; - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DomainId value) + public override DomainId Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + switch (context.Reader.CurrentBsonType) { - context.Writer.WriteString(value.ToString()); - } + case BsonType.String: + return DomainId.Create(context.Reader.ReadString()); + case BsonType.Binary: + var binary = context.Reader.ReadBinaryData(); - public BsonDomainIdSerializer WithRepresentation(BsonType representation) - { - if (representation != BsonType.String) - { + if (binary.SubType is BsonBinarySubType.UuidLegacy or BsonBinarySubType.UuidStandard) + { + return DomainId.Create(binary.ToGuid()); + } + + return DomainId.Create(binary.ToString()); + default: ThrowHelper.NotSupportedException(); return default!; - } - - return this; } + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DomainId value) + { + context.Writer.WriteString(value.ToString()); + } - IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) + public BsonDomainIdSerializer WithRepresentation(BsonType representation) + { + if (representation != BsonType.String) { - return WithRepresentation(representation); + ThrowHelper.NotSupportedException(); + return default!; } + + return this; + } + + IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) + { + return WithRepresentation(representation); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonEscapedDictionarySerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonEscapedDictionarySerializer.cs index 56c70c9d83..0511ba9f3b 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonEscapedDictionarySerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonEscapedDictionarySerializer.cs @@ -10,56 +10,55 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public sealed class BsonEscapedDictionarySerializer<TValue, TInstance> : ClassSerializerBase<TInstance> where TInstance : Dictionary<string, TValue?>, new() { - public sealed class BsonEscapedDictionarySerializer<TValue, TInstance> : ClassSerializerBase<TInstance> where TInstance : Dictionary<string, TValue?>, new() + public static void Register() { - public static void Register() + try { - try - { - BsonSerializer.RegisterSerializer(new BsonEscapedDictionarySerializer<TValue, TInstance>()); - } - catch (BsonSerializationException) - { - return; - } + BsonSerializer.RegisterSerializer(new BsonEscapedDictionarySerializer<TValue, TInstance>()); } - - protected override TInstance DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) + catch (BsonSerializationException) { - var reader = context.Reader; - - var result = new TInstance(); + return; + } + } - reader.ReadStartDocument(); + protected override TInstance DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var reader = context.Reader; - while (reader.ReadBsonType() != BsonType.EndOfDocument) - { - var key = reader.ReadName().BsonToJsonName(); + var result = new TInstance(); - result.Add(key, BsonSerializer.Deserialize<TValue>(reader)); - } + reader.ReadStartDocument(); - reader.ReadEndDocument(); + while (reader.ReadBsonType() != BsonType.EndOfDocument) + { + var key = reader.ReadName().BsonToJsonName(); - return result; + result.Add(key, BsonSerializer.Deserialize<TValue>(reader)); } - protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, TInstance value) - { - var writer = context.Writer; + reader.ReadEndDocument(); + + return result; + } - writer.WriteStartDocument(); + protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, TInstance value) + { + var writer = context.Writer; - foreach (var property in value) - { - writer.WriteName(property.Key.JsonToBsonName()); + writer.WriteStartDocument(); - BsonSerializer.Serialize(writer, property.Value); - } + foreach (var property in value) + { + writer.WriteName(property.Key.JsonToBsonName()); - writer.WriteEndDocument(); + BsonSerializer.Serialize(writer, property.Value); } + + writer.WriteEndDocument(); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs index b70980787b..07975565c0 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs @@ -5,63 +5,62 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public static class BsonHelper { - public static class BsonHelper + private const string Empty = "§empty"; + private const string TypeBson = "§type"; + private const string TypeJson = "$type"; + private const string DotBson = "_§§_"; + private const string DotJson = "."; + + public static string BsonToJsonName(this string value) { - private const string Empty = "§empty"; - private const string TypeBson = "§type"; - private const string TypeJson = "$type"; - private const string DotBson = "_§§_"; - private const string DotJson = "."; + if (value == Empty) + { + return string.Empty; + } - public static string BsonToJsonName(this string value) + if (value == TypeBson) { - if (value == Empty) - { - return string.Empty; - } + return TypeJson; + } - if (value == TypeBson) - { - return TypeJson; - } + var result = value.ReplaceFirst('§', '$').Replace(DotBson, DotJson, StringComparison.Ordinal); - var result = value.ReplaceFirst('§', '$').Replace(DotBson, DotJson, StringComparison.Ordinal); + return result; + } - return result; + public static string JsonToBsonName(this string value) + { + if (value.Length == 0) + { + return Empty; } - public static string JsonToBsonName(this string value) + if (value == TypeJson) { - if (value.Length == 0) - { - return Empty; - } + return TypeBson; + } - if (value == TypeJson) - { - return TypeBson; - } + var result = value.ReplaceFirst('$', '§').Replace(DotJson, DotBson, StringComparison.Ordinal); - var result = value.ReplaceFirst('$', '§').Replace(DotJson, DotBson, StringComparison.Ordinal); + return result; + } - return result; + private static string ReplaceFirst(this string value, char toReplace, char replacement) + { + if (value.Length == 0 || value[0] != toReplace) + { + return value; } - private static string ReplaceFirst(this string value, char toReplace, char replacement) + if (value.Length == 1) { - if (value.Length == 0 || value[0] != toReplace) - { - return value; - } - - if (value.Length == 1) - { - return toReplace.ToString(); - } - - return replacement + value[1..]; + return toReplace.ToString(); } + + return replacement + value[1..]; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs index 24bbae3b8f..9aa0a4566b 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs @@ -11,89 +11,88 @@ using NodaTime; using NodaTime.Text; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public sealed class BsonInstantSerializer : SerializerBase<Instant>, IBsonPolymorphicSerializer, IRepresentationConfigurable<BsonInstantSerializer> { - public sealed class BsonInstantSerializer : SerializerBase<Instant>, IBsonPolymorphicSerializer, IRepresentationConfigurable<BsonInstantSerializer> + public static void Register() { - public static void Register() + try { - try - { - BsonSerializer.RegisterSerializer(new BsonInstantSerializer()); - } - catch (BsonSerializationException) - { - return; - } + BsonSerializer.RegisterSerializer(new BsonInstantSerializer()); } - - public bool IsDiscriminatorCompatibleWithObjectSerializer + catch (BsonSerializationException) { - get => true; + return; } + } - public BsonType Representation { get; } + public bool IsDiscriminatorCompatibleWithObjectSerializer + { + get => true; + } - public BsonInstantSerializer() - : this(BsonType.DateTime) - { - } + public BsonType Representation { get; } - public BsonInstantSerializer(BsonType representation) - { - if (representation is not BsonType.DateTime and not BsonType.Int64 and not BsonType.String) - { - throw new ArgumentException("Unsupported representation.", nameof(representation)); - } + public BsonInstantSerializer() + : this(BsonType.DateTime) + { + } - Representation = representation; + public BsonInstantSerializer(BsonType representation) + { + if (representation is not BsonType.DateTime and not BsonType.Int64 and not BsonType.String) + { + throw new ArgumentException("Unsupported representation.", nameof(representation)); } - public override Instant Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) - { - var reader = context.Reader; + Representation = representation; + } - switch (reader.CurrentBsonType) - { - case BsonType.DateTime: - return Instant.FromUnixTimeMilliseconds(context.Reader.ReadDateTime()); - case BsonType.Int64: - return Instant.FromUnixTimeMilliseconds(context.Reader.ReadInt64()); - case BsonType.String: - return InstantPattern.ExtendedIso.Parse(context.Reader.ReadString()).Value; - default: - ThrowHelper.NotSupportedException("Unsupported Representation."); - return default!; - } - } + public override Instant Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var reader = context.Reader; - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Instant value) + switch (reader.CurrentBsonType) { - switch (Representation) - { - case BsonType.DateTime: - context.Writer.WriteDateTime(value.ToUnixTimeMilliseconds()); - break; - case BsonType.Int64: - context.Writer.WriteInt64(value.ToUnixTimeMilliseconds()); - break; - case BsonType.String: - context.Writer.WriteString(InstantPattern.ExtendedIso.Format(value)); - break; - default: - ThrowHelper.NotSupportedException("Unsupported Representation."); - break; - } + case BsonType.DateTime: + return Instant.FromUnixTimeMilliseconds(context.Reader.ReadDateTime()); + case BsonType.Int64: + return Instant.FromUnixTimeMilliseconds(context.Reader.ReadInt64()); + case BsonType.String: + return InstantPattern.ExtendedIso.Parse(context.Reader.ReadString()).Value; + default: + ThrowHelper.NotSupportedException("Unsupported Representation."); + return default!; } + } - public BsonInstantSerializer WithRepresentation(BsonType representation) + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Instant value) + { + switch (Representation) { - return Representation == representation ? this : new BsonInstantSerializer(representation); + case BsonType.DateTime: + context.Writer.WriteDateTime(value.ToUnixTimeMilliseconds()); + break; + case BsonType.Int64: + context.Writer.WriteInt64(value.ToUnixTimeMilliseconds()); + break; + case BsonType.String: + context.Writer.WriteString(InstantPattern.ExtendedIso.Format(value)); + break; + default: + ThrowHelper.NotSupportedException("Unsupported Representation."); + break; } + } - IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) - { - return WithRepresentation(representation); - } + public BsonInstantSerializer WithRepresentation(BsonType representation) + { + return Representation == representation ? this : new BsonInstantSerializer(representation); + } + + IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) + { + return WithRepresentation(representation); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs index 9e67a2d1be..49c2bd8fe0 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class BsonJsonAttribute : Attribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class BsonJsonAttribute : Attribute - { - } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index 20f24058d9..155f8845c9 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -11,58 +11,57 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public static class BsonJsonConvention { - public static class BsonJsonConvention - { - private static bool isRegistered; + private static bool isRegistered; - public static JsonSerializerOptions Options { get; private set; } = new JsonSerializerOptions(JsonSerializerDefaults.Web); + public static JsonSerializerOptions Options { get; private set; } = new JsonSerializerOptions(JsonSerializerDefaults.Web); - public static BsonType Representation { get; private set; } = BsonType.Document; + public static BsonType Representation { get; private set; } = BsonType.Document; - public static void Register(JsonSerializerOptions? options = null, BsonType? representation = null) + public static void Register(JsonSerializerOptions? options = null, BsonType? representation = null) + { + try { - try + if (options != null) { - if (options != null) - { - Options = options; - } + Options = options; + } - if (representation != null) - { - Representation = representation.Value; - } + if (representation != null) + { + Representation = representation.Value; + } - if (isRegistered) - { - return; - } + if (isRegistered) + { + return; + } - var pack = new ConventionPack(); + var pack = new ConventionPack(); - pack.AddMemberMapConvention("JsonBson", memberMap => - { - var attributes = memberMap.MemberInfo.GetCustomAttributes(); + pack.AddMemberMapConvention("JsonBson", memberMap => + { + var attributes = memberMap.MemberInfo.GetCustomAttributes(); - if (attributes.OfType<BsonJsonAttribute>().Any()) - { - var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); - var bsonSerializer = Activator.CreateInstance(bsonSerializerType); + if (attributes.OfType<BsonJsonAttribute>().Any()) + { + var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); + var bsonSerializer = Activator.CreateInstance(bsonSerializerType); - memberMap.SetSerializer((IBsonSerializer)bsonSerializer!); - } - }); + memberMap.SetSerializer((IBsonSerializer)bsonSerializer!); + } + }); - ConventionRegistry.Register("json", pack, t => true); + ConventionRegistry.Register("json", pack, t => true); - isRegistered = true; - } - catch (BsonSerializationException) - { - return; - } + isRegistered = true; + } + catch (BsonSerializationException) + { + return; } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs index 532393c4f5..35ddb45682 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs @@ -12,262 +12,261 @@ using MongoDB.Bson.Serialization.Serializers; using Squidex.Infrastructure.ObjectPool; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public sealed class BsonJsonSerializer<T> : SerializerBase<T?>, IRepresentationConfigurable<BsonJsonSerializer<T>> where T : class { - public sealed class BsonJsonSerializer<T> : SerializerBase<T?>, IRepresentationConfigurable<BsonJsonSerializer<T>> where T : class - { - public BsonType Representation { get; } + public BsonType Representation { get; } - public BsonType ActualRepresentation + public BsonType ActualRepresentation + { + get { - get - { - var result = Representation; - - if (result == BsonType.Undefined) - { - result = BsonJsonConvention.Representation; - } - - if (result == BsonType.Undefined) - { - result = BsonType.Document; - } + var result = Representation; - return result; + if (result == BsonType.Undefined) + { + result = BsonJsonConvention.Representation; } - } - public JsonSerializerOptions Options - { - get => BsonJsonConvention.Options; - } - - public BsonJsonSerializer() - : this(BsonType.Undefined) - { - } - - public BsonJsonSerializer(BsonType representation) - { - if (representation is not BsonType.Undefined and not BsonType.String and not BsonType.Binary and not BsonType.Document) + if (result == BsonType.Undefined) { - throw new ArgumentException("Unsupported representation.", nameof(representation)); + result = BsonType.Document; } - Representation = representation; + return result; } + } - public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) - { - var reader = context.Reader; - - switch (reader.GetCurrentBsonType()) - { - case BsonType.Null: - reader.ReadNull(); - return null; - case BsonType.String: - var valueString = reader.ReadString(); - return JsonSerializer.Deserialize<T>(valueString, Options); - case BsonType.Binary: - var valueBinary = reader.ReadBytes(); - return JsonSerializer.Deserialize<T>(valueBinary, Options); - default: - using (var stream = DefaultPools.MemoryStream.GetStream()) - { - using (var writer = new Utf8JsonWriter(stream)) - { - FromBson(reader, writer); - } + public JsonSerializerOptions Options + { + get => BsonJsonConvention.Options; + } - stream.Position = 0; + public BsonJsonSerializer() + : this(BsonType.Undefined) + { + } - return JsonSerializer.Deserialize<T>(stream, Options); - } - } + public BsonJsonSerializer(BsonType representation) + { + if (representation is not BsonType.Undefined and not BsonType.String and not BsonType.Binary and not BsonType.Document) + { + throw new ArgumentException("Unsupported representation.", nameof(representation)); } - private static void FromBson(IBsonReader reader, Utf8JsonWriter writer) + Representation = representation; + } + + public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var reader = context.Reader; + + switch (reader.GetCurrentBsonType()) { - void ReadDocument() - { - reader.ReadStartDocument(); + case BsonType.Null: + reader.ReadNull(); + return null; + case BsonType.String: + var valueString = reader.ReadString(); + return JsonSerializer.Deserialize<T>(valueString, Options); + case BsonType.Binary: + var valueBinary = reader.ReadBytes(); + return JsonSerializer.Deserialize<T>(valueBinary, Options); + default: + using (var stream = DefaultPools.MemoryStream.GetStream()) { - writer.WriteStartObject(); - - while (reader.ReadBsonType() != BsonType.EndOfDocument) + using (var writer = new Utf8JsonWriter(stream)) { - Read(); + FromBson(reader, writer); } - writer.WriteEndObject(); - } + stream.Position = 0; - reader.ReadEndDocument(); - } + return JsonSerializer.Deserialize<T>(stream, Options); + } + } + } - void ReadArray() + private static void FromBson(IBsonReader reader, Utf8JsonWriter writer) + { + void ReadDocument() + { + reader.ReadStartDocument(); { - reader.ReadStartArray(); - { - writer.WriteStartArray(); + writer.WriteStartObject(); - while (reader.ReadBsonType() != BsonType.EndOfDocument) - { - Read(); - } - - writer.WriteEndArray(); + while (reader.ReadBsonType() != BsonType.EndOfDocument) + { + Read(); } - reader.ReadEndArray(); + writer.WriteEndObject(); } - void Read() + reader.ReadEndDocument(); + } + + void ReadArray() + { + reader.ReadStartArray(); { - switch (reader.State) + writer.WriteStartArray(); + + while (reader.ReadBsonType() != BsonType.EndOfDocument) { - case BsonReaderState.Initial: - case BsonReaderState.Type: - reader.ReadBsonType(); - Read(); - break; - case BsonReaderState.Name: - writer.WritePropertyName(reader.ReadName().BsonToJsonName()); - Read(); - break; - case BsonReaderState.Value: - switch (reader.CurrentBsonType) - { - case BsonType.Null: - reader.ReadNull(); - writer.WriteNullValue(); - break; - case BsonType.Binary: - var valueBinary = reader.ReadBinaryData(); - writer.WriteBase64StringValue(valueBinary.Bytes.AsSpan()); - break; - case BsonType.Boolean: - var valueBoolean = reader.ReadBoolean(); - writer.WriteBooleanValue(valueBoolean); - break; - case BsonType.Int32: - var valueInt32 = reader.ReadInt32(); - writer.WriteNumberValue(valueInt32); - break; - case BsonType.Int64: - var valueInt64 = reader.ReadInt64(); - writer.WriteNumberValue(valueInt64); - break; - case BsonType.Double: - var valueDouble = reader.ReadDouble(); - writer.WriteNumberValue(valueDouble); - break; - case BsonType.String: - var valueString = reader.ReadString(); - writer.WriteStringValue(valueString); - break; - case BsonType.Array: - ReadArray(); - break; - case BsonType.Document: - ReadDocument(); - break; - default: - throw new NotSupportedException(); - } - - break; - case BsonReaderState.Done: - break; - case BsonReaderState.Closed: - break; + Read(); } + + writer.WriteEndArray(); } - Read(); + reader.ReadEndArray(); } - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) + void Read() { - var writer = context.Writer; - - switch (ActualRepresentation) + switch (reader.State) { - case BsonType.String: - var jsonString = JsonSerializer.Serialize(value, args.NominalType, Options); - writer.WriteString(jsonString); + case BsonReaderState.Initial: + case BsonReaderState.Type: + reader.ReadBsonType(); + Read(); break; - case BsonType.Binary: - var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(value, args.NominalType, Options); - writer.WriteBytes(jsonBytes); + case BsonReaderState.Name: + writer.WritePropertyName(reader.ReadName().BsonToJsonName()); + Read(); break; - default: - using (var jsonDocument = JsonSerializer.SerializeToDocument(value, args.NominalType, Options)) + case BsonReaderState.Value: + switch (reader.CurrentBsonType) { - WriteElement(writer, jsonDocument.RootElement); + case BsonType.Null: + reader.ReadNull(); + writer.WriteNullValue(); + break; + case BsonType.Binary: + var valueBinary = reader.ReadBinaryData(); + writer.WriteBase64StringValue(valueBinary.Bytes.AsSpan()); + break; + case BsonType.Boolean: + var valueBoolean = reader.ReadBoolean(); + writer.WriteBooleanValue(valueBoolean); + break; + case BsonType.Int32: + var valueInt32 = reader.ReadInt32(); + writer.WriteNumberValue(valueInt32); + break; + case BsonType.Int64: + var valueInt64 = reader.ReadInt64(); + writer.WriteNumberValue(valueInt64); + break; + case BsonType.Double: + var valueDouble = reader.ReadDouble(); + writer.WriteNumberValue(valueDouble); + break; + case BsonType.String: + var valueString = reader.ReadString(); + writer.WriteStringValue(valueString); + break; + case BsonType.Array: + ReadArray(); + break; + case BsonType.Document: + ReadDocument(); + break; + default: + throw new NotSupportedException(); } + break; + case BsonReaderState.Done: + break; + case BsonReaderState.Closed: break; } } - private static void WriteElement(IBsonWriter writer, JsonElement element) + Read(); + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) + { + var writer = context.Writer; + + switch (ActualRepresentation) { - switch (element.ValueKind) - { - case JsonValueKind.Null: - writer.WriteNull(); - break; - case JsonValueKind.String: - writer.WriteString(element.GetString()); - break; - case JsonValueKind.Number: - writer.WriteDouble(element.GetDouble()); - break; - case JsonValueKind.True: - writer.WriteBoolean(true); - break; - case JsonValueKind.False: - writer.WriteBoolean(false); - break; - case JsonValueKind.Array: - writer.WriteStartArray(); + case BsonType.String: + var jsonString = JsonSerializer.Serialize(value, args.NominalType, Options); + writer.WriteString(jsonString); + break; + case BsonType.Binary: + var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(value, args.NominalType, Options); + writer.WriteBytes(jsonBytes); + break; + default: + using (var jsonDocument = JsonSerializer.SerializeToDocument(value, args.NominalType, Options)) + { + WriteElement(writer, jsonDocument.RootElement); + } - foreach (var item in element.EnumerateArray()) - { - WriteElement(writer, item); - } + break; + } + } - writer.WriteEndArray(); - break; - case JsonValueKind.Object: - writer.WriteStartDocument(); + private static void WriteElement(IBsonWriter writer, JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Null: + writer.WriteNull(); + break; + case JsonValueKind.String: + writer.WriteString(element.GetString()); + break; + case JsonValueKind.Number: + writer.WriteDouble(element.GetDouble()); + break; + case JsonValueKind.True: + writer.WriteBoolean(true); + break; + case JsonValueKind.False: + writer.WriteBoolean(false); + break; + case JsonValueKind.Array: + writer.WriteStartArray(); + + foreach (var item in element.EnumerateArray()) + { + WriteElement(writer, item); + } - foreach (var property in element.EnumerateObject()) - { - writer.WriteName(property.Name.JsonToBsonName()); + writer.WriteEndArray(); + break; + case JsonValueKind.Object: + writer.WriteStartDocument(); - WriteElement(writer, property.Value); - } + foreach (var property in element.EnumerateObject()) + { + writer.WriteName(property.Name.JsonToBsonName()); - writer.WriteEndDocument(); - break; - default: - ThrowHelper.NotSupportedException(); - break; - } - } + WriteElement(writer, property.Value); + } - public BsonJsonSerializer<T> WithRepresentation(BsonType representation) - { - return Representation == representation ? this : new BsonJsonSerializer<T>(representation); + writer.WriteEndDocument(); + break; + default: + ThrowHelper.NotSupportedException(); + break; } + } - IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) - { - return WithRepresentation(representation); - } + public BsonJsonSerializer<T> WithRepresentation(BsonType representation) + { + return Representation == representation ? this : new BsonJsonSerializer<T>(representation); + } + + IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) + { + return WithRepresentation(representation); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs index d3d1378206..f31c547147 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs @@ -10,82 +10,81 @@ using MongoDB.Bson.Serialization.Serializers; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public sealed class BsonJsonValueSerializer : SerializerBase<JsonValue> { - public sealed class BsonJsonValueSerializer : SerializerBase<JsonValue> + public static void Register() { - public static void Register() + try { - try - { - BsonSerializer.RegisterSerializer(new BsonJsonValueSerializer()); - } - catch (BsonSerializationException) - { - return; - } + BsonSerializer.RegisterSerializer(new BsonJsonValueSerializer()); } - - public override JsonValue Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + catch (BsonSerializationException) { - var reader = context.Reader; - - switch (reader.CurrentBsonType) - { - case BsonType.Undefined: - reader.ReadUndefined(); - return JsonValue.Null; - case BsonType.Null: - reader.ReadNull(); - return JsonValue.Null; - case BsonType.Boolean: - return reader.ReadBoolean(); - case BsonType.Double: - return reader.ReadDouble(); - case BsonType.Int32: - return reader.ReadInt32(); - case BsonType.Int64: - return reader.ReadInt64(); - case BsonType.String: - return reader.ReadString(); - case BsonType.Array: - return BsonSerializer.Deserialize<JsonArray>(reader); - case BsonType.Document: - return BsonSerializer.Deserialize<JsonObject>(reader); - default: - ThrowHelper.NotSupportedException("Unsupported Representation."); - return default!; - } + return; } + } + + public override JsonValue Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var reader = context.Reader; - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JsonValue value) + switch (reader.CurrentBsonType) { - var writer = context.Writer; + case BsonType.Undefined: + reader.ReadUndefined(); + return JsonValue.Null; + case BsonType.Null: + reader.ReadNull(); + return JsonValue.Null; + case BsonType.Boolean: + return reader.ReadBoolean(); + case BsonType.Double: + return reader.ReadDouble(); + case BsonType.Int32: + return reader.ReadInt32(); + case BsonType.Int64: + return reader.ReadInt64(); + case BsonType.String: + return reader.ReadString(); + case BsonType.Array: + return BsonSerializer.Deserialize<JsonArray>(reader); + case BsonType.Document: + return BsonSerializer.Deserialize<JsonObject>(reader); + default: + ThrowHelper.NotSupportedException("Unsupported Representation."); + return default!; + } + } - switch (value.Value) - { - case null: - writer.WriteNull(); - break; - case bool b: - writer.WriteBoolean(b); - break; - case string s: - writer.WriteString(s); - break; - case double n: - writer.WriteDouble(n); - break; - case JsonArray a: - BsonSerializer.Serialize(writer, a); - break; - case JsonObject o: - BsonSerializer.Serialize(writer, o); - break; - default: - ThrowHelper.NotSupportedException(); - break; - } + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JsonValue value) + { + var writer = context.Writer; + + switch (value.Value) + { + case null: + writer.WriteNull(); + break; + case bool b: + writer.WriteBoolean(b); + break; + case string s: + writer.WriteString(s); + break; + case double n: + writer.WriteDouble(n); + break; + case JsonArray a: + BsonSerializer.Serialize(writer, a); + break; + case JsonObject o: + BsonSerializer.Serialize(writer, o); + break; + default: + ThrowHelper.NotSupportedException(); + break; } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs index 42518e6344..5b958132f3 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs @@ -10,52 +10,51 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public sealed class BsonStringSerializer<T> : SerializerBase<T> { - public sealed class BsonStringSerializer<T> : SerializerBase<T> - { - private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(T)); + private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(T)); - public static void Register() + public static void Register() + { + try + { + BsonSerializer.RegisterSerializer(new BsonStringSerializer<T>()); + } + catch (BsonSerializationException) { - try - { - BsonSerializer.RegisterSerializer(new BsonStringSerializer<T>()); - } - catch (BsonSerializationException) - { - return; - } + return; } + } + + public override T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + if (context.Reader.CurrentBsonType == BsonType.Null) + { + context.Reader.ReadNull(); - public override T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + return default!; + } + else { - if (context.Reader.CurrentBsonType == BsonType.Null) - { - context.Reader.ReadNull(); - - return default!; - } - else - { - var value = context.Reader.ReadString(); - - return (T)typeConverter.ConvertFromInvariantString(value)!; - } + var value = context.Reader.ReadString(); + + return (T)typeConverter.ConvertFromInvariantString(value)!; } + } - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T value) + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T value) + { + var text = value?.ToString(); + + if (text != null) + { + context.Writer.WriteString(text); + } + else { - var text = value?.ToString(); - - if (text != null) - { - context.Writer.WriteString(text); - } - else - { - context.Writer.WriteNull(); - } + context.Writer.WriteNull(); } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Field.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Field.cs index ef0a283eaf..2e11601392 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Field.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Field.cs @@ -7,31 +7,30 @@ using MongoDB.Bson.Serialization; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public static class Field { - public static class Field + public static string Of<T>(Func<T, string> mapper) { - public static string Of<T>(Func<T, string> mapper) - { - var name = mapper(default!); + var name = mapper(default!); - var classMap = BsonClassMap.LookupClassMap(typeof(T)); - - // The class map does not contain all inherited members, therefore we have to loop over the hierarchy. - while (classMap != null) - { - var member = classMap.GetMemberMap(name); + var classMap = BsonClassMap.LookupClassMap(typeof(T)); - if (member != null) - { - return member.ElementName; - } + // The class map does not contain all inherited members, therefore we have to loop over the hierarchy. + while (classMap != null) + { + var member = classMap.GetMemberMap(name); - classMap = classMap.BaseClassMap; + if (member != null) + { + return member.ElementName; } - ThrowHelper.InvalidOperationException($"Cannot find member '{name}' in type '{typeof(T)}."); - return null!; + classMap = classMap.BaseClassMap; } + + ThrowHelper.InvalidOperationException($"Cannot find member '{name}' in type '{typeof(T)}."); + return null!; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs index d972aaa2aa..8d5c7f724b 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/IVersionedEntity.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public interface IVersionedEntity<T> { - public interface IVersionedEntity<T> - { - T DocumentId { get; set; } + T DocumentId { get; set; } - long Version { get; set; } - } + long Version { get; set; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs index 1d9c48f21b..46d9ca0cd5 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs @@ -11,49 +11,48 @@ #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public abstract class MongoBase<TEntity> { - public abstract class MongoBase<TEntity> - { - protected static readonly FilterDefinitionBuilder<TEntity> Filter = - Builders<TEntity>.Filter; + protected static readonly FilterDefinitionBuilder<TEntity> Filter = + Builders<TEntity>.Filter; - protected static readonly IndexKeysDefinitionBuilder<TEntity> Index = - Builders<TEntity>.IndexKeys; + protected static readonly IndexKeysDefinitionBuilder<TEntity> Index = + Builders<TEntity>.IndexKeys; - protected static readonly ProjectionDefinitionBuilder<TEntity> Projection = - Builders<TEntity>.Projection; + protected static readonly ProjectionDefinitionBuilder<TEntity> Projection = + Builders<TEntity>.Projection; - protected static readonly SortDefinitionBuilder<TEntity> Sort = - Builders<TEntity>.Sort; + protected static readonly SortDefinitionBuilder<TEntity> Sort = + Builders<TEntity>.Sort; - protected static readonly UpdateDefinitionBuilder<TEntity> Update = - Builders<TEntity>.Update; + protected static readonly UpdateDefinitionBuilder<TEntity> Update = + Builders<TEntity>.Update; - protected static readonly BulkWriteOptions BulkUnordered = - new BulkWriteOptions { IsOrdered = true }; + protected static readonly BulkWriteOptions BulkUnordered = + new BulkWriteOptions { IsOrdered = true }; - protected static readonly InsertManyOptions InsertUnordered = - new InsertManyOptions { IsOrdered = true }; + protected static readonly InsertManyOptions InsertUnordered = + new InsertManyOptions { IsOrdered = true }; - protected static readonly ReplaceOptions UpsertReplace = - new ReplaceOptions { IsUpsert = true }; + protected static readonly ReplaceOptions UpsertReplace = + new ReplaceOptions { IsUpsert = true }; - protected static readonly UpdateOptions Upsert = - new UpdateOptions { IsUpsert = true }; + protected static readonly UpdateOptions Upsert = + new UpdateOptions { IsUpsert = true }; - protected static readonly BsonDocument FindAll = - new BsonDocument(); + protected static readonly BsonDocument FindAll = + new BsonDocument(); - static MongoBase() - { - BsonDefaultConventions.Register(); - BsonDomainIdSerializer.Register(); - BsonEscapedDictionarySerializer<JsonValue, JsonObject>.Register(); - BsonInstantSerializer.Register(); - BsonJsonConvention.Register(); - BsonJsonValueSerializer.Register(); - BsonStringSerializer<RefToken>.Register(); - } + static MongoBase() + { + BsonDefaultConventions.Register(); + BsonDomainIdSerializer.Register(); + BsonEscapedDictionarySerializer<JsonValue, JsonObject>.Register(); + BsonInstantSerializer.Register(); + BsonJsonConvention.Register(); + BsonJsonValueSerializer.Register(); + BsonStringSerializer<RefToken>.Register(); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbErrorCodes.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbErrorCodes.cs index 68512d03f0..b974f2b255 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbErrorCodes.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbErrorCodes.cs @@ -7,10 +7,9 @@ #pragma warning disable SA1310 // Field names should not contain underscore -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public static class MongoDbErrorCodes { - public static class MongoDbErrorCodes - { - public const int Errror16755_InvalidGeoData = 16755; - } + public const int Errror16755_InvalidGeoData = 16755; } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index 9e6b4473c0..b13d80ebfe 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -12,246 +12,245 @@ using MongoDB.Driver; using Squidex.Infrastructure.States; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public static class MongoExtensions { - public static class MongoExtensions - { - private static readonly ReplaceOptions UpsertReplace = new ReplaceOptions { IsUpsert = true }; + private static readonly ReplaceOptions UpsertReplace = new ReplaceOptions { IsUpsert = true }; - public static async Task<bool> CollectionExistsAsync(this IMongoDatabase database, string collectionName, - CancellationToken ct = default) + public static async Task<bool> CollectionExistsAsync(this IMongoDatabase database, string collectionName, + CancellationToken ct = default) + { + var options = new ListCollectionNamesOptions { - var options = new ListCollectionNamesOptions - { - Filter = new BsonDocument("name", collectionName) - }; + Filter = new BsonDocument("name", collectionName) + }; - var collections = await database.ListCollectionNamesAsync(options, ct); + var collections = await database.ListCollectionNamesAsync(options, ct); - return await collections.AnyAsync(ct); - } + return await collections.AnyAsync(ct); + } - public static Task<bool> AnyAsync<T>(this IMongoCollection<T> collection, - CancellationToken ct = default) - { - var find = collection.Find(new BsonDocument()).Limit(1); + public static Task<bool> AnyAsync<T>(this IMongoCollection<T> collection, + CancellationToken ct = default) + { + var find = collection.Find(new BsonDocument()).Limit(1); - return find.AnyAsync(ct); - } + return find.AnyAsync(ct); + } - public static async Task<bool> InsertOneIfNotExistsAsync<T>(this IMongoCollection<T> collection, T document, - CancellationToken ct = default) + public static async Task<bool> InsertOneIfNotExistsAsync<T>(this IMongoCollection<T> collection, T document, + CancellationToken ct = default) + { + try { - try - { - await collection.InsertOneAsync(document, null, ct); - } - catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) - { - return false; - } - - return true; + await collection.InsertOneAsync(document, null, ct); } - - public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IFindFluent<T, T> find, - [EnumeratorCancellation] CancellationToken ct = default) + catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { - var cursor = await find.ToCursorAsync(ct); + return false; + } + + return true; + } - while (await cursor.MoveNextAsync(ct)) + public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IFindFluent<T, T> find, + [EnumeratorCancellation] CancellationToken ct = default) + { + var cursor = await find.ToCursorAsync(ct); + + while (await cursor.MoveNextAsync(ct)) + { + foreach (var item in cursor.Current) { - foreach (var item in cursor.Current) - { - ct.ThrowIfCancellationRequested(); + ct.ThrowIfCancellationRequested(); - yield return item; - } + yield return item; } } + } - public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find, - Expression<Func<T, object>> include) - { - return find.Project<BsonDocument>(Builders<T>.Projection.Include(include)); - } + public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find, + Expression<Func<T, object>> include) + { + return find.Project<BsonDocument>(Builders<T>.Projection.Include(include)); + } - public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find, - Expression<Func<T, object>> include1, - Expression<Func<T, object>> include2) - { - return find.Project<BsonDocument>(Builders<T>.Projection.Include(include1).Include(include2)); - } + public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find, + Expression<Func<T, object>> include1, + Expression<Func<T, object>> include2) + { + return find.Project<BsonDocument>(Builders<T>.Projection.Include(include1).Include(include2)); + } - public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find, - Expression<Func<T, object>> include1, - Expression<Func<T, object>> include2, - Expression<Func<T, object>> include3) - { - return find.Project<BsonDocument>(Builders<T>.Projection.Include(include1).Include(include2).Include(include3)); - } + public static IFindFluent<T, BsonDocument> Only<T>(this IFindFluent<T, T> find, + Expression<Func<T, object>> include1, + Expression<Func<T, object>> include2, + Expression<Func<T, object>> include3) + { + return find.Project<BsonDocument>(Builders<T>.Projection.Include(include1).Include(include2).Include(include3)); + } - public static IFindFluent<T, T> Not<T>(this IFindFluent<T, T> find, - Expression<Func<T, object>> exclude) - { - return find.Project<T>(Builders<T>.Projection.Exclude(exclude)); - } + public static IFindFluent<T, T> Not<T>(this IFindFluent<T, T> find, + Expression<Func<T, object>> exclude) + { + return find.Project<T>(Builders<T>.Projection.Exclude(exclude)); + } - public static IFindFluent<T, T> Not<T>(this IFindFluent<T, T> find, - Expression<Func<T, object>> exclude1, - Expression<Func<T, object>> exclude2) - { - return find.Project<T>(Builders<T>.Projection.Exclude(exclude1).Exclude(exclude2)); - } + public static IFindFluent<T, T> Not<T>(this IFindFluent<T, T> find, + Expression<Func<T, object>> exclude1, + Expression<Func<T, object>> exclude2) + { + return find.Project<T>(Builders<T>.Projection.Exclude(exclude1).Exclude(exclude2)); + } - public static long ToLong(this BsonValue value) + public static long ToLong(this BsonValue value) + { + switch (value.BsonType) { - switch (value.BsonType) - { - case BsonType.Int32: - return value.AsInt32; - case BsonType.Int64: - return value.AsInt64; - case BsonType.Double: - return (long)value.AsDouble; - default: - throw new InvalidCastException($"Cannot cast from {value.BsonType} to long."); - } + case BsonType.Int32: + return value.AsInt32; + case BsonType.Int64: + return value.AsInt64; + case BsonType.Double: + return (long)value.AsDouble; + default: + throw new InvalidCastException($"Cannot cast from {value.BsonType} to long."); } + } + + public static async Task<bool> UpsertVersionedAsync<T>(this IMongoCollection<T> collection, IClientSessionHandle session, SnapshotWriteJob<T> job, + CancellationToken ct = default) + where T : IVersionedEntity<DomainId> + { + var field2 = Field.Of<T>(x => nameof(x.Version)); - public static async Task<bool> UpsertVersionedAsync<T>(this IMongoCollection<T> collection, IClientSessionHandle session, SnapshotWriteJob<T> job, - CancellationToken ct = default) - where T : IVersionedEntity<DomainId> + var (key, snapshot, newVersion, oldVersion) = job; + try { - var field2 = Field.Of<T>(x => nameof(x.Version)); + snapshot.DocumentId = key; + snapshot.Version = newVersion; - var (key, snapshot, newVersion, oldVersion) = job; - try - { - snapshot.DocumentId = key; - snapshot.Version = newVersion; + Expression<Func<T, bool>> filter = + oldVersion > EtagVersion.Any ? + x => x.DocumentId.Equals(key) && x.Version == oldVersion : + x => x.DocumentId.Equals(key); - Expression<Func<T, bool>> filter = - oldVersion > EtagVersion.Any ? - x => x.DocumentId.Equals(key) && x.Version == oldVersion : - x => x.DocumentId.Equals(key); + var result = await collection.ReplaceOneAsync(session, filter, job.Value, UpsertReplace, ct); - var result = await collection.ReplaceOneAsync(session, filter, job.Value, UpsertReplace, ct); + return result.IsAcknowledged && result.ModifiedCount == 1; + } + catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await collection.Find(session, x => x.DocumentId.Equals(key)).Only(x => x.DocumentId, x => x.Version) + .FirstOrDefaultAsync(ct); + + if (existingVersion != null) + { + var field = Field.Of<T>(x => nameof(x.Version)); - return result.IsAcknowledged && result.ModifiedCount == 1; + throw new InconsistentStateException(existingVersion[field].AsInt64, oldVersion); } - catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + else { - var existingVersion = - await collection.Find(session, x => x.DocumentId.Equals(key)).Only(x => x.DocumentId, x => x.Version) - .FirstOrDefaultAsync(ct); - - if (existingVersion != null) - { - var field = Field.Of<T>(x => nameof(x.Version)); - - throw new InconsistentStateException(existingVersion[field].AsInt64, oldVersion); - } - else - { - throw new InconsistentStateException(EtagVersion.Any, oldVersion); - } + throw new InconsistentStateException(EtagVersion.Any, oldVersion); } } + } + + public static async Task<bool> UpsertVersionedAsync<T>(this IMongoCollection<T> collection, SnapshotWriteJob<T> job, + CancellationToken ct = default) + where T : IVersionedEntity<DomainId> + { + var field2 = Field.Of<T>(x => nameof(x.Version)); - public static async Task<bool> UpsertVersionedAsync<T>(this IMongoCollection<T> collection, SnapshotWriteJob<T> job, - CancellationToken ct = default) - where T : IVersionedEntity<DomainId> + var (key, snapshot, newVersion, oldVersion) = job; + try { - var field2 = Field.Of<T>(x => nameof(x.Version)); + snapshot.DocumentId = key; + snapshot.Version = newVersion; - var (key, snapshot, newVersion, oldVersion) = job; - try - { - snapshot.DocumentId = key; - snapshot.Version = newVersion; + Expression<Func<T, bool>> filter = + oldVersion > EtagVersion.Any ? + x => x.DocumentId.Equals(key) && x.Version == oldVersion : + x => x.DocumentId.Equals(key); + + var result = await collection.ReplaceOneAsync(filter, snapshot, UpsertReplace, ct); - Expression<Func<T, bool>> filter = - oldVersion > EtagVersion.Any ? - x => x.DocumentId.Equals(key) && x.Version == oldVersion : - x => x.DocumentId.Equals(key); + return result.IsAcknowledged && result.ModifiedCount == 1; + } + catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await collection.Find(x => x.DocumentId.Equals(key)).Only(x => x.DocumentId, x => x.Version) + .FirstOrDefaultAsync(ct); - var result = await collection.ReplaceOneAsync(filter, snapshot, UpsertReplace, ct); + if (existingVersion != null) + { + var field = Field.Of<T>(x => nameof(x.Version)); - return result.IsAcknowledged && result.ModifiedCount == 1; + throw new InconsistentStateException(existingVersion[field].AsInt64, oldVersion); } - catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + else { - var existingVersion = - await collection.Find(x => x.DocumentId.Equals(key)).Only(x => x.DocumentId, x => x.Version) - .FirstOrDefaultAsync(ct); - - if (existingVersion != null) - { - var field = Field.Of<T>(x => nameof(x.Version)); - - throw new InconsistentStateException(existingVersion[field].AsInt64, oldVersion); - } - else - { - throw new InconsistentStateException(EtagVersion.Any, oldVersion); - } + throw new InconsistentStateException(EtagVersion.Any, oldVersion); } } + } - public static async Task<int> GetMajorVersionAsync(this IMongoDatabase database, - CancellationToken ct = default) - { - var command = - new BsonDocumentCommand<BsonDocument>(new BsonDocument - { - { "buildInfo", 1 } - }); + public static async Task<int> GetMajorVersionAsync(this IMongoDatabase database, + CancellationToken ct = default) + { + var command = + new BsonDocumentCommand<BsonDocument>(new BsonDocument + { + { "buildInfo", 1 } + }); - var document = await database.RunCommandAsync(command, cancellationToken: ct); + var document = await database.RunCommandAsync(command, cancellationToken: ct); - var versionString = document["version"].AsString; - var versionMajor = versionString.Split('.')[0]; + var versionString = document["version"].AsString; + var versionMajor = versionString.Split('.')[0]; - int.TryParse(versionMajor, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result); + int.TryParse(versionMajor, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result); - return result; - } + return result; + } - public static async Task<List<T>> ToListRandomAsync<T>(this IFindFluent<T, T> find, IMongoCollection<T> collection, long take, - CancellationToken ct = default) + public static async Task<List<T>> ToListRandomAsync<T>(this IFindFluent<T, T> find, IMongoCollection<T> collection, long take, + CancellationToken ct = default) + { + if (take <= 0) { - if (take <= 0) - { - return await find.ToListAsync(ct); - } + return await find.ToListAsync(ct); + } - var idDocuments = await find.Project<BsonDocument>(Builders<T>.Projection.Include("_id")).ToListAsync(ct); - var idValues = idDocuments.Select(x => x["_id"]); + var idDocuments = await find.Project<BsonDocument>(Builders<T>.Projection.Include("_id")).ToListAsync(ct); + var idValues = idDocuments.Select(x => x["_id"]); - var randomIds = idValues.TakeRandom(take); + var randomIds = idValues.TakeRandom(take); - var documents = await collection.Find(Builders<T>.Filter.In("_id", randomIds)).ToListAsync(ct); + var documents = await collection.Find(Builders<T>.Filter.In("_id", randomIds)).ToListAsync(ct); - return documents.Shuffle().ToList(); - } + return documents.Shuffle().ToList(); + } - public static async Task<List<T>> ToListRandomAsync<T>(this IAggregateFluent<T> find, IMongoCollection<T> collection, long take, - CancellationToken ct = default) + public static async Task<List<T>> ToListRandomAsync<T>(this IAggregateFluent<T> find, IMongoCollection<T> collection, long take, + CancellationToken ct = default) + { + if (take <= 0) { - if (take <= 0) - { - return await find.ToListAsync(ct); - } + return await find.ToListAsync(ct); + } - var idDocuments = await find.Project<BsonDocument>(Builders<T>.Projection.Include("_id")).ToListAsync(ct); - var idValues = idDocuments.Select(x => x["_id"]); + var idDocuments = await find.Project<BsonDocument>(Builders<T>.Projection.Include("_id")).ToListAsync(ct); + var idValues = idDocuments.Select(x => x["_id"]); - var randomIds = idValues.TakeRandom(take); + var randomIds = idValues.TakeRandom(take); - var documents = await collection.Find(Builders<T>.Filter.In("_id", randomIds)).ToListAsync(ct); + var documents = await collection.Find(Builders<T>.Filter.In("_id", randomIds)).ToListAsync(ct); - return documents.Shuffle().ToList(); - } + return documents.Shuffle().ToList(); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 36513d7711..2383447615 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -10,102 +10,101 @@ using Squidex.Hosting; using Squidex.Hosting.Configuration; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public abstract class MongoRepositoryBase<T> : MongoBase<T>, IInitializable { - public abstract class MongoRepositoryBase<T> : MongoBase<T>, IInitializable - { - private readonly IMongoDatabase mongoDatabase; - private IMongoCollection<T> mongoCollection; + private readonly IMongoDatabase mongoDatabase; + private IMongoCollection<T> mongoCollection; - protected IMongoCollection<T> Collection + protected IMongoCollection<T> Collection + { + get { - get + if (mongoCollection == null) { - if (mongoCollection == null) - { - InitializeAsync(default).Wait(); - } - - if (mongoCollection == null) - { - ThrowHelper.InvalidOperationException("Collection has not been initialized yet."); - return default!; - } - - return mongoCollection; + InitializeAsync(default).Wait(); } - } - protected IMongoDatabase Database - { - get => mongoDatabase; + if (mongoCollection == null) + { + ThrowHelper.InvalidOperationException("Collection has not been initialized yet."); + return default!; + } + + return mongoCollection; } + } - protected MongoRepositoryBase(IMongoDatabase database) - { - Guard.NotNull(database); + protected IMongoDatabase Database + { + get => mongoDatabase; + } - mongoDatabase = database; - } + protected MongoRepositoryBase(IMongoDatabase database) + { + Guard.NotNull(database); - protected virtual MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings(); - } + mongoDatabase = database; + } - protected virtual string CollectionName() - { - return string.Format(CultureInfo.InvariantCulture, "{0}Set", typeof(T).Name); - } + protected virtual MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings(); + } - protected virtual Task SetupCollectionAsync(IMongoCollection<T> collection, - CancellationToken ct) + protected virtual string CollectionName() + { + return string.Format(CultureInfo.InvariantCulture, "{0}Set", typeof(T).Name); + } + + protected virtual Task SetupCollectionAsync(IMongoCollection<T> collection, + CancellationToken ct) + { + return Task.CompletedTask; + } + + public virtual async Task ClearAsync( + CancellationToken ct = default) + { + try { - return Task.CompletedTask; + await Database.DropCollectionAsync(CollectionName(), ct); } - - public virtual async Task ClearAsync( - CancellationToken ct = default) + catch (MongoCommandException ex) { - try + if (ex.Code != 26) { - await Database.DropCollectionAsync(CollectionName(), ct); + throw; } - catch (MongoCommandException ex) - { - if (ex.Code != 26) - { - throw; - } - } - - await InitializeAsync(ct); } - public async Task InitializeAsync( - CancellationToken ct) + await InitializeAsync(ct); + } + + public async Task InitializeAsync( + CancellationToken ct) + { + try { - try - { - CreateCollection(); + CreateCollection(); - await SetupCollectionAsync(Collection, ct); - } - catch (Exception ex) - { - var databaseName = Database.DatabaseNamespace.DatabaseName; + await SetupCollectionAsync(Collection, ct); + } + catch (Exception ex) + { + var databaseName = Database.DatabaseNamespace.DatabaseName; - var error = new ConfigurationError($"MongoDb connection failed to connect to database {databaseName}."); + var error = new ConfigurationError($"MongoDb connection failed to connect to database {databaseName}."); - throw new ConfigurationException(error, ex); - } + throw new ConfigurationException(error, ex); } + } - private void CreateCollection() - { - mongoCollection = mongoDatabase.GetCollection<T>( - CollectionName(), - CollectionSettings() ?? new MongoCollectionSettings()); - } + private void CreateCollection() + { + mongoCollection = mongoDatabase.GetCollection<T>( + CollectionName(), + CollectionSettings() ?? new MongoCollectionSettings()); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs index 04a8558127..e22b670faa 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs @@ -10,33 +10,32 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Infrastructure.MongoDb.Queries +namespace Squidex.Infrastructure.MongoDb.Queries; + +public static class FilterBuilder { - public static class FilterBuilder + public static (FilterDefinition<TDocument>? Filter, bool Last) BuildFilter<TDocument>(this ClrQuery query, bool supportsSearch = true) { - public static (FilterDefinition<TDocument>? Filter, bool Last) BuildFilter<TDocument>(this ClrQuery query, bool supportsSearch = true) + if (query.FullText != null) { - if (query.FullText != null) - { - if (!supportsSearch) - { - throw new ValidationException(T.Get("common.fullTextNotSupported")); - } - - return (Builders<TDocument>.Filter.Text(query.FullText), false); - } - - if (query.Filter != null) + if (!supportsSearch) { - return (query.Filter.BuildFilter<TDocument>(), true); + throw new ValidationException(T.Get("common.fullTextNotSupported")); } - return (null, false); + return (Builders<TDocument>.Filter.Text(query.FullText), false); } - public static FilterDefinition<T> BuildFilter<T>(this FilterNode<ClrValue> filterNode) + if (query.Filter != null) { - return FilterVisitor<T>.Visit(filterNode); + return (query.Filter.BuildFilter<TDocument>(), true); } + + return (null, false); + } + + public static FilterDefinition<T> BuildFilter<T>(this FilterNode<ClrValue> filterNode) + { + return FilterVisitor<T>.Visit(filterNode); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs index bf6f95f633..33425c789d 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs @@ -11,114 +11,113 @@ using MongoDB.Driver; using Squidex.Infrastructure.Queries; -namespace Squidex.Infrastructure.MongoDb.Queries +namespace Squidex.Infrastructure.MongoDb.Queries; + +public sealed class FilterVisitor<T> : FilterNodeVisitor<FilterDefinition<T>, ClrValue, None> { - public sealed class FilterVisitor<T> : FilterNodeVisitor<FilterDefinition<T>, ClrValue, None> + private static readonly FilterDefinitionBuilder<T> Filter = Builders<T>.Filter; + private static readonly FilterVisitor<T> Instance = new FilterVisitor<T>(); + + private FilterVisitor() { - private static readonly FilterDefinitionBuilder<T> Filter = Builders<T>.Filter; - private static readonly FilterVisitor<T> Instance = new FilterVisitor<T>(); + } - private FilterVisitor() - { - } + public static FilterDefinition<T> Visit(FilterNode<ClrValue> node) + { + return node.Accept(Instance, None.Value); + } - public static FilterDefinition<T> Visit(FilterNode<ClrValue> node) - { - return node.Accept(Instance, None.Value); - } + public override FilterDefinition<T> Visit(NegateFilter<ClrValue> nodeIn, None args) + { + return Filter.Not(nodeIn.Filter.Accept(this, args)); + } - public override FilterDefinition<T> Visit(NegateFilter<ClrValue> nodeIn, None args) + public override FilterDefinition<T> Visit(LogicalFilter<ClrValue> nodeIn, None args) + { + if (nodeIn.Type == LogicalFilterType.And) { - return Filter.Not(nodeIn.Filter.Accept(this, args)); + return Filter.And(nodeIn.Filters.Select(x => x.Accept(this, args))); } - - public override FilterDefinition<T> Visit(LogicalFilter<ClrValue> nodeIn, None args) + else { - if (nodeIn.Type == LogicalFilterType.And) - { - return Filter.And(nodeIn.Filters.Select(x => x.Accept(this, args))); - } - else - { - return Filter.Or(nodeIn.Filters.Select(x => x.Accept(this, args))); - } + return Filter.Or(nodeIn.Filters.Select(x => x.Accept(this, args))); } + } - public override FilterDefinition<T> Visit(CompareFilter<ClrValue> nodeIn, None args) + public override FilterDefinition<T> Visit(CompareFilter<ClrValue> nodeIn, None args) + { + var propertyName = nodeIn.Path.ToString(); + + var value = nodeIn.Value.Value; + + switch (nodeIn.Operator) { - var propertyName = nodeIn.Path.ToString(); + case CompareOperator.Empty: + return Filter.Or( + Filter.Exists(propertyName, false), + Filter.Eq<object?>(propertyName, null), + Filter.Eq<object?>(propertyName, string.Empty), + Filter.Size(propertyName, 0)); + case CompareOperator.Exists: + return Filter.And( + Filter.Exists(propertyName, true), + Filter.Ne<object?>(propertyName, null)); + case CompareOperator.Matchs: + return Filter.Regex(propertyName, BuildMatchRegex(nodeIn)); + case CompareOperator.StartsWith: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); + case CompareOperator.Contains: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s)); + case CompareOperator.EndsWith: + return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$")); + case CompareOperator.Equals: + return Filter.Eq(propertyName, value); + case CompareOperator.GreaterThan: + return Filter.Gt(propertyName, value); + case CompareOperator.GreaterThanOrEqual: + return Filter.Gte(propertyName, value); + case CompareOperator.LessThan: + return Filter.Lt(propertyName, value); + case CompareOperator.LessThanOrEqual: + return Filter.Lte(propertyName, value); + case CompareOperator.NotEquals: + return Filter.Ne(propertyName, value); + case CompareOperator.In: + return Filter.In(propertyName, ((IList)value!).OfType<object>()); + } - var value = nodeIn.Value.Value; + ThrowHelper.NotSupportedException(); + return default!; + } - switch (nodeIn.Operator) - { - case CompareOperator.Empty: - return Filter.Or( - Filter.Exists(propertyName, false), - Filter.Eq<object?>(propertyName, null), - Filter.Eq<object?>(propertyName, string.Empty), - Filter.Size(propertyName, 0)); - case CompareOperator.Exists: - return Filter.And( - Filter.Exists(propertyName, true), - Filter.Ne<object?>(propertyName, null)); - case CompareOperator.Matchs: - return Filter.Regex(propertyName, BuildMatchRegex(nodeIn)); - case CompareOperator.StartsWith: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); - case CompareOperator.Contains: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s)); - case CompareOperator.EndsWith: - return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$")); - case CompareOperator.Equals: - return Filter.Eq(propertyName, value); - case CompareOperator.GreaterThan: - return Filter.Gt(propertyName, value); - case CompareOperator.GreaterThanOrEqual: - return Filter.Gte(propertyName, value); - case CompareOperator.LessThan: - return Filter.Lt(propertyName, value); - case CompareOperator.LessThanOrEqual: - return Filter.Lte(propertyName, value); - case CompareOperator.NotEquals: - return Filter.Ne(propertyName, value); - case CompareOperator.In: - return Filter.In(propertyName, ((IList)value!).OfType<object>()); - } + private static BsonRegularExpression BuildMatchRegex(CompareFilter<ClrValue> node) + { + var value = node.Value.Value?.ToString(); - ThrowHelper.NotSupportedException(); - return default!; + if (value == null) + { + return new BsonRegularExpression("null", "i"); } - private static BsonRegularExpression BuildMatchRegex(CompareFilter<ClrValue> node) + if (value.Length > 3 && ((value[0] == '/' && value[^1] == '/') || value[^2] == '/')) { - var value = node.Value.Value?.ToString(); - - if (value == null) + if (value[^1] == 'i') { - return new BsonRegularExpression("null", "i"); + return new BsonRegularExpression(value[1..^2], "i"); } - - if (value.Length > 3 && ((value[0] == '/' && value[^1] == '/') || value[^2] == '/')) + else { - if (value[^1] == 'i') - { - return new BsonRegularExpression(value[1..^2], "i"); - } - else - { - return new BsonRegularExpression(value[1..^1]); - } + return new BsonRegularExpression(value[1..^1]); } - - return new BsonRegularExpression(value, "i"); } - private static BsonRegularExpression BuildRegex(CompareFilter<ClrValue> node, Func<string, string> formatter) - { - var value = formatter(Regex.Escape(node.Value.Value?.ToString() ?? "null")); + return new BsonRegularExpression(value, "i"); + } - return new BsonRegularExpression(value, "i"); - } + private static BsonRegularExpression BuildRegex(CompareFilter<ClrValue> node, Func<string, string> formatter) + { + var value = formatter(Regex.Escape(node.Value.Value?.ToString() ?? "null")); + + return new BsonRegularExpression(value, "i"); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs index 5f0c085dfe..c6fea157af 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs @@ -8,58 +8,57 @@ using MongoDB.Driver; using Squidex.Infrastructure.Queries; -namespace Squidex.Infrastructure.MongoDb.Queries +namespace Squidex.Infrastructure.MongoDb.Queries; + +public static class LimitExtensions { - public static class LimitExtensions + public static IAggregateFluent<T> QueryLimit<T>(this IAggregateFluent<T> cursor, ClrQuery query) { - public static IAggregateFluent<T> QueryLimit<T>(this IAggregateFluent<T> cursor, ClrQuery query) + if (query.Take < long.MaxValue) { - if (query.Take < long.MaxValue) - { - cursor = cursor.Limit((int)query.Take); - } - - return cursor; + cursor = cursor.Limit((int)query.Take); } - public static IFindFluent<T, T> QueryLimit<T>(this IFindFluent<T, T> cursor, ClrQuery query) - { - if (query.Take < long.MaxValue) - { - cursor = cursor.Limit((int)query.Take); - } - - return cursor; - } + return cursor; + } - public static IAggregateFluent<T> QuerySkip<T>(this IAggregateFluent<T> cursor, ClrQuery query) + public static IFindFluent<T, T> QueryLimit<T>(this IFindFluent<T, T> cursor, ClrQuery query) + { + if (query.Take < long.MaxValue) { - if (query.Skip > 0) - { - cursor = cursor.Skip((int)query.Skip); - } - - return cursor; + cursor = cursor.Limit((int)query.Take); } - public static IFindFluent<T, T> QuerySkip<T>(this IFindFluent<T, T> cursor, ClrQuery query) - { - if (query.Skip > 0) - { - cursor = cursor.Skip((int)query.Skip); - } - - return cursor; - } + return cursor; + } - public static IFindFluent<T, T> QuerySort<T>(this IFindFluent<T, T> cursor, ClrQuery query) + public static IAggregateFluent<T> QuerySkip<T>(this IAggregateFluent<T> cursor, ClrQuery query) + { + if (query.Skip > 0) { - return cursor.Sort(query.BuildSort<T>()); + cursor = cursor.Skip((int)query.Skip); } - public static IAggregateFluent<T> QuerySort<T>(this IAggregateFluent<T> cursor, ClrQuery query) + return cursor; + } + + public static IFindFluent<T, T> QuerySkip<T>(this IFindFluent<T, T> cursor, ClrQuery query) + { + if (query.Skip > 0) { - return cursor.Sort(query.BuildSort<T>()); + cursor = cursor.Skip((int)query.Skip); } + + return cursor; + } + + public static IFindFluent<T, T> QuerySort<T>(this IFindFluent<T, T> cursor, ClrQuery query) + { + return cursor.Sort(query.BuildSort<T>()); + } + + public static IAggregateFluent<T> QuerySort<T>(this IAggregateFluent<T> cursor, ClrQuery query) + { + return cursor.Sort(query.BuildSort<T>()); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs index e5fdfa9c70..a7ed38ef36 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs @@ -8,41 +8,40 @@ using MongoDB.Driver; using Squidex.Infrastructure.Queries; -namespace Squidex.Infrastructure.MongoDb.Queries +namespace Squidex.Infrastructure.MongoDb.Queries; + +public static class SortBuilder { - public static class SortBuilder + public static SortDefinition<T>? BuildSort<T>(this ClrQuery query) { - public static SortDefinition<T>? BuildSort<T>(this ClrQuery query) + if (query is { Sort: not null, Sort: { Count: > 0 } }) { - if (query is { Sort: not null, Sort: { Count: > 0 } }) - { - var sorts = query.Sort.Select(OrderBy<T>).ToList(); + var sorts = query.Sort.Select(OrderBy<T>).ToList(); - if (sorts.Count > 1) - { - return Builders<T>.Sort.Combine(sorts); - } - else - { - return sorts[0]; - } - } - - return null; - } - - public static SortDefinition<T> OrderBy<T>(SortNode sort) - { - var propertyName = string.Join(".", sort.Path); - - if (sort.Order == SortOrder.Ascending) + if (sorts.Count > 1) { - return Builders<T>.Sort.Ascending(propertyName); + return Builders<T>.Sort.Combine(sorts); } else { - return Builders<T>.Sort.Descending(propertyName); + return sorts[0]; } } + + return null; + } + + public static SortDefinition<T> OrderBy<T>(SortNode sort) + { + var propertyName = string.Join(".", sort.Path); + + if (sort.Order == SortOrder.Ascending) + { + return Builders<T>.Sort.Ascending(propertyName); + } + else + { + return Builders<T>.Sort.Descending(propertyName); + } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index ef3f14b31f..8ec2d395de 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -7,13 +7,12 @@ using MongoDB.Driver; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public sealed class MongoSnapshotStore<T> : MongoSnapshotStoreBase<T, MongoState<T>> { - public sealed class MongoSnapshotStore<T> : MongoSnapshotStoreBase<T, MongoState<T>> + public MongoSnapshotStore(IMongoDatabase database) + : base(database) { - public MongoSnapshotStore(IMongoDatabase database) - : base(database) - { - } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs index 55062cac3c..3eea392664 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs @@ -9,118 +9,117 @@ using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public abstract class MongoSnapshotStoreBase<T, TState> : MongoRepositoryBase<TState>, ISnapshotStore<T> where TState : MongoState<T>, new() { - public abstract class MongoSnapshotStoreBase<T, TState> : MongoRepositoryBase<TState>, ISnapshotStore<T> where TState : MongoState<T>, new() + protected MongoSnapshotStoreBase(IMongoDatabase database) + : base(database) { - protected MongoSnapshotStoreBase(IMongoDatabase database) - : base(database) - { - } + } - protected override string CollectionName() - { - var attribute = typeof(T).GetCustomAttributes(true).OfType<CollectionNameAttribute>().FirstOrDefault(); + protected override string CollectionName() + { + var attribute = typeof(T).GetCustomAttributes(true).OfType<CollectionNameAttribute>().FirstOrDefault(); - var name = attribute?.Name ?? typeof(T).Name; + var name = attribute?.Name ?? typeof(T).Name; - return $"States_{name}"; - } + return $"States_{name}"; + } - public async Task<SnapshotResult<T>> ReadAsync(DomainId key, - CancellationToken ct = default) + public async Task<SnapshotResult<T>> ReadAsync(DomainId key, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/ReadAsync")) { - using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/ReadAsync")) - { - var existing = - await Collection.Find(x => x.DocumentId.Equals(key)) - .FirstOrDefaultAsync(ct); + var existing = + await Collection.Find(x => x.DocumentId.Equals(key)) + .FirstOrDefaultAsync(ct); - if (existing != null) + if (existing != null) + { + if (existing.Document is IOnRead onRead) { - if (existing.Document is IOnRead onRead) - { - await onRead.OnReadAsync(); - } - - return new SnapshotResult<T>(existing.DocumentId, existing.Document, existing.Version); + await onRead.OnReadAsync(); } - return new SnapshotResult<T>(default, default!, EtagVersion.Empty); + return new SnapshotResult<T>(existing.DocumentId, existing.Document, existing.Version); } + + return new SnapshotResult<T>(default, default!, EtagVersion.Empty); } + } - public async Task WriteAsync(SnapshotWriteJob<T> job, - CancellationToken ct = default) + public async Task WriteAsync(SnapshotWriteJob<T> job, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/WriteAsync")) { - using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/WriteAsync")) - { - var entityJob = job.As(CreateDocument(job.Key, job.Value, job.OldVersion)); + var entityJob = job.As(CreateDocument(job.Key, job.Value, job.OldVersion)); - await Collection.UpsertVersionedAsync(entityJob, ct); - } + await Collection.UpsertVersionedAsync(entityJob, ct); } + } - public async Task WriteManyAsync(IEnumerable<SnapshotWriteJob<T>> jobs, - CancellationToken ct = default) + public async Task WriteManyAsync(IEnumerable<SnapshotWriteJob<T>> jobs, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/WriteManyAsync")) { - using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/WriteManyAsync")) - { - var writes = jobs.Select(x => - new ReplaceOneModel<TState>(Filter.Eq(y => y.DocumentId, x.Key), CreateDocument(x.Key, x.Value, x.NewVersion)) - { - IsUpsert = true - }).ToList(); - - if (writes.Count == 0) + var writes = jobs.Select(x => + new ReplaceOneModel<TState>(Filter.Eq(y => y.DocumentId, x.Key), CreateDocument(x.Key, x.Value, x.NewVersion)) { - return; - } + IsUpsert = true + }).ToList(); - await Collection.BulkWriteAsync(writes, BulkUnordered, ct); + if (writes.Count == 0) + { + return; } + + await Collection.BulkWriteAsync(writes, BulkUnordered, ct); } + } - public async Task RemoveAsync(DomainId key, - CancellationToken ct = default) + public async Task RemoveAsync(DomainId key, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/RemoveAsync")) { - using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/RemoveAsync")) - { - await Collection.DeleteOneAsync(x => x.DocumentId.Equals(key), ct); - } + await Collection.DeleteOneAsync(x => x.DocumentId.Equals(key), ct); } + } - public async IAsyncEnumerable<SnapshotResult<T>> ReadAllAsync( - [EnumeratorCancellation] CancellationToken ct = default) + public async IAsyncEnumerable<SnapshotResult<T>> ReadAllAsync( + [EnumeratorCancellation] CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/ReadAllAsync")) { - using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/ReadAllAsync")) - { - var find = Collection.Find(FindAll, Batching.Options); + var find = Collection.Find(FindAll, Batching.Options); - await foreach (var document in find.ToAsyncEnumerable(ct)) + await foreach (var document in find.ToAsyncEnumerable(ct)) + { + if (document.Document is IOnRead onRead) { - if (document.Document is IOnRead onRead) - { - await onRead.OnReadAsync(); - } - - yield return new SnapshotResult<T>(document.DocumentId, document.Document, document.Version, true); + await onRead.OnReadAsync(); } + + yield return new SnapshotResult<T>(document.DocumentId, document.Document, document.Version, true); } } + } - private static TState CreateDocument(DomainId id, T doc, long version) + private static TState CreateDocument(DomainId id, T doc, long version) + { + var result = new TState { - var result = new TState - { - Document = doc, - DocumentId = id, - Version = version - }; + Document = doc, + DocumentId = id, + Version = version + }; - result.Prepare(); + result.Prepare(); - return result; - } + return result; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs index 3bd5263ebd..9607192e9e 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs @@ -9,25 +9,24 @@ using MongoDB.Bson.Serialization.Attributes; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public class MongoState<T> : IVersionedEntity<DomainId> { - public class MongoState<T> : IVersionedEntity<DomainId> - { - [BsonId] - [BsonElement("_id")] - public DomainId DocumentId { get; set; } + [BsonId] + [BsonElement("_id")] + public DomainId DocumentId { get; set; } - [BsonRequired] - [BsonElement("Doc")] - [BsonJson] - public T Document { get; set; } + [BsonRequired] + [BsonElement("Doc")] + [BsonJson] + public T Document { get; set; } - [BsonRequired] - [BsonElement(nameof(Version))] - public long Version { get; set; } + [BsonRequired] + [BsonElement(nameof(Version))] + public long Version { get; set; } - public virtual void Prepare() - { - } + public virtual void Prepare() + { } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs index 528249925e..a0b2868da8 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs @@ -8,29 +8,28 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public sealed class MongoUsage { - public sealed class MongoUsage - { - [BsonId] - [BsonElement("_id")] - public string Id { get; set; } + [BsonId] + [BsonElement("_id")] + public string Id { get; set; } - [BsonRequired] - [BsonElement(nameof(Date))] - [BsonDateTimeOptions(DateOnly = true)] - public DateTime Date { get; set; } + [BsonRequired] + [BsonElement(nameof(Date))] + [BsonDateTimeOptions(DateOnly = true)] + public DateTime Date { get; set; } - [BsonRequired] - [BsonElement(nameof(Key))] - public string Key { get; set; } + [BsonRequired] + [BsonElement(nameof(Key))] + public string Key { get; set; } - [BsonIgnoreIfNull] - [BsonElement(nameof(Category))] - public string Category { get; set; } + [BsonIgnoreIfNull] + [BsonElement(nameof(Category))] + public string Category { get; set; } - [BsonRequired] - [BsonElement(nameof(Counters))] - public Counters Counters { get; set; } = new Counters(); - } + [BsonRequired] + [BsonElement(nameof(Counters))] + public Counters Counters { get; set; } = new Counters(); } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs index 1e1b6e9866..5a1f61f43b 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs @@ -9,113 +9,112 @@ using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public sealed class MongoUsageRepository : MongoRepositoryBase<MongoUsage>, IUsageRepository { - public sealed class MongoUsageRepository : MongoRepositoryBase<MongoUsage>, IUsageRepository + public MongoUsageRepository(IMongoDatabase database) + : base(database) { - public MongoUsageRepository(IMongoDatabase database) - : base(database) - { - } + } - protected override string CollectionName() - { - return "UsagesV2"; - } + protected override string CollectionName() + { + return "UsagesV2"; + } - protected override Task SetupCollectionAsync(IMongoCollection<MongoUsage> collection, - CancellationToken ct) - { - return collection.Indexes.CreateOneAsync( - new CreateIndexModel<MongoUsage>( - Index - .Ascending(x => x.Key) - .Ascending(x => x.Category) - .Ascending(x => x.Date)), - cancellationToken: ct); - } + protected override Task SetupCollectionAsync(IMongoCollection<MongoUsage> collection, + CancellationToken ct) + { + return collection.Indexes.CreateOneAsync( + new CreateIndexModel<MongoUsage>( + Index + .Ascending(x => x.Key) + .Ascending(x => x.Category) + .Ascending(x => x.Date)), + cancellationToken: ct); + } - public Task DeleteAsync(string key, - CancellationToken ct = default) - { - Guard.NotNull(key); + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + Guard.NotNull(key); - return Collection.DeleteManyAsync(x => x.Key == key, ct); - } + return Collection.DeleteManyAsync(x => x.Key == key, ct); + } - public Task DeleteByKeyPatternAsync(string pattern, - CancellationToken ct = default) - { - Guard.NotNull(pattern); + public Task DeleteByKeyPatternAsync(string pattern, + CancellationToken ct = default) + { + Guard.NotNull(pattern); - return Collection.DeleteManyAsync(Filter.Regex(x => x.Key, new BsonRegularExpression(pattern)), ct); - } + return Collection.DeleteManyAsync(Filter.Regex(x => x.Key, new BsonRegularExpression(pattern)), ct); + } - public async Task TrackUsagesAsync(UsageUpdate update, - CancellationToken ct = default) - { - Guard.NotNull(update); + public async Task TrackUsagesAsync(UsageUpdate update, + CancellationToken ct = default) + { + Guard.NotNull(update); - if (update.Counters.Count > 0) - { - var (filter, updateStatement) = CreateOperation(update); + if (update.Counters.Count > 0) + { + var (filter, updateStatement) = CreateOperation(update); - await Collection.UpdateOneAsync(filter, updateStatement, Upsert, ct); - } + await Collection.UpdateOneAsync(filter, updateStatement, Upsert, ct); } + } + + public async Task TrackUsagesAsync(UsageUpdate[] updates, + CancellationToken ct = default) + { + Guard.NotNull(updates); - public async Task TrackUsagesAsync(UsageUpdate[] updates, - CancellationToken ct = default) + if (updates.Length == 1) + { + await TrackUsagesAsync(updates[0], ct); + } + else if (updates.Length > 0) { - Guard.NotNull(updates); + var writes = new List<WriteModel<MongoUsage>>(); - if (updates.Length == 1) + foreach (var update in updates) { - await TrackUsagesAsync(updates[0], ct); - } - else if (updates.Length > 0) - { - var writes = new List<WriteModel<MongoUsage>>(); - - foreach (var update in updates) + if (update.Counters.Count > 0) { - if (update.Counters.Count > 0) - { - var (filter, updateStatement) = CreateOperation(update); + var (filter, updateStatement) = CreateOperation(update); - writes.Add(new UpdateOneModel<MongoUsage>(filter, updateStatement) { IsUpsert = true }); - } + writes.Add(new UpdateOneModel<MongoUsage>(filter, updateStatement) { IsUpsert = true }); } - - await Collection.BulkWriteAsync(writes, BulkUnordered, ct); } + + await Collection.BulkWriteAsync(writes, BulkUnordered, ct); } + } - private static (FilterDefinition<MongoUsage>, UpdateDefinition<MongoUsage>) CreateOperation(UsageUpdate usageUpdate) - { - var id = $"{usageUpdate.Key}_{usageUpdate.Date:yyyy-MM-dd}_{usageUpdate.Category}"; + private static (FilterDefinition<MongoUsage>, UpdateDefinition<MongoUsage>) CreateOperation(UsageUpdate usageUpdate) + { + var id = $"{usageUpdate.Key}_{usageUpdate.Date:yyyy-MM-dd}_{usageUpdate.Category}"; - var update = Update - .SetOnInsert(x => x.Key, usageUpdate.Key) - .SetOnInsert(x => x.Date, usageUpdate.Date) - .SetOnInsert(x => x.Category, usageUpdate.Category); + var update = Update + .SetOnInsert(x => x.Key, usageUpdate.Key) + .SetOnInsert(x => x.Date, usageUpdate.Date) + .SetOnInsert(x => x.Category, usageUpdate.Category); - foreach (var (key, value) in usageUpdate.Counters) - { - update = update.Inc($"Counters.{key}", value); - } + foreach (var (key, value) in usageUpdate.Counters) + { + update = update.Inc($"Counters.{key}", value); + } - var filter = Filter.Eq(x => x.Id, id); + var filter = Filter.Eq(x => x.Id, id); - return (filter, update); - } + return (filter, update); + } - public async Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default) - { - var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(ct); + public async Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(ct); - return entities.Select(x => new StoredUsage(x.Category, x.Date, x.Counters)).ToList(); - } + return entities.Select(x => new StoredUsage(x.Category, x.Date, x.Counters)).ToList(); } } diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs index 8cef970a41..f5e0234b13 100644 --- a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs +++ b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs @@ -12,86 +12,85 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; -namespace Squidex.Infrastructure.CQRS.Events +namespace Squidex.Infrastructure.CQRS.Events; + +public sealed class RabbitMqEventConsumer : DisposableObjectBase, IInitializable, IEventConsumer { - public sealed class RabbitMqEventConsumer : DisposableObjectBase, IInitializable, IEventConsumer + private readonly IJsonSerializer serializer; + private readonly string eventPublisherName; + private readonly string exchange; + private readonly string eventsFilter; + private readonly ConnectionFactory connectionFactory; + private readonly Lazy<IConnection> connection; + private readonly Lazy<IModel> channel; + + public string Name { - private readonly IJsonSerializer serializer; - private readonly string eventPublisherName; - private readonly string exchange; - private readonly string eventsFilter; - private readonly ConnectionFactory connectionFactory; - private readonly Lazy<IConnection> connection; - private readonly Lazy<IModel> channel; - - public string Name - { - get => eventPublisherName; - } + get => eventPublisherName; + } - public string EventsFilter - { - get => eventsFilter; - } + public string EventsFilter + { + get => eventsFilter; + } - public RabbitMqEventConsumer(IJsonSerializer serializer, string eventPublisherName, string uri, string exchange, string eventsFilter) - { - connectionFactory = new ConnectionFactory { Uri = new Uri(uri, UriKind.Absolute) }; - connection = new Lazy<IConnection>(connectionFactory.CreateConnection); - channel = new Lazy<IModel>(connection.Value.CreateModel); - - this.exchange = exchange; - this.eventsFilter = eventsFilter; - this.eventPublisherName = eventPublisherName; - this.serializer = serializer; - } + public RabbitMqEventConsumer(IJsonSerializer serializer, string eventPublisherName, string uri, string exchange, string eventsFilter) + { + connectionFactory = new ConnectionFactory { Uri = new Uri(uri, UriKind.Absolute) }; + connection = new Lazy<IConnection>(connectionFactory.CreateConnection); + channel = new Lazy<IModel>(connection.Value.CreateModel); + + this.exchange = exchange; + this.eventsFilter = eventsFilter; + this.eventPublisherName = eventPublisherName; + this.serializer = serializer; + } - protected override void DisposeObject(bool disposing) + protected override void DisposeObject(bool disposing) + { + if (connection.IsValueCreated) { - if (connection.IsValueCreated) - { - connection.Value.Close(); - connection.Value.Dispose(); - } + connection.Value.Close(); + connection.Value.Dispose(); } + } - public Task InitializeAsync( - CancellationToken ct) + public Task InitializeAsync( + CancellationToken ct) + { + try { - try - { - var currentConnection = connection.Value; - - if (!currentConnection.IsOpen) - { - var error = new ConfigurationError($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}."); - - throw new ConfigurationException(error); - } + var currentConnection = connection.Value; - return Task.CompletedTask; - } - catch (Exception ex) + if (!currentConnection.IsOpen) { var error = new ConfigurationError($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}."); - throw new ConfigurationException(error, ex); + throw new ConfigurationException(error); } - } - public Task On(Envelope<IEvent> @event) + return Task.CompletedTask; + } + catch (Exception ex) { - if (@event.Headers.Restored()) - { - return Task.CompletedTask; - } + var error = new ConfigurationError($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}."); - var jsonString = serializer.Serialize(@event); - var jsonBytes = Encoding.UTF8.GetBytes(jsonString); - - channel.Value.BasicPublish(exchange, string.Empty, null, jsonBytes); + throw new ConfigurationException(error, ex); + } + } + public Task On(Envelope<IEvent> @event) + { + if (@event.Headers.Restored()) + { return Task.CompletedTask; } + + var jsonString = serializer.Serialize(@event); + var jsonBytes = Encoding.UTF8.GetBytes(jsonString); + + channel.Value.BasicPublish(exchange, string.Empty, null, jsonBytes); + + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs b/backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs index fdbd445e86..98dcbea0b4 100644 --- a/backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs +++ b/backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Caching +namespace Squidex.Infrastructure.Caching; + +public interface IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey> { - public interface IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey> - { - void SetMany(IEnumerable<(TKey, T?)> results, - TimeSpan? permanentDuration = null); + void SetMany(IEnumerable<(TKey, T?)> results, + TimeSpan? permanentDuration = null); - Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query, - TimeSpan? permanentDuration = null); - } + Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query, + TimeSpan? permanentDuration = null); } diff --git a/backend/src/Squidex.Infrastructure/Caching/IRequestCache.cs b/backend/src/Squidex.Infrastructure/Caching/IRequestCache.cs index 0e2532d16d..4df62b1ab5 100644 --- a/backend/src/Squidex.Infrastructure/Caching/IRequestCache.cs +++ b/backend/src/Squidex.Infrastructure/Caching/IRequestCache.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Caching +namespace Squidex.Infrastructure.Caching; + +public interface IRequestCache { - public interface IRequestCache - { - void AddDependency(DomainId key, long version); + void AddDependency(DomainId key, long version); - void AddDependency<T>(T value); + void AddDependency<T>(T value); - void AddHeader(string header); - } + void AddHeader(string header); } diff --git a/backend/src/Squidex.Infrastructure/Caching/QueryCache.cs b/backend/src/Squidex.Infrastructure/Caching/QueryCache.cs index f96eccf19a..e8d94cfcea 100644 --- a/backend/src/Squidex.Infrastructure/Caching/QueryCache.cs +++ b/backend/src/Squidex.Infrastructure/Caching/QueryCache.cs @@ -8,87 +8,86 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Caching.Memory; -namespace Squidex.Infrastructure.Caching +namespace Squidex.Infrastructure.Caching; + +public class QueryCache<TKey, T> : IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey> { - public class QueryCache<TKey, T> : IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey> + private readonly ConcurrentDictionary<TKey, T?> entries = new ConcurrentDictionary<TKey, T?>(); + private readonly IMemoryCache? memoryCache; + + public QueryCache(IMemoryCache? memoryCache = null) { - private readonly ConcurrentDictionary<TKey, T?> entries = new ConcurrentDictionary<TKey, T?>(); - private readonly IMemoryCache? memoryCache; + this.memoryCache = memoryCache; + } - public QueryCache(IMemoryCache? memoryCache = null) - { - this.memoryCache = memoryCache; - } + public void SetMany(IEnumerable<(TKey, T?)> results, + TimeSpan? permanentDuration = null) + { + Guard.NotNull(results); - public void SetMany(IEnumerable<(TKey, T?)> results, - TimeSpan? permanentDuration = null) + foreach (var (key, value) in results) { - Guard.NotNull(results); - - foreach (var (key, value) in results) - { - Set(key, value, permanentDuration); - } + Set(key, value, permanentDuration); } + } - private void Set(TKey key, T? value, - TimeSpan? permanentDuration = null) - { - entries[key] = value; + private void Set(TKey key, T? value, + TimeSpan? permanentDuration = null) + { + entries[key] = value; - if (memoryCache != null && permanentDuration > TimeSpan.Zero) - { - memoryCache.Set(key, value, permanentDuration.Value); - } + if (memoryCache != null && permanentDuration > TimeSpan.Zero) + { + memoryCache.Set(key, value, permanentDuration.Value); } + } - public async Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query, - TimeSpan? permanentDuration = null) - { - Guard.NotNull(keys); - Guard.NotNull(query); + public async Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query, + TimeSpan? permanentDuration = null) + { + Guard.NotNull(keys); + Guard.NotNull(query); - var items = GetMany(keys, permanentDuration.HasValue); + var items = GetMany(keys, permanentDuration.HasValue); - var pendingIds = new HashSet<TKey>(keys.Where(key => !items.ContainsKey(key))); + var pendingIds = new HashSet<TKey>(keys.Where(key => !items.ContainsKey(key))); - if (pendingIds.Count > 0) - { - var queried = (await query(pendingIds)).ToDictionary(x => x.Id); + if (pendingIds.Count > 0) + { + var queried = (await query(pendingIds)).ToDictionary(x => x.Id); - foreach (var id in pendingIds) - { - queried.TryGetValue(id, out var item); + foreach (var id in pendingIds) + { + queried.TryGetValue(id, out var item); - items[id] = item; + items[id] = item; - Set(id, item, permanentDuration); - } + Set(id, item, permanentDuration); } - - return items.Values.NotNull().ToList(); } - private Dictionary<TKey, T?> GetMany(IEnumerable<TKey> keys, - bool fromPermanentCache = false) - { - var result = new Dictionary<TKey, T?>(); + return items.Values.NotNull().ToList(); + } + + private Dictionary<TKey, T?> GetMany(IEnumerable<TKey> keys, + bool fromPermanentCache = false) + { + var result = new Dictionary<TKey, T?>(); - foreach (var key in keys) + foreach (var key in keys) + { + if (entries.TryGetValue(key, out var value)) { - if (entries.TryGetValue(key, out var value)) - { - result[key] = value; - } - else if (fromPermanentCache && memoryCache != null && memoryCache.TryGetValue(key, out value)) - { - result[key] = value; - - entries[key] = value; - } + result[key] = value; } + else if (fromPermanentCache && memoryCache != null && memoryCache.TryGetValue(key, out value)) + { + result[key] = value; - return result; + entries[key] = value; + } } + + return result; } } diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs index e0f15ec158..67d5c34228 100644 --- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -7,420 +7,419 @@ using System.Diagnostics.CodeAnalysis; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class CollectionExtensions { - public static class CollectionExtensions + public static bool TryAdd<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull { - public static bool TryAdd<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull - { - result = null; + result = null; - if (!source.ContainsKey(key)) + if (!source.ContainsKey(key)) + { + var clone = new Dictionary<TKey, TValue>(source) { - var clone = new Dictionary<TKey, TValue>(source) - { - [key] = value - }; + [key] = value + }; - result = clone; + result = clone; - return true; - } - - return false; + return true; } - public static bool TrySet<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull - { - result = null; + return false; + } + + public static bool TrySet<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull + { + result = null; - if (!source.TryGetValue(key, out var found) || !Equals(found, value)) + if (!source.TryGetValue(key, out var found) || !Equals(found, value)) + { + var clone = new Dictionary<TKey, TValue>(source) { - var clone = new Dictionary<TKey, TValue>(source) - { - [key] = value - }; + [key] = value + }; - result = clone; + result = clone; - return true; - } - - return false; + return true; } - public static bool TryUpdate<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull - { - result = null; + return false; + } - if (source.TryGetValue(key, out var found) && !Equals(found, value)) - { - var clone = new Dictionary<TKey, TValue>(source) - { - [key] = value - }; + public static bool TryUpdate<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull + { + result = null; - result = clone; + if (source.TryGetValue(key, out var found) && !Equals(found, value)) + { + var clone = new Dictionary<TKey, TValue>(source) + { + [key] = value + }; - return true; - } + result = clone; - return false; + return true; } - public static bool TryRemove<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull - { - result = null; - - if (source.ContainsKey(key)) - { - var clone = new Dictionary<TKey, TValue>(source); + return false; + } - result = clone; - result.Remove(key); + public static bool TryRemove<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull + { + result = null; - return true; - } + if (source.ContainsKey(key)) + { + var clone = new Dictionary<TKey, TValue>(source); - return false; - } + result = clone; + result.Remove(key); - public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other) - { - return source.Count == other.Count && source.Intersect(other).Count() == other.Count; + return true; } - public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(this IEnumerable<TSource> source, int size) - { - TSource[]? bucket = null; - - var bucketIndex = 0; + return false; + } - foreach (var item in source) - { - bucket ??= new TSource[size]; - bucket[bucketIndex++] = item; + public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other) + { + return source.Count == other.Count && source.Intersect(other).Count() == other.Count; + } - if (bucketIndex != size) - { - continue; - } + public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(this IEnumerable<TSource> source, int size) + { + TSource[]? bucket = null; - yield return bucket; + var bucketIndex = 0; - bucket = null; - bucketIndex = 0; - } + foreach (var item in source) + { + bucket ??= new TSource[size]; + bucket[bucketIndex++] = item; - if (bucket != null && bucketIndex > 0) + if (bucketIndex != size) { - yield return bucket.Take(bucketIndex); + continue; } - } - public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other, IEqualityComparer<T> comparer) - { - return source.Count == other.Count && source.Intersect(other, comparer).Count() == other.Count; - } + yield return bucket; - public static IEnumerable<T> Reverse<T>(this IEnumerable<T> source, bool reverse) - { - return reverse ? source.Reverse() : source; - } - - public static IResultList<T> SortSet<T, TKey>(this IResultList<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class - { - return ResultList.Create(input.Total, SortList(input, idProvider, ids)); + bucket = null; + bucketIndex = 0; } - public static IEnumerable<T> SortList<T, TKey>(this IEnumerable<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class + if (bucket != null && bucketIndex > 0) { - return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).NotNull(); + yield return bucket.Take(bucketIndex); } + } - public static IEnumerable<T> Duplicates<T>(this IEnumerable<T> input) - { - return input.GroupBy(x => x).Where(x => x.Count() > 1).Select(x => x.Key); - } + public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other, IEqualityComparer<T> comparer) + { + return source.Count == other.Count && source.Intersect(other, comparer).Count() == other.Count; + } - public static int IndexOf<T>(this IEnumerable<T> input, Func<T, bool> predicate) - { - var i = 0; + public static IEnumerable<T> Reverse<T>(this IEnumerable<T> source, bool reverse) + { + return reverse ? source.Reverse() : source; + } - foreach (var item in input) - { - if (predicate(item)) - { - return i; - } + public static IResultList<T> SortSet<T, TKey>(this IResultList<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class + { + return ResultList.Create(input.Total, SortList(input, idProvider, ids)); + } - i++; - } + public static IEnumerable<T> SortList<T, TKey>(this IEnumerable<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class + { + return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).NotNull(); + } - return -1; - } + public static IEnumerable<T> Duplicates<T>(this IEnumerable<T> input) + { + return input.GroupBy(x => x).Where(x => x.Count() > 1).Select(x => x.Key); + } - public static IEnumerable<TResult> Duplicates<TResult, T>(this IEnumerable<T> input, Func<T, TResult> selector) - { - return input.GroupBy(selector).Where(x => x.Count() > 1).Select(x => x.Key); - } + public static int IndexOf<T>(this IEnumerable<T> input, Func<T, bool> predicate) + { + var i = 0; - public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source) + foreach (var item in input) { - foreach (var value in source) + if (predicate(item)) { - target.Add(value); + return i; } - } - public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable) - { - return enumerable.OrderBy(x => Random.Shared.Next()).ToList(); + i++; } - public static IEnumerable<T> OrEmpty<T>(this IEnumerable<T>? source) - { - return source ?? Enumerable.Empty<T>(); - } + return -1; + } - public static IEnumerable<T> NotNull<T>(this IEnumerable<T?> source) where T : class - { - return source.Where(x => x != null)!; - } + public static IEnumerable<TResult> Duplicates<TResult, T>(this IEnumerable<T> input, Func<T, TResult> selector) + { + return input.GroupBy(selector).Where(x => x.Count() > 1).Select(x => x.Key); + } - public static IEnumerable<TOut> NotNull<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, TOut?> selector) where TOut : class + public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source) + { + foreach (var value in source) { - return source.Select(selector).Where(x => x != null)!; + target.Add(value); } + } - public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, T value) - { - return source.Concat(Enumerable.Repeat(value, 1)); - } + public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable) + { + return enumerable.OrderBy(x => Random.Shared.Next()).ToList(); + } - public static int SequentialHashCode<T>(this IEnumerable<T> collection) - { - return collection.SequentialHashCode(EqualityComparer<T>.Default); - } + public static IEnumerable<T> OrEmpty<T>(this IEnumerable<T>? source) + { + return source ?? Enumerable.Empty<T>(); + } - public static int SequentialHashCode<T>(this IEnumerable<T> collection, IEqualityComparer<T> comparer) - { - var hashCode = 17; + public static IEnumerable<T> NotNull<T>(this IEnumerable<T?> source) where T : class + { + return source.Where(x => x != null)!; + } - foreach (var item in collection) - { - if (!Equals(item, null)) - { - hashCode = (hashCode * 23) + comparer.GetHashCode(item); - } - } + public static IEnumerable<TOut> NotNull<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, TOut?> selector) where TOut : class + { + return source.Select(selector).Where(x => x != null)!; + } - return hashCode; - } + public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, T value) + { + return source.Concat(Enumerable.Repeat(value, 1)); + } - public static int DictionaryHashCode<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull - { - return DictionaryHashCode(dictionary, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default); - } + public static int SequentialHashCode<T>(this IEnumerable<T> collection) + { + return collection.SequentialHashCode(EqualityComparer<T>.Default); + } - public static int DictionaryHashCode<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer) where TKey : notnull - { - var hashCode = 17; + public static int SequentialHashCode<T>(this IEnumerable<T> collection, IEqualityComparer<T> comparer) + { + var hashCode = 17; - foreach (var (key, value) in dictionary.OrderBy(x => x.Key)) + foreach (var item in collection) + { + if (!Equals(item, null)) { - hashCode = (hashCode * 23) + keyComparer.GetHashCode(key); - - if (!Equals(value, null)) - { - hashCode = (hashCode * 23) + valueComparer.GetHashCode(value); - } + hashCode = (hashCode * 23) + comparer.GetHashCode(item); } - - return hashCode; } - public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue>? other) where TKey : notnull - { - return EqualsDictionary(dictionary, other, EqualityComparer<TValue>.Default); - } + return hashCode; + } + + public static int DictionaryHashCode<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull + { + return DictionaryHashCode(dictionary, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default); + } - public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue>? other, IEqualityComparer<TValue> valueComparer) where TKey : notnull + public static int DictionaryHashCode<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer) where TKey : notnull + { + var hashCode = 17; + + foreach (var (key, value) in dictionary.OrderBy(x => x.Key)) { - if (other == null) - { - return false; - } + hashCode = (hashCode * 23) + keyComparer.GetHashCode(key); - if (ReferenceEquals(dictionary, other)) + if (!Equals(value, null)) { - return true; + hashCode = (hashCode * 23) + valueComparer.GetHashCode(value); } + } - if (dictionary.Count != other.Count) - { - return false; - } + return hashCode; + } - foreach (var (key, value) in dictionary) - { - if (!other.TryGetValue(key, out var otherValue) || !valueComparer.Equals(value, otherValue)) - { - return false; - } - } + public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue>? other) where TKey : notnull + { + return EqualsDictionary(dictionary, other, EqualityComparer<TValue>.Default); + } - return true; + public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue>? other, IEqualityComparer<TValue> valueComparer) where TKey : notnull + { + if (other == null) + { + return false; } - public static bool EqualsList<T>(this IReadOnlyList<T> list, IReadOnlyList<T>? other) + if (ReferenceEquals(dictionary, other)) { - return EqualsList(list, other, EqualityComparer<T>.Default); + return true; } - public static bool EqualsList<T>(this IReadOnlyList<T> list, IReadOnlyList<T>? other, IEqualityComparer<T> comparer) + if (dictionary.Count != other.Count) { - if (other == null) - { - return false; - } - - if (ReferenceEquals(list, other)) - { - return true; - } + return false; + } - if (list.Count != other.Count) + foreach (var (key, value) in dictionary) + { + if (!other.TryGetValue(key, out var otherValue) || !valueComparer.Equals(value, otherValue)) { return false; } + } - for (var i = 0; i < list.Count; i++) - { - if (!comparer.Equals(list[i], other[i])) - { - return false; - } - } + return true; + } - return true; - } + public static bool EqualsList<T>(this IReadOnlyList<T> list, IReadOnlyList<T>? other) + { + return EqualsList(list, other, EqualityComparer<T>.Default); + } - public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull + public static bool EqualsList<T>(this IReadOnlyList<T> list, IReadOnlyList<T>? other, IEqualityComparer<T> comparer) + { + if (other == null) { - return dictionary.ToDictionary(x => x.Key, x => x.Value); + return false; } - public static TValue GetOrAddDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull + if (ReferenceEquals(list, other)) { - return dictionary.GetOrAdd(key, _ => default!); + return true; } - public static TValue GetOrAddNew<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull where TValue : class, new() + if (list.Count != other.Count) { - return dictionary.GetOrAdd(key, _ => new TValue()); + return false; } - public static TValue GetOrCreate<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> valueFactory) where TKey : notnull + for (var i = 0; i < list.Count; i++) { - if (!dictionary.TryGetValue(key, out var result)) + if (!comparer.Equals(list[i], other[i])) { - result = valueFactory(key); + return false; } - - return result; } - public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue fallback) where TKey : notnull - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = fallback; + return true; + } - dictionary.Add(key, result); - } + public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull + { + return dictionary.ToDictionary(x => x.Key, x => x.Value); + } - return result; - } + public static TValue GetOrAddDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull + { + return dictionary.GetOrAdd(key, _ => default!); + } + + public static TValue GetOrAddNew<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull where TValue : class, new() + { + return dictionary.GetOrAdd(key, _ => new TValue()); + } - public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> valueFactory) where TKey : notnull + public static TValue GetOrCreate<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> valueFactory) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) { - if (!dictionary.TryGetValue(key, out var result)) - { - result = valueFactory(key); + result = valueFactory(key); + } - dictionary.Add(key, result); - } + return result; + } - return result; + public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue fallback) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = fallback; + + dictionary.Add(key, result); } - public static TValue GetOrAdd<TKey, TArg, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument) where TKey : notnull - { - if (!dictionary.TryGetValue(key, out var result)) - { - result = valueFactory(key, factoryArgument); + return result; + } - dictionary.Add(key, result); - } + public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> valueFactory) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) + { + result = valueFactory(key); - return result; + dictionary.Add(key, result); } - public static void Foreach<T>(this IEnumerable<T> collection, Action<T> action) + return result; + } + + public static TValue GetOrAdd<TKey, TArg, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument) where TKey : notnull + { + if (!dictionary.TryGetValue(key, out var result)) { - collection.Foreach((x, i) => action(x)); + result = valueFactory(key, factoryArgument); + + dictionary.Add(key, result); } - public static void Foreach<T>(this IEnumerable<T> collection, Action<T, int> action) - { - var index = 0; + return result; + } - foreach (var item in collection) - { - action(item, index); + public static void Foreach<T>(this IEnumerable<T> collection, Action<T> action) + { + collection.Foreach((x, i) => action(x)); + } - index++; - } + public static void Foreach<T>(this IEnumerable<T> collection, Action<T, int> action) + { + var index = 0; + + foreach (var item in collection) + { + action(item, index); + + index++; } + } - public static IEnumerable<T> TakeRandom<T>(this IEnumerable<T> source, long take) + public static IEnumerable<T> TakeRandom<T>(this IEnumerable<T> source, long take) + { + var sourceList = new LinkedList<T>(source); + + static LinkedListNode<T> TakeNode(LinkedList<T> source, int index) { - var sourceList = new LinkedList<T>(source); + var actual = source.First!; - static LinkedListNode<T> TakeNode(LinkedList<T> source, int index) + for (var i = 0; i < index; i++) { - var actual = source.First!; + var next = actual?.Next; - for (var i = 0; i < index; i++) + if (next == null) { - var next = actual?.Next; - - if (next == null) - { - return actual!; - } - - actual = next; + return actual!; } - return actual; + actual = next; } - for (var i = 0; i < take; i++) + return actual; + } + + for (var i = 0; i < take; i++) + { + if (sourceList.Count == 0) { - if (sourceList.Count == 0) - { - break; - } + break; + } - var takenIndex = Random.Shared.Next(sourceList.Count); - var takenElement = TakeNode(sourceList, takenIndex); + var takenIndex = Random.Shared.Next(sourceList.Count); + var takenElement = TakeNode(sourceList, takenIndex); - sourceList.Remove(takenElement); + sourceList.Remove(takenElement); - yield return takenElement.Value; - } + yield return takenElement.Value; } } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs index 20334ad9a5..4bece008db 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs @@ -7,119 +7,118 @@ using System.Collections; -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public partial class ListDictionary<TKey, TValue> { - public partial class ListDictionary<TKey, TValue> + private sealed class KeyCollection : ICollection<TKey> { - private sealed class KeyCollection : ICollection<TKey> + private readonly ListDictionary<TKey, TValue> dictionary; + + public int Count { - private readonly ListDictionary<TKey, TValue> dictionary; + get => dictionary.Count; + } - public int Count - { - get => dictionary.Count; - } + public bool IsReadOnly + { + get => false; + } - public bool IsReadOnly - { - get => false; - } + public KeyCollection(ListDictionary<TKey, TValue> dictionary) + { + this.dictionary = dictionary; + } - public KeyCollection(ListDictionary<TKey, TValue> dictionary) - { - this.dictionary = dictionary; - } + public void Add(TKey item) + { + throw new NotSupportedException(); + } - public void Add(TKey item) - { - throw new NotSupportedException(); - } + public void Clear() + { + throw new NotSupportedException(); + } - public void Clear() + public void CopyTo(TKey[] array, int arrayIndex) + { + var i = 0; + foreach (var (key, _) in dictionary.entries) { - throw new NotSupportedException(); + array[arrayIndex + i] = key; + i++; } + } - public void CopyTo(TKey[] array, int arrayIndex) + public bool Remove(TKey item) + { + throw new NotSupportedException(); + } + + public bool Contains(TKey item) + { + foreach (var entry in dictionary.entries) { - var i = 0; - foreach (var (key, _) in dictionary.entries) + if (dictionary.comparer.Equals(entry.Key, item)) { - array[arrayIndex + i] = key; - i++; + return true; } } - public bool Remove(TKey item) + return false; + } + + public IEnumerator<TKey> GetEnumerator() + { + return new Enumerator(dictionary); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(dictionary); + } + + private struct Enumerator : IEnumerator<TKey>, IEnumerator + { + private readonly ListDictionary<TKey, TValue> dictionary; + private int index = -1; + private TKey value = default!; + + readonly TKey IEnumerator<TKey>.Current { - throw new NotSupportedException(); + get => value!; } - public bool Contains(TKey item) + readonly object IEnumerator.Current { - foreach (var entry in dictionary.entries) - { - if (dictionary.comparer.Equals(entry.Key, item)) - { - return true; - } - } - - return false; + get => value!; } - public IEnumerator<TKey> GetEnumerator() + public Enumerator(ListDictionary<TKey, TValue> dictionary) { - return new Enumerator(dictionary); + this.dictionary = dictionary; } - IEnumerator IEnumerable.GetEnumerator() + public readonly void Dispose() { - return new Enumerator(dictionary); } - private struct Enumerator : IEnumerator<TKey>, IEnumerator + public bool MoveNext() { - private readonly ListDictionary<TKey, TValue> dictionary; - private int index = -1; - private TKey value = default!; - - readonly TKey IEnumerator<TKey>.Current - { - get => value!; - } - - readonly object IEnumerator.Current - { - get => value!; - } - - public Enumerator(ListDictionary<TKey, TValue> dictionary) + if (index >= dictionary.entries.Count - 1) { - this.dictionary = dictionary; + return false; } - public readonly void Dispose() - { - } - - public bool MoveNext() - { - if (index >= dictionary.entries.Count - 1) - { - return false; - } + index++; - index++; - - value = dictionary.entries[index].Key; - return true; - } + value = dictionary.entries[index].Key; + return true; + } - public void Reset() - { - index = -1; - } + public void Reset() + { + index = -1; } } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs index cfc0041404..4f99870689 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs @@ -7,119 +7,118 @@ using System.Collections; -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public partial class ListDictionary<TKey, TValue> { - public partial class ListDictionary<TKey, TValue> + private sealed class ValueCollection : ICollection<TValue> { - private sealed class ValueCollection : ICollection<TValue> + private readonly ListDictionary<TKey, TValue> dictionary; + + public int Count { - private readonly ListDictionary<TKey, TValue> dictionary; + get => dictionary.Count; + } - public int Count - { - get => dictionary.Count; - } + public bool IsReadOnly + { + get => false; + } - public bool IsReadOnly - { - get => false; - } + public ValueCollection(ListDictionary<TKey, TValue> dictionary) + { + this.dictionary = dictionary; + } - public ValueCollection(ListDictionary<TKey, TValue> dictionary) - { - this.dictionary = dictionary; - } + public void Add(TValue item) + { + throw new NotSupportedException(); + } - public void Add(TValue item) - { - throw new NotSupportedException(); - } + public void Clear() + { + throw new NotSupportedException(); + } - public void Clear() + public void CopyTo(TValue[] array, int arrayIndex) + { + var i = 0; + foreach (var (_, value) in dictionary.entries) { - throw new NotSupportedException(); + array[arrayIndex + i] = value; + i++; } + } - public void CopyTo(TValue[] array, int arrayIndex) + public bool Remove(TValue item) + { + throw new NotSupportedException(); + } + + public bool Contains(TValue item) + { + foreach (var entry in dictionary.entries) { - var i = 0; - foreach (var (_, value) in dictionary.entries) + if (Equals(entry.Value, item)) { - array[arrayIndex + i] = value; - i++; + return true; } } - public bool Remove(TValue item) + return false; + } + + public IEnumerator<TValue> GetEnumerator() + { + return new Enumerator(dictionary); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(dictionary); + } + + private struct Enumerator : IEnumerator<TValue>, IEnumerator + { + private readonly ListDictionary<TKey, TValue> dictionary; + private int index = -1; + private TValue value = default!; + + readonly TValue IEnumerator<TValue>.Current { - throw new NotSupportedException(); + get => value!; } - public bool Contains(TValue item) + readonly object IEnumerator.Current { - foreach (var entry in dictionary.entries) - { - if (Equals(entry.Value, item)) - { - return true; - } - } - - return false; + get => value!; } - public IEnumerator<TValue> GetEnumerator() + public Enumerator(ListDictionary<TKey, TValue> dictionary) { - return new Enumerator(dictionary); + this.dictionary = dictionary; } - IEnumerator IEnumerable.GetEnumerator() + public readonly void Dispose() { - return new Enumerator(dictionary); } - private struct Enumerator : IEnumerator<TValue>, IEnumerator + public bool MoveNext() { - private readonly ListDictionary<TKey, TValue> dictionary; - private int index = -1; - private TValue value = default!; - - readonly TValue IEnumerator<TValue>.Current - { - get => value!; - } - - readonly object IEnumerator.Current - { - get => value!; - } - - public Enumerator(ListDictionary<TKey, TValue> dictionary) + if (index >= dictionary.entries.Count - 1) { - this.dictionary = dictionary; + return false; } - public readonly void Dispose() - { - } - - public bool MoveNext() - { - if (index >= dictionary.entries.Count - 1) - { - return false; - } + index++; - index++; - - value = dictionary.entries[index].Value; - return true; - } + value = dictionary.entries[index].Value; + return true; + } - public void Reset() - { - index = -1; - } + public void Reset() + { + index = -1; } } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs index bbffab4560..a8af283485 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs @@ -8,267 +8,266 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public partial class ListDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TKey : notnull { - public partial class ListDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TKey : notnull + private readonly List<KeyValuePair<TKey, TValue>> entries = new List<KeyValuePair<TKey, TValue>>(); + private readonly IEqualityComparer<TKey> comparer; + + private struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>, IEnumerator { - private readonly List<KeyValuePair<TKey, TValue>> entries = new List<KeyValuePair<TKey, TValue>>(); - private readonly IEqualityComparer<TKey> comparer; + private readonly ListDictionary<TKey, TValue> dictionary; + private int index = -1; + private KeyValuePair<TKey, TValue> value = default!; - private struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>, IEnumerator + readonly KeyValuePair<TKey, TValue> IEnumerator<KeyValuePair<TKey, TValue>>.Current { - private readonly ListDictionary<TKey, TValue> dictionary; - private int index = -1; - private KeyValuePair<TKey, TValue> value = default!; + get => value!; + } - readonly KeyValuePair<TKey, TValue> IEnumerator<KeyValuePair<TKey, TValue>>.Current - { - get => value!; - } + readonly object IEnumerator.Current + { + get => value!; + } - readonly object IEnumerator.Current - { - get => value!; - } + public Enumerator(ListDictionary<TKey, TValue> dictionary) + { + this.dictionary = dictionary; + } - public Enumerator(ListDictionary<TKey, TValue> dictionary) - { - this.dictionary = dictionary; - } + public readonly void Dispose() + { + } - public readonly void Dispose() + public bool MoveNext() + { + if (index >= dictionary.entries.Count - 1) { + return false; } - public bool MoveNext() - { - if (index >= dictionary.entries.Count - 1) - { - return false; - } + index++; - index++; + value = dictionary.entries[index]; + return true; + } - value = dictionary.entries[index]; - return true; - } + public void Reset() + { + index = -1; + } + } - public void Reset() + public TValue this[TKey key] + { + get + { + if (!TryGetValue(key, out var result)) { - index = -1; + ThrowHelper.KeyNotFoundException(); + return default!; } - } - public TValue this[TKey key] + return result; + } + set { - get + var index = -1; + + for (var i = 0; i < entries.Count; i++) { - if (!TryGetValue(key, out var result)) + if (comparer.Equals(entries[i].Key, key)) { - ThrowHelper.KeyNotFoundException(); - return default!; + index = i; + break; } + } - return result; + if (index >= 0) + { + entries[index] = new KeyValuePair<TKey, TValue>(key, value); } - set + else { - var index = -1; - - for (var i = 0; i < entries.Count; i++) - { - if (comparer.Equals(entries[i].Key, key)) - { - index = i; - break; - } - } - - if (index >= 0) - { - entries[index] = new KeyValuePair<TKey, TValue>(key, value); - } - else - { - entries.Add(new KeyValuePair<TKey, TValue>(key, value)); - } + entries.Add(new KeyValuePair<TKey, TValue>(key, value)); } } + } - public ICollection<TKey> Keys - { - get => new KeyCollection(this); - } + public ICollection<TKey> Keys + { + get => new KeyCollection(this); + } - public ICollection<TValue> Values - { - get => new ValueCollection(this); - } + public ICollection<TValue> Values + { + get => new ValueCollection(this); + } - public int Count - { - get => entries.Count; - } + public int Count + { + get => entries.Count; + } - public int Capacity - { - get => entries.Capacity; - } + public int Capacity + { + get => entries.Capacity; + } - public bool IsReadOnly - { - get => false; - } + public bool IsReadOnly + { + get => false; + } - IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys - { - get => new KeyCollection(this); - } + IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys + { + get => new KeyCollection(this); + } - IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values - { - get => new ValueCollection(this); - } + IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values + { + get => new ValueCollection(this); + } - public ListDictionary() - : this(1, null) - { - } + public ListDictionary() + : this(1, null) + { + } - public ListDictionary(ListDictionary<TKey, TValue> source, IEqualityComparer<TKey>? comparer = null) - { - Guard.NotNull(source); + public ListDictionary(ListDictionary<TKey, TValue> source, IEqualityComparer<TKey>? comparer = null) + { + Guard.NotNull(source); - entries = source.entries.ToList(); + entries = source.entries.ToList(); - this.comparer = comparer ?? EqualityComparer<TKey>.Default; - } + this.comparer = comparer ?? EqualityComparer<TKey>.Default; + } - public ListDictionary(int capacity, IEqualityComparer<TKey>? comparer = null) - { - Guard.GreaterEquals(capacity, 0); + public ListDictionary(int capacity, IEqualityComparer<TKey>? comparer = null) + { + Guard.GreaterEquals(capacity, 0); - entries = new List<KeyValuePair<TKey, TValue>>(capacity); + entries = new List<KeyValuePair<TKey, TValue>>(capacity); - this.comparer = comparer ?? EqualityComparer<TKey>.Default; - } + this.comparer = comparer ?? EqualityComparer<TKey>.Default; + } - public void Add(TKey key, TValue value) + public void Add(TKey key, TValue value) + { + if (ContainsKey(key)) { - if (ContainsKey(key)) - { - ThrowHelper.ArgumentException("Key already exists.", nameof(key)); - } - - AddUnsafe(key, value); + ThrowHelper.ArgumentException("Key already exists.", nameof(key)); } - public void Add(KeyValuePair<TKey, TValue> item) - { - Add(item.Key, item.Value); - } + AddUnsafe(key, value); + } - public void AddUnsafe(TKey key, TValue value) - { - entries.Add(new KeyValuePair<TKey, TValue>(key, value)); - } + public void Add(KeyValuePair<TKey, TValue> item) + { + Add(item.Key, item.Value); + } - public void Clear() - { - entries.Clear(); - } + public void AddUnsafe(TKey key, TValue value) + { + entries.Add(new KeyValuePair<TKey, TValue>(key, value)); + } - public bool Contains(KeyValuePair<TKey, TValue> item) + public void Clear() + { + entries.Clear(); + } + + public bool Contains(KeyValuePair<TKey, TValue> item) + { + foreach (var entry in entries) { - foreach (var entry in entries) + if (comparer.Equals(entry.Key, item.Key) && Equals(entry.Value, item.Value)) { - if (comparer.Equals(entry.Key, item.Key) && Equals(entry.Value, item.Value)) - { - return true; - } + return true; } - - return false; } - public bool ContainsKey(TKey key) + return false; + } + + public bool ContainsKey(TKey key) + { + foreach (var entry in entries) { - foreach (var entry in entries) + if (comparer.Equals(entry.Key, key)) { - if (comparer.Equals(entry.Key, key)) - { - return true; - } + return true; } - - return false; } - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + return false; + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + foreach (var entry in entries) { - foreach (var entry in entries) + if (comparer.Equals(entry.Key, key)) { - if (comparer.Equals(entry.Key, key)) - { - value = entry.Value; - return true; - } + value = entry.Value; + return true; } - - value = default; - return false; } - public bool Remove(TKey key) + value = default; + return false; + } + + public bool Remove(TKey key) + { + for (var i = 0; i < entries.Count; i++) { - for (var i = 0; i < entries.Count; i++) - { - var entry = entries[i]; + var entry = entries[i]; - if (comparer.Equals(entry.Key, key)) - { - entries.RemoveAt(i); - return true; - } + if (comparer.Equals(entry.Key, key)) + { + entries.RemoveAt(i); + return true; } - - return false; } - public bool Remove(KeyValuePair<TKey, TValue> item) + return false; + } + + public bool Remove(KeyValuePair<TKey, TValue> item) + { + for (var i = 0; i < entries.Count; i++) { - for (var i = 0; i < entries.Count; i++) - { - var entry = entries[i]; + var entry = entries[i]; - if (comparer.Equals(entry.Key, item.Key) && Equals(entry.Value, item.Value)) - { - entries.RemoveAt(i); - return true; - } + if (comparer.Equals(entry.Key, item.Key) && Equals(entry.Value, item.Value)) + { + entries.RemoveAt(i); + return true; } - - return false; } - public void TrimExcess() - { - entries.TrimExcess(); - } + return false; + } - public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) - { - entries.CopyTo(array, arrayIndex); - } + public void TrimExcess() + { + entries.TrimExcess(); + } - public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() - { - return new Enumerator(this); - } + public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) + { + entries.CopyTo(array, arrayIndex); + } - IEnumerator IEnumerable.GetEnumerator() - { - return new Enumerator(this); - } + public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this); } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs b/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs index 01be52e717..3a0522e355 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs @@ -5,52 +5,51 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public static class ReadonlyDictionary { - public static class ReadonlyDictionary + private static class Empties<TKey, TValue> where TKey : notnull { - private static class Empties<TKey, TValue> where TKey : notnull - { - public static readonly ReadonlyDictionary<TKey, TValue> Instance = new ReadonlyDictionary<TKey, TValue>(); - } + public static readonly ReadonlyDictionary<TKey, TValue> Instance = new ReadonlyDictionary<TKey, TValue>(); + } - public static ReadonlyDictionary<TKey, TValue> Empty<TKey, TValue>() where TKey : notnull - { - return Empties<TKey, TValue>.Instance; - } + public static ReadonlyDictionary<TKey, TValue> Empty<TKey, TValue>() where TKey : notnull + { + return Empties<TKey, TValue>.Instance; + } - public static ReadonlyDictionary<TKey, TValue> ToReadonlyDictionary<TKey, TValue>(this Dictionary<TKey, TValue> source) where TKey : notnull + public static ReadonlyDictionary<TKey, TValue> ToReadonlyDictionary<TKey, TValue>(this Dictionary<TKey, TValue> source) where TKey : notnull + { + if (source.Count == 0) { - if (source.Count == 0) - { - return Empty<TKey, TValue>(); - } - - return new ReadonlyDictionary<TKey, TValue>(source); + return Empty<TKey, TValue>(); } - public static ReadonlyDictionary<TKey, TValue> ToReadonlyDictionary<TKey, TValue>(this IEnumerable<TValue> source, Func<TValue, TKey> keySelector) where TKey : notnull - { - var inner = source.ToDictionary(keySelector); + return new ReadonlyDictionary<TKey, TValue>(source); + } - if (inner.Count == 0) - { - return Empty<TKey, TValue>(); - } + public static ReadonlyDictionary<TKey, TValue> ToReadonlyDictionary<TKey, TValue>(this IEnumerable<TValue> source, Func<TValue, TKey> keySelector) where TKey : notnull + { + var inner = source.ToDictionary(keySelector); - return new ReadonlyDictionary<TKey, TValue>(inner); + if (inner.Count == 0) + { + return Empty<TKey, TValue>(); } - public static ReadonlyDictionary<TKey, TValue> ToReadonlyDictionary<TSource, TKey, TValue>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> elementSelector) where TKey : notnull - { - var inner = source.ToDictionary(keySelector, elementSelector); + return new ReadonlyDictionary<TKey, TValue>(inner); + } - if (inner.Count == 0) - { - return Empty<TKey, TValue>(); - } + public static ReadonlyDictionary<TKey, TValue> ToReadonlyDictionary<TSource, TKey, TValue>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> elementSelector) where TKey : notnull + { + var inner = source.ToDictionary(keySelector, elementSelector); - return new ReadonlyDictionary<TKey, TValue>(inner); + if (inner.Count == 0) + { + return Empty<TKey, TValue>(); } + + return new ReadonlyDictionary<TKey, TValue>(inner); } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary{TKey,TValue}.cs b/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary{TKey,TValue}.cs index ba21aa4a85..d003e0b2fb 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary{TKey,TValue}.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary{TKey,TValue}.cs @@ -8,91 +8,90 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public class ReadonlyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>, IEquatable<ReadonlyDictionary<TKey, TValue>> where TKey : notnull { - public class ReadonlyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>, IEquatable<ReadonlyDictionary<TKey, TValue>> where TKey : notnull - { - private static readonly Dictionary<TKey, TValue> EmptyInner = new Dictionary<TKey, TValue>(); - private readonly IDictionary<TKey, TValue> inner; + private static readonly Dictionary<TKey, TValue> EmptyInner = new Dictionary<TKey, TValue>(); + private readonly IDictionary<TKey, TValue> inner; - public TValue this[TKey key] + public TValue this[TKey key] + { + get { - get + if (!TryGetValue(key, out var value)) { - if (!TryGetValue(key, out var value)) - { - throw new KeyNotFoundException(); - } - - return value; + throw new KeyNotFoundException(); } - } - public IEnumerable<TKey> Keys - { - get => inner.Keys; + return value; } + } - public IEnumerable<TValue> Values - { - get => inner.Values; - } + public IEnumerable<TKey> Keys + { + get => inner.Keys; + } - public int Count - { - get => inner.Count; - } + public IEnumerable<TValue> Values + { + get => inner.Values; + } - public ReadonlyDictionary() - : this(EmptyInner) - { - } + public int Count + { + get => inner.Count; + } - public ReadonlyDictionary(IDictionary<TKey, TValue> inner) - { - Guard.NotNull(inner); + public ReadonlyDictionary() + : this(EmptyInner) + { + } - this.inner = inner; - } + public ReadonlyDictionary(IDictionary<TKey, TValue> inner) + { + Guard.NotNull(inner); - public bool ContainsKey(TKey key) - { - return inner.ContainsKey(key); - } + this.inner = inner; + } - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) - { - return inner.TryGetValue(key, out value); - } + public bool ContainsKey(TKey key) + { + return inner.ContainsKey(key); + } - IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() - { - return GetEnumerable(inner).GetEnumerator(); - } + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + return inner.TryGetValue(key, out value); + } - IEnumerator IEnumerable.GetEnumerator() - { - return inner.GetEnumerator(); - } + IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() + { + return GetEnumerable(inner).GetEnumerator(); + } - private static IEnumerable<TItem> GetEnumerable<TItem>(IEnumerable<TItem> collection) - { - return collection; - } + IEnumerator IEnumerable.GetEnumerator() + { + return inner.GetEnumerator(); + } - public override bool Equals(object? obj) - { - return Equals(obj as ReadonlyDictionary<TKey, TValue>); - } + private static IEnumerable<TItem> GetEnumerable<TItem>(IEnumerable<TItem> collection) + { + return collection; + } - public bool Equals(ReadonlyDictionary<TKey, TValue>? other) - { - return this.EqualsDictionary(other); - } + public override bool Equals(object? obj) + { + return Equals(obj as ReadonlyDictionary<TKey, TValue>); + } - public override int GetHashCode() - { - return this.DictionaryHashCode(); - } + public bool Equals(ReadonlyDictionary<TKey, TValue>? other) + { + return this.EqualsDictionary(other); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ReadonlyList.cs b/backend/src/Squidex.Infrastructure/Collections/ReadonlyList.cs index d0e3413b4a..df00a79626 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ReadonlyList.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ReadonlyList.cs @@ -5,42 +5,41 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public static class ReadonlyList { - public static class ReadonlyList + private static class Empties<T> { - private static class Empties<T> - { #pragma warning disable SA1401 // Fields should be private - public static readonly ReadonlyList<T> Instance = new ReadonlyList<T>(); + public static readonly ReadonlyList<T> Instance = new ReadonlyList<T>(); #pragma warning restore SA1401 // Fields should be private - } + } - public static ReadonlyList<T> Empty<T>() - { - return Empties<T>.Instance; - } + public static ReadonlyList<T> Empty<T>() + { + return Empties<T>.Instance; + } - public static ReadonlyList<T> Create<T>(params T[]? items) + public static ReadonlyList<T> Create<T>(params T[]? items) + { + if (items == null || items.Length == 0) { - if (items == null || items.Length == 0) - { - return Empty<T>(); - } - - return new ReadonlyList<T>(items.ToList()); + return Empty<T>(); } - public static ReadonlyList<T> ToReadonlyList<T>(this IEnumerable<T> source) - { - var inner = source.ToList(); + return new ReadonlyList<T>(items.ToList()); + } - if (inner.Count == 0) - { - return Empty<T>(); - } + public static ReadonlyList<T> ToReadonlyList<T>(this IEnumerable<T> source) + { + var inner = source.ToList(); - return new ReadonlyList<T>(inner); + if (inner.Count == 0) + { + return Empty<T>(); } + + return new ReadonlyList<T>(inner); } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ReadonlyList{T}.cs b/backend/src/Squidex.Infrastructure/Collections/ReadonlyList{T}.cs index 223a32d6a7..51510e48ab 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ReadonlyList{T}.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ReadonlyList{T}.cs @@ -7,35 +7,34 @@ using System.Collections.ObjectModel; -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public class ReadonlyList<T> : ReadOnlyCollection<T>, IEquatable<ReadonlyList<T>> { - public class ReadonlyList<T> : ReadOnlyCollection<T>, IEquatable<ReadonlyList<T>> + private static readonly List<T> EmptyInner = new List<T>(); + + public ReadonlyList() + : base(EmptyInner) + { + } + + public ReadonlyList(IList<T> list) + : base(list) + { + } + + public override bool Equals(object? obj) + { + return Equals(obj as ReadonlyList<T>); + } + + public virtual bool Equals(ReadonlyList<T>? other) + { + return this.EqualsList(other); + } + + public override int GetHashCode() { - private static readonly List<T> EmptyInner = new List<T>(); - - public ReadonlyList() - : base(EmptyInner) - { - } - - public ReadonlyList(IList<T> list) - : base(list) - { - } - - public override bool Equals(object? obj) - { - return Equals(obj as ReadonlyList<T>); - } - - public virtual bool Equals(ReadonlyList<T>? other) - { - return this.EqualsList(other); - } - - public override int GetHashCode() - { - return this.SequentialHashCode(); - } + return this.SequentialHashCode(); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/AggregateCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/AggregateCommandMiddleware.cs index 66cf2a923f..d59f27d534 100644 --- a/backend/src/Squidex.Infrastructure/Commands/AggregateCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/AggregateCommandMiddleware.cs @@ -5,56 +5,55 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class AggregateCommandMiddleware<TCommand, T1> : ICommandMiddleware + where TCommand : IAggregateCommand where T1 : IAggregate { - public class AggregateCommandMiddleware<TCommand, T1> : ICommandMiddleware - where TCommand : IAggregateCommand where T1 : IAggregate - { - private readonly IDomainObjectFactory domainObjectFactory; + private readonly IDomainObjectFactory domainObjectFactory; - public AggregateCommandMiddleware(IDomainObjectFactory domainObjectFactory) - { - this.domainObjectFactory = domainObjectFactory; - } + public AggregateCommandMiddleware(IDomainObjectFactory domainObjectFactory) + { + this.domainObjectFactory = domainObjectFactory; + } - public virtual async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - await ExecuteCommandAsync(context, ct); + public virtual async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + await ExecuteCommandAsync(context, ct); - await next(context, ct); - } + await next(context, ct); + } - protected async Task ExecuteCommandAsync(CommandContext context, - CancellationToken ct) + protected async Task ExecuteCommandAsync(CommandContext context, + CancellationToken ct) + { + if (context.Command is TCommand typedCommand) { - if (context.Command is TCommand typedCommand) - { - var commandResult = await ExecuteCommandAsync(typedCommand, ct); - var commandPayload = await EnrichResultAsync(context, commandResult, ct); + var commandResult = await ExecuteCommandAsync(typedCommand, ct); + var commandPayload = await EnrichResultAsync(context, commandResult, ct); - context.Complete(commandPayload); - } + context.Complete(commandPayload); } + } - protected virtual Task<object> EnrichResultAsync(CommandContext context, CommandResult result, - CancellationToken ct) - { - return Task.FromResult(result.Payload is None ? result : result.Payload); - } + protected virtual Task<object> EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) + { + return Task.FromResult(result.Payload is None ? result : result.Payload); + } - protected virtual Task<CommandResult> ExecuteCommandAsync(TCommand command, - CancellationToken ct) - { - var executable = domainObjectFactory.Create<T1>(command.AggregateId); + protected virtual Task<CommandResult> ExecuteCommandAsync(TCommand command, + CancellationToken ct) + { + var executable = domainObjectFactory.Create<T1>(command.AggregateId); - return ExecuteCommandAsync(executable, command, ct); - } + return ExecuteCommandAsync(executable, command, ct); + } - protected virtual Task<CommandResult> ExecuteCommandAsync(T1 executable, TCommand command, - CancellationToken ct) - { - return executable.ExecuteAsync(command, ct); - } + protected virtual Task<CommandResult> ExecuteCommandAsync(T1 executable, TCommand command, + CancellationToken ct) + { + return executable.ExecuteAsync(command, ct); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/CachingDomainObjectMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/CachingDomainObjectMiddleware.cs index b1dbe841e5..a8e060d7d5 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CachingDomainObjectMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CachingDomainObjectMiddleware.cs @@ -5,36 +5,35 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands -{ - public class CachingDomainObjectMiddleware<TCommand, T, TState> : AggregateCommandMiddleware<TCommand, T> - where TCommand : IAggregateCommand where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() - { - private readonly IDomainObjectCache domainObjectCache; +namespace Squidex.Infrastructure.Commands; - public CachingDomainObjectMiddleware(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) - : base(domainObjectFactory) - { - this.domainObjectCache = domainObjectCache; - } +public class CachingDomainObjectMiddleware<TCommand, T, TState> : AggregateCommandMiddleware<TCommand, T> + where TCommand : IAggregateCommand where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() +{ + private readonly IDomainObjectCache domainObjectCache; - protected override async Task<CommandResult> ExecuteCommandAsync(T executable, TCommand command, - CancellationToken ct) - { - var oldSnapshot = executable.Snapshot; + public CachingDomainObjectMiddleware(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) + : base(domainObjectFactory) + { + this.domainObjectCache = domainObjectCache; + } - var result = await base.ExecuteCommandAsync(executable, command, ct); + protected override async Task<CommandResult> ExecuteCommandAsync(T executable, TCommand command, + CancellationToken ct) + { + var oldSnapshot = executable.Snapshot; - var newSnapshot = executable.Snapshot; + var result = await base.ExecuteCommandAsync(executable, command, ct); - if (newSnapshot.Version != oldSnapshot.Version) - { - // If we are so far it is not worth to cancel the flow anymore. - await domainObjectCache.SetAsync(executable.UniqueId, oldSnapshot.Version, oldSnapshot, default); - await domainObjectCache.SetAsync(executable.UniqueId, newSnapshot.Version, newSnapshot, default); - } + var newSnapshot = executable.Snapshot; - return result; + if (newSnapshot.Version != oldSnapshot.Version) + { + // If we are so far it is not worth to cancel the flow anymore. + await domainObjectCache.SetAsync(executable.UniqueId, oldSnapshot.Version, oldSnapshot, default); + await domainObjectCache.SetAsync(executable.UniqueId, newSnapshot.Version, newSnapshot, default); } + + return result; } } diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs b/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs index 71ce7bf650..76400c55a3 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CommandContext.cs @@ -5,42 +5,41 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class CommandContext { - public sealed class CommandContext - { - public DomainId ContextId { get; } = DomainId.NewGuid(); + public DomainId ContextId { get; } = DomainId.NewGuid(); - public ICommand Command { get; } + public ICommand Command { get; } - public ICommandBus CommandBus { get; } + public ICommandBus CommandBus { get; } - public object? PlainResult { get; private set; } + public object? PlainResult { get; private set; } - public bool IsCompleted - { - get => PlainResult != null; - } + public bool IsCompleted + { + get => PlainResult != null; + } - public CommandContext(ICommand command, ICommandBus commandBus) - { - Guard.NotNull(command); - Guard.NotNull(commandBus); + public CommandContext(ICommand command, ICommandBus commandBus) + { + Guard.NotNull(command); + Guard.NotNull(commandBus); - Command = command; - CommandBus = commandBus; - } + Command = command; + CommandBus = commandBus; + } - public CommandContext Complete(object? resultValue = null) - { - PlainResult = resultValue ?? None.Value; + public CommandContext Complete(object? resultValue = null) + { + PlainResult = resultValue ?? None.Value; - return this; - } + return this; + } - public T Result<T>() - { - return (T)PlainResult!; - } + public T Result<T>() + { + return (T)PlainResult!; } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index 7af5b47625..b41edefcaf 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -7,28 +7,27 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public static class CommandExtensions { - public static class CommandExtensions - { - private static readonly NextDelegate End = (c, ct) => Task.CompletedTask; + private static readonly NextDelegate End = (c, ct) => Task.CompletedTask; - public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context, - CancellationToken ct) - { - return commandMiddleware.HandleAsync(context, End, ct); - } + public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context, + CancellationToken ct) + { + return commandMiddleware.HandleAsync(context, End, ct); + } - public static Envelope<IEvent> Migrate<T>(this Envelope<IEvent> @event, T snapshot) + public static Envelope<IEvent> Migrate<T>(this Envelope<IEvent> @event, T snapshot) + { + if (@event.Payload is IMigratedStateEvent<T> migratable) { - if (@event.Payload is IMigratedStateEvent<T> migratable) - { - var payload = migratable.Migrate(snapshot); + var payload = migratable.Migrate(snapshot); - @event = new Envelope<IEvent>(payload, @event.Headers); - } - - return @event; + @event = new Envelope<IEvent>(payload, @event.Headers); } + + return @event; } } diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandRequest.cs b/backend/src/Squidex.Infrastructure/Commands/CommandRequest.cs index 7c4410af6f..31dae43344 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CommandRequest.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CommandRequest.cs @@ -7,63 +7,62 @@ using System.Globalization; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class CommandRequest { - public sealed class CommandRequest + public IAggregateCommand Command { get; } + + public string Culture { get; } + + public string CultureUI { get; } + + public CommandRequest(IAggregateCommand command, string culture, string cultureUI) { - public IAggregateCommand Command { get; } + Command = command; + + Culture = culture; + CultureUI = cultureUI; + } - public string Culture { get; } + public static CommandRequest Create(IAggregateCommand command) + { + return new CommandRequest(command, + CultureInfo.CurrentCulture.Name, + CultureInfo.CurrentUICulture.Name); + } - public string CultureUI { get; } + public void ApplyContext() + { + var culture = GetCulture(Culture); - public CommandRequest(IAggregateCommand command, string culture, string cultureUI) + if (culture != null) { - Command = command; - - Culture = culture; - CultureUI = cultureUI; + CultureInfo.CurrentCulture = culture; } - public static CommandRequest Create(IAggregateCommand command) + var uiCulture = GetCulture(CultureUI); + + if (uiCulture != null) { - return new CommandRequest(command, - CultureInfo.CurrentCulture.Name, - CultureInfo.CurrentUICulture.Name); + CultureInfo.CurrentUICulture = uiCulture; } + } - public void ApplyContext() + private static CultureInfo? GetCulture(string name) + { + if (string.IsNullOrWhiteSpace(name)) { - var culture = GetCulture(Culture); - - if (culture != null) - { - CultureInfo.CurrentCulture = culture; - } - - var uiCulture = GetCulture(CultureUI); - - if (uiCulture != null) - { - CultureInfo.CurrentUICulture = uiCulture; - } + return null; } - private static CultureInfo? GetCulture(string name) + try { - if (string.IsNullOrWhiteSpace(name)) - { - return null; - } - - try - { - return CultureInfo.GetCultureInfo(name); - } - catch (CultureNotFoundException) - { - return null; - } + return CultureInfo.GetCultureInfo(name); + } + catch (CultureNotFoundException) + { + return null; } } } diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandResult.cs b/backend/src/Squidex.Infrastructure/Commands/CommandResult.cs index 4eb3973a2d..2b90ff6446 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CommandResult.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CommandResult.cs @@ -7,17 +7,16 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public record CommandResult(DomainId Id, long NewVersion, long OldVersion, object Payload) { - public record CommandResult(DomainId Id, long NewVersion, long OldVersion, object Payload) - { - public bool IsCreated => OldVersion < 0; + public bool IsCreated => OldVersion < 0; - public bool IsChanged => OldVersion != NewVersion; + public bool IsChanged => OldVersion != NewVersion; - public static CommandResult Empty(DomainId id, long newVersion, long oldVersion) - { - return new CommandResult(id, newVersion, oldVersion, None.Value); - } + public static CommandResult Empty(DomainId id, long newVersion, long oldVersion) + { + return new CommandResult(id, newVersion, oldVersion, None.Value); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs b/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs index 3d3422a2f3..eb1d9f9cb4 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs @@ -5,31 +5,30 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class CustomCommandMiddlewareRunner : ICommandMiddleware { - public sealed class CustomCommandMiddlewareRunner : ICommandMiddleware + private readonly IEnumerable<ICustomCommandMiddleware> extensions; + + public CustomCommandMiddlewareRunner(IEnumerable<ICustomCommandMiddleware> extensions) { - private readonly IEnumerable<ICustomCommandMiddleware> extensions; + this.extensions = extensions.Reverse().ToList(); + } - public CustomCommandMiddlewareRunner(IEnumerable<ICustomCommandMiddleware> extensions) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + foreach (var handler in extensions) { - this.extensions = extensions.Reverse().ToList(); + next = Join(handler, next); } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - foreach (var handler in extensions) - { - next = Join(handler, next); - } - - await next(context, ct); - } + await next(context, ct); + } - private static NextDelegate Join(ICommandMiddleware handler, NextDelegate next) - { - return (context, ct) => handler.HandleAsync(context, next, ct); - } + private static NextDelegate Join(ICommandMiddleware handler, NextDelegate next) + { + return (context, ct) => handler.HandleAsync(context, next, ct); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs b/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs index fffa37df55..b20283e628 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs @@ -11,85 +11,84 @@ using Squidex.Infrastructure.Json; using Squidex.Infrastructure.ObjectPool; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class DefaultDomainObjectCache : IDomainObjectCache { - public sealed class DefaultDomainObjectCache : IDomainObjectCache + private readonly DistributedCacheEntryOptions cacheOptions; + private readonly IMemoryCache cache; + private readonly IJsonSerializer serializer; + private readonly IDistributedCache distributedCache; + + public DefaultDomainObjectCache(IMemoryCache cache, IJsonSerializer serializer, IDistributedCache distributedCache, + IOptions<DomainObjectCacheOptions> options) { - private readonly DistributedCacheEntryOptions cacheOptions; - private readonly IMemoryCache cache; - private readonly IJsonSerializer serializer; - private readonly IDistributedCache distributedCache; + this.cache = cache; + this.serializer = serializer; + this.distributedCache = distributedCache; - public DefaultDomainObjectCache(IMemoryCache cache, IJsonSerializer serializer, IDistributedCache distributedCache, - IOptions<DomainObjectCacheOptions> options) + cacheOptions = new DistributedCacheEntryOptions { - this.cache = cache; - this.serializer = serializer; - this.distributedCache = distributedCache; + AbsoluteExpirationRelativeToNow = options.Value.CacheDuration + }; + } - cacheOptions = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = options.Value.CacheDuration - }; - } + public async Task<T> GetAsync<T>(DomainId id, long version, + CancellationToken ct = default) + { + var cacheKey = CacheKey(id, version); - public async Task<T> GetAsync<T>(DomainId id, long version, - CancellationToken ct = default) + if (cache.TryGetValue(cacheKey, out var found) && found is T typed) { - var cacheKey = CacheKey(id, version); - - if (cache.TryGetValue(cacheKey, out var found) && found is T typed) - { - return typed; - } + return typed; + } - var buffer = await distributedCache.GetAsync(cacheKey, ct); + var buffer = await distributedCache.GetAsync(cacheKey, ct); - if (buffer == null) - { - return default!; - } + if (buffer == null) + { + return default!; + } - try + try + { + using (var stream = new MemoryStream(buffer)) { - using (var stream = new MemoryStream(buffer)) - { - var result = serializer.Deserialize<T>(stream); + var result = serializer.Deserialize<T>(stream); - return result; - } - } - catch - { - return default!; + return result; } } - - public async Task SetAsync<T>(DomainId id, long version, T snapshot, - CancellationToken ct = default) + catch { - var cacheKey = CacheKey(id, version); + return default!; + } + } - cache.Set(cacheKey, snapshot, cacheOptions.AbsoluteExpirationRelativeToNow!.Value); + public async Task SetAsync<T>(DomainId id, long version, T snapshot, + CancellationToken ct = default) + { + var cacheKey = CacheKey(id, version); - try - { - using (var stream = DefaultPools.MemoryStream.GetStream()) - { - serializer.Serialize(snapshot, stream, true); + cache.Set(cacheKey, snapshot, cacheOptions.AbsoluteExpirationRelativeToNow!.Value); - await distributedCache.SetAsync(cacheKey, stream.ToArray(), cacheOptions, ct); - } - } - catch + try + { + using (var stream = DefaultPools.MemoryStream.GetStream()) { - return; + serializer.Serialize(snapshot, stream, true); + + await distributedCache.SetAsync(cacheKey, stream.ToArray(), cacheOptions, ct); } } - - private static string CacheKey(DomainId key, long version) + catch { - return $"{key}_{version}"; + return; } } + + private static string CacheKey(DomainId key, long version) + { + return $"{key}_{version}"; + } } diff --git a/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectFactory.cs b/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectFactory.cs index 40c511592b..1788eec373 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectFactory.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectFactory.cs @@ -10,47 +10,46 @@ #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class DefaultDomainObjectFactory : IDomainObjectFactory { - public sealed class DefaultDomainObjectFactory : IDomainObjectFactory + private readonly IServiceProvider serviceProvider; + + private static class DefaultFactory<T> { - private readonly IServiceProvider serviceProvider; + private static readonly ObjectFactory ObjectFactory = + ActivatorUtilities.CreateFactory(typeof(T), new[] { typeof(DomainId) }); - private static class DefaultFactory<T> + public static T Create(IServiceProvider serviceProvider, DomainId id) { - private static readonly ObjectFactory ObjectFactory = - ActivatorUtilities.CreateFactory(typeof(T), new[] { typeof(DomainId) }); - - public static T Create(IServiceProvider serviceProvider, DomainId id) - { - return (T)ObjectFactory(serviceProvider, new object[] { id }); - } + return (T)ObjectFactory(serviceProvider, new object[] { id }); } + } - private static class PersistenceFactory<T, TState> - { - private static readonly ObjectFactory ObjectFactory = - ActivatorUtilities.CreateFactory(typeof(T), new[] { typeof(DomainId), typeof(IPersistenceFactory<TState>) }); - - public static T Create(IServiceProvider serviceProvider, DomainId id, IPersistenceFactory<TState> persistenceFactory) - { - return (T)ObjectFactory(serviceProvider, new object[] { id, persistenceFactory }); - } - } + private static class PersistenceFactory<T, TState> + { + private static readonly ObjectFactory ObjectFactory = + ActivatorUtilities.CreateFactory(typeof(T), new[] { typeof(DomainId), typeof(IPersistenceFactory<TState>) }); - public DefaultDomainObjectFactory(IServiceProvider serviceProvider) + public static T Create(IServiceProvider serviceProvider, DomainId id, IPersistenceFactory<TState> persistenceFactory) { - this.serviceProvider = serviceProvider; + return (T)ObjectFactory(serviceProvider, new object[] { id, persistenceFactory }); } + } - public T Create<T>(DomainId id) - { - return DefaultFactory<T>.Create(serviceProvider, id); - } + public DefaultDomainObjectFactory(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } - public T Create<T, TState>(DomainId id, IPersistenceFactory<TState> factory) - { - return PersistenceFactory<T, TState>.Create(serviceProvider, id, factory); - } + public T Create<T>(DomainId id) + { + return DefaultFactory<T>.Create(serviceProvider, id); + } + + public T Create<T, TState>(DomainId id, IPersistenceFactory<TState> factory) + { + return PersistenceFactory<T, TState>.Create(serviceProvider, id, factory); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs index 40ff87ad65..37c858a355 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs @@ -5,271 +5,270 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public partial class DomainObject<T> { - public partial class DomainObject<T> + protected Task<CommandResult> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, + CancellationToken ct = default) where TCommand : ICommand { - protected Task<CommandResult> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, - CancellationToken ct = default) where TCommand : ICommand - { - EnsureCanCreate(command); - - return UpsertCoreAsync(command, handler, true, ct); - } - - protected Task<CommandResult> CreateReturn<TCommand>(TCommand command, Func<TCommand, object?> handler, - CancellationToken ct = default) where TCommand : ICommand - { - return CreateReturnAsync(command, (c, _) => - { - var result = handler(c); + EnsureCanCreate(command); - return Task.FromResult(result); - }, ct); - } + return UpsertCoreAsync(command, handler, true, ct); + } - protected Task<CommandResult> CreateAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, - CancellationToken ct = default) where TCommand : ICommand + protected Task<CommandResult> CreateReturn<TCommand>(TCommand command, Func<TCommand, object?> handler, + CancellationToken ct = default) where TCommand : ICommand + { + return CreateReturnAsync(command, (c, _) => { - EnsureCanCreate(command); + var result = handler(c); - return UpsertCoreAsync(command, async (c, ct) => - { - await handler(c, ct); + return Task.FromResult(result); + }, ct); + } - return None.Value; - }, true, ct); - } + protected Task<CommandResult> CreateAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, + CancellationToken ct = default) where TCommand : ICommand + { + EnsureCanCreate(command); - protected Task<CommandResult> Create<TCommand>(TCommand command, Action<TCommand> handler, - CancellationToken ct = default) where TCommand : ICommand + return UpsertCoreAsync(command, async (c, ct) => { - return CreateAsync(command, (c, ct) => - { - handler(c); + await handler(c, ct); - return Task.FromResult<object?>(None.Value); - }, ct); - } + return None.Value; + }, true, ct); + } - protected async Task<CommandResult> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, - CancellationToken ct = default) where TCommand : ICommand + protected Task<CommandResult> Create<TCommand>(TCommand command, Action<TCommand> handler, + CancellationToken ct = default) where TCommand : ICommand + { + return CreateAsync(command, (c, ct) => { - await EnsureCanUpdateAsync(command, ct); + handler(c); - return await UpsertCoreAsync(command, handler, false, ct); - } + return Task.FromResult<object?>(None.Value); + }, ct); + } - protected Task<CommandResult> UpdateReturn<TCommand>(TCommand command, Func<TCommand, object?> handler, - CancellationToken ct = default) where TCommand : ICommand - { - return UpdateReturnAsync(command, (c, ct) => - { - var result = handler(c); + protected async Task<CommandResult> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, + CancellationToken ct = default) where TCommand : ICommand + { + await EnsureCanUpdateAsync(command, ct); - return Task.FromResult(result); - }, ct); - } + return await UpsertCoreAsync(command, handler, false, ct); + } - protected async Task<CommandResult> UpdateAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, - CancellationToken ct = default) where TCommand : ICommand + protected Task<CommandResult> UpdateReturn<TCommand>(TCommand command, Func<TCommand, object?> handler, + CancellationToken ct = default) where TCommand : ICommand + { + return UpdateReturnAsync(command, (c, ct) => { - await EnsureCanUpdateAsync(command, ct); + var result = handler(c); - return await UpsertCoreAsync(command, async (c, ct) => - { - await handler(c, ct); + return Task.FromResult(result); + }, ct); + } - return None.Value; - }, false, ct); - } + protected async Task<CommandResult> UpdateAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, + CancellationToken ct = default) where TCommand : ICommand + { + await EnsureCanUpdateAsync(command, ct); - protected async Task<CommandResult> Update<TCommand>(TCommand command, Action<TCommand> handler, - CancellationToken ct = default) where TCommand : ICommand + return await UpsertCoreAsync(command, async (c, ct) => { - return await UpdateAsync(command, (c, _) => - { - handler(c); + await handler(c, ct); - return Task.FromResult<object?>(None.Value); - }, ct); - } + return None.Value; + }, false, ct); + } - protected async Task<CommandResult> UpsertReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, - CancellationToken ct = default) where TCommand : ICommand + protected async Task<CommandResult> Update<TCommand>(TCommand command, Action<TCommand> handler, + CancellationToken ct = default) where TCommand : ICommand + { + return await UpdateAsync(command, (c, _) => { - await EnsureCanUpsertAsync(command, ct); + handler(c); - return await UpsertCoreAsync(command, handler, true, ct); - } + return Task.FromResult<object?>(None.Value); + }, ct); + } - protected async Task<CommandResult> UpsertReturn<TCommand>(TCommand command, Func<TCommand, object?> handler, - CancellationToken ct = default) where TCommand : ICommand - { - return await UpsertReturnAsync(command, (c, _) => - { - var result = handler(c); + protected async Task<CommandResult> UpsertReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, + CancellationToken ct = default) where TCommand : ICommand + { + await EnsureCanUpsertAsync(command, ct); - return Task.FromResult(result); - }, ct); - } + return await UpsertCoreAsync(command, handler, true, ct); + } - protected async Task<CommandResult> UpsertAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, - CancellationToken ct = default) where TCommand : ICommand + protected async Task<CommandResult> UpsertReturn<TCommand>(TCommand command, Func<TCommand, object?> handler, + CancellationToken ct = default) where TCommand : ICommand + { + return await UpsertReturnAsync(command, (c, _) => { - await EnsureCanUpsertAsync(command, ct); + var result = handler(c); - return await UpsertCoreAsync(command, async (c, ct) => - { - await handler(c, ct); + return Task.FromResult(result); + }, ct); + } - return None.Value; - }, true, ct); - } + protected async Task<CommandResult> UpsertAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, + CancellationToken ct = default) where TCommand : ICommand + { + await EnsureCanUpsertAsync(command, ct); - protected async Task<CommandResult> Upsert<TCommand>(TCommand command, Action<TCommand> handler, - CancellationToken ct = default) where TCommand : ICommand + return await UpsertCoreAsync(command, async (c, ct) => { - Guard.NotNull(handler); + await handler(c, ct); - return await UpsertAsync(command, (c, _) => - { - handler(c); + return None.Value; + }, true, ct); + } - return Task.FromResult<object?>(None.Value); - }, ct); - } + protected async Task<CommandResult> Upsert<TCommand>(TCommand command, Action<TCommand> handler, + CancellationToken ct = default) where TCommand : ICommand + { + Guard.NotNull(handler); - protected async Task<CommandResult> DeletePermanentAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, - CancellationToken ct = default) where TCommand : ICommand + return await UpsertAsync(command, (c, _) => { - Guard.NotNull(handler); + handler(c); - await EnsureCanDeleteAsync(command, ct); + return Task.FromResult<object?>(None.Value); + }, ct); + } - return await DeleteCoreAsync(command, async (c, ct) => - { - await handler(c, ct); + protected async Task<CommandResult> DeletePermanentAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, + CancellationToken ct = default) where TCommand : ICommand + { + Guard.NotNull(handler); - return None.Value; - }, ct); - } + await EnsureCanDeleteAsync(command, ct); - protected async Task<CommandResult> DeletePermanent<TCommand>(TCommand command, Action<TCommand> handler, - CancellationToken ct = default) where TCommand : ICommand + return await DeleteCoreAsync(command, async (c, ct) => { - Guard.NotNull(handler); + await handler(c, ct); - return await DeletePermanentAsync(command, (c, _) => - { - handler(c); + return None.Value; + }, ct); + } - return Task.FromResult<object?>(None.Value); - }, ct); - } + protected async Task<CommandResult> DeletePermanent<TCommand>(TCommand command, Action<TCommand> handler, + CancellationToken ct = default) where TCommand : ICommand + { + Guard.NotNull(handler); - private void EnsureCanCreate<TCommand>(TCommand command) where TCommand : ICommand + return await DeletePermanentAsync(command, (c, _) => { - Guard.NotNull(command); + handler(c); - if (Version != EtagVersion.Empty && !(IsDeleted(Snapshot) && CanRecreate())) - { - throw new DomainObjectConflictException(uniqueId.ToString()); - } + return Task.FromResult<object?>(None.Value); + }, ct); + } - MatchingVersion(command); - MatchingCreateCommand(command); - } + private void EnsureCanCreate<TCommand>(TCommand command) where TCommand : ICommand + { + Guard.NotNull(command); - private async Task EnsureCanUpdateAsync<TCommand>(TCommand command, - CancellationToken ct) where TCommand : ICommand + if (Version != EtagVersion.Empty && !(IsDeleted(Snapshot) && CanRecreate())) { - Guard.NotNull(command); + throw new DomainObjectConflictException(uniqueId.ToString()); + } - await EnsureLoadedAsync(ct); + MatchingVersion(command); + MatchingCreateCommand(command); + } - NotDeleted(); - NotEmpty(); + private async Task EnsureCanUpdateAsync<TCommand>(TCommand command, + CancellationToken ct) where TCommand : ICommand + { + Guard.NotNull(command); - MatchingVersion(command); - MatchingCommand(command); - } + await EnsureLoadedAsync(ct); - private async Task EnsureCanUpsertAsync<TCommand>(TCommand command, - CancellationToken ct) where TCommand : ICommand - { - Guard.NotNull(command); + NotDeleted(); + NotEmpty(); - await EnsureLoadedAsync(ct); + MatchingVersion(command); + MatchingCommand(command); + } - if (IsDeleted(Snapshot) && !CanRecreate()) - { - throw new DomainObjectDeletedException(uniqueId.ToString()); - } + private async Task EnsureCanUpsertAsync<TCommand>(TCommand command, + CancellationToken ct) where TCommand : ICommand + { + Guard.NotNull(command); - MatchingVersion(command); + await EnsureLoadedAsync(ct); - if (Version <= EtagVersion.Empty) - { - MatchingCreateCommand(command); - } - else - { - MatchingCommand(command); - } + if (IsDeleted(Snapshot) && !CanRecreate()) + { + throw new DomainObjectDeletedException(uniqueId.ToString()); } - private async Task EnsureCanDeleteAsync<TCommand>(TCommand command, - CancellationToken ct) where TCommand : ICommand + MatchingVersion(command); + + if (Version <= EtagVersion.Empty) { - Guard.NotNull(command); + MatchingCreateCommand(command); + } + else + { + MatchingCommand(command); + } + } - await EnsureLoadedAsync(ct); + private async Task EnsureCanDeleteAsync<TCommand>(TCommand command, + CancellationToken ct) where TCommand : ICommand + { + Guard.NotNull(command); - NotEmpty(); + await EnsureLoadedAsync(ct); - MatchingVersion(command); - MatchingCommand(command); - } + NotEmpty(); + + MatchingVersion(command); + MatchingCommand(command); + } - private void NotDeleted() + private void NotDeleted() + { + if (IsDeleted(Snapshot)) { - if (IsDeleted(Snapshot)) - { - throw new DomainObjectDeletedException(uniqueId.ToString()); - } + throw new DomainObjectDeletedException(uniqueId.ToString()); } + } - private void NotEmpty() + private void NotEmpty() + { + if (Version <= EtagVersion.Empty) { - if (Version <= EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(uniqueId.ToString()); - } + throw new DomainObjectNotFoundException(uniqueId.ToString()); } + } - private void MatchingVersion<TCommand>(TCommand command) where TCommand : ICommand + private void MatchingVersion<TCommand>(TCommand command) where TCommand : ICommand + { + if (Version > EtagVersion.Empty && command.ExpectedVersion > EtagVersion.Any && Version != command.ExpectedVersion) { - if (Version > EtagVersion.Empty && command.ExpectedVersion > EtagVersion.Any && Version != command.ExpectedVersion) - { - throw new DomainObjectVersionException(uniqueId.ToString(), Version, command.ExpectedVersion); - } + throw new DomainObjectVersionException(uniqueId.ToString(), Version, command.ExpectedVersion); } + } - private void MatchingCreateCommand<TCommand>(TCommand command) where TCommand : ICommand + private void MatchingCreateCommand<TCommand>(TCommand command) where TCommand : ICommand + { + if (!CanAcceptCreation(command)) { - if (!CanAcceptCreation(command)) - { - throw new DomainException("Invalid command."); - } + throw new DomainException("Invalid command."); } + } - private void MatchingCommand<TCommand>(TCommand command) where TCommand : ICommand + private void MatchingCommand<TCommand>(TCommand command) where TCommand : ICommand + { + if (!CanAccept(command)) { - if (!CanAccept(command)) - { - throw new DomainException("Invalid command."); - } + throw new DomainException("Invalid command."); } } } diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs index a4f9012202..89a7eeb377 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs @@ -12,356 +12,355 @@ #pragma warning disable MA0056 // Do not call overridable members in constructor #pragma warning disable RECS0082 // Parameter has the same name as a member and hides it -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public abstract partial class DomainObject<T> : IAggregate where T : class, IDomainState<T>, new() { - public abstract partial class DomainObject<T> : IAggregate where T : class, IDomainState<T>, new() + private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>(); + private readonly ILogger log; + private readonly IPersistenceFactory<T> persistenceFactory; + private readonly IPersistence<T> persistence; + private readonly DomainId uniqueId; + private T snapshot = new T { Version = EtagVersion.Empty }; + private bool isLoaded; + + public DomainId UniqueId { - private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>(); - private readonly ILogger log; - private readonly IPersistenceFactory<T> persistenceFactory; - private readonly IPersistence<T> persistence; - private readonly DomainId uniqueId; - private T snapshot = new T { Version = EtagVersion.Empty }; - private bool isLoaded; - - public DomainId UniqueId - { - get => uniqueId; - } + get => uniqueId; + } - public virtual T Snapshot - { - get => snapshot; - } + public virtual T Snapshot + { + get => snapshot; + } - public virtual long Version - { - get => snapshot.Version; - } + public virtual long Version + { + get => snapshot.Version; + } - protected DomainObject(DomainId uniqueId, IPersistenceFactory<T> persistenceFactory, - ILogger log) - { - Guard.NotNull(persistenceFactory); - Guard.NotNull(log); + protected DomainObject(DomainId uniqueId, IPersistenceFactory<T> persistenceFactory, + ILogger log) + { + Guard.NotNull(persistenceFactory); + Guard.NotNull(log); - this.uniqueId = uniqueId; + this.uniqueId = uniqueId; - this.persistenceFactory = persistenceFactory; + this.persistenceFactory = persistenceFactory; - persistence = persistenceFactory.WithSnapshotsAndEventSourcing(GetType(), UniqueId, - new HandleSnapshot<T>((newSnapshot, version) => - { - newSnapshot.Version = version; + persistence = persistenceFactory.WithSnapshotsAndEventSourcing(GetType(), UniqueId, + new HandleSnapshot<T>((newSnapshot, version) => + { + newSnapshot.Version = version; - snapshot = newSnapshot; - }), - @event => + snapshot = newSnapshot; + }), + @event => + { + var (newSnapshot, changed) = ApplyEvent(@event, Snapshot, Version, false, true); + + if (changed && newSnapshot != null) { - var (newSnapshot, changed) = ApplyEvent(@event, Snapshot, Version, false, true); + snapshot = newSnapshot; + } - if (changed && newSnapshot != null) - { - snapshot = newSnapshot; - } + return true; + }); - return true; - }); + this.log = log; + } - this.log = log; + public virtual async Task<T> GetSnapshotAsync(long version, + CancellationToken ct = default) + { + if (version <= EtagVersion.Any) + { + await EnsureLoadedAsync(ct); + return Snapshot; } - public virtual async Task<T> GetSnapshotAsync(long version, - CancellationToken ct = default) + var result = new T { - if (version <= EtagVersion.Any) - { - await EnsureLoadedAsync(ct); - return Snapshot; - } + Version = EtagVersion.Empty + }; - var result = new T - { - Version = EtagVersion.Empty - }; + if (version == result.Version) + { + return result; + } + + var allEvents = persistenceFactory.WithEventSourcing(GetType(), UniqueId, @event => + { + var (newSnapshot, _) = ApplyEvent(@event, result, result.Version, false, false); - if (version == result.Version) + // Can only be null in case of errors or inconsistent streams. + if (newSnapshot != null && newSnapshot.Version <= version) { - return result; + result = newSnapshot; } - var allEvents = persistenceFactory.WithEventSourcing(GetType(), UniqueId, @event => - { - var (newSnapshot, _) = ApplyEvent(@event, result, result.Version, false, false); + return result.Version <= version; + }); - // Can only be null in case of errors or inconsistent streams. - if (newSnapshot != null && newSnapshot.Version <= version) - { - result = newSnapshot; - } + await allEvents.ReadAsync(ct: ct); - return result.Version <= version; - }); - - await allEvents.ReadAsync(ct: ct); + if (result.Version != version) + { + result = new T { Version = EtagVersion.Empty }; + } - if (result.Version != version) - { - result = new T { Version = EtagVersion.Empty }; - } + return result; + } - return result; + public virtual async Task EnsureLoadedAsync( + CancellationToken ct = default) + { + if (isLoaded) + { + return; } - public virtual async Task EnsureLoadedAsync( - CancellationToken ct = default) + await persistence.ReadAsync(ct: ct); + + if (persistence.IsSnapshotStale) { - if (isLoaded) + try { - return; + await persistence.WriteSnapshotAsync(Snapshot, default); } - - await persistence.ReadAsync(ct: ct); - - if (persistence.IsSnapshotStale) + catch (Exception ex) { - try - { - await persistence.WriteSnapshotAsync(Snapshot, default); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to repair snapshot for domain object of type {type} with ID {id}.", GetType(), UniqueId); - } + log.LogError(ex, "Failed to repair snapshot for domain object of type {type} with ID {id}.", GetType(), UniqueId); } - - isLoaded = true; } - protected void RaiseEvent(IEvent @event) + isLoaded = true; + } + + protected void RaiseEvent(IEvent @event) + { + RaiseEvent(Envelope.Create(@event)); + } + + protected virtual void RaiseEvent(Envelope<IEvent> @event) + { + Guard.NotNull(@event, nameof(@event)); + + @event.SetAggregateId(uniqueId); + + if (ApplyEvent(@event, Snapshot, Version, false, true).Success) { - RaiseEvent(Envelope.Create(@event)); + uncomittedEvents.Add(@event); } + } - protected virtual void RaiseEvent(Envelope<IEvent> @event) - { - Guard.NotNull(@event, nameof(@event)); + public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents() + { + return uncomittedEvents; + } + + public void ClearUncommittedEvents() + { + uncomittedEvents.Clear(); + } + + private async Task<CommandResult> DeleteCoreAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, + CancellationToken ct = default) where TCommand : ICommand + { + Guard.NotNull(handler); - @event.SetAggregateId(uniqueId); + var previousSnapshot = Snapshot; + var previousVersion = Version; + try + { + var result = (await handler(command, ct)) ?? None.Value; - if (ApplyEvent(@event, Snapshot, Version, false, true).Success) + if (uncomittedEvents.Count > 0) { - uncomittedEvents.Add(@event); + var deletedId = DomainId.Combine(UniqueId, DomainId.Create("deleted")); + var deletedStream = persistenceFactory.WithEventSourcing(GetType(), deletedId, null); + + // Write to the deleted stream first so we never loose this information. + await deletedStream.WriteEventsAsync(uncomittedEvents, ct); } - } - public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents() + // Cleanup the primary stream second. + await persistence.DeleteAsync(ct); + + snapshot = new T + { + Version = EtagVersion.Empty + }; + + return new CommandResult(UniqueId, Version, previousVersion, result); + } + catch { - return uncomittedEvents; + snapshot = previousSnapshot; + throw; } - - public void ClearUncommittedEvents() + finally { - uncomittedEvents.Clear(); + ClearUncommittedEvents(); } + } - private async Task<CommandResult> DeleteCoreAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, - CancellationToken ct = default) where TCommand : ICommand + private async Task<CommandResult> UpsertCoreAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, bool isCreation, + CancellationToken ct) where TCommand : ICommand + { + Guard.NotNull(handler); + + var previousSnapshot = Snapshot; + var previousVersion = Version; + try { - Guard.NotNull(handler); + var result = (await handler(command, ct)) ?? None.Value; - var previousSnapshot = Snapshot; - var previousVersion = Version; + var events = uncomittedEvents.ToArray(); try { - var result = (await handler(command, ct)) ?? None.Value; - - if (uncomittedEvents.Count > 0) - { - var deletedId = DomainId.Combine(UniqueId, DomainId.Create("deleted")); - var deletedStream = persistenceFactory.WithEventSourcing(GetType(), deletedId, null); - - // Write to the deleted stream first so we never loose this information. - await deletedStream.WriteEventsAsync(uncomittedEvents, ct); - } - - // Cleanup the primary stream second. - await persistence.DeleteAsync(ct); - - snapshot = new T - { - Version = EtagVersion.Empty - }; - - return new CommandResult(UniqueId, Version, previousVersion, result); + await WriteAsync(events, ct); } - catch + catch (InconsistentStateException ex) { + // Start from the previous, unchanged snapshot. snapshot = previousSnapshot; - throw; - } - finally - { - ClearUncommittedEvents(); - } - } - private async Task<CommandResult> UpsertCoreAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, bool isCreation, - CancellationToken ct) where TCommand : ICommand - { - Guard.NotNull(handler); + // Create commands do not load the domain object for performance reasons, therefore we ensure it is loaded. + await EnsureLoadedAsync(ct); - var previousSnapshot = Snapshot; - var previousVersion = Version; - try - { - var result = (await handler(command, ct)) ?? None.Value; + var isDeleted = IsDeleted(Snapshot); - var events = uncomittedEvents.ToArray(); - try + if (isDeleted && isCreation && CanRecreate()) { + foreach (var @event in uncomittedEvents) + { + ApplyEvent(@event, Snapshot, Version, false, true); + } + await WriteAsync(events, ct); } - catch (InconsistentStateException ex) + else if (isDeleted) { - // Start from the previous, unchanged snapshot. - snapshot = previousSnapshot; - - // Create commands do not load the domain object for performance reasons, therefore we ensure it is loaded. - await EnsureLoadedAsync(ct); - - var isDeleted = IsDeleted(Snapshot); - - if (isDeleted && isCreation && CanRecreate()) - { - foreach (var @event in uncomittedEvents) - { - ApplyEvent(@event, Snapshot, Version, false, true); - } - - await WriteAsync(events, ct); - } - else if (isDeleted) - { - throw new DomainObjectDeletedException(uniqueId.ToString()); - } - else if (isCreation) - { - throw new DomainObjectConflictException(uniqueId.ToString()); - } - else - { - throw new DomainObjectVersionException(uniqueId.ToString(), ex.VersionCurrent, ex.VersionExpected, ex); - } + throw new DomainObjectDeletedException(uniqueId.ToString()); + } + else if (isCreation) + { + throw new DomainObjectConflictException(uniqueId.ToString()); + } + else + { + throw new DomainObjectVersionException(uniqueId.ToString(), ex.VersionCurrent, ex.VersionExpected, ex); } - - isLoaded = true; - - return new CommandResult(UniqueId, Version, previousVersion, result); - } - catch - { - snapshot = previousSnapshot; - throw; - } - finally - { - ClearUncommittedEvents(); } - } - protected virtual bool CanAcceptCreation(ICommand command) - { - return true; - } - - protected virtual bool CanAccept(ICommand command) - { - return true; - } + isLoaded = true; - protected virtual bool IsDeleted(T snapshot) - { - return false; + return new CommandResult(UniqueId, Version, previousVersion, result); } - - protected virtual bool CanRecreate() + catch { - return false; + snapshot = previousSnapshot; + throw; } - - protected virtual bool CanRecreate(IEvent @event) + finally { - return false; + ClearUncommittedEvents(); } + } - private (T?, bool Success) ApplyEvent(Envelope<IEvent> @event, T snapshot, long version, bool loading, bool update) - { - if (IsDeleted(snapshot)) - { - if (!CanRecreate(@event.Payload)) - { - return default; - } + protected virtual bool CanAcceptCreation(ICommand command) + { + return true; + } - snapshot = new T - { - Version = Version - }; - } + protected virtual bool CanAccept(ICommand command) + { + return true; + } - @event = @event.Migrate(snapshot); + protected virtual bool IsDeleted(T snapshot) + { + return false; + } - var newVersion = version + 1; - var newSnapshot = Apply(snapshot, @event); + protected virtual bool CanRecreate() + { + return false; + } - var isChanged = loading || !ReferenceEquals(snapshot, newSnapshot); + protected virtual bool CanRecreate(IEvent @event) + { + return false; + } - // If we are loading events at the moment, we will always update the version. - if (isChanged) + private (T?, bool Success) ApplyEvent(Envelope<IEvent> @event, T snapshot, long version, bool loading, bool update) + { + if (IsDeleted(snapshot)) + { + if (!CanRecreate(@event.Payload)) { - newSnapshot.Version = newVersion; + return default; } - if (update) + snapshot = new T { - this.snapshot = newSnapshot; - } - - return (newSnapshot, isChanged); + Version = Version + }; } - private async Task WriteAsync(Envelope<IEvent>[] newEvents, - CancellationToken ct) + @event = @event.Migrate(snapshot); + + var newVersion = version + 1; + var newSnapshot = Apply(snapshot, @event); + + var isChanged = loading || !ReferenceEquals(snapshot, newSnapshot); + + // If we are loading events at the moment, we will always update the version. + if (isChanged) { - if (newEvents.Length > 0) - { - // Writing the events is the first step, so we can cancel it if requested by the user, but if events are written, - // we should also write the snapshots to keep both collection / database as consitent as possible. - await persistence.WriteEventsAsync(newEvents, ct); - await persistence.WriteSnapshotAsync(Snapshot, default); - } + newSnapshot.Version = newVersion; } - public async Task RebuildStateAsync( - CancellationToken ct = default) + if (update) { - await EnsureLoadedAsync(ct); + this.snapshot = newSnapshot; + } - if (Version <= EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(UniqueId.ToString()); - } + return (newSnapshot, isChanged); + } - await persistence.WriteSnapshotAsync(Snapshot, ct); + private async Task WriteAsync(Envelope<IEvent>[] newEvents, + CancellationToken ct) + { + if (newEvents.Length > 0) + { + // Writing the events is the first step, so we can cancel it if requested by the user, but if events are written, + // we should also write the snapshots to keep both collection / database as consitent as possible. + await persistence.WriteEventsAsync(newEvents, ct); + await persistence.WriteSnapshotAsync(Snapshot, default); } + } - protected virtual T Apply(T snapshot, Envelope<IEvent> @event) + public async Task RebuildStateAsync( + CancellationToken ct = default) + { + await EnsureLoadedAsync(ct); + + if (Version <= EtagVersion.Empty) { - return snapshot.Apply(@event); + throw new DomainObjectNotFoundException(UniqueId.ToString()); } - public abstract Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct); + await persistence.WriteSnapshotAsync(Snapshot, ct); } + + protected virtual T Apply(T snapshot, Envelope<IEvent> @event) + { + return snapshot.Apply(@event); + } + + public abstract Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct); } diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectCacheOptions.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectCacheOptions.cs index 22d93f580a..c7e88a7bc8 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectCacheOptions.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectCacheOptions.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class DomainObjectCacheOptions { - public sealed class DomainObjectCacheOptions - { - public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(10); - } + public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(10); } diff --git a/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs index 93065d954a..4306bece56 100644 --- a/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs @@ -7,21 +7,20 @@ using NodaTime; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class EnrichWithTimestampCommandMiddleware : ICommandMiddleware { - public sealed class EnrichWithTimestampCommandMiddleware : ICommandMiddleware - { - public IClock Clock { get; set; } = SystemClock.Instance; + public IClock Clock { get; set; } = SystemClock.Instance; - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is ITimestampCommand timestampCommand) { - if (context.Command is ITimestampCommand timestampCommand) - { - timestampCommand.Timestamp = Clock.GetCurrentInstant(); - } - - return next(context, ct); + timestampCommand.Timestamp = Clock.GetCurrentInstant(); } + + return next(context, ct); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/IAggregate.cs b/backend/src/Squidex.Infrastructure/Commands/IAggregate.cs index d15f4683f5..a400e49549 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IAggregate.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IAggregate.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface IAggregate { - public interface IAggregate - { - Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct); - } + Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct); } diff --git a/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs b/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs index fd1ccd7c83..432fb9f189 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface IAggregateCommand : ICommand { - public interface IAggregateCommand : ICommand - { - DomainId AggregateId { get; } - } + DomainId AggregateId { get; } } diff --git a/backend/src/Squidex.Infrastructure/Commands/ICommand.cs b/backend/src/Squidex.Infrastructure/Commands/ICommand.cs index 890a642b68..a1555296d0 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ICommand.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ICommand.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface ICommand { - public interface ICommand - { - long ExpectedVersion { get; set; } - } + long ExpectedVersion { get; set; } } diff --git a/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs index 3443b52a5a..fd796e0260 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface ICommandBus { - public interface ICommandBus - { - Task<CommandContext> PublishAsync(ICommand command, - CancellationToken ct); - } + Task<CommandContext> PublishAsync(ICommand command, + CancellationToken ct); } diff --git a/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs index 5924dc5f84..d690c701f5 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs @@ -7,13 +7,12 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.Commands -{ - public delegate Task NextDelegate(CommandContext context, CancellationToken ct); +namespace Squidex.Infrastructure.Commands; + +public delegate Task NextDelegate(CommandContext context, CancellationToken ct); - public interface ICommandMiddleware - { - Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct); - } +public interface ICommandMiddleware +{ + Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct); } diff --git a/backend/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs index e0413288fb..7c46ed7f10 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ICustomCommandMiddleware.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface ICustomCommandMiddleware : ICommandMiddleware { - public interface ICustomCommandMiddleware : ICommandMiddleware - { - } } diff --git a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectCache.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectCache.cs index 2fe65998f1..e10cb7d3a5 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectCache.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectCache.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface IDomainObjectCache { - public interface IDomainObjectCache - { - Task<T> GetAsync<T>(DomainId id, long version, - CancellationToken ct = default); + Task<T> GetAsync<T>(DomainId id, long version, + CancellationToken ct = default); - Task SetAsync<T>(DomainId id, long version, T snapshot, - CancellationToken ct = default); - } + Task SetAsync<T>(DomainId id, long version, T snapshot, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectFactory.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectFactory.cs index abfea658ab..f770bca395 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectFactory.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectFactory.cs @@ -7,12 +7,11 @@ using Squidex.Infrastructure.States; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface IDomainObjectFactory { - public interface IDomainObjectFactory - { - T Create<T>(DomainId id); + T Create<T>(DomainId id); - T Create<T, TState>(DomainId id, IPersistenceFactory<TState> factory); - } + T Create<T, TState>(DomainId id, IPersistenceFactory<TState> factory); } diff --git a/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs index 50d43920e7..e7aa82729d 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs @@ -7,12 +7,11 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface IDomainState<out T> { - public interface IDomainState<out T> - { - long Version { get; set; } + long Version { get; set; } - T Apply(Envelope<IEvent> @event); - } + T Apply(Envelope<IEvent> @event); } diff --git a/backend/src/Squidex.Infrastructure/Commands/IMigratedStateEvent.cs b/backend/src/Squidex.Infrastructure/Commands/IMigratedStateEvent.cs index a3329452d1..055d2051b5 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IMigratedStateEvent.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IMigratedStateEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface IMigratedStateEvent<in T> { - public interface IMigratedStateEvent<in T> - { - IEvent Migrate(T state); - } + IEvent Migrate(T state); } diff --git a/backend/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs b/backend/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs index 75f5a7ab81..56396ef65d 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ITimestampCommand.cs @@ -7,10 +7,9 @@ using NodaTime; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public interface ITimestampCommand : ICommand { - public interface ITimestampCommand : ICommand - { - Instant Timestamp { get; set; } - } + Instant Timestamp { get; set; } } diff --git a/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs index 9ec067f916..1963f7565e 100644 --- a/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs +++ b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs @@ -5,41 +5,40 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands -{ - public sealed class InMemoryCommandBus : ICommandBus - { - private readonly NextDelegate pipeline; - - public InMemoryCommandBus(IEnumerable<ICommandMiddleware> middlewares) - { - var reverseMiddlewares = middlewares.Reverse().ToList(); +namespace Squidex.Infrastructure.Commands; - NextDelegate next = (c, ct) => Task.CompletedTask; +public sealed class InMemoryCommandBus : ICommandBus +{ + private readonly NextDelegate pipeline; - foreach (var middleware in middlewares.Reverse()) - { - next = Create(next, middleware); - } + public InMemoryCommandBus(IEnumerable<ICommandMiddleware> middlewares) + { + var reverseMiddlewares = middlewares.Reverse().ToList(); - pipeline = next; - } + NextDelegate next = (c, ct) => Task.CompletedTask; - private static NextDelegate Create(NextDelegate next, ICommandMiddleware middleware) + foreach (var middleware in middlewares.Reverse()) { - return (c, ct) => middleware.HandleAsync(c, next, ct); + next = Create(next, middleware); } - public async Task<CommandContext> PublishAsync(ICommand command, - CancellationToken ct) - { - Guard.NotNull(command); + pipeline = next; + } - var context = new CommandContext(command, this); + private static NextDelegate Create(NextDelegate next, ICommandMiddleware middleware) + { + return (c, ct) => middleware.HandleAsync(c, next, ct); + } - await pipeline(context, ct); + public async Task<CommandContext> PublishAsync(ICommand command, + CancellationToken ct) + { + Guard.NotNull(command); - return context; - } + var context = new CommandContext(command, this); + + await pipeline(context, ct); + + return context; } } diff --git a/backend/src/Squidex.Infrastructure/Commands/Is.cs b/backend/src/Squidex.Infrastructure/Commands/Is.cs index 6ae7c785a0..483c0fe092 100644 --- a/backend/src/Squidex.Infrastructure/Commands/Is.cs +++ b/backend/src/Squidex.Infrastructure/Commands/Is.cs @@ -7,38 +7,37 @@ using System.Diagnostics.CodeAnalysis; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public static class Is { - public static class Is + public static bool Change<T>(T oldValue, T newValue) + { + return !Equals(oldValue, newValue); + } + + public static bool OptionalChange<T>(T oldValue, [NotNullWhen(true)] T? newValue) where T : struct + { + return newValue != null && !Equals(oldValue, newValue.Value); + } + + public static bool OptionalChange<T>(T oldValue, [NotNullWhen(true)] T? newValue) where T : class + { + return newValue != null && !Equals(oldValue, newValue); + } + + public static bool OptionalChange(string oldValue, [NotNullWhen(true)] string? newValue) + { + return !string.IsNullOrWhiteSpace(newValue) && !string.Equals(oldValue, newValue, StringComparison.Ordinal); + } + + public static bool OptionalSetChange<T>(ISet<T> oldValue, [NotNullWhen(true)] ISet<T>? newValue) + { + return newValue != null && !newValue.SetEquals(oldValue); + } + + public static bool OptionalMapChange<TKey, TValue>(IReadOnlyDictionary<TKey, TValue> oldValue, [NotNullWhen(true)] IReadOnlyDictionary<TKey, TValue>? newValue) where TKey : notnull { - public static bool Change<T>(T oldValue, T newValue) - { - return !Equals(oldValue, newValue); - } - - public static bool OptionalChange<T>(T oldValue, [NotNullWhen(true)] T? newValue) where T : struct - { - return newValue != null && !Equals(oldValue, newValue.Value); - } - - public static bool OptionalChange<T>(T oldValue, [NotNullWhen(true)] T? newValue) where T : class - { - return newValue != null && !Equals(oldValue, newValue); - } - - public static bool OptionalChange(string oldValue, [NotNullWhen(true)] string? newValue) - { - return !string.IsNullOrWhiteSpace(newValue) && !string.Equals(oldValue, newValue, StringComparison.Ordinal); - } - - public static bool OptionalSetChange<T>(ISet<T> oldValue, [NotNullWhen(true)] ISet<T>? newValue) - { - return newValue != null && !newValue.SetEquals(oldValue); - } - - public static bool OptionalMapChange<TKey, TValue>(IReadOnlyDictionary<TKey, TValue> oldValue, [NotNullWhen(true)] IReadOnlyDictionary<TKey, TValue>? newValue) where TKey : notnull - { - return newValue != null && !newValue.EqualsDictionary(oldValue); - } + return newValue != null && !newValue.EqualsDictionary(oldValue); } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs index 4b503d3ea2..fae3d07cf5 100644 --- a/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs @@ -7,51 +7,50 @@ using Microsoft.Extensions.Logging; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class LogCommandMiddleware : ICommandMiddleware { - public sealed class LogCommandMiddleware : ICommandMiddleware + private readonly ILogger<LogCommandMiddleware> log; + + public LogCommandMiddleware(ILogger<LogCommandMiddleware> log) { - private readonly ILogger<LogCommandMiddleware> log; + this.log = log; + } - public LogCommandMiddleware(ILogger<LogCommandMiddleware> log) - { - this.log = log; - } + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + var type = context.Command.GetType(); - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + try { - var type = context.Command.GetType(); + if (log.IsEnabled(LogLevel.Debug)) + { + log.LogDebug("Command {command} with ID {id} started.", type, context.ContextId); + } + var watch = ValueStopwatch.StartNew(); try { - if (log.IsEnabled(LogLevel.Debug)) - { - log.LogDebug("Command {command} with ID {id} started.", type, context.ContextId); - } - - var watch = ValueStopwatch.StartNew(); - try - { - await next(context, ct); - - log.LogInformation("Command {command} with ID {id} succeeded.", type, context.ContextId); - } - finally - { - log.LogInformation("Command {command} with ID {id} completed after {time}ms.", type, context.ContextId, watch.Stop()); - } + await next(context, ct); + + log.LogInformation("Command {command} with ID {id} succeeded.", type, context.ContextId); } - catch (Exception ex) + finally { - log.LogError(ex, "Command {command} with ID {id} failed.", type, context.ContextId); - throw; + log.LogInformation("Command {command} with ID {id} completed after {time}ms.", type, context.ContextId, watch.Stop()); } + } + catch (Exception ex) + { + log.LogError(ex, "Command {command} with ID {id} failed.", type, context.ContextId); + throw; + } - if (!context.IsCompleted) - { - log.LogCritical("Command {command} with ID {id} not handled.", type, context.ContextId); - } + if (!context.IsCompleted) + { + log.LogCritical("Command {command} with ID {id} not handled.", type, context.ContextId); } } } diff --git a/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs index 5e7458e40e..1ec5faac9a 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs @@ -8,26 +8,25 @@ using Microsoft.Extensions.Options; using Squidex.Infrastructure.Translations; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class ReadonlyCommandMiddleware : ICommandMiddleware { - public sealed class ReadonlyCommandMiddleware : ICommandMiddleware + private readonly ReadonlyOptions options; + + public ReadonlyCommandMiddleware(IOptions<ReadonlyOptions> options) { - private readonly ReadonlyOptions options; + this.options = options.Value; + } - public ReadonlyCommandMiddleware(IOptions<ReadonlyOptions> options) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (options.IsReadonly) { - this.options = options.Value; + throw new DomainException(T.Get("common.readonlyMode")); } - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - if (options.IsReadonly) - { - throw new DomainException(T.Get("common.readonlyMode")); - } - - return next(context, ct); - } + return next(context, ct); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs b/backend/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs index b5682c63dc..0fefec51a9 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ReadonlyOptions.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public sealed class ReadonlyOptions { - public sealed class ReadonlyOptions - { - public bool IsReadonly { get; set; } - } + public bool IsReadonly { get; set; } } diff --git a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs index 2806222972..1d1ad422ad 100644 --- a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs +++ b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs @@ -13,156 +13,155 @@ using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class Rebuilder { - public class Rebuilder + private readonly IDomainObjectFactory domainObjectFactory; + private readonly ILocalCache localCache; + private readonly IEventStore eventStore; + private readonly IServiceProvider serviceProvider; + private readonly ILogger<Rebuilder> log; + + public Rebuilder( + IDomainObjectFactory domainObjectFactory, + ILocalCache localCache, + IEventStore eventStore, + IServiceProvider serviceProvider, + ILogger<Rebuilder> log) { - private readonly IDomainObjectFactory domainObjectFactory; - private readonly ILocalCache localCache; - private readonly IEventStore eventStore; - private readonly IServiceProvider serviceProvider; - private readonly ILogger<Rebuilder> log; - - public Rebuilder( - IDomainObjectFactory domainObjectFactory, - ILocalCache localCache, - IEventStore eventStore, - IServiceProvider serviceProvider, - ILogger<Rebuilder> log) - { - this.eventStore = eventStore; - this.serviceProvider = serviceProvider; - this.domainObjectFactory = domainObjectFactory; - this.localCache = localCache; - this.log = log; - } + this.eventStore = eventStore; + this.serviceProvider = serviceProvider; + this.domainObjectFactory = domainObjectFactory; + this.localCache = localCache; + this.log = log; + } - public virtual Task RebuildAsync<T, TState>(string filter, int batchSize, - CancellationToken ct = default) - where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() - { - return RebuildAsync<T, TState>(filter, batchSize, 0, ct); - } + public virtual Task RebuildAsync<T, TState>(string filter, int batchSize, + CancellationToken ct = default) + where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() + { + return RebuildAsync<T, TState>(filter, batchSize, 0, ct); + } - public virtual async Task RebuildAsync<T, TState>(string filter, int batchSize, double errorThreshold, - CancellationToken ct = default) - where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() - { - await ClearAsync<TState>(); + public virtual async Task RebuildAsync<T, TState>(string filter, int batchSize, double errorThreshold, + CancellationToken ct = default) + where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() + { + await ClearAsync<TState>(); - var ids = eventStore.QueryAllAsync(filter, ct: ct).Select(x => x.Data.Headers.AggregateId()); + var ids = eventStore.QueryAllAsync(filter, ct: ct).Select(x => x.Data.Headers.AggregateId()); - await InsertManyAsync<T, TState>(ids, batchSize, errorThreshold, ct); - } + await InsertManyAsync<T, TState>(ids, batchSize, errorThreshold, ct); + } - public virtual Task InsertManyAsync<T, TState>(IEnumerable<DomainId> source, int batchSize, - CancellationToken ct = default) - where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() - { - return InsertManyAsync<T, TState>(source, batchSize, 0, ct); - } + public virtual Task InsertManyAsync<T, TState>(IEnumerable<DomainId> source, int batchSize, + CancellationToken ct = default) + where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() + { + return InsertManyAsync<T, TState>(source, batchSize, 0, ct); + } - public virtual async Task InsertManyAsync<T, TState>(IEnumerable<DomainId> source, int batchSize, double errorThreshold = 0, - CancellationToken ct = default) - where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() - { - Guard.NotNull(source); + public virtual async Task InsertManyAsync<T, TState>(IEnumerable<DomainId> source, int batchSize, double errorThreshold = 0, + CancellationToken ct = default) + where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() + { + Guard.NotNull(source); - var ids = source.ToAsyncEnumerable(); + var ids = source.ToAsyncEnumerable(); - await InsertManyAsync<T, TState>(ids, batchSize, errorThreshold, ct); - } + await InsertManyAsync<T, TState>(ids, batchSize, errorThreshold, ct); + } - private async Task InsertManyAsync<T, TState>(IAsyncEnumerable<DomainId> source, int batchSize, double errorThreshold, - CancellationToken ct = default) - where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() - { - var store = serviceProvider.GetRequiredService<IStore<TState>>(); + private async Task InsertManyAsync<T, TState>(IAsyncEnumerable<DomainId> source, int batchSize, double errorThreshold, + CancellationToken ct = default) + where T : DomainObject<TState> where TState : class, IDomainState<TState>, new() + { + var store = serviceProvider.GetRequiredService<IStore<TState>>(); - var parallelism = Environment.ProcessorCount; + var parallelism = Environment.ProcessorCount; - var handledIds = new HashSet<DomainId>(); - var handlerErrors = 0; + var handledIds = new HashSet<DomainId>(); + var handlerErrors = 0; - using (localCache.StartContext()) + using (localCache.StartContext()) + { + var workerBlock = new ActionBlock<DomainId[]>(async ids => { - var workerBlock = new ActionBlock<DomainId[]>(async ids => + try { - try + await using (var context = store.WithBatchContext(typeof(T))) { - await using (var context = store.WithBatchContext(typeof(T))) + await context.LoadAsync(ids); + + foreach (var id in ids) { - await context.LoadAsync(ids); + try + { + var domainObject = domainObjectFactory.Create<T, TState>(id, context); - foreach (var id in ids) + await domainObject.RebuildStateAsync(ct); + } + catch (DomainObjectNotFoundException) + { + return; + } + catch (Exception ex) { - try - { - var domainObject = domainObjectFactory.Create<T, TState>(id, context); - - await domainObject.RebuildStateAsync(ct); - } - catch (DomainObjectNotFoundException) - { - return; - } - catch (Exception ex) - { - log.LogWarning(ex, "Found corrupt domain object of type {type} with ID {id}.", typeof(T), id); - Interlocked.Increment(ref handlerErrors); - } + log.LogWarning(ex, "Found corrupt domain object of type {type} with ID {id}.", typeof(T), id); + Interlocked.Increment(ref handlerErrors); } } } - catch (OperationCanceledException ex) - { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); - } - }, - new ExecutionDataflowBlockOptions + } + catch (OperationCanceledException ex) { - MaxDegreeOfParallelism = parallelism, - MaxMessagesPerTask = 10, - BoundedCapacity = parallelism - }); + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); + } + }, + new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = parallelism, + MaxMessagesPerTask = 10, + BoundedCapacity = parallelism + }); - var batchBlock = new BatchBlock<DomainId>(batchSize, new GroupingDataflowBlockOptions - { - BoundedCapacity = batchSize - }); + var batchBlock = new BatchBlock<DomainId>(batchSize, new GroupingDataflowBlockOptions + { + BoundedCapacity = batchSize + }); - batchBlock.BidirectionalLinkTo(workerBlock); + batchBlock.BidirectionalLinkTo(workerBlock); - await foreach (var id in source.WithCancellation(ct)) + await foreach (var id in source.WithCancellation(ct)) + { + if (handledIds.Add(id)) { - if (handledIds.Add(id)) + if (!await batchBlock.SendAsync(id, ct)) { - if (!await batchBlock.SendAsync(id, ct)) - { - break; - } + break; } } - - batchBlock.Complete(); - - await workerBlock.Completion; } - var errorRate = (double)handlerErrors / handledIds.Count; + batchBlock.Complete(); - if (errorRate > errorThreshold) - { - ThrowHelper.InvalidOperationException($"Error rate of {errorRate} is above threshold {errorThreshold}."); - } + await workerBlock.Completion; } - private async Task ClearAsync<TState>() where TState : class, IDomainState<TState>, new() - { - var store = serviceProvider.GetRequiredService<IStore<TState>>(); + var errorRate = (double)handlerErrors / handledIds.Count; - await store.ClearSnapshotsAsync(); + if (errorRate > errorThreshold) + { + ThrowHelper.InvalidOperationException($"Error rate of {errorRate} is above threshold {errorThreshold}."); } } + + private async Task ClearAsync<TState>() where TState : class, IDomainState<TState>, new() + { + var store = serviceProvider.GetRequiredService<IStore<TState>>(); + + await store.ClearSnapshotsAsync(); + } } diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/Diagnoser.cs b/backend/src/Squidex.Infrastructure/Diagnostics/Diagnoser.cs index e50d368661..10d78b5ac0 100644 --- a/backend/src/Squidex.Infrastructure/Diagnostics/Diagnoser.cs +++ b/backend/src/Squidex.Infrastructure/Diagnostics/Diagnoser.cs @@ -10,147 +10,146 @@ using Squidex.Assets; using Squidex.Hosting; -namespace Squidex.Infrastructure.Diagnostics +namespace Squidex.Infrastructure.Diagnostics; + +public sealed class Diagnoser : IInitializable { - public sealed class Diagnoser : IInitializable - { - private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(30); - private readonly DiagnoserOptions options; - private readonly IAssetStore assetStore; - private Task? scheduledGcDumpTask; - private Task? scheduledDumpTask; - private Timer? timer; + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(30); + private readonly DiagnoserOptions options; + private readonly IAssetStore assetStore; + private Task? scheduledGcDumpTask; + private Task? scheduledDumpTask; + private Timer? timer; - public int Order => int.MaxValue; + public int Order => int.MaxValue; + + public Diagnoser(IOptions<DiagnoserOptions> options, IAssetStore assetStore) + { + this.options = options.Value; + this.assetStore = assetStore; + } - public Diagnoser(IOptions<DiagnoserOptions> options, IAssetStore assetStore) + public Task InitializeAsync( + CancellationToken ct) + { + if (options.DumpTriggerInMB > 0 || options.GCDumpTriggerInMB > 0) { - this.options = options.Value; - this.assetStore = assetStore; + timer = new Timer(CollectDump); + timer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(5)); } - public Task InitializeAsync( - CancellationToken ct) + return Task.CompletedTask; + } + + public Task ReleaseAsync( + CancellationToken ct) + { + var tasks = new List<Task>(); + + if (timer != null) { - if (options.DumpTriggerInMB > 0 || options.GCDumpTriggerInMB > 0) - { - timer = new Timer(CollectDump); - timer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(5)); - } + tasks.Add(timer.DisposeAsync().AsTask()); + } - return Task.CompletedTask; + if (scheduledDumpTask != null) + { + tasks.Add(scheduledDumpTask); } - public Task ReleaseAsync( - CancellationToken ct) + if (scheduledGcDumpTask != null) { - var tasks = new List<Task>(); + tasks.Add(scheduledGcDumpTask); + } - if (timer != null) - { - tasks.Add(timer.DisposeAsync().AsTask()); - } + return Task.WhenAll(tasks); + } - if (scheduledDumpTask != null) - { - tasks.Add(scheduledDumpTask); - } + private void CollectDump(object? state) + { + try + { + var workingSet = Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024); - if (scheduledGcDumpTask != null) + if (options.DumpTriggerInMB > 0 && workingSet > options.DumpTriggerInMB && scheduledDumpTask == null) { - tasks.Add(scheduledGcDumpTask); + scheduledDumpTask = CreateDumpAsync(); } - return Task.WhenAll(tasks); - } - - private void CollectDump(object? state) - { - try + if (options.GCDumpTriggerInMB > 0 && workingSet > options.GCDumpTriggerInMB && scheduledGcDumpTask == null) { - var workingSet = Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024); - - if (options.DumpTriggerInMB > 0 && workingSet > options.DumpTriggerInMB && scheduledDumpTask == null) - { - scheduledDumpTask = CreateDumpAsync(); - } - - if (options.GCDumpTriggerInMB > 0 && workingSet > options.GCDumpTriggerInMB && scheduledGcDumpTask == null) - { - scheduledGcDumpTask = CreateGCDumpAsync(); - } + scheduledGcDumpTask = CreateGCDumpAsync(); } + } #pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - catch + catch #pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body - { - } - } - - public Task<bool> CreateDumpAsync( - CancellationToken ct = default) { - return CreateDumpAsync(options.DumpTool, "dump", ct); } + } - public Task<bool> CreateGCDumpAsync( - CancellationToken ct = default) - { - return CreateDumpAsync(options.GcDumpTool, "gcdump", ct); - } + public Task<bool> CreateDumpAsync( + CancellationToken ct = default) + { + return CreateDumpAsync(options.DumpTool, "dump", ct); + } + + public Task<bool> CreateGCDumpAsync( + CancellationToken ct = default) + { + return CreateDumpAsync(options.GcDumpTool, "gcdump", ct); + } - private async Task<bool> CreateDumpAsync(string? tool, string extension, - CancellationToken ct = default) + private async Task<bool> CreateDumpAsync(string? tool, string extension, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(tool)) { - if (string.IsNullOrWhiteSpace(tool)) - { - return false; - } + return false; + } - using var combined = CancellationTokenSource.CreateLinkedTokenSource(ct); + using var combined = CancellationTokenSource.CreateLinkedTokenSource(ct); - // Enforce a hard timeout. - combined.CancelAfter(DefaultTimeout); + // Enforce a hard timeout. + combined.CancelAfter(DefaultTimeout); - var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); - var writtenFile = $"{tempPath}.{extension}"; - try - { - using var process = new Process(); - process.StartInfo.Arguments = $"collect -p {Environment.ProcessId} -o {tempPath}"; - process.StartInfo.FileName = tool; - process.StartInfo.UseShellExecute = false; - process.Start(); + var writtenFile = $"{tempPath}.{extension}"; + try + { + using var process = new Process(); + process.StartInfo.Arguments = $"collect -p {Environment.ProcessId} -o {tempPath}"; + process.StartInfo.FileName = tool; + process.StartInfo.UseShellExecute = false; + process.Start(); - await process.WaitForExitAsync(combined.Token); + await process.WaitForExitAsync(combined.Token); - if (process.ExitCode != 0) - { - ThrowHelper.InvalidOperationException($"Failed to execute tool. Got exit code: {process.ExitCode}."); - } + if (process.ExitCode != 0) + { + ThrowHelper.InvalidOperationException($"Failed to execute tool. Got exit code: {process.ExitCode}."); + } - await using (var fs = new FileStream(writtenFile, FileMode.Open)) - { - var name = $"diagnostics/{extension}/{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}.{extension}"; + await using (var fs = new FileStream(writtenFile, FileMode.Open)) + { + var name = $"diagnostics/{extension}/{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}.{extension}"; - await assetStore.UploadAsync(name, fs, true, combined.Token); - } + await assetStore.UploadAsync(name, fs, true, combined.Token); } - finally + } + finally + { + try { - try - { - File.Delete(tempPath); - } + File.Delete(tempPath); + } #pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - catch + catch #pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body - { - } + { } - - return true; } + + return true; } } diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/DiagnoserOptions.cs b/backend/src/Squidex.Infrastructure/Diagnostics/DiagnoserOptions.cs index 5104e7362d..057c74adfc 100644 --- a/backend/src/Squidex.Infrastructure/Diagnostics/DiagnoserOptions.cs +++ b/backend/src/Squidex.Infrastructure/Diagnostics/DiagnoserOptions.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Diagnostics +namespace Squidex.Infrastructure.Diagnostics; + +public sealed class DiagnoserOptions { - public sealed class DiagnoserOptions - { - public string? GcDumpTool { get; set; } + public string? GcDumpTool { get; set; } - public string? DumpTool { get; set; } + public string? DumpTool { get; set; } - public int GCDumpTriggerInMB { get; set; } + public int GCDumpTriggerInMB { get; set; } - public int DumpTriggerInMB { get; set; } - } + public int DumpTriggerInMB { get; set; } } diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs index d66647fbed..ded9e6c72f 100644 --- a/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs @@ -9,42 +9,41 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class GCHealthCheck : IHealthCheck - { - private readonly long threshold; - - public GCHealthCheck(IOptions<GCHealthCheckOptions> options) - { - threshold = 1024 * 1024 * options.Value.ThresholdInMB; - } - - public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, - CancellationToken cancellationToken = default) - { - var workingSet = Process.GetCurrentProcess().WorkingSet64; +namespace Squidex.Infrastructure.Diagnostics; - var heapSize = GC.GetTotalMemory(false); +public sealed class GCHealthCheck : IHealthCheck +{ + private readonly long threshold; - var data = new Dictionary<string, object> - { - { "Gen0CollectionCount", GC.CollectionCount(0) }, - { "Gen1CollectionCount", GC.CollectionCount(1) }, - { "Gen2CollectionCount", GC.CollectionCount(2) }, - { "HeapSizeBytes", heapSize }, - { "HeapSizeString", heapSize.ToReadableSize() }, - { "WorkingSetBytes", workingSet }, - { "WorkingSetString", workingSet.ToReadableSize() } - }; + public GCHealthCheck(IOptions<GCHealthCheckOptions> options) + { + threshold = 1024 * 1024 * options.Value.ThresholdInMB; + } - var status = workingSet < threshold ? - HealthStatus.Healthy : - HealthStatus.Unhealthy; + public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) + { + var workingSet = Process.GetCurrentProcess().WorkingSet64; - var message = $"Application must consume less than {threshold.ToReadableSize()} memory."; + var heapSize = GC.GetTotalMemory(false); - return Task.FromResult(new HealthCheckResult(status, message, data: data)); - } + var data = new Dictionary<string, object> + { + { "Gen0CollectionCount", GC.CollectionCount(0) }, + { "Gen1CollectionCount", GC.CollectionCount(1) }, + { "Gen2CollectionCount", GC.CollectionCount(2) }, + { "HeapSizeBytes", heapSize }, + { "HeapSizeString", heapSize.ToReadableSize() }, + { "WorkingSetBytes", workingSet }, + { "WorkingSetString", workingSet.ToReadableSize() } + }; + + var status = workingSet < threshold ? + HealthStatus.Healthy : + HealthStatus.Unhealthy; + + var message = $"Application must consume less than {threshold.ToReadableSize()} memory."; + + return Task.FromResult(new HealthCheckResult(status, message, data: data)); } } diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs index 63434d1c1b..337378b881 100644 --- a/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs +++ b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Diagnostics +namespace Squidex.Infrastructure.Diagnostics; + +public sealed class GCHealthCheckOptions { - public sealed class GCHealthCheckOptions - { - public long ThresholdInMB { get; set; } = 8192; - } + public long ThresholdInMB { get; set; } = 8192; } diff --git a/backend/src/Squidex.Infrastructure/DisposableObjectBase.cs b/backend/src/Squidex.Infrastructure/DisposableObjectBase.cs index 6eb711fbfb..6c196cb71c 100644 --- a/backend/src/Squidex.Infrastructure/DisposableObjectBase.cs +++ b/backend/src/Squidex.Infrastructure/DisposableObjectBase.cs @@ -5,48 +5,47 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public abstract class DisposableObjectBase : IDisposable { - public abstract class DisposableObjectBase : IDisposable + private readonly object disposeLock = new object(); + private bool isDisposed; + + public bool IsDisposed => isDisposed; + + public void Dispose() { - private readonly object disposeLock = new object(); - private bool isDisposed; + Dispose(true); - public bool IsDisposed => isDisposed; + GC.SuppressFinalize(this); + } - public void Dispose() + protected void Dispose(bool disposing) + { + if (isDisposed) { - Dispose(true); - - GC.SuppressFinalize(this); + return; } - protected void Dispose(bool disposing) + lock (disposeLock) { - if (isDisposed) + if (!isDisposed) { - return; + DisposeObject(disposing); } - - lock (disposeLock) - { - if (!isDisposed) - { - DisposeObject(disposing); - } - } - - isDisposed = true; } - protected abstract void DisposeObject(bool disposing); + isDisposed = true; + } + + protected abstract void DisposeObject(bool disposing); - protected void ThrowIfDisposed() + protected void ThrowIfDisposed() + { + if (isDisposed) { - if (isDisposed) - { - throw new ObjectDisposedException(GetType().Name); - } + throw new ObjectDisposedException(GetType().Name); } } } diff --git a/backend/src/Squidex.Infrastructure/DomainException.cs b/backend/src/Squidex.Infrastructure/DomainException.cs index cfe64b85bb..5b1e8b67ca 100644 --- a/backend/src/Squidex.Infrastructure/DomainException.cs +++ b/backend/src/Squidex.Infrastructure/DomainException.cs @@ -7,35 +7,34 @@ using System.Runtime.Serialization; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[Serializable] +public class DomainException : Exception { - [Serializable] - public class DomainException : Exception + public string? ErrorCode { get; } + + public DomainException(string message, Exception? inner = null) + : base(message, inner) + { + } + + public DomainException(string message, string? errorCode, Exception? inner = null) + : base(message, inner) + { + ErrorCode = errorCode; + } + + protected DomainException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + ErrorCode = info.GetString(nameof(ErrorCode)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) { - public string? ErrorCode { get; } - - public DomainException(string message, Exception? inner = null) - : base(message, inner) - { - } - - public DomainException(string message, string? errorCode, Exception? inner = null) - : base(message, inner) - { - ErrorCode = errorCode; - } - - protected DomainException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - ErrorCode = info.GetString(nameof(ErrorCode)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(ErrorCode), ErrorCode); - - base.GetObjectData(info, context); - } + info.AddValue(nameof(ErrorCode), ErrorCode); + + base.GetObjectData(info, context); } } diff --git a/backend/src/Squidex.Infrastructure/DomainForbiddenException.cs b/backend/src/Squidex.Infrastructure/DomainForbiddenException.cs index f4ee12997f..8fc1141a66 100644 --- a/backend/src/Squidex.Infrastructure/DomainForbiddenException.cs +++ b/backend/src/Squidex.Infrastructure/DomainForbiddenException.cs @@ -7,21 +7,20 @@ using System.Runtime.Serialization; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[Serializable] +public class DomainForbiddenException : DomainException { - [Serializable] - public class DomainForbiddenException : DomainException - { - private const string ValidationError = "FORBIDDEN"; + private const string ValidationError = "FORBIDDEN"; - public DomainForbiddenException(string message, Exception? inner = null) - : base(message, ValidationError, inner) - { - } + public DomainForbiddenException(string message, Exception? inner = null) + : base(message, ValidationError, inner) + { + } - protected DomainForbiddenException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected DomainForbiddenException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/backend/src/Squidex.Infrastructure/DomainId.cs b/backend/src/Squidex.Infrastructure/DomainId.cs index 90e83eea6d..42e795d7f9 100644 --- a/backend/src/Squidex.Infrastructure/DomainId.cs +++ b/backend/src/Squidex.Infrastructure/DomainId.cs @@ -7,100 +7,99 @@ using System.ComponentModel; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[TypeConverter(typeof(DomainIdTypeConverter))] +public readonly struct DomainId : IEquatable<DomainId>, IComparable<DomainId> { - [TypeConverter(typeof(DomainIdTypeConverter))] - public readonly struct DomainId : IEquatable<DomainId>, IComparable<DomainId> - { - private static readonly string EmptyString = Guid.Empty.ToString(); - public static readonly DomainId Empty = default; - public static readonly string IdSeparator = "--"; + private static readonly string EmptyString = Guid.Empty.ToString(); + public static readonly DomainId Empty = default; + public static readonly string IdSeparator = "--"; - private readonly string? id; + private readonly string? id; - private DomainId(string id) - { - this.id = id; - } + private DomainId(string id) + { + this.id = id; + } - public static DomainId? CreateNullable(string? value) + public static DomainId? CreateNullable(string? value) + { + if (value == null) { - if (value == null) - { - return null; - } - - return Create(value); + return null; } - public static DomainId Create(string value) - { - if (value == null || string.Equals(value, EmptyString, StringComparison.OrdinalIgnoreCase)) - { - return Empty; - } - - return new DomainId(value); - } + return Create(value); + } - public static DomainId Create(Guid value) + public static DomainId Create(string value) + { + if (value == null || string.Equals(value, EmptyString, StringComparison.OrdinalIgnoreCase)) { - if (value == Guid.Empty) - { - return Empty; - } - - return new DomainId(value.ToString()); + return Empty; } - public override bool Equals(object? obj) - { - return obj is DomainId status && Equals(status); - } + return new DomainId(value); + } - public bool Equals(DomainId other) + public static DomainId Create(Guid value) + { + if (value == Guid.Empty) { - return string.Equals(ToString(), other.ToString(), StringComparison.Ordinal); + return Empty; } - public override int GetHashCode() - { - return ToString().GetHashCode(StringComparison.Ordinal); - } + return new DomainId(value.ToString()); + } - public override string ToString() - { - return id ?? EmptyString; - } + public override bool Equals(object? obj) + { + return obj is DomainId status && Equals(status); + } - public int CompareTo(DomainId other) - { - return string.Compare(ToString(), other.ToString(), StringComparison.Ordinal); - } + public bool Equals(DomainId other) + { + return string.Equals(ToString(), other.ToString(), StringComparison.Ordinal); + } - public static bool operator ==(DomainId lhs, DomainId rhs) - { - return lhs.Equals(rhs); - } + public override int GetHashCode() + { + return ToString().GetHashCode(StringComparison.Ordinal); + } - public static bool operator !=(DomainId lhs, DomainId rhs) - { - return !lhs.Equals(rhs); - } + public override string ToString() + { + return id ?? EmptyString; + } - public static DomainId NewGuid() - { - return new DomainId(Guid.NewGuid().ToString()); - } + public int CompareTo(DomainId other) + { + return string.Compare(ToString(), other.ToString(), StringComparison.Ordinal); + } - public static DomainId Combine(NamedId<DomainId> id1, DomainId id2) - { - return new DomainId($"{id1.Id}{IdSeparator}{id2}"); - } + public static bool operator ==(DomainId lhs, DomainId rhs) + { + return lhs.Equals(rhs); + } - public static DomainId Combine(DomainId id1, DomainId id2) - { - return new DomainId($"{id1}{IdSeparator}{id2}"); - } + public static bool operator !=(DomainId lhs, DomainId rhs) + { + return !lhs.Equals(rhs); + } + + public static DomainId NewGuid() + { + return new DomainId(Guid.NewGuid().ToString()); + } + + public static DomainId Combine(NamedId<DomainId> id1, DomainId id2) + { + return new DomainId($"{id1.Id}{IdSeparator}{id2}"); + } + + public static DomainId Combine(DomainId id1, DomainId id2) + { + return new DomainId($"{id1}{IdSeparator}{id2}"); } } diff --git a/backend/src/Squidex.Infrastructure/DomainIdTypeConverter.cs b/backend/src/Squidex.Infrastructure/DomainIdTypeConverter.cs index 384da04fd7..a80d0b86a2 100644 --- a/backend/src/Squidex.Infrastructure/DomainIdTypeConverter.cs +++ b/backend/src/Squidex.Infrastructure/DomainIdTypeConverter.cs @@ -8,38 +8,37 @@ using System.ComponentModel; using System.Globalization; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public sealed class DomainIdTypeConverter : TypeConverter { - public sealed class DomainIdTypeConverter : TypeConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - { - return sourceType == typeof(string) || sourceType == typeof(Guid); - } + return sourceType == typeof(string) || sourceType == typeof(Guid); + } - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) - { - return destinationType == typeof(string); - } + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string); + } - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is string text) { - if (value is string text) - { - return DomainId.Create(text); - } - - if (value is Guid guid) - { - return DomainId.Create(guid); - } - - return DomainId.Empty; + return DomainId.Create(text); } - public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) + if (value is Guid guid) { - return value?.ToString()!; + return DomainId.Create(guid); } + + return DomainId.Empty; + } + + public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) + { + return value?.ToString()!; } } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs b/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs index 05bd658d11..621dcfcfc0 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs @@ -8,26 +8,25 @@ using System.Runtime.Serialization; using Squidex.Infrastructure.Translations; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[Serializable] +public class DomainObjectConflictException : DomainObjectException { - [Serializable] - public class DomainObjectConflictException : DomainObjectException - { - private const string ExposedErrorCode = "OBJECT_CONFLICT"; + private const string ExposedErrorCode = "OBJECT_CONFLICT"; - public DomainObjectConflictException(string id, Exception? inner = null) - : base(FormatMessage(id), id, ExposedErrorCode, inner) - { - } + public DomainObjectConflictException(string id, Exception? inner = null) + : base(FormatMessage(id), id, ExposedErrorCode, inner) + { + } - protected DomainObjectConflictException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected DomainObjectConflictException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } - private static string FormatMessage(string id) - { - return T.Get("exceptions.domainObjectConflict", new { id }); - } + private static string FormatMessage(string id) + { + return T.Get("exceptions.domainObjectConflict", new { id }); } } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs b/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs index 37e94c82ff..413a6b4044 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs @@ -8,26 +8,25 @@ using System.Runtime.Serialization; using Squidex.Infrastructure.Translations; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[Serializable] +public class DomainObjectDeletedException : DomainObjectException { - [Serializable] - public class DomainObjectDeletedException : DomainObjectException - { - private const string ExposedErrorCode = "OBJECT_DELETED"; + private const string ExposedErrorCode = "OBJECT_DELETED"; - public DomainObjectDeletedException(string id, Exception? inner = null) - : base(FormatMessage(id), id, ExposedErrorCode, inner) - { - } + public DomainObjectDeletedException(string id, Exception? inner = null) + : base(FormatMessage(id), id, ExposedErrorCode, inner) + { + } - protected DomainObjectDeletedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected DomainObjectDeletedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } - private static string FormatMessage(string id) - { - return T.Get("exceptions.domainObjectDeleted", new { id }); - } + private static string FormatMessage(string id) + { + return T.Get("exceptions.domainObjectDeleted", new { id }); } } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectException.cs b/backend/src/Squidex.Infrastructure/DomainObjectException.cs index eb50783475..d635edcc04 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectException.cs @@ -7,32 +7,31 @@ using System.Runtime.Serialization; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[Serializable] +public class DomainObjectException : DomainException { - [Serializable] - public class DomainObjectException : DomainException - { - public string Id { get; } + public string Id { get; } - public DomainObjectException(string message, string id, string errorCode, Exception? inner = null) - : base(message, errorCode, inner) - { - Guard.NotNullOrEmpty(id); + public DomainObjectException(string message, string id, string errorCode, Exception? inner = null) + : base(message, errorCode, inner) + { + Guard.NotNullOrEmpty(id); - Id = id; - } + Id = id; + } - public DomainObjectException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Id = info.GetString(nameof(Id))!; - } + public DomainObjectException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Id = info.GetString(nameof(Id))!; + } - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Id), Id); + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Id), Id); - base.GetObjectData(info, context); - } + base.GetObjectData(info, context); } } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs b/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs index 140d389d40..ab05ebdd9e 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs @@ -8,26 +8,25 @@ using System.Runtime.Serialization; using Squidex.Infrastructure.Translations; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[Serializable] +public class DomainObjectNotFoundException : DomainObjectException { - [Serializable] - public class DomainObjectNotFoundException : DomainObjectException - { - private const string ExposedErrorCode = "OBJECT_NOTFOUND"; + private const string ExposedErrorCode = "OBJECT_NOTFOUND"; - public DomainObjectNotFoundException(string id, Exception? inner = null) - : base(FormatMessage(id), id, ExposedErrorCode, inner) - { - } + public DomainObjectNotFoundException(string id, Exception? inner = null) + : base(FormatMessage(id), id, ExposedErrorCode, inner) + { + } - protected DomainObjectNotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected DomainObjectNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } - private static string FormatMessage(string id) - { - return T.Get("exceptions.domainObjectNotFound", new { id }); - } + private static string FormatMessage(string id) + { + return T.Get("exceptions.domainObjectNotFound", new { id }); } } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs b/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs index 3dd4ae7ecd..76f4af1d1e 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs @@ -8,44 +8,43 @@ using System.Runtime.Serialization; using Squidex.Infrastructure.Translations; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[Serializable] +public class DomainObjectVersionException : DomainObjectException { - [Serializable] - public class DomainObjectVersionException : DomainObjectException - { - private const string ExposedErrorCode = "OBJECT_VERSION_CONFLICT"; + private const string ExposedErrorCode = "OBJECT_VERSION_CONFLICT"; - public long CurrentVersion { get; } + public long CurrentVersion { get; } - public long ExpectedVersion { get; } + public long ExpectedVersion { get; } - public DomainObjectVersionException(string id, long currentVersion, long expectedVersion, Exception? inner = null) - : base(FormatMessage(id, currentVersion, expectedVersion), id, ExposedErrorCode, inner) - { - CurrentVersion = currentVersion; + public DomainObjectVersionException(string id, long currentVersion, long expectedVersion, Exception? inner = null) + : base(FormatMessage(id, currentVersion, expectedVersion), id, ExposedErrorCode, inner) + { + CurrentVersion = currentVersion; - ExpectedVersion = expectedVersion; - } + ExpectedVersion = expectedVersion; + } - protected DomainObjectVersionException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - CurrentVersion = info.GetInt64(nameof(CurrentVersion)); + protected DomainObjectVersionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + CurrentVersion = info.GetInt64(nameof(CurrentVersion)); - ExpectedVersion = info.GetInt64(nameof(ExpectedVersion)); - } + ExpectedVersion = info.GetInt64(nameof(ExpectedVersion)); + } - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(CurrentVersion), CurrentVersion); - info.AddValue(nameof(ExpectedVersion), ExpectedVersion); + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(CurrentVersion), CurrentVersion); + info.AddValue(nameof(ExpectedVersion), ExpectedVersion); - base.GetObjectData(info, context); - } + base.GetObjectData(info, context); + } - private static string FormatMessage(string id, long currentVersion, long expectedVersion) - { - return T.Get("exceptions.domainObjectVersion", new { id, currentVersion, expectedVersion }); - } + private static string FormatMessage(string id, long currentVersion, long expectedVersion) + { + return T.Get("exceptions.domainObjectVersion", new { id, currentVersion, expectedVersion }); } } diff --git a/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs b/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs index f2a57f2505..3b56b0fa9e 100644 --- a/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs +++ b/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Email +namespace Squidex.Infrastructure.Email; + +public interface IEmailSender { - public interface IEmailSender - { - Task SendAsync(string recipient, string subject, string body, - CancellationToken ct = default); - } + Task SendAsync(string recipient, string subject, string body, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs b/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs index 3009c61d63..2d8bbfb7ab 100644 --- a/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs +++ b/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs @@ -12,70 +12,69 @@ using MimeKit; using MimeKit.Text; -namespace Squidex.Infrastructure.Email +namespace Squidex.Infrastructure.Email; + +[ExcludeFromCodeCoverage] +public sealed class SmtpEmailSender : IEmailSender { - [ExcludeFromCodeCoverage] - public sealed class SmtpEmailSender : IEmailSender - { - private readonly SmtpOptions options; - private readonly ObjectPool<SmtpClient> clientPool; + private readonly SmtpOptions options; + private readonly ObjectPool<SmtpClient> clientPool; - public SmtpEmailSender(IOptions<SmtpOptions> options) - { - this.options = options.Value; + public SmtpEmailSender(IOptions<SmtpOptions> options) + { + this.options = options.Value; - clientPool = new DefaultObjectPoolProvider().Create(new DefaultPooledObjectPolicy<SmtpClient>()); - } + clientPool = new DefaultObjectPoolProvider().Create(new DefaultPooledObjectPolicy<SmtpClient>()); + } - public async Task SendAsync(string recipient, string subject, string body, - CancellationToken ct = default) + public async Task SendAsync(string recipient, string subject, string body, + CancellationToken ct = default) + { + var smtpClient = clientPool.Get(); + try { - var smtpClient = clientPool.Get(); - try + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // Enforce a hard timeout from the configuration. - combined.CancelAfter(options.Timeout); + // Enforce a hard timeout from the configuration. + combined.CancelAfter(options.Timeout); - await EnsureConnectedAsync(smtpClient, combined.Token); + await EnsureConnectedAsync(smtpClient, combined.Token); - var smtpMessage = new MimeMessage(); + var smtpMessage = new MimeMessage(); - smtpMessage.From.Add(MailboxAddress.Parse( - options.Sender)); + smtpMessage.From.Add(MailboxAddress.Parse( + options.Sender)); - smtpMessage.To.Add(MailboxAddress.Parse( - recipient)); + smtpMessage.To.Add(MailboxAddress.Parse( + recipient)); - smtpMessage.Body = new TextPart(TextFormat.Html) - { - Text = body - }; + smtpMessage.Body = new TextPart(TextFormat.Html) + { + Text = body + }; - smtpMessage.Subject = subject; + smtpMessage.Subject = subject; - await smtpClient.SendAsync(smtpMessage, ct); - } - } - finally - { - clientPool.Return(smtpClient); + await smtpClient.SendAsync(smtpMessage, ct); } } + finally + { + clientPool.Return(smtpClient); + } + } - private async Task EnsureConnectedAsync(SmtpClient smtpClient, - CancellationToken ct) + private async Task EnsureConnectedAsync(SmtpClient smtpClient, + CancellationToken ct) + { + if (!smtpClient.IsConnected) { - if (!smtpClient.IsConnected) - { - await smtpClient.ConnectAsync(options.Server, options.Port, cancellationToken: ct); - } + await smtpClient.ConnectAsync(options.Server, options.Port, cancellationToken: ct); + } - if (!smtpClient.IsAuthenticated) - { - await smtpClient.AuthenticateAsync(options.Username, options.Password, ct); - } + if (!smtpClient.IsAuthenticated) + { + await smtpClient.AuthenticateAsync(options.Username, options.Password, ct); } } } diff --git a/backend/src/Squidex.Infrastructure/Email/SmtpOptions.cs b/backend/src/Squidex.Infrastructure/Email/SmtpOptions.cs index a9e2610e36..ade64c9b76 100644 --- a/backend/src/Squidex.Infrastructure/Email/SmtpOptions.cs +++ b/backend/src/Squidex.Infrastructure/Email/SmtpOptions.cs @@ -5,31 +5,30 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Email +namespace Squidex.Infrastructure.Email; + +public sealed class SmtpOptions { - public sealed class SmtpOptions - { - public string Server { get; set; } + public string Server { get; set; } - public string Sender { get; set; } + public string Sender { get; set; } - public string Username { get; set; } + public string Username { get; set; } - public string Password { get; set; } + public string Password { get; set; } - public bool EnableSsl { get; set; } + public bool EnableSsl { get; set; } - public int Timeout { get; set; } = 5000; + public int Timeout { get; set; } = 5000; - public int Port { get; set; } = 587; + public int Port { get; set; } = 587; - public bool IsConfigured() - { - return - !string.IsNullOrWhiteSpace(Server) && - !string.IsNullOrWhiteSpace(Sender) && - !string.IsNullOrWhiteSpace(Username) && - !string.IsNullOrWhiteSpace(Password); - } + public bool IsConfigured() + { + return + !string.IsNullOrWhiteSpace(Server) && + !string.IsNullOrWhiteSpace(Sender) && + !string.IsNullOrWhiteSpace(Username) && + !string.IsNullOrWhiteSpace(Password); } } diff --git a/backend/src/Squidex.Infrastructure/EtagVersion.cs b/backend/src/Squidex.Infrastructure/EtagVersion.cs index 0c19eafab6..62137ac9ba 100644 --- a/backend/src/Squidex.Infrastructure/EtagVersion.cs +++ b/backend/src/Squidex.Infrastructure/EtagVersion.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class EtagVersion { - public static class EtagVersion - { - public const long Auto = -3; + public const long Auto = -3; - public const long Any = -2; + public const long Any = -2; - public const long Empty = -1; - } + public const long Empty = -1; } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs b/backend/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs index bda1e9e128..256ef3000e 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs @@ -5,22 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public static class CommonHeaders { - public static class CommonHeaders - { - public static readonly string AggregateId = nameof(AggregateId); + public static readonly string AggregateId = nameof(AggregateId); - public static readonly string CommitId = nameof(CommitId); + public static readonly string CommitId = nameof(CommitId); - public static readonly string EventId = nameof(EventId); + public static readonly string EventId = nameof(EventId); - public static readonly string EventNumber = nameof(EventNumber); + public static readonly string EventNumber = nameof(EventNumber); - public static readonly string EventStreamNumber = nameof(EventStreamNumber); + public static readonly string EventStreamNumber = nameof(EventStreamNumber); - public static readonly string Restored = nameof(Restored); + public static readonly string Restored = nameof(Restored); - public static readonly string Timestamp = nameof(Timestamp); - } + public static readonly string Timestamp = nameof(Timestamp); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscription.cs index 32f81d1fd9..1b89f757c4 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscription.cs @@ -8,158 +8,157 @@ using System.Threading.Channels; using Squidex.Infrastructure.Tasks; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +internal sealed class BatchSubscription : IEventSubscriber<ParsedEvent>, IEventSubscription { - internal sealed class BatchSubscription : IEventSubscriber<ParsedEvent>, IEventSubscription + private readonly IEventSubscription eventSubscription; + private readonly Channel<object> taskQueue; + private readonly Channel<object> batchQueue; + private readonly Task handleTask; + private readonly CancellationTokenSource completed = new CancellationTokenSource(); + + public BatchSubscription( + IEventConsumer eventConsumer, + IEventSubscriber<ParsedEvents> eventSubscriber, + EventSubscriptionSource<ParsedEvent> eventSource) { - private readonly IEventSubscription eventSubscription; - private readonly Channel<object> taskQueue; - private readonly Channel<object> batchQueue; - private readonly Task handleTask; - private readonly CancellationTokenSource completed = new CancellationTokenSource(); - - public BatchSubscription( - IEventConsumer eventConsumer, - IEventSubscriber<ParsedEvents> eventSubscriber, - EventSubscriptionSource<ParsedEvent> eventSource) - { - var batchSize = Math.Max(1, eventConsumer.BatchSize); - var batchDelay = Math.Max(100, eventConsumer.BatchDelay); + var batchSize = Math.Max(1, eventConsumer.BatchSize); + var batchDelay = Math.Max(100, eventConsumer.BatchDelay); - taskQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2) - { - SingleReader = true, - SingleWriter = true - }); + taskQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2) + { + SingleReader = true, + SingleWriter = true + }); - batchQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(batchSize) - { - AllowSynchronousContinuations = true, - SingleReader = true, - SingleWriter = true - }); + batchQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(batchSize) + { + AllowSynchronousContinuations = true, + SingleReader = true, + SingleWriter = true + }); - batchQueue.Batch<ParsedEvent>(taskQueue, batchSize, batchDelay, completed.Token); + batchQueue.Batch<ParsedEvent>(taskQueue, batchSize, batchDelay, completed.Token); - handleTask = Run(eventSubscriber); + handleTask = Run(eventSubscriber); - // Run last to subscribe after everything is configured. - eventSubscription = eventSource(this); - } + // Run last to subscribe after everything is configured. + eventSubscription = eventSource(this); + } - private async Task Run(IEventSubscriber<ParsedEvents> eventSink) + private async Task Run(IEventSubscriber<ParsedEvents> eventSink) + { + try { - try - { - var isStopped = false; + var isStopped = false; - await foreach (var task in taskQueue.Reader.ReadAllAsync(completed.Token)) + await foreach (var task in taskQueue.Reader.ReadAllAsync(completed.Token)) + { + switch (task) { - switch (task) - { - case Exception exception when exception is not OperationCanceledException: + case Exception exception when exception is not OperationCanceledException: + { + if (!completed.IsCancellationRequested) { - if (!completed.IsCancellationRequested) - { - await eventSink.OnErrorAsync(this, exception); - } - - isStopped = true; - break; + await eventSink.OnErrorAsync(this, exception); } - case List<ParsedEvent> batch: - { - if (!completed.IsCancellationRequested) - { - // Events can be null if the event consumer is not interested in the stored event. - var eventList = batch.Select(x => x.Event).NotNull().ToList(); - var eventPosition = batch[^1].Position; + isStopped = true; + break; + } - // Use a struct here to save a few allocations. - await eventSink.OnNextAsync(this, new ParsedEvents(eventList, eventPosition)); - } + case List<ParsedEvent> batch: + { + if (!completed.IsCancellationRequested) + { + // Events can be null if the event consumer is not interested in the stored event. + var eventList = batch.Select(x => x.Event).NotNull().ToList(); + var eventPosition = batch[^1].Position; - break; + // Use a struct here to save a few allocations. + await eventSink.OnNextAsync(this, new ParsedEvents(eventList, eventPosition)); } - } - if (isStopped) - { - break; - } + break; + } } - } - catch (OperationCanceledException) - { - return; - } - catch (Exception ex) - { - if (!completed.IsCancellationRequested) + + if (isStopped) { - await eventSink.OnErrorAsync(this, ex); + break; } } } - - public void Dispose() + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) { - if (completed.IsCancellationRequested) + if (!completed.IsCancellationRequested) { - return; + await eventSink.OnErrorAsync(this, ex); } - - // It is not necessary to dispose the cancellation token source. - completed.Cancel(); - - // We do not lock here, it is the responsibility of the source subscription to be thread safe. - eventSubscription.Dispose(); } + } - public async ValueTask CompleteAsync() + public void Dispose() + { + if (completed.IsCancellationRequested) { - await eventSubscription.CompleteAsync(); + return; + } - batchQueue.Writer.TryComplete(); + // It is not necessary to dispose the cancellation token source. + completed.Cancel(); - await handleTask; - } + // We do not lock here, it is the responsibility of the source subscription to be thread safe. + eventSubscription.Dispose(); + } - public void WakeUp() + public async ValueTask CompleteAsync() + { + await eventSubscription.CompleteAsync(); + + batchQueue.Writer.TryComplete(); + + await handleTask; + } + + public void WakeUp() + { + eventSubscription.WakeUp(); + } + + async ValueTask IEventSubscriber<ParsedEvent>.OnErrorAsync(IEventSubscription subscription, Exception exception) + { + try { - eventSubscription.WakeUp(); + // Forward the exception from one task only, but bypass the batch. + await taskQueue.Writer.WriteAsync(exception, completed.Token); } - - async ValueTask IEventSubscriber<ParsedEvent>.OnErrorAsync(IEventSubscription subscription, Exception exception) + catch (OperationCanceledException) { - try - { - // Forward the exception from one task only, but bypass the batch. - await taskQueue.Writer.WriteAsync(exception, completed.Token); - } - catch (OperationCanceledException) - { - // These exception are acceptable and happens when an exception has been thrown before. - } - catch (ChannelClosedException) - { - } + // These exception are acceptable and happens when an exception has been thrown before. } + catch (ChannelClosedException) + { + } + } - async ValueTask IEventSubscriber<ParsedEvent>.OnNextAsync(IEventSubscription subscription, ParsedEvent @event) + async ValueTask IEventSubscriber<ParsedEvent>.OnNextAsync(IEventSubscription subscription, ParsedEvent @event) + { + try + { + await batchQueue.Writer.WriteAsync(@event, completed.Token); + } + catch (OperationCanceledException) + { + // These exception are acceptable and happens when an exception has been thrown before. + } + catch (ChannelClosedException) { - try - { - await batchQueue.Writer.WriteAsync(@event, completed.Token); - } - catch (OperationCanceledException) - { - // These exception are acceptable and happens when an exception has been thrown before. - } - catch (ChannelClosedException) - { - } } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerManager.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerManager.cs index c08e1d1ec3..c77f2ea545 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerManager.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerManager.cs @@ -8,73 +8,72 @@ using Squidex.Infrastructure.States; using Squidex.Messaging; -namespace Squidex.Infrastructure.EventSourcing.Consume -{ - public sealed class EventConsumerManager : IEventConsumerManager - { - private readonly IPersistenceFactory<EventConsumerState> persistence; - private readonly IMessageBus messaging; - private readonly HashSet<string> activeNames; +namespace Squidex.Infrastructure.EventSourcing.Consume; - public EventConsumerManager(IPersistenceFactory<EventConsumerState> persistence, IEnumerable<IEventConsumer> eventConsumers, - IMessageBus messaging) - { - this.persistence = persistence; - this.messaging = messaging; - this.activeNames = eventConsumers.Select(x => x.Name).ToHashSet(); - } +public sealed class EventConsumerManager : IEventConsumerManager +{ + private readonly IPersistenceFactory<EventConsumerState> persistence; + private readonly IMessageBus messaging; + private readonly HashSet<string> activeNames; - public async Task<List<EventConsumerInfo>> GetConsumersAsync( - CancellationToken ct = default) - { - var snapshots = await persistence.Snapshots.ReadAllAsync(ct).ToListAsync(ct); + public EventConsumerManager(IPersistenceFactory<EventConsumerState> persistence, IEnumerable<IEventConsumer> eventConsumers, + IMessageBus messaging) + { + this.persistence = persistence; + this.messaging = messaging; + this.activeNames = eventConsumers.Select(x => x.Name).ToHashSet(); + } - return snapshots.Where(x => activeNames.Contains(x.Key.ToString())).Select(x => x.Value.ToInfo(x.Key.ToString())).ToList(); - } + public async Task<List<EventConsumerInfo>> GetConsumersAsync( + CancellationToken ct = default) + { + var snapshots = await persistence.Snapshots.ReadAllAsync(ct).ToListAsync(ct); - public async Task<EventConsumerInfo> ResetAsync(string consumerName, - CancellationToken ct = default) - { - var state = await GetStateAsync(consumerName, ct); + return snapshots.Where(x => activeNames.Contains(x.Key.ToString())).Select(x => x.Value.ToInfo(x.Key.ToString())).ToList(); + } - await messaging.PublishAsync(new EventConsumerReset(consumerName), ct: ct); + public async Task<EventConsumerInfo> ResetAsync(string consumerName, + CancellationToken ct = default) + { + var state = await GetStateAsync(consumerName, ct); - return state.Value.ToInfo(consumerName); - } + await messaging.PublishAsync(new EventConsumerReset(consumerName), ct: ct); - public async Task<EventConsumerInfo> StartAsync(string consumerName, - CancellationToken ct = default) - { - var state = await GetStateAsync(consumerName, ct); + return state.Value.ToInfo(consumerName); + } - await messaging.PublishAsync(new EventConsumerStart(consumerName), ct: ct); + public async Task<EventConsumerInfo> StartAsync(string consumerName, + CancellationToken ct = default) + { + var state = await GetStateAsync(consumerName, ct); - return state.Value.ToInfo(consumerName); - } + await messaging.PublishAsync(new EventConsumerStart(consumerName), ct: ct); - public async Task<EventConsumerInfo> StopAsync(string consumerName, - CancellationToken ct = default) - { - var state = await GetStateAsync(consumerName, ct); + return state.Value.ToInfo(consumerName); + } - await messaging.PublishAsync(new EventConsumerStop(consumerName), ct: ct); + public async Task<EventConsumerInfo> StopAsync(string consumerName, + CancellationToken ct = default) + { + var state = await GetStateAsync(consumerName, ct); - return state.Value.ToInfo(consumerName); - } + await messaging.PublishAsync(new EventConsumerStop(consumerName), ct: ct); - private async Task<SimpleState<EventConsumerState>> GetStateAsync(string consumerName, - CancellationToken ct) - { - var state = new SimpleState<EventConsumerState>(persistence, GetType(), DomainId.Create(consumerName)); + return state.Value.ToInfo(consumerName); + } - await state.LoadAsync(ct); + private async Task<SimpleState<EventConsumerState>> GetStateAsync(string consumerName, + CancellationToken ct) + { + var state = new SimpleState<EventConsumerState>(persistence, GetType(), DomainId.Create(consumerName)); - if (state.Version <= EtagVersion.Empty || !activeNames.Contains(consumerName)) - { - throw new DomainObjectNotFoundException(consumerName); - } + await state.LoadAsync(ct); - return state; + if (state.Version <= EtagVersion.Empty || !activeNames.Contains(consumerName)) + { + throw new DomainObjectNotFoundException(consumerName); } + + return state; } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs index aa3147bda8..582b9f3d30 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs @@ -10,273 +10,272 @@ using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public class EventConsumerProcessor : IEventSubscriber<ParsedEvents> { - public class EventConsumerProcessor : IEventSubscriber<ParsedEvents> + private readonly SimpleState<EventConsumerState> state; + private readonly IEventFormatter eventFormatter; + private readonly IEventConsumer eventConsumer; + private readonly IEventStore eventStore; + private readonly ILogger<EventConsumerProcessor> log; + private readonly AsyncLock asyncLock = new AsyncLock(); + private IEventSubscription? currentSubscription; + + public EventConsumerState State { - private readonly SimpleState<EventConsumerState> state; - private readonly IEventFormatter eventFormatter; - private readonly IEventConsumer eventConsumer; - private readonly IEventStore eventStore; - private readonly ILogger<EventConsumerProcessor> log; - private readonly AsyncLock asyncLock = new AsyncLock(); - private IEventSubscription? currentSubscription; - - public EventConsumerState State - { - get => state.Value; - set => state.Value = value; - } + get => state.Value; + set => state.Value = value; + } - public EventConsumerProcessor( - IPersistenceFactory<EventConsumerState> persistenceFactory, - IEventConsumer eventConsumer, - IEventFormatter eventFormatter, - IEventStore eventStore, - ILogger<EventConsumerProcessor> log) - { - this.eventStore = eventStore; - this.eventFormatter = eventFormatter; - this.eventConsumer = eventConsumer; - this.log = log; + public EventConsumerProcessor( + IPersistenceFactory<EventConsumerState> persistenceFactory, + IEventConsumer eventConsumer, + IEventFormatter eventFormatter, + IEventStore eventStore, + ILogger<EventConsumerProcessor> log) + { + this.eventStore = eventStore; + this.eventFormatter = eventFormatter; + this.eventConsumer = eventConsumer; + this.log = log; - state = new SimpleState<EventConsumerState>(persistenceFactory, GetType(), eventConsumer.Name); - } + state = new SimpleState<EventConsumerState>(persistenceFactory, GetType(), eventConsumer.Name); + } - public virtual async Task InitializeAsync( - CancellationToken ct) - { - await state.LoadAsync(ct); + public virtual async Task InitializeAsync( + CancellationToken ct) + { + await state.LoadAsync(ct); - if (eventConsumer.StartLatest && string.IsNullOrWhiteSpace(State.Position)) - { - var latest = await eventStore.QueryAllReverseAsync(eventConsumer.EventsFilter, default, 1, ct).FirstOrDefaultAsync(ct); + if (eventConsumer.StartLatest && string.IsNullOrWhiteSpace(State.Position)) + { + var latest = await eventStore.QueryAllReverseAsync(eventConsumer.EventsFilter, default, 1, ct).FirstOrDefaultAsync(ct); - State = State.Handled(latest?.EventPosition, 0); - } + State = State.Handled(latest?.EventPosition, 0); } + } - public virtual async Task CompleteAsync() + public virtual async Task CompleteAsync() + { + // This is only needed for tests to wait for all asynchronous tasks inside the subscriptions. + if (currentSubscription != null) { - // This is only needed for tests to wait for all asynchronous tasks inside the subscriptions. - if (currentSubscription != null) + try { - try - { - await currentSubscription.CompleteAsync(); - } - catch (Exception ex) - { - log.LogCritical(ex, "Failed to complete consumer."); - } + await currentSubscription.CompleteAsync(); } - } - - public virtual async ValueTask OnNextAsync(IEventSubscription subscription, ParsedEvents @event) - { - await UpdateAsync(async () => + catch (Exception ex) { - if (!ReferenceEquals(subscription, currentSubscription)) - { - return; - } - - await DispatchAsync(@event.Events); - - State = State.Handled(@event.Position, @event.Events.Count); - }, State.Position); + log.LogCritical(ex, "Failed to complete consumer."); + } } + } - public virtual async ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception) + public virtual async ValueTask OnNextAsync(IEventSubscription subscription, ParsedEvents @event) + { + await UpdateAsync(async () => { - await UpdateAsync(() => + if (!ReferenceEquals(subscription, currentSubscription)) { - if (!ReferenceEquals(subscription, currentSubscription)) - { - return; - } + return; + } - Unsubscribe(); + await DispatchAsync(@event.Events); - State = State.Stopped(exception); - }, State.Position); - } + State = State.Handled(@event.Position, @event.Events.Count); + }, State.Position); + } - public virtual Task ActivateAsync() + public virtual async ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception) + { + await UpdateAsync(() => { - return UpdateAsync(() => + if (!ReferenceEquals(subscription, currentSubscription)) { - if (State.IsFailed) - { - Subscribe(); + return; + } - State = State.Started(); - } - else if (!State.IsStopped) - { - Subscribe(); - } - }, State.Position); - } + Unsubscribe(); + + State = State.Stopped(exception); + }, State.Position); + } - public virtual Task StartAsync() + public virtual Task ActivateAsync() + { + return UpdateAsync(() => { - return UpdateAsync(() => + if (State.IsFailed) { - if (!State.IsStopped) - { - return; - } - Subscribe(); State = State.Started(); - }, State.Position); - } + } + else if (!State.IsStopped) + { + Subscribe(); + } + }, State.Position); + } - public virtual Task StopAsync() + public virtual Task StartAsync() + { + return UpdateAsync(() => { - return UpdateAsync(() => + if (!State.IsStopped) { - if (State.IsStopped) - { - return; - } + return; + } - Unsubscribe(); + Subscribe(); - State = State.Stopped(); - }, State.Position); - } + State = State.Started(); + }, State.Position); + } - public virtual async Task ResetAsync() + public virtual Task StopAsync() + { + return UpdateAsync(() => { - if (!eventConsumer.CanClear) + if (State.IsStopped) { return; } - await UpdateAsync(async () => - { - Unsubscribe(); - - await ClearAsync(); + Unsubscribe(); - State = EventConsumerState.Initial; + State = State.Stopped(); + }, State.Position); + } - Subscribe(); - }, State.Position); + public virtual async Task ResetAsync() + { + if (!eventConsumer.CanClear) + { + return; } - private async Task DispatchAsync(IReadOnlyList<Envelope<IEvent>> events) + await UpdateAsync(async () => { - if (events.Count > 0) - { - await eventConsumer.On(events); - } + Unsubscribe(); + + await ClearAsync(); + + State = EventConsumerState.Initial; + + Subscribe(); + }, State.Position); + } + + private async Task DispatchAsync(IReadOnlyList<Envelope<IEvent>> events) + { + if (events.Count > 0) + { + await eventConsumer.On(events); } + } - private Task UpdateAsync(Action action, string? position, [CallerMemberName] string? caller = null) + private Task UpdateAsync(Action action, string? position, [CallerMemberName] string? caller = null) + { + return UpdateAsync(() => { - return UpdateAsync(() => - { - action(); + action(); - return Task.CompletedTask; - }, position, caller); - } + return Task.CompletedTask; + }, position, caller); + } - private async Task UpdateAsync(Func<Task> action, string? position, [CallerMemberName] string? caller = null) + private async Task UpdateAsync(Func<Task> action, string? position, [CallerMemberName] string? caller = null) + { + // We do not want to deal with concurrency in this class, therefore we just use a lock. + using (await asyncLock.EnterAsync()) { - // We do not want to deal with concurrency in this class, therefore we just use a lock. - using (await asyncLock.EnterAsync()) - { - var previousState = State; + var previousState = State; + try + { + await action(); + } + catch (Exception ex) + { try { - await action(); + Unsubscribe(); } - catch (Exception ex) + catch (Exception unsubscribeException) { - try - { - Unsubscribe(); - } - catch (Exception unsubscribeException) - { - ex = new AggregateException(ex, unsubscribeException); - } - - log.LogCritical(ex, "Failed to update consumer {consumer} at position {position} from {caller}.", - eventConsumer.Name, position, caller); - - State = previousState.Stopped(ex); + ex = new AggregateException(ex, unsubscribeException); } - // The state is a record, therefore we can check for value equality. - if (!Equals(previousState, State)) - { - await state.WriteAsync(); - } - } - } + log.LogCritical(ex, "Failed to update consumer {consumer} at position {position} from {caller}.", + eventConsumer.Name, position, caller); - private async Task ClearAsync() - { - if (log.IsEnabled(LogLevel.Debug)) - { - log.LogDebug("Event consumer {consumer} reset started", eventConsumer.Name); + State = previousState.Stopped(ex); } - var watch = ValueStopwatch.StartNew(); - try - { - await eventConsumer.ClearAsync(); - } - finally + // The state is a record, therefore we can check for value equality. + if (!Equals(previousState, State)) { - log.LogDebug("Event consumer {consumer} reset completed after {time}ms.", eventConsumer.Name, watch.Stop()); + await state.WriteAsync(); } } + } - private void Unsubscribe() + private async Task ClearAsync() + { + if (log.IsEnabled(LogLevel.Debug)) { - if (currentSubscription != null) - { - currentSubscription.Dispose(); - currentSubscription = null; - } + log.LogDebug("Event consumer {consumer} reset started", eventConsumer.Name); } - private void Subscribe() + var watch = ValueStopwatch.StartNew(); + try { - if (currentSubscription == null) - { - currentSubscription = CreateRetrySubscription(this); - } - else - { - currentSubscription.WakeUp(); - } + await eventConsumer.ClearAsync(); } - - protected IEventSubscription CreatePipeline(IEventSubscriber<ParsedEvents> subscriber) + finally { - // Create a pipline of subscription inside a retry. - return new BatchSubscription(eventConsumer, subscriber, - x => new ParseSubscription(eventConsumer, eventFormatter, x, CreateSubscription)); + log.LogDebug("Event consumer {consumer} reset completed after {time}ms.", eventConsumer.Name, watch.Stop()); } + } - protected virtual IEventSubscription CreateRetrySubscription(IEventSubscriber<ParsedEvents> subscriber) + private void Unsubscribe() + { + if (currentSubscription != null) { - // It is very important to have the retry subscription as outer subscription, because we also need to cancel the batching in case of errors. - return new RetrySubscription<ParsedEvents>(subscriber, CreatePipeline); + currentSubscription.Dispose(); + currentSubscription = null; } + } - protected virtual IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> subscriber) + private void Subscribe() + { + if (currentSubscription == null) { - return eventStore.CreateSubscription(subscriber, eventConsumer.EventsFilter, State.Position); + currentSubscription = CreateRetrySubscription(this); } + else + { + currentSubscription.WakeUp(); + } + } + + protected IEventSubscription CreatePipeline(IEventSubscriber<ParsedEvents> subscriber) + { + // Create a pipline of subscription inside a retry. + return new BatchSubscription(eventConsumer, subscriber, + x => new ParseSubscription(eventConsumer, eventFormatter, x, CreateSubscription)); + } + + protected virtual IEventSubscription CreateRetrySubscription(IEventSubscriber<ParsedEvents> subscriber) + { + // It is very important to have the retry subscription as outer subscription, because we also need to cancel the batching in case of errors. + return new RetrySubscription<ParsedEvents>(subscriber, CreatePipeline); + } + + protected virtual IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> subscriber) + { + return eventStore.CreateSubscription(subscriber, eventConsumer.EventsFilter, State.Position); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerState.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerState.cs index 3341098068..bc0ca04f48 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerState.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerState.cs @@ -9,48 +9,47 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public sealed record EventConsumerState(string? Position, int Count, bool IsStopped = false, string? Error = null) { - public sealed record EventConsumerState(string? Position, int Count, bool IsStopped = false, string? Error = null) - { - public static readonly EventConsumerState Initial = new EventConsumerState(null, 0); + public static readonly EventConsumerState Initial = new EventConsumerState(null, 0); - public bool IsPaused - { - get => IsStopped && string.IsNullOrWhiteSpace(Error); - } + public bool IsPaused + { + get => IsStopped && string.IsNullOrWhiteSpace(Error); + } - public bool IsFailed - { - get => IsStopped && !string.IsNullOrWhiteSpace(Error); - } + public bool IsFailed + { + get => IsStopped && !string.IsNullOrWhiteSpace(Error); + } - public EventConsumerState() - : this(null, 0) - { - } + public EventConsumerState() + : this(null, 0) + { + } - public EventConsumerState Handled(string? position, int offset = 1) - { - return new EventConsumerState(position, Count + offset); - } + public EventConsumerState Handled(string? position, int offset = 1) + { + return new EventConsumerState(position, Count + offset); + } - public EventConsumerState Stopped(Exception? ex = null) - { - return new EventConsumerState(Position, Count, true, ex?.Message); - } + public EventConsumerState Stopped(Exception? ex = null) + { + return new EventConsumerState(Position, Count, true, ex?.Message); + } - public EventConsumerState Started() - { - return new EventConsumerState(Position, Count); - } + public EventConsumerState Started() + { + return new EventConsumerState(Position, Count); + } - public EventConsumerInfo ToInfo(string name) + public EventConsumerInfo ToInfo(string name) + { + return SimpleMapper.Map(this, new EventConsumerInfo { - return SimpleMapper.Map(this, new EventConsumerInfo - { - Name = name - }); - } + Name = name + }); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs index 16165ee685..9b4ab1759d 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs @@ -9,75 +9,74 @@ using Squidex.Infrastructure.Timers; using Squidex.Messaging; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public sealed class EventConsumerWorker : + IMessageHandler<EventConsumerStart>, + IMessageHandler<EventConsumerStop>, + IMessageHandler<EventConsumerReset>, + IBackgroundProcess { - public sealed class EventConsumerWorker : - IMessageHandler<EventConsumerStart>, - IMessageHandler<EventConsumerStop>, - IMessageHandler<EventConsumerReset>, - IBackgroundProcess + private readonly Dictionary<string, EventConsumerProcessor> processors = new Dictionary<string, EventConsumerProcessor>(); + private CompletionTimer? timer; + + public EventConsumerWorker(IEnumerable<IEventConsumer> eventConsumers, + Func<IEventConsumer, EventConsumerProcessor> factory) { - private readonly Dictionary<string, EventConsumerProcessor> processors = new Dictionary<string, EventConsumerProcessor>(); - private CompletionTimer? timer; + foreach (var consumer in eventConsumers) + { + processors[consumer.Name] = factory(consumer); + } + } - public EventConsumerWorker(IEnumerable<IEventConsumer> eventConsumers, - Func<IEventConsumer, EventConsumerProcessor> factory) + public async Task StartAsync( + CancellationToken ct) + { + foreach (var (_, processor) in processors) { - foreach (var consumer in eventConsumers) - { - processors[consumer.Name] = factory(consumer); - } + await processor.InitializeAsync(ct); + await processor.ActivateAsync(); } - public async Task StartAsync( - CancellationToken ct) + timer = new CompletionTimer(TimeSpan.FromSeconds(30), async ct => { foreach (var (_, processor) in processors) { - await processor.InitializeAsync(ct); await processor.ActivateAsync(); } + }); + } - timer = new CompletionTimer(TimeSpan.FromSeconds(30), async ct => - { - foreach (var (_, processor) in processors) - { - await processor.ActivateAsync(); - } - }); - } - - public Task StopAsync( - CancellationToken ct) - { - return timer?.StopAsync() ?? Task.CompletedTask; - } + public Task StopAsync( + CancellationToken ct) + { + return timer?.StopAsync() ?? Task.CompletedTask; + } - public async Task HandleAsync(EventConsumerStart message, - CancellationToken ct) + public async Task HandleAsync(EventConsumerStart message, + CancellationToken ct) + { + if (processors.TryGetValue(message.Name, out var processor)) { - if (processors.TryGetValue(message.Name, out var processor)) - { - await processor.StartAsync(); - } + await processor.StartAsync(); } + } - public async Task HandleAsync(EventConsumerStop message, - CancellationToken ct) + public async Task HandleAsync(EventConsumerStop message, + CancellationToken ct) + { + if (processors.TryGetValue(message.Name, out var processor)) { - if (processors.TryGetValue(message.Name, out var processor)) - { - await processor.StopAsync(); - } + await processor.StopAsync(); } + } - public async Task HandleAsync(EventConsumerReset message, - CancellationToken ct) + public async Task HandleAsync(EventConsumerReset message, + CancellationToken ct) + { + if (processors.TryGetValue(message.Name, out var processor)) { - if (processors.TryGetValue(message.Name, out var processor)) - { - await processor.ResetAsync(); - } + await processor.ResetAsync(); } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/IEventConsumerManager.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/IEventConsumerManager.cs index 025628ad3a..77b4e98814 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/IEventConsumerManager.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/IEventConsumerManager.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public interface IEventConsumerManager { - public interface IEventConsumerManager - { - Task<List<EventConsumerInfo>> GetConsumersAsync( - CancellationToken ct = default); + Task<List<EventConsumerInfo>> GetConsumersAsync( + CancellationToken ct = default); - Task<EventConsumerInfo> ResetAsync(string consumerName, - CancellationToken ct = default); + Task<EventConsumerInfo> ResetAsync(string consumerName, + CancellationToken ct = default); - Task<EventConsumerInfo> StopAsync(string consumerName, - CancellationToken ct = default); + Task<EventConsumerInfo> StopAsync(string consumerName, + CancellationToken ct = default); - Task<EventConsumerInfo> StartAsync(string consumerName, - CancellationToken ct = default); - } + Task<EventConsumerInfo> StartAsync(string consumerName, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/Messages.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/Messages.cs index 511bfd34d1..bc2aab0d1e 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/Messages.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/Messages.cs @@ -8,11 +8,10 @@ #pragma warning disable MA0048 // File name must match type name #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.EventSourcing.Consume -{ - public sealed record EventConsumerReset(string Name); +namespace Squidex.Infrastructure.EventSourcing.Consume; - public sealed record EventConsumerStart(string Name); +public sealed record EventConsumerReset(string Name); - public sealed record EventConsumerStop(string Name); -} +public sealed record EventConsumerStart(string Name); + +public sealed record EventConsumerStop(string Name); diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs index 406c30464b..a75380ec7b 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs @@ -7,148 +7,147 @@ using System.Threading.Channels; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +internal sealed class ParseSubscription : IEventSubscriber<StoredEvent>, IEventSubscription { - internal sealed class ParseSubscription : IEventSubscriber<StoredEvent>, IEventSubscription + private readonly Channel<object> deserializeQueue; + private readonly CancellationTokenSource completed = new CancellationTokenSource(); + private readonly Task deserializeTask; + private readonly IEventSubscription eventSubscription; + + public ParseSubscription( + IEventConsumer eventConsumer, + IEventFormatter eventFormatter, + IEventSubscriber<ParsedEvent> eventSubscriber, + EventSubscriptionSource<StoredEvent> eventSource) { - private readonly Channel<object> deserializeQueue; - private readonly CancellationTokenSource completed = new CancellationTokenSource(); - private readonly Task deserializeTask; - private readonly IEventSubscription eventSubscription; - - public ParseSubscription( - IEventConsumer eventConsumer, - IEventFormatter eventFormatter, - IEventSubscriber<ParsedEvent> eventSubscriber, - EventSubscriptionSource<StoredEvent> eventSource) + deserializeQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2) { - deserializeQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2) - { - AllowSynchronousContinuations = true, - SingleReader = true, - SingleWriter = true - }); + AllowSynchronousContinuations = true, + SingleReader = true, + SingleWriter = true + }); #pragma warning disable MA0040 // Flow the cancellation token - deserializeTask = Task.Run(async () => + deserializeTask = Task.Run(async () => + { + try { - try - { - var isFailed = false; + var isFailed = false; - await foreach (var input in deserializeQueue.Reader.ReadAllAsync(completed.Token)) + await foreach (var input in deserializeQueue.Reader.ReadAllAsync(completed.Token)) + { + switch (input) { - switch (input) - { - case Exception exception: - { - // Not very likely that the task is cancelled. - await eventSubscriber.OnErrorAsync(this, exception); + case Exception exception: + { + // Not very likely that the task is cancelled. + await eventSubscriber.OnErrorAsync(this, exception); - isFailed = true; - break; - } + isFailed = true; + break; + } - case StoredEvent storedEvent: - { - Envelope<IEvent>? @event = null; + case StoredEvent storedEvent: + { + Envelope<IEvent>? @event = null; - if (eventConsumer.Handles(storedEvent)) - { - @event = eventFormatter.ParseIfKnown(storedEvent); - } - - // Parsing takes a little bit of time, so the task might have been cancelled. - if (!completed.IsCancellationRequested) - { - // Also invoke the subscriber if the event is null to update the position. - await eventSubscriber.OnNextAsync(this, new ParsedEvent(@event, storedEvent.EventPosition)); - } + if (eventConsumer.Handles(storedEvent)) + { + @event = eventFormatter.ParseIfKnown(storedEvent); + } - break; + // Parsing takes a little bit of time, so the task might have been cancelled. + if (!completed.IsCancellationRequested) + { + // Also invoke the subscriber if the event is null to update the position. + await eventSubscriber.OnNextAsync(this, new ParsedEvent(@event, storedEvent.EventPosition)); } - } - if (isFailed) - { - break; - } + break; + } } - } - catch (OperationCanceledException) - { - return; - } - catch (Exception ex) - { - if (!completed.IsCancellationRequested) + + if (isFailed) { - await eventSubscriber.OnErrorAsync(this, ex); + break; } } - }).ContinueWith(x => deserializeQueue.Writer.TryComplete(x.Exception)); - - // Run last to subscribe after everything is configured. - eventSubscription = eventSource(this); - } - - public void Dispose() - { - if (completed.IsCancellationRequested) + } + catch (OperationCanceledException) { return; } + catch (Exception ex) + { + if (!completed.IsCancellationRequested) + { + await eventSubscriber.OnErrorAsync(this, ex); + } + } + }).ContinueWith(x => deserializeQueue.Writer.TryComplete(x.Exception)); - // It is not necessary to dispose the cancellation token source. - completed.Cancel(); + // Run last to subscribe after everything is configured. + eventSubscription = eventSource(this); + } - // We do not lock here, it is the responsibility of the source subscription to be thread safe. - eventSubscription.Dispose(); + public void Dispose() + { + if (completed.IsCancellationRequested) + { + return; } - public async ValueTask CompleteAsync() - { - await eventSubscription.CompleteAsync(); + // It is not necessary to dispose the cancellation token source. + completed.Cancel(); - deserializeQueue.Writer.TryComplete(); + // We do not lock here, it is the responsibility of the source subscription to be thread safe. + eventSubscription.Dispose(); + } - await deserializeTask; - } + public async ValueTask CompleteAsync() + { + await eventSubscription.CompleteAsync(); + + deserializeQueue.Writer.TryComplete(); - public void WakeUp() + await deserializeTask; + } + + public void WakeUp() + { + eventSubscription.WakeUp(); + } + + async ValueTask IEventSubscriber<StoredEvent>.OnErrorAsync(IEventSubscription subscription, Exception exception) + { + try { - eventSubscription.WakeUp(); + // Forward the exception from one task only. + await deserializeQueue.Writer.WriteAsync(exception, completed.Token); } - - async ValueTask IEventSubscriber<StoredEvent>.OnErrorAsync(IEventSubscription subscription, Exception exception) + catch (OperationCanceledException) { - try - { - // Forward the exception from one task only. - await deserializeQueue.Writer.WriteAsync(exception, completed.Token); - } - catch (OperationCanceledException) - { - // These exception are acceptable and happens when an exception has been thrown before. - } - catch (ChannelClosedException) - { - } + // These exception are acceptable and happens when an exception has been thrown before. } + catch (ChannelClosedException) + { + } + } - async ValueTask IEventSubscriber<StoredEvent>.OnNextAsync(IEventSubscription subscription, StoredEvent @event) + async ValueTask IEventSubscriber<StoredEvent>.OnNextAsync(IEventSubscription subscription, StoredEvent @event) + { + try + { + await deserializeQueue.Writer.WriteAsync(@event, completed.Token); + } + catch (OperationCanceledException) + { + // These exception are acceptable and happens when an exception has been thrown before. + } + catch (ChannelClosedException) { - try - { - await deserializeQueue.Writer.WriteAsync(@event, completed.Token); - } - catch (OperationCanceledException) - { - // These exception are acceptable and happens when an exception has been thrown before. - } - catch (ChannelClosedException) - { - } } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParsedEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParsedEvent.cs index f7f62bc299..13b245afc2 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParsedEvent.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParsedEvent.cs @@ -8,9 +8,8 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.EventSourcing.Consume -{ - public record struct ParsedEvent(Envelope<IEvent>? Event, string Position); +namespace Squidex.Infrastructure.EventSourcing.Consume; - public record struct ParsedEvents(List<Envelope<IEvent>> Events, string Position); -} +public record struct ParsedEvent(Envelope<IEvent>? Event, string Position); + +public record struct ParsedEvents(List<Envelope<IEvent>> Events, string Position); diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs index 933cc03736..ca8fcc9ae3 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs @@ -9,77 +9,76 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class DefaultEventFormatter : IEventFormatter { - public sealed class DefaultEventFormatter : IEventFormatter + private readonly IJsonSerializer serializer; + private readonly TypeNameRegistry typeNameRegistry; + + public DefaultEventFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer) { - private readonly IJsonSerializer serializer; - private readonly TypeNameRegistry typeNameRegistry; + this.typeNameRegistry = typeNameRegistry; + this.serializer = serializer; + } - public DefaultEventFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer) - { - this.typeNameRegistry = typeNameRegistry; - this.serializer = serializer; - } + public Envelope<IEvent>? ParseIfKnown(StoredEvent storedEvent) + { + return ParseCore(storedEvent); + } + + public Envelope<IEvent> Parse(StoredEvent storedEvent) + { + var envelope = ParseCore(storedEvent); - public Envelope<IEvent>? ParseIfKnown(StoredEvent storedEvent) + if (envelope == null) { - return ParseCore(storedEvent); + throw new TypeNameNotFoundException($"Cannot find event with type name '{storedEvent.Data.Type}'."); } - public Envelope<IEvent> Parse(StoredEvent storedEvent) - { - var envelope = ParseCore(storedEvent); + return envelope; + } - if (envelope == null) - { - throw new TypeNameNotFoundException($"Cannot find event with type name '{storedEvent.Data.Type}'."); - } + private Envelope<IEvent>? ParseCore(StoredEvent storedEvent) + { + Guard.NotNull(storedEvent); - return envelope; - } + var payloadType = typeNameRegistry.GetTypeOrNull(storedEvent.Data.Type); - private Envelope<IEvent>? ParseCore(StoredEvent storedEvent) + if (payloadType == null) { - Guard.NotNull(storedEvent); - - var payloadType = typeNameRegistry.GetTypeOrNull(storedEvent.Data.Type); + return null; + } - if (payloadType == null) - { - return null; - } + var payloadValue = serializer.Deserialize<IEvent>(storedEvent.Data.Payload, payloadType); - var payloadValue = serializer.Deserialize<IEvent>(storedEvent.Data.Payload, payloadType); + if (payloadValue is IMigrated<IEvent> migratedEvent) + { + payloadValue = migratedEvent.Migrate(); + } - if (payloadValue is IMigrated<IEvent> migratedEvent) - { - payloadValue = migratedEvent.Migrate(); - } + var envelope = new Envelope<IEvent>(payloadValue, storedEvent.Data.Headers); - var envelope = new Envelope<IEvent>(payloadValue, storedEvent.Data.Headers); + envelope.SetEventPosition(storedEvent.EventPosition); + envelope.SetEventStreamNumber(storedEvent.EventStreamNumber); - envelope.SetEventPosition(storedEvent.EventPosition); - envelope.SetEventStreamNumber(storedEvent.EventStreamNumber); + return envelope; + } - return envelope; - } + public EventData ToEventData(Envelope<IEvent> envelope, Guid commitId, bool migrate = true) + { + var payloadValue = envelope.Payload; - public EventData ToEventData(Envelope<IEvent> envelope, Guid commitId, bool migrate = true) + if (migrate && payloadValue is IMigrated<IEvent> migratedEvent) { - var payloadValue = envelope.Payload; - - if (migrate && payloadValue is IMigrated<IEvent> migratedEvent) - { - payloadValue = migratedEvent.Migrate(); - } + payloadValue = migratedEvent.Migrate(); + } - var payloadType = typeNameRegistry.GetName(payloadValue.GetType()); - var payloadJson = serializer.Serialize(envelope.Payload, envelope.Payload.GetType()); + var payloadType = typeNameRegistry.GetName(payloadValue.GetType()); + var payloadJson = serializer.Serialize(envelope.Payload, envelope.Payload.GetType()); - envelope.SetCommitId(commitId); + envelope.SetCommitId(commitId); - return new EventData(payloadType, envelope.Headers, payloadJson); - } + return new EventData(payloadType, envelope.Headers, payloadJson); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope.cs index 72d19b8120..6160d29288 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope.cs @@ -7,20 +7,19 @@ using NodaTime; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public static class Envelope { - public static class Envelope + public static Envelope<TPayload> Create<TPayload>(TPayload payload) where TPayload : class, IEvent { - public static Envelope<TPayload> Create<TPayload>(TPayload payload) where TPayload : class, IEvent - { - var eventId = Guid.NewGuid(); + var eventId = Guid.NewGuid(); - var envelope = - new Envelope<TPayload>(payload) - .SetEventId(eventId) - .SetTimestamp(SystemClock.Instance.GetCurrentInstant()); + var envelope = + new Envelope<TPayload>(payload) + .SetEventId(eventId) + .SetTimestamp(SystemClock.Instance.GetCurrentInstant()); - return envelope; - } + return envelope; } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs index c871c5178b..de69af9c24 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs @@ -9,149 +9,148 @@ using NodaTime; using NodaTime.Text; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public static class EnvelopeExtensions { - public static class EnvelopeExtensions + public static string EventPosition(this EnvelopeHeaders headers) { - public static string EventPosition(this EnvelopeHeaders headers) - { - return headers.GetString(CommonHeaders.EventNumber); - } + return headers.GetString(CommonHeaders.EventNumber); + } - public static Envelope<T> SetEventPosition<T>(this Envelope<T> envelope, string value) where T : class, IEvent - { - envelope.Headers[CommonHeaders.EventNumber] = value; + public static Envelope<T> SetEventPosition<T>(this Envelope<T> envelope, string value) where T : class, IEvent + { + envelope.Headers[CommonHeaders.EventNumber] = value; - return envelope; - } + return envelope; + } - public static long EventStreamNumber(this EnvelopeHeaders headers) - { - return headers.GetLong(CommonHeaders.EventStreamNumber); - } + public static long EventStreamNumber(this EnvelopeHeaders headers) + { + return headers.GetLong(CommonHeaders.EventStreamNumber); + } - public static Envelope<T> SetEventStreamNumber<T>(this Envelope<T> envelope, long value) where T : class, IEvent - { - envelope.Headers[CommonHeaders.EventStreamNumber] = (double)value; + public static Envelope<T> SetEventStreamNumber<T>(this Envelope<T> envelope, long value) where T : class, IEvent + { + envelope.Headers[CommonHeaders.EventStreamNumber] = (double)value; - return envelope; - } + return envelope; + } - public static Guid CommitId(this EnvelopeHeaders headers) - { - return headers.GetGuid(CommonHeaders.CommitId); - } + public static Guid CommitId(this EnvelopeHeaders headers) + { + return headers.GetGuid(CommonHeaders.CommitId); + } - public static Envelope<T> SetCommitId<T>(this Envelope<T> envelope, Guid value) where T : class, IEvent - { - envelope.Headers[CommonHeaders.CommitId] = value.ToString(); + public static Envelope<T> SetCommitId<T>(this Envelope<T> envelope, Guid value) where T : class, IEvent + { + envelope.Headers[CommonHeaders.CommitId] = value.ToString(); - return envelope; - } + return envelope; + } - public static DomainId AggregateId(this EnvelopeHeaders headers) - { - return DomainId.Create(headers.GetString(CommonHeaders.AggregateId)); - } + public static DomainId AggregateId(this EnvelopeHeaders headers) + { + return DomainId.Create(headers.GetString(CommonHeaders.AggregateId)); + } - public static Envelope<T> SetAggregateId<T>(this Envelope<T> envelope, DomainId value) where T : class, IEvent - { - envelope.Headers[CommonHeaders.AggregateId] = value; + public static Envelope<T> SetAggregateId<T>(this Envelope<T> envelope, DomainId value) where T : class, IEvent + { + envelope.Headers[CommonHeaders.AggregateId] = value; - return envelope; - } + return envelope; + } - public static Guid EventId(this EnvelopeHeaders headers) - { - return headers.GetGuid(CommonHeaders.EventId); - } + public static Guid EventId(this EnvelopeHeaders headers) + { + return headers.GetGuid(CommonHeaders.EventId); + } - public static Envelope<T> SetEventId<T>(this Envelope<T> envelope, Guid value) where T : class, IEvent - { - envelope.Headers[CommonHeaders.EventId] = value.ToString(); + public static Envelope<T> SetEventId<T>(this Envelope<T> envelope, Guid value) where T : class, IEvent + { + envelope.Headers[CommonHeaders.EventId] = value.ToString(); - return envelope; - } + return envelope; + } - public static Instant Timestamp(this EnvelopeHeaders headers) - { - return headers.GetInstant(CommonHeaders.Timestamp); - } + public static Instant Timestamp(this EnvelopeHeaders headers) + { + return headers.GetInstant(CommonHeaders.Timestamp); + } - public static Envelope<T> SetTimestamp<T>(this Envelope<T> envelope, Instant value) where T : class, IEvent - { - envelope.Headers[CommonHeaders.Timestamp] = value; + public static Envelope<T> SetTimestamp<T>(this Envelope<T> envelope, Instant value) where T : class, IEvent + { + envelope.Headers[CommonHeaders.Timestamp] = value; - return envelope; - } + return envelope; + } - public static bool Restored(this EnvelopeHeaders headers) - { - return headers.GetBoolean(CommonHeaders.Restored); - } + public static bool Restored(this EnvelopeHeaders headers) + { + return headers.GetBoolean(CommonHeaders.Restored); + } - public static Envelope<T> SetRestored<T>(this Envelope<T> envelope, bool value = true) where T : class, IEvent - { - envelope.Headers[CommonHeaders.Restored] = value; + public static Envelope<T> SetRestored<T>(this Envelope<T> envelope, bool value = true) where T : class, IEvent + { + envelope.Headers[CommonHeaders.Restored] = value; - return envelope; - } + return envelope; + } - public static long GetLong(this EnvelopeHeaders obj, string key) + public static long GetLong(this EnvelopeHeaders obj, string key) + { + if (obj.TryGetValue(key, out var found)) { - if (obj.TryGetValue(key, out var found)) + if (found.Value is double d) { - if (found.Value is double d) - { - return (long)d; - } - else if (found.Value is string s && double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) - { - return (long)result; - } + return (long)d; } - - return 0; - } - - public static Guid GetGuid(this EnvelopeHeaders obj, string key) - { - if (obj.TryGetValue(key, out var found) && found.Value is string s && Guid.TryParse(s, out var guid)) + else if (found.Value is string s && double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { - return guid; + return (long)result; } - - return default; } - public static Instant GetInstant(this EnvelopeHeaders obj, string key) - { - if (obj.TryGetValue(key, out var found) && found.Value is string s && InstantPattern.ExtendedIso.Parse(s).TryGetValue(default, out var instant)) - { - return instant; - } + return 0; + } - return default; + public static Guid GetGuid(this EnvelopeHeaders obj, string key) + { + if (obj.TryGetValue(key, out var found) && found.Value is string s && Guid.TryParse(s, out var guid)) + { + return guid; } - public static string GetString(this EnvelopeHeaders obj, string key) - { - if (obj.TryGetValue(key, out var found)) - { - return found.ToString(); - } + return default; + } - return string.Empty; + public static Instant GetInstant(this EnvelopeHeaders obj, string key) + { + if (obj.TryGetValue(key, out var found) && found.Value is string s && InstantPattern.ExtendedIso.Parse(s).TryGetValue(default, out var instant)) + { + return instant; } - public static bool GetBoolean(this EnvelopeHeaders obj, string key) + return default; + } + + public static string GetString(this EnvelopeHeaders obj, string key) + { + if (obj.TryGetValue(key, out var found)) { - if (obj.TryGetValue(key, out var found) && found.Value is bool b) - { - return b; - } + return found.ToString(); + } - return false; + return string.Empty; + } + + public static bool GetBoolean(this EnvelopeHeaders obj, string key) + { + if (obj.TryGetValue(key, out var found) && found.Value is bool b) + { + return b; } + + return false; } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs index 8c6b436fde..f951167f52 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeHeaders.cs @@ -7,22 +7,21 @@ using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class EnvelopeHeaders : Dictionary<string, JsonValue> { - public sealed class EnvelopeHeaders : Dictionary<string, JsonValue> + public EnvelopeHeaders() { - public EnvelopeHeaders() - { - } + } - public EnvelopeHeaders(IDictionary<string, JsonValue> headers) - : base(headers) - { - } + public EnvelopeHeaders(IDictionary<string, JsonValue> headers) + : base(headers) + { + } - public EnvelopeHeaders CloneHeaders() - { - return new EnvelopeHeaders(this); - } + public EnvelopeHeaders CloneHeaders() + { + return new EnvelopeHeaders(this); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs index f5049aa910..2cf5d30ad8 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs @@ -5,40 +5,39 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public class Envelope<T> where T : class, IEvent { - public class Envelope<T> where T : class, IEvent - { - private readonly EnvelopeHeaders headers; - private readonly T payload; + private readonly EnvelopeHeaders headers; + private readonly T payload; - public EnvelopeHeaders Headers - { - get => headers; - } + public EnvelopeHeaders Headers + { + get => headers; + } - public T Payload - { - get => payload; - } + public T Payload + { + get => payload; + } - public Envelope(T payload, EnvelopeHeaders? headers = null) - { - Guard.NotNull(payload); + public Envelope(T payload, EnvelopeHeaders? headers = null) + { + Guard.NotNull(payload); - this.payload = payload; + this.payload = payload; - this.headers = headers ?? new EnvelopeHeaders(); - } + this.headers = headers ?? new EnvelopeHeaders(); + } - public Envelope<TOther> To<TOther>() where TOther : class, IEvent - { - return new Envelope<TOther>((payload as TOther)!, headers.CloneHeaders()); - } + public Envelope<TOther> To<TOther>() where TOther : class, IEvent + { + return new Envelope<TOther>((payload as TOther)!, headers.CloneHeaders()); + } - public static implicit operator Envelope<IEvent>(Envelope<T> source) - { - return source == null ? source! : new Envelope<IEvent>(source.payload, source.headers); - } + public static implicit operator Envelope<IEvent>(Envelope<T> source) + { + return source == null ? source! : new Envelope<IEvent>(source.payload, source.headers); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs index 1f06c48960..b2bf43e315 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs @@ -7,22 +7,21 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed record EventCommit(Guid Id, string StreamName, long Offset, ICollection<EventData> Events) { - public sealed record EventCommit(Guid Id, string StreamName, long Offset, ICollection<EventData> Events) + public static EventCommit Create(Guid id, string streamName, long offset, EventData @event) { - public static EventCommit Create(Guid id, string streamName, long offset, EventData @event) - { - return new EventCommit(id, streamName, offset, new List<EventData> { @event }); - } + return new EventCommit(id, streamName, offset, new List<EventData> { @event }); + } - public static EventCommit Create(string streamName, long offset, Envelope<IEvent> envelope, IEventFormatter eventFormatter) - { - var id = Guid.NewGuid(); + public static EventCommit Create(string streamName, long offset, Envelope<IEvent> envelope, IEventFormatter eventFormatter) + { + var id = Guid.NewGuid(); - var eventData = eventFormatter.ToEventData(envelope, id); + var eventData = eventFormatter.ToEventData(envelope, id); - return new EventCommit(id, streamName, offset, new List<EventData> { eventData }); - } + return new EventCommit(id, streamName, offset, new List<EventData> { eventData }); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs index 2241fef5ed..40176c0e61 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs @@ -5,18 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed record EventConsumerInfo { - public sealed record EventConsumerInfo - { - public bool IsStopped { get; init; } + public bool IsStopped { get; init; } - public int Count { get; init; } + public int Count { get; init; } - public string Name { get; init; } + public string Name { get; init; } - public string Error { get; init; } + public string Error { get; init; } - public string Position { get; init; } - } + public string Position { get; init; } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs index 0aaaf6d420..6ff4ec3134 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventData.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed record EventData(string Type, EnvelopeHeaders Headers, string Payload); -} \ No newline at end of file +namespace Squidex.Infrastructure.EventSourcing; + +public sealed record EventData(string Type, EnvelopeHeaders Headers, string Payload); \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs index af5f23e9f6..3642333330 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs @@ -7,19 +7,18 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class EventTypeAttribute : TypeNameAttribute { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class EventTypeAttribute : TypeNameAttribute + public EventTypeAttribute(string typeName, int version = 1) + : base(CreateTypeName(typeName, version)) { - public EventTypeAttribute(string typeName, int version = 1) - : base(CreateTypeName(typeName, version)) - { - } + } - private static string CreateTypeName(string typeName, int version) - { - return $"{typeName}Event" + (version > 1 ? $"V{version}" : string.Empty); - } + private static string CreateTypeName(string typeName, int version) + { + return $"{typeName}Event" + (version > 1 ? $"V{version}" : string.Empty); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEvent.cs index 9c2892ec38..d781d61e1e 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEvent.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEvent.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public interface IEvent { - public interface IEvent - { - } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs index 6cddd2e7a7..cc664b1f3d 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs @@ -5,43 +5,42 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public interface IEventConsumer { - public interface IEventConsumer - { - int BatchDelay => 500; + int BatchDelay => 500; - int BatchSize => 1; + int BatchSize => 1; - string Name { get; } + string Name { get; } - string EventsFilter => ".*"; + string EventsFilter => ".*"; - bool StartLatest => false; + bool StartLatest => false; - bool CanClear => true; + bool CanClear => true; - bool Handles(StoredEvent @event) - { - return true; - } + bool Handles(StoredEvent @event) + { + return true; + } - Task ClearAsync() - { - return Task.CompletedTask; - } + Task ClearAsync() + { + return Task.CompletedTask; + } - Task On(Envelope<IEvent> @event) - { - return Task.CompletedTask; - } + Task On(Envelope<IEvent> @event) + { + return Task.CompletedTask; + } - async Task On(IEnumerable<Envelope<IEvent>> events) + async Task On(IEnumerable<Envelope<IEvent>> events) + { + foreach (var @event in events) { - foreach (var @event in events) - { - await On(@event); - } + await On(@event); } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventFormatter.cs index c1bb31a6da..e9ab3cb214 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventFormatter.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventFormatter.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public interface IEventFormatter { - public interface IEventFormatter - { - Envelope<IEvent> Parse(StoredEvent storedEvent); + Envelope<IEvent> Parse(StoredEvent storedEvent); - Envelope<IEvent>? ParseIfKnown(StoredEvent storedEvent); + Envelope<IEvent>? ParseIfKnown(StoredEvent storedEvent); - EventData ToEventData(Envelope<IEvent> envelope, Guid commitId, bool migrate = true); - } + EventData ToEventData(Envelope<IEvent> envelope, Guid commitId, bool migrate = true); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs index 8c5f13cbe3..8303359310 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs @@ -7,17 +7,16 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public interface IEventNotifier { - public interface IEventNotifier - { - void NotifyEventsStored(string streamName); - } + void NotifyEventsStored(string streamName); +} - public sealed class NoopEventNotifier : IEventNotifier +public sealed class NoopEventNotifier : IEventNotifier +{ + public void NotifyEventsStored(string streamName) { - public void NotifyEventsStored(string streamName) - { - } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs index 0b81586edf..432796be76 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs @@ -7,56 +7,55 @@ using NodaTime; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public interface IEventStore { - public interface IEventStore - { - Task<IReadOnlyList<StoredEvent>> QueryReverseAsync(string streamName, int take = int.MaxValue, - CancellationToken ct = default); + Task<IReadOnlyList<StoredEvent>> QueryReverseAsync(string streamName, int take = int.MaxValue, + CancellationToken ct = default); - Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0, - CancellationToken ct = default); + Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0, + CancellationToken ct = default); - IAsyncEnumerable<StoredEvent> QueryAllReverseAsync(string? streamFilter = null, Instant timestamp = default, int take = int.MaxValue, - CancellationToken ct = default); + IAsyncEnumerable<StoredEvent> QueryAllReverseAsync(string? streamFilter = null, Instant timestamp = default, int take = int.MaxValue, + CancellationToken ct = default); - IAsyncEnumerable<StoredEvent> QueryAllAsync(string? streamFilter = null, string? position = null, int take = int.MaxValue, - CancellationToken ct = default); + IAsyncEnumerable<StoredEvent> QueryAllAsync(string? streamFilter = null, string? position = null, int take = int.MaxValue, + CancellationToken ct = default); - Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events, - CancellationToken ct = default); + Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events, + CancellationToken ct = default); - Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events, - CancellationToken ct = default); + Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> events, + CancellationToken ct = default); - Task DeleteAsync(string streamFilter, - CancellationToken ct = default); + Task DeleteAsync(string streamFilter, + CancellationToken ct = default); - Task DeleteStreamAsync(string streamName, - CancellationToken ct = default); + Task DeleteStreamAsync(string streamName, + CancellationToken ct = default); - IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> eventSubscriber, string? streamFilter = null, string? position = null); + IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> eventSubscriber, string? streamFilter = null, string? position = null); - async Task AppendUnsafeAsync(IEnumerable<EventCommit> commits, - CancellationToken ct = default) + async Task AppendUnsafeAsync(IEnumerable<EventCommit> commits, + CancellationToken ct = default) + { + foreach (var commit in commits) { - foreach (var commit in commits) - { - await AppendAsync(commit.Id, commit.StreamName, commit.Offset, commit.Events, ct); - } + await AppendAsync(commit.Id, commit.StreamName, commit.Offset, commit.Events, ct); } + } - async Task<IReadOnlyDictionary<string, IReadOnlyList<StoredEvent>>> QueryManyAsync(IEnumerable<string> streamNames, - CancellationToken ct = default) - { - var result = new Dictionary<string, IReadOnlyList<StoredEvent>>(); - - foreach (var streamName in streamNames) - { - result[streamName] = await QueryAsync(streamName, 0, ct); - } + async Task<IReadOnlyDictionary<string, IReadOnlyList<StoredEvent>>> QueryManyAsync(IEnumerable<string> streamNames, + CancellationToken ct = default) + { + var result = new Dictionary<string, IReadOnlyList<StoredEvent>>(); - return result; + foreach (var streamName in streamNames) + { + result[streamName] = await QueryAsync(streamName, 0, ct); } + + return result; } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs index 6e94121118..267d1ae7e4 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs @@ -7,14 +7,13 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.EventSourcing -{ - public delegate IEventSubscription EventSubscriptionSource<T>(IEventSubscriber<T> target); +namespace Squidex.Infrastructure.EventSourcing; + +public delegate IEventSubscription EventSubscriptionSource<T>(IEventSubscriber<T> target); - public interface IEventSubscriber<T> - { - ValueTask OnNextAsync(IEventSubscription subscription, T @event); +public interface IEventSubscriber<T> +{ + ValueTask OnNextAsync(IEventSubscription subscription, T @event); - ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception); - } + ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs index d6f8d8cd25..8dd024b60c 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public interface IEventSubscription : IDisposable { - public interface IEventSubscription : IDisposable - { - void WakeUp(); + void WakeUp(); - ValueTask CompleteAsync(); - } + ValueTask CompleteAsync(); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs index 927240c744..acc12d0425 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs @@ -8,49 +8,48 @@ using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Timers; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class PollingSubscription : IEventSubscription { - public sealed class PollingSubscription : IEventSubscription - { - private readonly CompletionTimer timer; + private readonly CompletionTimer timer; - public PollingSubscription( - IEventStore eventStore, - IEventSubscriber<StoredEvent> eventSubscriber, - string? streamFilter, - string? position) + public PollingSubscription( + IEventStore eventStore, + IEventSubscriber<StoredEvent> eventSubscriber, + string? streamFilter, + string? position) + { + timer = new CompletionTimer(5000, async ct => { - timer = new CompletionTimer(5000, async ct => + try { - try + await foreach (var storedEvent in eventStore.QueryAllAsync(streamFilter, position, ct: ct)) { - await foreach (var storedEvent in eventStore.QueryAllAsync(streamFilter, position, ct: ct)) - { - await eventSubscriber.OnNextAsync(this, storedEvent); + await eventSubscriber.OnNextAsync(this, storedEvent); - position = storedEvent.EventPosition; - } - } - catch (Exception ex) - { - await eventSubscriber.OnErrorAsync(this, ex); + position = storedEvent.EventPosition; } - }); - } + } + catch (Exception ex) + { + await eventSubscriber.OnErrorAsync(this, ex); + } + }); + } - public ValueTask CompleteAsync() - { - return new ValueTask(timer.StopAsync()); - } + public ValueTask CompleteAsync() + { + return new ValueTask(timer.StopAsync()); + } - public void Dispose() - { - timer.StopAsync().Forget(); - } + public void Dispose() + { + timer.StopAsync().Forget(); + } - public void WakeUp() - { - timer.SkipCurrentDelay(); - } + public void WakeUp() + { + timer.SkipCurrentDelay(); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs index 72a716382e..adccb47168 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs @@ -5,133 +5,132 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class RetrySubscription<T> : IEventSubscription, IEventSubscriber<T> { - public sealed class RetrySubscription<T> : IEventSubscription, IEventSubscriber<T> - { - private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); - private readonly IEventSubscriber<T> eventSubscriber; - private readonly EventSubscriptionSource<T> eventSource; - private SubscriptionHolder? currentSubscription; + private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); + private readonly IEventSubscriber<T> eventSubscriber; + private readonly EventSubscriptionSource<T> eventSource; + private SubscriptionHolder? currentSubscription; - public int ReconnectWaitMs { get; set; } = 5000; + public int ReconnectWaitMs { get; set; } = 5000; - public bool IsSubscribed => currentSubscription != null; + public bool IsSubscribed => currentSubscription != null; - // Holds all information for a current subscription. Therefore we only have to maintain one reference. - private sealed class SubscriptionHolder : IDisposable - { - public CancellationTokenSource Cancellation { get; } = new CancellationTokenSource(); + // Holds all information for a current subscription. Therefore we only have to maintain one reference. + private sealed class SubscriptionHolder : IDisposable + { + public CancellationTokenSource Cancellation { get; } = new CancellationTokenSource(); - public IEventSubscription Subscription { get; } + public IEventSubscription Subscription { get; } - public SubscriptionHolder(IEventSubscription subscription) - { - Subscription = subscription; - } + public SubscriptionHolder(IEventSubscription subscription) + { + Subscription = subscription; + } - public void Dispose() - { - Cancellation.Cancel(); + public void Dispose() + { + Cancellation.Cancel(); - Subscription.Dispose(); - } + Subscription.Dispose(); } + } - public RetrySubscription(IEventSubscriber<T> eventSubscriber, - EventSubscriptionSource<T> eventSource) - { - Guard.NotNull(eventSubscriber); - Guard.NotNull(eventSource); + public RetrySubscription(IEventSubscriber<T> eventSubscriber, + EventSubscriptionSource<T> eventSource) + { + Guard.NotNull(eventSubscriber); + Guard.NotNull(eventSource); - this.eventSubscriber = eventSubscriber; - this.eventSource = eventSource; + this.eventSubscriber = eventSubscriber; + this.eventSource = eventSource; - Subscribe(); - } + Subscribe(); + } - public void Dispose() - { - Unsubscribe(); - } + public void Dispose() + { + Unsubscribe(); + } - private void Subscribe() + private void Subscribe() + { + lock (retryWindow) { - lock (retryWindow) + if (currentSubscription != null) { - if (currentSubscription != null) - { - return; - } - - currentSubscription = new SubscriptionHolder(eventSource(this)); + return; } + + currentSubscription = new SubscriptionHolder(eventSource(this)); } + } - private void Unsubscribe() + private void Unsubscribe() + { + lock (retryWindow) { - lock (retryWindow) + if (currentSubscription == null) { - if (currentSubscription == null) - { - return; - } - - currentSubscription.Dispose(); - currentSubscription = null; + return; } - } - public void WakeUp() - { - currentSubscription?.Subscription.WakeUp(); + currentSubscription.Dispose(); + currentSubscription = null; } + } + + public void WakeUp() + { + currentSubscription?.Subscription.WakeUp(); + } - public ValueTask CompleteAsync() + public ValueTask CompleteAsync() + { + return currentSubscription?.Subscription.CompleteAsync() ?? default; + } + + async ValueTask IEventSubscriber<T>.OnNextAsync(IEventSubscription subscription, T @event) + { + if (!ReferenceEquals(subscription, currentSubscription?.Subscription)) { - return currentSubscription?.Subscription.CompleteAsync() ?? default; + return; } - async ValueTask IEventSubscriber<T>.OnNextAsync(IEventSubscription subscription, T @event) - { - if (!ReferenceEquals(subscription, currentSubscription?.Subscription)) - { - return; - } + await eventSubscriber.OnNextAsync(this, @event); + } - await eventSubscriber.OnNextAsync(this, @event); + async ValueTask IEventSubscriber<T>.OnErrorAsync(IEventSubscription subscription, Exception exception) + { + if (exception is OperationCanceledException) + { + return; } - async ValueTask IEventSubscriber<T>.OnErrorAsync(IEventSubscription subscription, Exception exception) + if (!ReferenceEquals(subscription, currentSubscription?.Subscription)) { - if (exception is OperationCanceledException) - { - return; - } - - if (!ReferenceEquals(subscription, currentSubscription?.Subscription)) - { - return; - } + return; + } - Unsubscribe(); + Unsubscribe(); - if (!retryWindow.CanRetryAfterFailure()) - { - await eventSubscriber.OnErrorAsync(this, exception); - return; - } - - try - { - await Task.Delay(ReconnectWaitMs, currentSubscription?.Cancellation?.Token ?? default); - } - catch (OperationCanceledException) - { - return; - } + if (!retryWindow.CanRetryAfterFailure()) + { + await eventSubscriber.OnErrorAsync(this, exception); + return; + } - Subscribe(); + try + { + await Task.Delay(ReconnectWaitMs, currentSubscription?.Cancellation?.Token ?? default); + } + catch (OperationCanceledException) + { + return; } + + Subscribe(); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs b/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs index cbdc9b292d..88811dbcca 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/StoredEvent.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed record StoredEvent(string StreamName, string EventPosition, long EventStreamNumber, EventData Data); -} +namespace Squidex.Infrastructure.EventSourcing; + +public sealed record StoredEvent(string StreamName, string EventPosition, long EventStreamNumber, EventData Data); diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs index fe394428c3..89605f3dc7 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs @@ -7,16 +7,15 @@ using System.Diagnostics.CodeAnalysis; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public static class StreamFilter { - public static class StreamFilter + public static bool IsAll([NotNullWhen(false)] string? filter) { - public static bool IsAll([NotNullWhen(false)] string? filter) - { - return string.IsNullOrWhiteSpace(filter) - || string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase) - || string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase) - || string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase); - } + return string.IsNullOrWhiteSpace(filter) + || string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs b/backend/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs index 4a45984e85..04249dbd61 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/WrongEventVersionException.cs @@ -7,42 +7,41 @@ using System.Runtime.Serialization; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +[Serializable] +public class WrongEventVersionException : Exception { - [Serializable] - public class WrongEventVersionException : Exception - { - public long CurrentVersion { get; } + public long CurrentVersion { get; } - public long ExpectedVersion { get; } + public long ExpectedVersion { get; } - public WrongEventVersionException(long currentVersion, long expectedVersion, Exception? inner = null) - : base(FormatMessage(currentVersion, expectedVersion), inner) - { - CurrentVersion = currentVersion; + public WrongEventVersionException(long currentVersion, long expectedVersion, Exception? inner = null) + : base(FormatMessage(currentVersion, expectedVersion), inner) + { + CurrentVersion = currentVersion; - ExpectedVersion = expectedVersion; - } + ExpectedVersion = expectedVersion; + } - protected WrongEventVersionException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - CurrentVersion = info.GetInt64(nameof(CurrentVersion)); + protected WrongEventVersionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + CurrentVersion = info.GetInt64(nameof(CurrentVersion)); - ExpectedVersion = info.GetInt64(nameof(ExpectedVersion)); - } + ExpectedVersion = info.GetInt64(nameof(ExpectedVersion)); + } - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(CurrentVersion), CurrentVersion); - info.AddValue(nameof(ExpectedVersion), ExpectedVersion); + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(CurrentVersion), CurrentVersion); + info.AddValue(nameof(ExpectedVersion), ExpectedVersion); - base.GetObjectData(info, context); - } + base.GetObjectData(info, context); + } - private static string FormatMessage(long currentVersion, long expectedVersion) - { - return $"Requested version {expectedVersion}, but found {currentVersion}."; - } + private static string FormatMessage(long currentVersion, long expectedVersion) + { + return $"Requested version {expectedVersion}, but found {currentVersion}."; } } diff --git a/backend/src/Squidex.Infrastructure/ExceptionHelper.cs b/backend/src/Squidex.Infrastructure/ExceptionHelper.cs index 65ea120f1b..589d822493 100644 --- a/backend/src/Squidex.Infrastructure/ExceptionHelper.cs +++ b/backend/src/Squidex.Infrastructure/ExceptionHelper.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class ExceptionHelper { - public static class ExceptionHelper + public static bool Is<T>(this Exception ex) where T : Exception { - public static bool Is<T>(this Exception ex) where T : Exception + if (ex is AggregateException aggregateException) { - if (ex is AggregateException aggregateException) - { - aggregateException = aggregateException.Flatten(); - - return aggregateException.InnerExceptions.Count == 1 && Is<T>(aggregateException.InnerExceptions[0]); - } + aggregateException = aggregateException.Flatten(); - return ex is T; + return aggregateException.InnerExceptions.Count == 1 && Is<T>(aggregateException.InnerExceptions[0]); } + + return ex is T; } } diff --git a/backend/src/Squidex.Infrastructure/FileExtensions.cs b/backend/src/Squidex.Infrastructure/FileExtensions.cs index fdc7b95e57..64a68cc1da 100644 --- a/backend/src/Squidex.Infrastructure/FileExtensions.cs +++ b/backend/src/Squidex.Infrastructure/FileExtensions.cs @@ -7,80 +7,79 @@ using System.Globalization; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class FileExtensions { - public static class FileExtensions + private static readonly string[] Extensions = { - private static readonly string[] Extensions = - { - "bytes", - "kB", - "MB", - "GB", - "TB" - }; + "bytes", + "kB", + "MB", + "GB", + "TB" + }; + + private static readonly Dictionary<string, string> UnifiedExtensions = new Dictionary<string, string> + { + ["jpeg"] = "jpg" + }; - private static readonly Dictionary<string, string> UnifiedExtensions = new Dictionary<string, string> + public static string FileType(this string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) { - ["jpeg"] = "jpg" - }; + return "blob"; + } - public static string FileType(this string fileName) + try { - if (string.IsNullOrWhiteSpace(fileName)) - { - return "blob"; - } + var fileInfo = new FileInfo(fileName); + var fileType = fileInfo.Extension[1..].ToLowerInvariant(); - try + if (UnifiedExtensions.TryGetValue(fileType, out var unified)) { - var fileInfo = new FileInfo(fileName); - var fileType = fileInfo.Extension[1..].ToLowerInvariant(); - - if (UnifiedExtensions.TryGetValue(fileType, out var unified)) - { - return unified; - } - else - { - return fileType; - } + return unified; } - catch + else { - return "blob"; + return fileType; } } - - public static string ToReadableSize(this int value) + catch { - return ToReadableSize((long)value); + return "blob"; } + } - public static string ToReadableSize(this long value) - { - if (value < 0) - { - return string.Empty; - } + public static string ToReadableSize(this int value) + { + return ToReadableSize((long)value); + } - var d = (double)value; - var u = 0; + public static string ToReadableSize(this long value) + { + if (value < 0) + { + return string.Empty; + } - const int multiplier = 1024; + var d = (double)value; + var u = 0; - while ((d >= multiplier || -d >= multiplier) && u < Extensions.Length - 1) - { - d /= multiplier; - u++; - } + const int multiplier = 1024; - if (u >= Extensions.Length - 1) - { - u = Extensions.Length - 1; - } + while ((d >= multiplier || -d >= multiplier) && u < Extensions.Length - 1) + { + d /= multiplier; + u++; + } - return $"{Math.Round(d, 1).ToString(CultureInfo.InvariantCulture)} {Extensions[u]}"; + if (u >= Extensions.Length - 1) + { + u = Extensions.Length - 1; } + + return $"{Math.Round(d, 1).ToString(CultureInfo.InvariantCulture)} {Extensions[u]}"; } } diff --git a/backend/src/Squidex.Infrastructure/GravatarHelper.cs b/backend/src/Squidex.Infrastructure/GravatarHelper.cs index eba120a670..a9e11cb828 100644 --- a/backend/src/Squidex.Infrastructure/GravatarHelper.cs +++ b/backend/src/Squidex.Infrastructure/GravatarHelper.cs @@ -9,40 +9,39 @@ using System.Security.Cryptography; using System.Text; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class GravatarHelper { - public static class GravatarHelper + public static string CreatePictureUrl(string email) { - public static string CreatePictureUrl(string email) - { - var gravatarUrl = $"https://www.gravatar.com/avatar/{Hash(email)}"; + var gravatarUrl = $"https://www.gravatar.com/avatar/{Hash(email)}"; - return gravatarUrl; - } + return gravatarUrl; + } - public static string CreateProfileUrl(string email) - { - var gravatarUrl = $"https://www.gravatar.com/{Hash(email)}"; + public static string CreateProfileUrl(string email) + { + var gravatarUrl = $"https://www.gravatar.com/{Hash(email)}"; - return gravatarUrl; - } + return gravatarUrl; + } - private static string Hash(string email) + private static string Hash(string email) + { + using (var md5 = MD5.Create()) { - using (var md5 = MD5.Create()) - { - var normalizedEmail = email.ToLowerInvariant().Trim(); + var normalizedEmail = email.ToLowerInvariant().Trim(); - var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(normalizedEmail)); - var hashBuilder = new StringBuilder(); + var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(normalizedEmail)); + var hashBuilder = new StringBuilder(); - for (var i = 0; i < hashBytes.Length; i++) - { - hashBuilder.Append(hashBytes[i].ToString("x2", CultureInfo.InvariantCulture)); - } - - return hashBuilder.ToString(); + for (var i = 0; i < hashBytes.Length; i++) + { + hashBuilder.Append(hashBytes[i].ToString("x2", CultureInfo.InvariantCulture)); } + + return hashBuilder.ToString(); } } } diff --git a/backend/src/Squidex.Infrastructure/Guard.cs b/backend/src/Squidex.Infrastructure/Guard.cs index 84e9501e0a..d6e3d14720 100644 --- a/backend/src/Squidex.Infrastructure/Guard.cs +++ b/backend/src/Squidex.Infrastructure/Guard.cs @@ -10,298 +10,297 @@ using Squidex.Infrastructure.Validation; using Squidex.Text; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class Guard { - public static class Guard + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ValidNumber(float target, + [CallerArgumentExpression("target")] string? parameterName = null) { - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float ValidNumber(float target, - [CallerArgumentExpression("target")] string? parameterName = null) + if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target)) { - if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target)) - { - ThrowHelper.ArgumentException("Value must be a valid number.", parameterName); - return default!; - } - - return target; + ThrowHelper.ArgumentException("Value must be a valid number.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double ValidNumber(double target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target)) - { - ThrowHelper.ArgumentException("Value must be a valid number.", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double ValidNumber(double target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target)) + { + ThrowHelper.ArgumentException("Value must be a valid number.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ValidSlug(string? target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - NotNullOrEmpty(target, parameterName); + return target; + } - if (!target!.IsSlug()) - { - ThrowHelper.ArgumentException("Target is not a valid slug.", parameterName); - return default!; - } + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ValidSlug(string? target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); - return target!; + if (!target!.IsSlug()) + { + ThrowHelper.ArgumentException("Target is not a valid slug.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ValidPropertyName(string? target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - NotNullOrEmpty(target, parameterName); + return target!; + } - if (!target!.IsPropertyName()) - { - ThrowHelper.ArgumentException("Target is not a valid property name.", parameterName); - return default!; - } + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ValidPropertyName(string? target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); - return target!; + if (!target!.IsPropertyName()) + { + ThrowHelper.ArgumentException("Target is not a valid property name.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static object? HasType<T>(object? target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - if (target != null && target.GetType() != typeof(T)) - { - ThrowHelper.ArgumentException($"The parameter must be of type {typeof(T)}", parameterName); - return default!; - } + return target!; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object? HasType<T>(object? target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target != null && target.GetType() != typeof(T)) + { + ThrowHelper.ArgumentException($"The parameter must be of type {typeof(T)}", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static object? HasType(object? target, Type? expectedType, - [CallerArgumentExpression("target")] string? parameterName = null) - { - if (target != null && expectedType != null && target.GetType() != expectedType) - { - ThrowHelper.ArgumentException($"The parameter must be of type {expectedType}", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object? HasType(object? target, Type? expectedType, + [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target != null && expectedType != null && target.GetType() != expectedType) + { + ThrowHelper.ArgumentException($"The parameter must be of type {expectedType}", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TValue Between<TValue>(TValue target, TValue lower, TValue upper, - [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable - { - if (!target.IsBetween(lower, upper)) - { - ThrowHelper.ArgumentException($"Value must be between {lower} and {upper}", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue Between<TValue>(TValue target, TValue lower, TValue upper, + [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (!target.IsBetween(lower, upper)) + { + ThrowHelper.ArgumentException($"Value must be between {lower} and {upper}", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TEnum Enum<TEnum>(TEnum target, - [CallerArgumentExpression("target")] string? parameterName = null) where TEnum : struct - { - if (!target.IsEnumValue()) - { - ThrowHelper.ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TEnum Enum<TEnum>(TEnum target, + [CallerArgumentExpression("target")] string? parameterName = null) where TEnum : struct + { + if (!target.IsEnumValue()) + { + ThrowHelper.ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TValue GreaterThan<TValue>(TValue target, TValue lower, - [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable - { - if (target.CompareTo(lower) <= 0) - { - ThrowHelper.ArgumentException($"Value must be greater than {lower}", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue GreaterThan<TValue>(TValue target, TValue lower, + [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(lower) <= 0) + { + ThrowHelper.ArgumentException($"Value must be greater than {lower}", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TValue GreaterEquals<TValue>(TValue target, TValue lower, - [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable - { - if (target.CompareTo(lower) < 0) - { - ThrowHelper.ArgumentException($"Value must be greater or equal to {lower}", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue GreaterEquals<TValue>(TValue target, TValue lower, + [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(lower) < 0) + { + ThrowHelper.ArgumentException($"Value must be greater or equal to {lower}", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TValue LessThan<TValue>(TValue target, TValue upper, - [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable - { - if (target.CompareTo(upper) >= 0) - { - ThrowHelper.ArgumentException($"Value must be less than {upper}", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue LessThan<TValue>(TValue target, TValue upper, + [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(upper) >= 0) + { + ThrowHelper.ArgumentException($"Value must be less than {upper}", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TValue LessEquals<TValue>(TValue target, TValue upper, - [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable - { - if (target.CompareTo(upper) > 0) - { - ThrowHelper.ArgumentException($"Value must be less or equal to {upper}", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue LessEquals<TValue>(TValue target, TValue upper, + [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable + { + if (target.CompareTo(upper) > 0) + { + ThrowHelper.ArgumentException($"Value must be less or equal to {upper}", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IReadOnlyCollection<TType> NotEmpty<TType>(IReadOnlyCollection<TType>? target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - NotNull(target, parameterName); + return target; + } - if (target is { Count: 0 }) - { - ThrowHelper.ArgumentException("Collection does not contain an item.", parameterName); - return default!; - } + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IReadOnlyCollection<TType> NotEmpty<TType>(IReadOnlyCollection<TType>? target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNull(target, parameterName); - return target!; + if (target is { Count: 0 }) + { + ThrowHelper.ArgumentException("Collection does not contain an item.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid NotEmpty(Guid target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - if (target == Guid.Empty) - { - ThrowHelper.ArgumentException("Value cannot be empty.", parameterName); - return default!; - } + return target!; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid NotEmpty(Guid target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target == Guid.Empty) + { + ThrowHelper.ArgumentException("Value cannot be empty.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DomainId NotEmpty(DomainId target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - if (target == DomainId.Empty) - { - ThrowHelper.ArgumentException("Value cannot be empty.", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DomainId NotEmpty(DomainId target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target == DomainId.Empty) + { + ThrowHelper.ArgumentException("Value cannot be empty.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TValue NotNull<TValue>(TValue? target, - [CallerArgumentExpression("target")] string? parameterName = null) where TValue : class - { - if (target == null) - { - ThrowHelper.ArgumentNullException(parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue NotNull<TValue>(TValue? target, + [CallerArgumentExpression("target")] string? parameterName = null) where TValue : class + { + if (target == null) + { + ThrowHelper.ArgumentNullException(parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static object? NotNull(object? target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - if (target == null) - { - ThrowHelper.ArgumentNullException(parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object? NotNull(object? target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + if (target == null) + { + ThrowHelper.ArgumentNullException(parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TValue NotDefault<TValue>(TValue target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - if (Equals(target, default(TValue)!)) - { - ThrowHelper.ArgumentException("Value cannot be an the default value.", parameterName); - return default!; - } + return target; + } - return target; + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue NotDefault<TValue>(TValue target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + if (Equals(target, default(TValue)!)) + { + ThrowHelper.ArgumentException("Value cannot be an the default value.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string NotNullOrEmpty(string? target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - NotNull(target, parameterName); + return target; + } - if (string.IsNullOrWhiteSpace(target)) - { - ThrowHelper.ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName); - return default!; - } + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string NotNullOrEmpty(string? target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNull(target, parameterName); - return target; + if (string.IsNullOrWhiteSpace(target)) + { + ThrowHelper.ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName); + return default!; } - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ValidFileName(string? target, - [CallerArgumentExpression("target")] string? parameterName = null) - { - NotNullOrEmpty(target, parameterName); + return target; + } - if (target != null && target.Intersect(Path.GetInvalidFileNameChars()).Any()) - { - ThrowHelper.ArgumentException("Value contains an invalid character.", parameterName); - return default!; - } + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ValidFileName(string? target, + [CallerArgumentExpression("target")] string? parameterName = null) + { + NotNullOrEmpty(target, parameterName); - return target!; + if (target != null && target.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + ThrowHelper.ArgumentException("Value contains an invalid character.", parameterName); + return default!; } + + return target!; } } diff --git a/backend/src/Squidex.Infrastructure/HashSet.cs b/backend/src/Squidex.Infrastructure/HashSet.cs index 1be05f86fa..75d1691329 100644 --- a/backend/src/Squidex.Infrastructure/HashSet.cs +++ b/backend/src/Squidex.Infrastructure/HashSet.cs @@ -5,23 +5,22 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class HashSet { - public static class HashSet + public static HashSet<T> Of<T>(params T[] items) { - public static HashSet<T> Of<T>(params T[] items) - { - return new HashSet<T>(items); - } + return new HashSet<T>(items); + } - public static HashSet<T> Of<T>(T item1) - { - return new HashSet<T> { item1 }; - } + public static HashSet<T> Of<T>(T item1) + { + return new HashSet<T> { item1 }; + } - public static HashSet<T> Of<T>(T item1, T item2) - { - return new HashSet<T> { item1, item2 }; - } + public static HashSet<T> Of<T>(T item1, T item2) + { + return new HashSet<T> { item1, item2 }; } } diff --git a/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs b/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs index 16520e19b0..11c88ed91c 100644 --- a/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs +++ b/backend/src/Squidex.Infrastructure/Http/DumpFormatter.cs @@ -10,97 +10,96 @@ using System.Net.Http.Headers; using System.Text; -namespace Squidex.Infrastructure.Http +namespace Squidex.Infrastructure.Http; + +public static class DumpFormatter { - public static class DumpFormatter + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? responseBody) { - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? responseBody) - { - return BuildDump(request, response, null, responseBody, TimeSpan.Zero); - } + return BuildDump(request, response, null, responseBody, TimeSpan.Zero); + } - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody) - { - return BuildDump(request, response, requestBody, responseBody, TimeSpan.Zero); - } + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody) + { + return BuildDump(request, response, requestBody, responseBody, TimeSpan.Zero); + } - public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody, TimeSpan elapsed, bool isTimeout = false) - { - var writer = new StringBuilder(); + public static string BuildDump(HttpRequestMessage request, HttpResponseMessage? response, string? requestBody, string? responseBody, TimeSpan elapsed, bool isTimeout = false) + { + var writer = new StringBuilder(); - writer.AppendLine("Request:"); - writer.AppendRequest(request, requestBody); + writer.AppendLine("Request:"); + writer.AppendRequest(request, requestBody); - writer.AppendLine(); - writer.AppendLine(); + writer.AppendLine(); + writer.AppendLine(); + + writer.AppendLine("Response:"); + writer.AppendResponse(response, responseBody, elapsed, isTimeout); + + return writer.ToString(); + } + + private static void AppendRequest(this StringBuilder writer, HttpRequestMessage request, string? requestBody) + { + var method = request.Method.ToString().ToUpperInvariant(); + + writer.AppendLine(CultureInfo.InvariantCulture, $"{method}: {request.RequestUri} HTTP/{request.Version}"); - writer.AppendLine("Response:"); - writer.AppendResponse(response, responseBody, elapsed, isTimeout); + writer.AppendHeaders(request.Headers); + writer.AppendHeaders(request.Content?.Headers); - return writer.ToString(); + if (!string.IsNullOrWhiteSpace(requestBody)) + { + writer.AppendLine(); + writer.AppendLine(requestBody); } + } - private static void AppendRequest(this StringBuilder writer, HttpRequestMessage request, string? requestBody) + private static void AppendResponse(this StringBuilder writer, HttpResponseMessage? response, string? responseBody, TimeSpan elapsed, bool isTimeout) + { + if (response != null) { - var method = request.Method.ToString().ToUpperInvariant(); + var responseCode = (int)response.StatusCode; + var responseText = Enum.GetName(typeof(HttpStatusCode), response.StatusCode); - writer.AppendLine(CultureInfo.InvariantCulture, $"{method}: {request.RequestUri} HTTP/{request.Version}"); + writer.AppendLine(CultureInfo.InvariantCulture, $"HTTP/{response.Version} {responseCode} {responseText}"); - writer.AppendHeaders(request.Headers); - writer.AppendHeaders(request.Content?.Headers); + writer.AppendHeaders(response.Headers); + writer.AppendHeaders(response.Content?.Headers); + } - if (!string.IsNullOrWhiteSpace(requestBody)) - { - writer.AppendLine(); - writer.AppendLine(requestBody); - } + if (!string.IsNullOrWhiteSpace(responseBody)) + { + writer.AppendLine(); + writer.AppendLine(responseBody); } - private static void AppendResponse(this StringBuilder writer, HttpResponseMessage? response, string? responseBody, TimeSpan elapsed, bool isTimeout) + if (response != null && elapsed != TimeSpan.Zero) { - if (response != null) - { - var responseCode = (int)response.StatusCode; - var responseText = Enum.GetName(typeof(HttpStatusCode), response.StatusCode); - - writer.AppendLine(CultureInfo.InvariantCulture, $"HTTP/{response.Version} {responseCode} {responseText}"); - - writer.AppendHeaders(response.Headers); - writer.AppendHeaders(response.Content?.Headers); - } - - if (!string.IsNullOrWhiteSpace(responseBody)) - { - writer.AppendLine(); - writer.AppendLine(responseBody); - } - - if (response != null && elapsed != TimeSpan.Zero) - { - writer.AppendLine(); - writer.AppendLine(CultureInfo.InvariantCulture, $"Elapsed: {elapsed}"); - } - - if (isTimeout) - { - writer.AppendLine(CultureInfo.InvariantCulture, $"Timeout after {elapsed}"); - } + writer.AppendLine(); + writer.AppendLine(CultureInfo.InvariantCulture, $"Elapsed: {elapsed}"); } - private static void AppendHeaders(this StringBuilder writer, HttpHeaders? headers) + if (isTimeout) { - if (headers == null) - { - return; - } - - foreach (var (key, value) in headers) - { - writer.Append(key); - writer.Append(": "); - writer.Append(string.Join("; ", value)); - writer.AppendLine(); - } + writer.AppendLine(CultureInfo.InvariantCulture, $"Timeout after {elapsed}"); + } + } + + private static void AppendHeaders(this StringBuilder writer, HttpHeaders? headers) + { + if (headers == null) + { + return; + } + + foreach (var (key, value) in headers) + { + writer.Append(key); + writer.Append(": "); + writer.Append(string.Join("; ", value)); + writer.AppendLine(); } } } diff --git a/backend/src/Squidex.Infrastructure/IResultList.cs b/backend/src/Squidex.Infrastructure/IResultList.cs index 05dd7dae3f..f892b10b5a 100644 --- a/backend/src/Squidex.Infrastructure/IResultList.cs +++ b/backend/src/Squidex.Infrastructure/IResultList.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public interface IResultList<out T> : IReadOnlyList<T> { - public interface IResultList<out T> : IReadOnlyList<T> - { - long Total { get; } - } + long Total { get; } } diff --git a/backend/src/Squidex.Infrastructure/ISurrogate.cs b/backend/src/Squidex.Infrastructure/ISurrogate.cs index 1dce88ef3c..7f49f5084d 100644 --- a/backend/src/Squidex.Infrastructure/ISurrogate.cs +++ b/backend/src/Squidex.Infrastructure/ISurrogate.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public interface ISurrogate<T> { - public interface ISurrogate<T> - { - void FromSource(T source); + void FromSource(T source); - T ToSource(); - } + T ToSource(); } diff --git a/backend/src/Squidex.Infrastructure/ITelemetryConfigurator.cs b/backend/src/Squidex.Infrastructure/ITelemetryConfigurator.cs index 13e6e6a2f7..7a8cfe005f 100644 --- a/backend/src/Squidex.Infrastructure/ITelemetryConfigurator.cs +++ b/backend/src/Squidex.Infrastructure/ITelemetryConfigurator.cs @@ -7,12 +7,11 @@ using OpenTelemetry.Trace; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public interface ITelemetryConfigurator { - public interface ITelemetryConfigurator + void Configure(TracerProviderBuilder builder) { - void Configure(TracerProviderBuilder builder) - { - } } } diff --git a/backend/src/Squidex.Infrastructure/IWithId.cs b/backend/src/Squidex.Infrastructure/IWithId.cs index 1cc689437f..ced5759f24 100644 --- a/backend/src/Squidex.Infrastructure/IWithId.cs +++ b/backend/src/Squidex.Infrastructure/IWithId.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public interface IWithId<out T> { - public interface IWithId<out T> - { - T Id { get; } - } + T Id { get; } } diff --git a/backend/src/Squidex.Infrastructure/InstantExtensions.cs b/backend/src/Squidex.Infrastructure/InstantExtensions.cs index 087af3ffc4..84de30ceb4 100644 --- a/backend/src/Squidex.Infrastructure/InstantExtensions.cs +++ b/backend/src/Squidex.Infrastructure/InstantExtensions.cs @@ -7,18 +7,17 @@ using NodaTime; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class InstantExtensions { - public static class InstantExtensions + public static Instant WithoutMs(this Instant value) { - public static Instant WithoutMs(this Instant value) - { - return Instant.FromUnixTimeSeconds(value.ToUnixTimeSeconds()); - } + return Instant.FromUnixTimeSeconds(value.ToUnixTimeSeconds()); + } - public static Instant WithoutNs(this Instant value) - { - return Instant.FromUnixTimeMilliseconds(value.ToUnixTimeMilliseconds()); - } + public static Instant WithoutNs(this Instant value) + { + return Instant.FromUnixTimeMilliseconds(value.ToUnixTimeMilliseconds()); } } diff --git a/backend/src/Squidex.Infrastructure/Json/ClaimsPrincipalSurrogate.cs b/backend/src/Squidex.Infrastructure/Json/ClaimsPrincipalSurrogate.cs index 94652c7f17..d34894a96f 100644 --- a/backend/src/Squidex.Infrastructure/Json/ClaimsPrincipalSurrogate.cs +++ b/backend/src/Squidex.Infrastructure/Json/ClaimsPrincipalSurrogate.cs @@ -9,70 +9,69 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.Json +namespace Squidex.Infrastructure.Json; + +public sealed class ClaimsPrincipalSurrogate : List<ClaimsIdentitySurrogate>, ISurrogate<ClaimsPrincipal> { - public sealed class ClaimsPrincipalSurrogate : List<ClaimsIdentitySurrogate>, ISurrogate<ClaimsPrincipal> + public void FromSource(ClaimsPrincipal source) { - public void FromSource(ClaimsPrincipal source) + foreach (var identity in source.Identities) { - foreach (var identity in source.Identities) - { - var surrogate = new ClaimsIdentitySurrogate(); - - surrogate.FromSource(identity); + var surrogate = new ClaimsIdentitySurrogate(); - Add(surrogate); - } - } + surrogate.FromSource(identity); - public ClaimsPrincipal ToSource() - { - return new ClaimsPrincipal(this.Select(x => x.ToSource())); + Add(surrogate); } } - public sealed class ClaimsIdentitySurrogate : ISurrogate<ClaimsIdentity> + public ClaimsPrincipal ToSource() { - public string? AuthenticationType { get; set; } + return new ClaimsPrincipal(this.Select(x => x.ToSource())); + } +} - public ClaimSurrogate[] Claims { get; set; } +public sealed class ClaimsIdentitySurrogate : ISurrogate<ClaimsIdentity> +{ + public string? AuthenticationType { get; set; } - public void FromSource(ClaimsIdentity source) - { - AuthenticationType = source.AuthenticationType; + public ClaimSurrogate[] Claims { get; set; } - Claims = source.Claims.Select(claim => - { - var surrogate = new ClaimSurrogate(); + public void FromSource(ClaimsIdentity source) + { + AuthenticationType = source.AuthenticationType; - surrogate.FromSource(claim); + Claims = source.Claims.Select(claim => + { + var surrogate = new ClaimSurrogate(); - return surrogate; - }).ToArray(); - } + surrogate.FromSource(claim); - public ClaimsIdentity ToSource() - { - return new ClaimsIdentity(Claims.Select(x => x.ToSource()), AuthenticationType); - } + return surrogate; + }).ToArray(); } - public sealed class ClaimSurrogate : ISurrogate<Claim> + public ClaimsIdentity ToSource() { - public string Type { get; set; } + return new ClaimsIdentity(Claims.Select(x => x.ToSource()), AuthenticationType); + } +} - public string Value { get; set; } +public sealed class ClaimSurrogate : ISurrogate<Claim> +{ + public string Type { get; set; } - public void FromSource(Claim source) - { - Type = source.Type; + public string Value { get; set; } - Value = source.Value; - } + public void FromSource(Claim source) + { + Type = source.Type; - public Claim ToSource() - { - return new Claim(Type, Value); - } + Value = source.Value; + } + + public Claim ToSource() + { + return new Claim(Type, Value); } } diff --git a/backend/src/Squidex.Infrastructure/Json/GeoJson.cs b/backend/src/Squidex.Infrastructure/Json/GeoJson.cs index d2f3bb2259..7bf97229cb 100644 --- a/backend/src/Squidex.Infrastructure/Json/GeoJson.cs +++ b/backend/src/Squidex.Infrastructure/Json/GeoJson.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Json +namespace Squidex.Infrastructure.Json; + +public static class GeoJson { - public static class GeoJson - { - public const string Format = "geo-json"; - } + public const string Format = "geo-json"; } diff --git a/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs index 56c9585a33..1816c4fb99 100644 --- a/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs +++ b/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Json +namespace Squidex.Infrastructure.Json; + +public interface IJsonSerializer { - public interface IJsonSerializer - { - string Serialize<T>(T value, bool indented = false); + string Serialize<T>(T value, bool indented = false); - string Serialize(object? value, Type type, bool indented = false); + string Serialize(object? value, Type type, bool indented = false); - void Serialize<T>(T value, Stream stream, bool leaveOpen = false); + void Serialize<T>(T value, Stream stream, bool leaveOpen = false); - void Serialize(object? value, Type type, Stream stream, bool leaveOpen = false); + void Serialize(object? value, Type type, Stream stream, bool leaveOpen = false); - T Deserialize<T>(string value, Type? actualType = null); + T Deserialize<T>(string value, Type? actualType = null); - T Deserialize<T>(Stream stream, Type? actualType = null, bool leaveOpen = false); - } + T Deserialize<T>(Stream stream, Type? actualType = null, bool leaveOpen = false); } diff --git a/backend/src/Squidex.Infrastructure/Json/ISupportedTypes.cs b/backend/src/Squidex.Infrastructure/Json/ISupportedTypes.cs index 5c4a4cf640..29f1008f89 100644 --- a/backend/src/Squidex.Infrastructure/Json/ISupportedTypes.cs +++ b/backend/src/Squidex.Infrastructure/Json/ISupportedTypes.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Json +namespace Squidex.Infrastructure.Json; + +public interface ISupportedTypes { - public interface ISupportedTypes - { - IEnumerable<Type> SupportedTypes { get; } - } + IEnumerable<Type> SupportedTypes { get; } } diff --git a/backend/src/Squidex.Infrastructure/Json/JsonException.cs b/backend/src/Squidex.Infrastructure/Json/JsonException.cs index 22a5ef10fa..b7bf1a73f3 100644 --- a/backend/src/Squidex.Infrastructure/Json/JsonException.cs +++ b/backend/src/Squidex.Infrastructure/Json/JsonException.cs @@ -7,28 +7,27 @@ using System.Runtime.Serialization; -namespace Squidex.Infrastructure.Json +namespace Squidex.Infrastructure.Json; + +[Serializable] +public class JsonException : Exception { - [Serializable] - public class JsonException : Exception + public JsonException() { - public JsonException() - { - } + } - public JsonException(string? message) - : base(message) - { - } + public JsonException(string? message) + : base(message) + { + } - public JsonException(string? message, Exception? inner) - : base(message, inner) - { - } + public JsonException(string? message, Exception? inner) + : base(message, inner) + { + } - protected JsonException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected JsonException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs index 8aa4455d23..aeadae133d 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs @@ -8,76 +8,75 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; -namespace Squidex.Infrastructure.Json.Objects +namespace Squidex.Infrastructure.Json.Objects; + +public sealed class JsonArray : List<JsonValue>, IEquatable<JsonArray> { - public sealed class JsonArray : List<JsonValue>, IEquatable<JsonArray> + public JsonArray() { - public JsonArray() - { - } + } - public JsonArray(int capacity) - : base(capacity) - { - } + public JsonArray(int capacity) + : base(capacity) + { + } - public JsonArray(JsonArray source) - : base(source) - { - } + public JsonArray(JsonArray source) + : base(source) + { + } - public JsonArray(IEnumerable<JsonValue>? source) + public JsonArray(IEnumerable<JsonValue>? source) + { + if (source != null) { - if (source != null) + foreach (var item in source) { - foreach (var item in source) - { - Add(item); - } + Add(item); } } + } - public new JsonArray Add(JsonValue value) - { - base.Add(value); - - return this; - } + public new JsonArray Add(JsonValue value) + { + base.Add(value); - public override bool Equals(object? obj) - { - return Equals(obj as JsonArray); - } + return this; + } - public bool Equals(JsonArray? array) - { - return this.EqualsList(array); - } + public override bool Equals(object? obj) + { + return Equals(obj as JsonArray); + } - public override int GetHashCode() - { - return this.SequentialHashCode(); - } + public bool Equals(JsonArray? array) + { + return this.EqualsList(array); + } - public override string ToString() - { - return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]"; - } + public override int GetHashCode() + { + return this.SequentialHashCode(); + } - public bool TryGetValue(string pathSegment, [MaybeNullWhen(false)] out JsonValue result) - { - Guard.NotNull(pathSegment); + public override string ToString() + { + return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]"; + } - result = default; + public bool TryGetValue(string pathSegment, [MaybeNullWhen(false)] out JsonValue result) + { + Guard.NotNull(pathSegment); - if (pathSegment != null && int.TryParse(pathSegment, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) && index >= 0 && index < Count) - { - result = this[index]; + result = default; - return true; - } + if (pathSegment != null && int.TryParse(pathSegment, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) && index >= 0 && index < Count) + { + result = this[index]; - return false; + return true; } + + return false; } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs index 18f1c0fa68..fe9882473f 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs @@ -5,49 +5,48 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Json.Objects +namespace Squidex.Infrastructure.Json.Objects; + +public class JsonObject : Dictionary<string, JsonValue>, IEquatable<JsonObject> { - public class JsonObject : Dictionary<string, JsonValue>, IEquatable<JsonObject> - { - public JsonObject() - { - } - - public JsonObject(int capacity) - : base(capacity) - { - } - - public JsonObject(JsonObject source) - : base(source) - { - } - - public override bool Equals(object? obj) - { - return Equals(obj as JsonObject); - } - - public bool Equals(JsonObject? other) - { - return this.EqualsDictionary(other); - } - - public override int GetHashCode() - { - return this.DictionaryHashCode(); - } - - public override string ToString() - { - return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}"; - } - - public new JsonObject Add(string key, JsonValue value) - { - this[key] = value; - - return this; - } + public JsonObject() + { + } + + public JsonObject(int capacity) + : base(capacity) + { + } + + public JsonObject(JsonObject source) + : base(source) + { + } + + public override bool Equals(object? obj) + { + return Equals(obj as JsonObject); + } + + public bool Equals(JsonObject? other) + { + return this.EqualsDictionary(other); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); + } + + public override string ToString() + { + return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}"; + } + + public new JsonObject Add(string key, JsonValue value) + { + this[key] = value; + + return this; } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs index 70f6fb19ac..73c29f326d 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs @@ -9,465 +9,464 @@ using System.Globalization; using NodaTime; -namespace Squidex.Infrastructure.Json.Objects +namespace Squidex.Infrastructure.Json.Objects; + +public readonly struct JsonValue : IEquatable<JsonValue> { - public readonly struct JsonValue : IEquatable<JsonValue> - { - private static readonly char[] PathSeparators = { '.', '[', ']' }; + private static readonly char[] PathSeparators = { '.', '[', ']' }; - public static readonly JsonValue Null; - public static readonly JsonValue True = new JsonValue(true); - public static readonly JsonValue False = new JsonValue(false); - public static readonly JsonValue Zero = new JsonValue(0); + public static readonly JsonValue Null; + public static readonly JsonValue True = new JsonValue(true); + public static readonly JsonValue False = new JsonValue(false); + public static readonly JsonValue Zero = new JsonValue(0); - public readonly object? Value; + public readonly object? Value; - public JsonValueType Type + public JsonValueType Type + { + get { - get + switch (Value) { - switch (Value) - { - case null: - return JsonValueType.Null; - case bool: - return JsonValueType.Boolean; - case double: - return JsonValueType.Number; - case string: - return JsonValueType.String; - case JsonArray: - return JsonValueType.Array; - case JsonObject: - return JsonValueType.Object; - default: - ThrowInvalidType(); - return default!; - } + case null: + return JsonValueType.Null; + case bool: + return JsonValueType.Boolean; + case double: + return JsonValueType.Number; + case string: + return JsonValueType.String; + case JsonArray: + return JsonValueType.Array; + case JsonObject: + return JsonValueType.Object; + default: + ThrowInvalidType(); + return default!; } } + } - public bool AsBoolean + public bool AsBoolean + { + get { - get + if (Value is bool typed) { - if (Value is bool typed) - { - return typed; - } - - ThrowInvalidType(); - return default!; + return typed; } + + ThrowInvalidType(); + return default!; } + } - public double AsNumber + public double AsNumber + { + get { - get + if (Value is double typed) { - if (Value is double typed) - { - return typed; - } - - ThrowInvalidType(); - return default!; + return typed; } + + ThrowInvalidType(); + return default!; } + } - public string AsString + public string AsString + { + get { - get + if (Value is string typed) { - if (Value is string typed) - { - return typed; - } - - ThrowInvalidType(); - return default!; + return typed; } + + ThrowInvalidType(); + return default!; } + } - public JsonArray AsArray + public JsonArray AsArray + { + get { - get + if (Value is JsonArray typed) { - if (Value is JsonArray typed) - { - return typed; - } - - ThrowInvalidType(); - return default!; + return typed; } + + ThrowInvalidType(); + return default!; } + } - public JsonObject AsObject + public JsonObject AsObject + { + get { - get + if (Value is JsonObject typed) { - if (Value is JsonObject typed) - { - return typed; - } - - ThrowInvalidType(); - return default!; + return typed; } - } - - public JsonValue(double value) - { - Guard.ValidNumber(value); - Value = value; + ThrowInvalidType(); + return default!; } + } - public JsonValue(bool value) - { - Value = value; - } + public JsonValue(double value) + { + Guard.ValidNumber(value); - public JsonValue(string? value) - { - Value = value; - } + Value = value; + } - public JsonValue(JsonArray? value) - { - Value = value; - } + public JsonValue(bool value) + { + Value = value; + } - public JsonValue(JsonObject? value) - { - Value = value; - } + public JsonValue(string? value) + { + Value = value; + } - public static JsonValue Create<T>(IReadOnlyDictionary<string, T>? values) - { - var source = new JsonObject(values?.Count ?? 0); + public JsonValue(JsonArray? value) + { + Value = value; + } - if (values != null) - { - foreach (var (key, value) in values) - { - source[key] = Create(value); - } - } + public JsonValue(JsonObject? value) + { + Value = value; + } - return source; - } + public static JsonValue Create<T>(IReadOnlyDictionary<string, T>? values) + { + var source = new JsonObject(values?.Count ?? 0); - public static JsonValue Create(object? value) + if (values != null) { - if (value == null) + foreach (var (key, value) in values) { - return default; + source[key] = Create(value); } + } - if (value is JsonValue v) - { - return v; - } + return source; + } - switch (value) - { - case Guid typed: - return Create(typed.ToString()); - case DomainId typed: - return Create(typed); - case Instant typed: - return Create(typed); - case bool typed: - return Create(typed); - case float typed: - return Create(typed); - case double typed: - return Create(typed); - case int typed: - return Create(typed); - case long typed: - return Create(typed); - case string typed: - return Create(typed); - case object[] typed: - return Array(typed); - case JsonArray typed: - return typed; - case JsonObject typed: - return typed; - case IReadOnlyDictionary<string, object?> typed: - return Create(typed); - } + public static JsonValue Create(object? value) + { + if (value == null) + { + return default; + } + + if (value is JsonValue v) + { + return v; + } + + switch (value) + { + case Guid typed: + return Create(typed.ToString()); + case DomainId typed: + return Create(typed); + case Instant typed: + return Create(typed); + case bool typed: + return Create(typed); + case float typed: + return Create(typed); + case double typed: + return Create(typed); + case int typed: + return Create(typed); + case long typed: + return Create(typed); + case string typed: + return Create(typed); + case object[] typed: + return Array(typed); + case JsonArray typed: + return typed; + case JsonObject typed: + return typed; + case IReadOnlyDictionary<string, object?> typed: + return Create(typed); + } + + ThrowArgumentException(nameof(value)); + return default!; + } - ThrowArgumentException(nameof(value)); - return default!; - } + public static JsonObject Object() + { + return new JsonObject(); + } - public static JsonObject Object() - { - return new JsonObject(); - } + public static JsonArray Array() + { + return new JsonArray(); + } - public static JsonArray Array() - { - return new JsonArray(); - } + public static JsonValue Array<T>(IEnumerable<T> values) + { + return new JsonArray(values?.OfType<object?>().Select(Create)); + } - public static JsonValue Array<T>(IEnumerable<T> values) - { - return new JsonArray(values?.OfType<object?>().Select(Create)); - } + public static JsonValue Array<T>(params T?[] values) + { + return new JsonArray(values?.OfType<object?>().Select(Create)); + } - public static JsonValue Array<T>(params T?[] values) - { - return new JsonArray(values?.OfType<object?>().Select(Create)); - } + public static JsonValue Create(DomainId value) + { + return new JsonValue(value.ToString()); + } - public static JsonValue Create(DomainId value) - { - return new JsonValue(value.ToString()); - } + public static JsonValue Create(Instant value) + { + return new JsonValue(value.ToString()); + } - public static JsonValue Create(Instant value) - { - return new JsonValue(value.ToString()); - } + public static JsonValue Create(double value) + { + return new JsonValue(value); + } - public static JsonValue Create(double value) - { - return new JsonValue(value); - } + public static JsonValue Create(bool value) + { + return new JsonValue(value); + } - public static JsonValue Create(bool value) - { - return new JsonValue(value); - } + public static JsonValue Create(string? value) + { + return new JsonValue(value); + } - public static JsonValue Create(string? value) - { - return new JsonValue(value); - } + public static JsonValue Create(JsonArray? array) + { + return new JsonValue(array); + } - public static JsonValue Create(JsonArray? array) - { - return new JsonValue(array); - } + public static JsonValue Create(JsonObject? @object) + { + return new JsonValue(@object); + } - public static JsonValue Create(JsonObject? @object) - { - return new JsonValue(@object); - } + public static implicit operator JsonValue(DomainId value) + { + return Create(value); + } - public static implicit operator JsonValue(DomainId value) - { - return Create(value); - } + public static implicit operator JsonValue(Instant value) + { + return Create(value); + } - public static implicit operator JsonValue(Instant value) - { - return Create(value); - } + public static implicit operator JsonValue(bool value) + { + return Create(value); + } - public static implicit operator JsonValue(bool value) - { - return Create(value); - } + public static implicit operator JsonValue(double value) + { + return Create(value); + } - public static implicit operator JsonValue(double value) - { - return Create(value); - } + public static implicit operator JsonValue(string? value) + { + return Create(value); + } - public static implicit operator JsonValue(string? value) - { - return Create(value); - } + public static implicit operator JsonValue(JsonArray? value) + { + return Create(value); + } - public static implicit operator JsonValue(JsonArray? value) - { - return Create(value); - } + public static implicit operator JsonValue(JsonObject? value) + { + return Create(value); + } - public static implicit operator JsonValue(JsonObject? value) - { - return Create(value); - } + public static bool operator ==(JsonValue left, JsonValue right) + { + return left.Equals(right); + } - public static bool operator ==(JsonValue left, JsonValue right) - { - return left.Equals(right); - } + public static bool operator !=(JsonValue left, JsonValue right) + { + return !(left == right); + } - public static bool operator !=(JsonValue left, JsonValue right) - { - return !(left == right); - } + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is JsonValue typed && Equals(typed); + } - public override bool Equals([NotNullWhen(true)] object? obj) - { - return obj is JsonValue typed && Equals(typed); - } + public bool Equals(JsonValue other) + { + return Equals(other.Value, Value); + } - public bool Equals(JsonValue other) - { - return Equals(other.Value, Value); - } + public override int GetHashCode() + { + return Value?.GetHashCode() ?? 0; + } - public override int GetHashCode() - { - return Value?.GetHashCode() ?? 0; + public override string ToString() + { + switch (Value) + { + case null: + return "null"; + case bool b: + return b ? "true" : "false"; + case double n: + return n.ToString(CultureInfo.InvariantCulture); + case string s: + return s; + case JsonArray a: + return a.ToString(); + case JsonObject o: + return o.ToString(); + default: + ThrowInvalidType(); + return default!; } + } - public override string ToString() - { - switch (Value) - { - case null: - return "null"; - case bool b: - return b ? "true" : "false"; - case double n: - return n.ToString(CultureInfo.InvariantCulture); - case string s: - return s; - case JsonArray a: - return a.ToString(); - case JsonObject o: - return o.ToString(); - default: - ThrowInvalidType(); - return default!; - } + public string ToJsonString() + { + switch (Value) + { + case null: + return "null"; + case bool b: + return b ? "true" : "false"; + case double n: + return n.ToString(CultureInfo.InvariantCulture); + case string s: + return $"\"{s}\""; + case JsonArray a: + return a.ToString(); + case JsonObject o: + return o.ToString(); + default: + ThrowInvalidType(); + return default!; } + } - public string ToJsonString() + public JsonValue Clone() + { + switch (Value) { - switch (Value) - { - case null: - return "null"; - case bool b: - return b ? "true" : "false"; - case double n: - return n.ToString(CultureInfo.InvariantCulture); - case string s: - return $"\"{s}\""; - case JsonArray a: - return a.ToString(); - case JsonObject o: - return o.ToString(); - default: - ThrowInvalidType(); - return default!; - } - } + case JsonArray a: + { + var result = new JsonArray(a.Count); - public JsonValue Clone() - { - switch (Value) - { - case JsonArray a: + foreach (var item in a) { - var result = new JsonArray(a.Count); - - foreach (var item in a) - { - result.Add(item.Clone()); - } - - return result; + result.Add(item.Clone()); } - case JsonObject o: - { - var result = new JsonObject(o.Count); + return result; + } - foreach (var (key, value) in o) - { - result.Add(key, value.Clone()); - } + case JsonObject o: + { + var result = new JsonObject(o.Count); - return result; + foreach (var (key, value) in o) + { + result.Add(key, value.Clone()); } - default: - return this; - } - } + return result; + } - public bool TryGetByPath(string? path, out JsonValue result) - { - return TryGetByPath(path?.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries), out result!); + default: + return this; } + } - public bool TryGetByPath(IEnumerable<string>? path, [MaybeNullWhen(false)] out JsonValue result) - { - result = this; - - if (path == null) - { - return false; - } - - var hasSegment = false; - - foreach (var pathSegment in path) - { - hasSegment = true; + public bool TryGetByPath(string? path, out JsonValue result) + { + return TryGetByPath(path?.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries), out result!); + } - if (!result.TryGetValue(pathSegment, out var found)) - { - result = default; - return false; - } - else - { - result = found; - } - } + public bool TryGetByPath(IEnumerable<string>? path, [MaybeNullWhen(false)] out JsonValue result) + { + result = this; - return hasSegment; + if (path == null) + { + return false; } - public bool TryGetValue(string pathSegment, out JsonValue result) + var hasSegment = false; + + foreach (var pathSegment in path) { - result = default!; + hasSegment = true; - if (pathSegment == null) + if (!result.TryGetValue(pathSegment, out var found)) { + result = default; return false; } - - switch (Value) + else { - case null: - return false; - case bool: - return false; - case double: - return false; - case string: - return false; - case JsonArray a: - return a.TryGetValue(pathSegment, out result); - case JsonObject o: - return o.TryGetValue(pathSegment, out result); - default: - ThrowInvalidType(); - return default!; + result = found; } } - private static void ThrowInvalidType() + return hasSegment; + } + + public bool TryGetValue(string pathSegment, out JsonValue result) + { + result = default!; + + if (pathSegment == null) { - ThrowHelper.InvalidOperationException("Invalid type."); + return false; } - private static void ThrowArgumentException(string parameterName) + switch (Value) { - ThrowHelper.ArgumentException("Invalid type.", parameterName); + case null: + return false; + case bool: + return false; + case double: + return false; + case string: + return false; + case JsonArray a: + return a.TryGetValue(pathSegment, out result); + case JsonObject o: + return o.TryGetValue(pathSegment, out result); + default: + ThrowInvalidType(); + return default!; } } + + private static void ThrowInvalidType() + { + ThrowHelper.InvalidOperationException("Invalid type."); + } + + private static void ThrowArgumentException(string parameterName) + { + ThrowHelper.ArgumentException("Invalid type.", parameterName); + } } diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs index 8b4a130223..de3be6143f 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs @@ -5,15 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Json.Objects +namespace Squidex.Infrastructure.Json.Objects; + +public enum JsonValueType { - public enum JsonValueType - { - Array, - Boolean, - Null, - Number, - Object, - String - } + Array, + Boolean, + Null, + Number, + Object, + String } diff --git a/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs index dedb1b5112..89fb9bd067 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs @@ -7,47 +7,46 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public sealed class InheritanceConverter<T> : InheritanceConverterBase<T> where T : notnull { - public sealed class InheritanceConverter<T> : InheritanceConverterBase<T> where T : notnull + private readonly TypeNameRegistry typeNameRegistry; + + public InheritanceConverter(TypeNameRegistry typeNameRegistry) + : base("$type") { - private readonly TypeNameRegistry typeNameRegistry; + this.typeNameRegistry = typeNameRegistry; + } - public InheritanceConverter(TypeNameRegistry typeNameRegistry) - : base("$type") + public override Type GetDiscriminatorType(string name, Type typeToConvert) + { + var typeInfo = typeNameRegistry.GetTypeOrNull(name); + + if (typeInfo == null) { - this.typeNameRegistry = typeNameRegistry; + typeInfo = Type.GetType(name); } - public override Type GetDiscriminatorType(string name, Type typeToConvert) + if (typeInfo == null) { - var typeInfo = typeNameRegistry.GetTypeOrNull(name); - - if (typeInfo == null) - { - typeInfo = Type.GetType(name); - } - - if (typeInfo == null) - { - ThrowHelper.JsonException($"Object has invalid discriminator '{name}'."); - return default!; - } - - return typeInfo; + ThrowHelper.JsonException($"Object has invalid discriminator '{name}'."); + return default!; } - public override string GetDiscriminatorValue(Type type) - { - var typeName = typeNameRegistry.GetNameOrNull(type); + return typeInfo; + } - if (typeName == null) - { - // Use the type name as a fallback. - typeName = type.AssemblyQualifiedName!; - } + public override string GetDiscriminatorValue(Type type) + { + var typeName = typeNameRegistry.GetNameOrNull(type); - return typeName; + if (typeName == null) + { + // Use the type name as a fallback. + typeName = type.AssemblyQualifiedName!; } + + return typeName; } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs b/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs index 9f7b6b8b39..53c7147b88 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs @@ -9,87 +9,86 @@ using System.Text.Json.Serialization; using Squidex.Infrastructure.ObjectPool; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public abstract class InheritanceConverterBase<T> : JsonConverter<T> where T : notnull { - public abstract class InheritanceConverterBase<T> : JsonConverter<T> where T : notnull + private readonly JsonEncodedText discriminatorProperty; + + public string DiscriminatorName { get; } + + protected InheritanceConverterBase(string discriminatorName) { - private readonly JsonEncodedText discriminatorProperty; + discriminatorProperty = JsonEncodedText.Encode(discriminatorName); + + DiscriminatorName = discriminatorName; + } + + public abstract Type GetDiscriminatorType(string name, Type typeToConvert); + + public abstract string GetDiscriminatorValue(Type type); - public string DiscriminatorName { get; } + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Creating a copy of the reader (The derived deserialisation has to be done from the start) + Utf8JsonReader typeReader = reader; - protected InheritanceConverterBase(string discriminatorName) + if (typeReader.TokenType != JsonTokenType.StartObject) { - discriminatorProperty = JsonEncodedText.Encode(discriminatorName); + throw new JsonException(); + } - DiscriminatorName = discriminatorName; + if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); } - public abstract Type GetDiscriminatorType(string name, Type typeToConvert); + var propertyName = typeReader.GetString(); - public abstract string GetDiscriminatorValue(Type type); + if (typeReader.Read() && typeReader.TokenType == JsonTokenType.String && propertyName == DiscriminatorName) + { + var type = GetDiscriminatorType(typeReader.GetString()!, typeToConvert); - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + return (T?)JsonSerializer.Deserialize(ref reader, type, options); + } + else { - // Creating a copy of the reader (The derived deserialisation has to be done from the start) - Utf8JsonReader typeReader = reader; + using var document = JsonDocument.ParseValue(ref reader); - if (typeReader.TokenType != JsonTokenType.StartObject) + if (!document.RootElement.TryGetProperty(DiscriminatorName, out var discriminator)) { - throw new JsonException(); + ThrowHelper.JsonException($"Object has no discriminator '{DiscriminatorName}."); + return default!; } - if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.PropertyName) - { - throw new JsonException(); - } + var type = GetDiscriminatorType(discriminator.GetString()!, typeToConvert); - var propertyName = typeReader.GetString(); + using var bufferWriter = DefaultPools.MemoryStream.GetStream(); - if (typeReader.Read() && typeReader.TokenType == JsonTokenType.String && propertyName == DiscriminatorName) + using (var writer = new Utf8JsonWriter(bufferWriter)) { - var type = GetDiscriminatorType(typeReader.GetString()!, typeToConvert); - - return (T?)JsonSerializer.Deserialize(ref reader, type, options); + document.RootElement.WriteTo(writer); } - else - { - using var document = JsonDocument.ParseValue(ref reader); - - if (!document.RootElement.TryGetProperty(DiscriminatorName, out var discriminator)) - { - ThrowHelper.JsonException($"Object has no discriminator '{DiscriminatorName}."); - return default!; - } - - var type = GetDiscriminatorType(discriminator.GetString()!, typeToConvert); - using var bufferWriter = DefaultPools.MemoryStream.GetStream(); - - using (var writer = new Utf8JsonWriter(bufferWriter)) - { - document.RootElement.WriteTo(writer); - } - - return (T?)JsonSerializer.Deserialize(bufferWriter.ToArray(), type, options); - } + return (T?)JsonSerializer.Deserialize(bufferWriter.ToArray(), type, options); } + } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - var name = GetDiscriminatorValue(value.GetType()); + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var name = GetDiscriminatorValue(value.GetType()); - writer.WriteStartObject(); - writer.WriteString(discriminatorProperty, name); + writer.WriteStartObject(); + writer.WriteString(discriminatorProperty, name); - using (var document = JsonSerializer.SerializeToDocument(value, value.GetType(), options)) + using (var document = JsonSerializer.SerializeToDocument(value, value.GetType(), options)) + { + foreach (var property in document.RootElement.EnumerateObject()) { - foreach (var property in document.RootElement.EnumerateObject()) - { - property.WriteTo(writer); - } + property.WriteTo(writer); } - - writer.WriteEndObject(); } + + writer.WriteEndObject(); } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs index 1e5ad318d6..86be278c66 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs @@ -11,62 +11,61 @@ using SquidexJsonObject = Squidex.Infrastructure.Json.Objects.JsonObject; using SquidexJsonValue = Squidex.Infrastructure.Json.Objects.JsonValue; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public sealed class JsonValueConverter : JsonConverter<SquidexJsonValue> { - public sealed class JsonValueConverter : JsonConverter<SquidexJsonValue> - { - public override bool HandleNull => true; + public override bool HandleNull => true; - public override SquidexJsonValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SquidexJsonValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) { - switch (reader.TokenType) - { - case JsonTokenType.True: - return SquidexJsonValue.True; - case JsonTokenType.False: - return SquidexJsonValue.False; - case JsonTokenType.Null: - return SquidexJsonValue.Null; - case JsonTokenType.Number: - return SquidexJsonValue.Create(reader.GetDouble()); - case JsonTokenType.String: - return SquidexJsonValue.Create(reader.GetString()); - case JsonTokenType.StartObject: - return JsonSerializer.Deserialize<SquidexJsonObject>(ref reader, options); - case JsonTokenType.StartArray: - return JsonSerializer.Deserialize<SquidexJsonArray>(ref reader, options); - default: - ThrowHelper.NotSupportedException(); - return default; - } + case JsonTokenType.True: + return SquidexJsonValue.True; + case JsonTokenType.False: + return SquidexJsonValue.False; + case JsonTokenType.Null: + return SquidexJsonValue.Null; + case JsonTokenType.Number: + return SquidexJsonValue.Create(reader.GetDouble()); + case JsonTokenType.String: + return SquidexJsonValue.Create(reader.GetString()); + case JsonTokenType.StartObject: + return JsonSerializer.Deserialize<SquidexJsonObject>(ref reader, options); + case JsonTokenType.StartArray: + return JsonSerializer.Deserialize<SquidexJsonArray>(ref reader, options); + default: + ThrowHelper.NotSupportedException(); + return default; } + } - public override void Write(Utf8JsonWriter writer, SquidexJsonValue value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SquidexJsonValue value, JsonSerializerOptions options) + { + switch (value.Value) { - switch (value.Value) - { - case null: - writer.WriteNullValue(); - break; - case bool b: - writer.WriteBooleanValue(b); - break; - case string s: - writer.WriteStringValue(s); - break; - case double n: - writer.WriteNumberValue(n); - break; - case SquidexJsonArray a: - JsonSerializer.Serialize(writer, a, options); - break; - case SquidexJsonObject o: - JsonSerializer.Serialize(writer, o, options); - break; - default: - ThrowHelper.NotSupportedException(); - break; - } + case null: + writer.WriteNullValue(); + break; + case bool b: + writer.WriteBooleanValue(b); + break; + case string s: + writer.WriteStringValue(s); + break; + case double n: + writer.WriteNumberValue(n); + break; + case SquidexJsonArray a: + JsonSerializer.Serialize(writer, a, options); + break; + case SquidexJsonObject o: + JsonSerializer.Serialize(writer, o, options); + break; + default: + ThrowHelper.NotSupportedException(); + break; } } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs b/backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs index bf8ff255dd..5a4317fbd8 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs @@ -9,58 +9,57 @@ using System.Text.Json.Serialization; using Squidex.Infrastructure.Collections; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public sealed class ReadonlyDictionaryConverterFactory : JsonConverterFactory { - public sealed class ReadonlyDictionaryConverterFactory : JsonConverterFactory + private sealed class Converter<TKey, TValue, TInstance> : JsonConverter<TInstance> where TKey : notnull { - private sealed class Converter<TKey, TValue, TInstance> : JsonConverter<TInstance> where TKey : notnull - { - private readonly Type innerType = typeof(IReadOnlyDictionary<TKey, TValue>); - private readonly Func<IDictionary<TKey, TValue>, TInstance> creator; - - public Converter() - { - creator = ReflectionHelper.CreateParameterizedConstructor<TInstance, IDictionary<TKey, TValue>>(); - } + private readonly Type innerType = typeof(IReadOnlyDictionary<TKey, TValue>); + private readonly Func<IDictionary<TKey, TValue>, TInstance> creator; - public override TInstance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var inner = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(ref reader, options)!; + public Converter() + { + creator = ReflectionHelper.CreateParameterizedConstructor<TInstance, IDictionary<TKey, TValue>>(); + } - return creator(inner); - } + public override TInstance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var inner = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(ref reader, options)!; - public override void Write(Utf8JsonWriter writer, TInstance value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value, innerType, options); - } + return creator(inner); } - public override bool CanConvert(Type typeToConvert) + public override void Write(Utf8JsonWriter writer, TInstance value, JsonSerializerOptions options) { - return IsReadonlyDictionary(typeToConvert) || IsReadonlyDictionary(typeToConvert.BaseType); + JsonSerializer.Serialize(writer, value, innerType, options); } + } - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - var typeToCreate = IsReadonlyDictionary(typeToConvert) ? typeToConvert : typeToConvert.BaseType!; + public override bool CanConvert(Type typeToConvert) + { + return IsReadonlyDictionary(typeToConvert) || IsReadonlyDictionary(typeToConvert.BaseType); + } - var concreteType = typeof(Converter<,,>).MakeGenericType( - new Type[] - { - typeToCreate.GetGenericArguments()[0], - typeToCreate.GetGenericArguments()[1], - typeToConvert - }); + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var typeToCreate = IsReadonlyDictionary(typeToConvert) ? typeToConvert : typeToConvert.BaseType!; - var converter = (JsonConverter)Activator.CreateInstance(concreteType)!; + var concreteType = typeof(Converter<,,>).MakeGenericType( + new Type[] + { + typeToCreate.GetGenericArguments()[0], + typeToCreate.GetGenericArguments()[1], + typeToConvert + }); - return converter; - } + var converter = (JsonConverter)Activator.CreateInstance(concreteType)!; - private static bool IsReadonlyDictionary(Type? type) - { - return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReadonlyDictionary<,>); - } + return converter; + } + + private static bool IsReadonlyDictionary(Type? type) + { + return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReadonlyDictionary<,>); } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs b/backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs index 7d10a033bf..92d924157d 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs @@ -9,57 +9,56 @@ using System.Text.Json.Serialization; using Squidex.Infrastructure.Collections; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public sealed class ReadonlyListConverterFactory : JsonConverterFactory { - public sealed class ReadonlyListConverterFactory : JsonConverterFactory + private sealed class Converter<T, TInstance> : JsonConverter<TInstance> { - private sealed class Converter<T, TInstance> : JsonConverter<TInstance> - { - private readonly Type innerType = typeof(IReadOnlyList<T>); - private readonly Func<IList<T>, TInstance> creator; - - public Converter() - { - creator = ReflectionHelper.CreateParameterizedConstructor<TInstance, IList<T>>(); - } + private readonly Type innerType = typeof(IReadOnlyList<T>); + private readonly Func<IList<T>, TInstance> creator; - public override TInstance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var inner = JsonSerializer.Deserialize<List<T>>(ref reader, options)!; + public Converter() + { + creator = ReflectionHelper.CreateParameterizedConstructor<TInstance, IList<T>>(); + } - return creator(inner); - } + public override TInstance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var inner = JsonSerializer.Deserialize<List<T>>(ref reader, options)!; - public override void Write(Utf8JsonWriter writer, TInstance value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value, innerType, options); - } + return creator(inner); } - public override bool CanConvert(Type typeToConvert) + public override void Write(Utf8JsonWriter writer, TInstance value, JsonSerializerOptions options) { - return IsReadonlyList(typeToConvert) || IsReadonlyList(typeToConvert.BaseType); + JsonSerializer.Serialize(writer, value, innerType, options); } + } - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - var typeToCreate = IsReadonlyList(typeToConvert) ? typeToConvert : typeToConvert.BaseType!; + public override bool CanConvert(Type typeToConvert) + { + return IsReadonlyList(typeToConvert) || IsReadonlyList(typeToConvert.BaseType); + } - var concreteType = typeof(Converter<,>).MakeGenericType( - new Type[] - { - typeToCreate.GetGenericArguments()[0], - typeToConvert, - }); + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var typeToCreate = IsReadonlyList(typeToConvert) ? typeToConvert : typeToConvert.BaseType!; - var converter = (JsonConverter)Activator.CreateInstance(concreteType)!; + var concreteType = typeof(Converter<,>).MakeGenericType( + new Type[] + { + typeToCreate.GetGenericArguments()[0], + typeToConvert, + }); - return converter; - } + var converter = (JsonConverter)Activator.CreateInstance(concreteType)!; - private static bool IsReadonlyList(Type? type) - { - return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReadonlyList<>); - } + return converter; + } + + private static bool IsReadonlyList(Type? type) + { + return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReadonlyList<>); } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs b/backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs index e8b472815c..4b706b910c 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs @@ -8,39 +8,38 @@ using System.Reflection; using System.Reflection.Emit; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +internal static class ReflectionHelper { - internal static class ReflectionHelper + public static Func<TInput, TInstance> CreateParameterizedConstructor<TInstance, TInput>() + { + var method = CreateParameterizedConstructor(typeof(TInstance), typeof(TInput)); + + return method.CreateDelegate<Func<TInput, TInstance>>(); + } + + private static DynamicMethod CreateParameterizedConstructor(Type type, Type parameterType) { - public static Func<TInput, TInstance> CreateParameterizedConstructor<TInstance, TInput>() - { - var method = CreateParameterizedConstructor(typeof(TInstance), typeof(TInput)); - - return method.CreateDelegate<Func<TInput, TInstance>>(); - } - - private static DynamicMethod CreateParameterizedConstructor(Type type, Type parameterType) - { - var constructor = - type.GetConstructors() - .Single(x => - x.GetParameters().Length == 1 && - x.GetParameters()[0].ParameterType == parameterType); - - var dynamicMethod = new DynamicMethod( - ConstructorInfo.ConstructorName, - type, - new[] { parameterType }, - typeof(ReflectionHelper).Module, - true); - - var generator = dynamicMethod.GetILGenerator(); - - generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Newobj, constructor); - generator.Emit(OpCodes.Ret); - - return dynamicMethod; - } + var constructor = + type.GetConstructors() + .Single(x => + x.GetParameters().Length == 1 && + x.GetParameters()[0].ParameterType == parameterType); + + var dynamicMethod = new DynamicMethod( + ConstructorInfo.ConstructorName, + type, + new[] { parameterType }, + typeof(ReflectionHelper).Module, + true); + + var generator = dynamicMethod.GetILGenerator(); + + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Newobj, constructor); + generator.Emit(OpCodes.Ret); + + return dynamicMethod; } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs index 2981051b3b..85129586e1 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs @@ -9,70 +9,69 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public sealed class StringConverter<T> : JsonConverter<T> where T : notnull { - public sealed class StringConverter<T> : JsonConverter<T> where T : notnull - { - private readonly Func<string, T> convertFromString; - private readonly Func<T, string?> convertToString; + private readonly Func<string, T> convertFromString; + private readonly Func<T, string?> convertToString; - public StringConverter(Func<string, T> convertFromString, Func<T, string?>? convertToString = null) - { - this.convertFromString = convertFromString; - this.convertToString = convertToString ?? (x => x.ToString()); - } + public StringConverter(Func<string, T> convertFromString, Func<T, string?>? convertToString = null) + { + this.convertFromString = convertFromString; + this.convertToString = convertToString ?? (x => x.ToString()); + } - public StringConverter() - { - var typeConverter = TypeDescriptor.GetConverter(typeof(T)); + public StringConverter() + { + var typeConverter = TypeDescriptor.GetConverter(typeof(T)); - convertFromString = x => (T)typeConverter.ConvertFromInvariantString(x)!; - convertToString = x => typeConverter.ConvertToInvariantString(x)!; - } + convertFromString = x => (T)typeConverter.ConvertFromInvariantString(x)!; + convertToString = x => typeConverter.ConvertToInvariantString(x)!; + } - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) { - switch (reader.TokenType) - { - case JsonTokenType.String: - var text = reader.GetString(); - try - { - return convertFromString(text!); - } - catch (Exception ex) - { - ThrowHelper.JsonException("Error while converting from string.", ex); - return default; - } + case JsonTokenType.String: + var text = reader.GetString(); + try + { + return convertFromString(text!); + } + catch (Exception ex) + { + ThrowHelper.JsonException("Error while converting from string.", ex); + return default; + } - case JsonTokenType.StartObject: - var optionsWithoutSelf = new JsonSerializerOptions(options); + case JsonTokenType.StartObject: + var optionsWithoutSelf = new JsonSerializerOptions(options); - // Remove the current converter, otherwise we would create a stackoverflow exception. - optionsWithoutSelf.Converters.Remove(this); + // Remove the current converter, otherwise we would create a stackoverflow exception. + optionsWithoutSelf.Converters.Remove(this); - return JsonSerializer.Deserialize<T>(ref reader, optionsWithoutSelf); + return JsonSerializer.Deserialize<T>(ref reader, optionsWithoutSelf); - default: - ThrowHelper.JsonException($"Expected string or object, got {reader.TokenType}."); - return default; - } + default: + ThrowHelper.JsonException($"Expected string or object, got {reader.TokenType}."); + return default; } + } - public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return convertFromString(reader.GetString()!); - } + public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return convertFromString(reader.GetString()!); + } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(convertToString(value)); - } + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(convertToString(value)); + } - public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WritePropertyName(convertToString(value)!); - } + public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WritePropertyName(convertToString(value)!); } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs index c0b066eeb1..47810bedc8 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs @@ -8,24 +8,23 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public sealed class SurrogateJsonConverter<T, TSurrogate> : JsonConverter<T> where T : class where TSurrogate : ISurrogate<T>, new() { - public sealed class SurrogateJsonConverter<T, TSurrogate> : JsonConverter<T> where T : class where TSurrogate : ISurrogate<T>, new() + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var surrogate = JsonSerializer.Deserialize<TSurrogate>(ref reader, options); + var surrogate = JsonSerializer.Deserialize<TSurrogate>(ref reader, options); - return surrogate?.ToSource(); - } + return surrogate?.ToSource(); + } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - var surrogate = new TSurrogate(); + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var surrogate = new TSurrogate(); - surrogate.FromSource(value); + surrogate.FromSource(value); - JsonSerializer.Serialize(writer, surrogate, options); - } + JsonSerializer.Serialize(writer, surrogate, options); } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs index 93a26518f3..6ca80f36b9 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs @@ -8,100 +8,99 @@ using System.Text.Json; using SystemJsonException = System.Text.Json.JsonException; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public sealed class SystemJsonSerializer : IJsonSerializer { - public sealed class SystemJsonSerializer : IJsonSerializer + private readonly JsonSerializerOptions optionsNormal; + private readonly JsonSerializerOptions optionsIndented; + + public SystemJsonSerializer(JsonSerializerOptions options) { - private readonly JsonSerializerOptions optionsNormal; - private readonly JsonSerializerOptions optionsIndented; + optionsNormal = new JsonSerializerOptions(options) + { + WriteIndented = false + }; - public SystemJsonSerializer(JsonSerializerOptions options) + optionsIndented = new JsonSerializerOptions(options) { - optionsNormal = new JsonSerializerOptions(options) - { - WriteIndented = false - }; + WriteIndented = true + }; + } - optionsIndented = new JsonSerializerOptions(options) - { - WriteIndented = true - }; + public T Deserialize<T>(string value, Type? actualType = null) + { + try + { + return (T)JsonSerializer.Deserialize(value, actualType ?? typeof(T), optionsNormal)!; } - - public T Deserialize<T>(string value, Type? actualType = null) + catch (SystemJsonException ex) { - try - { - return (T)JsonSerializer.Deserialize(value, actualType ?? typeof(T), optionsNormal)!; - } - catch (SystemJsonException ex) - { - ThrowHelper.JsonException(ex.Message, ex); - return default!; - } + ThrowHelper.JsonException(ex.Message, ex); + return default!; } + } - public T Deserialize<T>(Stream stream, Type? actualType = null, bool leaveOpen = false) + public T Deserialize<T>(Stream stream, Type? actualType = null, bool leaveOpen = false) + { + try { - try - { - return (T)JsonSerializer.Deserialize(stream, actualType ?? typeof(T), optionsNormal)!; - } - catch (SystemJsonException ex) - { - ThrowHelper.JsonException(ex.Message, ex); - return default!; - } - finally - { - if (!leaveOpen) - { - stream.Dispose(); - } - } + return (T)JsonSerializer.Deserialize(stream, actualType ?? typeof(T), optionsNormal)!; } - - public string Serialize<T>(T value, bool indented = false) + catch (SystemJsonException ex) { - return Serialize(value, typeof(T), indented); + ThrowHelper.JsonException(ex.Message, ex); + return default!; } - - public string Serialize(object? value, Type type, bool intented = false) + finally { - try + if (!leaveOpen) { - var options = intented ? optionsIndented : optionsNormal; - - return JsonSerializer.Serialize(value, type, options); - } - catch (SystemJsonException ex) - { - ThrowHelper.JsonException(ex.Message, ex); - return default!; + stream.Dispose(); } } + } + + public string Serialize<T>(T value, bool indented = false) + { + return Serialize(value, typeof(T), indented); + } - public void Serialize<T>(T value, Stream stream, bool leaveOpen = false) + public string Serialize(object? value, Type type, bool intented = false) + { + try + { + var options = intented ? optionsIndented : optionsNormal; + + return JsonSerializer.Serialize(value, type, options); + } + catch (SystemJsonException ex) { - Serialize(value, typeof(T), stream, leaveOpen); + ThrowHelper.JsonException(ex.Message, ex); + return default!; } + } - public void Serialize(object? value, Type type, Stream stream, bool leaveOpen = false) + public void Serialize<T>(T value, Stream stream, bool leaveOpen = false) + { + Serialize(value, typeof(T), stream, leaveOpen); + } + + public void Serialize(object? value, Type type, Stream stream, bool leaveOpen = false) + { + try { - try - { - JsonSerializer.Serialize(stream, value, optionsNormal); - } - catch (SystemJsonException ex) - { - ThrowHelper.JsonException(ex.Message, ex); - } - finally + JsonSerializer.Serialize(stream, value, optionsNormal); + } + catch (SystemJsonException ex) + { + ThrowHelper.JsonException(ex.Message, ex); + } + finally + { + if (!leaveOpen) { - if (!leaveOpen) - { - stream.Dispose(); - } + stream.Dispose(); } } } diff --git a/backend/src/Squidex.Infrastructure/Json/System/UnsafeRawJsonConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/UnsafeRawJsonConverter.cs index 5b8d8d0fb7..57526a26ee 100644 --- a/backend/src/Squidex.Infrastructure/Json/System/UnsafeRawJsonConverter.cs +++ b/backend/src/Squidex.Infrastructure/Json/System/UnsafeRawJsonConverter.cs @@ -8,20 +8,19 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public sealed class UnsafeRawJsonConverter : JsonConverter<string> { - public sealed class UnsafeRawJsonConverter : JsonConverter<string> + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var document = JsonDocument.ParseValue(ref reader); + using var document = JsonDocument.ParseValue(ref reader); - return document.RootElement.GetRawText(); - } + return document.RootElement.GetRawText(); + } - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - { - writer.WriteRawValue(value, true); - } + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteRawValue(value, true); } } diff --git a/backend/src/Squidex.Infrastructure/Language.cs b/backend/src/Squidex.Infrastructure/Language.cs index fd23136178..f4118cb979 100644 --- a/backend/src/Squidex.Infrastructure/Language.cs +++ b/backend/src/Squidex.Infrastructure/Language.cs @@ -9,103 +9,102 @@ using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +[TypeConverter(typeof(LanguageTypeConverter))] +public partial record Language { - [TypeConverter(typeof(LanguageTypeConverter))] - public partial record Language + private static readonly Regex CultureRegex = new Regex("^(?<Code>[a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); + + public static Language GetLanguage(string iso2Code) { - private static readonly Regex CultureRegex = new Regex("^(?<Code>[a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); + Guard.NotNullOrEmpty(iso2Code); - public static Language GetLanguage(string iso2Code) + if (LanguageByCode.TryGetValue(iso2Code, out var result)) { - Guard.NotNullOrEmpty(iso2Code); + return result; + } - if (LanguageByCode.TryGetValue(iso2Code, out var result)) - { - return result; - } + return new Language(iso2Code.Trim()); + } - return new Language(iso2Code.Trim()); - } + public static IReadOnlyCollection<Language> AllLanguages + { + get => LanguageByCode.Values; + } - public static IReadOnlyCollection<Language> AllLanguages - { - get => LanguageByCode.Values; - } + public string Iso2Code { get; } - public string Iso2Code { get; } + public string EnglishName + { + get => NamesEnglish.GetValueOrDefault(Iso2Code) ?? string.Empty; + } - public string EnglishName - { - get => NamesEnglish.GetValueOrDefault(Iso2Code) ?? string.Empty; - } + public string NativeName + { + get => NamesNative.GetValueOrDefault(Iso2Code) ?? string.Empty; + } - public string NativeName - { - get => NamesNative.GetValueOrDefault(Iso2Code) ?? string.Empty; - } + private Language(string iso2Code) + { + Iso2Code = iso2Code; + } - private Language(string iso2Code) - { - Iso2Code = iso2Code; - } + public static bool IsDefault(string iso2Code) + { + Guard.NotNull(iso2Code); - public static bool IsDefault(string iso2Code) - { - Guard.NotNull(iso2Code); + return LanguageByCode.ContainsKey(iso2Code); + } - return LanguageByCode.ContainsKey(iso2Code); - } + public static bool TryGetLanguage(string iso2Code, [MaybeNullWhen(false)] out Language language) + { + Guard.NotNull(iso2Code); - public static bool TryGetLanguage(string iso2Code, [MaybeNullWhen(false)] out Language language) - { - Guard.NotNull(iso2Code); + return LanguageByCode.TryGetValue(iso2Code, out language!); + } - return LanguageByCode.TryGetValue(iso2Code, out language!); - } + public static implicit operator string(Language language) + { + return language.Iso2Code; + } - public static implicit operator string(Language language) - { - return language.Iso2Code; - } + public static implicit operator Language(string iso2Code) + { + return GetLanguage(iso2Code!); + } - public static implicit operator Language(string iso2Code) + public static Language? ParseOrNull(string input) + { + if (string.IsNullOrWhiteSpace(input)) { - return GetLanguage(iso2Code!); + return null; } - public static Language? ParseOrNull(string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - return null; - } - - input = input.Trim(); - - if (input.Length != 2) - { - var match = CultureRegex.Match(input); + input = input.Trim(); - if (!match.Success) - { - return null; - } - - input = match.Groups["Code"].Value; - } + if (input.Length != 2) + { + var match = CultureRegex.Match(input); - if (TryGetLanguage(input.ToLowerInvariant(), out var result)) + if (!match.Success) { - return result; + return null; } - return null; + input = match.Groups["Code"].Value; } - public override string ToString() + if (TryGetLanguage(input.ToLowerInvariant(), out var result)) { - return EnglishName; + return result; } + + return null; + } + + public override string ToString() + { + return EnglishName; } } diff --git a/backend/src/Squidex.Infrastructure/LanguageTypeConverter.cs b/backend/src/Squidex.Infrastructure/LanguageTypeConverter.cs index dcd02664c1..e8f008c34a 100644 --- a/backend/src/Squidex.Infrastructure/LanguageTypeConverter.cs +++ b/backend/src/Squidex.Infrastructure/LanguageTypeConverter.cs @@ -8,28 +8,27 @@ using System.ComponentModel; using System.Globalization; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public sealed class LanguageTypeConverter : TypeConverter { - public sealed class LanguageTypeConverter : TypeConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - { - return sourceType == typeof(string); - } + return sourceType == typeof(string); + } - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) - { - return destinationType == typeof(string); - } + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string); + } - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - return Language.GetLanguage((string)value); - } + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + return Language.GetLanguage((string)value); + } - public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) - { - return ((Language)value!).Iso2Code; - } + public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) + { + return ((Language)value!).Iso2Code; } } diff --git a/backend/src/Squidex.Infrastructure/Languages.cs b/backend/src/Squidex.Infrastructure/Languages.cs index d7626db407..d3cba8fe76 100644 --- a/backend/src/Squidex.Infrastructure/Languages.cs +++ b/backend/src/Squidex.Infrastructure/Languages.cs @@ -9,617 +9,616 @@ using System.CodeDom.Compiler; -namespace Squidex.Infrastructure -{ - [GeneratedCode("LanguagesGenerator", "1.0")] - public partial record Language - { - private static readonly Dictionary<string, Language> LanguageByCode = new Dictionary<string, Language>(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary<string, string> NamesEnglish = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary<string, string> NamesNative = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); +namespace Squidex.Infrastructure; - internal static Language AddLanguage(string iso2Code, string englishName, string nativeName) - { - NamesEnglish[iso2Code] = englishName; - NamesNative[iso2Code] = nativeName; +[GeneratedCode("LanguagesGenerator", "1.0")] +public partial record Language +{ + private static readonly Dictionary<string, Language> LanguageByCode = new Dictionary<string, Language>(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary<string, string> NamesEnglish = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary<string, string> NamesNative = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - return LanguageByCode.GetOrAdd(iso2Code, code => new Language(code)); - } + internal static Language AddLanguage(string iso2Code, string englishName, string nativeName) + { + NamesEnglish[iso2Code] = englishName; + NamesNative[iso2Code] = nativeName; - public static readonly Language AA = AddLanguage("aa", "Afar", "Afaraf"); - public static readonly Language AB = AddLanguage("ab", "Abkhaz", "аҧсуа бызшәа, аҧсшәа"); - public static readonly Language AE = AddLanguage("ae", "Avestan", "avesta"); - public static readonly Language AF = AddLanguage("af", "Afrikaans", "Afrikaans"); - public static readonly Language AK = AddLanguage("ak", "Akan", "Akan"); - public static readonly Language AM = AddLanguage("am", "Amharic", "አማርኛ"); - public static readonly Language AN = AddLanguage("an", "Aragonese", "aragonés"); - public static readonly Language AR = AddLanguage("ar", "Arabic", "العربية"); - public static readonly Language AS = AddLanguage("as", "Assamese", "অসমীয়া"); - public static readonly Language AV = AddLanguage("av", "Avaric", "авар мацӀ, магӀарул мацӀ"); - public static readonly Language AY = AddLanguage("ay", "Aymara", "aymar aru"); - public static readonly Language AZ = AddLanguage("az", "Azerbaijani", "azərbaycan dili"); - public static readonly Language BA = AddLanguage("ba", "Bashkir", "башҡорт теле"); - public static readonly Language BE = AddLanguage("be", "Belarusian", "беларуская мова"); - public static readonly Language BG = AddLanguage("bg", "Bulgarian", "български език"); - public static readonly Language BH = AddLanguage("bh", "Bihari", "भोजपुरी"); - public static readonly Language BI = AddLanguage("bi", "Bislama", "Bislama"); - public static readonly Language BM = AddLanguage("bm", "Bambara", "bamanankan"); - public static readonly Language BN = AddLanguage("bn", "Bengali, Bangla", "বাংলা"); - public static readonly Language BO = AddLanguage("bo", "Tibetan Standard, Tibetan, Central", "བོད་ཡིག"); - public static readonly Language BR = AddLanguage("br", "Breton", "brezhoneg"); - public static readonly Language BS = AddLanguage("bs", "Bosnian", "bosanski jezik"); - public static readonly Language CA = AddLanguage("ca", "Catalan", "català"); - public static readonly Language CE = AddLanguage("ce", "Chechen", "нохчийн мотт"); - public static readonly Language CH = AddLanguage("ch", "Chamorro", "Chamoru"); - public static readonly Language CO = AddLanguage("co", "Corsican", "corsu, lingua corsa"); - public static readonly Language CR = AddLanguage("cr", "Cree", "ᓀᐦᐃᔭᐍᐏᐣ"); - public static readonly Language CS = AddLanguage("cs", "Czech", "čeština, český jazyk"); - public static readonly Language CU = AddLanguage("cu", "Old Church Slavonic, Church Slavonic, Old Bulgarian", "ѩзыкъ словѣньскъ"); - public static readonly Language CV = AddLanguage("cv", "Chuvash", "чӑваш чӗлхи"); - public static readonly Language CY = AddLanguage("cy", "Welsh", "Cymraeg"); - public static readonly Language DA = AddLanguage("da", "Danish", "dansk"); - public static readonly Language DE = AddLanguage("de", "German", "Deutsch"); - public static readonly Language DV = AddLanguage("dv", "Divehi, Dhivehi, Maldivian", "ދިވެހި"); - public static readonly Language DZ = AddLanguage("dz", "Dzongkha", "རྫོང་ཁ"); - public static readonly Language EE = AddLanguage("ee", "Ewe", "Eʋegbe"); - public static readonly Language EL = AddLanguage("el", "Greek (modern)", "ελληνικά"); - public static readonly Language EN = AddLanguage("en", "English", "English"); - public static readonly Language EO = AddLanguage("eo", "Esperanto", "Esperanto"); - public static readonly Language ES = AddLanguage("es", "Spanish", "Español"); - public static readonly Language ET = AddLanguage("et", "Estonian", "eesti, eesti keel"); - public static readonly Language EU = AddLanguage("eu", "Basque", "euskara, euskera"); - public static readonly Language FA = AddLanguage("fa", "Persian (Farsi)", "فارسی"); - public static readonly Language FF = AddLanguage("ff", "Fula, Fulah, Pulaar, Pular", "Fulfulde, Pulaar, Pular"); - public static readonly Language FI = AddLanguage("fi", "Finnish", "suomi, suomen kieli"); - public static readonly Language FJ = AddLanguage("fj", "Fijian", "vosa Vakaviti"); - public static readonly Language FO = AddLanguage("fo", "Faroese", "føroyskt"); - public static readonly Language FR = AddLanguage("fr", "French", "français, langue française"); - public static readonly Language FY = AddLanguage("fy", "Western Frisian", "Frysk"); - public static readonly Language GA = AddLanguage("ga", "Irish", "Gaeilge"); - public static readonly Language GD = AddLanguage("gd", "Scottish Gaelic, Gaelic", "Gàidhlig"); - public static readonly Language GL = AddLanguage("gl", "Galician", "galego"); - public static readonly Language GN = AddLanguage("gn", "Guaraní", "Avañe'ẽ"); - public static readonly Language GU = AddLanguage("gu", "Gujarati", "ગુજરાતી"); - public static readonly Language GV = AddLanguage("gv", "Manx", "Gaelg, Gailck"); - public static readonly Language HA = AddLanguage("ha", "Hausa", "(Hausa) هَوُسَ"); - public static readonly Language HE = AddLanguage("he", "Hebrew (modern)", "עברית"); - public static readonly Language HI = AddLanguage("hi", "Hindi", "हिन्दी, हिंदी"); - public static readonly Language HO = AddLanguage("ho", "Hiri Motu", "Hiri Motu"); - public static readonly Language HR = AddLanguage("hr", "Croatian", "hrvatski jezik"); - public static readonly Language HT = AddLanguage("ht", "Haitian, Haitian Creole", "Kreyòl ayisyen"); - public static readonly Language HU = AddLanguage("hu", "Hungarian", "magyar"); - public static readonly Language HY = AddLanguage("hy", "Armenian", "Հայերեն"); - public static readonly Language HZ = AddLanguage("hz", "Herero", "Otjiherero"); - public static readonly Language IA = AddLanguage("ia", "Interlingua", "Interlingua"); - public static readonly Language ID = AddLanguage("id", "Indonesian", "Bahasa Indonesia"); - public static readonly Language IE = AddLanguage("ie", "Interlingue", "Originally called Occidental; then Interlingue after WWII"); - public static readonly Language IG = AddLanguage("ig", "Igbo", "Asụsụ Igbo"); - public static readonly Language II = AddLanguage("ii", "Nuosu", "ꆈꌠ꒿ Nuosuhxop"); - public static readonly Language IK = AddLanguage("ik", "Inupiaq", "Iñupiaq, Iñupiatun"); - public static readonly Language IO = AddLanguage("io", "Ido", "Ido"); - public static readonly Language IS = AddLanguage("is", "Icelandic", "Íslenska"); - public static readonly Language IT = AddLanguage("it", "Italian", "Italiano"); - public static readonly Language IU = AddLanguage("iu", "Inuktitut", "ᐃᓄᒃᑎᑐᑦ"); - public static readonly Language JA = AddLanguage("ja", "Japanese", "日本語 (にほんご)"); - public static readonly Language JV = AddLanguage("jv", "Javanese", "ꦧꦱꦗꦮ, Basa Jawa"); - public static readonly Language KA = AddLanguage("ka", "Georgian", "ქართული"); - public static readonly Language KG = AddLanguage("kg", "Kongo", "Kikongo"); - public static readonly Language KI = AddLanguage("ki", "Kikuyu, Gikuyu", "Gĩkũyũ"); - public static readonly Language KJ = AddLanguage("kj", "Kwanyama, Kuanyama", "Kuanyama"); - public static readonly Language KK = AddLanguage("kk", "Kazakh", "қазақ тілі"); - public static readonly Language KL = AddLanguage("kl", "Kalaallisut, Greenlandic", "kalaallisut, kalaallit oqaasii"); - public static readonly Language KM = AddLanguage("km", "Khmer", "ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ"); - public static readonly Language KN = AddLanguage("kn", "Kannada", "ಕನ್ನಡ"); - public static readonly Language KO = AddLanguage("ko", "Korean", "한국어"); - public static readonly Language KR = AddLanguage("kr", "Kanuri", "Kanuri"); - public static readonly Language KS = AddLanguage("ks", "Kashmiri", "कश्मीरी, كشميري‎"); - public static readonly Language KU = AddLanguage("ku", "Kurdish", "Kurdî, كوردی‎"); - public static readonly Language KV = AddLanguage("kv", "Komi", "коми кыв"); - public static readonly Language KW = AddLanguage("kw", "Cornish", "Kernewek"); - public static readonly Language KY = AddLanguage("ky", "Kyrgyz", "Кыргызча, Кыргыз тили"); - public static readonly Language LA = AddLanguage("la", "Latin", "latine, lingua latina"); - public static readonly Language LB = AddLanguage("lb", "Luxembourgish, Letzeburgesch", "Lëtzebuergesch"); - public static readonly Language LG = AddLanguage("lg", "Ganda", "Luganda"); - public static readonly Language LI = AddLanguage("li", "Limburgish, Limburgan, Limburger", "Limburgs"); - public static readonly Language LN = AddLanguage("ln", "Lingala", "Lingála"); - public static readonly Language LO = AddLanguage("lo", "Lao", "ພາສາລາວ"); - public static readonly Language LT = AddLanguage("lt", "Lithuanian", "lietuvių kalba"); - public static readonly Language LU = AddLanguage("lu", "Luba-Katanga", "Tshiluba"); - public static readonly Language LV = AddLanguage("lv", "Latvian", "latviešu valoda"); - public static readonly Language MG = AddLanguage("mg", "Malagasy", "fiteny malagasy"); - public static readonly Language MH = AddLanguage("mh", "Marshallese", "Kajin M̧ajeļ"); - public static readonly Language MI = AddLanguage("mi", "Māori", "te reo Māori"); - public static readonly Language MK = AddLanguage("mk", "Macedonian", "македонски јазик"); - public static readonly Language ML = AddLanguage("ml", "Malayalam", "മലയാളം"); - public static readonly Language MN = AddLanguage("mn", "Mongolian", "Монгол хэл"); - public static readonly Language MR = AddLanguage("mr", "Marathi (Marāṭhī)", "मराठी"); - public static readonly Language MS = AddLanguage("ms", "Malay", "bahasa Melayu, بهاس ملايو‎"); - public static readonly Language MT = AddLanguage("mt", "Maltese", "Malti"); - public static readonly Language MY = AddLanguage("my", "Burmese", "ဗမာစာ"); - public static readonly Language NA = AddLanguage("na", "Nauruan", "Dorerin Naoero"); - public static readonly Language NB = AddLanguage("nb", "Norwegian Bokmål", "Norsk bokmål"); - public static readonly Language ND = AddLanguage("nd", "Northern Ndebele", "isiNdebele"); - public static readonly Language NE = AddLanguage("ne", "Nepali", "नेपाली"); - public static readonly Language NG = AddLanguage("ng", "Ndonga", "Owambo"); - public static readonly Language NL = AddLanguage("nl", "Dutch", "Nederlands, Vlaams"); - public static readonly Language NN = AddLanguage("nn", "Norwegian Nynorsk", "Norsk nynorsk"); - public static readonly Language NO = AddLanguage("no", "Norwegian", "Norsk"); - public static readonly Language NR = AddLanguage("nr", "Southern Ndebele", "isiNdebele"); - public static readonly Language NV = AddLanguage("nv", "Navajo, Navaho", "Diné bizaad"); - public static readonly Language NY = AddLanguage("ny", "Chichewa, Chewa, Nyanja", "chiCheŵa, chinyanja"); - public static readonly Language OC = AddLanguage("oc", "Occitan", "occitan, lenga d'òc"); - public static readonly Language OJ = AddLanguage("oj", "Ojibwe, Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ"); - public static readonly Language OM = AddLanguage("om", "Oromo", "Afaan Oromoo"); - public static readonly Language OR = AddLanguage("or", "Oriya", "ଓଡ଼ିଆ"); - public static readonly Language OS = AddLanguage("os", "Ossetian, Ossetic", "ирон æвзаг"); - public static readonly Language PA = AddLanguage("pa", "(Eastern) Punjabi", "ਪੰਜਾਬੀ"); - public static readonly Language PI = AddLanguage("pi", "Pāli", "पाऴि"); - public static readonly Language PL = AddLanguage("pl", "Polish", "język polski, polszczyzna"); - public static readonly Language PS = AddLanguage("ps", "Pashto, Pushto", "پښتو"); - public static readonly Language PT = AddLanguage("pt", "Portuguese", "Português"); - public static readonly Language QU = AddLanguage("qu", "Quechua", "Runa Simi, Kichwa"); - public static readonly Language RM = AddLanguage("rm", "Romansh", "rumantsch grischun"); - public static readonly Language RN = AddLanguage("rn", "Kirundi", "Ikirundi"); - public static readonly Language RO = AddLanguage("ro", "Romanian", "Română"); - public static readonly Language RU = AddLanguage("ru", "Russian", "Русский"); - public static readonly Language RW = AddLanguage("rw", "Kinyarwanda", "Ikinyarwanda"); - public static readonly Language SA = AddLanguage("sa", "Sanskrit (Saṁskṛta)", "संस्कृतम्"); - public static readonly Language SC = AddLanguage("sc", "Sardinian", "sardu"); - public static readonly Language SD = AddLanguage("sd", "Sindhi", "सिन्धी, سنڌي، سندھی‎"); - public static readonly Language SE = AddLanguage("se", "Northern Sami", "Davvisámegiella"); - public static readonly Language SG = AddLanguage("sg", "Sango", "yângâ tî sängö"); - public static readonly Language SI = AddLanguage("si", "Sinhalese, Sinhala", "සිංහල"); - public static readonly Language SK = AddLanguage("sk", "Slovak", "slovenčina, slovenský jazyk"); - public static readonly Language SL = AddLanguage("sl", "Slovene", "slovenski jezik, slovenščina"); - public static readonly Language SM = AddLanguage("sm", "Samoan", "gagana fa'a Samoa"); - public static readonly Language SN = AddLanguage("sn", "Shona", "chiShona"); - public static readonly Language SO = AddLanguage("so", "Somali", "Soomaaliga, af Soomaali"); - public static readonly Language SQ = AddLanguage("sq", "Albanian", "Shqip"); - public static readonly Language SR = AddLanguage("sr", "Serbian", "српски језик"); - public static readonly Language SS = AddLanguage("ss", "Swati", "SiSwati"); - public static readonly Language ST = AddLanguage("st", "Southern Sotho", "Sesotho"); - public static readonly Language SU = AddLanguage("su", "Sundanese", "Basa Sunda"); - public static readonly Language SV = AddLanguage("sv", "Swedish", "svenska"); - public static readonly Language SW = AddLanguage("sw", "Swahili", "Kiswahili"); - public static readonly Language TA = AddLanguage("ta", "Tamil", "தமிழ்"); - public static readonly Language TE = AddLanguage("te", "Telugu", "తెలుగు"); - public static readonly Language TG = AddLanguage("tg", "Tajik", "тоҷикӣ, toçikī, تاجیکی‎"); - public static readonly Language TH = AddLanguage("th", "Thai", "ไทย"); - public static readonly Language TI = AddLanguage("ti", "Tigrinya", "ትግርኛ"); - public static readonly Language TK = AddLanguage("tk", "Turkmen", "Türkmen, Түркмен"); - public static readonly Language TL = AddLanguage("tl", "Tagalog", "Wikang Tagalog"); - public static readonly Language TN = AddLanguage("tn", "Tswana", "Setswana"); - public static readonly Language TO = AddLanguage("to", "Tonga (Tonga Islands)", "faka Tonga"); - public static readonly Language TR = AddLanguage("tr", "Turkish", "Türkçe"); - public static readonly Language TS = AddLanguage("ts", "Tsonga", "Xitsonga"); - public static readonly Language TT = AddLanguage("tt", "Tatar", "татар теле, tatar tele"); - public static readonly Language TW = AddLanguage("tw", "Twi", "Twi"); - public static readonly Language TY = AddLanguage("ty", "Tahitian", "Reo Tahiti"); - public static readonly Language UG = AddLanguage("ug", "Uyghur", "ئۇيغۇرچە‎, Uyghurche"); - public static readonly Language UK = AddLanguage("uk", "Ukrainian", "Українська"); - public static readonly Language UR = AddLanguage("ur", "Urdu", "اردو"); - public static readonly Language UZ = AddLanguage("uz", "Uzbek", "Oʻzbek, Ўзбек, أۇزبېك‎"); - public static readonly Language VE = AddLanguage("ve", "Venda", "Tshivenḓa"); - public static readonly Language VI = AddLanguage("vi", "Vietnamese", "Tiếng Việt"); - public static readonly Language VO = AddLanguage("vo", "Volapük", "Volapük"); - public static readonly Language WA = AddLanguage("wa", "Walloon", "walon"); - public static readonly Language WO = AddLanguage("wo", "Wolof", "Wollof"); - public static readonly Language XH = AddLanguage("xh", "Xhosa", "isiXhosa"); - public static readonly Language YI = AddLanguage("yi", "Yiddish", "ייִדיש"); - public static readonly Language YO = AddLanguage("yo", "Yoruba", "Yorùbá"); - public static readonly Language ZA = AddLanguage("za", "Zhuang, Chuang", "Saɯ cueŋƅ, Saw cuengh"); - public static readonly Language ZH = AddLanguage("zh", "Chinese", "中文 (Zhōngwén), 汉语, 漢語"); - public static readonly Language ZU = AddLanguage("zu", "Zulu", "isiZulu"); - public static readonly Language AfarDjibouti = AddLanguage("aa-DJ", "Afar (Djibouti)", "Afar (Djibouti)"); - public static readonly Language AfarEritrea = AddLanguage("aa-ER", "Afar (Eritrea)", "Afar (Eritrea)"); - public static readonly Language AfarEthiopia = AddLanguage("aa-ET", "Afar (Ethiopia)", "Afar (Ethiopia)"); - public static readonly Language AfrikaansNamibia = AddLanguage("af-NA", "Afrikaans (Namibia)", "Afrikaans (Namibië)"); - public static readonly Language AfrikaansSouthAfrica = AddLanguage("af-ZA", "Afrikaans (South Africa)", "Afrikaans (Suid-Afrika)"); - public static readonly Language AkanGhana = AddLanguage("ak-GH", "Akan (Ghana)", "Akan (Gaana)"); - public static readonly Language AmharicEthiopia = AddLanguage("am-ET", "Amharic (Ethiopia)", "አማርኛ (ኢትዮጵያ)"); - public static readonly Language ArabicUnitedArabEmirates = AddLanguage("ar-AE", "Arabic (United Arab Emirates)", "العربية (الإمارات العربية المتحدة)"); - public static readonly Language ArabicBahrain = AddLanguage("ar-BH", "Arabic (Bahrain)", "العربية (البحرين)"); - public static readonly Language ArabicDjibouti = AddLanguage("ar-DJ", "Arabic (Djibouti)", "العربية (جيبوتي)"); - public static readonly Language ArabicAlgeria = AddLanguage("ar-DZ", "Arabic (Algeria)", "العربية (الجزائر)"); - public static readonly Language ArabicEgypt = AddLanguage("ar-EG", "Arabic (Egypt)", "العربية (مصر)"); - public static readonly Language ArabicEritrea = AddLanguage("ar-ER", "Arabic (Eritrea)", "العربية (إريتريا)"); - public static readonly Language ArabicIsrael = AddLanguage("ar-IL", "Arabic (Israel)", "العربية (إسرائيل)"); - public static readonly Language ArabicIraq = AddLanguage("ar-IQ", "Arabic (Iraq)", "العربية (العراق)"); - public static readonly Language ArabicJordan = AddLanguage("ar-JO", "Arabic (Jordan)", "العربية (الأردن)"); - public static readonly Language ArabicComoros = AddLanguage("ar-KM", "Arabic (Comoros)", "العربية (جزر القمر)"); - public static readonly Language ArabicKuwait = AddLanguage("ar-KW", "Arabic (Kuwait)", "العربية (الكويت)"); - public static readonly Language ArabicLebanon = AddLanguage("ar-LB", "Arabic (Lebanon)", "العربية (لبنان)"); - public static readonly Language ArabicLibya = AddLanguage("ar-LY", "Arabic (Libya)", "العربية (ليبيا)"); - public static readonly Language ArabicMorocco = AddLanguage("ar-MA", "Arabic (Morocco)", "العربية (المغرب)"); - public static readonly Language ArabicMauritania = AddLanguage("ar-MR", "Arabic (Mauritania)", "العربية (موريتانيا)"); - public static readonly Language ArabicOman = AddLanguage("ar-OM", "Arabic (Oman)", "العربية (عُمان)"); - public static readonly Language ArabicPalestinianAuthority = AddLanguage("ar-PS", "Arabic (Palestinian Authority)", "العربية (السلطة الفلسطينية)"); - public static readonly Language ArabicQatar = AddLanguage("ar-QA", "Arabic (Qatar)", "العربية (قطر)"); - public static readonly Language ArabicSaudiArabia = AddLanguage("ar-SA", "Arabic (Saudi Arabia)", "العربية (المملكة العربية السعودية)"); - public static readonly Language ArabicSudan = AddLanguage("ar-SD", "Arabic (Sudan)", "العربية (السودان)"); - public static readonly Language ArabicSomalia = AddLanguage("ar-SO", "Arabic (Somalia)", "العربية (الصومال)"); - public static readonly Language ArabicSouthSudan = AddLanguage("ar-SS", "Arabic (South Sudan)", "العربية (جنوب السودان)"); - public static readonly Language ArabicSyria = AddLanguage("ar-SY", "Arabic (Syria)", "العربية (سوريا)"); - public static readonly Language ArabicChad = AddLanguage("ar-TD", "Arabic (Chad)", "العربية (تشاد)"); - public static readonly Language ArabicTunisia = AddLanguage("ar-TN", "Arabic (Tunisia)", "العربية (تونس)"); - public static readonly Language ArabicYemen = AddLanguage("ar-YE", "Arabic (Yemen)", "العربية (اليمن)"); - public static readonly Language AssameseIndia = AddLanguage("as-IN", "Assamese (India)", "অসমীয়া (ভাৰত)"); - public static readonly Language BashkirRussia = AddLanguage("ba-RU", "Bashkir (Russia)", "Bashkir (Russia)"); - public static readonly Language BelarusianBelarus = AddLanguage("be-BY", "Belarusian (Belarus)", "беларуская (Беларусь)"); - public static readonly Language BulgarianBulgaria = AddLanguage("bg-BG", "Bulgarian (Bulgaria)", "български (България)"); - public static readonly Language BamanankanMali = AddLanguage("bm-ML", "Bamanankan (Mali)", "bamanakan (Mali)"); - public static readonly Language BanglaBangladesh = AddLanguage("bn-BD", "Bangla (Bangladesh)", "বাংলা (বাংলাদেশ)"); - public static readonly Language BanglaIndia = AddLanguage("bn-IN", "Bangla (India)", "বাংলা (ভারত)"); - public static readonly Language TibetanChina = AddLanguage("bo-CN", "Tibetan (China)", "བོད་སྐད་ (རྒྱ་ནག)"); - public static readonly Language TibetanIndia = AddLanguage("bo-IN", "Tibetan (India)", "བོད་སྐད་ (རྒྱ་གར་)"); - public static readonly Language BretonFrance = AddLanguage("br-FR", "Breton (France)", "brezhoneg (Frañs)"); - public static readonly Language CatalanAndorra = AddLanguage("ca-AD", "Catalan (Andorra)", "català (Andorra)"); - public static readonly Language CatalanSpain = AddLanguage("ca-ES", "Catalan (Spain)", "català (Espanya)"); - public static readonly Language CatalanFrance = AddLanguage("ca-FR", "Catalan (France)", "català (França)"); - public static readonly Language CatalanItaly = AddLanguage("ca-IT", "Catalan (Italy)", "català (Itàlia)"); - public static readonly Language ChechenRussia = AddLanguage("ce-RU", "Chechen (Russia)", "нохчийн (Росси)"); - public static readonly Language CorsicanFrance = AddLanguage("co-FR", "Corsican (France)", "Corsican (France)"); - public static readonly Language CzechCzechia = AddLanguage("cs-CZ", "Czech (Czechia)", "čeština (Česko)"); - public static readonly Language ChurchSlavicRussia = AddLanguage("cu-RU", "Church Slavic (Russia)", "Church Slavic (Russia)"); - public static readonly Language WelshUnitedKingdom = AddLanguage("cy-GB", "Welsh (United Kingdom)", "Cymraeg (Y Deyrnas Unedig)"); - public static readonly Language DanishDenmark = AddLanguage("da-DK", "Danish (Denmark)", "dansk (Danmark)"); - public static readonly Language DanishGreenland = AddLanguage("da-GL", "Danish (Greenland)", "dansk (Grønland)"); - public static readonly Language GermanAustria = AddLanguage("de-AT", "German (Austria)", "Deutsch (Österreich)"); - public static readonly Language GermanBelgium = AddLanguage("de-BE", "German (Belgium)", "Deutsch (Belgien)"); - public static readonly Language GermanSwitzerland = AddLanguage("de-CH", "German (Switzerland)", "Deutsch (Schweiz)"); - public static readonly Language GermanGermany = AddLanguage("de-DE", "German (Germany)", "Deutsch (Deutschland)"); - public static readonly Language GermanItaly = AddLanguage("de-IT", "German (Italy)", "Deutsch (Italien)"); - public static readonly Language GermanLiechtenstein = AddLanguage("de-LI", "German (Liechtenstein)", "Deutsch (Liechtenstein)"); - public static readonly Language GermanLuxembourg = AddLanguage("de-LU", "German (Luxembourg)", "Deutsch (Luxemburg)"); - public static readonly Language DivehiMaldives = AddLanguage("dv-MV", "Divehi (Maldives)", "Divehi (Maldives)"); - public static readonly Language DzongkhaBhutan = AddLanguage("dz-BT", "Dzongkha (Bhutan)", "རྫོང་ཁ། (འབྲུག།)"); - public static readonly Language EweGhana = AddLanguage("ee-GH", "Ewe (Ghana)", "Eʋegbe (Ghana nutome)"); - public static readonly Language EweTogo = AddLanguage("ee-TG", "Ewe (Togo)", "Eʋegbe (Togo nutome)"); - public static readonly Language GreekCyprus = AddLanguage("el-CY", "Greek (Cyprus)", "Ελληνικά (Κύπρος)"); - public static readonly Language GreekGreece = AddLanguage("el-GR", "Greek (Greece)", "Ελληνικά (Ελλάδα)"); - public static readonly Language EnglishUnitedArabEmirates = AddLanguage("en-AE", "English (United Arab Emirates)", "English (United Arab Emirates)"); - public static readonly Language EnglishAntiguaBarbuda = AddLanguage("en-AG", "English (Antigua & Barbuda)", "English (Antigua & Barbuda)"); - public static readonly Language EnglishAnguilla = AddLanguage("en-AI", "English (Anguilla)", "English (Anguilla)"); - public static readonly Language EnglishAmericanSamoa = AddLanguage("en-AS", "English (American Samoa)", "English (American Samoa)"); - public static readonly Language EnglishAustria = AddLanguage("en-AT", "English (Austria)", "English (Austria)"); - public static readonly Language EnglishAustralia = AddLanguage("en-AU", "English (Australia)", "English (Australia)"); - public static readonly Language EnglishBarbados = AddLanguage("en-BB", "English (Barbados)", "English (Barbados)"); - public static readonly Language EnglishBelgium = AddLanguage("en-BE", "English (Belgium)", "English (Belgium)"); - public static readonly Language EnglishBurundi = AddLanguage("en-BI", "English (Burundi)", "English (Burundi)"); - public static readonly Language EnglishBermuda = AddLanguage("en-BM", "English (Bermuda)", "English (Bermuda)"); - public static readonly Language EnglishBahamas = AddLanguage("en-BS", "English (Bahamas)", "English (Bahamas)"); - public static readonly Language EnglishBotswana = AddLanguage("en-BW", "English (Botswana)", "English (Botswana)"); - public static readonly Language EnglishBelize = AddLanguage("en-BZ", "English (Belize)", "English (Belize)"); - public static readonly Language EnglishCanada = AddLanguage("en-CA", "English (Canada)", "English (Canada)"); - public static readonly Language EnglishCocosKeelingIslands = AddLanguage("en-CC", "English (Cocos [Keeling] Islands)", "English (Cocos [Keeling] Islands)"); - public static readonly Language EnglishSwitzerland = AddLanguage("en-CH", "English (Switzerland)", "English (Switzerland)"); - public static readonly Language EnglishCookIslands = AddLanguage("en-CK", "English (Cook Islands)", "English (Cook Islands)"); - public static readonly Language EnglishCameroon = AddLanguage("en-CM", "English (Cameroon)", "English (Cameroon)"); - public static readonly Language EnglishChristmasIsland = AddLanguage("en-CX", "English (Christmas Island)", "English (Christmas Island)"); - public static readonly Language EnglishCyprus = AddLanguage("en-CY", "English (Cyprus)", "English (Cyprus)"); - public static readonly Language EnglishGermany = AddLanguage("en-DE", "English (Germany)", "English (Germany)"); - public static readonly Language EnglishDenmark = AddLanguage("en-DK", "English (Denmark)", "English (Denmark)"); - public static readonly Language EnglishDominica = AddLanguage("en-DM", "English (Dominica)", "English (Dominica)"); - public static readonly Language EnglishEritrea = AddLanguage("en-ER", "English (Eritrea)", "English (Eritrea)"); - public static readonly Language EnglishFinland = AddLanguage("en-FI", "English (Finland)", "English (Finland)"); - public static readonly Language EnglishFiji = AddLanguage("en-FJ", "English (Fiji)", "English (Fiji)"); - public static readonly Language EnglishFalklandIslands = AddLanguage("en-FK", "English (Falkland Islands)", "English (Falkland Islands)"); - public static readonly Language EnglishMicronesia = AddLanguage("en-FM", "English (Micronesia)", "English (Micronesia)"); - public static readonly Language EnglishUnitedKingdom = AddLanguage("en-GB", "English (United Kingdom)", "English (United Kingdom)"); - public static readonly Language EnglishGrenada = AddLanguage("en-GD", "English (Grenada)", "English (Grenada)"); - public static readonly Language EnglishGuernsey = AddLanguage("en-GG", "English (Guernsey)", "English (Guernsey)"); - public static readonly Language EnglishGhana = AddLanguage("en-GH", "English (Ghana)", "English (Ghana)"); - public static readonly Language EnglishGibraltar = AddLanguage("en-GI", "English (Gibraltar)", "English (Gibraltar)"); - public static readonly Language EnglishGambia = AddLanguage("en-GM", "English (Gambia)", "English (Gambia)"); - public static readonly Language EnglishGuam = AddLanguage("en-GU", "English (Guam)", "English (Guam)"); - public static readonly Language EnglishGuyana = AddLanguage("en-GY", "English (Guyana)", "English (Guyana)"); - public static readonly Language EnglishHongKongSAR = AddLanguage("en-HK", "English (Hong Kong SAR)", "English (Hong Kong SAR)"); - public static readonly Language EnglishIreland = AddLanguage("en-IE", "English (Ireland)", "English (Ireland)"); - public static readonly Language EnglishIsrael = AddLanguage("en-IL", "English (Israel)", "English (Israel)"); - public static readonly Language EnglishIsleofMan = AddLanguage("en-IM", "English (Isle of Man)", "English (Isle of Man)"); - public static readonly Language EnglishIndia = AddLanguage("en-IN", "English (India)", "English (India)"); - public static readonly Language EnglishBritishIndianOceanTerritory = AddLanguage("en-IO", "English (British Indian Ocean Territory)", "English (British Indian Ocean Territory)"); - public static readonly Language EnglishJersey = AddLanguage("en-JE", "English (Jersey)", "English (Jersey)"); - public static readonly Language EnglishJamaica = AddLanguage("en-JM", "English (Jamaica)", "English (Jamaica)"); - public static readonly Language EnglishKenya = AddLanguage("en-KE", "English (Kenya)", "English (Kenya)"); - public static readonly Language EnglishKiribati = AddLanguage("en-KI", "English (Kiribati)", "English (Kiribati)"); - public static readonly Language EnglishStKittsNevis = AddLanguage("en-KN", "English (St. Kitts & Nevis)", "English (St. Kitts & Nevis)"); - public static readonly Language EnglishCaymanIslands = AddLanguage("en-KY", "English (Cayman Islands)", "English (Cayman Islands)"); - public static readonly Language EnglishStLucia = AddLanguage("en-LC", "English (St. Lucia)", "English (St. Lucia)"); - public static readonly Language EnglishLiberia = AddLanguage("en-LR", "English (Liberia)", "English (Liberia)"); - public static readonly Language EnglishLesotho = AddLanguage("en-LS", "English (Lesotho)", "English (Lesotho)"); - public static readonly Language EnglishMadagascar = AddLanguage("en-MG", "English (Madagascar)", "English (Madagascar)"); - public static readonly Language EnglishMarshallIslands = AddLanguage("en-MH", "English (Marshall Islands)", "English (Marshall Islands)"); - public static readonly Language EnglishMacaoSAR = AddLanguage("en-MO", "English (Macao SAR)", "English (Macao SAR)"); - public static readonly Language EnglishNorthernMarianaIslands = AddLanguage("en-MP", "English (Northern Mariana Islands)", "English (Northern Mariana Islands)"); - public static readonly Language EnglishMontserrat = AddLanguage("en-MS", "English (Montserrat)", "English (Montserrat)"); - public static readonly Language EnglishMalta = AddLanguage("en-MT", "English (Malta)", "English (Malta)"); - public static readonly Language EnglishMauritius = AddLanguage("en-MU", "English (Mauritius)", "English (Mauritius)"); - public static readonly Language EnglishMalawi = AddLanguage("en-MW", "English (Malawi)", "English (Malawi)"); - public static readonly Language EnglishMalaysia = AddLanguage("en-MY", "English (Malaysia)", "English (Malaysia)"); - public static readonly Language EnglishNamibia = AddLanguage("en-NA", "English (Namibia)", "English (Namibia)"); - public static readonly Language EnglishNorfolkIsland = AddLanguage("en-NF", "English (Norfolk Island)", "English (Norfolk Island)"); - public static readonly Language EnglishNigeria = AddLanguage("en-NG", "English (Nigeria)", "English (Nigeria)"); - public static readonly Language EnglishNetherlands = AddLanguage("en-NL", "English (Netherlands)", "English (Netherlands)"); - public static readonly Language EnglishNauru = AddLanguage("en-NR", "English (Nauru)", "English (Nauru)"); - public static readonly Language EnglishNiue = AddLanguage("en-NU", "English (Niue)", "English (Niue)"); - public static readonly Language EnglishNewZealand = AddLanguage("en-NZ", "English (New Zealand)", "English (New Zealand)"); - public static readonly Language EnglishPapuaNewGuinea = AddLanguage("en-PG", "English (Papua New Guinea)", "English (Papua New Guinea)"); - public static readonly Language EnglishPhilippines = AddLanguage("en-PH", "English (Philippines)", "English (Philippines)"); - public static readonly Language EnglishPakistan = AddLanguage("en-PK", "English (Pakistan)", "English (Pakistan)"); - public static readonly Language EnglishPitcairnIslands = AddLanguage("en-PN", "English (Pitcairn Islands)", "English (Pitcairn Islands)"); - public static readonly Language EnglishPuertoRico = AddLanguage("en-PR", "English (Puerto Rico)", "English (Puerto Rico)"); - public static readonly Language EnglishPalau = AddLanguage("en-PW", "English (Palau)", "English (Palau)"); - public static readonly Language EnglishRwanda = AddLanguage("en-RW", "English (Rwanda)", "English (Rwanda)"); - public static readonly Language EnglishSolomonIslands = AddLanguage("en-SB", "English (Solomon Islands)", "English (Solomon Islands)"); - public static readonly Language EnglishSeychelles = AddLanguage("en-SC", "English (Seychelles)", "English (Seychelles)"); - public static readonly Language EnglishSudan = AddLanguage("en-SD", "English (Sudan)", "English (Sudan)"); - public static readonly Language EnglishSweden = AddLanguage("en-SE", "English (Sweden)", "English (Sweden)"); - public static readonly Language EnglishSingapore = AddLanguage("en-SG", "English (Singapore)", "English (Singapore)"); - public static readonly Language EnglishStHelenaAscensionTristandaCunha = AddLanguage("en-SH", "English (St Helena, Ascension, Tristan da Cunha)", "English (St Helena, Ascension, Tristan da Cunha)"); - public static readonly Language EnglishSlovenia = AddLanguage("en-SI", "English (Slovenia)", "English (Slovenia)"); - public static readonly Language EnglishSierraLeone = AddLanguage("en-SL", "English (Sierra Leone)", "English (Sierra Leone)"); - public static readonly Language EnglishSouthSudan = AddLanguage("en-SS", "English (South Sudan)", "English (South Sudan)"); - public static readonly Language EnglishSintMaarten = AddLanguage("en-SX", "English (Sint Maarten)", "English (Sint Maarten)"); - public static readonly Language EnglishEswatini = AddLanguage("en-SZ", "English (Eswatini)", "English (Eswatini)"); - public static readonly Language EnglishTurksCaicosIslands = AddLanguage("en-TC", "English (Turks & Caicos Islands)", "English (Turks & Caicos Islands)"); - public static readonly Language EnglishTokelau = AddLanguage("en-TK", "English (Tokelau)", "English (Tokelau)"); - public static readonly Language EnglishTonga = AddLanguage("en-TO", "English (Tonga)", "English (Tonga)"); - public static readonly Language EnglishTrinidadTobago = AddLanguage("en-TT", "English (Trinidad & Tobago)", "English (Trinidad & Tobago)"); - public static readonly Language EnglishTuvalu = AddLanguage("en-TV", "English (Tuvalu)", "English (Tuvalu)"); - public static readonly Language EnglishTanzania = AddLanguage("en-TZ", "English (Tanzania)", "English (Tanzania)"); - public static readonly Language EnglishUganda = AddLanguage("en-UG", "English (Uganda)", "English (Uganda)"); - public static readonly Language EnglishUSOutlyingIslands = AddLanguage("en-UM", "English (U.S. Outlying Islands)", "English (U.S. Outlying Islands)"); - public static readonly Language EnglishUnitedStates = AddLanguage("en-US", "English (United States)", "English (United States)"); - public static readonly Language EnglishStVincentGrenadines = AddLanguage("en-VC", "English (St. Vincent & Grenadines)", "English (St. Vincent & Grenadines)"); - public static readonly Language EnglishBritishVirginIslands = AddLanguage("en-VG", "English (British Virgin Islands)", "English (British Virgin Islands)"); - public static readonly Language EnglishUSVirginIslands = AddLanguage("en-VI", "English (U.S. Virgin Islands)", "English (U.S. Virgin Islands)"); - public static readonly Language EnglishVanuatu = AddLanguage("en-VU", "English (Vanuatu)", "English (Vanuatu)"); - public static readonly Language EnglishSamoa = AddLanguage("en-WS", "English (Samoa)", "English (Samoa)"); - public static readonly Language EnglishSouthAfrica = AddLanguage("en-ZA", "English (South Africa)", "English (South Africa)"); - public static readonly Language EnglishZambia = AddLanguage("en-ZM", "English (Zambia)", "English (Zambia)"); - public static readonly Language EnglishZimbabwe = AddLanguage("en-ZW", "English (Zimbabwe)", "English (Zimbabwe)"); - public static readonly Language SpanishArgentina = AddLanguage("es-AR", "Spanish (Argentina)", "español (Argentina)"); - public static readonly Language SpanishBolivia = AddLanguage("es-BO", "Spanish (Bolivia)", "español (Bolivia)"); - public static readonly Language SpanishBrazil = AddLanguage("es-BR", "Spanish (Brazil)", "español (Brasil)"); - public static readonly Language SpanishBelize = AddLanguage("es-BZ", "Spanish (Belize)", "español (Belice)"); - public static readonly Language SpanishChile = AddLanguage("es-CL", "Spanish (Chile)", "español (Chile)"); - public static readonly Language SpanishColombia = AddLanguage("es-CO", "Spanish (Colombia)", "español (Colombia)"); - public static readonly Language SpanishCostaRica = AddLanguage("es-CR", "Spanish (Costa Rica)", "español (Costa Rica)"); - public static readonly Language SpanishCuba = AddLanguage("es-CU", "Spanish (Cuba)", "español (Cuba)"); - public static readonly Language SpanishDominicanRepublic = AddLanguage("es-DO", "Spanish (Dominican Republic)", "español (República Dominicana)"); - public static readonly Language SpanishEcuador = AddLanguage("es-EC", "Spanish (Ecuador)", "español (Ecuador)"); - public static readonly Language SpanishSpain = AddLanguage("es-ES", "Spanish (Spain)", "español (España)"); - public static readonly Language SpanishEquatorialGuinea = AddLanguage("es-GQ", "Spanish (Equatorial Guinea)", "español (Guinea Ecuatorial)"); - public static readonly Language SpanishGuatemala = AddLanguage("es-GT", "Spanish (Guatemala)", "español (Guatemala)"); - public static readonly Language SpanishHonduras = AddLanguage("es-HN", "Spanish (Honduras)", "español (Honduras)"); - public static readonly Language SpanishMexico = AddLanguage("es-MX", "Spanish (Mexico)", "español (México)"); - public static readonly Language SpanishNicaragua = AddLanguage("es-NI", "Spanish (Nicaragua)", "español (Nicaragua)"); - public static readonly Language SpanishPanama = AddLanguage("es-PA", "Spanish (Panama)", "español (Panamá)"); - public static readonly Language SpanishPeru = AddLanguage("es-PE", "Spanish (Peru)", "español (Perú)"); - public static readonly Language SpanishPhilippines = AddLanguage("es-PH", "Spanish (Philippines)", "español (Filipinas)"); - public static readonly Language SpanishPuertoRico = AddLanguage("es-PR", "Spanish (Puerto Rico)", "español (Puerto Rico)"); - public static readonly Language SpanishParaguay = AddLanguage("es-PY", "Spanish (Paraguay)", "español (Paraguay)"); - public static readonly Language SpanishElSalvador = AddLanguage("es-SV", "Spanish (El Salvador)", "español (El Salvador)"); - public static readonly Language SpanishUnitedStates = AddLanguage("es-US", "Spanish (United States)", "español (Estados Unidos)"); - public static readonly Language SpanishUruguay = AddLanguage("es-UY", "Spanish (Uruguay)", "español (Uruguay)"); - public static readonly Language SpanishVenezuela = AddLanguage("es-VE", "Spanish (Venezuela)", "español (Venezuela)"); - public static readonly Language EstonianEstonia = AddLanguage("et-EE", "Estonian (Estonia)", "eesti (Eesti)"); - public static readonly Language BasqueSpain = AddLanguage("eu-ES", "Basque (Spain)", "euskara (Espainia)"); - public static readonly Language PersianAfghanistan = AddLanguage("fa-AF", "Persian (Afghanistan)", "فارسی (افغانستان)"); - public static readonly Language PersianIran = AddLanguage("fa-IR", "Persian (Iran)", "فارسی (ایران)"); - public static readonly Language FinnishFinland = AddLanguage("fi-FI", "Finnish (Finland)", "suomi (Suomi)"); - public static readonly Language FaroeseDenmark = AddLanguage("fo-DK", "Faroese (Denmark)", "føroyskt (Danmark)"); - public static readonly Language FaroeseFaroeIslands = AddLanguage("fo-FO", "Faroese (Faroe Islands)", "føroyskt (Føroyar)"); - public static readonly Language FrenchBelgium = AddLanguage("fr-BE", "French (Belgium)", "français (Belgique)"); - public static readonly Language FrenchBurkinaFaso = AddLanguage("fr-BF", "French (Burkina Faso)", "français (Burkina Faso)"); - public static readonly Language FrenchBurundi = AddLanguage("fr-BI", "French (Burundi)", "français (Burundi)"); - public static readonly Language FrenchBenin = AddLanguage("fr-BJ", "French (Benin)", "français (Bénin)"); - public static readonly Language FrenchStBarthélemy = AddLanguage("fr-BL", "French (St. Barthélemy)", "français (Saint-Barthélemy)"); - public static readonly Language FrenchCanada = AddLanguage("fr-CA", "French (Canada)", "français (Canada)"); - public static readonly Language FrenchCongoDRC = AddLanguage("fr-CD", "French (Congo [DRC])", "français (Congo [République démocratique du])"); - public static readonly Language FrenchCentralAfricanRepublic = AddLanguage("fr-CF", "French (Central African Republic)", "français (République centrafricaine)"); - public static readonly Language FrenchCongo = AddLanguage("fr-CG", "French (Congo)", "français (Congo)"); - public static readonly Language FrenchSwitzerland = AddLanguage("fr-CH", "French (Switzerland)", "français (Suisse)"); - public static readonly Language FrenchCôtedIvoire = AddLanguage("fr-CI", "French (Côte d’Ivoire)", "français (Côte d’Ivoire)"); - public static readonly Language FrenchCameroon = AddLanguage("fr-CM", "French (Cameroon)", "français (Cameroun)"); - public static readonly Language FrenchDjibouti = AddLanguage("fr-DJ", "French (Djibouti)", "français (Djibouti)"); - public static readonly Language FrenchAlgeria = AddLanguage("fr-DZ", "French (Algeria)", "français (Algérie)"); - public static readonly Language FrenchFrance = AddLanguage("fr-FR", "French (France)", "français (France)"); - public static readonly Language FrenchGabon = AddLanguage("fr-GA", "French (Gabon)", "français (Gabon)"); - public static readonly Language FrenchFrenchGuiana = AddLanguage("fr-GF", "French (French Guiana)", "français (Guyane française)"); - public static readonly Language FrenchGuinea = AddLanguage("fr-GN", "French (Guinea)", "français (Guinée)"); - public static readonly Language FrenchGuadeloupe = AddLanguage("fr-GP", "French (Guadeloupe)", "français (Guadeloupe)"); - public static readonly Language FrenchEquatorialGuinea = AddLanguage("fr-GQ", "French (Equatorial Guinea)", "français (Guinée équatoriale)"); - public static readonly Language FrenchHaiti = AddLanguage("fr-HT", "French (Haiti)", "français (Haïti)"); - public static readonly Language FrenchComoros = AddLanguage("fr-KM", "French (Comoros)", "français (Comores)"); - public static readonly Language FrenchLuxembourg = AddLanguage("fr-LU", "French (Luxembourg)", "français (Luxembourg)"); - public static readonly Language FrenchMorocco = AddLanguage("fr-MA", "French (Morocco)", "français (Maroc)"); - public static readonly Language FrenchMonaco = AddLanguage("fr-MC", "French (Monaco)", "français (Monaco)"); - public static readonly Language FrenchStMartin = AddLanguage("fr-MF", "French (St. Martin)", "français (Saint-Martin)"); - public static readonly Language FrenchMadagascar = AddLanguage("fr-MG", "French (Madagascar)", "français (Madagascar)"); - public static readonly Language FrenchMali = AddLanguage("fr-ML", "French (Mali)", "français (Mali)"); - public static readonly Language FrenchMartinique = AddLanguage("fr-MQ", "French (Martinique)", "français (Martinique)"); - public static readonly Language FrenchMauritania = AddLanguage("fr-MR", "French (Mauritania)", "français (Mauritanie)"); - public static readonly Language FrenchMauritius = AddLanguage("fr-MU", "French (Mauritius)", "français (Maurice)"); - public static readonly Language FrenchNewCaledonia = AddLanguage("fr-NC", "French (New Caledonia)", "français (Nouvelle-Calédonie)"); - public static readonly Language FrenchNiger = AddLanguage("fr-NE", "French (Niger)", "français (Niger)"); - public static readonly Language FrenchFrenchPolynesia = AddLanguage("fr-PF", "French (French Polynesia)", "français (Polynésie française)"); - public static readonly Language FrenchStPierreMiquelon = AddLanguage("fr-PM", "French (St. Pierre & Miquelon)", "français (Saint-Pierre-et-Miquelon)"); - public static readonly Language FrenchRéunion = AddLanguage("fr-RE", "French (Réunion)", "français (La Réunion)"); - public static readonly Language FrenchRwanda = AddLanguage("fr-RW", "French (Rwanda)", "français (Rwanda)"); - public static readonly Language FrenchSeychelles = AddLanguage("fr-SC", "French (Seychelles)", "français (Seychelles)"); - public static readonly Language FrenchSenegal = AddLanguage("fr-SN", "French (Senegal)", "français (Sénégal)"); - public static readonly Language FrenchSyria = AddLanguage("fr-SY", "French (Syria)", "français (Syrie)"); - public static readonly Language FrenchChad = AddLanguage("fr-TD", "French (Chad)", "français (Tchad)"); - public static readonly Language FrenchTogo = AddLanguage("fr-TG", "French (Togo)", "français (Togo)"); - public static readonly Language FrenchTunisia = AddLanguage("fr-TN", "French (Tunisia)", "français (Tunisie)"); - public static readonly Language FrenchVanuatu = AddLanguage("fr-VU", "French (Vanuatu)", "français (Vanuatu)"); - public static readonly Language FrenchWallisFutuna = AddLanguage("fr-WF", "French (Wallis & Futuna)", "français (Wallis-et-Futuna)"); - public static readonly Language FrenchMayotte = AddLanguage("fr-YT", "French (Mayotte)", "français (Mayotte)"); - public static readonly Language WesternFrisianNetherlands = AddLanguage("fy-NL", "Western Frisian (Netherlands)", "Frysk (Nederlân)"); - public static readonly Language IrishIreland = AddLanguage("ga-IE", "Irish (Ireland)", "Gaeilge (Éire)"); - public static readonly Language ScottishGaelicUnitedKingdom = AddLanguage("gd-GB", "Scottish Gaelic (United Kingdom)", "Gàidhlig (An Rìoghachd Aonaichte)"); - public static readonly Language GalicianSpain = AddLanguage("gl-ES", "Galician (Spain)", "galego (España)"); - public static readonly Language GuaraniParaguay = AddLanguage("gn-PY", "Guarani (Paraguay)", "Guarani (Paraguay)"); - public static readonly Language GujaratiIndia = AddLanguage("gu-IN", "Gujarati (India)", "ગુજરાતી (ભારત)"); - public static readonly Language ManxIsleofMan = AddLanguage("gv-IM", "Manx (Isle of Man)", "Gaelg (Ellan Vannin)"); - public static readonly Language HausaGhana = AddLanguage("ha-GH", "Hausa (Ghana)", "Hausa (Gana)"); - public static readonly Language HausaNiger = AddLanguage("ha-NE", "Hausa (Niger)", "Hausa (Nijar)"); - public static readonly Language HausaNigeria = AddLanguage("ha-NG", "Hausa (Nigeria)", "Hausa (Najeriya)"); - public static readonly Language HebrewIsrael = AddLanguage("he-IL", "Hebrew (Israel)", "עברית (ישראל)"); - public static readonly Language HindiIndia = AddLanguage("hi-IN", "Hindi (India)", "हिन्दी (भारत)"); - public static readonly Language CroatianBosniaHerzegovina = AddLanguage("hr-BA", "Croatian (Bosnia & Herzegovina)", "hrvatski (Bosna i Hercegovina)"); - public static readonly Language CroatianCroatia = AddLanguage("hr-HR", "Croatian (Croatia)", "hrvatski (Hrvatska)"); - public static readonly Language HungarianHungary = AddLanguage("hu-HU", "Hungarian (Hungary)", "magyar (Magyarország)"); - public static readonly Language ArmenianArmenia = AddLanguage("hy-AM", "Armenian (Armenia)", "հայերեն (Հայաստան)"); - public static readonly Language IndonesianIndonesia = AddLanguage("id-ID", "Indonesian (Indonesia)", "Indonesia (Indonesia)"); - public static readonly Language IgboNigeria = AddLanguage("ig-NG", "Igbo (Nigeria)", "Asụsụ Igbo (Naịjịrịa)"); - public static readonly Language YiChina = AddLanguage("ii-CN", "Yi (China)", "ꆈꌠꉙ (ꍏꇩ)"); - public static readonly Language IcelandicIceland = AddLanguage("is-IS", "Icelandic (Iceland)", "íslenska (Ísland)"); - public static readonly Language ItalianSwitzerland = AddLanguage("it-CH", "Italian (Switzerland)", "italiano (Svizzera)"); - public static readonly Language ItalianItaly = AddLanguage("it-IT", "Italian (Italy)", "italiano (Italia)"); - public static readonly Language ItalianSanMarino = AddLanguage("it-SM", "Italian (San Marino)", "italiano (San Marino)"); - public static readonly Language ItalianVaticanCity = AddLanguage("it-VA", "Italian (Vatican City)", "italiano (Città del Vaticano)"); - public static readonly Language InuktitutCanada = AddLanguage("iu-CA", "Inuktitut (Canada)", "Inuktitut (Canada)"); - public static readonly Language JapaneseJapan = AddLanguage("ja-JP", "Japanese (Japan)", "日本語 (日本)"); - public static readonly Language JavaneseIndonesia = AddLanguage("jv-ID", "Javanese (Indonesia)", "Jawa (Indonésia)"); - public static readonly Language GeorgianGeorgia = AddLanguage("ka-GE", "Georgian (Georgia)", "ქართული (საქართველო)"); - public static readonly Language KikuyuKenya = AddLanguage("ki-KE", "Kikuyu (Kenya)", "Gikuyu (Kenya)"); - public static readonly Language KazakhKazakhstan = AddLanguage("kk-KZ", "Kazakh (Kazakhstan)", "қазақ тілі (Қазақстан)"); - public static readonly Language KalaallisutGreenland = AddLanguage("kl-GL", "Kalaallisut (Greenland)", "kalaallisut (Kalaallit Nunaat)"); - public static readonly Language KhmerCambodia = AddLanguage("km-KH", "Khmer (Cambodia)", "ខ្មែរ (កម្ពុជា)"); - public static readonly Language KannadaIndia = AddLanguage("kn-IN", "Kannada (India)", "ಕನ್ನಡ (ಭಾರತ)"); - public static readonly Language KoreanNorthKorea = AddLanguage("ko-KP", "Korean (North Korea)", "한국어(조선민주주의인민공화국)"); - public static readonly Language KoreanKorea = AddLanguage("ko-KR", "Korean (Korea)", "한국어(대한민국)"); - public static readonly Language KashmiriIndia = AddLanguage("ks-IN", "Kashmiri (India)", "کٲشُر (ہِندوستان)"); - public static readonly Language CornishUnitedKingdom = AddLanguage("kw-GB", "Cornish (United Kingdom)", "kernewek (Rywvaneth Unys)"); - public static readonly Language KyrgyzKyrgyzstan = AddLanguage("ky-KG", "Kyrgyz (Kyrgyzstan)", "кыргызча (Кыргызстан)"); - public static readonly Language LuxembourgishLuxembourg = AddLanguage("lb-LU", "Luxembourgish (Luxembourg)", "Lëtzebuergesch (Lëtzebuerg)"); - public static readonly Language GandaUganda = AddLanguage("lg-UG", "Ganda (Uganda)", "Luganda (Yuganda)"); - public static readonly Language LingalaAngola = AddLanguage("ln-AO", "Lingala (Angola)", "lingála (Angóla)"); - public static readonly Language LingalaCongoDRC = AddLanguage("ln-CD", "Lingala (Congo [DRC])", "lingála (Republíki ya Kongó Demokratíki)"); - public static readonly Language LingalaCentralAfricanRepublic = AddLanguage("ln-CF", "Lingala (Central African Republic)", "lingála (Repibiki ya Afríka ya Káti)"); - public static readonly Language LingalaCongo = AddLanguage("ln-CG", "Lingala (Congo)", "lingála (Kongo)"); - public static readonly Language LaoLaos = AddLanguage("lo-LA", "Lao (Laos)", "ລາວ (ລາວ)"); - public static readonly Language LithuanianLithuania = AddLanguage("lt-LT", "Lithuanian (Lithuania)", "lietuvių (Lietuva)"); - public static readonly Language LubaKatangaCongoDRC = AddLanguage("lu-CD", "Luba-Katanga (Congo [DRC])", "Tshiluba (Ditunga wa Kongu)"); - public static readonly Language LatvianLatvia = AddLanguage("lv-LV", "Latvian (Latvia)", "latviešu (Latvija)"); - public static readonly Language MalagasyMadagascar = AddLanguage("mg-MG", "Malagasy (Madagascar)", "Malagasy (Madagasikara)"); - public static readonly Language MaoriNewZealand = AddLanguage("mi-NZ", "Maori (New Zealand)", "Māori (Aotearoa)"); - public static readonly Language MacedonianNorthMacedonia = AddLanguage("mk-MK", "Macedonian (North Macedonia)", "македонски (Северна Македонија)"); - public static readonly Language MalayalamIndia = AddLanguage("ml-IN", "Malayalam (India)", "മലയാളം (ഇന്ത്യ)"); - public static readonly Language MongolianMongolia = AddLanguage("mn-MN", "Mongolian (Mongolia)", "монгол (Монгол)"); - public static readonly Language MarathiIndia = AddLanguage("mr-IN", "Marathi (India)", "मराठी (भारत)"); - public static readonly Language MalayBrunei = AddLanguage("ms-BN", "Malay (Brunei)", "Melayu (Brunei)"); - public static readonly Language MalayMalaysia = AddLanguage("ms-MY", "Malay (Malaysia)", "Melayu (Malaysia)"); - public static readonly Language MalaySingapore = AddLanguage("ms-SG", "Malay (Singapore)", "Melayu (Singapura)"); - public static readonly Language MalteseMalta = AddLanguage("mt-MT", "Maltese (Malta)", "Malti (Malta)"); - public static readonly Language BurmeseMyanmar = AddLanguage("my-MM", "Burmese (Myanmar)", "မြန်မာ (မြန်မာ)"); - public static readonly Language NorwegianBokmålNorway = AddLanguage("nb-NO", "Norwegian Bokmål (Norway)", "norsk bokmål (Norge)"); - public static readonly Language NorwegianBokmålSvalbardJanMayen = AddLanguage("nb-SJ", "Norwegian Bokmål (Svalbard & Jan Mayen)", "norsk bokmål (Svalbard og Jan Mayen)"); - public static readonly Language NorthNdebeleZimbabwe = AddLanguage("nd-ZW", "North Ndebele (Zimbabwe)", "isiNdebele (Zimbabwe)"); - public static readonly Language NepaliIndia = AddLanguage("ne-IN", "Nepali (India)", "नेपाली (भारत)"); - public static readonly Language NepaliNepal = AddLanguage("ne-NP", "Nepali (Nepal)", "नेपाली (नेपाल)"); - public static readonly Language DutchAruba = AddLanguage("nl-AW", "Dutch (Aruba)", "Nederlands (Aruba)"); - public static readonly Language DutchBelgium = AddLanguage("nl-BE", "Dutch (Belgium)", "Nederlands (België)"); - public static readonly Language DutchBonaireSintEustatiusandSaba = AddLanguage("nl-BQ", "Dutch (Bonaire, Sint Eustatius and Saba)", "Nederlands (Bonaire, Sint Eustatius en Saba)"); - public static readonly Language DutchCuraçao = AddLanguage("nl-CW", "Dutch (Curaçao)", "Nederlands (Curaçao)"); - public static readonly Language DutchNetherlands = AddLanguage("nl-NL", "Dutch (Netherlands)", "Nederlands (Nederland)"); - public static readonly Language DutchSuriname = AddLanguage("nl-SR", "Dutch (Suriname)", "Nederlands (Suriname)"); - public static readonly Language DutchSintMaarten = AddLanguage("nl-SX", "Dutch (Sint Maarten)", "Nederlands (Sint-Maarten)"); - public static readonly Language NorwegianNynorskNorway = AddLanguage("nn-NO", "Norwegian Nynorsk (Norway)", "nynorsk (Noreg)"); - public static readonly Language SouthNdebeleSouthAfrica = AddLanguage("nr-ZA", "South Ndebele (South Africa)", "South Ndebele (South Africa)"); - public static readonly Language OccitanFrance = AddLanguage("oc-FR", "Occitan (France)", "Occitan (France)"); - public static readonly Language OromoEthiopia = AddLanguage("om-ET", "Oromo (Ethiopia)", "Oromoo (Itoophiyaa)"); - public static readonly Language OromoKenya = AddLanguage("om-KE", "Oromo (Kenya)", "Oromoo (Keeniyaa)"); - public static readonly Language OdiaIndia = AddLanguage("or-IN", "Odia (India)", "ଓଡ଼ିଆ (ଭାରତ)"); - public static readonly Language OsseticGeorgia = AddLanguage("os-GE", "Ossetic (Georgia)", "ирон (Гуырдзыстон)"); - public static readonly Language OsseticRussia = AddLanguage("os-RU", "Ossetic (Russia)", "ирон (Уӕрӕсе)"); - public static readonly Language PolishPoland = AddLanguage("pl-PL", "Polish (Poland)", "polski (Polska)"); - public static readonly Language PashtoAfghanistan = AddLanguage("ps-AF", "Pashto (Afghanistan)", "پښتو (افغانستان)"); - public static readonly Language PashtoPakistan = AddLanguage("ps-PK", "Pashto (Pakistan)", "پښتو (پاکستان)"); - public static readonly Language PortugueseAngola = AddLanguage("pt-AO", "Portuguese (Angola)", "português (Angola)"); - public static readonly Language PortugueseBrazil = AddLanguage("pt-BR", "Portuguese (Brazil)", "português (Brasil)"); - public static readonly Language PortugueseSwitzerland = AddLanguage("pt-CH", "Portuguese (Switzerland)", "português (Suíça)"); - public static readonly Language PortugueseCaboVerde = AddLanguage("pt-CV", "Portuguese (Cabo Verde)", "português (Cabo Verde)"); - public static readonly Language PortugueseEquatorialGuinea = AddLanguage("pt-GQ", "Portuguese (Equatorial Guinea)", "português (Guiné Equatorial)"); - public static readonly Language PortugueseGuineaBissau = AddLanguage("pt-GW", "Portuguese (Guinea-Bissau)", "português (Guiné-Bissau)"); - public static readonly Language PortugueseLuxembourg = AddLanguage("pt-LU", "Portuguese (Luxembourg)", "português (Luxemburgo)"); - public static readonly Language PortugueseMacaoSAR = AddLanguage("pt-MO", "Portuguese (Macao SAR)", "português (RAE de Macau)"); - public static readonly Language PortugueseMozambique = AddLanguage("pt-MZ", "Portuguese (Mozambique)", "português (Moçambique)"); - public static readonly Language PortuguesePortugal = AddLanguage("pt-PT", "Portuguese (Portugal)", "português (Portugal)"); - public static readonly Language PortugueseSãoToméPríncipe = AddLanguage("pt-ST", "Portuguese (São Tomé & Príncipe)", "português (São Tomé e Príncipe)"); - public static readonly Language PortugueseTimorLeste = AddLanguage("pt-TL", "Portuguese (Timor-Leste)", "português (Timor-Leste)"); - public static readonly Language QuechuaBolivia = AddLanguage("qu-BO", "Quechua (Bolivia)", "Runasimi (Bolivia)"); - public static readonly Language QuechuaEcuador = AddLanguage("qu-EC", "Quechua (Ecuador)", "Runasimi (Ecuador)"); - public static readonly Language QuechuaPeru = AddLanguage("qu-PE", "Quechua (Peru)", "Runasimi (Perú)"); - public static readonly Language RomanshSwitzerland = AddLanguage("rm-CH", "Romansh (Switzerland)", "rumantsch (Svizra)"); - public static readonly Language RundiBurundi = AddLanguage("rn-BI", "Rundi (Burundi)", "Ikirundi (Uburundi)"); - public static readonly Language RomanianMoldova = AddLanguage("ro-MD", "Romanian (Moldova)", "română (Republica Moldova)"); - public static readonly Language RomanianRomania = AddLanguage("ro-RO", "Romanian (Romania)", "română (România)"); - public static readonly Language RussianBelarus = AddLanguage("ru-BY", "Russian (Belarus)", "русский (Беларусь)"); - public static readonly Language RussianKyrgyzstan = AddLanguage("ru-KG", "Russian (Kyrgyzstan)", "русский (Киргизия)"); - public static readonly Language RussianKazakhstan = AddLanguage("ru-KZ", "Russian (Kazakhstan)", "русский (Казахстан)"); - public static readonly Language RussianMoldova = AddLanguage("ru-MD", "Russian (Moldova)", "русский (Молдова)"); - public static readonly Language RussianRussia = AddLanguage("ru-RU", "Russian (Russia)", "русский (Россия)"); - public static readonly Language RussianUkraine = AddLanguage("ru-UA", "Russian (Ukraine)", "русский (Украина)"); - public static readonly Language KinyarwandaRwanda = AddLanguage("rw-RW", "Kinyarwanda (Rwanda)", "Kinyarwanda (U Rwanda)"); - public static readonly Language SanskritIndia = AddLanguage("sa-IN", "Sanskrit (India)", "Sanskrit (India)"); - public static readonly Language SindhiPakistan = AddLanguage("sd-PK", "Sindhi (Pakistan)", "سنڌي (پاڪستان)"); - public static readonly Language NorthernSamiFinland = AddLanguage("se-FI", "Northern Sami (Finland)", "davvisámegiella (Suopma)"); - public static readonly Language NorthernSamiNorway = AddLanguage("se-NO", "Northern Sami (Norway)", "davvisámegiella (Norga)"); - public static readonly Language NorthernSamiSweden = AddLanguage("se-SE", "Northern Sami (Sweden)", "davvisámegiella (Ruoŧŧa)"); - public static readonly Language SangoCentralAfricanRepublic = AddLanguage("sg-CF", "Sango (Central African Republic)", "Sängö (Ködörösêse tî Bêafrîka)"); - public static readonly Language SinhalaSriLanka = AddLanguage("si-LK", "Sinhala (Sri Lanka)", "සිංහල (ශ්‍රී ලංකාව)"); - public static readonly Language SlovakSlovakia = AddLanguage("sk-SK", "Slovak (Slovakia)", "slovenčina (Slovensko)"); - public static readonly Language SlovenianSlovenia = AddLanguage("sl-SI", "Slovenian (Slovenia)", "slovenščina (Slovenija)"); - public static readonly Language ShonaZimbabwe = AddLanguage("sn-ZW", "Shona (Zimbabwe)", "chiShona (Zimbabwe)"); - public static readonly Language SomaliDjibouti = AddLanguage("so-DJ", "Somali (Djibouti)", "Soomaali (Jabuuti)"); - public static readonly Language SomaliEthiopia = AddLanguage("so-ET", "Somali (Ethiopia)", "Soomaali (Itoobiya)"); - public static readonly Language SomaliKenya = AddLanguage("so-KE", "Somali (Kenya)", "Soomaali (Kenya)"); - public static readonly Language SomaliSomalia = AddLanguage("so-SO", "Somali (Somalia)", "Soomaali (Soomaaliya)"); - public static readonly Language AlbanianAlbania = AddLanguage("sq-AL", "Albanian (Albania)", "shqip (Shqipëri)"); - public static readonly Language AlbanianNorthMacedonia = AddLanguage("sq-MK", "Albanian (North Macedonia)", "shqip (Maqedonia e Veriut)"); - public static readonly Language AlbanianKosovo = AddLanguage("sq-XK", "Albanian (Kosovo)", "shqip (Kosovë)"); - public static readonly Language siSwatiEswatini = AddLanguage("ss-SZ", "siSwati (Eswatini)", "siSwati (eSwatini)"); - public static readonly Language siSwatiSouthAfrica = AddLanguage("ss-ZA", "siSwati (South Africa)", "siSwati (South Africa)"); - public static readonly Language SesothoLesotho = AddLanguage("st-LS", "Sesotho (Lesotho)", "Sesotho (Lesotho)"); - public static readonly Language SesothoSouthAfrica = AddLanguage("st-ZA", "Sesotho (South Africa)", "Sesotho (South Africa)"); - public static readonly Language SwedishÅlandIslands = AddLanguage("sv-AX", "Swedish (Åland Islands)", "svenska (Åland)"); - public static readonly Language SwedishFinland = AddLanguage("sv-FI", "Swedish (Finland)", "svenska (Finland)"); - public static readonly Language SwedishSweden = AddLanguage("sv-SE", "Swedish (Sweden)", "svenska (Sverige)"); - public static readonly Language KiswahiliCongoDRC = AddLanguage("sw-CD", "Kiswahili (Congo [DRC])", "Kiswahili (Jamhuri ya Kidemokrasia ya Kongo)"); - public static readonly Language KiswahiliKenya = AddLanguage("sw-KE", "Kiswahili (Kenya)", "Kiswahili (Kenya)"); - public static readonly Language KiswahiliTanzania = AddLanguage("sw-TZ", "Kiswahili (Tanzania)", "Kiswahili (Tanzania)"); - public static readonly Language KiswahiliUganda = AddLanguage("sw-UG", "Kiswahili (Uganda)", "Kiswahili (Uganda)"); - public static readonly Language TamilIndia = AddLanguage("ta-IN", "Tamil (India)", "தமிழ் (இந்தியா)"); - public static readonly Language TamilSriLanka = AddLanguage("ta-LK", "Tamil (Sri Lanka)", "தமிழ் (இலங்கை)"); - public static readonly Language TamilMalaysia = AddLanguage("ta-MY", "Tamil (Malaysia)", "தமிழ் (மலேசியா)"); - public static readonly Language TamilSingapore = AddLanguage("ta-SG", "Tamil (Singapore)", "தமிழ் (சிங்கப்பூர்)"); - public static readonly Language TeluguIndia = AddLanguage("te-IN", "Telugu (India)", "తెలుగు (భారతదేశం)"); - public static readonly Language TajikTajikistan = AddLanguage("tg-TJ", "Tajik (Tajikistan)", "тоҷикӣ (Тоҷикистон)"); - public static readonly Language ThaiThailand = AddLanguage("th-TH", "Thai (Thailand)", "ไทย (ไทย)"); - public static readonly Language TigrinyaEritrea = AddLanguage("ti-ER", "Tigrinya (Eritrea)", "ትግርኛ (ኤርትራ)"); - public static readonly Language TigrinyaEthiopia = AddLanguage("ti-ET", "Tigrinya (Ethiopia)", "ትግርኛ (ኢትዮጵያ)"); - public static readonly Language TurkmenTurkmenistan = AddLanguage("tk-TM", "Turkmen (Turkmenistan)", "türkmen dili (Türkmenistan)"); - public static readonly Language SetswanaBotswana = AddLanguage("tn-BW", "Setswana (Botswana)", "Setswana (Botswana)"); - public static readonly Language SetswanaSouthAfrica = AddLanguage("tn-ZA", "Setswana (South Africa)", "Setswana (South Africa)"); - public static readonly Language TonganTonga = AddLanguage("to-TO", "Tongan (Tonga)", "lea fakatonga (Tonga)"); - public static readonly Language TurkishCyprus = AddLanguage("tr-CY", "Turkish (Cyprus)", "Türkçe (Kıbrıs)"); - public static readonly Language TurkishTurkey = AddLanguage("tr-TR", "Turkish (Turkey)", "Türkçe (Türkiye)"); - public static readonly Language XitsongaSouthAfrica = AddLanguage("ts-ZA", "Xitsonga (South Africa)", "Xitsonga (South Africa)"); - public static readonly Language TatarRussia = AddLanguage("tt-RU", "Tatar (Russia)", "татар (Россия)"); - public static readonly Language UyghurChina = AddLanguage("ug-CN", "Uyghur (China)", "ئۇيغۇرچە (جۇڭگو)"); - public static readonly Language UkrainianUkraine = AddLanguage("uk-UA", "Ukrainian (Ukraine)", "українська (Україна)"); - public static readonly Language UrduIndia = AddLanguage("ur-IN", "Urdu (India)", "اردو (بھارت)"); - public static readonly Language UrduPakistan = AddLanguage("ur-PK", "Urdu (Pakistan)", "اردو (پاکستان)"); - public static readonly Language VendaSouthAfrica = AddLanguage("ve-ZA", "Venda (South Africa)", "Venda (South Africa)"); - public static readonly Language VietnameseVietnam = AddLanguage("vi-VN", "Vietnamese (Vietnam)", "Tiếng Việt (Việt Nam)"); - public static readonly Language WolofSenegal = AddLanguage("wo-SN", "Wolof (Senegal)", "Wolof (Senegaal)"); - public static readonly Language isiXhosaSouthAfrica = AddLanguage("xh-ZA", "isiXhosa (South Africa)", "isiXhosa (eMzantsi Afrika)"); - public static readonly Language YorubaBenin = AddLanguage("yo-BJ", "Yoruba (Benin)", "Èdè Yorùbá (Orílɛ́ède Bɛ̀nɛ̀)"); - public static readonly Language YorubaNigeria = AddLanguage("yo-NG", "Yoruba (Nigeria)", "Èdè Yorùbá (Orilẹ̀-èdè Nàìjíríà)"); - public static readonly Language isiZuluSouthAfrica = AddLanguage("zu-ZA", "isiZulu (South Africa)", "isiZulu (iNingizimu Afrika)"); + return LanguageByCode.GetOrAdd(iso2Code, code => new Language(code)); } + + public static readonly Language AA = AddLanguage("aa", "Afar", "Afaraf"); + public static readonly Language AB = AddLanguage("ab", "Abkhaz", "аҧсуа бызшәа, аҧсшәа"); + public static readonly Language AE = AddLanguage("ae", "Avestan", "avesta"); + public static readonly Language AF = AddLanguage("af", "Afrikaans", "Afrikaans"); + public static readonly Language AK = AddLanguage("ak", "Akan", "Akan"); + public static readonly Language AM = AddLanguage("am", "Amharic", "አማርኛ"); + public static readonly Language AN = AddLanguage("an", "Aragonese", "aragonés"); + public static readonly Language AR = AddLanguage("ar", "Arabic", "العربية"); + public static readonly Language AS = AddLanguage("as", "Assamese", "অসমীয়া"); + public static readonly Language AV = AddLanguage("av", "Avaric", "авар мацӀ, магӀарул мацӀ"); + public static readonly Language AY = AddLanguage("ay", "Aymara", "aymar aru"); + public static readonly Language AZ = AddLanguage("az", "Azerbaijani", "azərbaycan dili"); + public static readonly Language BA = AddLanguage("ba", "Bashkir", "башҡорт теле"); + public static readonly Language BE = AddLanguage("be", "Belarusian", "беларуская мова"); + public static readonly Language BG = AddLanguage("bg", "Bulgarian", "български език"); + public static readonly Language BH = AddLanguage("bh", "Bihari", "भोजपुरी"); + public static readonly Language BI = AddLanguage("bi", "Bislama", "Bislama"); + public static readonly Language BM = AddLanguage("bm", "Bambara", "bamanankan"); + public static readonly Language BN = AddLanguage("bn", "Bengali, Bangla", "বাংলা"); + public static readonly Language BO = AddLanguage("bo", "Tibetan Standard, Tibetan, Central", "བོད་ཡིག"); + public static readonly Language BR = AddLanguage("br", "Breton", "brezhoneg"); + public static readonly Language BS = AddLanguage("bs", "Bosnian", "bosanski jezik"); + public static readonly Language CA = AddLanguage("ca", "Catalan", "català"); + public static readonly Language CE = AddLanguage("ce", "Chechen", "нохчийн мотт"); + public static readonly Language CH = AddLanguage("ch", "Chamorro", "Chamoru"); + public static readonly Language CO = AddLanguage("co", "Corsican", "corsu, lingua corsa"); + public static readonly Language CR = AddLanguage("cr", "Cree", "ᓀᐦᐃᔭᐍᐏᐣ"); + public static readonly Language CS = AddLanguage("cs", "Czech", "čeština, český jazyk"); + public static readonly Language CU = AddLanguage("cu", "Old Church Slavonic, Church Slavonic, Old Bulgarian", "ѩзыкъ словѣньскъ"); + public static readonly Language CV = AddLanguage("cv", "Chuvash", "чӑваш чӗлхи"); + public static readonly Language CY = AddLanguage("cy", "Welsh", "Cymraeg"); + public static readonly Language DA = AddLanguage("da", "Danish", "dansk"); + public static readonly Language DE = AddLanguage("de", "German", "Deutsch"); + public static readonly Language DV = AddLanguage("dv", "Divehi, Dhivehi, Maldivian", "ދިވެހި"); + public static readonly Language DZ = AddLanguage("dz", "Dzongkha", "རྫོང་ཁ"); + public static readonly Language EE = AddLanguage("ee", "Ewe", "Eʋegbe"); + public static readonly Language EL = AddLanguage("el", "Greek (modern)", "ελληνικά"); + public static readonly Language EN = AddLanguage("en", "English", "English"); + public static readonly Language EO = AddLanguage("eo", "Esperanto", "Esperanto"); + public static readonly Language ES = AddLanguage("es", "Spanish", "Español"); + public static readonly Language ET = AddLanguage("et", "Estonian", "eesti, eesti keel"); + public static readonly Language EU = AddLanguage("eu", "Basque", "euskara, euskera"); + public static readonly Language FA = AddLanguage("fa", "Persian (Farsi)", "فارسی"); + public static readonly Language FF = AddLanguage("ff", "Fula, Fulah, Pulaar, Pular", "Fulfulde, Pulaar, Pular"); + public static readonly Language FI = AddLanguage("fi", "Finnish", "suomi, suomen kieli"); + public static readonly Language FJ = AddLanguage("fj", "Fijian", "vosa Vakaviti"); + public static readonly Language FO = AddLanguage("fo", "Faroese", "føroyskt"); + public static readonly Language FR = AddLanguage("fr", "French", "français, langue française"); + public static readonly Language FY = AddLanguage("fy", "Western Frisian", "Frysk"); + public static readonly Language GA = AddLanguage("ga", "Irish", "Gaeilge"); + public static readonly Language GD = AddLanguage("gd", "Scottish Gaelic, Gaelic", "Gàidhlig"); + public static readonly Language GL = AddLanguage("gl", "Galician", "galego"); + public static readonly Language GN = AddLanguage("gn", "Guaraní", "Avañe'ẽ"); + public static readonly Language GU = AddLanguage("gu", "Gujarati", "ગુજરાતી"); + public static readonly Language GV = AddLanguage("gv", "Manx", "Gaelg, Gailck"); + public static readonly Language HA = AddLanguage("ha", "Hausa", "(Hausa) هَوُسَ"); + public static readonly Language HE = AddLanguage("he", "Hebrew (modern)", "עברית"); + public static readonly Language HI = AddLanguage("hi", "Hindi", "हिन्दी, हिंदी"); + public static readonly Language HO = AddLanguage("ho", "Hiri Motu", "Hiri Motu"); + public static readonly Language HR = AddLanguage("hr", "Croatian", "hrvatski jezik"); + public static readonly Language HT = AddLanguage("ht", "Haitian, Haitian Creole", "Kreyòl ayisyen"); + public static readonly Language HU = AddLanguage("hu", "Hungarian", "magyar"); + public static readonly Language HY = AddLanguage("hy", "Armenian", "Հայերեն"); + public static readonly Language HZ = AddLanguage("hz", "Herero", "Otjiherero"); + public static readonly Language IA = AddLanguage("ia", "Interlingua", "Interlingua"); + public static readonly Language ID = AddLanguage("id", "Indonesian", "Bahasa Indonesia"); + public static readonly Language IE = AddLanguage("ie", "Interlingue", "Originally called Occidental; then Interlingue after WWII"); + public static readonly Language IG = AddLanguage("ig", "Igbo", "Asụsụ Igbo"); + public static readonly Language II = AddLanguage("ii", "Nuosu", "ꆈꌠ꒿ Nuosuhxop"); + public static readonly Language IK = AddLanguage("ik", "Inupiaq", "Iñupiaq, Iñupiatun"); + public static readonly Language IO = AddLanguage("io", "Ido", "Ido"); + public static readonly Language IS = AddLanguage("is", "Icelandic", "Íslenska"); + public static readonly Language IT = AddLanguage("it", "Italian", "Italiano"); + public static readonly Language IU = AddLanguage("iu", "Inuktitut", "ᐃᓄᒃᑎᑐᑦ"); + public static readonly Language JA = AddLanguage("ja", "Japanese", "日本語 (にほんご)"); + public static readonly Language JV = AddLanguage("jv", "Javanese", "ꦧꦱꦗꦮ, Basa Jawa"); + public static readonly Language KA = AddLanguage("ka", "Georgian", "ქართული"); + public static readonly Language KG = AddLanguage("kg", "Kongo", "Kikongo"); + public static readonly Language KI = AddLanguage("ki", "Kikuyu, Gikuyu", "Gĩkũyũ"); + public static readonly Language KJ = AddLanguage("kj", "Kwanyama, Kuanyama", "Kuanyama"); + public static readonly Language KK = AddLanguage("kk", "Kazakh", "қазақ тілі"); + public static readonly Language KL = AddLanguage("kl", "Kalaallisut, Greenlandic", "kalaallisut, kalaallit oqaasii"); + public static readonly Language KM = AddLanguage("km", "Khmer", "ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ"); + public static readonly Language KN = AddLanguage("kn", "Kannada", "ಕನ್ನಡ"); + public static readonly Language KO = AddLanguage("ko", "Korean", "한국어"); + public static readonly Language KR = AddLanguage("kr", "Kanuri", "Kanuri"); + public static readonly Language KS = AddLanguage("ks", "Kashmiri", "कश्मीरी, كشميري‎"); + public static readonly Language KU = AddLanguage("ku", "Kurdish", "Kurdî, كوردی‎"); + public static readonly Language KV = AddLanguage("kv", "Komi", "коми кыв"); + public static readonly Language KW = AddLanguage("kw", "Cornish", "Kernewek"); + public static readonly Language KY = AddLanguage("ky", "Kyrgyz", "Кыргызча, Кыргыз тили"); + public static readonly Language LA = AddLanguage("la", "Latin", "latine, lingua latina"); + public static readonly Language LB = AddLanguage("lb", "Luxembourgish, Letzeburgesch", "Lëtzebuergesch"); + public static readonly Language LG = AddLanguage("lg", "Ganda", "Luganda"); + public static readonly Language LI = AddLanguage("li", "Limburgish, Limburgan, Limburger", "Limburgs"); + public static readonly Language LN = AddLanguage("ln", "Lingala", "Lingála"); + public static readonly Language LO = AddLanguage("lo", "Lao", "ພາສາລາວ"); + public static readonly Language LT = AddLanguage("lt", "Lithuanian", "lietuvių kalba"); + public static readonly Language LU = AddLanguage("lu", "Luba-Katanga", "Tshiluba"); + public static readonly Language LV = AddLanguage("lv", "Latvian", "latviešu valoda"); + public static readonly Language MG = AddLanguage("mg", "Malagasy", "fiteny malagasy"); + public static readonly Language MH = AddLanguage("mh", "Marshallese", "Kajin M̧ajeļ"); + public static readonly Language MI = AddLanguage("mi", "Māori", "te reo Māori"); + public static readonly Language MK = AddLanguage("mk", "Macedonian", "македонски јазик"); + public static readonly Language ML = AddLanguage("ml", "Malayalam", "മലയാളം"); + public static readonly Language MN = AddLanguage("mn", "Mongolian", "Монгол хэл"); + public static readonly Language MR = AddLanguage("mr", "Marathi (Marāṭhī)", "मराठी"); + public static readonly Language MS = AddLanguage("ms", "Malay", "bahasa Melayu, بهاس ملايو‎"); + public static readonly Language MT = AddLanguage("mt", "Maltese", "Malti"); + public static readonly Language MY = AddLanguage("my", "Burmese", "ဗမာစာ"); + public static readonly Language NA = AddLanguage("na", "Nauruan", "Dorerin Naoero"); + public static readonly Language NB = AddLanguage("nb", "Norwegian Bokmål", "Norsk bokmål"); + public static readonly Language ND = AddLanguage("nd", "Northern Ndebele", "isiNdebele"); + public static readonly Language NE = AddLanguage("ne", "Nepali", "नेपाली"); + public static readonly Language NG = AddLanguage("ng", "Ndonga", "Owambo"); + public static readonly Language NL = AddLanguage("nl", "Dutch", "Nederlands, Vlaams"); + public static readonly Language NN = AddLanguage("nn", "Norwegian Nynorsk", "Norsk nynorsk"); + public static readonly Language NO = AddLanguage("no", "Norwegian", "Norsk"); + public static readonly Language NR = AddLanguage("nr", "Southern Ndebele", "isiNdebele"); + public static readonly Language NV = AddLanguage("nv", "Navajo, Navaho", "Diné bizaad"); + public static readonly Language NY = AddLanguage("ny", "Chichewa, Chewa, Nyanja", "chiCheŵa, chinyanja"); + public static readonly Language OC = AddLanguage("oc", "Occitan", "occitan, lenga d'òc"); + public static readonly Language OJ = AddLanguage("oj", "Ojibwe, Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ"); + public static readonly Language OM = AddLanguage("om", "Oromo", "Afaan Oromoo"); + public static readonly Language OR = AddLanguage("or", "Oriya", "ଓଡ଼ିଆ"); + public static readonly Language OS = AddLanguage("os", "Ossetian, Ossetic", "ирон æвзаг"); + public static readonly Language PA = AddLanguage("pa", "(Eastern) Punjabi", "ਪੰਜਾਬੀ"); + public static readonly Language PI = AddLanguage("pi", "Pāli", "पाऴि"); + public static readonly Language PL = AddLanguage("pl", "Polish", "język polski, polszczyzna"); + public static readonly Language PS = AddLanguage("ps", "Pashto, Pushto", "پښتو"); + public static readonly Language PT = AddLanguage("pt", "Portuguese", "Português"); + public static readonly Language QU = AddLanguage("qu", "Quechua", "Runa Simi, Kichwa"); + public static readonly Language RM = AddLanguage("rm", "Romansh", "rumantsch grischun"); + public static readonly Language RN = AddLanguage("rn", "Kirundi", "Ikirundi"); + public static readonly Language RO = AddLanguage("ro", "Romanian", "Română"); + public static readonly Language RU = AddLanguage("ru", "Russian", "Русский"); + public static readonly Language RW = AddLanguage("rw", "Kinyarwanda", "Ikinyarwanda"); + public static readonly Language SA = AddLanguage("sa", "Sanskrit (Saṁskṛta)", "संस्कृतम्"); + public static readonly Language SC = AddLanguage("sc", "Sardinian", "sardu"); + public static readonly Language SD = AddLanguage("sd", "Sindhi", "सिन्धी, سنڌي، سندھی‎"); + public static readonly Language SE = AddLanguage("se", "Northern Sami", "Davvisámegiella"); + public static readonly Language SG = AddLanguage("sg", "Sango", "yângâ tî sängö"); + public static readonly Language SI = AddLanguage("si", "Sinhalese, Sinhala", "සිංහල"); + public static readonly Language SK = AddLanguage("sk", "Slovak", "slovenčina, slovenský jazyk"); + public static readonly Language SL = AddLanguage("sl", "Slovene", "slovenski jezik, slovenščina"); + public static readonly Language SM = AddLanguage("sm", "Samoan", "gagana fa'a Samoa"); + public static readonly Language SN = AddLanguage("sn", "Shona", "chiShona"); + public static readonly Language SO = AddLanguage("so", "Somali", "Soomaaliga, af Soomaali"); + public static readonly Language SQ = AddLanguage("sq", "Albanian", "Shqip"); + public static readonly Language SR = AddLanguage("sr", "Serbian", "српски језик"); + public static readonly Language SS = AddLanguage("ss", "Swati", "SiSwati"); + public static readonly Language ST = AddLanguage("st", "Southern Sotho", "Sesotho"); + public static readonly Language SU = AddLanguage("su", "Sundanese", "Basa Sunda"); + public static readonly Language SV = AddLanguage("sv", "Swedish", "svenska"); + public static readonly Language SW = AddLanguage("sw", "Swahili", "Kiswahili"); + public static readonly Language TA = AddLanguage("ta", "Tamil", "தமிழ்"); + public static readonly Language TE = AddLanguage("te", "Telugu", "తెలుగు"); + public static readonly Language TG = AddLanguage("tg", "Tajik", "тоҷикӣ, toçikī, تاجیکی‎"); + public static readonly Language TH = AddLanguage("th", "Thai", "ไทย"); + public static readonly Language TI = AddLanguage("ti", "Tigrinya", "ትግርኛ"); + public static readonly Language TK = AddLanguage("tk", "Turkmen", "Türkmen, Түркмен"); + public static readonly Language TL = AddLanguage("tl", "Tagalog", "Wikang Tagalog"); + public static readonly Language TN = AddLanguage("tn", "Tswana", "Setswana"); + public static readonly Language TO = AddLanguage("to", "Tonga (Tonga Islands)", "faka Tonga"); + public static readonly Language TR = AddLanguage("tr", "Turkish", "Türkçe"); + public static readonly Language TS = AddLanguage("ts", "Tsonga", "Xitsonga"); + public static readonly Language TT = AddLanguage("tt", "Tatar", "татар теле, tatar tele"); + public static readonly Language TW = AddLanguage("tw", "Twi", "Twi"); + public static readonly Language TY = AddLanguage("ty", "Tahitian", "Reo Tahiti"); + public static readonly Language UG = AddLanguage("ug", "Uyghur", "ئۇيغۇرچە‎, Uyghurche"); + public static readonly Language UK = AddLanguage("uk", "Ukrainian", "Українська"); + public static readonly Language UR = AddLanguage("ur", "Urdu", "اردو"); + public static readonly Language UZ = AddLanguage("uz", "Uzbek", "Oʻzbek, Ўзбек, أۇزبېك‎"); + public static readonly Language VE = AddLanguage("ve", "Venda", "Tshivenḓa"); + public static readonly Language VI = AddLanguage("vi", "Vietnamese", "Tiếng Việt"); + public static readonly Language VO = AddLanguage("vo", "Volapük", "Volapük"); + public static readonly Language WA = AddLanguage("wa", "Walloon", "walon"); + public static readonly Language WO = AddLanguage("wo", "Wolof", "Wollof"); + public static readonly Language XH = AddLanguage("xh", "Xhosa", "isiXhosa"); + public static readonly Language YI = AddLanguage("yi", "Yiddish", "ייִדיש"); + public static readonly Language YO = AddLanguage("yo", "Yoruba", "Yorùbá"); + public static readonly Language ZA = AddLanguage("za", "Zhuang, Chuang", "Saɯ cueŋƅ, Saw cuengh"); + public static readonly Language ZH = AddLanguage("zh", "Chinese", "中文 (Zhōngwén), 汉语, 漢語"); + public static readonly Language ZU = AddLanguage("zu", "Zulu", "isiZulu"); + public static readonly Language AfarDjibouti = AddLanguage("aa-DJ", "Afar (Djibouti)", "Afar (Djibouti)"); + public static readonly Language AfarEritrea = AddLanguage("aa-ER", "Afar (Eritrea)", "Afar (Eritrea)"); + public static readonly Language AfarEthiopia = AddLanguage("aa-ET", "Afar (Ethiopia)", "Afar (Ethiopia)"); + public static readonly Language AfrikaansNamibia = AddLanguage("af-NA", "Afrikaans (Namibia)", "Afrikaans (Namibië)"); + public static readonly Language AfrikaansSouthAfrica = AddLanguage("af-ZA", "Afrikaans (South Africa)", "Afrikaans (Suid-Afrika)"); + public static readonly Language AkanGhana = AddLanguage("ak-GH", "Akan (Ghana)", "Akan (Gaana)"); + public static readonly Language AmharicEthiopia = AddLanguage("am-ET", "Amharic (Ethiopia)", "አማርኛ (ኢትዮጵያ)"); + public static readonly Language ArabicUnitedArabEmirates = AddLanguage("ar-AE", "Arabic (United Arab Emirates)", "العربية (الإمارات العربية المتحدة)"); + public static readonly Language ArabicBahrain = AddLanguage("ar-BH", "Arabic (Bahrain)", "العربية (البحرين)"); + public static readonly Language ArabicDjibouti = AddLanguage("ar-DJ", "Arabic (Djibouti)", "العربية (جيبوتي)"); + public static readonly Language ArabicAlgeria = AddLanguage("ar-DZ", "Arabic (Algeria)", "العربية (الجزائر)"); + public static readonly Language ArabicEgypt = AddLanguage("ar-EG", "Arabic (Egypt)", "العربية (مصر)"); + public static readonly Language ArabicEritrea = AddLanguage("ar-ER", "Arabic (Eritrea)", "العربية (إريتريا)"); + public static readonly Language ArabicIsrael = AddLanguage("ar-IL", "Arabic (Israel)", "العربية (إسرائيل)"); + public static readonly Language ArabicIraq = AddLanguage("ar-IQ", "Arabic (Iraq)", "العربية (العراق)"); + public static readonly Language ArabicJordan = AddLanguage("ar-JO", "Arabic (Jordan)", "العربية (الأردن)"); + public static readonly Language ArabicComoros = AddLanguage("ar-KM", "Arabic (Comoros)", "العربية (جزر القمر)"); + public static readonly Language ArabicKuwait = AddLanguage("ar-KW", "Arabic (Kuwait)", "العربية (الكويت)"); + public static readonly Language ArabicLebanon = AddLanguage("ar-LB", "Arabic (Lebanon)", "العربية (لبنان)"); + public static readonly Language ArabicLibya = AddLanguage("ar-LY", "Arabic (Libya)", "العربية (ليبيا)"); + public static readonly Language ArabicMorocco = AddLanguage("ar-MA", "Arabic (Morocco)", "العربية (المغرب)"); + public static readonly Language ArabicMauritania = AddLanguage("ar-MR", "Arabic (Mauritania)", "العربية (موريتانيا)"); + public static readonly Language ArabicOman = AddLanguage("ar-OM", "Arabic (Oman)", "العربية (عُمان)"); + public static readonly Language ArabicPalestinianAuthority = AddLanguage("ar-PS", "Arabic (Palestinian Authority)", "العربية (السلطة الفلسطينية)"); + public static readonly Language ArabicQatar = AddLanguage("ar-QA", "Arabic (Qatar)", "العربية (قطر)"); + public static readonly Language ArabicSaudiArabia = AddLanguage("ar-SA", "Arabic (Saudi Arabia)", "العربية (المملكة العربية السعودية)"); + public static readonly Language ArabicSudan = AddLanguage("ar-SD", "Arabic (Sudan)", "العربية (السودان)"); + public static readonly Language ArabicSomalia = AddLanguage("ar-SO", "Arabic (Somalia)", "العربية (الصومال)"); + public static readonly Language ArabicSouthSudan = AddLanguage("ar-SS", "Arabic (South Sudan)", "العربية (جنوب السودان)"); + public static readonly Language ArabicSyria = AddLanguage("ar-SY", "Arabic (Syria)", "العربية (سوريا)"); + public static readonly Language ArabicChad = AddLanguage("ar-TD", "Arabic (Chad)", "العربية (تشاد)"); + public static readonly Language ArabicTunisia = AddLanguage("ar-TN", "Arabic (Tunisia)", "العربية (تونس)"); + public static readonly Language ArabicYemen = AddLanguage("ar-YE", "Arabic (Yemen)", "العربية (اليمن)"); + public static readonly Language AssameseIndia = AddLanguage("as-IN", "Assamese (India)", "অসমীয়া (ভাৰত)"); + public static readonly Language BashkirRussia = AddLanguage("ba-RU", "Bashkir (Russia)", "Bashkir (Russia)"); + public static readonly Language BelarusianBelarus = AddLanguage("be-BY", "Belarusian (Belarus)", "беларуская (Беларусь)"); + public static readonly Language BulgarianBulgaria = AddLanguage("bg-BG", "Bulgarian (Bulgaria)", "български (България)"); + public static readonly Language BamanankanMali = AddLanguage("bm-ML", "Bamanankan (Mali)", "bamanakan (Mali)"); + public static readonly Language BanglaBangladesh = AddLanguage("bn-BD", "Bangla (Bangladesh)", "বাংলা (বাংলাদেশ)"); + public static readonly Language BanglaIndia = AddLanguage("bn-IN", "Bangla (India)", "বাংলা (ভারত)"); + public static readonly Language TibetanChina = AddLanguage("bo-CN", "Tibetan (China)", "བོད་སྐད་ (རྒྱ་ནག)"); + public static readonly Language TibetanIndia = AddLanguage("bo-IN", "Tibetan (India)", "བོད་སྐད་ (རྒྱ་གར་)"); + public static readonly Language BretonFrance = AddLanguage("br-FR", "Breton (France)", "brezhoneg (Frañs)"); + public static readonly Language CatalanAndorra = AddLanguage("ca-AD", "Catalan (Andorra)", "català (Andorra)"); + public static readonly Language CatalanSpain = AddLanguage("ca-ES", "Catalan (Spain)", "català (Espanya)"); + public static readonly Language CatalanFrance = AddLanguage("ca-FR", "Catalan (France)", "català (França)"); + public static readonly Language CatalanItaly = AddLanguage("ca-IT", "Catalan (Italy)", "català (Itàlia)"); + public static readonly Language ChechenRussia = AddLanguage("ce-RU", "Chechen (Russia)", "нохчийн (Росси)"); + public static readonly Language CorsicanFrance = AddLanguage("co-FR", "Corsican (France)", "Corsican (France)"); + public static readonly Language CzechCzechia = AddLanguage("cs-CZ", "Czech (Czechia)", "čeština (Česko)"); + public static readonly Language ChurchSlavicRussia = AddLanguage("cu-RU", "Church Slavic (Russia)", "Church Slavic (Russia)"); + public static readonly Language WelshUnitedKingdom = AddLanguage("cy-GB", "Welsh (United Kingdom)", "Cymraeg (Y Deyrnas Unedig)"); + public static readonly Language DanishDenmark = AddLanguage("da-DK", "Danish (Denmark)", "dansk (Danmark)"); + public static readonly Language DanishGreenland = AddLanguage("da-GL", "Danish (Greenland)", "dansk (Grønland)"); + public static readonly Language GermanAustria = AddLanguage("de-AT", "German (Austria)", "Deutsch (Österreich)"); + public static readonly Language GermanBelgium = AddLanguage("de-BE", "German (Belgium)", "Deutsch (Belgien)"); + public static readonly Language GermanSwitzerland = AddLanguage("de-CH", "German (Switzerland)", "Deutsch (Schweiz)"); + public static readonly Language GermanGermany = AddLanguage("de-DE", "German (Germany)", "Deutsch (Deutschland)"); + public static readonly Language GermanItaly = AddLanguage("de-IT", "German (Italy)", "Deutsch (Italien)"); + public static readonly Language GermanLiechtenstein = AddLanguage("de-LI", "German (Liechtenstein)", "Deutsch (Liechtenstein)"); + public static readonly Language GermanLuxembourg = AddLanguage("de-LU", "German (Luxembourg)", "Deutsch (Luxemburg)"); + public static readonly Language DivehiMaldives = AddLanguage("dv-MV", "Divehi (Maldives)", "Divehi (Maldives)"); + public static readonly Language DzongkhaBhutan = AddLanguage("dz-BT", "Dzongkha (Bhutan)", "རྫོང་ཁ། (འབྲུག།)"); + public static readonly Language EweGhana = AddLanguage("ee-GH", "Ewe (Ghana)", "Eʋegbe (Ghana nutome)"); + public static readonly Language EweTogo = AddLanguage("ee-TG", "Ewe (Togo)", "Eʋegbe (Togo nutome)"); + public static readonly Language GreekCyprus = AddLanguage("el-CY", "Greek (Cyprus)", "Ελληνικά (Κύπρος)"); + public static readonly Language GreekGreece = AddLanguage("el-GR", "Greek (Greece)", "Ελληνικά (Ελλάδα)"); + public static readonly Language EnglishUnitedArabEmirates = AddLanguage("en-AE", "English (United Arab Emirates)", "English (United Arab Emirates)"); + public static readonly Language EnglishAntiguaBarbuda = AddLanguage("en-AG", "English (Antigua & Barbuda)", "English (Antigua & Barbuda)"); + public static readonly Language EnglishAnguilla = AddLanguage("en-AI", "English (Anguilla)", "English (Anguilla)"); + public static readonly Language EnglishAmericanSamoa = AddLanguage("en-AS", "English (American Samoa)", "English (American Samoa)"); + public static readonly Language EnglishAustria = AddLanguage("en-AT", "English (Austria)", "English (Austria)"); + public static readonly Language EnglishAustralia = AddLanguage("en-AU", "English (Australia)", "English (Australia)"); + public static readonly Language EnglishBarbados = AddLanguage("en-BB", "English (Barbados)", "English (Barbados)"); + public static readonly Language EnglishBelgium = AddLanguage("en-BE", "English (Belgium)", "English (Belgium)"); + public static readonly Language EnglishBurundi = AddLanguage("en-BI", "English (Burundi)", "English (Burundi)"); + public static readonly Language EnglishBermuda = AddLanguage("en-BM", "English (Bermuda)", "English (Bermuda)"); + public static readonly Language EnglishBahamas = AddLanguage("en-BS", "English (Bahamas)", "English (Bahamas)"); + public static readonly Language EnglishBotswana = AddLanguage("en-BW", "English (Botswana)", "English (Botswana)"); + public static readonly Language EnglishBelize = AddLanguage("en-BZ", "English (Belize)", "English (Belize)"); + public static readonly Language EnglishCanada = AddLanguage("en-CA", "English (Canada)", "English (Canada)"); + public static readonly Language EnglishCocosKeelingIslands = AddLanguage("en-CC", "English (Cocos [Keeling] Islands)", "English (Cocos [Keeling] Islands)"); + public static readonly Language EnglishSwitzerland = AddLanguage("en-CH", "English (Switzerland)", "English (Switzerland)"); + public static readonly Language EnglishCookIslands = AddLanguage("en-CK", "English (Cook Islands)", "English (Cook Islands)"); + public static readonly Language EnglishCameroon = AddLanguage("en-CM", "English (Cameroon)", "English (Cameroon)"); + public static readonly Language EnglishChristmasIsland = AddLanguage("en-CX", "English (Christmas Island)", "English (Christmas Island)"); + public static readonly Language EnglishCyprus = AddLanguage("en-CY", "English (Cyprus)", "English (Cyprus)"); + public static readonly Language EnglishGermany = AddLanguage("en-DE", "English (Germany)", "English (Germany)"); + public static readonly Language EnglishDenmark = AddLanguage("en-DK", "English (Denmark)", "English (Denmark)"); + public static readonly Language EnglishDominica = AddLanguage("en-DM", "English (Dominica)", "English (Dominica)"); + public static readonly Language EnglishEritrea = AddLanguage("en-ER", "English (Eritrea)", "English (Eritrea)"); + public static readonly Language EnglishFinland = AddLanguage("en-FI", "English (Finland)", "English (Finland)"); + public static readonly Language EnglishFiji = AddLanguage("en-FJ", "English (Fiji)", "English (Fiji)"); + public static readonly Language EnglishFalklandIslands = AddLanguage("en-FK", "English (Falkland Islands)", "English (Falkland Islands)"); + public static readonly Language EnglishMicronesia = AddLanguage("en-FM", "English (Micronesia)", "English (Micronesia)"); + public static readonly Language EnglishUnitedKingdom = AddLanguage("en-GB", "English (United Kingdom)", "English (United Kingdom)"); + public static readonly Language EnglishGrenada = AddLanguage("en-GD", "English (Grenada)", "English (Grenada)"); + public static readonly Language EnglishGuernsey = AddLanguage("en-GG", "English (Guernsey)", "English (Guernsey)"); + public static readonly Language EnglishGhana = AddLanguage("en-GH", "English (Ghana)", "English (Ghana)"); + public static readonly Language EnglishGibraltar = AddLanguage("en-GI", "English (Gibraltar)", "English (Gibraltar)"); + public static readonly Language EnglishGambia = AddLanguage("en-GM", "English (Gambia)", "English (Gambia)"); + public static readonly Language EnglishGuam = AddLanguage("en-GU", "English (Guam)", "English (Guam)"); + public static readonly Language EnglishGuyana = AddLanguage("en-GY", "English (Guyana)", "English (Guyana)"); + public static readonly Language EnglishHongKongSAR = AddLanguage("en-HK", "English (Hong Kong SAR)", "English (Hong Kong SAR)"); + public static readonly Language EnglishIreland = AddLanguage("en-IE", "English (Ireland)", "English (Ireland)"); + public static readonly Language EnglishIsrael = AddLanguage("en-IL", "English (Israel)", "English (Israel)"); + public static readonly Language EnglishIsleofMan = AddLanguage("en-IM", "English (Isle of Man)", "English (Isle of Man)"); + public static readonly Language EnglishIndia = AddLanguage("en-IN", "English (India)", "English (India)"); + public static readonly Language EnglishBritishIndianOceanTerritory = AddLanguage("en-IO", "English (British Indian Ocean Territory)", "English (British Indian Ocean Territory)"); + public static readonly Language EnglishJersey = AddLanguage("en-JE", "English (Jersey)", "English (Jersey)"); + public static readonly Language EnglishJamaica = AddLanguage("en-JM", "English (Jamaica)", "English (Jamaica)"); + public static readonly Language EnglishKenya = AddLanguage("en-KE", "English (Kenya)", "English (Kenya)"); + public static readonly Language EnglishKiribati = AddLanguage("en-KI", "English (Kiribati)", "English (Kiribati)"); + public static readonly Language EnglishStKittsNevis = AddLanguage("en-KN", "English (St. Kitts & Nevis)", "English (St. Kitts & Nevis)"); + public static readonly Language EnglishCaymanIslands = AddLanguage("en-KY", "English (Cayman Islands)", "English (Cayman Islands)"); + public static readonly Language EnglishStLucia = AddLanguage("en-LC", "English (St. Lucia)", "English (St. Lucia)"); + public static readonly Language EnglishLiberia = AddLanguage("en-LR", "English (Liberia)", "English (Liberia)"); + public static readonly Language EnglishLesotho = AddLanguage("en-LS", "English (Lesotho)", "English (Lesotho)"); + public static readonly Language EnglishMadagascar = AddLanguage("en-MG", "English (Madagascar)", "English (Madagascar)"); + public static readonly Language EnglishMarshallIslands = AddLanguage("en-MH", "English (Marshall Islands)", "English (Marshall Islands)"); + public static readonly Language EnglishMacaoSAR = AddLanguage("en-MO", "English (Macao SAR)", "English (Macao SAR)"); + public static readonly Language EnglishNorthernMarianaIslands = AddLanguage("en-MP", "English (Northern Mariana Islands)", "English (Northern Mariana Islands)"); + public static readonly Language EnglishMontserrat = AddLanguage("en-MS", "English (Montserrat)", "English (Montserrat)"); + public static readonly Language EnglishMalta = AddLanguage("en-MT", "English (Malta)", "English (Malta)"); + public static readonly Language EnglishMauritius = AddLanguage("en-MU", "English (Mauritius)", "English (Mauritius)"); + public static readonly Language EnglishMalawi = AddLanguage("en-MW", "English (Malawi)", "English (Malawi)"); + public static readonly Language EnglishMalaysia = AddLanguage("en-MY", "English (Malaysia)", "English (Malaysia)"); + public static readonly Language EnglishNamibia = AddLanguage("en-NA", "English (Namibia)", "English (Namibia)"); + public static readonly Language EnglishNorfolkIsland = AddLanguage("en-NF", "English (Norfolk Island)", "English (Norfolk Island)"); + public static readonly Language EnglishNigeria = AddLanguage("en-NG", "English (Nigeria)", "English (Nigeria)"); + public static readonly Language EnglishNetherlands = AddLanguage("en-NL", "English (Netherlands)", "English (Netherlands)"); + public static readonly Language EnglishNauru = AddLanguage("en-NR", "English (Nauru)", "English (Nauru)"); + public static readonly Language EnglishNiue = AddLanguage("en-NU", "English (Niue)", "English (Niue)"); + public static readonly Language EnglishNewZealand = AddLanguage("en-NZ", "English (New Zealand)", "English (New Zealand)"); + public static readonly Language EnglishPapuaNewGuinea = AddLanguage("en-PG", "English (Papua New Guinea)", "English (Papua New Guinea)"); + public static readonly Language EnglishPhilippines = AddLanguage("en-PH", "English (Philippines)", "English (Philippines)"); + public static readonly Language EnglishPakistan = AddLanguage("en-PK", "English (Pakistan)", "English (Pakistan)"); + public static readonly Language EnglishPitcairnIslands = AddLanguage("en-PN", "English (Pitcairn Islands)", "English (Pitcairn Islands)"); + public static readonly Language EnglishPuertoRico = AddLanguage("en-PR", "English (Puerto Rico)", "English (Puerto Rico)"); + public static readonly Language EnglishPalau = AddLanguage("en-PW", "English (Palau)", "English (Palau)"); + public static readonly Language EnglishRwanda = AddLanguage("en-RW", "English (Rwanda)", "English (Rwanda)"); + public static readonly Language EnglishSolomonIslands = AddLanguage("en-SB", "English (Solomon Islands)", "English (Solomon Islands)"); + public static readonly Language EnglishSeychelles = AddLanguage("en-SC", "English (Seychelles)", "English (Seychelles)"); + public static readonly Language EnglishSudan = AddLanguage("en-SD", "English (Sudan)", "English (Sudan)"); + public static readonly Language EnglishSweden = AddLanguage("en-SE", "English (Sweden)", "English (Sweden)"); + public static readonly Language EnglishSingapore = AddLanguage("en-SG", "English (Singapore)", "English (Singapore)"); + public static readonly Language EnglishStHelenaAscensionTristandaCunha = AddLanguage("en-SH", "English (St Helena, Ascension, Tristan da Cunha)", "English (St Helena, Ascension, Tristan da Cunha)"); + public static readonly Language EnglishSlovenia = AddLanguage("en-SI", "English (Slovenia)", "English (Slovenia)"); + public static readonly Language EnglishSierraLeone = AddLanguage("en-SL", "English (Sierra Leone)", "English (Sierra Leone)"); + public static readonly Language EnglishSouthSudan = AddLanguage("en-SS", "English (South Sudan)", "English (South Sudan)"); + public static readonly Language EnglishSintMaarten = AddLanguage("en-SX", "English (Sint Maarten)", "English (Sint Maarten)"); + public static readonly Language EnglishEswatini = AddLanguage("en-SZ", "English (Eswatini)", "English (Eswatini)"); + public static readonly Language EnglishTurksCaicosIslands = AddLanguage("en-TC", "English (Turks & Caicos Islands)", "English (Turks & Caicos Islands)"); + public static readonly Language EnglishTokelau = AddLanguage("en-TK", "English (Tokelau)", "English (Tokelau)"); + public static readonly Language EnglishTonga = AddLanguage("en-TO", "English (Tonga)", "English (Tonga)"); + public static readonly Language EnglishTrinidadTobago = AddLanguage("en-TT", "English (Trinidad & Tobago)", "English (Trinidad & Tobago)"); + public static readonly Language EnglishTuvalu = AddLanguage("en-TV", "English (Tuvalu)", "English (Tuvalu)"); + public static readonly Language EnglishTanzania = AddLanguage("en-TZ", "English (Tanzania)", "English (Tanzania)"); + public static readonly Language EnglishUganda = AddLanguage("en-UG", "English (Uganda)", "English (Uganda)"); + public static readonly Language EnglishUSOutlyingIslands = AddLanguage("en-UM", "English (U.S. Outlying Islands)", "English (U.S. Outlying Islands)"); + public static readonly Language EnglishUnitedStates = AddLanguage("en-US", "English (United States)", "English (United States)"); + public static readonly Language EnglishStVincentGrenadines = AddLanguage("en-VC", "English (St. Vincent & Grenadines)", "English (St. Vincent & Grenadines)"); + public static readonly Language EnglishBritishVirginIslands = AddLanguage("en-VG", "English (British Virgin Islands)", "English (British Virgin Islands)"); + public static readonly Language EnglishUSVirginIslands = AddLanguage("en-VI", "English (U.S. Virgin Islands)", "English (U.S. Virgin Islands)"); + public static readonly Language EnglishVanuatu = AddLanguage("en-VU", "English (Vanuatu)", "English (Vanuatu)"); + public static readonly Language EnglishSamoa = AddLanguage("en-WS", "English (Samoa)", "English (Samoa)"); + public static readonly Language EnglishSouthAfrica = AddLanguage("en-ZA", "English (South Africa)", "English (South Africa)"); + public static readonly Language EnglishZambia = AddLanguage("en-ZM", "English (Zambia)", "English (Zambia)"); + public static readonly Language EnglishZimbabwe = AddLanguage("en-ZW", "English (Zimbabwe)", "English (Zimbabwe)"); + public static readonly Language SpanishArgentina = AddLanguage("es-AR", "Spanish (Argentina)", "español (Argentina)"); + public static readonly Language SpanishBolivia = AddLanguage("es-BO", "Spanish (Bolivia)", "español (Bolivia)"); + public static readonly Language SpanishBrazil = AddLanguage("es-BR", "Spanish (Brazil)", "español (Brasil)"); + public static readonly Language SpanishBelize = AddLanguage("es-BZ", "Spanish (Belize)", "español (Belice)"); + public static readonly Language SpanishChile = AddLanguage("es-CL", "Spanish (Chile)", "español (Chile)"); + public static readonly Language SpanishColombia = AddLanguage("es-CO", "Spanish (Colombia)", "español (Colombia)"); + public static readonly Language SpanishCostaRica = AddLanguage("es-CR", "Spanish (Costa Rica)", "español (Costa Rica)"); + public static readonly Language SpanishCuba = AddLanguage("es-CU", "Spanish (Cuba)", "español (Cuba)"); + public static readonly Language SpanishDominicanRepublic = AddLanguage("es-DO", "Spanish (Dominican Republic)", "español (República Dominicana)"); + public static readonly Language SpanishEcuador = AddLanguage("es-EC", "Spanish (Ecuador)", "español (Ecuador)"); + public static readonly Language SpanishSpain = AddLanguage("es-ES", "Spanish (Spain)", "español (España)"); + public static readonly Language SpanishEquatorialGuinea = AddLanguage("es-GQ", "Spanish (Equatorial Guinea)", "español (Guinea Ecuatorial)"); + public static readonly Language SpanishGuatemala = AddLanguage("es-GT", "Spanish (Guatemala)", "español (Guatemala)"); + public static readonly Language SpanishHonduras = AddLanguage("es-HN", "Spanish (Honduras)", "español (Honduras)"); + public static readonly Language SpanishMexico = AddLanguage("es-MX", "Spanish (Mexico)", "español (México)"); + public static readonly Language SpanishNicaragua = AddLanguage("es-NI", "Spanish (Nicaragua)", "español (Nicaragua)"); + public static readonly Language SpanishPanama = AddLanguage("es-PA", "Spanish (Panama)", "español (Panamá)"); + public static readonly Language SpanishPeru = AddLanguage("es-PE", "Spanish (Peru)", "español (Perú)"); + public static readonly Language SpanishPhilippines = AddLanguage("es-PH", "Spanish (Philippines)", "español (Filipinas)"); + public static readonly Language SpanishPuertoRico = AddLanguage("es-PR", "Spanish (Puerto Rico)", "español (Puerto Rico)"); + public static readonly Language SpanishParaguay = AddLanguage("es-PY", "Spanish (Paraguay)", "español (Paraguay)"); + public static readonly Language SpanishElSalvador = AddLanguage("es-SV", "Spanish (El Salvador)", "español (El Salvador)"); + public static readonly Language SpanishUnitedStates = AddLanguage("es-US", "Spanish (United States)", "español (Estados Unidos)"); + public static readonly Language SpanishUruguay = AddLanguage("es-UY", "Spanish (Uruguay)", "español (Uruguay)"); + public static readonly Language SpanishVenezuela = AddLanguage("es-VE", "Spanish (Venezuela)", "español (Venezuela)"); + public static readonly Language EstonianEstonia = AddLanguage("et-EE", "Estonian (Estonia)", "eesti (Eesti)"); + public static readonly Language BasqueSpain = AddLanguage("eu-ES", "Basque (Spain)", "euskara (Espainia)"); + public static readonly Language PersianAfghanistan = AddLanguage("fa-AF", "Persian (Afghanistan)", "فارسی (افغانستان)"); + public static readonly Language PersianIran = AddLanguage("fa-IR", "Persian (Iran)", "فارسی (ایران)"); + public static readonly Language FinnishFinland = AddLanguage("fi-FI", "Finnish (Finland)", "suomi (Suomi)"); + public static readonly Language FaroeseDenmark = AddLanguage("fo-DK", "Faroese (Denmark)", "føroyskt (Danmark)"); + public static readonly Language FaroeseFaroeIslands = AddLanguage("fo-FO", "Faroese (Faroe Islands)", "føroyskt (Føroyar)"); + public static readonly Language FrenchBelgium = AddLanguage("fr-BE", "French (Belgium)", "français (Belgique)"); + public static readonly Language FrenchBurkinaFaso = AddLanguage("fr-BF", "French (Burkina Faso)", "français (Burkina Faso)"); + public static readonly Language FrenchBurundi = AddLanguage("fr-BI", "French (Burundi)", "français (Burundi)"); + public static readonly Language FrenchBenin = AddLanguage("fr-BJ", "French (Benin)", "français (Bénin)"); + public static readonly Language FrenchStBarthélemy = AddLanguage("fr-BL", "French (St. Barthélemy)", "français (Saint-Barthélemy)"); + public static readonly Language FrenchCanada = AddLanguage("fr-CA", "French (Canada)", "français (Canada)"); + public static readonly Language FrenchCongoDRC = AddLanguage("fr-CD", "French (Congo [DRC])", "français (Congo [République démocratique du])"); + public static readonly Language FrenchCentralAfricanRepublic = AddLanguage("fr-CF", "French (Central African Republic)", "français (République centrafricaine)"); + public static readonly Language FrenchCongo = AddLanguage("fr-CG", "French (Congo)", "français (Congo)"); + public static readonly Language FrenchSwitzerland = AddLanguage("fr-CH", "French (Switzerland)", "français (Suisse)"); + public static readonly Language FrenchCôtedIvoire = AddLanguage("fr-CI", "French (Côte d’Ivoire)", "français (Côte d’Ivoire)"); + public static readonly Language FrenchCameroon = AddLanguage("fr-CM", "French (Cameroon)", "français (Cameroun)"); + public static readonly Language FrenchDjibouti = AddLanguage("fr-DJ", "French (Djibouti)", "français (Djibouti)"); + public static readonly Language FrenchAlgeria = AddLanguage("fr-DZ", "French (Algeria)", "français (Algérie)"); + public static readonly Language FrenchFrance = AddLanguage("fr-FR", "French (France)", "français (France)"); + public static readonly Language FrenchGabon = AddLanguage("fr-GA", "French (Gabon)", "français (Gabon)"); + public static readonly Language FrenchFrenchGuiana = AddLanguage("fr-GF", "French (French Guiana)", "français (Guyane française)"); + public static readonly Language FrenchGuinea = AddLanguage("fr-GN", "French (Guinea)", "français (Guinée)"); + public static readonly Language FrenchGuadeloupe = AddLanguage("fr-GP", "French (Guadeloupe)", "français (Guadeloupe)"); + public static readonly Language FrenchEquatorialGuinea = AddLanguage("fr-GQ", "French (Equatorial Guinea)", "français (Guinée équatoriale)"); + public static readonly Language FrenchHaiti = AddLanguage("fr-HT", "French (Haiti)", "français (Haïti)"); + public static readonly Language FrenchComoros = AddLanguage("fr-KM", "French (Comoros)", "français (Comores)"); + public static readonly Language FrenchLuxembourg = AddLanguage("fr-LU", "French (Luxembourg)", "français (Luxembourg)"); + public static readonly Language FrenchMorocco = AddLanguage("fr-MA", "French (Morocco)", "français (Maroc)"); + public static readonly Language FrenchMonaco = AddLanguage("fr-MC", "French (Monaco)", "français (Monaco)"); + public static readonly Language FrenchStMartin = AddLanguage("fr-MF", "French (St. Martin)", "français (Saint-Martin)"); + public static readonly Language FrenchMadagascar = AddLanguage("fr-MG", "French (Madagascar)", "français (Madagascar)"); + public static readonly Language FrenchMali = AddLanguage("fr-ML", "French (Mali)", "français (Mali)"); + public static readonly Language FrenchMartinique = AddLanguage("fr-MQ", "French (Martinique)", "français (Martinique)"); + public static readonly Language FrenchMauritania = AddLanguage("fr-MR", "French (Mauritania)", "français (Mauritanie)"); + public static readonly Language FrenchMauritius = AddLanguage("fr-MU", "French (Mauritius)", "français (Maurice)"); + public static readonly Language FrenchNewCaledonia = AddLanguage("fr-NC", "French (New Caledonia)", "français (Nouvelle-Calédonie)"); + public static readonly Language FrenchNiger = AddLanguage("fr-NE", "French (Niger)", "français (Niger)"); + public static readonly Language FrenchFrenchPolynesia = AddLanguage("fr-PF", "French (French Polynesia)", "français (Polynésie française)"); + public static readonly Language FrenchStPierreMiquelon = AddLanguage("fr-PM", "French (St. Pierre & Miquelon)", "français (Saint-Pierre-et-Miquelon)"); + public static readonly Language FrenchRéunion = AddLanguage("fr-RE", "French (Réunion)", "français (La Réunion)"); + public static readonly Language FrenchRwanda = AddLanguage("fr-RW", "French (Rwanda)", "français (Rwanda)"); + public static readonly Language FrenchSeychelles = AddLanguage("fr-SC", "French (Seychelles)", "français (Seychelles)"); + public static readonly Language FrenchSenegal = AddLanguage("fr-SN", "French (Senegal)", "français (Sénégal)"); + public static readonly Language FrenchSyria = AddLanguage("fr-SY", "French (Syria)", "français (Syrie)"); + public static readonly Language FrenchChad = AddLanguage("fr-TD", "French (Chad)", "français (Tchad)"); + public static readonly Language FrenchTogo = AddLanguage("fr-TG", "French (Togo)", "français (Togo)"); + public static readonly Language FrenchTunisia = AddLanguage("fr-TN", "French (Tunisia)", "français (Tunisie)"); + public static readonly Language FrenchVanuatu = AddLanguage("fr-VU", "French (Vanuatu)", "français (Vanuatu)"); + public static readonly Language FrenchWallisFutuna = AddLanguage("fr-WF", "French (Wallis & Futuna)", "français (Wallis-et-Futuna)"); + public static readonly Language FrenchMayotte = AddLanguage("fr-YT", "French (Mayotte)", "français (Mayotte)"); + public static readonly Language WesternFrisianNetherlands = AddLanguage("fy-NL", "Western Frisian (Netherlands)", "Frysk (Nederlân)"); + public static readonly Language IrishIreland = AddLanguage("ga-IE", "Irish (Ireland)", "Gaeilge (Éire)"); + public static readonly Language ScottishGaelicUnitedKingdom = AddLanguage("gd-GB", "Scottish Gaelic (United Kingdom)", "Gàidhlig (An Rìoghachd Aonaichte)"); + public static readonly Language GalicianSpain = AddLanguage("gl-ES", "Galician (Spain)", "galego (España)"); + public static readonly Language GuaraniParaguay = AddLanguage("gn-PY", "Guarani (Paraguay)", "Guarani (Paraguay)"); + public static readonly Language GujaratiIndia = AddLanguage("gu-IN", "Gujarati (India)", "ગુજરાતી (ભારત)"); + public static readonly Language ManxIsleofMan = AddLanguage("gv-IM", "Manx (Isle of Man)", "Gaelg (Ellan Vannin)"); + public static readonly Language HausaGhana = AddLanguage("ha-GH", "Hausa (Ghana)", "Hausa (Gana)"); + public static readonly Language HausaNiger = AddLanguage("ha-NE", "Hausa (Niger)", "Hausa (Nijar)"); + public static readonly Language HausaNigeria = AddLanguage("ha-NG", "Hausa (Nigeria)", "Hausa (Najeriya)"); + public static readonly Language HebrewIsrael = AddLanguage("he-IL", "Hebrew (Israel)", "עברית (ישראל)"); + public static readonly Language HindiIndia = AddLanguage("hi-IN", "Hindi (India)", "हिन्दी (भारत)"); + public static readonly Language CroatianBosniaHerzegovina = AddLanguage("hr-BA", "Croatian (Bosnia & Herzegovina)", "hrvatski (Bosna i Hercegovina)"); + public static readonly Language CroatianCroatia = AddLanguage("hr-HR", "Croatian (Croatia)", "hrvatski (Hrvatska)"); + public static readonly Language HungarianHungary = AddLanguage("hu-HU", "Hungarian (Hungary)", "magyar (Magyarország)"); + public static readonly Language ArmenianArmenia = AddLanguage("hy-AM", "Armenian (Armenia)", "հայերեն (Հայաստան)"); + public static readonly Language IndonesianIndonesia = AddLanguage("id-ID", "Indonesian (Indonesia)", "Indonesia (Indonesia)"); + public static readonly Language IgboNigeria = AddLanguage("ig-NG", "Igbo (Nigeria)", "Asụsụ Igbo (Naịjịrịa)"); + public static readonly Language YiChina = AddLanguage("ii-CN", "Yi (China)", "ꆈꌠꉙ (ꍏꇩ)"); + public static readonly Language IcelandicIceland = AddLanguage("is-IS", "Icelandic (Iceland)", "íslenska (Ísland)"); + public static readonly Language ItalianSwitzerland = AddLanguage("it-CH", "Italian (Switzerland)", "italiano (Svizzera)"); + public static readonly Language ItalianItaly = AddLanguage("it-IT", "Italian (Italy)", "italiano (Italia)"); + public static readonly Language ItalianSanMarino = AddLanguage("it-SM", "Italian (San Marino)", "italiano (San Marino)"); + public static readonly Language ItalianVaticanCity = AddLanguage("it-VA", "Italian (Vatican City)", "italiano (Città del Vaticano)"); + public static readonly Language InuktitutCanada = AddLanguage("iu-CA", "Inuktitut (Canada)", "Inuktitut (Canada)"); + public static readonly Language JapaneseJapan = AddLanguage("ja-JP", "Japanese (Japan)", "日本語 (日本)"); + public static readonly Language JavaneseIndonesia = AddLanguage("jv-ID", "Javanese (Indonesia)", "Jawa (Indonésia)"); + public static readonly Language GeorgianGeorgia = AddLanguage("ka-GE", "Georgian (Georgia)", "ქართული (საქართველო)"); + public static readonly Language KikuyuKenya = AddLanguage("ki-KE", "Kikuyu (Kenya)", "Gikuyu (Kenya)"); + public static readonly Language KazakhKazakhstan = AddLanguage("kk-KZ", "Kazakh (Kazakhstan)", "қазақ тілі (Қазақстан)"); + public static readonly Language KalaallisutGreenland = AddLanguage("kl-GL", "Kalaallisut (Greenland)", "kalaallisut (Kalaallit Nunaat)"); + public static readonly Language KhmerCambodia = AddLanguage("km-KH", "Khmer (Cambodia)", "ខ្មែរ (កម្ពុជា)"); + public static readonly Language KannadaIndia = AddLanguage("kn-IN", "Kannada (India)", "ಕನ್ನಡ (ಭಾರತ)"); + public static readonly Language KoreanNorthKorea = AddLanguage("ko-KP", "Korean (North Korea)", "한국어(조선민주주의인민공화국)"); + public static readonly Language KoreanKorea = AddLanguage("ko-KR", "Korean (Korea)", "한국어(대한민국)"); + public static readonly Language KashmiriIndia = AddLanguage("ks-IN", "Kashmiri (India)", "کٲشُر (ہِندوستان)"); + public static readonly Language CornishUnitedKingdom = AddLanguage("kw-GB", "Cornish (United Kingdom)", "kernewek (Rywvaneth Unys)"); + public static readonly Language KyrgyzKyrgyzstan = AddLanguage("ky-KG", "Kyrgyz (Kyrgyzstan)", "кыргызча (Кыргызстан)"); + public static readonly Language LuxembourgishLuxembourg = AddLanguage("lb-LU", "Luxembourgish (Luxembourg)", "Lëtzebuergesch (Lëtzebuerg)"); + public static readonly Language GandaUganda = AddLanguage("lg-UG", "Ganda (Uganda)", "Luganda (Yuganda)"); + public static readonly Language LingalaAngola = AddLanguage("ln-AO", "Lingala (Angola)", "lingála (Angóla)"); + public static readonly Language LingalaCongoDRC = AddLanguage("ln-CD", "Lingala (Congo [DRC])", "lingála (Republíki ya Kongó Demokratíki)"); + public static readonly Language LingalaCentralAfricanRepublic = AddLanguage("ln-CF", "Lingala (Central African Republic)", "lingála (Repibiki ya Afríka ya Káti)"); + public static readonly Language LingalaCongo = AddLanguage("ln-CG", "Lingala (Congo)", "lingála (Kongo)"); + public static readonly Language LaoLaos = AddLanguage("lo-LA", "Lao (Laos)", "ລາວ (ລາວ)"); + public static readonly Language LithuanianLithuania = AddLanguage("lt-LT", "Lithuanian (Lithuania)", "lietuvių (Lietuva)"); + public static readonly Language LubaKatangaCongoDRC = AddLanguage("lu-CD", "Luba-Katanga (Congo [DRC])", "Tshiluba (Ditunga wa Kongu)"); + public static readonly Language LatvianLatvia = AddLanguage("lv-LV", "Latvian (Latvia)", "latviešu (Latvija)"); + public static readonly Language MalagasyMadagascar = AddLanguage("mg-MG", "Malagasy (Madagascar)", "Malagasy (Madagasikara)"); + public static readonly Language MaoriNewZealand = AddLanguage("mi-NZ", "Maori (New Zealand)", "Māori (Aotearoa)"); + public static readonly Language MacedonianNorthMacedonia = AddLanguage("mk-MK", "Macedonian (North Macedonia)", "македонски (Северна Македонија)"); + public static readonly Language MalayalamIndia = AddLanguage("ml-IN", "Malayalam (India)", "മലയാളം (ഇന്ത്യ)"); + public static readonly Language MongolianMongolia = AddLanguage("mn-MN", "Mongolian (Mongolia)", "монгол (Монгол)"); + public static readonly Language MarathiIndia = AddLanguage("mr-IN", "Marathi (India)", "मराठी (भारत)"); + public static readonly Language MalayBrunei = AddLanguage("ms-BN", "Malay (Brunei)", "Melayu (Brunei)"); + public static readonly Language MalayMalaysia = AddLanguage("ms-MY", "Malay (Malaysia)", "Melayu (Malaysia)"); + public static readonly Language MalaySingapore = AddLanguage("ms-SG", "Malay (Singapore)", "Melayu (Singapura)"); + public static readonly Language MalteseMalta = AddLanguage("mt-MT", "Maltese (Malta)", "Malti (Malta)"); + public static readonly Language BurmeseMyanmar = AddLanguage("my-MM", "Burmese (Myanmar)", "မြန်မာ (မြန်မာ)"); + public static readonly Language NorwegianBokmålNorway = AddLanguage("nb-NO", "Norwegian Bokmål (Norway)", "norsk bokmål (Norge)"); + public static readonly Language NorwegianBokmålSvalbardJanMayen = AddLanguage("nb-SJ", "Norwegian Bokmål (Svalbard & Jan Mayen)", "norsk bokmål (Svalbard og Jan Mayen)"); + public static readonly Language NorthNdebeleZimbabwe = AddLanguage("nd-ZW", "North Ndebele (Zimbabwe)", "isiNdebele (Zimbabwe)"); + public static readonly Language NepaliIndia = AddLanguage("ne-IN", "Nepali (India)", "नेपाली (भारत)"); + public static readonly Language NepaliNepal = AddLanguage("ne-NP", "Nepali (Nepal)", "नेपाली (नेपाल)"); + public static readonly Language DutchAruba = AddLanguage("nl-AW", "Dutch (Aruba)", "Nederlands (Aruba)"); + public static readonly Language DutchBelgium = AddLanguage("nl-BE", "Dutch (Belgium)", "Nederlands (België)"); + public static readonly Language DutchBonaireSintEustatiusandSaba = AddLanguage("nl-BQ", "Dutch (Bonaire, Sint Eustatius and Saba)", "Nederlands (Bonaire, Sint Eustatius en Saba)"); + public static readonly Language DutchCuraçao = AddLanguage("nl-CW", "Dutch (Curaçao)", "Nederlands (Curaçao)"); + public static readonly Language DutchNetherlands = AddLanguage("nl-NL", "Dutch (Netherlands)", "Nederlands (Nederland)"); + public static readonly Language DutchSuriname = AddLanguage("nl-SR", "Dutch (Suriname)", "Nederlands (Suriname)"); + public static readonly Language DutchSintMaarten = AddLanguage("nl-SX", "Dutch (Sint Maarten)", "Nederlands (Sint-Maarten)"); + public static readonly Language NorwegianNynorskNorway = AddLanguage("nn-NO", "Norwegian Nynorsk (Norway)", "nynorsk (Noreg)"); + public static readonly Language SouthNdebeleSouthAfrica = AddLanguage("nr-ZA", "South Ndebele (South Africa)", "South Ndebele (South Africa)"); + public static readonly Language OccitanFrance = AddLanguage("oc-FR", "Occitan (France)", "Occitan (France)"); + public static readonly Language OromoEthiopia = AddLanguage("om-ET", "Oromo (Ethiopia)", "Oromoo (Itoophiyaa)"); + public static readonly Language OromoKenya = AddLanguage("om-KE", "Oromo (Kenya)", "Oromoo (Keeniyaa)"); + public static readonly Language OdiaIndia = AddLanguage("or-IN", "Odia (India)", "ଓଡ଼ିଆ (ଭାରତ)"); + public static readonly Language OsseticGeorgia = AddLanguage("os-GE", "Ossetic (Georgia)", "ирон (Гуырдзыстон)"); + public static readonly Language OsseticRussia = AddLanguage("os-RU", "Ossetic (Russia)", "ирон (Уӕрӕсе)"); + public static readonly Language PolishPoland = AddLanguage("pl-PL", "Polish (Poland)", "polski (Polska)"); + public static readonly Language PashtoAfghanistan = AddLanguage("ps-AF", "Pashto (Afghanistan)", "پښتو (افغانستان)"); + public static readonly Language PashtoPakistan = AddLanguage("ps-PK", "Pashto (Pakistan)", "پښتو (پاکستان)"); + public static readonly Language PortugueseAngola = AddLanguage("pt-AO", "Portuguese (Angola)", "português (Angola)"); + public static readonly Language PortugueseBrazil = AddLanguage("pt-BR", "Portuguese (Brazil)", "português (Brasil)"); + public static readonly Language PortugueseSwitzerland = AddLanguage("pt-CH", "Portuguese (Switzerland)", "português (Suíça)"); + public static readonly Language PortugueseCaboVerde = AddLanguage("pt-CV", "Portuguese (Cabo Verde)", "português (Cabo Verde)"); + public static readonly Language PortugueseEquatorialGuinea = AddLanguage("pt-GQ", "Portuguese (Equatorial Guinea)", "português (Guiné Equatorial)"); + public static readonly Language PortugueseGuineaBissau = AddLanguage("pt-GW", "Portuguese (Guinea-Bissau)", "português (Guiné-Bissau)"); + public static readonly Language PortugueseLuxembourg = AddLanguage("pt-LU", "Portuguese (Luxembourg)", "português (Luxemburgo)"); + public static readonly Language PortugueseMacaoSAR = AddLanguage("pt-MO", "Portuguese (Macao SAR)", "português (RAE de Macau)"); + public static readonly Language PortugueseMozambique = AddLanguage("pt-MZ", "Portuguese (Mozambique)", "português (Moçambique)"); + public static readonly Language PortuguesePortugal = AddLanguage("pt-PT", "Portuguese (Portugal)", "português (Portugal)"); + public static readonly Language PortugueseSãoToméPríncipe = AddLanguage("pt-ST", "Portuguese (São Tomé & Príncipe)", "português (São Tomé e Príncipe)"); + public static readonly Language PortugueseTimorLeste = AddLanguage("pt-TL", "Portuguese (Timor-Leste)", "português (Timor-Leste)"); + public static readonly Language QuechuaBolivia = AddLanguage("qu-BO", "Quechua (Bolivia)", "Runasimi (Bolivia)"); + public static readonly Language QuechuaEcuador = AddLanguage("qu-EC", "Quechua (Ecuador)", "Runasimi (Ecuador)"); + public static readonly Language QuechuaPeru = AddLanguage("qu-PE", "Quechua (Peru)", "Runasimi (Perú)"); + public static readonly Language RomanshSwitzerland = AddLanguage("rm-CH", "Romansh (Switzerland)", "rumantsch (Svizra)"); + public static readonly Language RundiBurundi = AddLanguage("rn-BI", "Rundi (Burundi)", "Ikirundi (Uburundi)"); + public static readonly Language RomanianMoldova = AddLanguage("ro-MD", "Romanian (Moldova)", "română (Republica Moldova)"); + public static readonly Language RomanianRomania = AddLanguage("ro-RO", "Romanian (Romania)", "română (România)"); + public static readonly Language RussianBelarus = AddLanguage("ru-BY", "Russian (Belarus)", "русский (Беларусь)"); + public static readonly Language RussianKyrgyzstan = AddLanguage("ru-KG", "Russian (Kyrgyzstan)", "русский (Киргизия)"); + public static readonly Language RussianKazakhstan = AddLanguage("ru-KZ", "Russian (Kazakhstan)", "русский (Казахстан)"); + public static readonly Language RussianMoldova = AddLanguage("ru-MD", "Russian (Moldova)", "русский (Молдова)"); + public static readonly Language RussianRussia = AddLanguage("ru-RU", "Russian (Russia)", "русский (Россия)"); + public static readonly Language RussianUkraine = AddLanguage("ru-UA", "Russian (Ukraine)", "русский (Украина)"); + public static readonly Language KinyarwandaRwanda = AddLanguage("rw-RW", "Kinyarwanda (Rwanda)", "Kinyarwanda (U Rwanda)"); + public static readonly Language SanskritIndia = AddLanguage("sa-IN", "Sanskrit (India)", "Sanskrit (India)"); + public static readonly Language SindhiPakistan = AddLanguage("sd-PK", "Sindhi (Pakistan)", "سنڌي (پاڪستان)"); + public static readonly Language NorthernSamiFinland = AddLanguage("se-FI", "Northern Sami (Finland)", "davvisámegiella (Suopma)"); + public static readonly Language NorthernSamiNorway = AddLanguage("se-NO", "Northern Sami (Norway)", "davvisámegiella (Norga)"); + public static readonly Language NorthernSamiSweden = AddLanguage("se-SE", "Northern Sami (Sweden)", "davvisámegiella (Ruoŧŧa)"); + public static readonly Language SangoCentralAfricanRepublic = AddLanguage("sg-CF", "Sango (Central African Republic)", "Sängö (Ködörösêse tî Bêafrîka)"); + public static readonly Language SinhalaSriLanka = AddLanguage("si-LK", "Sinhala (Sri Lanka)", "සිංහල (ශ්‍රී ලංකාව)"); + public static readonly Language SlovakSlovakia = AddLanguage("sk-SK", "Slovak (Slovakia)", "slovenčina (Slovensko)"); + public static readonly Language SlovenianSlovenia = AddLanguage("sl-SI", "Slovenian (Slovenia)", "slovenščina (Slovenija)"); + public static readonly Language ShonaZimbabwe = AddLanguage("sn-ZW", "Shona (Zimbabwe)", "chiShona (Zimbabwe)"); + public static readonly Language SomaliDjibouti = AddLanguage("so-DJ", "Somali (Djibouti)", "Soomaali (Jabuuti)"); + public static readonly Language SomaliEthiopia = AddLanguage("so-ET", "Somali (Ethiopia)", "Soomaali (Itoobiya)"); + public static readonly Language SomaliKenya = AddLanguage("so-KE", "Somali (Kenya)", "Soomaali (Kenya)"); + public static readonly Language SomaliSomalia = AddLanguage("so-SO", "Somali (Somalia)", "Soomaali (Soomaaliya)"); + public static readonly Language AlbanianAlbania = AddLanguage("sq-AL", "Albanian (Albania)", "shqip (Shqipëri)"); + public static readonly Language AlbanianNorthMacedonia = AddLanguage("sq-MK", "Albanian (North Macedonia)", "shqip (Maqedonia e Veriut)"); + public static readonly Language AlbanianKosovo = AddLanguage("sq-XK", "Albanian (Kosovo)", "shqip (Kosovë)"); + public static readonly Language siSwatiEswatini = AddLanguage("ss-SZ", "siSwati (Eswatini)", "siSwati (eSwatini)"); + public static readonly Language siSwatiSouthAfrica = AddLanguage("ss-ZA", "siSwati (South Africa)", "siSwati (South Africa)"); + public static readonly Language SesothoLesotho = AddLanguage("st-LS", "Sesotho (Lesotho)", "Sesotho (Lesotho)"); + public static readonly Language SesothoSouthAfrica = AddLanguage("st-ZA", "Sesotho (South Africa)", "Sesotho (South Africa)"); + public static readonly Language SwedishÅlandIslands = AddLanguage("sv-AX", "Swedish (Åland Islands)", "svenska (Åland)"); + public static readonly Language SwedishFinland = AddLanguage("sv-FI", "Swedish (Finland)", "svenska (Finland)"); + public static readonly Language SwedishSweden = AddLanguage("sv-SE", "Swedish (Sweden)", "svenska (Sverige)"); + public static readonly Language KiswahiliCongoDRC = AddLanguage("sw-CD", "Kiswahili (Congo [DRC])", "Kiswahili (Jamhuri ya Kidemokrasia ya Kongo)"); + public static readonly Language KiswahiliKenya = AddLanguage("sw-KE", "Kiswahili (Kenya)", "Kiswahili (Kenya)"); + public static readonly Language KiswahiliTanzania = AddLanguage("sw-TZ", "Kiswahili (Tanzania)", "Kiswahili (Tanzania)"); + public static readonly Language KiswahiliUganda = AddLanguage("sw-UG", "Kiswahili (Uganda)", "Kiswahili (Uganda)"); + public static readonly Language TamilIndia = AddLanguage("ta-IN", "Tamil (India)", "தமிழ் (இந்தியா)"); + public static readonly Language TamilSriLanka = AddLanguage("ta-LK", "Tamil (Sri Lanka)", "தமிழ் (இலங்கை)"); + public static readonly Language TamilMalaysia = AddLanguage("ta-MY", "Tamil (Malaysia)", "தமிழ் (மலேசியா)"); + public static readonly Language TamilSingapore = AddLanguage("ta-SG", "Tamil (Singapore)", "தமிழ் (சிங்கப்பூர்)"); + public static readonly Language TeluguIndia = AddLanguage("te-IN", "Telugu (India)", "తెలుగు (భారతదేశం)"); + public static readonly Language TajikTajikistan = AddLanguage("tg-TJ", "Tajik (Tajikistan)", "тоҷикӣ (Тоҷикистон)"); + public static readonly Language ThaiThailand = AddLanguage("th-TH", "Thai (Thailand)", "ไทย (ไทย)"); + public static readonly Language TigrinyaEritrea = AddLanguage("ti-ER", "Tigrinya (Eritrea)", "ትግርኛ (ኤርትራ)"); + public static readonly Language TigrinyaEthiopia = AddLanguage("ti-ET", "Tigrinya (Ethiopia)", "ትግርኛ (ኢትዮጵያ)"); + public static readonly Language TurkmenTurkmenistan = AddLanguage("tk-TM", "Turkmen (Turkmenistan)", "türkmen dili (Türkmenistan)"); + public static readonly Language SetswanaBotswana = AddLanguage("tn-BW", "Setswana (Botswana)", "Setswana (Botswana)"); + public static readonly Language SetswanaSouthAfrica = AddLanguage("tn-ZA", "Setswana (South Africa)", "Setswana (South Africa)"); + public static readonly Language TonganTonga = AddLanguage("to-TO", "Tongan (Tonga)", "lea fakatonga (Tonga)"); + public static readonly Language TurkishCyprus = AddLanguage("tr-CY", "Turkish (Cyprus)", "Türkçe (Kıbrıs)"); + public static readonly Language TurkishTurkey = AddLanguage("tr-TR", "Turkish (Turkey)", "Türkçe (Türkiye)"); + public static readonly Language XitsongaSouthAfrica = AddLanguage("ts-ZA", "Xitsonga (South Africa)", "Xitsonga (South Africa)"); + public static readonly Language TatarRussia = AddLanguage("tt-RU", "Tatar (Russia)", "татар (Россия)"); + public static readonly Language UyghurChina = AddLanguage("ug-CN", "Uyghur (China)", "ئۇيغۇرچە (جۇڭگو)"); + public static readonly Language UkrainianUkraine = AddLanguage("uk-UA", "Ukrainian (Ukraine)", "українська (Україна)"); + public static readonly Language UrduIndia = AddLanguage("ur-IN", "Urdu (India)", "اردو (بھارت)"); + public static readonly Language UrduPakistan = AddLanguage("ur-PK", "Urdu (Pakistan)", "اردو (پاکستان)"); + public static readonly Language VendaSouthAfrica = AddLanguage("ve-ZA", "Venda (South Africa)", "Venda (South Africa)"); + public static readonly Language VietnameseVietnam = AddLanguage("vi-VN", "Vietnamese (Vietnam)", "Tiếng Việt (Việt Nam)"); + public static readonly Language WolofSenegal = AddLanguage("wo-SN", "Wolof (Senegal)", "Wolof (Senegaal)"); + public static readonly Language isiXhosaSouthAfrica = AddLanguage("xh-ZA", "isiXhosa (South Africa)", "isiXhosa (eMzantsi Afrika)"); + public static readonly Language YorubaBenin = AddLanguage("yo-BJ", "Yoruba (Benin)", "Èdè Yorùbá (Orílɛ́ède Bɛ̀nɛ̀)"); + public static readonly Language YorubaNigeria = AddLanguage("yo-NG", "Yoruba (Nigeria)", "Èdè Yorùbá (Orilẹ̀-èdè Nàìjíríà)"); + public static readonly Language isiZuluSouthAfrica = AddLanguage("zu-ZA", "isiZulu (South Africa)", "isiZulu (iNingizimu Afrika)"); } diff --git a/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs b/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs index 4803593dd8..8e5f93eb02 100644 --- a/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs +++ b/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs @@ -8,34 +8,33 @@ using Microsoft.Extensions.Options; using Squidex.Hosting; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public sealed class LanguagesInitializer : IInitializable { - public sealed class LanguagesInitializer : IInitializable - { - private readonly LanguagesOptions options; + private readonly LanguagesOptions options; - public LanguagesInitializer(IOptions<LanguagesOptions> options) - { - Guard.NotNull(options); + public LanguagesInitializer(IOptions<LanguagesOptions> options) + { + Guard.NotNull(options); - this.options = options.Value; - } + this.options = options.Value; + } - public Task InitializeAsync( - CancellationToken ct) + public Task InitializeAsync( + CancellationToken ct) + { + foreach (var (key, value) in options) { - foreach (var (key, value) in options) + if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value)) { - if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value)) + if (!Language.TryGetLanguage(key, out _)) { - if (!Language.TryGetLanguage(key, out _)) - { - Language.AddLanguage(key, value, value); - } + Language.AddLanguage(key, value, value); } } - - return Task.CompletedTask; } + + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Infrastructure/LanguagesOptions.cs b/backend/src/Squidex.Infrastructure/LanguagesOptions.cs index 4cb1630da4..6fbe86ada3 100644 --- a/backend/src/Squidex.Infrastructure/LanguagesOptions.cs +++ b/backend/src/Squidex.Infrastructure/LanguagesOptions.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public sealed class LanguagesOptions : Dictionary<string, string> { - public sealed class LanguagesOptions : Dictionary<string, string> - { - } } diff --git a/backend/src/Squidex.Infrastructure/Log/BackgroundRequestLogStore.cs b/backend/src/Squidex.Infrastructure/Log/BackgroundRequestLogStore.cs index c16205d2f3..4eda064c1a 100644 --- a/backend/src/Squidex.Infrastructure/Log/BackgroundRequestLogStore.cs +++ b/backend/src/Squidex.Infrastructure/Log/BackgroundRequestLogStore.cs @@ -10,114 +10,113 @@ using Microsoft.Extensions.Options; using Squidex.Infrastructure.Timers; -namespace Squidex.Infrastructure.Log +namespace Squidex.Infrastructure.Log; + +public sealed class BackgroundRequestLogStore : DisposableObjectBase, IRequestLogStore { - public sealed class BackgroundRequestLogStore : DisposableObjectBase, IRequestLogStore - { - private readonly IRequestLogRepository logRepository; - private readonly ILogger<BackgroundRequestLogStore> log; - private readonly CompletionTimer timer; - private readonly RequestLogStoreOptions options; - private ConcurrentQueue<Request> jobs = new ConcurrentQueue<Request>(); + private readonly IRequestLogRepository logRepository; + private readonly ILogger<BackgroundRequestLogStore> log; + private readonly CompletionTimer timer; + private readonly RequestLogStoreOptions options; + private ConcurrentQueue<Request> jobs = new ConcurrentQueue<Request>(); - public bool ForceWrite { get; set; } + public bool ForceWrite { get; set; } - public bool IsEnabled => options.StoreEnabled; + public bool IsEnabled => options.StoreEnabled; - public BackgroundRequestLogStore(IOptions<RequestLogStoreOptions> options, - IRequestLogRepository logRepository, ILogger<BackgroundRequestLogStore> log) - { - this.options = options.Value; + public BackgroundRequestLogStore(IOptions<RequestLogStoreOptions> options, + IRequestLogRepository logRepository, ILogger<BackgroundRequestLogStore> log) + { + this.options = options.Value; - this.logRepository = logRepository; + this.logRepository = logRepository; - timer = new CompletionTimer(options.Value.WriteIntervall, TrackAsync, options.Value.WriteIntervall); + timer = new CompletionTimer(options.Value.WriteIntervall, TrackAsync, options.Value.WriteIntervall); - this.log = log; - } + this.log = log; + } - protected override void DisposeObject(bool disposing) + protected override void DisposeObject(bool disposing) + { + if (disposing) { - if (disposing) - { - timer.StopAsync().Wait(); - } + timer.StopAsync().Wait(); } + } - public void Next() - { - ThrowIfDisposed(); + public void Next() + { + ThrowIfDisposed(); + + timer.SkipCurrentDelay(); + } - timer.SkipCurrentDelay(); + private async Task TrackAsync( + CancellationToken ct) + { + if (!IsEnabled) + { + return; } - private async Task TrackAsync( - CancellationToken ct) + try { - if (!IsEnabled) - { - return; - } + var batchSize = options.BatchSize; - try - { - var batchSize = options.BatchSize; + var localJobs = Interlocked.Exchange(ref jobs, new ConcurrentQueue<Request>()); - var localJobs = Interlocked.Exchange(ref jobs, new ConcurrentQueue<Request>()); + if (!localJobs.IsEmpty) + { + var pages = (int)Math.Ceiling((double)localJobs.Count / batchSize); - if (!localJobs.IsEmpty) + for (var i = 0; i < pages; i++) { - var pages = (int)Math.Ceiling((double)localJobs.Count / batchSize); + var batch = localJobs.Skip(i * batchSize).Take(batchSize); - for (var i = 0; i < pages; i++) + if (ForceWrite) { - var batch = localJobs.Skip(i * batchSize).Take(batchSize); - - if (ForceWrite) - { - ct = default; - } - - await logRepository.InsertManyAsync(batch, ct); + ct = default; } + + await logRepository.InsertManyAsync(batch, ct); } } - catch (Exception ex) - { - log.LogError(ex, "Failed to track usage in background."); - } } - - public Task DeleteAsync(string key, - CancellationToken ct = default) + catch (Exception ex) { - return logRepository.DeleteAsync(key, ct); + log.LogError(ex, "Failed to track usage in background."); } + } - public IAsyncEnumerable<Request> QueryAllAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default) - { - if (!IsEnabled) - { - return AsyncEnumerable.Empty<Request>(); - } - - return logRepository.QueryAllAsync(key, fromDate, toDate, ct); - } + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + return logRepository.DeleteAsync(key, ct); + } - public Task LogAsync(Request request, - CancellationToken ct = default) + public IAsyncEnumerable<Request> QueryAllAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + if (!IsEnabled) { - Guard.NotNull(request); + return AsyncEnumerable.Empty<Request>(); + } - if (!IsEnabled) - { - return Task.CompletedTask; - } + return logRepository.QueryAllAsync(key, fromDate, toDate, ct); + } - jobs.Enqueue(request); + public Task LogAsync(Request request, + CancellationToken ct = default) + { + Guard.NotNull(request); + if (!IsEnabled) + { return Task.CompletedTask; } + + jobs.Enqueue(request); + + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Infrastructure/Log/IRequestLogRepository.cs b/backend/src/Squidex.Infrastructure/Log/IRequestLogRepository.cs index 04cfc60bdc..cd96527745 100644 --- a/backend/src/Squidex.Infrastructure/Log/IRequestLogRepository.cs +++ b/backend/src/Squidex.Infrastructure/Log/IRequestLogRepository.cs @@ -5,17 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Log +namespace Squidex.Infrastructure.Log; + +public interface IRequestLogRepository { - public interface IRequestLogRepository - { - Task InsertManyAsync(IEnumerable<Request> items, - CancellationToken ct = default); + Task InsertManyAsync(IEnumerable<Request> items, + CancellationToken ct = default); - Task DeleteAsync(string key, - CancellationToken ct = default); + Task DeleteAsync(string key, + CancellationToken ct = default); - IAsyncEnumerable<Request> QueryAllAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default); - } + IAsyncEnumerable<Request> QueryAllAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/Log/IRequestLogStore.cs b/backend/src/Squidex.Infrastructure/Log/IRequestLogStore.cs index 347cc73c34..849536216b 100644 --- a/backend/src/Squidex.Infrastructure/Log/IRequestLogStore.cs +++ b/backend/src/Squidex.Infrastructure/Log/IRequestLogStore.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Log +namespace Squidex.Infrastructure.Log; + +public interface IRequestLogStore { - public interface IRequestLogStore - { - bool IsEnabled { get; } + bool IsEnabled { get; } - Task LogAsync(Request request, - CancellationToken ct = default); + Task LogAsync(Request request, + CancellationToken ct = default); - Task DeleteAsync(string key, - CancellationToken ct = default); + Task DeleteAsync(string key, + CancellationToken ct = default); - IAsyncEnumerable<Request> QueryAllAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default); - } + IAsyncEnumerable<Request> QueryAllAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/Log/Request.cs b/backend/src/Squidex.Infrastructure/Log/Request.cs index d53e7e0ad6..06cb700ad3 100644 --- a/backend/src/Squidex.Infrastructure/Log/Request.cs +++ b/backend/src/Squidex.Infrastructure/Log/Request.cs @@ -9,14 +9,13 @@ #pragma warning disable SA1401 // Fields should be private -namespace Squidex.Infrastructure.Log +namespace Squidex.Infrastructure.Log; + +public sealed class Request { - public sealed class Request - { - public Instant Timestamp; + public Instant Timestamp; - public string Key; + public string Key; - public Dictionary<string, string> Properties = new Dictionary<string, string>(); - } + public Dictionary<string, string> Properties = new Dictionary<string, string>(); } diff --git a/backend/src/Squidex.Infrastructure/Log/RequestLogStoreOptions.cs b/backend/src/Squidex.Infrastructure/Log/RequestLogStoreOptions.cs index c7da4b2d6f..436f7d1825 100644 --- a/backend/src/Squidex.Infrastructure/Log/RequestLogStoreOptions.cs +++ b/backend/src/Squidex.Infrastructure/Log/RequestLogStoreOptions.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Log +namespace Squidex.Infrastructure.Log; + +public sealed class RequestLogStoreOptions { - public sealed class RequestLogStoreOptions - { - public bool StoreEnabled { get; set; } + public bool StoreEnabled { get; set; } - public int StoreRetentionInDays { get; set; } = 90; + public int StoreRetentionInDays { get; set; } = 90; - public int BatchSize { get; set; } = 1000; + public int BatchSize { get; set; } = 1000; - public int WriteIntervall { get; set; } = 1000; - } + public int WriteIntervall { get; set; } = 1000; } diff --git a/backend/src/Squidex.Infrastructure/Migrations/IMigrated.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrated.cs index eeb3755528..4a05575959 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/IMigrated.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/IMigrated.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +public interface IMigrated<out T> { - public interface IMigrated<out T> - { - T Migrate(); - } + T Migrate(); } diff --git a/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs index 7d8550b558..badc786b4a 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +public interface IMigration { - public interface IMigration - { - Task UpdateAsync( - CancellationToken ct); - } + Task UpdateAsync( + CancellationToken ct); } diff --git a/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs index 03552736c5..b9a3373d68 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/IMigrationPath.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +public interface IMigrationPath { - public interface IMigrationPath - { - (int Version, IEnumerable<IMigration>? Migrations) GetNext(int version); - } + (int Version, IEnumerable<IMigration>? Migrations) GetNext(int version); } diff --git a/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs index 096705f1fe..688302285d 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +public interface IMigrationStatus { - public interface IMigrationStatus - { - Task<int> GetVersionAsync( - CancellationToken ct = default); + Task<int> GetVersionAsync( + CancellationToken ct = default); - Task<bool> TryLockAsync( - CancellationToken ct = default); + Task<bool> TryLockAsync( + CancellationToken ct = default); - Task CompleteAsync(int newVersion, - CancellationToken ct = default); + Task CompleteAsync(int newVersion, + CancellationToken ct = default); - Task UnlockAsync( - CancellationToken ct = default); - } + Task UnlockAsync( + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs b/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs index 93af105a01..0075e664b5 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/MigrationFailedException.cs @@ -7,33 +7,32 @@ using System.Runtime.Serialization; -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +[Serializable] +public class MigrationFailedException : Exception { - [Serializable] - public class MigrationFailedException : Exception - { - public string Name { get; } + public string Name { get; } - public MigrationFailedException(string name, Exception? inner = null) - : base(FormatException(name), inner) - { - Name = name; - } + public MigrationFailedException(string name, Exception? inner = null) + : base(FormatException(name), inner) + { + Name = name; + } - protected MigrationFailedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Name = info.GetString(nameof(Name))!; - } + protected MigrationFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Name = info.GetString(nameof(Name))!; + } - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Name), Name); - } + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Name), Name); + } - private static string FormatException(string name) - { - return $"Failed to run migration '{name}'"; - } + private static string FormatException(string name) + { + return $"Failed to run migration '{name}'"; } } diff --git a/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs index d8f722d2f1..7b46b87914 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -7,100 +7,99 @@ using Microsoft.Extensions.Logging; -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +public sealed class Migrator { - public sealed class Migrator + private readonly IMigrationStatus migrationStatus; + private readonly IMigrationPath migrationPath; + private readonly ILogger<Migrator> log; + + public int LockWaitMs { get; set; } = 500; + + public Migrator(IMigrationStatus migrationStatus, IMigrationPath migrationPath, + ILogger<Migrator> log) { - private readonly IMigrationStatus migrationStatus; - private readonly IMigrationPath migrationPath; - private readonly ILogger<Migrator> log; + this.migrationStatus = migrationStatus; + this.migrationPath = migrationPath; - public int LockWaitMs { get; set; } = 500; + this.log = log; + } - public Migrator(IMigrationStatus migrationStatus, IMigrationPath migrationPath, - ILogger<Migrator> log) + public async Task MigrateAsync( + CancellationToken ct = default) + { + if (!await TryLockAsync(ct)) { - this.migrationStatus = migrationStatus; - this.migrationPath = migrationPath; - - this.log = log; + return; } - public async Task MigrateAsync( - CancellationToken ct = default) + try { - if (!await TryLockAsync(ct)) - { - return; - } + var version = await migrationStatus.GetVersionAsync(ct); - try + while (!ct.IsCancellationRequested) { - var version = await migrationStatus.GetVersionAsync(ct); + var (newVersion, migrations) = migrationPath.GetNext(version); - while (!ct.IsCancellationRequested) + if (migrations == null || !migrations.Any()) { - var (newVersion, migrations) = migrationPath.GetNext(version); - - if (migrations == null || !migrations.Any()) - { - break; - } + break; + } - foreach (var migration in migrations) - { - var name = migration.ToString()!; + foreach (var migration in migrations) + { + var name = migration.ToString()!; - log.LogInformation("Migration {migration} started.", name); + log.LogInformation("Migration {migration} started.", name); - try - { - var watch = ValueStopwatch.StartNew(); + try + { + var watch = ValueStopwatch.StartNew(); - await migration.UpdateAsync(ct); + await migration.UpdateAsync(ct); - log.LogInformation("Migration {migration} completed after {time}ms.", name, watch.Stop()); - } - catch (Exception ex) - { - log.LogCritical(ex, "Migration {migration} failed.", name); - throw new MigrationFailedException(name, ex); - } + log.LogInformation("Migration {migration} completed after {time}ms.", name, watch.Stop()); + } + catch (Exception ex) + { + log.LogCritical(ex, "Migration {migration} failed.", name); + throw new MigrationFailedException(name, ex); } + } - version = newVersion; + version = newVersion; - await migrationStatus.CompleteAsync(newVersion, ct); - } - } - finally - { - await UnlockAsync(); + await migrationStatus.CompleteAsync(newVersion, ct); } } + finally + { + await UnlockAsync(); + } + } - private async Task<bool> TryLockAsync( - CancellationToken ct) + private async Task<bool> TryLockAsync( + CancellationToken ct) + { + try { - try + while (!await migrationStatus.TryLockAsync(ct)) { - while (!await migrationStatus.TryLockAsync(ct)) - { - log.LogInformation("Could not acquire lock to start migrating. Tryping again in {time}ms.", LockWaitMs); - await Task.Delay(LockWaitMs, ct); - } + log.LogInformation("Could not acquire lock to start migrating. Tryping again in {time}ms.", LockWaitMs); + await Task.Delay(LockWaitMs, ct); } - catch (OperationCanceledException) - { - return false; - } - - return true; } - - private Task UnlockAsync() + catch (OperationCanceledException) { - return migrationStatus.UnlockAsync(); + return false; } + + return true; + } + + private Task UnlockAsync() + { + return migrationStatus.UnlockAsync(); } } diff --git a/backend/src/Squidex.Infrastructure/NamedId.cs b/backend/src/Squidex.Infrastructure/NamedId.cs index c582a77ad6..00bcf7956a 100644 --- a/backend/src/Squidex.Infrastructure/NamedId.cs +++ b/backend/src/Squidex.Infrastructure/NamedId.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class NamedId { - public static class NamedId + public static NamedId<T> Of<T>(T id, string name) where T : notnull { - public static NamedId<T> Of<T>(T id, string name) where T : notnull - { - return new NamedId<T>(id, name); - } + return new NamedId<T>(id, name); } } diff --git a/backend/src/Squidex.Infrastructure/NamedIdTypeConverter.cs b/backend/src/Squidex.Infrastructure/NamedIdTypeConverter.cs index 63883234c6..95376fd2f2 100644 --- a/backend/src/Squidex.Infrastructure/NamedIdTypeConverter.cs +++ b/backend/src/Squidex.Infrastructure/NamedIdTypeConverter.cs @@ -8,76 +8,75 @@ using System.ComponentModel; using System.Globalization; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +internal sealed class NamedIdTypeConverter : TypeConverter { - internal sealed class NamedIdTypeConverter : TypeConverter + private static readonly Parser<Guid> ParserGuid = Guid.TryParse; + private static readonly Parser<DomainId> ParserDomainId = ParseDomainId; + private static readonly Parser<string> ParserString = ParseString; + private static readonly Parser<long> ParserLong = long.TryParse; + private readonly Func<string, object>? converter; + + public NamedIdTypeConverter(Type type) { - private static readonly Parser<Guid> ParserGuid = Guid.TryParse; - private static readonly Parser<DomainId> ParserDomainId = ParseDomainId; - private static readonly Parser<string> ParserString = ParseString; - private static readonly Parser<long> ParserLong = long.TryParse; - private readonly Func<string, object>? converter; + var genericType = type?.GetGenericArguments()?[0]; - public NamedIdTypeConverter(Type type) + if (genericType == typeof(Guid)) { - var genericType = type?.GetGenericArguments()?[0]; - - if (genericType == typeof(Guid)) - { - converter = v => NamedId<Guid>.Parse(v, ParserGuid); - } - else if (genericType == typeof(DomainId)) - { - converter = v => NamedId<DomainId>.Parse(v, ParserDomainId); - } - else if (genericType == typeof(string)) - { - converter = v => NamedId<string>.Parse(v, ParserString); - } - else if (genericType == typeof(long)) - { - converter = v => NamedId<long>.Parse(v, ParserLong); - } + converter = v => NamedId<Guid>.Parse(v, ParserGuid); } - - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + else if (genericType == typeof(DomainId)) { - return sourceType == typeof(string); + converter = v => NamedId<DomainId>.Parse(v, ParserDomainId); } - - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + else if (genericType == typeof(string)) { - return destinationType == typeof(string); + converter = v => NamedId<string>.Parse(v, ParserString); } - - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + else if (genericType == typeof(long)) { - if (converter == null) - { - ThrowHelper.NotSupportedException(); - return default!; - } - - return converter((string)value); + converter = v => NamedId<long>.Parse(v, ParserLong); } + } + + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string); + } - public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (converter == null) { - return value?.ToString()!; + ThrowHelper.NotSupportedException(); + return default!; } - private static bool ParseDomainId(ReadOnlySpan<char> value, out DomainId result) - { - result = DomainId.Create(new string(value)); + return converter((string)value); + } - return true; - } + public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) + { + return value?.ToString()!; + } - private static bool ParseString(ReadOnlySpan<char> value, out string result) - { - result = new string(value); + private static bool ParseDomainId(ReadOnlySpan<char> value, out DomainId result) + { + result = DomainId.Create(new string(value)); - return true; - } + return true; + } + + private static bool ParseString(ReadOnlySpan<char> value, out string result) + { + result = new string(value); + + return true; } } diff --git a/backend/src/Squidex.Infrastructure/NamedId{T}.cs b/backend/src/Squidex.Infrastructure/NamedId{T}.cs index 7f922946d3..7725bafd3c 100644 --- a/backend/src/Squidex.Infrastructure/NamedId{T}.cs +++ b/backend/src/Squidex.Infrastructure/NamedId{T}.cs @@ -10,82 +10,81 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public delegate bool Parser<T>(ReadOnlySpan<char> input, out T result); + +[TypeConverter(typeof(NamedIdTypeConverter))] +public sealed record NamedId<T> where T : notnull { - public delegate bool Parser<T>(ReadOnlySpan<char> input, out T result); + private static readonly int GuidLength = Guid.Empty.ToString().Length; - [TypeConverter(typeof(NamedIdTypeConverter))] - public sealed record NamedId<T> where T : notnull - { - private static readonly int GuidLength = Guid.Empty.ToString().Length; + public T Id { get; } - public T Id { get; } + public string Name { get; } - public string Name { get; } + public NamedId(T id, string name) + { + Guard.NotNull(id); + Guard.NotNull(name); - public NamedId(T id, string name) - { - Guard.NotNull(id); - Guard.NotNull(name); + Id = id; - Id = id; + Name = name; + } - Name = name; - } + public override string ToString() + { + return $"{Id},{Name}"; + } - public override string ToString() + public static bool TryParse(string value, Parser<T> parser, [MaybeNullWhen(false)] out NamedId<T> result) + { + if (value != null) { - return $"{Id},{Name}"; - } + var span = value.AsSpan(); - public static bool TryParse(string value, Parser<T> parser, [MaybeNullWhen(false)] out NamedId<T> result) - { - if (value != null) + if (typeof(T) == typeof(Guid)) { - var span = value.AsSpan(); - - if (typeof(T) == typeof(Guid)) + if (value.Length > GuidLength + 1 && value[GuidLength] == ',') { - if (value.Length > GuidLength + 1 && value[GuidLength] == ',') + if (parser(span[..GuidLength], out var id)) { - if (parser(span[..GuidLength], out var id)) - { - result = new NamedId<T>(id, value[(GuidLength + 1)..]); + result = new NamedId<T>(id, value[(GuidLength + 1)..]); - return true; - } + return true; } } - else - { - var index = value.IndexOf(',', StringComparison.Ordinal); + } + else + { + var index = value.IndexOf(',', StringComparison.Ordinal); - if (index > 0 && index < value.Length - 1) + if (index > 0 && index < value.Length - 1) + { + if (parser(span[..index], out var id)) { - if (parser(span[..index], out var id)) - { - result = new NamedId<T>(id, value[(index + 1)..]); + result = new NamedId<T>(id, value[(index + 1)..]); - return true; - } + return true; } } } + } - result = null!; + result = null!; - return false; - } + return false; + } - public static NamedId<T> Parse(string value, Parser<T> parser) + public static NamedId<T> Parse(string value, Parser<T> parser) + { + if (!TryParse(value, parser, out var result)) { - if (!TryParse(value, parser, out var result)) - { - ThrowHelper.ArgumentException("Named id must have at least 2 parts divided by commata.", nameof(value)); - return default!; - } - - return result; + ThrowHelper.ArgumentException("Named id must have at least 2 parts divided by commata.", nameof(value)); + return default!; } + + return result; } } diff --git a/backend/src/Squidex.Infrastructure/Net/IPAddressComparer.cs b/backend/src/Squidex.Infrastructure/Net/IPAddressComparer.cs index 496137d96e..26a22d5e84 100644 --- a/backend/src/Squidex.Infrastructure/Net/IPAddressComparer.cs +++ b/backend/src/Squidex.Infrastructure/Net/IPAddressComparer.cs @@ -7,40 +7,39 @@ using System.Net; -namespace Squidex.Infrastructure.Net +namespace Squidex.Infrastructure.Net; + +public sealed class IPAddressComparer : IComparer<IPAddress> { - public sealed class IPAddressComparer : IComparer<IPAddress> + public static readonly IPAddressComparer Instance = new IPAddressComparer(); + + private IPAddressComparer() { - public static readonly IPAddressComparer Instance = new IPAddressComparer(); + } - private IPAddressComparer() + public int Compare(IPAddress? x, IPAddress? y) + { + if (x == null || y == null) { + return 0; } - public int Compare(IPAddress? x, IPAddress? y) - { - if (x == null || y == null) - { - return 0; - } - - var lbytes = x.GetAddressBytes(); - var rbytes = y.GetAddressBytes(); + var lbytes = x.GetAddressBytes(); + var rbytes = y.GetAddressBytes(); - if (lbytes.Length != rbytes.Length) - { - return lbytes.Length - rbytes.Length; - } + if (lbytes.Length != rbytes.Length) + { + return lbytes.Length - rbytes.Length; + } - for (var i = 0; i < lbytes.Length; i++) + for (var i = 0; i < lbytes.Length; i++) + { + if (lbytes[i] != rbytes[i]) { - if (lbytes[i] != rbytes[i]) - { - return lbytes[i] - rbytes[i]; - } + return lbytes[i] - rbytes[i]; } - - return 0; } + + return 0; } } diff --git a/backend/src/Squidex.Infrastructure/None.cs b/backend/src/Squidex.Infrastructure/None.cs index d330364ec5..a1e5c56ae3 100644 --- a/backend/src/Squidex.Infrastructure/None.cs +++ b/backend/src/Squidex.Infrastructure/None.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public sealed record None { - public sealed record None - { - public static readonly Type Type = typeof(None); + public static readonly Type Type = typeof(None); - public static readonly None Value = new None(); + public static readonly None Value = new None(); - private None() - { - } + private None() + { } } diff --git a/backend/src/Squidex.Infrastructure/ObjectPool/DefaultPools.cs b/backend/src/Squidex.Infrastructure/ObjectPool/DefaultPools.cs index c782f0affe..d4db14c67d 100644 --- a/backend/src/Squidex.Infrastructure/ObjectPool/DefaultPools.cs +++ b/backend/src/Squidex.Infrastructure/ObjectPool/DefaultPools.cs @@ -9,14 +9,13 @@ using Microsoft.Extensions.ObjectPool; using Microsoft.IO; -namespace Squidex.Infrastructure.ObjectPool +namespace Squidex.Infrastructure.ObjectPool; + +public static class DefaultPools { - public static class DefaultPools - { - public static readonly RecyclableMemoryStreamManager MemoryStream = - new RecyclableMemoryStreamManager(); + public static readonly RecyclableMemoryStreamManager MemoryStream = + new RecyclableMemoryStreamManager(); - public static readonly ObjectPool<StringBuilder> StringBuilder = - new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy()); - } + public static readonly ObjectPool<StringBuilder> StringBuilder = + new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy()); } diff --git a/backend/src/Squidex.Infrastructure/Plugins/IPlugin.cs b/backend/src/Squidex.Infrastructure/Plugins/IPlugin.cs index 9d5a2618ed..acf3893578 100644 --- a/backend/src/Squidex.Infrastructure/Plugins/IPlugin.cs +++ b/backend/src/Squidex.Infrastructure/Plugins/IPlugin.cs @@ -8,10 +8,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace Squidex.Infrastructure.Plugins +namespace Squidex.Infrastructure.Plugins; + +public interface IPlugin { - public interface IPlugin - { - void ConfigureServices(IServiceCollection services, IConfiguration config); - } + void ConfigureServices(IServiceCollection services, IConfiguration config); } diff --git a/backend/src/Squidex.Infrastructure/Plugins/PluginLoaders.cs b/backend/src/Squidex.Infrastructure/Plugins/PluginLoaders.cs index 3f0b4f382f..445da03b2e 100644 --- a/backend/src/Squidex.Infrastructure/Plugins/PluginLoaders.cs +++ b/backend/src/Squidex.Infrastructure/Plugins/PluginLoaders.cs @@ -9,54 +9,53 @@ using System.Reflection; using McMaster.NETCore.Plugins; -namespace Squidex.Infrastructure.Plugins +namespace Squidex.Infrastructure.Plugins; + +[ExcludeFromCodeCoverage] +public static class PluginLoaders { - [ExcludeFromCodeCoverage] - public static class PluginLoaders + public static PluginLoader? LoadPlugin(string pluginPath, AssemblyName[] sharedAssemblies) { - public static PluginLoader? LoadPlugin(string pluginPath, AssemblyName[] sharedAssemblies) + foreach (var candidate in GetPaths(pluginPath)) { - foreach (var candidate in GetPaths(pluginPath)) + if (candidate.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)) { - if (candidate.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)) + return PluginLoader.CreateFromAssemblyFile(candidate.FullName, config => { - return PluginLoader.CreateFromAssemblyFile(candidate.FullName, config => - { - config.PreferSharedTypes = true; + config.PreferSharedTypes = true; - config.SharedAssemblies.AddRange(sharedAssemblies); - }); - } + config.SharedAssemblies.AddRange(sharedAssemblies); + }); } - - return null; } - private static IEnumerable<FileInfo> GetPaths(string pluginPath) + return null; + } + + private static IEnumerable<FileInfo> GetPaths(string pluginPath) + { + var candidate = new FileInfo(Path.GetFullPath(pluginPath)); + + if (candidate.Exists) { - var candidate = new FileInfo(Path.GetFullPath(pluginPath)); + yield return candidate; + } - if (candidate.Exists) - { - yield return candidate; - } + if (!Path.IsPathRooted(pluginPath)) + { + var assembly = Assembly.GetEntryAssembly(); - if (!Path.IsPathRooted(pluginPath)) + if (assembly != null) { - var assembly = Assembly.GetEntryAssembly(); + var directory = Path.GetDirectoryName(assembly.Location); - if (assembly != null) + if (directory != null) { - var directory = Path.GetDirectoryName(assembly.Location); + candidate = new FileInfo(Path.Combine(directory, pluginPath)); - if (directory != null) + if (candidate.Exists) { - candidate = new FileInfo(Path.Combine(directory, pluginPath)); - - if (candidate.Exists) - { - yield return candidate; - } + yield return candidate; } } } diff --git a/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs b/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs index 46e4786fb5..758f7041b6 100644 --- a/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs +++ b/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs @@ -11,127 +11,126 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Log; -namespace Squidex.Infrastructure.Plugins +namespace Squidex.Infrastructure.Plugins; + +public sealed class PluginManager : DisposableObjectBase { - public sealed class PluginManager : DisposableObjectBase - { - private readonly HashSet<PluginLoader> pluginLoaders = new HashSet<PluginLoader>(); - private readonly HashSet<IPlugin> loadedPlugins = new HashSet<IPlugin>(); - private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>(); + private readonly HashSet<PluginLoader> pluginLoaders = new HashSet<PluginLoader>(); + private readonly HashSet<IPlugin> loadedPlugins = new HashSet<IPlugin>(); + private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>(); - protected override void DisposeObject(bool disposing) + protected override void DisposeObject(bool disposing) + { + if (disposing) { - if (disposing) + foreach (var loader in pluginLoaders) { - foreach (var loader in pluginLoaders) - { - loader.Dispose(); - } + loader.Dispose(); } } + } - public Assembly? Load(string path, AssemblyName[] sharedAssemblies) - { - Guard.NotNullOrEmpty(path); - Guard.NotNull(sharedAssemblies); + public Assembly? Load(string path, AssemblyName[] sharedAssemblies) + { + Guard.NotNullOrEmpty(path); + Guard.NotNull(sharedAssemblies); - Assembly? assembly = null; + Assembly? assembly = null; - var loader = PluginLoaders.LoadPlugin(path, sharedAssemblies); + var loader = PluginLoaders.LoadPlugin(path, sharedAssemblies); - if (loader != null) + if (loader != null) + { + try { - try - { - assembly = loader.LoadDefaultAssembly(); + assembly = loader.LoadDefaultAssembly(); - Add(path, assembly); - - pluginLoaders.Add(loader); - } - catch (Exception ex) - { - LogException(path, "LoadingAssembly", ex); + Add(path, assembly); - loader.Dispose(); - } + pluginLoaders.Add(loader); } - else + catch (Exception ex) { - LogException(path, "LoadingPlugin", new FileNotFoundException($"Cannot find plugin at {path}")); - } - - return assembly; - } - - private void Add(string name, Assembly assembly) - { - var pluginTypes = - assembly.GetTypes() - .Where(t => typeof(IPlugin).IsAssignableFrom(t)) - .Where(t => !t.IsAbstract); + LogException(path, "LoadingAssembly", ex); - foreach (var pluginType in pluginTypes) - { - try - { - var plugin = (IPlugin)Activator.CreateInstance(pluginType)!; - - loadedPlugins.Add(plugin); - } - catch (Exception ex) - { - LogException(name, "Instantiating", ex); - } + loader.Dispose(); } } - - private void LogException(string plugin, string action, Exception exception) + else { - exceptions.Add((plugin, action, exception)); + LogException(path, "LoadingPlugin", new FileNotFoundException($"Cannot find plugin at {path}")); } - public void ConfigureServices(IServiceCollection services, IConfiguration config) + return assembly; + } + + private void Add(string name, Assembly assembly) + { + var pluginTypes = + assembly.GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t)) + .Where(t => !t.IsAbstract); + + foreach (var pluginType in pluginTypes) { - Guard.NotNull(services); - Guard.NotNull(config); + try + { + var plugin = (IPlugin)Activator.CreateInstance(pluginType)!; - foreach (var plugin in loadedPlugins) + loadedPlugins.Add(plugin); + } + catch (Exception ex) { - plugin.ConfigureServices(services, config); + LogException(name, "Instantiating", ex); } } + } - public void Log(ISemanticLog log) + private void LogException(string plugin, string action, Exception exception) + { + exceptions.Add((plugin, action, exception)); + } + + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + Guard.NotNull(services); + Guard.NotNull(config); + + foreach (var plugin in loadedPlugins) { - Guard.NotNull(log); + plugin.ConfigureServices(services, config); + } + } - if (loadedPlugins.Count > 0 || exceptions.Count > 0) - { - var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed"; + public void Log(ISemanticLog log) + { + Guard.NotNull(log); - log.LogInformation(w => w - .WriteProperty("message", "Plugins loaded.") - .WriteProperty("action", "pluginsLoaded") - .WriteProperty("status", status) - .WriteArray("errors", e => + if (loadedPlugins.Count > 0 || exceptions.Count > 0) + { + var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed"; + + log.LogInformation(w => w + .WriteProperty("message", "Plugins loaded.") + .WriteProperty("action", "pluginsLoaded") + .WriteProperty("status", status) + .WriteArray("errors", e => + { + foreach (var (plugin, action, exception) in exceptions) { - foreach (var (plugin, action, exception) in exceptions) - { - e.WriteObject(x => x - .WriteProperty("plugin", plugin) - .WriteProperty("action", action) - .WriteException(exception)); - } - }) - .WriteArray("plugins", a => + e.WriteObject(x => x + .WriteProperty("plugin", plugin) + .WriteProperty("action", action) + .WriteException(exception)); + } + }) + .WriteArray("plugins", a => + { + foreach (var plugin in loadedPlugins) { - foreach (var plugin in loadedPlugins) - { - a.WriteValue(plugin.GetType().ToString()); - } - })); - } + a.WriteValue(plugin.GetType().ToString()); + } + })); } } } diff --git a/backend/src/Squidex.Infrastructure/Plugins/PluginOptions.cs b/backend/src/Squidex.Infrastructure/Plugins/PluginOptions.cs index 561e0845af..7d712fcaae 100644 --- a/backend/src/Squidex.Infrastructure/Plugins/PluginOptions.cs +++ b/backend/src/Squidex.Infrastructure/Plugins/PluginOptions.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Plugins +namespace Squidex.Infrastructure.Plugins; + +public sealed class PluginOptions { - public sealed class PluginOptions - { - public string[] Plugins { get; set; } - } + public string[] Plugins { get; set; } } diff --git a/backend/src/Squidex.Infrastructure/Properties/Resources.Designer.cs b/backend/src/Squidex.Infrastructure/Properties/Resources.Designer.cs index 3c819e9b94..4929a2f7b0 100644 --- a/backend/src/Squidex.Infrastructure/Properties/Resources.Designer.cs +++ b/backend/src/Squidex.Infrastructure/Properties/Resources.Designer.cs @@ -8,155 +8,154 @@ // </auto-generated> //------------------------------------------------------------------------------ -namespace Squidex.Infrastructure.Properties { - using System; +namespace Squidex.Infrastructure.Properties; +using System; + + +/// <summary> +/// A strongly-typed resource class, for looking up localized strings, etc. +/// </summary> +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } /// <summary> - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Returns the cached ResourceManager instance used by this class. /// </summary> - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// <summary> - /// Returns the cached ResourceManager instance used by this class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Infrastructure.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Infrastructure.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// <summary> - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// <summary> - /// Looks up a localized string similar to Json query not valid: {0}.. - /// </summary> - internal static string QueryInvalid { - get { - return ResourceManager.GetString("QueryInvalid", resourceCulture); - } + set { + resourceCulture = value; } - - /// <summary> - /// Looks up a localized string similar to Array value is not allowed for '{0}' operator and path '{1}'.. - /// </summary> - internal static string QueryInvalidArray { - get { - return ResourceManager.GetString("QueryInvalidArray", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Json query not valid: {0}.. + /// </summary> + internal static string QueryInvalid { + get { + return ResourceManager.GetString("QueryInvalid", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Json query not valid json: {0}.. - /// </summary> - internal static string QueryInvalidJson { - get { - return ResourceManager.GetString("QueryInvalidJson", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Array value is not allowed for '{0}' operator and path '{1}'.. + /// </summary> + internal static string QueryInvalidArray { + get { + return ResourceManager.GetString("QueryInvalidArray", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Either 'and', 'or', 'not' or 'path'+'op' must be set.. - /// </summary> - internal static string QueryInvalidJsonStructure { - get { - return ResourceManager.GetString("QueryInvalidJsonStructure", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Json query not valid json: {0}.. + /// </summary> + internal static string QueryInvalidJson { + get { + return ResourceManager.GetString("QueryInvalidJson", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to '{0}' is not a valid operator for type {1} at '{2}'.. - /// </summary> - internal static string QueryInvalidOperator { - get { - return ResourceManager.GetString("QueryInvalidOperator", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Either 'and', 'or', 'not' or 'path'+'op' must be set.. + /// </summary> + internal static string QueryInvalidJsonStructure { + get { + return ResourceManager.GetString("QueryInvalidJsonStructure", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Path '{0}' does not point to a valid property in the model.. - /// </summary> - internal static string QueryInvalidPath { - get { - return ResourceManager.GetString("QueryInvalidPath", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to '{0}' is not a valid operator for type {1} at '{2}'.. + /// </summary> + internal static string QueryInvalidOperator { + get { + return ResourceManager.GetString("QueryInvalidOperator", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to {0} is not a valid regular expression at path '{1}'.. - /// </summary> - internal static string QueryInvalidRegex { - get { - return ResourceManager.GetString("QueryInvalidRegex", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Path '{0}' does not point to a valid property in the model.. + /// </summary> + internal static string QueryInvalidPath { + get { + return ResourceManager.GetString("QueryInvalidPath", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Expected {0} for path '{2}', but got {1}.. - /// </summary> - internal static string QueryWrongExpectedType { - get { - return ResourceManager.GetString("QueryWrongExpectedType", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to {0} is not a valid regular expression at path '{1}'.. + /// </summary> + internal static string QueryInvalidRegex { + get { + return ResourceManager.GetString("QueryInvalidRegex", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Expected {0} for path '{1}', but got invalid String.. - /// </summary> - internal static string QueryWrongFormat { - get { - return ResourceManager.GetString("QueryWrongFormat", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Expected {0} for path '{2}', but got {1}.. + /// </summary> + internal static string QueryWrongExpectedType { + get { + return ResourceManager.GetString("QueryWrongExpectedType", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Expected primitive for path '{1}', but got {0}.. - /// </summary> - internal static string QueryWrongPrimitive { - get { - return ResourceManager.GetString("QueryWrongPrimitive", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Expected {0} for path '{1}', but got invalid String.. + /// </summary> + internal static string QueryWrongFormat { + get { + return ResourceManager.GetString("QueryWrongFormat", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Unsupported type {0} for path '{1}'.. - /// </summary> - internal static string QueryWrongType { - get { - return ResourceManager.GetString("QueryWrongType", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Expected primitive for path '{1}', but got {0}.. + /// </summary> + internal static string QueryWrongPrimitive { + get { + return ResourceManager.GetString("QueryWrongPrimitive", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unsupported type {0} for path '{1}'.. + /// </summary> + internal static string QueryWrongType { + get { + return ResourceManager.GetString("QueryWrongType", resourceCulture); } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/AsyncTransformVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/AsyncTransformVisitor.cs index 9f49161526..9aa3d01dfe 100644 --- a/backend/src/Squidex.Infrastructure/Queries/AsyncTransformVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/AsyncTransformVisitor.cs @@ -5,42 +5,41 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public abstract class AsyncTransformVisitor<TValue, TArgs> : FilterNodeVisitor<ValueTask<FilterNode<TValue>?>, TValue, TArgs> { - public abstract class AsyncTransformVisitor<TValue, TArgs> : FilterNodeVisitor<ValueTask<FilterNode<TValue>?>, TValue, TArgs> + public override ValueTask<FilterNode<TValue>?> Visit(CompareFilter<TValue> nodeIn, TArgs args) { - public override ValueTask<FilterNode<TValue>?> Visit(CompareFilter<TValue> nodeIn, TArgs args) - { - return new ValueTask<FilterNode<TValue>?>(nodeIn); - } + return new ValueTask<FilterNode<TValue>?>(nodeIn); + } - public override async ValueTask<FilterNode<TValue>?> Visit(LogicalFilter<TValue> nodeIn, TArgs args) + public override async ValueTask<FilterNode<TValue>?> Visit(LogicalFilter<TValue> nodeIn, TArgs args) + { + var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count); + + foreach (var inner in nodeIn.Filters) { - var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count); + var transformed = await inner.Accept(this, args); - foreach (var inner in nodeIn.Filters) + if (transformed != null) { - var transformed = await inner.Accept(this, args); - - if (transformed != null) - { - pruned.Add(transformed); - } + pruned.Add(transformed); } - - return new LogicalFilter<TValue>(nodeIn.Type, pruned); } - public override async ValueTask<FilterNode<TValue>?> Visit(NegateFilter<TValue> nodeIn, TArgs args) - { - var inner = await nodeIn.Filter.Accept(this, args); + return new LogicalFilter<TValue>(nodeIn.Type, pruned); + } - if (inner == null) - { - return inner; - } + public override async ValueTask<FilterNode<TValue>?> Visit(NegateFilter<TValue> nodeIn, TArgs args) + { + var inner = await nodeIn.Filter.Accept(this, args); - return new NegateFilter<TValue>(inner); + if (inner == null) + { + return inner; } + + return new NegateFilter<TValue>(inner); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs index 685e99fb41..fad3e79dfd 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs @@ -5,103 +5,102 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public static class ClrFilter { - public static class ClrFilter - { - public static LogicalFilter<ClrValue> And(params FilterNode<ClrValue>[] filters) - { - return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters); - } - - public static LogicalFilter<ClrValue> And(IReadOnlyList<FilterNode<ClrValue>> filters) - { - return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters); - } - - public static LogicalFilter<ClrValue> Or(params FilterNode<ClrValue>[] filters) - { - return new LogicalFilter<ClrValue>(LogicalFilterType.Or, filters); - } - - public static LogicalFilter<ClrValue> Or(IReadOnlyList<FilterNode<ClrValue>> filters) - { - return new LogicalFilter<ClrValue>(LogicalFilterType.Or, filters); - } - - public static NegateFilter<ClrValue> Not(FilterNode<ClrValue> filter) - { - return new NegateFilter<ClrValue>(filter); - } - - public static CompareFilter<ClrValue> Eq(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.Equals, value); - } - - public static CompareFilter<ClrValue> Ne(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.NotEquals, value); - } - - public static CompareFilter<ClrValue> Lt(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.LessThan, value); - } - - public static CompareFilter<ClrValue> Le(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.LessThanOrEqual, value); - } - - public static CompareFilter<ClrValue> Gt(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.GreaterThan, value); - } - - public static CompareFilter<ClrValue> Ge(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.GreaterThanOrEqual, value); - } - - public static CompareFilter<ClrValue> Contains(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.Contains, value); - } - - public static CompareFilter<ClrValue> Matchs(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.Matchs, value); - } - - public static CompareFilter<ClrValue> EndsWith(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.EndsWith, value); - } - - public static CompareFilter<ClrValue> StartsWith(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.StartsWith, value); - } - - public static CompareFilter<ClrValue> Empty(PropertyPath path) - { - return Binary(path, CompareOperator.Empty, null); - } - - public static CompareFilter<ClrValue> Exists(PropertyPath path) - { - return Binary(path, CompareOperator.Exists, null); - } - - public static CompareFilter<ClrValue> In(PropertyPath path, ClrValue value) - { - return Binary(path, CompareOperator.In, value); - } - - private static CompareFilter<ClrValue> Binary(PropertyPath path, CompareOperator @operator, ClrValue? value) - { - return new CompareFilter<ClrValue>(path, @operator, value ?? ClrValue.Null); - } + public static LogicalFilter<ClrValue> And(params FilterNode<ClrValue>[] filters) + { + return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters); + } + + public static LogicalFilter<ClrValue> And(IReadOnlyList<FilterNode<ClrValue>> filters) + { + return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters); + } + + public static LogicalFilter<ClrValue> Or(params FilterNode<ClrValue>[] filters) + { + return new LogicalFilter<ClrValue>(LogicalFilterType.Or, filters); + } + + public static LogicalFilter<ClrValue> Or(IReadOnlyList<FilterNode<ClrValue>> filters) + { + return new LogicalFilter<ClrValue>(LogicalFilterType.Or, filters); + } + + public static NegateFilter<ClrValue> Not(FilterNode<ClrValue> filter) + { + return new NegateFilter<ClrValue>(filter); + } + + public static CompareFilter<ClrValue> Eq(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.Equals, value); + } + + public static CompareFilter<ClrValue> Ne(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.NotEquals, value); + } + + public static CompareFilter<ClrValue> Lt(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.LessThan, value); + } + + public static CompareFilter<ClrValue> Le(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.LessThanOrEqual, value); + } + + public static CompareFilter<ClrValue> Gt(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.GreaterThan, value); + } + + public static CompareFilter<ClrValue> Ge(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.GreaterThanOrEqual, value); + } + + public static CompareFilter<ClrValue> Contains(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.Contains, value); + } + + public static CompareFilter<ClrValue> Matchs(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.Matchs, value); + } + + public static CompareFilter<ClrValue> EndsWith(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.EndsWith, value); + } + + public static CompareFilter<ClrValue> StartsWith(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.StartsWith, value); + } + + public static CompareFilter<ClrValue> Empty(PropertyPath path) + { + return Binary(path, CompareOperator.Empty, null); + } + + public static CompareFilter<ClrValue> Exists(PropertyPath path) + { + return Binary(path, CompareOperator.Exists, null); + } + + public static CompareFilter<ClrValue> In(PropertyPath path, ClrValue value) + { + return Binary(path, CompareOperator.In, value); + } + + private static CompareFilter<ClrValue> Binary(PropertyPath path, CompareOperator @operator, ClrValue? value) + { + return new CompareFilter<ClrValue>(path, @operator, value ?? ClrValue.Null); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrQuery.cs b/backend/src/Squidex.Infrastructure/Queries/ClrQuery.cs index 0d94e18fec..7005e75965 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrQuery.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrQuery.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed class ClrQuery : Query<ClrValue> { - public sealed class ClrQuery : Query<ClrValue> - { - } } diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs index 9faedab4c8..7f49e3def0 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs @@ -11,171 +11,170 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed record ClrValue(object? Value, ClrValueType ValueType, bool IsList) { - public sealed record ClrValue(object? Value, ClrValueType ValueType, bool IsList) + private static readonly Func<object?, string> ToStringDelegate = ToString; + + public static readonly ClrValue Null = new ClrValue(null, ClrValueType.Null, false); + + public static implicit operator ClrValue(FilterSphere value) { - private static readonly Func<object?, string> ToStringDelegate = ToString; + return new ClrValue(value, ClrValueType.Sphere, false); + } - public static readonly ClrValue Null = new ClrValue(null, ClrValueType.Null, false); + public static implicit operator ClrValue(Instant value) + { + return new ClrValue(value, ClrValueType.Instant, false); + } - public static implicit operator ClrValue(FilterSphere value) - { - return new ClrValue(value, ClrValueType.Sphere, false); - } + public static implicit operator ClrValue(Guid value) + { + return new ClrValue(value, ClrValueType.Guid, false); + } - public static implicit operator ClrValue(Instant value) - { - return new ClrValue(value, ClrValueType.Instant, false); - } + public static implicit operator ClrValue(bool value) + { + return new ClrValue(value, ClrValueType.Boolean, false); + } - public static implicit operator ClrValue(Guid value) - { - return new ClrValue(value, ClrValueType.Guid, false); - } + public static implicit operator ClrValue(float value) + { + return new ClrValue(value, ClrValueType.Single, false); + } - public static implicit operator ClrValue(bool value) - { - return new ClrValue(value, ClrValueType.Boolean, false); - } + public static implicit operator ClrValue(double value) + { + return new ClrValue(value, ClrValueType.Double, false); + } - public static implicit operator ClrValue(float value) - { - return new ClrValue(value, ClrValueType.Single, false); - } + public static implicit operator ClrValue(int value) + { + return new ClrValue(value, ClrValueType.Int32, false); + } - public static implicit operator ClrValue(double value) - { - return new ClrValue(value, ClrValueType.Double, false); - } + public static implicit operator ClrValue(long value) + { + return new ClrValue(value, ClrValueType.Int64, false); + } - public static implicit operator ClrValue(int value) - { - return new ClrValue(value, ClrValueType.Int32, false); - } + public static implicit operator ClrValue(string? value) + { + return value != null ? new ClrValue(value, ClrValueType.String, false) : Null; + } - public static implicit operator ClrValue(long value) - { - return new ClrValue(value, ClrValueType.Int64, false); - } + public static implicit operator ClrValue(List<FilterSphere> value) + { + return value != null ? new ClrValue(value, ClrValueType.Sphere, true) : Null; + } - public static implicit operator ClrValue(string? value) - { - return value != null ? new ClrValue(value, ClrValueType.String, false) : Null; - } + public static implicit operator ClrValue(List<Instant> value) + { + return value != null ? new ClrValue(value, ClrValueType.Instant, true) : Null; + } - public static implicit operator ClrValue(List<FilterSphere> value) - { - return value != null ? new ClrValue(value, ClrValueType.Sphere, true) : Null; - } + public static implicit operator ClrValue(List<Guid> value) + { + return value != null ? new ClrValue(value, ClrValueType.Guid, true) : Null; + } - public static implicit operator ClrValue(List<Instant> value) - { - return value != null ? new ClrValue(value, ClrValueType.Instant, true) : Null; - } + public static implicit operator ClrValue(List<bool> value) + { + return value != null ? new ClrValue(value, ClrValueType.Boolean, true) : Null; + } - public static implicit operator ClrValue(List<Guid> value) - { - return value != null ? new ClrValue(value, ClrValueType.Guid, true) : Null; - } + public static implicit operator ClrValue(List<float> value) + { + return value != null ? new ClrValue(value, ClrValueType.Single, true) : Null; + } - public static implicit operator ClrValue(List<bool> value) - { - return value != null ? new ClrValue(value, ClrValueType.Boolean, true) : Null; - } + public static implicit operator ClrValue(List<double> value) + { + return value != null ? new ClrValue(value, ClrValueType.Double, true) : Null; + } - public static implicit operator ClrValue(List<float> value) - { - return value != null ? new ClrValue(value, ClrValueType.Single, true) : Null; - } + public static implicit operator ClrValue(List<int> value) + { + return value != null ? new ClrValue(value, ClrValueType.Int32, true) : Null; + } - public static implicit operator ClrValue(List<double> value) - { - return value != null ? new ClrValue(value, ClrValueType.Double, true) : Null; - } + public static implicit operator ClrValue(List<long> value) + { + return value != null ? new ClrValue(value, ClrValueType.Int64, true) : Null; + } - public static implicit operator ClrValue(List<int> value) - { - return value != null ? new ClrValue(value, ClrValueType.Int32, true) : Null; - } + public static implicit operator ClrValue(List<string> value) + { + return value != null ? new ClrValue(value, ClrValueType.String, true) : Null; + } - public static implicit operator ClrValue(List<long> value) + public static implicit operator ClrValue(List<object?> value) + { + return value != null ? new ClrValue(value, ClrValueType.Dynamic, true) : Null; + } + + public ClrValue ToList() + { + var value = Value; + + if (IsList || ValueType == ClrValueType.Null || value == null) { - return value != null ? new ClrValue(value, ClrValueType.Int64, true) : Null; + return this; } - public static implicit operator ClrValue(List<string> value) + ClrValue BuildList<T>(T value) { - return value != null ? new ClrValue(value, ClrValueType.String, true) : Null; + return new ClrValue(new List<T> { value }, ValueType, true); } - public static implicit operator ClrValue(List<object?> value) + switch (value) { - return value != null ? new ClrValue(value, ClrValueType.Dynamic, true) : Null; + case FilterSphere typed: + return BuildList(typed); + case Instant typed: + return BuildList(typed); + case Guid typed: + return BuildList(typed); + case bool typed: + return BuildList(typed); + case float typed: + return BuildList(typed); + case double typed: + return BuildList(typed); + case int typed: + return BuildList(typed); + case long typed: + return BuildList(typed); + case string typed: + return BuildList(typed); } - public ClrValue ToList() - { - var value = Value; - - if (IsList || ValueType == ClrValueType.Null || value == null) - { - return this; - } - - ClrValue BuildList<T>(T value) - { - return new ClrValue(new List<T> { value }, ValueType, true); - } - - switch (value) - { - case FilterSphere typed: - return BuildList(typed); - case Instant typed: - return BuildList(typed); - case Guid typed: - return BuildList(typed); - case bool typed: - return BuildList(typed); - case float typed: - return BuildList(typed); - case double typed: - return BuildList(typed); - case int typed: - return BuildList(typed); - case long typed: - return BuildList(typed); - case string typed: - return BuildList(typed); - } + return this; + } - return this; + public override string ToString() + { + if (Value is IList list) + { + return $"[{string.Join(", ", list.OfType<object>().Select(ToStringDelegate).ToArray())}]"; } - public override string ToString() - { - if (Value is IList list) - { - return $"[{string.Join(", ", list.OfType<object>().Select(ToStringDelegate).ToArray())}]"; - } + return ToString(Value); + } - return ToString(Value); + private static string ToString(object? value) + { + if (value == null) + { + return "null"; } - private static string ToString(object? value) + if (value is string s) { - if (value == null) - { - return "null"; - } - - if (value is string s) - { - return $"'{s.Replace("'", "\\'", StringComparison.Ordinal)}'"; - } - - return string.Format(CultureInfo.InvariantCulture, "{0}", value); + return $"'{s.Replace("'", "\\'", StringComparison.Ordinal)}'"; } + + return string.Format(CultureInfo.InvariantCulture, "{0}", value); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs index 79dba9560a..502626c722 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrValueType.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public enum ClrValueType { - public enum ClrValueType - { - Boolean, - Dynamic, - Guid, - Double, - Instant, - Int32, - Int64, - Single, - Sphere, - String, - Null - } + Boolean, + Dynamic, + Guid, + Double, + Instant, + Int32, + Int64, + Single, + Sphere, + String, + Null } diff --git a/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs index a66469ca26..70ffb04bd5 100644 --- a/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/CompareFilter.cs @@ -7,53 +7,52 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed record CompareFilter<TValue>(PropertyPath Path, CompareOperator Operator, TValue Value) : FilterNode<TValue> { - public sealed record CompareFilter<TValue>(PropertyPath Path, CompareOperator Operator, TValue Value) : FilterNode<TValue> + public override void AddFields(HashSet<string> fields) { - public override void AddFields(HashSet<string> fields) - { - fields.Add(Path.ToString()); - } + fields.Add(Path.ToString()); + } - public override T Accept<T, TArgs>(FilterNodeVisitor<T, TValue, TArgs> visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept<T, TArgs>(FilterNodeVisitor<T, TValue, TArgs> visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override string ToString() + public override string ToString() + { + switch (Operator) { - switch (Operator) - { - case CompareOperator.Contains: - return $"contains({Path}, {Value})"; - case CompareOperator.Empty: - return $"empty({Path})"; - case CompareOperator.Exists: - return $"exists({Path})"; - case CompareOperator.Matchs: - return $"matchs({Path}, {Value})"; - case CompareOperator.EndsWith: - return $"endsWith({Path}, {Value})"; - case CompareOperator.StartsWith: - return $"startsWith({Path}, {Value})"; - case CompareOperator.Equals: - return $"{Path} == {Value}"; - case CompareOperator.NotEquals: - return $"{Path} != {Value}"; - case CompareOperator.GreaterThan: - return $"{Path} > {Value}"; - case CompareOperator.GreaterThanOrEqual: - return $"{Path} >= {Value}"; - case CompareOperator.LessThan: - return $"{Path} < {Value}"; - case CompareOperator.LessThanOrEqual: - return $"{Path} <= {Value}"; - case CompareOperator.In: - return $"{Path} in {Value}"; - default: - return string.Empty; - } + case CompareOperator.Contains: + return $"contains({Path}, {Value})"; + case CompareOperator.Empty: + return $"empty({Path})"; + case CompareOperator.Exists: + return $"exists({Path})"; + case CompareOperator.Matchs: + return $"matchs({Path}, {Value})"; + case CompareOperator.EndsWith: + return $"endsWith({Path}, {Value})"; + case CompareOperator.StartsWith: + return $"startsWith({Path}, {Value})"; + case CompareOperator.Equals: + return $"{Path} == {Value}"; + case CompareOperator.NotEquals: + return $"{Path} != {Value}"; + case CompareOperator.GreaterThan: + return $"{Path} > {Value}"; + case CompareOperator.GreaterThanOrEqual: + return $"{Path} >= {Value}"; + case CompareOperator.LessThan: + return $"{Path} < {Value}"; + case CompareOperator.LessThanOrEqual: + return $"{Path} <= {Value}"; + case CompareOperator.In: + return $"{Path} in {Value}"; + default: + return string.Empty; } } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs b/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs index c0f0793400..bd1aa15c18 100644 --- a/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs +++ b/backend/src/Squidex.Infrastructure/Queries/CompareOperator.cs @@ -7,23 +7,22 @@ using System.ComponentModel; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +[TypeConverter(typeof(CompareOperatorTypeConverter))] +public enum CompareOperator { - [TypeConverter(typeof(CompareOperatorTypeConverter))] - public enum CompareOperator - { - Contains, - Empty, - Exists, - EndsWith, - Equals, - GreaterThan, - GreaterThanOrEqual, - In, - LessThan, - LessThanOrEqual, - Matchs, - NotEquals, - StartsWith - } + Contains, + Empty, + Exists, + EndsWith, + Equals, + GreaterThan, + GreaterThanOrEqual, + In, + LessThan, + LessThanOrEqual, + Matchs, + NotEquals, + StartsWith } diff --git a/backend/src/Squidex.Infrastructure/Queries/CompareOperatorTypeConverter.cs b/backend/src/Squidex.Infrastructure/Queries/CompareOperatorTypeConverter.cs index 31757c385b..b12a60cd24 100644 --- a/backend/src/Squidex.Infrastructure/Queries/CompareOperatorTypeConverter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/CompareOperatorTypeConverter.cs @@ -8,92 +8,91 @@ using System.ComponentModel; using System.Globalization; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed class CompareOperatorTypeConverter : TypeConverter { - public sealed class CompareOperatorTypeConverter : TypeConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) - { - return destinationType == typeof(string); - } + return sourceType == typeof(string); + } - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - var op = (string)value; + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string); + } - switch (op.ToLowerInvariant()) - { - case "eq": - return CompareOperator.Equals; - case "ne": - return CompareOperator.NotEquals; - case "lt": - return CompareOperator.LessThan; - case "le": - return CompareOperator.LessThanOrEqual; - case "gt": - return CompareOperator.GreaterThan; - case "ge": - return CompareOperator.GreaterThanOrEqual; - case "empty": - return CompareOperator.Empty; - case "exists": - return CompareOperator.Exists; - case "matchs": - return CompareOperator.Matchs; - case "contains": - return CompareOperator.Contains; - case "endswith": - return CompareOperator.EndsWith; - case "startswith": - return CompareOperator.StartsWith; - case "in": - return CompareOperator.In; - } + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + var op = (string)value; - throw new InvalidCastException($"Unexpected compare operator, got {op}."); + switch (op.ToLowerInvariant()) + { + case "eq": + return CompareOperator.Equals; + case "ne": + return CompareOperator.NotEquals; + case "lt": + return CompareOperator.LessThan; + case "le": + return CompareOperator.LessThanOrEqual; + case "gt": + return CompareOperator.GreaterThan; + case "ge": + return CompareOperator.GreaterThanOrEqual; + case "empty": + return CompareOperator.Empty; + case "exists": + return CompareOperator.Exists; + case "matchs": + return CompareOperator.Matchs; + case "contains": + return CompareOperator.Contains; + case "endswith": + return CompareOperator.EndsWith; + case "startswith": + return CompareOperator.StartsWith; + case "in": + return CompareOperator.In; } - public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) - { - var op = (CompareOperator)value!; + throw new InvalidCastException($"Unexpected compare operator, got {op}."); + } - switch (op) - { - case CompareOperator.Equals: - return "eq"; - case CompareOperator.NotEquals: - return "ne"; - case CompareOperator.LessThan: - return "lt"; - case CompareOperator.LessThanOrEqual: - return "le"; - case CompareOperator.GreaterThan: - return "gt"; - case CompareOperator.GreaterThanOrEqual: - return "gt"; - case CompareOperator.Empty: - return "empty"; - case CompareOperator.Exists: - return "exists"; - case CompareOperator.Matchs: - return "matchs"; - case CompareOperator.Contains: - return "contains"; - case CompareOperator.EndsWith: - return "endsWith"; - case CompareOperator.StartsWith: - return "startsWith"; - case CompareOperator.In: - return "in"; - } + public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) + { + var op = (CompareOperator)value!; - throw new InvalidCastException($"Unexpected compare operator, got {op}."); + switch (op) + { + case CompareOperator.Equals: + return "eq"; + case CompareOperator.NotEquals: + return "ne"; + case CompareOperator.LessThan: + return "lt"; + case CompareOperator.LessThanOrEqual: + return "le"; + case CompareOperator.GreaterThan: + return "gt"; + case CompareOperator.GreaterThanOrEqual: + return "gt"; + case CompareOperator.Empty: + return "empty"; + case CompareOperator.Exists: + return "exists"; + case CompareOperator.Matchs: + return "matchs"; + case CompareOperator.Contains: + return "contains"; + case CompareOperator.EndsWith: + return "endsWith"; + case CompareOperator.StartsWith: + return "startsWith"; + case CompareOperator.In: + return "in"; } + + throw new InvalidCastException($"Unexpected compare operator, got {op}."); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterField.cs b/backend/src/Squidex.Infrastructure/Queries/FilterField.cs index 999aeea4f5..a49bd4bec0 100644 --- a/backend/src/Squidex.Infrastructure/Queries/FilterField.cs +++ b/backend/src/Squidex.Infrastructure/Queries/FilterField.cs @@ -7,8 +7,7 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Queries -{ - public sealed record FilterField(FilterSchema Schema, string Path, string? Description = null, - bool IsNullable = false); -} +namespace Squidex.Infrastructure.Queries; + +public sealed record FilterField(FilterSchema Schema, string Path, string? Description = null, + bool IsNullable = false); diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs b/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs index f87c924842..21d2ccca69 100644 --- a/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs +++ b/backend/src/Squidex.Infrastructure/Queries/FilterNode.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public abstract record FilterNode<TValue> { - public abstract record FilterNode<TValue> - { - public abstract T Accept<T, TArgs>(FilterNodeVisitor<T, TValue, TArgs> visitor, TArgs args); + public abstract T Accept<T, TArgs>(FilterNodeVisitor<T, TValue, TArgs> visitor, TArgs args); - public abstract void AddFields(HashSet<string> fields); + public abstract void AddFields(HashSet<string> fields); - public abstract override string ToString(); - } + public abstract override string ToString(); } diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs index 209c9091f3..15c02fbe0a 100644 --- a/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs @@ -7,23 +7,22 @@ #pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public abstract class FilterNodeVisitor<T, TValue, TArgs> { - public abstract class FilterNodeVisitor<T, TValue, TArgs> + public virtual T Visit(CompareFilter<TValue> nodeIn, TArgs args) { - public virtual T Visit(CompareFilter<TValue> nodeIn, TArgs args) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - public virtual T Visit(LogicalFilter<TValue> nodeIn, TArgs args) - { - throw new NotImplementedException(); - } + public virtual T Visit(LogicalFilter<TValue> nodeIn, TArgs args) + { + throw new NotImplementedException(); + } - public virtual T Visit(NegateFilter<TValue> nodeIn, TArgs args) - { - throw new NotImplementedException(); - } + public virtual T Visit(NegateFilter<TValue> nodeIn, TArgs args) + { + throw new NotImplementedException(); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterSchema.cs b/backend/src/Squidex.Infrastructure/Queries/FilterSchema.cs index 20797885d0..965b54d80a 100644 --- a/backend/src/Squidex.Infrastructure/Queries/FilterSchema.cs +++ b/backend/src/Squidex.Infrastructure/Queries/FilterSchema.cs @@ -9,98 +9,97 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed record FilterSchema(FilterSchemaType Type) { - public sealed record FilterSchema(FilterSchemaType Type) - { - public static readonly FilterSchema Any = new FilterSchema(FilterSchemaType.Any); - public static readonly FilterSchema Boolean = new FilterSchema(FilterSchemaType.Boolean); - public static readonly FilterSchema Date = new FilterSchema(FilterSchemaType.Date); - public static readonly FilterSchema DateTime = new FilterSchema(FilterSchemaType.DateTime); - public static readonly FilterSchema GeoObject = new FilterSchema(FilterSchemaType.GeoObject); - public static readonly FilterSchema Guid = new FilterSchema(FilterSchemaType.Guid); - public static readonly FilterSchema Number = new FilterSchema(FilterSchemaType.Number); - public static readonly FilterSchema String = new FilterSchema(FilterSchemaType.String); - public static readonly FilterSchema StringArray = new FilterSchema(FilterSchemaType.StringArray); + public static readonly FilterSchema Any = new FilterSchema(FilterSchemaType.Any); + public static readonly FilterSchema Boolean = new FilterSchema(FilterSchemaType.Boolean); + public static readonly FilterSchema Date = new FilterSchema(FilterSchemaType.Date); + public static readonly FilterSchema DateTime = new FilterSchema(FilterSchemaType.DateTime); + public static readonly FilterSchema GeoObject = new FilterSchema(FilterSchemaType.GeoObject); + public static readonly FilterSchema Guid = new FilterSchema(FilterSchemaType.Guid); + public static readonly FilterSchema Number = new FilterSchema(FilterSchemaType.Number); + public static readonly FilterSchema String = new FilterSchema(FilterSchemaType.String); + public static readonly FilterSchema StringArray = new FilterSchema(FilterSchemaType.StringArray); - public ReadonlyList<FilterField>? Fields { get; init; } + public ReadonlyList<FilterField>? Fields { get; init; } - public object? Extra { get; init; } + public object? Extra { get; init; } - public FilterSchema Flatten(int maxDepth = 7, Predicate<FilterSchema>? predicate = null) + public FilterSchema Flatten(int maxDepth = 7, Predicate<FilterSchema>? predicate = null) + { + if (Fields == null || Fields.Count == 0) { - if (Fields == null || Fields.Count == 0) - { - return this; - } - - var result = new List<FilterField>(); + return this; + } - var pathStack = new Stack<string>(); + var result = new List<FilterField>(); - void AddField(FilterField field) - { - pathStack.Push(field.Path); + var pathStack = new Stack<string>(); - if (predicate?.Invoke(field.Schema) != false) - { - var path = string.Join('.', pathStack.Reverse()); - - var schema = field.Schema with - { - Fields = null - }; + void AddField(FilterField field) + { + pathStack.Push(field.Path); - result?.Add(field with { Path = path, Schema = schema }); - } + if (predicate?.Invoke(field.Schema) != false) + { + var path = string.Join('.', pathStack.Reverse()); - if (field.Schema.Fields?.Count > 0 && pathStack.Count < maxDepth) + var schema = field.Schema with { - AddFields(field.Schema.Fields); - } + Fields = null + }; - pathStack.Pop(); + result?.Add(field with { Path = path, Schema = schema }); } - void AddFields(IEnumerable<FilterField> source) + if (field.Schema.Fields?.Count > 0 && pathStack.Count < maxDepth) { - foreach (var field in source) - { - AddField(field); - } + AddFields(field.Schema.Fields); } - AddFields(Fields); - - var conflictFree = GetConflictFreeFields(result); + pathStack.Pop(); + } - return this with + void AddFields(IEnumerable<FilterField> source) + { + foreach (var field in source) { - Fields = conflictFree.ToReadonlyList() - }; + AddField(field); + } } - public static IEnumerable<FilterField> GetConflictFreeFields(IEnumerable<FilterField> fields) + AddFields(Fields); + + var conflictFree = GetConflictFreeFields(result); + + return this with { - var conflictFree = fields.GroupBy(x => x.Path).Select(group => - { - var firstType = group.First().Schema.Type; + Fields = conflictFree.ToReadonlyList() + }; + } - if (group.Count() == 1) - { - return group.Take(1); - } - else if (group.All(x => x.Schema.Type == firstType)) - { - return group.Take(1).Select(x => x with { Description = null }); - } - else - { - return Enumerable.Empty<FilterField>(); - } - }).SelectMany(x => x); + public static IEnumerable<FilterField> GetConflictFreeFields(IEnumerable<FilterField> fields) + { + var conflictFree = fields.GroupBy(x => x.Path).Select(group => + { + var firstType = group.First().Schema.Type; - return conflictFree; - } + if (group.Count() == 1) + { + return group.Take(1); + } + else if (group.All(x => x.Schema.Type == firstType)) + { + return group.Take(1).Select(x => x with { Description = null }); + } + else + { + return Enumerable.Empty<FilterField>(); + } + }).SelectMany(x => x); + + return conflictFree; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterSchemaType.cs b/backend/src/Squidex.Infrastructure/Queries/FilterSchemaType.cs index cffa832aa4..4f185b127f 100644 --- a/backend/src/Squidex.Infrastructure/Queries/FilterSchemaType.cs +++ b/backend/src/Squidex.Infrastructure/Queries/FilterSchemaType.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public enum FilterSchemaType { - public enum FilterSchemaType - { - Any, - Boolean, - Date, - DateTime, - GeoObject, - Guid, - Number, - Object, - ObjectArray, - String, - StringArray - } + Any, + Boolean, + Date, + DateTime, + GeoObject, + Guid, + Number, + Object, + ObjectArray, + String, + StringArray } diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterSphere.cs b/backend/src/Squidex.Infrastructure/Queries/FilterSphere.cs index 236ce2821a..38c2595758 100644 --- a/backend/src/Squidex.Infrastructure/Queries/FilterSphere.cs +++ b/backend/src/Squidex.Infrastructure/Queries/FilterSphere.cs @@ -7,13 +7,12 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed record FilterSphere(double Longitude, double Latitude, double Radius) { - public sealed record FilterSphere(double Longitude, double Latitude, double Radius) + public override string ToString() { - public override string ToString() - { - return $"Radius({Longitude}, {Latitude}, {Radius})"; - } + return $"Radius({Longitude}, {Latitude}, {Radius})"; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/Errors.cs b/backend/src/Squidex.Infrastructure/Queries/Json/Errors.cs index 1a299de697..4e1ca972b4 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/Errors.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/Errors.cs @@ -8,63 +8,62 @@ using System.Globalization; using Squidex.Infrastructure.Properties; -namespace Squidex.Infrastructure.Queries.Json +namespace Squidex.Infrastructure.Queries.Json; + +internal static class Errors { - internal static class Errors + public static string InvalidJsonStructure() { - public static string InvalidJsonStructure() - { - return Resources.QueryInvalidJsonStructure; - } + return Resources.QueryInvalidJsonStructure; + } - public static string InvalidQuery(object message) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalid, message); - } + public static string InvalidQuery(object message) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalid, message); + } - public static string InvalidQueryJson(object message) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidJson, message); - } + public static string InvalidQueryJson(object message) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidJson, message); + } - public static string InvalidPath(PropertyPath path) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidPath, path); - } + public static string InvalidPath(PropertyPath path) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidPath, path); + } - public static string InvalidOperator(object @operator, object type, PropertyPath path) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidOperator, @operator, type, path); - } + public static string InvalidOperator(object @operator, object type, PropertyPath path) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidOperator, @operator, type, path); + } - public static string InvalidRegex(object value, PropertyPath path) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidRegex, value, path); - } + public static string InvalidRegex(object value, PropertyPath path) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidRegex, value, path); + } - public static string InvalidArray(object @operator, PropertyPath path) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidArray, @operator, path); - } + public static string InvalidArray(object @operator, PropertyPath path) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryInvalidArray, @operator, path); + } - public static string WrongExpectedType(object expected, object type, PropertyPath path) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongExpectedType, expected, type, path); - } + public static string WrongExpectedType(object expected, object type, PropertyPath path) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongExpectedType, expected, type, path); + } - public static string WrongFormat(object expected, PropertyPath path) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongFormat, expected, path); - } + public static string WrongFormat(object expected, PropertyPath path) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongFormat, expected, path); + } - public static string WrongType(object type, PropertyPath path) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongType, type, path); - } + public static string WrongType(object type, PropertyPath path) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongType, type, path); + } - public static string WrongPrimitive(object type, PropertyPath path) - { - return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongPrimitive, type, path); - } + public static string WrongPrimitive(object type, PropertyPath path) + { + return string.Format(CultureInfo.InvariantCulture, Resources.QueryWrongPrimitive, type, path); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs index c86cfb6ba2..18d38e114a 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs @@ -7,51 +7,50 @@ using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Infrastructure.Queries.Json +namespace Squidex.Infrastructure.Queries.Json; + +public sealed class JsonFilterSurrogate : ISurrogate<FilterNode<JsonValue>> { - public sealed class JsonFilterSurrogate : ISurrogate<FilterNode<JsonValue>> - { - public FilterNode<JsonValue>[]? And { get; set; } + public FilterNode<JsonValue>[]? And { get; set; } - public FilterNode<JsonValue>[]? Or { get; set; } + public FilterNode<JsonValue>[]? Or { get; set; } - public FilterNode<JsonValue>? Not { get; set; } + public FilterNode<JsonValue>? Not { get; set; } - public CompareOperator? Op { get; set; } + public CompareOperator? Op { get; set; } - public string? Path { get; set; } + public string? Path { get; set; } - public JsonValue Value { get; set; } + public JsonValue Value { get; set; } - public void FromSource(FilterNode<JsonValue> source) + public void FromSource(FilterNode<JsonValue> source) + { + throw new NotSupportedException(); + } + + public FilterNode<JsonValue> ToSource() + { + if (Not != null) { - throw new NotSupportedException(); + return new NegateFilter<JsonValue>(Not); } - public FilterNode<JsonValue> ToSource() + if (And != null) { - if (Not != null) - { - return new NegateFilter<JsonValue>(Not); - } - - if (And != null) - { - return new LogicalFilter<JsonValue>(LogicalFilterType.And, And); - } - - if (Or != null) - { - return new LogicalFilter<JsonValue>(LogicalFilterType.Or, Or); - } - - if (!string.IsNullOrWhiteSpace(Path)) - { - return new CompareFilter<JsonValue>(Path, Op ?? CompareOperator.Equals, Value); - } - - ThrowHelper.JsonException(Errors.InvalidJsonStructure()); - return default!; + return new LogicalFilter<JsonValue>(LogicalFilterType.And, And); } + + if (Or != null) + { + return new LogicalFilter<JsonValue>(LogicalFilterType.Or, Or); + } + + if (!string.IsNullOrWhiteSpace(Path)) + { + return new CompareFilter<JsonValue>(Path, Op ?? CompareOperator.Equals, Value); + } + + ThrowHelper.JsonException(Errors.InvalidJsonStructure()); + return default!; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs index ae2ab84d37..d7b1a977d1 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs @@ -10,96 +10,95 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Queries.Json +namespace Squidex.Infrastructure.Queries.Json; + +public sealed class JsonFilterVisitor : FilterNodeVisitor<FilterNode<ClrValue>, JsonValue, JsonFilterVisitor.Args> { - public sealed class JsonFilterVisitor : FilterNodeVisitor<FilterNode<ClrValue>, JsonValue, JsonFilterVisitor.Args> + private static readonly JsonFilterVisitor Instance = new JsonFilterVisitor(); + + public record struct Args(QueryModel Model, List<string> Errors); + + private JsonFilterVisitor() + { + } + + public static FilterNode<ClrValue>? Parse(FilterNode<JsonValue> filter, QueryModel model, List<string> errors) { - private static readonly JsonFilterVisitor Instance = new JsonFilterVisitor(); + var args = new Args(model, errors); - public record struct Args(QueryModel Model, List<string> Errors); + var parsed = filter.Accept(Instance, args); - private JsonFilterVisitor() + if (errors.Count > 0) { + return null; } - - public static FilterNode<ClrValue>? Parse(FilterNode<JsonValue> filter, QueryModel model, List<string> errors) + else { - var args = new Args(model, errors); + return parsed; + } + } - var parsed = filter.Accept(Instance, args); + public override FilterNode<ClrValue> Visit(NegateFilter<JsonValue> nodeIn, Args args) + { + return new NegateFilter<ClrValue>(nodeIn.Accept(this, args)); + } - if (errors.Count > 0) - { - return null; - } - else - { - return parsed; - } - } + public override FilterNode<ClrValue> Visit(LogicalFilter<JsonValue> nodeIn, Args args) + { + return new LogicalFilter<ClrValue>(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this, args)).ToList()); + } - public override FilterNode<ClrValue> Visit(NegateFilter<JsonValue> nodeIn, Args args) - { - return new NegateFilter<ClrValue>(nodeIn.Accept(this, args)); - } + public override FilterNode<ClrValue> Visit(CompareFilter<JsonValue> nodeIn, Args args) + { + var fieldMatches = nodeIn.Path.GetMatchingFields(args.Model.Schema, args.Errors); + var fieldErrors = new List<string>(); - public override FilterNode<ClrValue> Visit(LogicalFilter<JsonValue> nodeIn, Args args) + foreach (var field in fieldMatches) { - return new LogicalFilter<ClrValue>(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this, args)).ToList()); - } + fieldErrors.Clear(); - public override FilterNode<ClrValue> Visit(CompareFilter<JsonValue> nodeIn, Args args) - { - var fieldMatches = nodeIn.Path.GetMatchingFields(args.Model.Schema, args.Errors); - var fieldErrors = new List<string>(); + var isValidOperator = args.Model.Operators.TryGetValue(field.Schema.Type, out var operators) && operators.Contains(nodeIn.Operator); - foreach (var field in fieldMatches) + if (!isValidOperator) { - fieldErrors.Clear(); - - var isValidOperator = args.Model.Operators.TryGetValue(field.Schema.Type, out var operators) && operators.Contains(nodeIn.Operator); - - if (!isValidOperator) - { - fieldErrors.Add(Errors.InvalidOperator(nodeIn.Operator, field.Schema.Type, nodeIn.Path)); - } + fieldErrors.Add(Errors.InvalidOperator(nodeIn.Operator, field.Schema.Type, nodeIn.Path)); + } - var value = ValueConverter.Convert(field, nodeIn.Value, nodeIn.Path, fieldErrors); + var value = ValueConverter.Convert(field, nodeIn.Value, nodeIn.Path, fieldErrors); - if (value != null && isValidOperator) + if (value != null && isValidOperator) + { + if (nodeIn.Operator == CompareOperator.In) { - if (nodeIn.Operator == CompareOperator.In) - { - if (!value.IsList) - { - value = value.ToList(); - } - } - else + if (!value.IsList) { - if (value.IsList) - { - fieldErrors.Add(Errors.InvalidArray(nodeIn.Operator, nodeIn.Path)); - } + value = value.ToList(); } - - if (nodeIn.Operator == CompareOperator.Matchs && value.Value?.ToString()?.IsValidRegex() != true) + } + else + { + if (value.IsList) { - fieldErrors.Add(Errors.InvalidRegex(value.ToString(), nodeIn.Path)); + fieldErrors.Add(Errors.InvalidArray(nodeIn.Operator, nodeIn.Path)); } } - if (args.Errors.Count == 0 && fieldErrors.Count == 0 && value != null) - { - return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, value); - } - else if (field == fieldMatches.Last()) + if (nodeIn.Operator == CompareOperator.Matchs && value.Value?.ToString()?.IsValidRegex() != true) { - args.Errors.AddRange(fieldErrors); + fieldErrors.Add(Errors.InvalidRegex(value.ToString(), nodeIn.Path)); } } - return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, ClrValue.Null); + if (args.Errors.Count == 0 && fieldErrors.Count == 0 && value != null) + { + return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, value); + } + else if (field == fieldMatches.Last()) + { + args.Errors.AddRange(fieldErrors); + } } + + return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, ClrValue.Null); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs index 63c2230178..3c0c12e443 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/PropertyPathValidator.cs @@ -5,47 +5,46 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries.Json +namespace Squidex.Infrastructure.Queries.Json; + +public static class PropertyPathValidator { - public static class PropertyPathValidator + public static IEnumerable<FilterField> GetMatchingFields(this PropertyPath path, FilterSchema schema, List<string> errors) { - public static IEnumerable<FilterField> GetMatchingFields(this PropertyPath path, FilterSchema schema, List<string> errors) + var lastIndex = path.Count - 1; + + List<FilterField>? result = null; + + void Check(int index, FilterSchema schema) { - var lastIndex = path.Count - 1; + if (schema.Fields == null) + { + return; + } - List<FilterField>? result = null; + var fields = schema.Fields.Where(x => x.Path == path[index]); - void Check(int index, FilterSchema schema) + foreach (var field in fields) { - if (schema.Fields == null) + if (index == lastIndex || field.Schema.Type == FilterSchemaType.Any) { - return; + result ??= new List<FilterField>(); + result.Add(field); } - - var fields = schema.Fields.Where(x => x.Path == path[index]); - - foreach (var field in fields) + else { - if (index == lastIndex || field.Schema.Type == FilterSchemaType.Any) - { - result ??= new List<FilterField>(); - result.Add(field); - } - else - { - Check(index + 1, field.Schema); - } + Check(index + 1, field.Schema); } } + } - Check(0, schema); - - if (result == null) - { - errors.Add(Errors.InvalidPath(path.ToString())); - } + Check(0, schema); - return result as IEnumerable<FilterField> ?? Array.Empty<FilterField>(); + if (result == null) + { + errors.Add(Errors.InvalidPath(path.ToString())); } + + return result as IEnumerable<FilterField> ?? Array.Empty<FilterField>(); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs index d2242f807b..0e52d9aeeb 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs @@ -10,89 +10,88 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Infrastructure.Queries.Json +namespace Squidex.Infrastructure.Queries.Json; + +public static class QueryParser { - public static class QueryParser + public static ClrQuery Parse(this QueryModel model, string json, IJsonSerializer serializer) { - public static ClrQuery Parse(this QueryModel model, string json, IJsonSerializer serializer) + if (string.IsNullOrWhiteSpace(json)) { - if (string.IsNullOrWhiteSpace(json)) - { - return new ClrQuery(); - } + return new ClrQuery(); + } - var query = ParseFromJson(json, serializer); + var query = ParseFromJson(json, serializer); - return Convert(model, query); - } + return Convert(model, query); + } - public static ClrQuery Convert(this QueryModel model, Query<JsonValue> query) + public static ClrQuery Convert(this QueryModel model, Query<JsonValue> query) + { + if (query == null) { - if (query == null) - { - return new ClrQuery(); - } - - var result = SimpleMapper.Map(query, new ClrQuery()); + return new ClrQuery(); + } - var errors = new List<string>(); + var result = SimpleMapper.Map(query, new ClrQuery()); - model.ConvertSorting(result, errors); - model.ConvertFilters(result, errors, query); + var errors = new List<string>(); - if (errors.Count > 0) - { - throw new ValidationException(errors.Select(BuildError).ToList()); - } + model.ConvertSorting(result, errors); + model.ConvertFilters(result, errors, query); - return result; + if (errors.Count > 0) + { + throw new ValidationException(errors.Select(BuildError).ToList()); } - private static void ConvertFilters(this QueryModel model, ClrQuery result, List<string> errors, Query<JsonValue> query) + return result; + } + + private static void ConvertFilters(this QueryModel model, ClrQuery result, List<string> errors, Query<JsonValue> query) + { + if (query.Filter == null) { - if (query.Filter == null) - { - return; - } + return; + } - var filter = JsonFilterVisitor.Parse(query.Filter, model, errors); + var filter = JsonFilterVisitor.Parse(query.Filter, model, errors); - if (filter != null) - { - result.Filter = Optimizer<ClrValue>.Optimize(filter); - } + if (filter != null) + { + result.Filter = Optimizer<ClrValue>.Optimize(filter); } + } - private static void ConvertSorting(this QueryModel model, ClrQuery result, List<string> errors) + private static void ConvertSorting(this QueryModel model, ClrQuery result, List<string> errors) + { + if (result.Sort == null) { - if (result.Sort == null) - { - return; - } - - foreach (var sorting in result.Sort) - { - sorting.Path.GetMatchingFields(model.Schema, errors); - } + return; } - public static Query<JsonValue> ParseFromJson(string json, IJsonSerializer serializer) + foreach (var sorting in result.Sort) { - try - { - return serializer.Deserialize<Query<JsonValue>>(json); - } - catch (JsonException ex) - { - var error = Errors.InvalidQueryJson(ex.Message); - - throw new ValidationException(error); - } + sorting.Path.GetMatchingFields(model.Schema, errors); } + } - private static ValidationError BuildError(string message) + public static Query<JsonValue> ParseFromJson(string json, IJsonSerializer serializer) + { + try { - return new ValidationError(Errors.InvalidQuery(message)); + return serializer.Deserialize<Query<JsonValue>>(json); } + catch (JsonException ex) + { + var error = Errors.InvalidQueryJson(ex.Message); + + throw new ValidationException(error); + } + } + + private static ValidationError BuildError(string message) + { + return new ValidationError(Errors.InvalidQuery(message)); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs index 39f463ab5d..f304015bd5 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs @@ -9,326 +9,325 @@ using NodaTime.Text; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Infrastructure.Queries.Json +namespace Squidex.Infrastructure.Queries.Json; + +public static class ValueConverter { - public static class ValueConverter + private delegate bool Parser<T>(List<string> errors, PropertyPath path, JsonValue value, out T result); + + private static readonly InstantPattern[] InstantPatterns = { - private delegate bool Parser<T>(List<string> errors, PropertyPath path, JsonValue value, out T result); + InstantPattern.General, + InstantPattern.ExtendedIso, + InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd") + }; - private static readonly InstantPattern[] InstantPatterns = - { - InstantPattern.General, - InstantPattern.ExtendedIso, - InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd") - }; + public static ClrValue? Convert(FilterField field, JsonValue value, PropertyPath path, List<string> errors) + { + ClrValue? result = null; + + var type = field.Schema.Type; - public static ClrValue? Convert(FilterField field, JsonValue value, PropertyPath path, List<string> errors) + if (value == default && type != FilterSchemaType.GeoObject && field.IsNullable) { - ClrValue? result = null; + return ClrValue.Null; + } - var type = field.Schema.Type; + switch (type) + { + case FilterSchemaType.GeoObject: + { + if (TryParseGeoJson(errors, path, value, out var temp)) + { + result = temp; + } - if (value == default && type != FilterSchemaType.GeoObject && field.IsNullable) - { - return ClrValue.Null; - } + break; + } - switch (type) - { - case FilterSchemaType.GeoObject: + case FilterSchemaType.Any: + { + if (value.Value is JsonArray a) { - if (TryParseGeoJson(errors, path, value, out var temp)) - { - result = temp; - } + var array = ParseArray<ClrValue?>(errors, path, a, TryParseDynamic); - break; + result = array.Select(x => x?.Value).ToList(); } - - case FilterSchemaType.Any: + else if (TryParseDynamic(errors, path, value, out var temp)) { - if (value.Value is JsonArray a) - { - var array = ParseArray<ClrValue?>(errors, path, a, TryParseDynamic); + result = temp; + } - result = array.Select(x => x?.Value).ToList(); - } - else if (TryParseDynamic(errors, path, value, out var temp)) - { - result = temp; - } + break; + } - break; + case FilterSchemaType.Boolean: + { + if (value.Value is JsonArray a) + { + result = ParseArray<bool>(errors, path, a, TryParseBoolean); } - - case FilterSchemaType.Boolean: + else if (TryParseBoolean(errors, path, value, out var temp)) { - if (value.Value is JsonArray a) - { - result = ParseArray<bool>(errors, path, a, TryParseBoolean); - } - else if (TryParseBoolean(errors, path, value, out var temp)) - { - result = temp; - } - - break; + result = temp; } - case FilterSchemaType.Number: - { - if (value.Value is JsonArray a) - { - result = ParseArray<double>(errors, path, a, TryParseNumber); - } - else if (TryParseNumber(errors, path, value, out var temp)) - { - result = temp; - } + break; + } - break; + case FilterSchemaType.Number: + { + if (value.Value is JsonArray a) + { + result = ParseArray<double>(errors, path, a, TryParseNumber); } - - case FilterSchemaType.Guid: + else if (TryParseNumber(errors, path, value, out var temp)) { - if (value.Value is JsonArray a) - { - result = ParseArray<Guid>(errors, path, a, TryParseGuid); - } - else if (TryParseGuid(errors, path, value, out var temp)) - { - result = temp; - } - - break; + result = temp; } - case FilterSchemaType.DateTime: - { - if (value.Value is JsonArray a) - { - result = ParseArray<Instant>(errors, path, a, TryParseDateTime); - } - else if (TryParseDateTime(errors, path, value, out var temp)) - { - result = temp; - } + break; + } - break; + case FilterSchemaType.Guid: + { + if (value.Value is JsonArray a) + { + result = ParseArray<Guid>(errors, path, a, TryParseGuid); } - - case FilterSchemaType.StringArray: - case FilterSchemaType.String: + else if (TryParseGuid(errors, path, value, out var temp)) { - if (value.Value is JsonArray a) - { - result = ParseArray<string>(errors, path, a, TryParseString!); - } - else if (TryParseString(errors, path, value, out var temp)) - { - result = temp; - } + result = temp; + } + + break; + } - break; + case FilterSchemaType.DateTime: + { + if (value.Value is JsonArray a) + { + result = ParseArray<Instant>(errors, path, a, TryParseDateTime); } + else if (TryParseDateTime(errors, path, value, out var temp)) + { + result = temp; + } + + break; + } - default: + case FilterSchemaType.StringArray: + case FilterSchemaType.String: + { + if (value.Value is JsonArray a) { - errors.Add(Errors.WrongType(type.ToString(), path)); - break; + result = ParseArray<string>(errors, path, a, TryParseString!); + } + else if (TryParseString(errors, path, value, out var temp)) + { + result = temp; } - } - return result; + break; + } + + default: + { + errors.Add(Errors.WrongType(type.ToString(), path)); + break; + } } - private static List<T> ParseArray<T>(List<string> errors, PropertyPath path, JsonArray array, Parser<T> parser) - { - var items = new List<T>(); + return result; + } - foreach (var item in array) + private static List<T> ParseArray<T>(List<string> errors, PropertyPath path, JsonArray array, Parser<T> parser) + { + var items = new List<T>(); + + foreach (var item in array) + { + if (parser(errors, path, item, out var temp)) { - if (parser(errors, path, item, out var temp)) - { - items.Add(temp); - } + items.Add(temp); } - - return items; } - private static bool TryParseGeoJson(List<string> errors, PropertyPath path, JsonValue value, out FilterSphere result) + return items; + } + + private static bool TryParseGeoJson(List<string> errors, PropertyPath path, JsonValue value, out FilterSphere result) + { + const string expected = "Object(geo-json)"; + + result = default!; + + if (value.Value is JsonObject o && + o.TryGetValue("latitude", out var found) && found.Value is double lat && + o.TryGetValue("longitude", out found) && found.Value is double lon && + o.TryGetValue("distance", out found) && found.Value is double distance) { - const string expected = "Object(geo-json)"; + result = new FilterSphere(lon, lat, distance); - result = default!; + return true; + } - if (value.Value is JsonObject o && - o.TryGetValue("latitude", out var found) && found.Value is double lat && - o.TryGetValue("longitude", out found) && found.Value is double lon && - o.TryGetValue("distance", out found) && found.Value is double distance) - { - result = new FilterSphere(lon, lat, distance); + errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); - return true; - } + return false; + } - errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); + private static bool TryParseBoolean(List<string> errors, PropertyPath path, JsonValue value, out bool result) + { + const string expected = "Boolean"; - return false; - } + result = default; - private static bool TryParseBoolean(List<string> errors, PropertyPath path, JsonValue value, out bool result) + if (value.Value is bool b) { - const string expected = "Boolean"; + result = b; - result = default; + return true; + } - if (value.Value is bool b) - { - result = b; + errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); - return true; - } + return false; + } - errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); + private static bool TryParseNumber(List<string> errors, PropertyPath path, JsonValue value, out double result) + { + const string expected = "Number"; - return false; - } + result = default; - private static bool TryParseNumber(List<string> errors, PropertyPath path, JsonValue value, out double result) + if (value.Value is double n) { - const string expected = "Number"; + result = n; - result = default; + return true; + } - if (value.Value is double n) - { - result = n; + errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); - return true; - } + return false; + } - errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); + private static bool TryParseString(List<string> errors, PropertyPath path, JsonValue value, out string? result) + { + const string expected = "String"; - return false; - } + result = default; - private static bool TryParseString(List<string> errors, PropertyPath path, JsonValue value, out string? result) + if (value.Value is string s) { - const string expected = "String"; + result = s; - result = default; + return true; + } - if (value.Value is string s) - { - result = s; + errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); + return false; + } + + private static bool TryParseGuid(List<string> errors, PropertyPath path, JsonValue value, out Guid result) + { + const string expected = "String (Guid)"; + + result = default; + + if (value.Value is string s) + { + if (Guid.TryParse(s, out result)) + { return true; } + errors.Add(Errors.WrongFormat(expected, path)); + } + else + { errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); - - return false; } - private static bool TryParseGuid(List<string> errors, PropertyPath path, JsonValue value, out Guid result) - { - const string expected = "String (Guid)"; + return false; + } - result = default; + private static bool TryParseDateTime(List<string> errors, PropertyPath path, JsonValue value, out Instant result) + { + const string expected = "String (ISO8601 DateTime)"; + + result = default; - if (value.Value is string s) + if (value.Value is string s) + { + foreach (var pattern in InstantPatterns) { - if (Guid.TryParse(s, out result)) + var parsed = pattern.Parse(s); + + if (parsed.Success) { + result = parsed.Value; + return true; } - - errors.Add(Errors.WrongFormat(expected, path)); - } - else - { - errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); } - return false; + errors.Add(Errors.WrongFormat(expected, path)); } - - private static bool TryParseDateTime(List<string> errors, PropertyPath path, JsonValue value, out Instant result) + else { - const string expected = "String (ISO8601 DateTime)"; + errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); + } - result = default; + return false; + } - if (value.Value is string s) - { - foreach (var pattern in InstantPatterns) - { - var parsed = pattern.Parse(s); + private static bool TryParseDynamic(List<string> errors, PropertyPath path, JsonValue value, out ClrValue? result) + { + result = null; - if (parsed.Success) + switch (value.Value) + { + case null: + return true; + case bool b: + result = b; + return true; + case double n: + result = n; + return true; + case string s: + { + if (Guid.TryParse(s, out var guid)) { - result = parsed.Value; + result = guid; return true; } - } - - errors.Add(Errors.WrongFormat(expected, path)); - } - else - { - errors.Add(Errors.WrongExpectedType(expected, value.Type.ToString(), path)); - } - return false; - } - - private static bool TryParseDynamic(List<string> errors, PropertyPath path, JsonValue value, out ClrValue? result) - { - result = null; - - switch (value.Value) - { - case null: - return true; - case bool b: - result = b; - return true; - case double n: - result = n; - return true; - case string s: + foreach (var pattern in InstantPatterns) { - if (Guid.TryParse(s, out var guid)) - { - result = guid; - - return true; - } + var parsed = pattern.Parse(s); - foreach (var pattern in InstantPatterns) + if (parsed.Success) { - var parsed = pattern.Parse(s); - - if (parsed.Success) - { - result = parsed.Value; + result = parsed.Value; - return true; - } + return true; } - - result = s; - - return true; } - } - errors.Add(Errors.WrongPrimitive(value.Type.ToString(), path)); + result = s; - return false; + return true; + } } + + errors.Add(Errors.WrongPrimitive(value.Type.ToString(), path)); + + return false; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs b/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs index 3b7f4e2c19..ff0d3064fe 100644 --- a/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/LogicalFilter.cs @@ -7,26 +7,25 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed record LogicalFilter<TValue>(LogicalFilterType Type, IReadOnlyList<FilterNode<TValue>> Filters) : FilterNode<TValue> { - public sealed record LogicalFilter<TValue>(LogicalFilterType Type, IReadOnlyList<FilterNode<TValue>> Filters) : FilterNode<TValue> + public override void AddFields(HashSet<string> fields) { - public override void AddFields(HashSet<string> fields) + foreach (var filter in Filters) { - foreach (var filter in Filters) - { - filter.AddFields(fields); - } + filter.AddFields(fields); } + } - public override T Accept<T, TArgs>(FilterNodeVisitor<T, TValue, TArgs> visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept<T, TArgs>(FilterNodeVisitor<T, TValue, TArgs> visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override string ToString() - { - return $"({string.Join(Type == LogicalFilterType.And ? " && " : " || ", Filters)})"; - } + public override string ToString() + { + return $"({string.Join(Type == LogicalFilterType.And ? " && " : " || ", Filters)})"; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs b/backend/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs index 4d34eb1ea3..5098a7feeb 100644 --- a/backend/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs +++ b/backend/src/Squidex.Infrastructure/Queries/LogicalFilterType.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public enum LogicalFilterType { - public enum LogicalFilterType - { - And, - Or - } + And, + Or } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs b/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs index baa68dce4b..55db939dc0 100644 --- a/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/NegateFilter.cs @@ -7,23 +7,22 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed record NegateFilter<TValue>(FilterNode<TValue> Filter) : FilterNode<TValue> { - public sealed record NegateFilter<TValue>(FilterNode<TValue> Filter) : FilterNode<TValue> + public override void AddFields(HashSet<string> fields) { - public override void AddFields(HashSet<string> fields) - { - Filter.AddFields(fields); - } + Filter.AddFields(fields); + } - public override T Accept<T, TArgs>(FilterNodeVisitor<T, TValue, TArgs> visitor, TArgs args) - { - return visitor.Visit(this, args); - } + public override T Accept<T, TArgs>(FilterNodeVisitor<T, TValue, TArgs> visitor, TArgs args) + { + return visitor.Visit(this, args); + } - public override string ToString() - { - return $"!({Filter})"; - } + public override string ToString() + { + return $"!({Filter})"; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs index 98b3fe0dcc..47fff786d1 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs @@ -11,164 +11,163 @@ using NodaTime; using NodaTime.Text; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public sealed class ConstantWithTypeVisitor : QueryNodeVisitor<ClrValue> { - public sealed class ConstantWithTypeVisitor : QueryNodeVisitor<ClrValue> + private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); + private static readonly IEdmPrimitiveType DateType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Date); + private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); + private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double); + private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); + private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); + private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64); + private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single); + private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String); + + private static readonly ConstantWithTypeVisitor Instance = new ConstantWithTypeVisitor(); + + private ConstantWithTypeVisitor() { - private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); - private static readonly IEdmPrimitiveType DateType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Date); - private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); - private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double); - private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); - private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); - private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64); - private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single); - private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String); + } - private static readonly ConstantWithTypeVisitor Instance = new ConstantWithTypeVisitor(); + public static ClrValue Visit(QueryNode node) + { + return node.Accept(Instance); + } + + public override ClrValue Visit(ConvertNode nodeIn) + { + return nodeIn.Source.Accept(this); + } - private ConstantWithTypeVisitor() + public override ClrValue Visit(CollectionConstantNode nodeIn) + { + if (nodeIn.ItemType.Definition == DateTimeType || nodeIn.ItemType.Definition == DateType) { + return nodeIn.Collection.Select(x => ParseInstant(x.Value)).ToList(); } - public static ClrValue Visit(QueryNode node) + if (nodeIn.ItemType.Definition == GuidType) { - return node.Accept(Instance); + return nodeIn.Collection.Select(x => (Guid)x.Value).ToList(); } - public override ClrValue Visit(ConvertNode nodeIn) + if (nodeIn.ItemType.Definition == BooleanType) { - return nodeIn.Source.Accept(this); + return nodeIn.Collection.Select(x => (bool)x.Value).ToList(); } - public override ClrValue Visit(CollectionConstantNode nodeIn) + if (nodeIn.ItemType.Definition == SingleType) { - if (nodeIn.ItemType.Definition == DateTimeType || nodeIn.ItemType.Definition == DateType) - { - return nodeIn.Collection.Select(x => ParseInstant(x.Value)).ToList(); - } - - if (nodeIn.ItemType.Definition == GuidType) - { - return nodeIn.Collection.Select(x => (Guid)x.Value).ToList(); - } + return nodeIn.Collection.Select(x => (float)x.Value).ToList(); + } - if (nodeIn.ItemType.Definition == BooleanType) - { - return nodeIn.Collection.Select(x => (bool)x.Value).ToList(); - } + if (nodeIn.ItemType.Definition == DoubleType) + { + return nodeIn.Collection.Select(x => (double)x.Value).ToList(); + } - if (nodeIn.ItemType.Definition == SingleType) - { - return nodeIn.Collection.Select(x => (float)x.Value).ToList(); - } + if (nodeIn.ItemType.Definition == Int32Type) + { + return nodeIn.Collection.Select(x => (int)x.Value).ToList(); + } - if (nodeIn.ItemType.Definition == DoubleType) - { - return nodeIn.Collection.Select(x => (double)x.Value).ToList(); - } + if (nodeIn.ItemType.Definition == Int64Type) + { + return nodeIn.Collection.Select(x => (long)x.Value).ToList(); + } - if (nodeIn.ItemType.Definition == Int32Type) - { - return nodeIn.Collection.Select(x => (int)x.Value).ToList(); - } + if (nodeIn.ItemType.Definition == StringType) + { + return nodeIn.Collection.Select(x => (string)x.Value).ToList(); + } - if (nodeIn.ItemType.Definition == Int64Type) - { - return nodeIn.Collection.Select(x => (long)x.Value).ToList(); - } + ThrowHelper.NotSupportedException(); + return default!; + } - if (nodeIn.ItemType.Definition == StringType) - { - return nodeIn.Collection.Select(x => (string)x.Value).ToList(); - } + public override ClrValue Visit(ConstantNode nodeIn) + { + if (nodeIn.Value == null) + { + return ClrValue.Null; + } + if (nodeIn.TypeReference == null) + { ThrowHelper.NotSupportedException(); return default!; } - public override ClrValue Visit(ConstantNode nodeIn) + if (nodeIn.TypeReference.Definition == DateTimeType || nodeIn.TypeReference.Definition == DateType) { - if (nodeIn.Value == null) - { - return ClrValue.Null; - } - - if (nodeIn.TypeReference == null) - { - ThrowHelper.NotSupportedException(); - return default!; - } + return ParseInstant(nodeIn.Value); + } - if (nodeIn.TypeReference.Definition == DateTimeType || nodeIn.TypeReference.Definition == DateType) - { - return ParseInstant(nodeIn.Value); - } + if (nodeIn.TypeReference.Definition == GuidType) + { + return (Guid)nodeIn.Value; + } - if (nodeIn.TypeReference.Definition == GuidType) - { - return (Guid)nodeIn.Value; - } + if (nodeIn.TypeReference.Definition == BooleanType) + { + return (bool)nodeIn.Value; + } - if (nodeIn.TypeReference.Definition == BooleanType) - { - return (bool)nodeIn.Value; - } + if (nodeIn.TypeReference.Definition == SingleType) + { + return (float)nodeIn.Value; + } - if (nodeIn.TypeReference.Definition == SingleType) - { - return (float)nodeIn.Value; - } + if (nodeIn.TypeReference.Definition == DoubleType) + { + return (double)nodeIn.Value; + } - if (nodeIn.TypeReference.Definition == DoubleType) - { - return (double)nodeIn.Value; - } + if (nodeIn.TypeReference.Definition == Int32Type) + { + return (int)nodeIn.Value; + } - if (nodeIn.TypeReference.Definition == Int32Type) - { - return (int)nodeIn.Value; - } + if (nodeIn.TypeReference.Definition == Int64Type) + { + return (long)nodeIn.Value; + } - if (nodeIn.TypeReference.Definition == Int64Type) - { - return (long)nodeIn.Value; - } + if (nodeIn.TypeReference.Definition == StringType) + { + return (string)nodeIn.Value; + } - if (nodeIn.TypeReference.Definition == StringType) - { - return (string)nodeIn.Value; - } + ThrowHelper.NotSupportedException(); + return default!; + } - ThrowHelper.NotSupportedException(); - return default!; + private static Instant ParseInstant(object value) + { + if (value is DateTimeOffset dateTimeOffset) + { + return Instant.FromDateTimeOffset(dateTimeOffset.Add(dateTimeOffset.Offset)); } - private static Instant ParseInstant(object value) + if (value is DateTime dateTime) { - if (value is DateTimeOffset dateTimeOffset) - { - return Instant.FromDateTimeOffset(dateTimeOffset.Add(dateTimeOffset.Offset)); - } - - if (value is DateTime dateTime) - { - return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); - } - - if (value is Date date) - { - return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0); - } + return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); + } - var parseResult = InstantPattern.ExtendedIso.Parse(value.ToString()!); + if (value is Date date) + { + return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0); + } - if (!parseResult.Success) - { - throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); - } + var parseResult = InstantPattern.ExtendedIso.Parse(value.ToString()!); - return parseResult.Value; + if (!parseResult.Success) + { + throw new ODataException("Datetime is not in a valid format. Use ISO 8601"); } + + return parseResult.Value; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelConverter.cs b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelConverter.cs index 145fd6f9b8..01f76a9b41 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelConverter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelConverter.cs @@ -8,118 +8,117 @@ using Microsoft.OData.Edm; using Squidex.Text; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public static class EdmModelConverter { - public static class EdmModelConverter + private const int MaxDepth = 7; + + public static EdmModel ConvertToEdm(this QueryModel queryModel, string modelName, string name) { - private const int MaxDepth = 7; + var model = new EdmModel(); - public static EdmModel ConvertToEdm(this QueryModel queryModel, string modelName, string name) - { - var model = new EdmModel(); + var entityType = new EdmEntityType(modelName, name); + var entityPath = new Stack<string>(); - var entityType = new EdmEntityType(modelName, name); - var entityPath = new Stack<string>(); + void Convert(EdmStructuredType target, FilterSchema schema) + { + if (schema.Fields == null) + { + return; + } - void Convert(EdmStructuredType target, FilterSchema schema) + foreach (var field in FilterSchema.GetConflictFreeFields(schema.Fields)) { - if (schema.Fields == null) - { - return; - } + var fieldName = field.Path.EscapeEdmField(); - foreach (var field in FilterSchema.GetConflictFreeFields(schema.Fields)) + switch (field.Schema.Type) { - var fieldName = field.Path.EscapeEdmField(); - - switch (field.Schema.Type) - { - case FilterSchemaType.Boolean: - target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Boolean, field.IsNullable); - break; - case FilterSchemaType.DateTime: - target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.DateTimeOffset, field.IsNullable); - break; - case FilterSchemaType.GeoObject: - target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.GeographyPoint, field.IsNullable); - break; - case FilterSchemaType.Guid: - target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Guid, field.IsNullable); - break; - case FilterSchemaType.Number: - target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Double, field.IsNullable); - break; - case FilterSchemaType.String: - case FilterSchemaType.StringArray: - target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.String, field.IsNullable); - break; - case FilterSchemaType.Object: - case FilterSchemaType.ObjectArray: + case FilterSchemaType.Boolean: + target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Boolean, field.IsNullable); + break; + case FilterSchemaType.DateTime: + target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.DateTimeOffset, field.IsNullable); + break; + case FilterSchemaType.GeoObject: + target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.GeographyPoint, field.IsNullable); + break; + case FilterSchemaType.Guid: + target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Guid, field.IsNullable); + break; + case FilterSchemaType.Number: + target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.Double, field.IsNullable); + break; + case FilterSchemaType.String: + case FilterSchemaType.StringArray: + target.AddStructuralProperty(fieldName, EdmPrimitiveTypeKind.String, field.IsNullable); + break; + case FilterSchemaType.Object: + case FilterSchemaType.ObjectArray: + { + if (field.Schema.Fields == null || field.Schema.Fields.Count == 0 || entityPath.Count >= MaxDepth) { - if (field.Schema.Fields == null || field.Schema.Fields.Count == 0 || entityPath.Count >= MaxDepth) - { - break; - } - - entityPath.Push(fieldName); - - var typeName = string.Join("_", entityPath.Reverse().Select(x => x.EscapeEdmField().ToPascalCase())); + break; + } - var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == typeName); + entityPath.Push(fieldName); - if (result == null) - { - result = new EdmComplexType(modelName, typeName); + var typeName = string.Join("_", entityPath.Reverse().Select(x => x.EscapeEdmField().ToPascalCase())); - model.AddElement(result); + var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == typeName); - Convert(result, field.Schema); - } + if (result == null) + { + result = new EdmComplexType(modelName, typeName); - target.AddStructuralProperty(fieldName, new EdmComplexTypeReference(result, field.IsNullable)); + model.AddElement(result); - entityPath.Pop(); - break; + Convert(result, field.Schema); } - case FilterSchemaType.Any: - { - var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == "Any"); + target.AddStructuralProperty(fieldName, new EdmComplexTypeReference(result, field.IsNullable)); - if (result == null) - { - result = new EdmComplexType("Squidex", "Any", null, false, true); + entityPath.Pop(); + break; + } - model.AddElement(result); - } + case FilterSchemaType.Any: + { + var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == "Any"); - target.AddStructuralProperty(fieldName, new EdmComplexTypeReference(result, field.IsNullable)); - break; + if (result == null) + { + result = new EdmComplexType("Squidex", "Any", null, false, true); + + model.AddElement(result); } - } + + target.AddStructuralProperty(fieldName, new EdmComplexTypeReference(result, field.IsNullable)); + break; + } } } + } - Convert(entityType, queryModel.Schema); + Convert(entityType, queryModel.Schema); - var container = new EdmEntityContainer("Squidex", "Container"); + var container = new EdmEntityContainer("Squidex", "Container"); - container.AddEntitySet("ContentSet", entityType); + container.AddEntitySet("ContentSet", entityType); - model.AddElement(container); - model.AddElement(entityType); + model.AddElement(container); + model.AddElement(entityType); - return model; - } + return model; + } - public static string EscapeEdmField(this string field) - { - return field.Replace("-", "_", StringComparison.Ordinal); - } + public static string EscapeEdmField(this string field) + { + return field.Replace("-", "_", StringComparison.Ordinal); + } - public static string UnescapeEdmField(this string field) - { - return field.Replace("_", "-", StringComparison.Ordinal); - } + public static string UnescapeEdmField(this string field) + { + return field.Replace("_", "-", StringComparison.Ordinal); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs index 58a5050bb0..21ef51e5f7 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs @@ -8,71 +8,70 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public static class EdmModelExtensions { - public static class EdmModelExtensions + static EdmModelExtensions() { - static EdmModelExtensions() - { - CustomUriFunctions.AddCustomUriFunction("empty", - new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetBoolean(false), - EdmCoreModel.Instance.GetUntyped())); + CustomUriFunctions.AddCustomUriFunction("empty", + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetBoolean(false), + EdmCoreModel.Instance.GetUntyped())); - CustomUriFunctions.AddCustomUriFunction("exists", - new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetBoolean(false), - EdmCoreModel.Instance.GetUntyped())); + CustomUriFunctions.AddCustomUriFunction("exists", + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetBoolean(false), + EdmCoreModel.Instance.GetUntyped())); - CustomUriFunctions.AddCustomUriFunction("matchs", - new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetBoolean(false), - EdmCoreModel.Instance.GetString(false), - EdmCoreModel.Instance.GetString(false))); + CustomUriFunctions.AddCustomUriFunction("matchs", + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetBoolean(false), + EdmCoreModel.Instance.GetString(false), + EdmCoreModel.Instance.GetString(false))); - CustomUriFunctions.AddCustomUriFunction("distanceto", - new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetDouble(false), - EdmCoreModel.Instance.GetString(true), - EdmCoreModel.Instance.GetInt32(true), - EdmCoreModel.Instance.GetInt32(true))); - } + CustomUriFunctions.AddCustomUriFunction("distanceto", + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDouble(false), + EdmCoreModel.Instance.GetString(true), + EdmCoreModel.Instance.GetInt32(true), + EdmCoreModel.Instance.GetInt32(true))); + } - public static ODataUriParser? ParseQuery(this IEdmModel model, string query) + public static ODataUriParser? ParseQuery(this IEdmModel model, string query) + { + if (!model.EntityContainer.EntitySets().Any()) { - if (!model.EntityContainer.EntitySets().Any()) - { - return null; - } - - query ??= string.Empty; - - var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.')[^1]; + return null; + } - if (query.StartsWith('?')) - { - query = query[1..]; - } + query ??= string.Empty; - var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.')[^1]; - return parser; + if (query.StartsWith('?')) + { + query = query[1..]; } - public static ClrQuery ToQuery(this ODataUriParser? parser) - { - var query = new ClrQuery(); + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); - if (parser != null) - { - parser.ParseTake(query); - parser.ParseSkip(query); - parser.ParseFilter(query); - parser.ParseSort(query); - parser.ParseRandom(query); - } + return parser; + } - return query; + public static ClrQuery ToQuery(this ODataUriParser? parser) + { + var query = new ClrQuery(); + + if (parser != null) + { + parser.ParseTake(query); + parser.ParseSkip(query); + parser.ParseFilter(query); + parser.ParseSort(query); + parser.ParseRandom(query); } + + return query; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs index 7f45a17812..9729b406c1 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs @@ -10,47 +10,46 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public static class FilterBuilder { - public static class FilterBuilder + public static void ParseFilter(this ODataUriParser query, ClrQuery result) { - public static void ParseFilter(this ODataUriParser query, ClrQuery result) + SearchClause searchClause; + try + { + searchClause = query.ParseSearch(); + } + catch (ODataException ex) { - SearchClause searchClause; - try - { - searchClause = query.ParseSearch(); - } - catch (ODataException ex) - { - var error = T.Get("common.odataSearchNotValid", new { message = ex.Message }); - - throw new ValidationException(error, ex); - } - - if (searchClause != null) - { - result.FullText = SearchTermVisitor.Visit(searchClause.Expression).ToString(); - } - - FilterClause filterClause; - try - { - filterClause = query.ParseFilter(); - } - catch (ODataException ex) - { - var error = T.Get("common.odataFilterNotValid", new { message = ex.Message }); - - throw new ValidationException(error, ex); - } - - if (filterClause != null) - { - var filter = FilterVisitor.Visit(filterClause.Expression); - - result.Filter = Optimizer<ClrValue>.Optimize(filter); - } + var error = T.Get("common.odataSearchNotValid", new { message = ex.Message }); + + throw new ValidationException(error, ex); + } + + if (searchClause != null) + { + result.FullText = SearchTermVisitor.Visit(searchClause.Expression).ToString(); + } + + FilterClause filterClause; + try + { + filterClause = query.ParseFilter(); + } + catch (ODataException ex) + { + var error = T.Get("common.odataFilterNotValid", new { message = ex.Message }); + + throw new ValidationException(error, ex); + } + + if (filterClause != null) + { + var filter = FilterVisitor.Visit(filterClause.Expression); + + result.Filter = Optimizer<ClrValue>.Optimize(filter); } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs index aaab0dcb68..6ed15028d4 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs @@ -8,196 +8,195 @@ using Microsoft.OData.UriParser; using Microsoft.Spatial; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public sealed class FilterVisitor : QueryNodeVisitor<FilterNode<ClrValue>> { - public sealed class FilterVisitor : QueryNodeVisitor<FilterNode<ClrValue>> + private static readonly FilterVisitor Instance = new FilterVisitor(); + + private FilterVisitor() { - private static readonly FilterVisitor Instance = new FilterVisitor(); + } - private FilterVisitor() + public static FilterNode<ClrValue> Visit(QueryNode node) + { + return node.Accept(Instance); + } + + public override FilterNode<ClrValue> Visit(ConvertNode nodeIn) + { + return nodeIn.Source.Accept(this); + } + + public override FilterNode<ClrValue> Visit(UnaryOperatorNode nodeIn) + { + if (nodeIn.OperatorKind == UnaryOperatorKind.Not) { + return ClrFilter.Not(nodeIn.Operand.Accept(this)); } - public static FilterNode<ClrValue> Visit(QueryNode node) + ThrowHelper.NotSupportedException(); + return default!; + } + + public override FilterNode<ClrValue> Visit(InNode nodeIn) + { + var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); + + return ClrFilter.In(PropertyPathVisitor.Visit(nodeIn.Left), value); + } + + public override FilterNode<ClrValue> Visit(SingleValueFunctionCallNode nodeIn) + { + var fieldNode = nodeIn.Parameters.ElementAt(0); + + if (string.Equals(nodeIn.Name, "empty", StringComparison.OrdinalIgnoreCase)) { - return node.Accept(Instance); + return ClrFilter.Empty(PropertyPathVisitor.Visit(fieldNode)); } - public override FilterNode<ClrValue> Visit(ConvertNode nodeIn) + if (string.Equals(nodeIn.Name, "empty", StringComparison.OrdinalIgnoreCase)) { - return nodeIn.Source.Accept(this); + return ClrFilter.Empty(PropertyPathVisitor.Visit(fieldNode)); } - public override FilterNode<ClrValue> Visit(UnaryOperatorNode nodeIn) + if (string.Equals(nodeIn.Name, "exists", StringComparison.OrdinalIgnoreCase)) { - if (nodeIn.OperatorKind == UnaryOperatorKind.Not) - { - return ClrFilter.Not(nodeIn.Operand.Accept(this)); - } - - ThrowHelper.NotSupportedException(); - return default!; + return ClrFilter.Exists(PropertyPathVisitor.Visit(fieldNode)); } - public override FilterNode<ClrValue> Visit(InNode nodeIn) + var valueNode = nodeIn.Parameters.ElementAt(1); + + if (string.Equals(nodeIn.Name, "matchs", StringComparison.OrdinalIgnoreCase)) { - var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); + var value = ConstantWithTypeVisitor.Visit(valueNode); - return ClrFilter.In(PropertyPathVisitor.Visit(nodeIn.Left), value); + return ClrFilter.Matchs(PropertyPathVisitor.Visit(fieldNode), value); } - public override FilterNode<ClrValue> Visit(SingleValueFunctionCallNode nodeIn) + if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase)) { - var fieldNode = nodeIn.Parameters.ElementAt(0); - - if (string.Equals(nodeIn.Name, "empty", StringComparison.OrdinalIgnoreCase)) - { - return ClrFilter.Empty(PropertyPathVisitor.Visit(fieldNode)); - } + var value = ConstantWithTypeVisitor.Visit(valueNode); - if (string.Equals(nodeIn.Name, "empty", StringComparison.OrdinalIgnoreCase)) - { - return ClrFilter.Empty(PropertyPathVisitor.Visit(fieldNode)); - } - - if (string.Equals(nodeIn.Name, "exists", StringComparison.OrdinalIgnoreCase)) - { - return ClrFilter.Exists(PropertyPathVisitor.Visit(fieldNode)); - } - - var valueNode = nodeIn.Parameters.ElementAt(1); - - if (string.Equals(nodeIn.Name, "matchs", StringComparison.OrdinalIgnoreCase)) - { - var value = ConstantWithTypeVisitor.Visit(valueNode); - - return ClrFilter.Matchs(PropertyPathVisitor.Visit(fieldNode), value); - } + return ClrFilter.EndsWith(PropertyPathVisitor.Visit(fieldNode), value); + } - if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase)) - { - var value = ConstantWithTypeVisitor.Visit(valueNode); + if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase)) + { + var value = ConstantWithTypeVisitor.Visit(valueNode); - return ClrFilter.EndsWith(PropertyPathVisitor.Visit(fieldNode), value); - } + return ClrFilter.StartsWith(PropertyPathVisitor.Visit(fieldNode), value); + } - if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase)) - { - var value = ConstantWithTypeVisitor.Visit(valueNode); + if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase)) + { + var value = ConstantWithTypeVisitor.Visit(valueNode); - return ClrFilter.StartsWith(PropertyPathVisitor.Visit(fieldNode), value); - } + return ClrFilter.Contains(PropertyPathVisitor.Visit(fieldNode), value); + } - if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase)) - { - var value = ConstantWithTypeVisitor.Visit(valueNode); + ThrowHelper.NotSupportedException(); + return default!; + } - return ClrFilter.Contains(PropertyPathVisitor.Visit(fieldNode), value); - } + public override FilterNode<ClrValue> Visit(BinaryOperatorNode nodeIn) + { + if (nodeIn.OperatorKind == BinaryOperatorKind.And) + { + return ClrFilter.And(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); + } - ThrowHelper.NotSupportedException(); - return default!; + if (nodeIn.OperatorKind == BinaryOperatorKind.Or) + { + return ClrFilter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); } - public override FilterNode<ClrValue> Visit(BinaryOperatorNode nodeIn) + if (nodeIn.Left is SingleValueFunctionCallNode functionNode) { - if (nodeIn.OperatorKind == BinaryOperatorKind.And) + if (string.Equals(functionNode.Name, "geo.distance", StringComparison.OrdinalIgnoreCase) && nodeIn.OperatorKind == BinaryOperatorKind.LessThan) { - return ClrFilter.And(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); - } + var valueDistance = (double)ConstantWithTypeVisitor.Visit(nodeIn.Right).Value!; - if (nodeIn.OperatorKind == BinaryOperatorKind.Or) - { - return ClrFilter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); - } + if (functionNode.Parameters.ElementAt(1) is not ConstantNode constantNode) + { + ThrowHelper.NotSupportedException(); + return default!; + } - if (nodeIn.Left is SingleValueFunctionCallNode functionNode) - { - if (string.Equals(functionNode.Name, "geo.distance", StringComparison.OrdinalIgnoreCase) && nodeIn.OperatorKind == BinaryOperatorKind.LessThan) + if (constantNode.Value is not GeographyPoint geographyPoint) { - var valueDistance = (double)ConstantWithTypeVisitor.Visit(nodeIn.Right).Value!; + ThrowHelper.NotSupportedException(); + return default!; + } - if (functionNode.Parameters.ElementAt(1) is not ConstantNode constantNode) - { - ThrowHelper.NotSupportedException(); - return default!; - } + var property = PropertyPathVisitor.Visit(functionNode.Parameters.ElementAt(0)); - if (constantNode.Value is not GeographyPoint geographyPoint) - { - ThrowHelper.NotSupportedException(); - return default!; - } + return ClrFilter.Lt(property, new FilterSphere(geographyPoint.Longitude, geographyPoint.Latitude, valueDistance)); + } + else + { + var regexFilter = Visit(functionNode); - var property = PropertyPathVisitor.Visit(functionNode.Parameters.ElementAt(0)); + var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); - return ClrFilter.Lt(property, new FilterSphere(geographyPoint.Longitude, geographyPoint.Latitude, valueDistance)); - } - else + if (value.ValueType == ClrValueType.Boolean && value.Value is bool booleanRight) { - var regexFilter = Visit(functionNode); - - var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); - - if (value.ValueType == ClrValueType.Boolean && value.Value is bool booleanRight) + if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) || + (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight)) { - if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) || - (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight)) - { - regexFilter = ClrFilter.Not(regexFilter); - } - - return regexFilter; + regexFilter = ClrFilter.Not(regexFilter); } + + return regexFilter; } } - else + } + else + { + if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual) { - if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual) - { - var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); + var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); - return ClrFilter.Ne(PropertyPathVisitor.Visit(nodeIn.Left), value); - } + return ClrFilter.Ne(PropertyPathVisitor.Visit(nodeIn.Left), value); + } - if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) - { - var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); + if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) + { + var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); - return ClrFilter.Eq(PropertyPathVisitor.Visit(nodeIn.Left), value); - } + return ClrFilter.Eq(PropertyPathVisitor.Visit(nodeIn.Left), value); + } - if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) - { - var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); + if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) + { + var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); - return ClrFilter.Lt(PropertyPathVisitor.Visit(nodeIn.Left), value); - } + return ClrFilter.Lt(PropertyPathVisitor.Visit(nodeIn.Left), value); + } - if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual) - { - var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); + if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual) + { + var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); - return ClrFilter.Le(PropertyPathVisitor.Visit(nodeIn.Left), value); - } + return ClrFilter.Le(PropertyPathVisitor.Visit(nodeIn.Left), value); + } - if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan) - { - var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); + if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan) + { + var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); - return ClrFilter.Gt(PropertyPathVisitor.Visit(nodeIn.Left), value); - } + return ClrFilter.Gt(PropertyPathVisitor.Visit(nodeIn.Left), value); + } - if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual) - { - var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); + if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual) + { + var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); - return ClrFilter.Ge(PropertyPathVisitor.Visit(nodeIn.Left), value); - } + return ClrFilter.Ge(PropertyPathVisitor.Visit(nodeIn.Left), value); } - - ThrowHelper.NotSupportedException(); - return default!; } + + ThrowHelper.NotSupportedException(); + return default!; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs index 02068ddebb..fb3c13779b 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs @@ -8,44 +8,43 @@ using System.Globalization; using Microsoft.OData.UriParser; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public static class LimitExtensions { - public static class LimitExtensions + public static void ParseTake(this ODataUriParser query, ClrQuery result) { - public static void ParseTake(this ODataUriParser query, ClrQuery result) - { - var top = query.ParseTop(); + var top = query.ParseTop(); - if (top != null) - { - result.Take = top.Value; - } + if (top != null) + { + result.Take = top.Value; } + } - public static void ParseSkip(this ODataUriParser query, ClrQuery result) - { - var skip = query.ParseSkip(); + public static void ParseSkip(this ODataUriParser query, ClrQuery result) + { + var skip = query.ParseSkip(); - if (skip != null) - { - result.Skip = skip.Value; - } + if (skip != null) + { + result.Skip = skip.Value; } + } + + public static void ParseRandom(this ODataUriParser query, ClrQuery result) + { + var customQueries = query.CustomQueryOptions; + + var randomQuery = customQueries.FirstOrDefault(x => + string.Equals(x.Key, "random", StringComparison.OrdinalIgnoreCase) || + string.Equals(x.Key, "randomCount", StringComparison.OrdinalIgnoreCase) || + string.Equals(x.Key, "$random", StringComparison.OrdinalIgnoreCase) || + string.Equals(x.Key, "$randomCount", StringComparison.OrdinalIgnoreCase)); - public static void ParseRandom(this ODataUriParser query, ClrQuery result) + if (int.TryParse(randomQuery.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var random)) { - var customQueries = query.CustomQueryOptions; - - var randomQuery = customQueries.FirstOrDefault(x => - string.Equals(x.Key, "random", StringComparison.OrdinalIgnoreCase) || - string.Equals(x.Key, "randomCount", StringComparison.OrdinalIgnoreCase) || - string.Equals(x.Key, "$random", StringComparison.OrdinalIgnoreCase) || - string.Equals(x.Key, "$randomCount", StringComparison.OrdinalIgnoreCase)); - - if (int.TryParse(randomQuery.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var random)) - { - result.Random = random; - } + result.Random = random; } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs index ad4bbc3b8b..06d14dc7c6 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs @@ -9,58 +9,57 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public sealed class PropertyPathVisitor : QueryNodeVisitor<ImmutableList<string>> { - public sealed class PropertyPathVisitor : QueryNodeVisitor<ImmutableList<string>> + private static readonly PropertyPathVisitor Instance = new PropertyPathVisitor(); + + private PropertyPathVisitor() { - private static readonly PropertyPathVisitor Instance = new PropertyPathVisitor(); + } - private PropertyPathVisitor() - { - } + public static PropertyPath Visit(QueryNode node) + { + return new PropertyPath(node.Accept(Instance)); + } - public static PropertyPath Visit(QueryNode node) - { - return new PropertyPath(node.Accept(Instance)); - } + public override ImmutableList<string> Visit(ConvertNode nodeIn) + { + return nodeIn.Source.Accept(this); + } - public override ImmutableList<string> Visit(ConvertNode nodeIn) + public override ImmutableList<string> Visit(SingleComplexNode nodeIn) + { + if (nodeIn.Source is SingleComplexNode) { - return nodeIn.Source.Accept(this); + return nodeIn.Source.Accept(this).Add(UnescapeEdmField(nodeIn.Property)); } - - public override ImmutableList<string> Visit(SingleComplexNode nodeIn) + else { - if (nodeIn.Source is SingleComplexNode) - { - return nodeIn.Source.Accept(this).Add(UnescapeEdmField(nodeIn.Property)); - } - else - { - return ImmutableList.Create(UnescapeEdmField(nodeIn.Property)); - } + return ImmutableList.Create(UnescapeEdmField(nodeIn.Property)); } + } - public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn) + public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn) + { + if (nodeIn.Source is SingleComplexNode) { - if (nodeIn.Source is SingleComplexNode) - { - return nodeIn.Source.Accept(this).Add(UnescapeEdmField(nodeIn.Property)); - } - else - { - return ImmutableList.Create(UnescapeEdmField(nodeIn.Property)); - } + return nodeIn.Source.Accept(this).Add(UnescapeEdmField(nodeIn.Property)); } - - public override ImmutableList<string> Visit(SingleValueOpenPropertyAccessNode nodeIn) + else { - return nodeIn.Source.Accept(this).Add(nodeIn.Name); + return ImmutableList.Create(UnescapeEdmField(nodeIn.Property)); } + } - private static string UnescapeEdmField(IEdmNamedElement property) - { - return property.Name; - } + public override ImmutableList<string> Visit(SingleValueOpenPropertyAccessNode nodeIn) + { + return nodeIn.Source.Accept(this).Add(nodeIn.Name); + } + + private static string UnescapeEdmField(IEdmNamedElement property) + { + return property.Name; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs index fcad79031c..a48c3edaf7 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs @@ -7,35 +7,34 @@ using Microsoft.OData.UriParser; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public class SearchTermVisitor : QueryNodeVisitor<string> { - public class SearchTermVisitor : QueryNodeVisitor<string> + private static readonly SearchTermVisitor Instance = new SearchTermVisitor(); + + private SearchTermVisitor() { - private static readonly SearchTermVisitor Instance = new SearchTermVisitor(); + } - private SearchTermVisitor() - { - } + public static object Visit(QueryNode node) + { + return node.Accept(Instance); + } - public static object Visit(QueryNode node) + public override string Visit(BinaryOperatorNode nodeIn) + { + if (nodeIn.OperatorKind == BinaryOperatorKind.And) { - return node.Accept(Instance); + return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this); } - public override string Visit(BinaryOperatorNode nodeIn) - { - if (nodeIn.OperatorKind == BinaryOperatorKind.And) - { - return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this); - } - - ThrowHelper.NotSupportedException(); - return default!; - } + ThrowHelper.NotSupportedException(); + return default!; + } - public override string Visit(SearchTermNode nodeIn) - { - return nodeIn.Text; - } + public override string Visit(SearchTermNode nodeIn) + { + return nodeIn.Text; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs index 919e1671fc..1a815e0bed 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs @@ -7,38 +7,37 @@ using Microsoft.OData.UriParser; -namespace Squidex.Infrastructure.Queries.OData +namespace Squidex.Infrastructure.Queries.OData; + +public static class SortBuilder { - public static class SortBuilder + public static void ParseSort(this ODataUriParser query, ClrQuery result) { - public static void ParseSort(this ODataUriParser query, ClrQuery result) - { - var orderBy = query.ParseOrderBy(); + var orderBy = query.ParseOrderBy(); - if (orderBy != null) + if (orderBy != null) + { + while (orderBy != null) { - while (orderBy != null) - { - result.Sort ??= new List<SortNode>(); - result.Sort.Add(OrderBy(orderBy)); + result.Sort ??= new List<SortNode>(); + result.Sort.Add(OrderBy(orderBy)); - orderBy = orderBy.ThenBy; - } + orderBy = orderBy.ThenBy; } } + } - public static SortNode OrderBy(OrderByClause clause) - { - var path = PropertyPathVisitor.Visit(clause.Expression); + public static SortNode OrderBy(OrderByClause clause) + { + var path = PropertyPathVisitor.Visit(clause.Expression); - if (clause.Direction == OrderByDirection.Ascending) - { - return new SortNode(path, SortOrder.Ascending); - } - else - { - return new SortNode(path, SortOrder.Descending); - } + if (clause.Direction == OrderByDirection.Ascending) + { + return new SortNode(path, SortOrder.Ascending); + } + else + { + return new SortNode(path, SortOrder.Descending); } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs b/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs index a98898edde..9a734070bc 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Optimizer.cs @@ -5,76 +5,75 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed class Optimizer<TValue> : TransformVisitor<TValue, None> { - public sealed class Optimizer<TValue> : TransformVisitor<TValue, None> + private static readonly Optimizer<TValue> Instance = new Optimizer<TValue>(); + + private Optimizer() + { + } + + public static FilterNode<TValue>? Optimize(FilterNode<TValue> source) + { + return source?.Accept(Instance, None.Value); + } + + public override FilterNode<TValue>? Visit(LogicalFilter<TValue> nodeIn, None args) { - private static readonly Optimizer<TValue> Instance = new Optimizer<TValue>(); + var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count); - private Optimizer() + foreach (var filter in nodeIn.Filters) { + var transformed = filter.Accept(this, None.Value); + + if (transformed != null) + { + pruned.Add(transformed); + } } - public static FilterNode<TValue>? Optimize(FilterNode<TValue> source) + if (pruned.Count == 1) { - return source?.Accept(Instance, None.Value); + return pruned[0]; } - public override FilterNode<TValue>? Visit(LogicalFilter<TValue> nodeIn, None args) + if (pruned.Count == 0) { - var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count); - - foreach (var filter in nodeIn.Filters) - { - var transformed = filter.Accept(this, None.Value); - - if (transformed != null) - { - pruned.Add(transformed); - } - } + return null; + } - if (pruned.Count == 1) - { - return pruned[0]; - } + return nodeIn with { Filters = pruned }; + } - if (pruned.Count == 0) - { - return null; - } + public override FilterNode<TValue>? Visit(NegateFilter<TValue> nodeIn, None args) + { + var pruned = nodeIn.Filter.Accept(this, None.Value); - return nodeIn with { Filters = pruned }; + if (pruned == null) + { + return null; } - public override FilterNode<TValue>? Visit(NegateFilter<TValue> nodeIn, None args) + if (pruned is CompareFilter<TValue> comparison) { - var pruned = nodeIn.Filter.Accept(this, None.Value); - - if (pruned == null) - { - return null; - } - - if (pruned is CompareFilter<TValue> comparison) + if (comparison.Operator == CompareOperator.Equals) { - if (comparison.Operator == CompareOperator.Equals) - { - return comparison with { Operator = CompareOperator.NotEquals }; - } - - if (comparison.Operator == CompareOperator.NotEquals) - { - return comparison with { Operator = CompareOperator.Equals }; - } + return comparison with { Operator = CompareOperator.NotEquals }; } - if (ReferenceEquals(pruned, nodeIn.Filter)) + if (comparison.Operator == CompareOperator.NotEquals) { - return nodeIn; + return comparison with { Operator = CompareOperator.Equals }; } + } - return new NegateFilter<TValue>(pruned); + if (ReferenceEquals(pruned, nodeIn.Filter)) + { + return nodeIn; } + + return new NegateFilter<TValue>(pruned); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs b/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs index e098aa29fe..3f22376872 100644 --- a/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs @@ -7,24 +7,23 @@ using Squidex.Text; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed class PascalCasePathConverter<TValue> : TransformVisitor<TValue, None> { - public sealed class PascalCasePathConverter<TValue> : TransformVisitor<TValue, None> - { - private static readonly PascalCasePathConverter<TValue> Instance = new PascalCasePathConverter<TValue>(); + private static readonly PascalCasePathConverter<TValue> Instance = new PascalCasePathConverter<TValue>(); - private PascalCasePathConverter() - { - } + private PascalCasePathConverter() + { + } - public static FilterNode<TValue>? Transform(FilterNode<TValue> node) - { - return node.Accept(Instance, None.Value); - } + public static FilterNode<TValue>? Transform(FilterNode<TValue> node) + { + return node.Accept(Instance, None.Value); + } - public override FilterNode<TValue>? Visit(CompareFilter<TValue> nodeIn, None args) - { - return new CompareFilter<TValue>(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value); - } + public override FilterNode<TValue>? Visit(CompareFilter<TValue> nodeIn, None args) + { + return new CompareFilter<TValue>(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs b/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs index 3562c06f6b..4aae953e7b 100644 --- a/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs +++ b/backend/src/Squidex.Infrastructure/Queries/PropertyPath.cs @@ -7,107 +7,106 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed class PropertyPath : ReadonlyList<string> { - public sealed class PropertyPath : ReadonlyList<string> - { - private static readonly char[] Separators = { '.', '/' }; + private static readonly char[] Separators = { '.', '/' }; - public PropertyPath(IList<string> items) - : base(items) + public PropertyPath(IList<string> items) + : base(items) + { + if (items.Count == 0) { - if (items.Count == 0) - { - ThrowHelper.ArgumentException("Path cannot be empty.", nameof(items)); - } + ThrowHelper.ArgumentException("Path cannot be empty.", nameof(items)); } + } - public static implicit operator PropertyPath(string path) - { - var result = new List<string>(); + public static implicit operator PropertyPath(string path) + { + var result = new List<string>(); + + var currentPath = path.AsSpan(); + var currentPosition = 0; - var currentPath = path.AsSpan(); - var currentPosition = 0; + void Add(ReadOnlySpan<char> value) + { + var property = value.Trim(Separators).ToString(); - void Add(ReadOnlySpan<char> value) + if (property.Length == 0) { - var property = value.Trim(Separators).ToString(); + return; + } + + property = property.Replace("\\/", "/", StringComparison.OrdinalIgnoreCase); + property = property.Replace("\\.", ".", StringComparison.OrdinalIgnoreCase); - if (property.Length == 0) - { - return; - } + result.Add(property); + } - property = property.Replace("\\/", "/", StringComparison.OrdinalIgnoreCase); - property = property.Replace("\\.", ".", StringComparison.OrdinalIgnoreCase); + while (true) + { + var nextDot = currentPath[currentPosition..].IndexOfAny(Separators) + currentPosition; - result.Add(property); + if (nextDot < currentPosition) + { + Add(currentPath); + break; } - - while (true) + else if (nextDot == currentPosition) + { + currentPath = currentPath[1..]; + } + else if (currentPath[nextDot - 1] == '\\') { - var nextDot = currentPath[currentPosition..].IndexOfAny(Separators) + currentPosition; - - if (nextDot < currentPosition) - { - Add(currentPath); - break; - } - else if (nextDot == currentPosition) - { - currentPath = currentPath[1..]; - } - else if (currentPath[nextDot - 1] == '\\') - { - currentPosition = nextDot + 1; - } - else - { - Add(currentPath[..nextDot]); - - currentPath = currentPath[nextDot..].Trim(Separators); - currentPosition = 0; - } + currentPosition = nextDot + 1; } + else + { + Add(currentPath[..nextDot]); - return Create(result); + currentPath = currentPath[nextDot..].Trim(Separators); + currentPosition = 0; + } } - public static implicit operator PropertyPath(string[] path) - { - return Create(path); - } + return Create(result); + } - public static implicit operator PropertyPath(List<string> path) - { - return Create(path); - } + public static implicit operator PropertyPath(string[] path) + { + return Create(path); + } - public override string ToString() - { - return string.Join(".", this); - } + public static implicit operator PropertyPath(List<string> path) + { + return Create(path); + } + + public override string ToString() + { + return string.Join(".", this); + } - private static string Unescape(string source) + private static string Unescape(string source) + { + return source + .Replace("\\/", "/", StringComparison.OrdinalIgnoreCase) + .Replace("\\.", ".", StringComparison.OrdinalIgnoreCase); + } + + private static PropertyPath Create(IEnumerable<string>? source) + { + var inner = source?.ToList(); + + if (inner == null || inner.Count == 0) { - return source - .Replace("\\/", "/", StringComparison.OrdinalIgnoreCase) - .Replace("\\.", ".", StringComparison.OrdinalIgnoreCase); + ThrowHelper.ArgumentException("Path cannot be empty.", nameof(source)); + return null!; } - - private static PropertyPath Create(IEnumerable<string>? source) + else { - var inner = source?.ToList(); - - if (inner == null || inner.Count == 0) - { - ThrowHelper.ArgumentException("Path cannot be empty.", nameof(source)); - return null!; - } - else - { - return new PropertyPath(inner); - } + return new PropertyPath(inner); } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Query.cs b/backend/src/Squidex.Infrastructure/Queries/Query.cs index 66b5496800..353a1def2b 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Query.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Query.cs @@ -7,85 +7,84 @@ using System.Text; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public class Query<TValue> { - public class Query<TValue> - { - public FilterNode<TValue>? Filter { get; set; } + public FilterNode<TValue>? Filter { get; set; } - public string? FullText { get; set; } + public string? FullText { get; set; } - public long Skip { get; set; } + public long Skip { get; set; } - public long Take { get; set; } = long.MaxValue; + public long Take { get; set; } = long.MaxValue; - public long Random { get; set; } + public long Random { get; set; } - public long Top - { - set => Take = value; - } + public long Top + { + set => Take = value; + } - public List<SortNode>? Sort { get; set; } = new List<SortNode>(); + public List<SortNode>? Sort { get; set; } = new List<SortNode>(); - public HashSet<string> GetAllFields() - { - var result = new HashSet<string>(); + public HashSet<string> GetAllFields() + { + var result = new HashSet<string>(); - if (Sort != null) + if (Sort != null) + { + foreach (var sorting in Sort) { - foreach (var sorting in Sort) - { - result.Add(sorting.Path.ToString()); - } + result.Add(sorting.Path.ToString()); } - - Filter?.AddFields(result); - - return result; } - public override string ToString() - { - var sb = new StringBuilder(); + Filter?.AddFields(result); - if (Filter != null) - { - sb.AppendIfNotEmpty("; "); - sb.Append($"Filter: {Filter}"); - } + return result; + } - if (FullText != null) - { - sb.AppendIfNotEmpty("; "); - sb.Append($"FullText: '{FullText.Replace("'", "\'", StringComparison.Ordinal)}'"); - } + public override string ToString() + { + var sb = new StringBuilder(); - if (Skip > 0) - { - sb.AppendIfNotEmpty("; "); - sb.Append($"Skip: {Skip}"); - } + if (Filter != null) + { + sb.AppendIfNotEmpty("; "); + sb.Append($"Filter: {Filter}"); + } - if (Take < long.MaxValue) - { - sb.AppendIfNotEmpty("; "); - sb.Append($"Take: {Take}"); - } + if (FullText != null) + { + sb.AppendIfNotEmpty("; "); + sb.Append($"FullText: '{FullText.Replace("'", "\'", StringComparison.Ordinal)}'"); + } - if (Random > 0) - { - sb.AppendIfNotEmpty("; "); - sb.Append($"Random: {Random}"); - } + if (Skip > 0) + { + sb.AppendIfNotEmpty("; "); + sb.Append($"Skip: {Skip}"); + } - if (Sort != null && Sort.Count > 0) - { - sb.AppendIfNotEmpty("; "); - sb.Append($"Sort: {string.Join(", ", Sort)}"); - } + if (Take < long.MaxValue) + { + sb.AppendIfNotEmpty("; "); + sb.Append($"Take: {Take}"); + } - return sb.ToString(); + if (Random > 0) + { + sb.AppendIfNotEmpty("; "); + sb.Append($"Random: {Random}"); } + + if (Sort != null && Sort.Count > 0) + { + sb.AppendIfNotEmpty("; "); + sb.Append($"Sort: {string.Join(", ", Sort)}"); + } + + return sb.ToString(); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/QueryExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/QueryExtensions.cs index 31a277ec31..9e1388d912 100644 --- a/backend/src/Squidex.Infrastructure/Queries/QueryExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/QueryExtensions.cs @@ -5,27 +5,26 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public static class QueryExtensions { - public static class QueryExtensions + public static bool HasFilterField<T>(this Query<T>? query, string field) { - public static bool HasFilterField<T>(this Query<T>? query, string field) - { - return HasField(query?.Filter, field); - } + return HasField(query?.Filter, field); + } - public static bool HasField<T>(this FilterNode<T>? filter, string field) + public static bool HasField<T>(this FilterNode<T>? filter, string field) + { + if (filter == null) { - if (filter == null) - { - return false; - } + return false; + } - var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase); - filter.AddFields(fields); + filter.AddFields(fields); - return fields.Contains(field); - } + return fields.Contains(field); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/QueryModel.cs b/backend/src/Squidex.Infrastructure/Queries/QueryModel.cs index 900ca0ffea..8932d0adae 100644 --- a/backend/src/Squidex.Infrastructure/Queries/QueryModel.cs +++ b/backend/src/Squidex.Infrastructure/Queries/QueryModel.cs @@ -5,132 +5,131 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed class QueryModel { - public sealed class QueryModel + public static readonly IReadOnlyDictionary<FilterSchemaType, IReadOnlyList<CompareOperator>> DefaultOperators = new Dictionary<FilterSchemaType, IReadOnlyList<CompareOperator>> { - public static readonly IReadOnlyDictionary<FilterSchemaType, IReadOnlyList<CompareOperator>> DefaultOperators = new Dictionary<FilterSchemaType, IReadOnlyList<CompareOperator>> + [FilterSchemaType.Any] = Enum.GetValues(typeof(CompareOperator)).OfType<CompareOperator>().ToList(), + [FilterSchemaType.Boolean] = new List<CompareOperator> { - [FilterSchemaType.Any] = Enum.GetValues(typeof(CompareOperator)).OfType<CompareOperator>().ToList(), - [FilterSchemaType.Boolean] = new List<CompareOperator> - { - CompareOperator.Equals, - CompareOperator.Exists, - CompareOperator.In, - CompareOperator.NotEquals - }, - [FilterSchemaType.DateTime] = new List<CompareOperator> - { - CompareOperator.Contains, - CompareOperator.Empty, - CompareOperator.Exists, - CompareOperator.EndsWith, - CompareOperator.Equals, - CompareOperator.GreaterThan, - CompareOperator.GreaterThanOrEqual, - CompareOperator.In, - CompareOperator.LessThan, - CompareOperator.LessThanOrEqual, - CompareOperator.Matchs, - CompareOperator.NotEquals, - CompareOperator.StartsWith - }, - [FilterSchemaType.GeoObject] = new List<CompareOperator> - { - CompareOperator.LessThan, - CompareOperator.Exists - }, - [FilterSchemaType.Guid] = new List<CompareOperator> - { - CompareOperator.Contains, - CompareOperator.Empty, - CompareOperator.Exists, - CompareOperator.EndsWith, - CompareOperator.Equals, - CompareOperator.GreaterThan, - CompareOperator.GreaterThanOrEqual, - CompareOperator.In, - CompareOperator.LessThan, - CompareOperator.LessThanOrEqual, - CompareOperator.Matchs, - CompareOperator.NotEquals, - CompareOperator.StartsWith - }, - [FilterSchemaType.Object] = new List<CompareOperator>(), - [FilterSchemaType.ObjectArray] = new List<CompareOperator> - { - CompareOperator.Empty, - CompareOperator.Exists, - CompareOperator.Equals, - CompareOperator.In, - CompareOperator.NotEquals - }, - [FilterSchemaType.Number] = new List<CompareOperator> - { - CompareOperator.Equals, - CompareOperator.Exists, - CompareOperator.LessThan, - CompareOperator.LessThanOrEqual, - CompareOperator.GreaterThan, - CompareOperator.GreaterThanOrEqual, - CompareOperator.In, - CompareOperator.NotEquals - }, - [FilterSchemaType.String] = new List<CompareOperator> - { - CompareOperator.Contains, - CompareOperator.Empty, - CompareOperator.Exists, - CompareOperator.EndsWith, - CompareOperator.Equals, - CompareOperator.GreaterThan, - CompareOperator.GreaterThanOrEqual, - CompareOperator.In, - CompareOperator.LessThan, - CompareOperator.LessThanOrEqual, - CompareOperator.Matchs, - CompareOperator.NotEquals, - CompareOperator.StartsWith - }, - [FilterSchemaType.StringArray] = new List<CompareOperator> - { - CompareOperator.Contains, - CompareOperator.Empty, - CompareOperator.Exists, - CompareOperator.EndsWith, - CompareOperator.Equals, - CompareOperator.GreaterThan, - CompareOperator.GreaterThanOrEqual, - CompareOperator.In, - CompareOperator.LessThan, - CompareOperator.LessThanOrEqual, - CompareOperator.Matchs, - CompareOperator.NotEquals, - CompareOperator.StartsWith - } - }; - - public FilterSchema Schema { get; init; } = FilterSchema.Any; + CompareOperator.Equals, + CompareOperator.Exists, + CompareOperator.In, + CompareOperator.NotEquals + }, + [FilterSchemaType.DateTime] = new List<CompareOperator> + { + CompareOperator.Contains, + CompareOperator.Empty, + CompareOperator.Exists, + CompareOperator.EndsWith, + CompareOperator.Equals, + CompareOperator.GreaterThan, + CompareOperator.GreaterThanOrEqual, + CompareOperator.In, + CompareOperator.LessThan, + CompareOperator.LessThanOrEqual, + CompareOperator.Matchs, + CompareOperator.NotEquals, + CompareOperator.StartsWith + }, + [FilterSchemaType.GeoObject] = new List<CompareOperator> + { + CompareOperator.LessThan, + CompareOperator.Exists + }, + [FilterSchemaType.Guid] = new List<CompareOperator> + { + CompareOperator.Contains, + CompareOperator.Empty, + CompareOperator.Exists, + CompareOperator.EndsWith, + CompareOperator.Equals, + CompareOperator.GreaterThan, + CompareOperator.GreaterThanOrEqual, + CompareOperator.In, + CompareOperator.LessThan, + CompareOperator.LessThanOrEqual, + CompareOperator.Matchs, + CompareOperator.NotEquals, + CompareOperator.StartsWith + }, + [FilterSchemaType.Object] = new List<CompareOperator>(), + [FilterSchemaType.ObjectArray] = new List<CompareOperator> + { + CompareOperator.Empty, + CompareOperator.Exists, + CompareOperator.Equals, + CompareOperator.In, + CompareOperator.NotEquals + }, + [FilterSchemaType.Number] = new List<CompareOperator> + { + CompareOperator.Equals, + CompareOperator.Exists, + CompareOperator.LessThan, + CompareOperator.LessThanOrEqual, + CompareOperator.GreaterThan, + CompareOperator.GreaterThanOrEqual, + CompareOperator.In, + CompareOperator.NotEquals + }, + [FilterSchemaType.String] = new List<CompareOperator> + { + CompareOperator.Contains, + CompareOperator.Empty, + CompareOperator.Exists, + CompareOperator.EndsWith, + CompareOperator.Equals, + CompareOperator.GreaterThan, + CompareOperator.GreaterThanOrEqual, + CompareOperator.In, + CompareOperator.LessThan, + CompareOperator.LessThanOrEqual, + CompareOperator.Matchs, + CompareOperator.NotEquals, + CompareOperator.StartsWith + }, + [FilterSchemaType.StringArray] = new List<CompareOperator> + { + CompareOperator.Contains, + CompareOperator.Empty, + CompareOperator.Exists, + CompareOperator.EndsWith, + CompareOperator.Equals, + CompareOperator.GreaterThan, + CompareOperator.GreaterThanOrEqual, + CompareOperator.In, + CompareOperator.LessThan, + CompareOperator.LessThanOrEqual, + CompareOperator.Matchs, + CompareOperator.NotEquals, + CompareOperator.StartsWith + } + }; - public IReadOnlyDictionary<FilterSchemaType, IReadOnlyList<CompareOperator>> Operators { get; init; } = DefaultOperators; + public FilterSchema Schema { get; init; } = FilterSchema.Any; - public QueryModel Flatten(int maxDepth = 7, bool onlyWithOperators = true) - { - var predicate = (Predicate<FilterSchema>?)null; + public IReadOnlyDictionary<FilterSchemaType, IReadOnlyList<CompareOperator>> Operators { get; init; } = DefaultOperators; - if (onlyWithOperators) - { - predicate = x => Operators.TryGetValue(x.Type, out var operators) && operators.Count > 0; - } + public QueryModel Flatten(int maxDepth = 7, bool onlyWithOperators = true) + { + var predicate = (Predicate<FilterSchema>?)null; - var flatten = Schema.Flatten(maxDepth, predicate); + if (onlyWithOperators) + { + predicate = x => Operators.TryGetValue(x.Type, out var operators) && operators.Count > 0; + } - if (ReferenceEquals(flatten, Schema)) - { - return this; - } + var flatten = Schema.Flatten(maxDepth, predicate); - return new QueryModel { Operators = Operators, Schema = flatten }; + if (ReferenceEquals(flatten, Schema)) + { + return this; } + + return new QueryModel { Operators = Operators, Schema = flatten }; } } diff --git a/backend/src/Squidex.Infrastructure/Queries/SortBuilder.cs b/backend/src/Squidex.Infrastructure/Queries/SortBuilder.cs index 2f3346bb1c..955b128598 100644 --- a/backend/src/Squidex.Infrastructure/Queries/SortBuilder.cs +++ b/backend/src/Squidex.Infrastructure/Queries/SortBuilder.cs @@ -5,18 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public static class SortBuilder { - public static class SortBuilder + public static SortNode Ascending(string path) { - public static SortNode Ascending(string path) - { - return new SortNode(path.Split('.', '/').ToList(), SortOrder.Ascending); - } + return new SortNode(path.Split('.', '/').ToList(), SortOrder.Ascending); + } - public static SortNode Descending(string path) - { - return new SortNode(path.Split('.', '/').ToList(), SortOrder.Descending); - } + public static SortNode Descending(string path) + { + return new SortNode(path.Split('.', '/').ToList(), SortOrder.Descending); } } diff --git a/backend/src/Squidex.Infrastructure/Queries/SortNode.cs b/backend/src/Squidex.Infrastructure/Queries/SortNode.cs index 174bb7c221..76bf80a4c2 100644 --- a/backend/src/Squidex.Infrastructure/Queries/SortNode.cs +++ b/backend/src/Squidex.Infrastructure/Queries/SortNode.cs @@ -5,29 +5,28 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed class SortNode { - public sealed class SortNode - { - public PropertyPath Path { get; } + public PropertyPath Path { get; } - public SortOrder Order { get; set; } + public SortOrder Order { get; set; } - public SortNode(PropertyPath path, SortOrder order) - { - Guard.NotNull(path); - Guard.Enum(order); + public SortNode(PropertyPath path, SortOrder order) + { + Guard.NotNull(path); + Guard.Enum(order); - Path = path; + Path = path; - Order = order; - } + Order = order; + } - public override string ToString() - { - var path = string.Join(".", Path); + public override string ToString() + { + var path = string.Join(".", Path); - return $"{path} {Order}"; - } + return $"{path} {Order}"; } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Queries/SortOrder.cs b/backend/src/Squidex.Infrastructure/Queries/SortOrder.cs index cfec5b783d..0f10ad318d 100644 --- a/backend/src/Squidex.Infrastructure/Queries/SortOrder.cs +++ b/backend/src/Squidex.Infrastructure/Queries/SortOrder.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public enum SortOrder { - public enum SortOrder - { - Ascending, - Descending - } + Ascending, + Descending } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs index 41be68c80f..1b407ee903 100644 --- a/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/TransformVisitor.cs @@ -5,42 +5,41 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public abstract class TransformVisitor<TValue, TArgs> : FilterNodeVisitor<FilterNode<TValue>?, TValue, TArgs> { - public abstract class TransformVisitor<TValue, TArgs> : FilterNodeVisitor<FilterNode<TValue>?, TValue, TArgs> + public override FilterNode<TValue>? Visit(CompareFilter<TValue> nodeIn, TArgs args) { - public override FilterNode<TValue>? Visit(CompareFilter<TValue> nodeIn, TArgs args) - { - return nodeIn; - } + return nodeIn; + } - public override FilterNode<TValue>? Visit(LogicalFilter<TValue> nodeIn, TArgs args) + public override FilterNode<TValue>? Visit(LogicalFilter<TValue> nodeIn, TArgs args) + { + var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count); + + foreach (var inner in nodeIn.Filters) { - var pruned = new List<FilterNode<TValue>>(nodeIn.Filters.Count); + var transformed = inner.Accept(this, args); - foreach (var inner in nodeIn.Filters) + if (transformed != null) { - var transformed = inner.Accept(this, args); - - if (transformed != null) - { - pruned.Add(transformed); - } + pruned.Add(transformed); } - - return new LogicalFilter<TValue>(nodeIn.Type, pruned); } - public override FilterNode<TValue>? Visit(NegateFilter<TValue> nodeIn, TArgs args) - { - var inner = nodeIn.Filter.Accept(this, args); + return new LogicalFilter<TValue>(nodeIn.Type, pruned); + } - if (inner == null) - { - return inner; - } + public override FilterNode<TValue>? Visit(NegateFilter<TValue> nodeIn, TArgs args) + { + var inner = nodeIn.Filter.Accept(this, args); - return new NegateFilter<TValue>(inner); + if (inner == null) + { + return inner; } + + return new NegateFilter<TValue>(inner); } } diff --git a/backend/src/Squidex.Infrastructure/RandomHash.cs b/backend/src/Squidex.Infrastructure/RandomHash.cs index e14180ecb2..5506d22b8c 100644 --- a/backend/src/Squidex.Infrastructure/RandomHash.cs +++ b/backend/src/Squidex.Infrastructure/RandomHash.cs @@ -8,82 +8,81 @@ using System.Security.Cryptography; using System.Text; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class RandomHash { - public static class RandomHash + public static string New() { - public static string New() - { - return Guid.NewGuid() - .ToString().ToSha256Base64() - .ToLowerInvariant() - .Replace("+", "x", StringComparison.Ordinal) - .Replace("=", "x", StringComparison.Ordinal) - .Replace("/", "x", StringComparison.Ordinal); - } + return Guid.NewGuid() + .ToString().ToSha256Base64() + .ToLowerInvariant() + .Replace("+", "x", StringComparison.Ordinal) + .Replace("=", "x", StringComparison.Ordinal) + .Replace("/", "x", StringComparison.Ordinal); + } - public static string Simple() - { - return Guid.NewGuid().ToString().Replace("-", string.Empty, StringComparison.Ordinal); - } + public static string Simple() + { + return Guid.NewGuid().ToString().Replace("-", string.Empty, StringComparison.Ordinal); + } - public static string ToSha256Base64(this string value) - { - return ToSha256Base64(Encoding.UTF8.GetBytes(value)); - } + public static string ToSha256Base64(this string value) + { + return ToSha256Base64(Encoding.UTF8.GetBytes(value)); + } - public static string ToSha256Base64(this byte[] bytes) + public static string ToSha256Base64(this byte[] bytes) + { + using (var sha = SHA256.Create()) { - using (var sha = SHA256.Create()) - { - var bytesHash = sha.ComputeHash(bytes); + var bytesHash = sha.ComputeHash(bytes); - var result = Convert.ToBase64String(bytesHash); + var result = Convert.ToBase64String(bytesHash); - return result; - } + return result; } + } - public static string ToSha256(this string value) - { - return value.ToHashed(SHA256.Create()); - } + public static string ToSha256(this string value) + { + return value.ToHashed(SHA256.Create()); + } - public static string ToSha512(this string value) - { - return value.ToHashed(SHA512.Create()); - } + public static string ToSha512(this string value) + { + return value.ToHashed(SHA512.Create()); + } - public static string ToSha256(this byte[] bytes) - { - return bytes.ToHashed(SHA256.Create()); - } + public static string ToSha256(this byte[] bytes) + { + return bytes.ToHashed(SHA256.Create()); + } - public static string ToMD5(this string value) - { - return value.ToHashed(MD5.Create()); - } + public static string ToMD5(this string value) + { + return value.ToHashed(MD5.Create()); + } - public static string ToMD5(this byte[] bytes) - { - return bytes.ToHashed(MD5.Create()); - } + public static string ToMD5(this byte[] bytes) + { + return bytes.ToHashed(MD5.Create()); + } - public static string ToHashed(this string value, HashAlgorithm algorithm) - { - return Encoding.UTF8.GetBytes(value).ToHashed(algorithm); - } + public static string ToHashed(this string value, HashAlgorithm algorithm) + { + return Encoding.UTF8.GetBytes(value).ToHashed(algorithm); + } - public static string ToHashed(this byte[] bytes, HashAlgorithm algorithm) + public static string ToHashed(this byte[] bytes, HashAlgorithm algorithm) + { + using (algorithm) { - using (algorithm) - { - var bytesHash = algorithm.ComputeHash(bytes); + var bytesHash = algorithm.ComputeHash(bytes); - var result = Encoding.UTF8.GetString(bytesHash); + var result = Encoding.UTF8.GetString(bytesHash); - return result; - } + return result; } } } diff --git a/backend/src/Squidex.Infrastructure/RefToken.cs b/backend/src/Squidex.Infrastructure/RefToken.cs index c4b90d9a35..56876d557a 100644 --- a/backend/src/Squidex.Infrastructure/RefToken.cs +++ b/backend/src/Squidex.Infrastructure/RefToken.cs @@ -8,91 +8,90 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -namespace Squidex.Infrastructure -{ - [TypeConverter(typeof(RefTokenTypeConverter))] - public sealed record RefToken - { - private static readonly char[] TrimChars = { ' ', ':' }; +namespace Squidex.Infrastructure; - public RefTokenType Type { get; } +[TypeConverter(typeof(RefTokenTypeConverter))] +public sealed record RefToken +{ + private static readonly char[] TrimChars = { ' ', ':' }; - public string Identifier { get; } + public RefTokenType Type { get; } - public bool IsClient - { - get => Type == RefTokenType.Client; - } + public string Identifier { get; } - public bool IsUser - { - get => Type == RefTokenType.Subject; - } + public bool IsClient + { + get => Type == RefTokenType.Client; + } - public RefToken(RefTokenType type, string identifier) - { - Guard.NotNullOrEmpty(identifier); + public bool IsUser + { + get => Type == RefTokenType.Subject; + } - Type = type; + public RefToken(RefTokenType type, string identifier) + { + Guard.NotNullOrEmpty(identifier); - Identifier = identifier; - } + Type = type; - public static RefToken Client(string identifier) - { - return new RefToken(RefTokenType.Client, identifier); - } + Identifier = identifier; + } - public static RefToken User(string identifier) - { - return new RefToken(RefTokenType.Subject, identifier); - } + public static RefToken Client(string identifier) + { + return new RefToken(RefTokenType.Client, identifier); + } - public override string ToString() - { - return $"{Type.ToString().ToLowerInvariant()}:{Identifier}"; - } + public static RefToken User(string identifier) + { + return new RefToken(RefTokenType.Subject, identifier); + } - public static bool TryParse(string? value, [MaybeNullWhen(false)] out RefToken result) - { - value = value?.Trim(TrimChars); + public override string ToString() + { + return $"{Type.ToString().ToLowerInvariant()}:{Identifier}"; + } - if (string.IsNullOrWhiteSpace(value)) - { - result = null!; - return false; - } + public static bool TryParse(string? value, [MaybeNullWhen(false)] out RefToken result) + { + value = value?.Trim(TrimChars); - value = value.Trim(); + if (string.IsNullOrWhiteSpace(value)) + { + result = null!; + return false; + } - var idx = value.IndexOf(':', StringComparison.Ordinal); + value = value.Trim(); - if (idx > 0 && idx < value.Length - 1) - { - if (!Enum.TryParse<RefTokenType>(value[..idx], true, out var type)) - { - type = RefTokenType.Subject; - } + var idx = value.IndexOf(':', StringComparison.Ordinal); - result = new RefToken(type, value[(idx + 1)..]); - } - else + if (idx > 0 && idx < value.Length - 1) + { + if (!Enum.TryParse<RefTokenType>(value[..idx], true, out var type)) { - result = new RefToken(RefTokenType.Subject, value); + type = RefTokenType.Subject; } - return true; + result = new RefToken(type, value[(idx + 1)..]); } - - public static RefToken Parse(string value) + else { - if (!TryParse(value, out var result)) - { - ThrowHelper.ArgumentException("Ref token cannot be null or empty.", nameof(value)); - return default!; - } + result = new RefToken(RefTokenType.Subject, value); + } - return result; + return true; + } + + public static RefToken Parse(string value) + { + if (!TryParse(value, out var result)) + { + ThrowHelper.ArgumentException("Ref token cannot be null or empty.", nameof(value)); + return default!; } + + return result; } } diff --git a/backend/src/Squidex.Infrastructure/RefTokenType.cs b/backend/src/Squidex.Infrastructure/RefTokenType.cs index 5af7878f03..f25351e2db 100644 --- a/backend/src/Squidex.Infrastructure/RefTokenType.cs +++ b/backend/src/Squidex.Infrastructure/RefTokenType.cs @@ -5,11 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public enum RefTokenType { - public enum RefTokenType - { - Subject, - Client - } + Subject, + Client } diff --git a/backend/src/Squidex.Infrastructure/RefTokenTypeConverter.cs b/backend/src/Squidex.Infrastructure/RefTokenTypeConverter.cs index 779d4fe7cd..90b6aafb9c 100644 --- a/backend/src/Squidex.Infrastructure/RefTokenTypeConverter.cs +++ b/backend/src/Squidex.Infrastructure/RefTokenTypeConverter.cs @@ -8,28 +8,27 @@ using System.ComponentModel; using System.Globalization; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public sealed class RefTokenTypeConverter : TypeConverter { - public sealed class RefTokenTypeConverter : TypeConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - { - return sourceType == typeof(string); - } + return sourceType == typeof(string); + } - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) - { - return destinationType == typeof(string); - } + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string); + } - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - return RefToken.Parse((string)value); - } + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + return RefToken.Parse((string)value); + } - public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) - { - return value?.ToString()!; - } + public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) + { + return value?.ToString()!; } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs b/backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs index df81eef370..2b605b37fc 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +public sealed class AutoAssembyTypeProvider<T> : ITypeProvider { - public sealed class AutoAssembyTypeProvider<T> : ITypeProvider + public void Map(TypeNameRegistry typeNameRegistry) { - public void Map(TypeNameRegistry typeNameRegistry) - { - typeNameRegistry.MapUnmapped(typeof(T).Assembly); - } + typeNameRegistry.MapUnmapped(typeof(T).Assembly); } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs b/backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs index 3c494189e8..ecedf67ca7 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +public interface ITypeProvider { - public interface ITypeProvider - { - void Map(TypeNameRegistry typeNameRegistry); - } + void Map(TypeNameRegistry typeNameRegistry); } diff --git a/backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs b/backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs index 63463a676c..635135dc4b 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class IgnoreEqualsAttribute : Attribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class IgnoreEqualsAttribute : Attribute - { - } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs index 3cba0886d3..6493f24958 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs @@ -7,77 +7,76 @@ using System.Reflection; -namespace Squidex.Infrastructure.Reflection.Internal +namespace Squidex.Infrastructure.Reflection.Internal; + +public sealed class PropertyAccessor { - public sealed class PropertyAccessor + private interface IPropertyAccessor { - private interface IPropertyAccessor - { - object? Get(object target); + object? Get(object target); - void Set(object target, object? value); - } + void Set(object target, object? value); + } - private sealed class PropertyWrapper<TObject, TValue> : IPropertyAccessor - { - private readonly Func<TObject, TValue> getMethod; - private readonly Action<TObject, TValue> setMethod; + private sealed class PropertyWrapper<TObject, TValue> : IPropertyAccessor + { + private readonly Func<TObject, TValue> getMethod; + private readonly Action<TObject, TValue> setMethod; - public PropertyWrapper(PropertyInfo propertyInfo) + public PropertyWrapper(PropertyInfo propertyInfo) + { + if (propertyInfo.CanRead) { - if (propertyInfo.CanRead) - { - getMethod = (Func<TObject, TValue>)propertyInfo.GetGetMethod(true)!.CreateDelegate(typeof(Func<TObject, TValue>)); - } - else - { - getMethod = x => throw new NotSupportedException(); - } - - if (propertyInfo.CanWrite) - { - setMethod = (Action<TObject, TValue>)propertyInfo.GetSetMethod(true)!.CreateDelegate(typeof(Action<TObject, TValue>)); - } - else - { - setMethod = (x, y) => throw new NotSupportedException(); - } + getMethod = (Func<TObject, TValue>)propertyInfo.GetGetMethod(true)!.CreateDelegate(typeof(Func<TObject, TValue>)); } - - public object? Get(object source) + else { - return getMethod((TObject)source); + getMethod = x => throw new NotSupportedException(); } - public void Set(object source, object? value) + if (propertyInfo.CanWrite) + { + setMethod = (Action<TObject, TValue>)propertyInfo.GetSetMethod(true)!.CreateDelegate(typeof(Action<TObject, TValue>)); + } + else { - setMethod((TObject)source, (TValue)value!); + setMethod = (x, y) => throw new NotSupportedException(); } } - private readonly IPropertyAccessor internalAccessor; + public object? Get(object source) + { + return getMethod((TObject)source); + } - public PropertyAccessor(PropertyInfo propertyInfo) + public void Set(object source, object? value) { - Guard.NotNull(propertyInfo); + setMethod((TObject)source, (TValue)value!); + } + } - var type = typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType!, propertyInfo.PropertyType); + private readonly IPropertyAccessor internalAccessor; - internalAccessor = (IPropertyAccessor)Activator.CreateInstance(type, propertyInfo)!; - } + public PropertyAccessor(PropertyInfo propertyInfo) + { + Guard.NotNull(propertyInfo); - public object? Get(object target) - { - Guard.NotNull(target); + var type = typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType!, propertyInfo.PropertyType); - return internalAccessor.Get(target); - } + internalAccessor = (IPropertyAccessor)Activator.CreateInstance(type, propertyInfo)!; + } - public void Set(object target, object? value) - { - Guard.NotNull(target); + public object? Get(object target) + { + Guard.NotNull(target); - internalAccessor.Set(target, value); - } + return internalAccessor.Get(target); + } + + public void Set(object target, object? value) + { + Guard.NotNull(target); + + internalAccessor.Set(target, value); } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/Internal/ReflectionExtensions.cs b/backend/src/Squidex.Infrastructure/Reflection/Internal/ReflectionExtensions.cs index 1432535b0a..4d42294465 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/Internal/ReflectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/Internal/ReflectionExtensions.cs @@ -7,63 +7,62 @@ using System.Reflection; -namespace Squidex.Infrastructure.Reflection.Internal +namespace Squidex.Infrastructure.Reflection.Internal; + +public static class ReflectionExtensions { - public static class ReflectionExtensions + public static PropertyInfo[] GetPublicProperties(this Type type) { - public static PropertyInfo[] GetPublicProperties(this Type type) + const BindingFlags bindingFlags = + BindingFlags.FlattenHierarchy | + BindingFlags.Public | + BindingFlags.Instance; + + if (!type.IsInterface) { - const BindingFlags bindingFlags = - BindingFlags.FlattenHierarchy | - BindingFlags.Public | - BindingFlags.Instance; + return type.GetProperties(bindingFlags); + } - if (!type.IsInterface) - { - return type.GetProperties(bindingFlags); - } + var flattenProperties = new HashSet<PropertyInfo>(); - var flattenProperties = new HashSet<PropertyInfo>(); + var considered = new List<Type> + { + type + }; - var considered = new List<Type> - { - type - }; + var queue = new Queue<Type>(); - var queue = new Queue<Type>(); + queue.Enqueue(type); - queue.Enqueue(type); + while (queue.Count > 0) + { + var subType = queue.Dequeue(); - while (queue.Count > 0) + foreach (var subInterface in subType.GetInterfaces()) { - var subType = queue.Dequeue(); - - foreach (var subInterface in subType.GetInterfaces()) + if (considered.Contains(subInterface)) { - if (considered.Contains(subInterface)) - { - continue; - } - - considered.Add(subInterface); - - queue.Enqueue(subInterface); + continue; } - var typeProperties = subType.GetProperties(bindingFlags); + considered.Add(subInterface); - foreach (var property in typeProperties) - { - flattenProperties.Add(property); - } + queue.Enqueue(subInterface); } - return flattenProperties.ToArray(); - } + var typeProperties = subType.GetProperties(bindingFlags); - public static bool Implements<T>(this Type type) - { - return type.GetInterfaces().Contains(typeof(T)); + foreach (var property in typeProperties) + { + flattenProperties.Add(property); + } } + + return flattenProperties.ToArray(); + } + + public static bool Implements<T>(this Type type) + { + return type.GetInterfaces().Contains(typeof(T)); } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs index 1c0f25082b..327db0ad1f 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs @@ -11,224 +11,223 @@ #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +public static class SimpleMapper { - public static class SimpleMapper + private sealed class StringConversionPropertyMapper : PropertyMapper { - private sealed class StringConversionPropertyMapper : PropertyMapper + public StringConversionPropertyMapper( + PropertyAccessor sourceAccessor, + PropertyAccessor targetAccessor) + : base(sourceAccessor, targetAccessor) { - public StringConversionPropertyMapper( - PropertyAccessor sourceAccessor, - PropertyAccessor targetAccessor) - : base(sourceAccessor, targetAccessor) - { - } - - public override void MapProperty(object source, object target, CultureInfo culture) - { - var value = GetValue(source); - - SetValue(target, value?.ToString()); - } } - private sealed class ConversionPropertyMapper : PropertyMapper + public override void MapProperty(object source, object target, CultureInfo culture) { - private readonly Type targetType; - - public ConversionPropertyMapper( - PropertyAccessor sourceAccessor, - PropertyAccessor targetAccessor, - Type targetType) - : base(sourceAccessor, targetAccessor) - { - this.targetType = targetType; - } - - public override void MapProperty(object source, object target, CultureInfo culture) - { - var value = GetValue(source); + var value = GetValue(source); - if (value == null) - { - return; - } + SetValue(target, value?.ToString()); + } + } - try - { - var converted = Convert.ChangeType(value, targetType, culture); + private sealed class ConversionPropertyMapper : PropertyMapper + { + private readonly Type targetType; - SetValue(target, converted); - } - catch - { - return; - } - } + public ConversionPropertyMapper( + PropertyAccessor sourceAccessor, + PropertyAccessor targetAccessor, + Type targetType) + : base(sourceAccessor, targetAccessor) + { + this.targetType = targetType; } - private sealed class TypeConverterPropertyMapper : PropertyMapper + public override void MapProperty(object source, object target, CultureInfo culture) { - private readonly TypeConverter converter; + var value = GetValue(source); - public TypeConverterPropertyMapper( - PropertyAccessor sourceAccessor, - PropertyAccessor targetAccessor, - TypeConverter converter) - : base(sourceAccessor, targetAccessor) + if (value == null) { - this.converter = converter; + return; } - public override void MapProperty(object source, object target, CultureInfo culture) + try { - var value = GetValue(source); + var converted = Convert.ChangeType(value, targetType, culture); - if (value == null) - { - return; - } + SetValue(target, converted); + } + catch + { + return; + } + } + } - try - { - var converted = converter.ConvertFrom(null, culture, value); + private sealed class TypeConverterPropertyMapper : PropertyMapper + { + private readonly TypeConverter converter; - SetValue(target, converted); - } - catch - { - return; - } - } + public TypeConverterPropertyMapper( + PropertyAccessor sourceAccessor, + PropertyAccessor targetAccessor, + TypeConverter converter) + : base(sourceAccessor, targetAccessor) + { + this.converter = converter; } - private class PropertyMapper + public override void MapProperty(object source, object target, CultureInfo culture) { - private readonly PropertyAccessor sourceAccessor; - private readonly PropertyAccessor targetAccessor; + var value = GetValue(source); - public PropertyMapper(PropertyAccessor sourceAccessor, PropertyAccessor targetAccessor) + if (value == null) { - this.sourceAccessor = sourceAccessor; - this.targetAccessor = targetAccessor; + return; } - public virtual void MapProperty(object source, object target, CultureInfo culture) + try { - var value = GetValue(source); + var converted = converter.ConvertFrom(null, culture, value); - SetValue(target, value); + SetValue(target, converted); } - - protected void SetValue(object destination, object? value) + catch { - targetAccessor.Set(destination, value); + return; } + } + } - protected object? GetValue(object source) - { - return sourceAccessor.Get(source); - } + private class PropertyMapper + { + private readonly PropertyAccessor sourceAccessor; + private readonly PropertyAccessor targetAccessor; + + public PropertyMapper(PropertyAccessor sourceAccessor, PropertyAccessor targetAccessor) + { + this.sourceAccessor = sourceAccessor; + this.targetAccessor = targetAccessor; } - private static class ClassMapper<TSource, TTarget> where TSource : class where TTarget : class + public virtual void MapProperty(object source, object target, CultureInfo culture) { - private static readonly List<PropertyMapper> Mappers = new List<PropertyMapper>(); + var value = GetValue(source); - static ClassMapper() - { - var sourceClassType = typeof(TSource); - var sourceProperties = - sourceClassType.GetPublicProperties() - .Where(x => x.CanRead).ToList(); + SetValue(target, value); + } + + protected void SetValue(object destination, object? value) + { + targetAccessor.Set(destination, value); + } + + protected object? GetValue(object source) + { + return sourceAccessor.Get(source); + } + } + + private static class ClassMapper<TSource, TTarget> where TSource : class where TTarget : class + { + private static readonly List<PropertyMapper> Mappers = new List<PropertyMapper>(); + + static ClassMapper() + { + var sourceClassType = typeof(TSource); + var sourceProperties = + sourceClassType.GetPublicProperties() + .Where(x => x.CanRead).ToList(); - var targetClassType = typeof(TTarget); - var targetProperties = - targetClassType.GetPublicProperties() - .Where(x => x.CanWrite).ToList(); + var targetClassType = typeof(TTarget); + var targetProperties = + targetClassType.GetPublicProperties() + .Where(x => x.CanWrite).ToList(); - foreach (var sourceProperty in sourceProperties) + foreach (var sourceProperty in sourceProperties) + { + var targetProperty = targetProperties.Find(x => x.Name == sourceProperty.Name); + + if (targetProperty == null) { - var targetProperty = targetProperties.Find(x => x.Name == sourceProperty.Name); + continue; + } - if (targetProperty == null) - { - continue; - } + var sourceType = sourceProperty.PropertyType; + var targetType = targetProperty.PropertyType; - var sourceType = sourceProperty.PropertyType; - var targetType = targetProperty.PropertyType; + if (sourceType == targetType) + { + Mappers.Add(new PropertyMapper( + new PropertyAccessor(sourceProperty), + new PropertyAccessor(targetProperty))); + } + else if (targetType == typeof(string)) + { + Mappers.Add(new StringConversionPropertyMapper( + new PropertyAccessor(sourceProperty), + new PropertyAccessor(targetProperty))); + } + else + { + var converter = TypeDescriptor.GetConverter(targetType); - if (sourceType == targetType) + if (converter.CanConvertFrom(sourceType)) { - Mappers.Add(new PropertyMapper( + Mappers.Add(new TypeConverterPropertyMapper( new PropertyAccessor(sourceProperty), - new PropertyAccessor(targetProperty))); + new PropertyAccessor(targetProperty), + converter)); } - else if (targetType == typeof(string)) + else if (sourceType.Implements<IConvertible>() || targetType.Implements<IConvertible>()) { - Mappers.Add(new StringConversionPropertyMapper( + Mappers.Add(new ConversionPropertyMapper( new PropertyAccessor(sourceProperty), - new PropertyAccessor(targetProperty))); - } - else - { - var converter = TypeDescriptor.GetConverter(targetType); - - if (converter.CanConvertFrom(sourceType)) - { - Mappers.Add(new TypeConverterPropertyMapper( - new PropertyAccessor(sourceProperty), - new PropertyAccessor(targetProperty), - converter)); - } - else if (sourceType.Implements<IConvertible>() || targetType.Implements<IConvertible>()) - { - Mappers.Add(new ConversionPropertyMapper( - new PropertyAccessor(sourceProperty), - new PropertyAccessor(targetProperty), - targetType)); - } + new PropertyAccessor(targetProperty), + targetType)); } } } + } - public static TTarget MapClass(TSource source, TTarget destination, CultureInfo culture) + public static TTarget MapClass(TSource source, TTarget destination, CultureInfo culture) + { + for (var i = 0; i < Mappers.Count; i++) { - for (var i = 0; i < Mappers.Count; i++) - { - var mapper = Mappers[i]; - - mapper.MapProperty(source, destination, culture); - } + var mapper = Mappers[i]; - return destination; + mapper.MapProperty(source, destination, culture); } - } - public static TTarget Map<TSource, TTarget>(TSource source) - where TSource : class - where TTarget : class, new() - { - return Map(source, new TTarget(), CultureInfo.CurrentCulture); + return destination; } + } - public static TTarget Map<TSource, TTarget>(TSource source, TTarget target) - where TSource : class - where TTarget : class - { - return Map(source, target, CultureInfo.CurrentCulture); - } + public static TTarget Map<TSource, TTarget>(TSource source) + where TSource : class + where TTarget : class, new() + { + return Map(source, new TTarget(), CultureInfo.CurrentCulture); + } - public static TTarget Map<TSource, TTarget>(TSource source, TTarget target, CultureInfo culture) - where TSource : class - where TTarget : class - { - Guard.NotNull(source); - Guard.NotNull(culture); - Guard.NotNull(target); + public static TTarget Map<TSource, TTarget>(TSource source, TTarget target) + where TSource : class + where TTarget : class + { + return Map(source, target, CultureInfo.CurrentCulture); + } - return ClassMapper<TSource, TTarget>.MapClass(source, target, culture); - } + public static TTarget Map<TSource, TTarget>(TSource source, TTarget target, CultureInfo culture) + where TSource : class + where TTarget : class + { + Guard.NotNull(source); + Guard.NotNull(culture); + Guard.NotNull(target); + + return ClassMapper<TSource, TTarget>.MapClass(source, target, culture); } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs index 06247e059d..3322b1a7fd 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/TypeNameAttribute.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class TypeNameAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public class TypeNameAttribute : Attribute - { - public string TypeName { get; } + public string TypeName { get; } - public TypeNameAttribute(string typeName) - { - TypeName = typeName; - } + public TypeNameAttribute(string typeName) + { + TypeName = typeName; } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs index 2f695470c8..09fdcfa776 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs @@ -7,28 +7,27 @@ using Squidex.Text; -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +public static class TypeNameBuilder { - public static class TypeNameBuilder + public static string TypeName(this Type type, bool camelCase, params string[] suffixes) { - public static string TypeName(this Type type, bool camelCase, params string[] suffixes) - { - var typeName = type.Name; + var typeName = type.Name; - if (suffixes != null) + if (suffixes != null) + { + foreach (var suffix in suffixes) { - foreach (var suffix in suffixes) + if (typeName.EndsWith(suffix, StringComparison.Ordinal)) { - if (typeName.EndsWith(suffix, StringComparison.Ordinal)) - { - typeName = typeName[..^suffix.Length]; + typeName = typeName[..^suffix.Length]; - break; - } + break; } } - - return camelCase ? typeName.ToCamelCase() : typeName; } + + return camelCase ? typeName.ToCamelCase() : typeName; } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs index f6648df404..49c09143e4 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs @@ -7,19 +7,18 @@ using System.Runtime.Serialization; -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +[Serializable] +public class TypeNameNotFoundException : Exception { - [Serializable] - public class TypeNameNotFoundException : Exception + public TypeNameNotFoundException(string? message = null, Exception? inner = null) + : base(message, inner) { - public TypeNameNotFoundException(string? message = null, Exception? inner = null) - : base(message, inner) - { - } + } - protected TypeNameNotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected TypeNameNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs index b4e4da245f..fc5cb20731 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs @@ -7,151 +7,150 @@ using System.Reflection; -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +public sealed class TypeNameRegistry { - public sealed class TypeNameRegistry - { - private readonly Dictionary<Type, string> namesByType = new Dictionary<Type, string>(); - private readonly Dictionary<string, Type> typesByName = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<Type, string> namesByType = new Dictionary<Type, string>(); + private readonly Dictionary<string, Type> typesByName = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase); - public TypeNameRegistry(IEnumerable<ITypeProvider>? providers = null) + public TypeNameRegistry(IEnumerable<ITypeProvider>? providers = null) + { + if (providers != null) { - if (providers != null) + foreach (var provider in providers) { - foreach (var provider in providers) - { - Map(provider); - } + Map(provider); } } + } - public TypeNameRegistry MapObsolete(Type type, string name) - { - Guard.NotNull(type); - Guard.NotNull(name); + public TypeNameRegistry MapObsolete(Type type, string name) + { + Guard.NotNull(type); + Guard.NotNull(name); - lock (namesByType) + lock (namesByType) + { + if (typesByName.TryGetValue(name, out var existingType) && existingType != type) { - if (typesByName.TryGetValue(name, out var existingType) && existingType != type) - { - var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; - - ThrowHelper.ArgumentException(message, nameof(type)); - } + var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; - typesByName[name] = type; + ThrowHelper.ArgumentException(message, nameof(type)); } - return this; + typesByName[name] = type; } - public TypeNameRegistry Map(ITypeProvider provider) - { - Guard.NotNull(provider); + return this; + } - provider.Map(this); + public TypeNameRegistry Map(ITypeProvider provider) + { + Guard.NotNull(provider); - return this; - } + provider.Map(this); - public TypeNameRegistry Map(Type type) - { - Guard.NotNull(type); + return this; + } - var typeNameAttribute = type.GetCustomAttribute<TypeNameAttribute>(); + public TypeNameRegistry Map(Type type) + { + Guard.NotNull(type); - if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) - { - Map(type, typeNameAttribute.TypeName); - } + var typeNameAttribute = type.GetCustomAttribute<TypeNameAttribute>(); - return this; + if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) + { + Map(type, typeNameAttribute.TypeName); } - public TypeNameRegistry Map(Type type, string name) - { - Guard.NotNull(type); - Guard.NotNull(name); + return this; + } - lock (namesByType) - { - if (namesByType.TryGetValue(type, out var existingName) && existingName != name) - { - var message = $"The type '{type}' is already registered with name '{namesByType[type]}'"; + public TypeNameRegistry Map(Type type, string name) + { + Guard.NotNull(type); + Guard.NotNull(name); - ThrowHelper.ArgumentException(message, nameof(type)); - } + lock (namesByType) + { + if (namesByType.TryGetValue(type, out var existingName) && existingName != name) + { + var message = $"The type '{type}' is already registered with name '{namesByType[type]}'"; - namesByType[type] = name; + ThrowHelper.ArgumentException(message, nameof(type)); + } - if (typesByName.TryGetValue(name, out var existingType) && existingType != type) - { - var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; + namesByType[type] = name; - ThrowHelper.ArgumentException(message, nameof(type)); - } + if (typesByName.TryGetValue(name, out var existingType) && existingType != type) + { + var message = $"The name '{name}' is already registered with type '{typesByName[name]}'"; - typesByName[name] = type; + ThrowHelper.ArgumentException(message, nameof(type)); } - return this; + typesByName[name] = type; } - public TypeNameRegistry MapUnmapped(Assembly assembly) + return this; + } + + public TypeNameRegistry MapUnmapped(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) { - foreach (var type in assembly.GetTypes()) + if (!namesByType.ContainsKey(type)) { - if (!namesByType.ContainsKey(type)) - { - Map(type); - } + Map(type); } - - return this; } - public string GetName<T>() - { - return GetName(typeof(T)); - } + return this; + } - public string? GetNameOrNull<T>() - { - return GetNameOrNull(typeof(T)); - } + public string GetName<T>() + { + return GetName(typeof(T)); + } - public string? GetNameOrNull(Type type) - { - return namesByType.GetValueOrDefault(type); - } + public string? GetNameOrNull<T>() + { + return GetNameOrNull(typeof(T)); + } - public Type? GetTypeOrNull(string name) - { - return typesByName.GetValueOrDefault(name); - } + public string? GetNameOrNull(Type type) + { + return namesByType.GetValueOrDefault(type); + } - public string GetName(Type type) - { - var result = namesByType.GetValueOrDefault(type); + public Type? GetTypeOrNull(string name) + { + return typesByName.GetValueOrDefault(name); + } - if (result == null) - { - throw new TypeNameNotFoundException($"There is no name for type '{type}"); - } + public string GetName(Type type) + { + var result = namesByType.GetValueOrDefault(type); - return result; + if (result == null) + { + throw new TypeNameNotFoundException($"There is no name for type '{type}"); } - public Type GetType(string name) - { - var result = typesByName.GetValueOrDefault(name); + return result; + } - if (result == null) - { - throw new TypeNameNotFoundException($"There is no type for name '{name}"); - } + public Type GetType(string name) + { + var result = typesByName.GetValueOrDefault(name); - return result; + if (result == null) + { + throw new TypeNameNotFoundException($"There is no type for name '{name}"); } + + return result; } } diff --git a/backend/src/Squidex.Infrastructure/ResultList.cs b/backend/src/Squidex.Infrastructure/ResultList.cs index 2db0258578..232401def9 100644 --- a/backend/src/Squidex.Infrastructure/ResultList.cs +++ b/backend/src/Squidex.Infrastructure/ResultList.cs @@ -7,46 +7,45 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class ResultList { - public static class ResultList + private sealed class Impl<T> : ReadonlyList<T>, IResultList<T> { - private sealed class Impl<T> : ReadonlyList<T>, IResultList<T> - { - public long Total { get; } + public long Total { get; } - public Impl(List<T> items, long total) - : base(items) - { - Total = total; - } + public Impl(List<T> items, long total) + : base(items) + { + Total = total; } + } - private static class Empties<T> - { + private static class Empties<T> + { #pragma warning disable SA1401 // Fields should be private - public static Impl<T> Instance = new Impl<T>(new List<T>(), 0); + public static Impl<T> Instance = new Impl<T>(new List<T>(), 0); #pragma warning restore SA1401 // Fields should be private - } + } - public static IResultList<T> Empty<T>() - { - return Empties<T>.Instance; - } + public static IResultList<T> Empty<T>() + { + return Empties<T>.Instance; + } - public static IResultList<T> Create<T>(long total, List<T> items) - { - return new Impl<T>(items, total); - } + public static IResultList<T> Create<T>(long total, List<T> items) + { + return new Impl<T>(items, total); + } - public static IResultList<T> Create<T>(long total, IEnumerable<T> items) - { - return new Impl<T>(items.ToList(), total); - } + public static IResultList<T> Create<T>(long total, IEnumerable<T> items) + { + return new Impl<T>(items.ToList(), total); + } - public static IResultList<T> CreateFrom<T>(long total, params T[] items) - { - return new Impl<T>(items.ToList(), total); - } + public static IResultList<T> CreateFrom<T>(long total, params T[] items) + { + return new Impl<T>(items.ToList(), total); } } diff --git a/backend/src/Squidex.Infrastructure/RetryWindow.cs b/backend/src/Squidex.Infrastructure/RetryWindow.cs index ded4f7cf39..25e1bb245b 100644 --- a/backend/src/Squidex.Infrastructure/RetryWindow.cs +++ b/backend/src/Squidex.Infrastructure/RetryWindow.cs @@ -7,40 +7,39 @@ using NodaTime; -namespace Squidex.Infrastructure -{ - public sealed class RetryWindow - { - private readonly Duration windowDuration; - private readonly int windowSize; - private readonly Queue<Instant> retries = new Queue<Instant>(); - private readonly IClock clock; +namespace Squidex.Infrastructure; - public RetryWindow(TimeSpan windowDuration, int windowSize, IClock? clock = null) - { - this.windowDuration = Duration.FromTimeSpan(windowDuration); - this.windowSize = windowSize + 1; +public sealed class RetryWindow +{ + private readonly Duration windowDuration; + private readonly int windowSize; + private readonly Queue<Instant> retries = new Queue<Instant>(); + private readonly IClock clock; - this.clock = clock ?? SystemClock.Instance; - } + public RetryWindow(TimeSpan windowDuration, int windowSize, IClock? clock = null) + { + this.windowDuration = Duration.FromTimeSpan(windowDuration); + this.windowSize = windowSize + 1; - public void Reset() - { - retries.Clear(); - } + this.clock = clock ?? SystemClock.Instance; + } - public bool CanRetryAfterFailure() - { - var now = clock.GetCurrentInstant(); + public void Reset() + { + retries.Clear(); + } - retries.Enqueue(now); + public bool CanRetryAfterFailure() + { + var now = clock.GetCurrentInstant(); - while (retries.Count > windowSize) - { - retries.Dequeue(); - } + retries.Enqueue(now); - return retries.Count < windowSize || (retries.Count > 0 && (now - retries.Peek()) > windowDuration); + while (retries.Count > windowSize) + { + retries.Dequeue(); } + + return retries.Count < windowSize || (retries.Count > 0 && (now - retries.Peek()) > windowDuration); } } diff --git a/backend/src/Squidex.Infrastructure/Security/Extensions.cs b/backend/src/Squidex.Infrastructure/Security/Extensions.cs index 0a0e6e907c..294ff75788 100644 --- a/backend/src/Squidex.Infrastructure/Security/Extensions.cs +++ b/backend/src/Squidex.Infrastructure/Security/Extensions.cs @@ -7,69 +7,68 @@ using System.Security.Claims; -namespace Squidex.Infrastructure.Security +namespace Squidex.Infrastructure.Security; + +public static class Extensions { - public static class Extensions + public static RefToken? Token(this ClaimsPrincipal principal) { - public static RefToken? Token(this ClaimsPrincipal principal) - { - var subjectId = principal.OpenIdSubject(); - var subjectName = principal.OpenIdName(); - - var clientId = principal.OpenIdClientId(); - - if (!string.IsNullOrWhiteSpace(subjectId) && !string.IsNullOrWhiteSpace(subjectName)) - { - return RefToken.User(subjectId); - } - - if (!string.IsNullOrWhiteSpace(clientId)) - { - return RefToken.Client(clientId); - } + var subjectId = principal.OpenIdSubject(); + var subjectName = principal.OpenIdName(); - return null; - } + var clientId = principal.OpenIdClientId(); - public static string? OpenIdSubject(this ClaimsPrincipal principal) + if (!string.IsNullOrWhiteSpace(subjectId) && !string.IsNullOrWhiteSpace(subjectName)) { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Subject)?.Value; + return RefToken.User(subjectId); } - public static string? OpenIdClientId(this ClaimsPrincipal principal) + if (!string.IsNullOrWhiteSpace(clientId)) { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; + return RefToken.Client(clientId); } - public static string? UserOrClientId(this ClaimsPrincipal principal) - { - return principal.OpenIdSubject() ?? principal.OpenIdClientId(); - } + return null; + } - public static string? OpenIdPreferredUserName(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value; - } + public static string? OpenIdSubject(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Subject)?.Value; + } - public static string? OpenIdName(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Name)?.Value; - } + public static string? OpenIdClientId(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; + } - public static string? OpenIdEmail(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Email)?.Value; - } + public static string? UserOrClientId(this ClaimsPrincipal principal) + { + return principal.OpenIdSubject() ?? principal.OpenIdClientId(); + } - public static string? GetEmail(this ClaimsPrincipal principal) - { - return principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value ?? - principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Email)?.Value; - } + public static string? OpenIdPreferredUserName(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value; + } - public static bool IsInClient(this ClaimsPrincipal principal, string client) - { - return principal.Claims.Any(x => x.Type == OpenIdClaims.ClientId && string.Equals(x.Value, client, StringComparison.OrdinalIgnoreCase)); - } + public static string? OpenIdName(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Name)?.Value; + } + + public static string? OpenIdEmail(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Email)?.Value; + } + + public static string? GetEmail(this ClaimsPrincipal principal) + { + return principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value ?? + principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.Email)?.Value; + } + + public static bool IsInClient(this ClaimsPrincipal principal, string client) + { + return principal.Claims.Any(x => x.Type == OpenIdClaims.ClientId && string.Equals(x.Value, client, StringComparison.OrdinalIgnoreCase)); } } diff --git a/backend/src/Squidex.Infrastructure/Security/OpenIdClaims.cs b/backend/src/Squidex.Infrastructure/Security/OpenIdClaims.cs index 8adda35550..8d7ef53abf 100644 --- a/backend/src/Squidex.Infrastructure/Security/OpenIdClaims.cs +++ b/backend/src/Squidex.Infrastructure/Security/OpenIdClaims.cs @@ -5,38 +5,37 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Security +namespace Squidex.Infrastructure.Security; + +public static class OpenIdClaims { - public static class OpenIdClaims - { - /// <summary> - /// Unique Identifier for the End-User at the Issuer. - /// </summary> - public static readonly string Subject = "sub"; + /// <summary> + /// Unique Identifier for the End-User at the Issuer. + /// </summary> + public static readonly string Subject = "sub"; - /// <summary> - /// The client id claim. - /// </summary> - public static readonly string ClientId = "client_id"; + /// <summary> + /// The client id claim. + /// </summary> + public static readonly string ClientId = "client_id"; - /// <summary> - /// End-User's full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User's locale and preferences. - /// </summary> - public static readonly string Name = "name"; + /// <summary> + /// End-User's full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User's locale and preferences. + /// </summary> + public static readonly string Name = "name"; - /// <summary> - /// Casual name of the End-User that may or may not be the same as the given_name. For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. - /// </summary> - public static readonly string NickName = "nickname"; + /// <summary> + /// Casual name of the End-User that may or may not be the same as the given_name. For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. + /// </summary> + public static readonly string NickName = "nickname"; - /// <summary> - /// Shorthand name by which the End-User wishes to be referred to. - /// </summary> - public static readonly string PreferredUserName = "preferred_username"; + /// <summary> + /// Shorthand name by which the End-User wishes to be referred to. + /// </summary> + public static readonly string PreferredUserName = "preferred_username"; - /// <summary> - /// End-User's preferred e-mail address. - /// </summary> - public static readonly string Email = "email"; - } + /// <summary> + /// End-User's preferred e-mail address. + /// </summary> + public static readonly string Email = "email"; } diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs b/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs index e39edeff23..e8fd491e8a 100644 --- a/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs +++ b/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs @@ -5,51 +5,107 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Security +namespace Squidex.Infrastructure.Security; + +public sealed partial class Permission { - public sealed partial class Permission + internal readonly struct Part { - internal readonly struct Part - { - private const char SeparatorAlternative = '|'; - private const char SeparatorMain = '.'; - private const char CharAny = '*'; - private const char CharExclude = '^'; + private const char SeparatorAlternative = '|'; + private const char SeparatorMain = '.'; + private const char CharAny = '*'; + private const char CharExclude = '^'; - public readonly ReadOnlyMemory<char>[]? Alternatives; + public readonly ReadOnlyMemory<char>[]? Alternatives; - public readonly bool Exclusion; + public readonly bool Exclusion; - public Part(ReadOnlyMemory<char>[]? alternatives, bool exclusion) - { - Alternatives = alternatives; + public Part(ReadOnlyMemory<char>[]? alternatives, bool exclusion) + { + Alternatives = alternatives; + + Exclusion = exclusion; + } - Exclusion = exclusion; + public static Part[] ParsePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return Array.Empty<Part>(); } - public static Part[] ParsePath(string path) + var current = path.AsMemory(); + var currentSpan = current.Span; + + var result = new Part[CountOf(currentSpan, SeparatorMain) + 1]; + + if (result.Length == 1) { - if (string.IsNullOrWhiteSpace(path)) + result[0] = Parse(current); + } + else + { + for (int i = 0, j = 0; i < currentSpan.Length; i++) { - return Array.Empty<Part>(); + if (currentSpan[i] == SeparatorMain) + { + result[j] = Parse(current[..i]); + + current = current[(i + 1)..]; + currentSpan = current.Span; + + i = 0; + j++; + } + else if (i == currentSpan.Length - 1 || currentSpan[i] == SeparatorMain) + { + result[j] = Parse(current); + } } + } + + return result; + } + + public static Part Parse(ReadOnlyMemory<char> current) + { + var currentSpan = current.Span; + + if (currentSpan.Length == 0) + { + return new Part(Array.Empty<ReadOnlyMemory<char>>(), false); + } + + var isExclusion = false; + + if (currentSpan[0] == CharExclude) + { + isExclusion = true; + + current = current[1..]; + currentSpan = current.Span; + } - var current = path.AsMemory(); - var currentSpan = current.Span; + if (currentSpan.Length == 0) + { + return new Part(Array.Empty<ReadOnlyMemory<char>>(), isExclusion); + } - var result = new Part[CountOf(currentSpan, SeparatorMain) + 1]; + if (current.Length > 1 || currentSpan[0] != CharAny) + { + var alternatives = new ReadOnlyMemory<char>[CountOf(currentSpan, SeparatorAlternative) + 1]; - if (result.Length == 1) + if (alternatives.Length == 1) { - result[0] = Parse(current); + alternatives[0] = current; } else { - for (int i = 0, j = 0; i < currentSpan.Length; i++) + for (int i = 0, j = 0; i < current.Length; i++) { - if (currentSpan[i] == SeparatorMain) + if (currentSpan[i] == SeparatorAlternative) { - result[j] = Parse(current[..i]); + alternatives[j] = current[..i]; current = current[(i + 1)..]; currentSpan = current.Span; @@ -57,124 +113,67 @@ public static Part[] ParsePath(string path) i = 0; j++; } - else if (i == currentSpan.Length - 1 || currentSpan[i] == SeparatorMain) + else if (i == current.Length - 1) { - result[j] = Parse(current); + alternatives[j] = current; } } } - return result; + return new Part(alternatives, isExclusion); } - - public static Part Parse(ReadOnlyMemory<char> current) + else { - var currentSpan = current.Span; - - if (currentSpan.Length == 0) - { - return new Part(Array.Empty<ReadOnlyMemory<char>>(), false); - } - - var isExclusion = false; - - if (currentSpan[0] == CharExclude) - { - isExclusion = true; - - current = current[1..]; - currentSpan = current.Span; - } - - if (currentSpan.Length == 0) - { - return new Part(Array.Empty<ReadOnlyMemory<char>>(), isExclusion); - } + return new Part(null, isExclusion); + } + } - if (current.Length > 1 || currentSpan[0] != CharAny) - { - var alternatives = new ReadOnlyMemory<char>[CountOf(currentSpan, SeparatorAlternative) + 1]; + private static int CountOf(ReadOnlySpan<char> text, char character) + { + var length = text.Length; - if (alternatives.Length == 1) - { - alternatives[0] = current; - } - else - { - for (int i = 0, j = 0; i < current.Length; i++) - { - if (currentSpan[i] == SeparatorAlternative) - { - alternatives[j] = current[..i]; - - current = current[(i + 1)..]; - currentSpan = current.Span; - - i = 0; - j++; - } - else if (i == current.Length - 1) - { - alternatives[j] = current; - } - } - } + var count = 0; - return new Part(alternatives, isExclusion); - } - else + for (var i = 0; i < length; i++) + { + if (text[i] == character) { - return new Part(null, isExclusion); + count++; } } - private static int CountOf(ReadOnlySpan<char> text, char character) - { - var length = text.Length; - - var count = 0; - - for (var i = 0; i < length; i++) - { - if (text[i] == character) - { - count++; - } - } + return count; + } - return count; + public static bool Intersects(ref Part lhs, ref Part rhs, bool allowNull) + { + if (lhs.Alternatives == null) + { + return true; } - public static bool Intersects(ref Part lhs, ref Part rhs, bool allowNull) + if (rhs.Alternatives == null) { - if (lhs.Alternatives == null) - { - return true; - } - - if (rhs.Alternatives == null) - { - return allowNull; - } + return allowNull; + } - var shouldIntersect = !(lhs.Exclusion ^ rhs.Exclusion); + var shouldIntersect = !(lhs.Exclusion ^ rhs.Exclusion); - var isIntersected = false; + var isIntersected = false; - for (var i = 0; i < lhs.Alternatives.Length; i++) + for (var i = 0; i < lhs.Alternatives.Length; i++) + { + for (var j = 0; j < rhs.Alternatives.Length; j++) { - for (var j = 0; j < rhs.Alternatives.Length; j++) + if (lhs.Alternatives[i].Span.Equals(rhs.Alternatives[j].Span, StringComparison.OrdinalIgnoreCase)) { - if (lhs.Alternatives[i].Span.Equals(rhs.Alternatives[j].Span, StringComparison.OrdinalIgnoreCase)) - { - isIntersected = true; - break; - } + isIntersected = true; + break; } } - - return isIntersected == shouldIntersect; } + + return isIntersected == shouldIntersect; } } } diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.cs b/backend/src/Squidex.Infrastructure/Security/Permission.cs index 41bb6a9d6c..daac3ad972 100644 --- a/backend/src/Squidex.Infrastructure/Security/Permission.cs +++ b/backend/src/Squidex.Infrastructure/Security/Permission.cs @@ -5,108 +5,107 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Security +namespace Squidex.Infrastructure.Security; + +public sealed partial class Permission : IComparable<Permission>, IEquatable<Permission> { - public sealed partial class Permission : IComparable<Permission>, IEquatable<Permission> + public const string Any = "*"; + public const string Exclude = "^"; + + private Part[] path; + + public string Id { get; } + + private Part[] Path { - public const string Any = "*"; - public const string Exclude = "^"; + get => path ??= Part.ParsePath(Id); + } - private Part[] path; + public Permission(string id) + { + Guard.NotNullOrEmpty(id); - public string Id { get; } + Id = id; + } - private Part[] Path + public bool Allows(Permission permission) + { + if (permission == null) { - get => path ??= Part.ParsePath(Id); + return false; } - public Permission(string id) - { - Guard.NotNullOrEmpty(id); + return Covers(Path, permission.Path); + } - Id = id; + public bool Includes(Permission permission) + { + if (permission == null) + { + return false; } - public bool Allows(Permission permission) - { - if (permission == null) - { - return false; - } + return PartialCovers(Path, permission.Path); + } - return Covers(Path, permission.Path); + private static bool Covers(Part[] given, Part[] requested) + { + if (given.Length > requested.Length) + { + return false; } - public bool Includes(Permission permission) + for (var i = 0; i < given.Length; i++) { - if (permission == null) + if (!Part.Intersects(ref given[i], ref requested[i], false)) { return false; } - - return PartialCovers(Path, permission.Path); } - private static bool Covers(Part[] given, Part[] requested) + return true; + } + + private static bool PartialCovers(Part[] given, Part[] requested) + { + for (var i = 0; i < Math.Min(given.Length, requested.Length); i++) { - if (given.Length > requested.Length) + if (!Part.Intersects(ref given[i], ref requested[i], true)) { return false; } - - for (var i = 0; i < given.Length; i++) - { - if (!Part.Intersects(ref given[i], ref requested[i], false)) - { - return false; - } - } - - return true; } - private static bool PartialCovers(Part[] given, Part[] requested) - { - for (var i = 0; i < Math.Min(given.Length, requested.Length); i++) - { - if (!Part.Intersects(ref given[i], ref requested[i], true)) - { - return false; - } - } - - return true; - } + return true; + } - public bool StartsWith(string test) - { - return Id.StartsWith(test, StringComparison.OrdinalIgnoreCase); - } + public bool StartsWith(string test) + { + return Id.StartsWith(test, StringComparison.OrdinalIgnoreCase); + } - public override bool Equals(object? obj) - { - return Equals(obj as Permission); - } + public override bool Equals(object? obj) + { + return Equals(obj as Permission); + } - public bool Equals(Permission? other) - { - return other != null && other.Id.Equals(Id, StringComparison.Ordinal); - } + public bool Equals(Permission? other) + { + return other != null && other.Id.Equals(Id, StringComparison.Ordinal); + } - public override int GetHashCode() - { - return Id.GetHashCode(StringComparison.Ordinal); - } + public override int GetHashCode() + { + return Id.GetHashCode(StringComparison.Ordinal); + } - public override string ToString() - { - return Id; - } + public override string ToString() + { + return Id; + } - public int CompareTo(Permission? other) - { - return other == null ? -1 : string.Compare(Id, other.Id, StringComparison.Ordinal); - } + public int CompareTo(Permission? other) + { + return other == null ? -1 : string.Compare(Id, other.Id, StringComparison.Ordinal); } } diff --git a/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs b/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs index 4345772775..4aa32c3524 100644 --- a/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs +++ b/backend/src/Squidex.Infrastructure/Security/PermissionSet.cs @@ -7,82 +7,81 @@ using Squidex.Infrastructure.Collections; -namespace Squidex.Infrastructure.Security +namespace Squidex.Infrastructure.Security; + +public sealed class PermissionSet : ReadonlyList<Permission> { - public sealed class PermissionSet : ReadonlyList<Permission> - { - public static readonly PermissionSet Empty = new PermissionSet(Array.Empty<string>()); + public static readonly PermissionSet Empty = new PermissionSet(Array.Empty<string>()); - private readonly Lazy<string> display; + private readonly Lazy<string> display; - public PermissionSet(params Permission[] permissions) - : this(permissions?.ToList()!) - { - } + public PermissionSet(params Permission[] permissions) + : this(permissions?.ToList()!) + { + } - public PermissionSet(params string[] permissions) - : this(permissions?.Select(x => new Permission(x)).ToList()!) - { - } + public PermissionSet(params string[] permissions) + : this(permissions?.Select(x => new Permission(x)).ToList()!) + { + } - public PermissionSet(IEnumerable<string> permissions) - : this(permissions?.Select(x => new Permission(x)).ToList()!) - { - } + public PermissionSet(IEnumerable<string> permissions) + : this(permissions?.Select(x => new Permission(x)).ToList()!) + { + } - public PermissionSet(IEnumerable<Permission> permissions) - : this(permissions?.ToList()!) - { - } + public PermissionSet(IEnumerable<Permission> permissions) + : this(permissions?.ToList()!) + { + } - public PermissionSet(IList<Permission> permissions) - : base(permissions) - { - display = new Lazy<string>(() => string.Join(";", this)); - } + public PermissionSet(IList<Permission> permissions) + : base(permissions) + { + display = new Lazy<string>(() => string.Join(";", this)); + } - public PermissionSet Add(string permission) - { - Guard.NotNullOrEmpty(permission); + public PermissionSet Add(string permission) + { + Guard.NotNullOrEmpty(permission); - return Add(new Permission(permission)); - } + return Add(new Permission(permission)); + } - public PermissionSet Add(Permission permission) - { - Guard.NotNull(permission); + public PermissionSet Add(Permission permission) + { + Guard.NotNull(permission); - return new PermissionSet(this.Union(Enumerable.Repeat(permission, 1)).Distinct()); - } + return new PermissionSet(this.Union(Enumerable.Repeat(permission, 1)).Distinct()); + } - public bool Allows(Permission? other) + public bool Allows(Permission? other) + { + if (other == null) { - if (other == null) - { - return false; - } - - return this.Any(x => x.Allows(other)); + return false; } - public bool Includes(Permission? other) - { - if (other == null) - { - return false; - } - - return this.Any(x => x.Includes(other)); - } + return this.Any(x => x.Allows(other)); + } - public override string ToString() + public bool Includes(Permission? other) + { + if (other == null) { - return display.Value; + return false; } - public IEnumerable<string> ToIds() - { - return this.Select(x => x.Id); - } + return this.Any(x => x.Includes(other)); + } + + public override string ToString() + { + return display.Value; + } + + public IEnumerable<string> ToIds() + { + return this.Select(x => x.Id); } } diff --git a/backend/src/Squidex.Infrastructure/Singletons.cs b/backend/src/Squidex.Infrastructure/Singletons.cs index a86099af94..ef1582bf40 100644 --- a/backend/src/Squidex.Infrastructure/Singletons.cs +++ b/backend/src/Squidex.Infrastructure/Singletons.cs @@ -7,15 +7,14 @@ using System.Collections.Concurrent; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class Singletons<T> { - public static class Singletons<T> - { - private static readonly ConcurrentDictionary<string, T> Instances = new ConcurrentDictionary<string, T>(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary<string, T> Instances = new ConcurrentDictionary<string, T>(StringComparer.OrdinalIgnoreCase); - public static T GetOrAdd(string key, Func<string, T> factory) - { - return Instances.GetOrAdd(key, factory); - } + public static T GetOrAdd(string key, Func<string, T> factory) + { + return Instances.GetOrAdd(key, factory); } } diff --git a/backend/src/Squidex.Infrastructure/SquidexInfrastructure.cs b/backend/src/Squidex.Infrastructure/SquidexInfrastructure.cs index f3df7b643a..b2ee242878 100644 --- a/backend/src/Squidex.Infrastructure/SquidexInfrastructure.cs +++ b/backend/src/Squidex.Infrastructure/SquidexInfrastructure.cs @@ -9,10 +9,9 @@ #pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public sealed class SquidexInfrastructure { - public sealed class SquidexInfrastructure - { - public static readonly Assembly Assembly = typeof(SquidexInfrastructure).Assembly; - } + public static readonly Assembly Assembly = typeof(SquidexInfrastructure).Assembly; } diff --git a/backend/src/Squidex.Infrastructure/States/BatchContext.cs b/backend/src/Squidex.Infrastructure/States/BatchContext.cs index b49c0c25a7..3251233028 100644 --- a/backend/src/Squidex.Infrastructure/States/BatchContext.cs +++ b/backend/src/Squidex.Infrastructure/States/BatchContext.cs @@ -9,105 +9,104 @@ #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public sealed class BatchContext<T> : IBatchContext<T> { - public sealed class BatchContext<T> : IBatchContext<T> + private static readonly List<Envelope<IEvent>> EmptyStream = new List<Envelope<IEvent>>(); + private readonly Type owner; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly IEventStreamNames eventStreamNames; + private readonly ISnapshotStore<T> snapshotStore; + private readonly Dictionary<DomainId, (long, List<Envelope<IEvent>>)> events = new Dictionary<DomainId, (long, List<Envelope<IEvent>>)>(); + private Dictionary<DomainId, SnapshotWriteJob<T>>? snapshots; + + public ISnapshotStore<T> Snapshots => snapshotStore; + + internal BatchContext( + Type owner, + IEventFormatter eventFormatter, + IEventStore eventStore, + IEventStreamNames eventStreamNames, + ISnapshotStore<T> snapshotStore) { - private static readonly List<Envelope<IEvent>> EmptyStream = new List<Envelope<IEvent>>(); - private readonly Type owner; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly IEventStreamNames eventStreamNames; - private readonly ISnapshotStore<T> snapshotStore; - private readonly Dictionary<DomainId, (long, List<Envelope<IEvent>>)> events = new Dictionary<DomainId, (long, List<Envelope<IEvent>>)>(); - private Dictionary<DomainId, SnapshotWriteJob<T>>? snapshots; - - public ISnapshotStore<T> Snapshots => snapshotStore; - - internal BatchContext( - Type owner, - IEventFormatter eventFormatter, - IEventStore eventStore, - IEventStreamNames eventStreamNames, - ISnapshotStore<T> snapshotStore) + this.owner = owner; + this.eventFormatter = eventFormatter; + this.eventStore = eventStore; + this.eventStreamNames = eventStreamNames; + this.snapshotStore = snapshotStore; + } + + internal void Add(DomainId key, T snapshot, long version) + { + snapshots ??= new (); + + if (!snapshots.TryGetValue(key, out var existing) || existing.NewVersion < version) { - this.owner = owner; - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; - this.eventStreamNames = eventStreamNames; - this.snapshotStore = snapshotStore; + snapshots[key] = new SnapshotWriteJob<T>(key, snapshot, version); } + } - internal void Add(DomainId key, T snapshot, long version) - { - snapshots ??= new (); + public async Task LoadAsync(IEnumerable<DomainId> ids) + { + var streamNames = ids.ToDictionary(x => x, x => eventStreamNames.GetStreamName(owner, x.ToString())); - if (!snapshots.TryGetValue(key, out var existing) || existing.NewVersion < version) - { - snapshots[key] = new SnapshotWriteJob<T>(key, snapshot, version); - } + if (streamNames.Count == 0) + { + return; } - public async Task LoadAsync(IEnumerable<DomainId> ids) - { - var streamNames = ids.ToDictionary(x => x, x => eventStreamNames.GetStreamName(owner, x.ToString())); + var streams = await eventStore.QueryManyAsync(streamNames.Values); - if (streamNames.Count == 0) + foreach (var (id, streamName) in streamNames) + { + if (streams.TryGetValue(streamName, out var data)) { - return; - } - - var streams = await eventStore.QueryManyAsync(streamNames.Values); + var stream = data.Select(eventFormatter.ParseIfKnown).NotNull().ToList(); - foreach (var (id, streamName) in streamNames) + events[id] = (data.Count - 1, stream); + } + else { - if (streams.TryGetValue(streamName, out var data)) - { - var stream = data.Select(eventFormatter.ParseIfKnown).NotNull().ToList(); - - events[id] = (data.Count - 1, stream); - } - else - { - events[id] = (EtagVersion.Empty, EmptyStream); - } + events[id] = (EtagVersion.Empty, EmptyStream); } } + } - public async ValueTask DisposeAsync() - { - await CommitAsync(); - } - - public Task CommitAsync() - { - var current = Interlocked.Exchange(ref snapshots, null!); + public async ValueTask DisposeAsync() + { + await CommitAsync(); + } - if (current == null || current.Count == 0) - { - return Task.CompletedTask; - } + public Task CommitAsync() + { + var current = Interlocked.Exchange(ref snapshots, null!); - return snapshotStore.WriteManyAsync(current.Values); + if (current == null || current.Count == 0) + { + return Task.CompletedTask; } - public IPersistence<T> WithEventSourcing(Type owner, DomainId key, HandleEvent? applyEvent) - { - var (version, streamEvents) = events[key]; + return snapshotStore.WriteManyAsync(current.Values); + } - return new BatchPersistence<T>(key, this, version, streamEvents, applyEvent); - } + public IPersistence<T> WithEventSourcing(Type owner, DomainId key, HandleEvent? applyEvent) + { + var (version, streamEvents) = events[key]; - public IPersistence<T> WithSnapshotsAndEventSourcing(Type owner, DomainId key, HandleSnapshot<T>? applySnapshot, HandleEvent? applyEvent) - { - var (version, streamEvents) = events[key]; + return new BatchPersistence<T>(key, this, version, streamEvents, applyEvent); + } - return new BatchPersistence<T>(key, this, version, streamEvents, applyEvent); - } + public IPersistence<T> WithSnapshotsAndEventSourcing(Type owner, DomainId key, HandleSnapshot<T>? applySnapshot, HandleEvent? applyEvent) + { + var (version, streamEvents) = events[key]; - public IPersistence<T> WithSnapshots(Type owner, DomainId key, HandleSnapshot<T>? applySnapshot) - { - throw new NotSupportedException(); - } + return new BatchPersistence<T>(key, this, version, streamEvents, applyEvent); + } + + public IPersistence<T> WithSnapshots(Type owner, DomainId key, HandleSnapshot<T>? applySnapshot) + { + throw new NotSupportedException(); } } diff --git a/backend/src/Squidex.Infrastructure/States/BatchPersistence.cs b/backend/src/Squidex.Infrastructure/States/BatchPersistence.cs index 38a9e27751..de683fd99d 100644 --- a/backend/src/Squidex.Infrastructure/States/BatchPersistence.cs +++ b/backend/src/Squidex.Infrastructure/States/BatchPersistence.cs @@ -7,77 +7,76 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +internal sealed class BatchPersistence<T> : IPersistence<T> { - internal sealed class BatchPersistence<T> : IPersistence<T> - { - private readonly DomainId ownerKey; - private readonly BatchContext<T> context; - private readonly IReadOnlyList<Envelope<IEvent>> events; - private readonly HandleEvent? applyEvent; + private readonly DomainId ownerKey; + private readonly BatchContext<T> context; + private readonly IReadOnlyList<Envelope<IEvent>> events; + private readonly HandleEvent? applyEvent; - public long Version { get; } + public long Version { get; } - public bool IsSnapshotStale => false; + public bool IsSnapshotStale => false; - internal BatchPersistence(DomainId ownerKey, BatchContext<T> context, long version, IReadOnlyList<Envelope<IEvent>> events, - HandleEvent? applyEvent) - { - this.ownerKey = ownerKey; - this.context = context; - this.events = events; - this.applyEvent = applyEvent; + internal BatchPersistence(DomainId ownerKey, BatchContext<T> context, long version, IReadOnlyList<Envelope<IEvent>> events, + HandleEvent? applyEvent) + { + this.ownerKey = ownerKey; + this.context = context; + this.events = events; + this.applyEvent = applyEvent; - Version = version; - } + Version = version; + } - public Task DeleteAsync( - CancellationToken ct = default) - { - throw new NotSupportedException(); - } + public Task DeleteAsync( + CancellationToken ct = default) + { + throw new NotSupportedException(); + } - public Task WriteEventsAsync(IReadOnlyList<Envelope<IEvent>> events, - CancellationToken ct = default) - { - throw new NotSupportedException(); - } + public Task WriteEventsAsync(IReadOnlyList<Envelope<IEvent>> events, + CancellationToken ct = default) + { + throw new NotSupportedException(); + } - public Task ReadAsync(long expectedVersion = -2, - CancellationToken ct = default) + public Task ReadAsync(long expectedVersion = -2, + CancellationToken ct = default) + { + if (applyEvent != null) { - if (applyEvent != null) + foreach (var @event in events) { - foreach (var @event in events) + if (!applyEvent(@event)) { - if (!applyEvent(@event)) - { - break; - } + break; } } + } - if (expectedVersion > EtagVersion.Any && expectedVersion != Version) + if (expectedVersion > EtagVersion.Any && expectedVersion != Version) + { + if (Version == EtagVersion.Empty) { - if (Version == EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(ownerKey.ToString()!); - } - else - { - throw new InconsistentStateException(Version, expectedVersion); - } + throw new DomainObjectNotFoundException(ownerKey.ToString()!); + } + else + { + throw new InconsistentStateException(Version, expectedVersion); } - - return Task.CompletedTask; } - public Task WriteSnapshotAsync(T state, - CancellationToken ct = default) - { - context.Add(ownerKey, state, Version); + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public Task WriteSnapshotAsync(T state, + CancellationToken ct = default) + { + context.Add(ownerKey, state, Version); + + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs b/backend/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs index 547b9ad6f4..09d88ff932 100644 --- a/backend/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs +++ b/backend/src/Squidex.Infrastructure/States/CollectionNameAttribute.cs @@ -5,16 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class CollectionNameAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class)] - public sealed class CollectionNameAttribute : Attribute - { - public string Name { get; } + public string Name { get; } - public CollectionNameAttribute(string name) - { - Name = name; - } + public CollectionNameAttribute(string name) + { + Name = name; } } diff --git a/backend/src/Squidex.Infrastructure/States/DefaultEventStreamNames.cs b/backend/src/Squidex.Infrastructure/States/DefaultEventStreamNames.cs index bb73ca2735..1b4d5d36ac 100644 --- a/backend/src/Squidex.Infrastructure/States/DefaultEventStreamNames.cs +++ b/backend/src/Squidex.Infrastructure/States/DefaultEventStreamNames.cs @@ -7,18 +7,17 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public sealed class DefaultEventStreamNames : IEventStreamNames { - public sealed class DefaultEventStreamNames : IEventStreamNames - { - private static readonly string[] Suffixes = { "Processor", "DomainObject", "State" }; + private static readonly string[] Suffixes = { "Processor", "DomainObject", "State" }; - public string GetStreamName(Type aggregateType, string id) - { - Guard.NotNullOrEmpty(id); - Guard.NotNull(aggregateType); + public string GetStreamName(Type aggregateType, string id) + { + Guard.NotNullOrEmpty(id); + Guard.NotNull(aggregateType); - return $"{aggregateType.TypeName(true, Suffixes)}-{id}"; - } + return $"{aggregateType.TypeName(true, Suffixes)}-{id}"; } } diff --git a/backend/src/Squidex.Infrastructure/States/IBatchContext.cs b/backend/src/Squidex.Infrastructure/States/IBatchContext.cs index 2048175370..bf88b56b62 100644 --- a/backend/src/Squidex.Infrastructure/States/IBatchContext.cs +++ b/backend/src/Squidex.Infrastructure/States/IBatchContext.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public interface IBatchContext<T> : IAsyncDisposable, IPersistenceFactory<T> { - public interface IBatchContext<T> : IAsyncDisposable, IPersistenceFactory<T> - { - Task CommitAsync(); + Task CommitAsync(); - Task LoadAsync(IEnumerable<DomainId> ids); - } + Task LoadAsync(IEnumerable<DomainId> ids); } diff --git a/backend/src/Squidex.Infrastructure/States/IEventStreamNames.cs b/backend/src/Squidex.Infrastructure/States/IEventStreamNames.cs index d4a2d8c62b..f04c96e558 100644 --- a/backend/src/Squidex.Infrastructure/States/IEventStreamNames.cs +++ b/backend/src/Squidex.Infrastructure/States/IEventStreamNames.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public interface IEventStreamNames { - public interface IEventStreamNames - { - string GetStreamName(Type aggregateType, string id); - } + string GetStreamName(Type aggregateType, string id); } diff --git a/backend/src/Squidex.Infrastructure/States/IOnRead.cs b/backend/src/Squidex.Infrastructure/States/IOnRead.cs index 3e3de6b5e0..69e5e14dff 100644 --- a/backend/src/Squidex.Infrastructure/States/IOnRead.cs +++ b/backend/src/Squidex.Infrastructure/States/IOnRead.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public interface IOnRead { - public interface IOnRead - { - ValueTask OnReadAsync(); - } + ValueTask OnReadAsync(); } diff --git a/backend/src/Squidex.Infrastructure/States/IPersistence.cs b/backend/src/Squidex.Infrastructure/States/IPersistence.cs index 093c7148e2..4046ce7dce 100644 --- a/backend/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/backend/src/Squidex.Infrastructure/States/IPersistence.cs @@ -7,24 +7,23 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public interface IPersistence<in TState> { - public interface IPersistence<in TState> - { - long Version { get; } + long Version { get; } - bool IsSnapshotStale { get; } + bool IsSnapshotStale { get; } - Task DeleteAsync( - CancellationToken ct = default); + Task DeleteAsync( + CancellationToken ct = default); - Task WriteEventsAsync(IReadOnlyList<Envelope<IEvent>> events, - CancellationToken ct = default); + Task WriteEventsAsync(IReadOnlyList<Envelope<IEvent>> events, + CancellationToken ct = default); - Task WriteSnapshotAsync(TState state, - CancellationToken ct = default); + Task WriteSnapshotAsync(TState state, + CancellationToken ct = default); - Task ReadAsync(long expectedVersion = EtagVersion.Any, - CancellationToken ct = default); - } + Task ReadAsync(long expectedVersion = EtagVersion.Any, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/States/IPersistenceFactory.cs b/backend/src/Squidex.Infrastructure/States/IPersistenceFactory.cs index 31bc9943b7..59874d31de 100644 --- a/backend/src/Squidex.Infrastructure/States/IPersistenceFactory.cs +++ b/backend/src/Squidex.Infrastructure/States/IPersistenceFactory.cs @@ -9,20 +9,19 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.States -{ - public delegate bool HandleEvent(Envelope<IEvent> @event); +namespace Squidex.Infrastructure.States; + +public delegate bool HandleEvent(Envelope<IEvent> @event); - public delegate void HandleSnapshot<in T>(T state, long version); +public delegate void HandleSnapshot<in T>(T state, long version); - public interface IPersistenceFactory<T> - { - ISnapshotStore<T> Snapshots { get; } +public interface IPersistenceFactory<T> +{ + ISnapshotStore<T> Snapshots { get; } - IPersistence<T> WithEventSourcing(Type owner, DomainId id, HandleEvent? applyEvent); + IPersistence<T> WithEventSourcing(Type owner, DomainId id, HandleEvent? applyEvent); - IPersistence<T> WithSnapshots(Type owner, DomainId id, HandleSnapshot<T>? applySnapshot); + IPersistence<T> WithSnapshots(Type owner, DomainId id, HandleSnapshot<T>? applySnapshot); - IPersistence<T> WithSnapshotsAndEventSourcing(Type owner, DomainId id, HandleSnapshot<T>? applySnapshot, HandleEvent? applyEvent); - } + IPersistence<T> WithSnapshotsAndEventSourcing(Type owner, DomainId id, HandleSnapshot<T>? applySnapshot, HandleEvent? applyEvent); } diff --git a/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs index 085c1f7561..68c492a1e0 100644 --- a/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -8,36 +8,35 @@ #pragma warning disable MA0048 // File name must match type name #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public interface ISnapshotStore<T> { - public interface ISnapshotStore<T> - { - Task WriteAsync(SnapshotWriteJob<T> job, - CancellationToken ct = default); + Task WriteAsync(SnapshotWriteJob<T> job, + CancellationToken ct = default); - Task WriteManyAsync(IEnumerable<SnapshotWriteJob<T>> jobs, - CancellationToken ct = default); + Task WriteManyAsync(IEnumerable<SnapshotWriteJob<T>> jobs, + CancellationToken ct = default); - Task<SnapshotResult<T>> ReadAsync(DomainId key, - CancellationToken ct = default); + Task<SnapshotResult<T>> ReadAsync(DomainId key, + CancellationToken ct = default); - Task ClearAsync( - CancellationToken ct = default); + Task ClearAsync( + CancellationToken ct = default); - Task RemoveAsync(DomainId key, - CancellationToken ct = default); + Task RemoveAsync(DomainId key, + CancellationToken ct = default); - IAsyncEnumerable<SnapshotResult<T>> ReadAllAsync( - CancellationToken ct = default); - } + IAsyncEnumerable<SnapshotResult<T>> ReadAllAsync( + CancellationToken ct = default); +} - public record struct SnapshotResult<T>(DomainId Key, T Value, long Version, bool IsValid = true); +public record struct SnapshotResult<T>(DomainId Key, T Value, long Version, bool IsValid = true); - public record struct SnapshotWriteJob<T>(DomainId Key, T Value, long NewVersion, long OldVersion = EtagVersion.Any) +public record struct SnapshotWriteJob<T>(DomainId Key, T Value, long NewVersion, long OldVersion = EtagVersion.Any) +{ + public readonly SnapshotWriteJob<TOther> As<TOther>(TOther snapshot) { - public readonly SnapshotWriteJob<TOther> As<TOther>(TOther snapshot) - { - return new SnapshotWriteJob<TOther>(Key, snapshot, NewVersion, OldVersion); - } + return new SnapshotWriteJob<TOther>(Key, snapshot, NewVersion, OldVersion); } } diff --git a/backend/src/Squidex.Infrastructure/States/IStore.cs b/backend/src/Squidex.Infrastructure/States/IStore.cs index 8926865a37..b27a08fd8b 100644 --- a/backend/src/Squidex.Infrastructure/States/IStore.cs +++ b/backend/src/Squidex.Infrastructure/States/IStore.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public interface IStore<T> : IPersistenceFactory<T> { - public interface IStore<T> : IPersistenceFactory<T> - { - IBatchContext<T> WithBatchContext(Type owner); + IBatchContext<T> WithBatchContext(Type owner); - Task ClearSnapshotsAsync(); - } + Task ClearSnapshotsAsync(); } diff --git a/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs b/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs index 4854e585d5..f1229e8b37 100644 --- a/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs +++ b/backend/src/Squidex.Infrastructure/States/InconsistentStateException.cs @@ -7,40 +7,39 @@ using System.Runtime.Serialization; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +[Serializable] +public class InconsistentStateException : Exception { - [Serializable] - public class InconsistentStateException : Exception + public long VersionCurrent { get; } + + public long VersionExpected { get; } + + public InconsistentStateException(long current, long expected, Exception? inner = null) + : base(FormatMessage(current, expected), inner) + { + VersionCurrent = current; + VersionExpected = expected; + } + + protected InconsistentStateException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + VersionCurrent = info.GetInt64(nameof(VersionCurrent)); + VersionExpected = info.GetInt64(nameof(VersionExpected)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(VersionCurrent), VersionCurrent); + info.AddValue(nameof(VersionExpected), VersionExpected); + + base.GetObjectData(info, context); + } + + private static string FormatMessage(long current, long expected) { - public long VersionCurrent { get; } - - public long VersionExpected { get; } - - public InconsistentStateException(long current, long expected, Exception? inner = null) - : base(FormatMessage(current, expected), inner) - { - VersionCurrent = current; - VersionExpected = expected; - } - - protected InconsistentStateException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - VersionCurrent = info.GetInt64(nameof(VersionCurrent)); - VersionExpected = info.GetInt64(nameof(VersionExpected)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(VersionCurrent), VersionCurrent); - info.AddValue(nameof(VersionExpected), VersionExpected); - - base.GetObjectData(info, context); - } - - private static string FormatMessage(long current, long expected) - { - return $"Requested version {expected}, but found {current}."; - } + return $"Requested version {expected}, but found {current}."; } } diff --git a/backend/src/Squidex.Infrastructure/States/NameReservation.cs b/backend/src/Squidex.Infrastructure/States/NameReservation.cs index c32ccb661e..883c1daeac 100644 --- a/backend/src/Squidex.Infrastructure/States/NameReservation.cs +++ b/backend/src/Squidex.Infrastructure/States/NameReservation.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.States -{ - public sealed record NameReservation(string Token, string Name, DomainId Id); -} +namespace Squidex.Infrastructure.States; + +public sealed record NameReservation(string Token, string Name, DomainId Id); diff --git a/backend/src/Squidex.Infrastructure/States/NameReservationState.cs b/backend/src/Squidex.Infrastructure/States/NameReservationState.cs index 48625d6974..f85063b32c 100644 --- a/backend/src/Squidex.Infrastructure/States/NameReservationState.cs +++ b/backend/src/Squidex.Infrastructure/States/NameReservationState.cs @@ -5,77 +5,76 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public sealed class NameReservationState : SimpleState<NameReservationState.State> { - public sealed class NameReservationState : SimpleState<NameReservationState.State> + [CollectionName("Names")] + public sealed class State { - [CollectionName("Names")] - public sealed class State - { - public List<NameReservation> Reservations { get; set; } = new List<NameReservation>(); - - public (bool, string?) Reserve(DomainId id, string name) - { - string? token = null; + public List<NameReservation> Reservations { get; set; } = new List<NameReservation>(); - var reservation = Reservations.Find(x => x.Name == name); - var reserved = false; - - if (reservation?.Id == id) - { - token = reservation.Token; - } - else if (reservation == null) - { - token = RandomHash.Simple(); + public (bool, string?) Reserve(DomainId id, string name) + { + string? token = null; - Reservations.Add(new NameReservation(token, name, id)); - reserved = true; - } + var reservation = Reservations.Find(x => x.Name == name); + var reserved = false; - return (reserved, token); + if (reservation?.Id == id) + { + token = reservation.Token; } - - public bool Remove(string? token) + else if (reservation == null) { - return Reservations.RemoveAll(x => x.Token == token) > 0; + token = RandomHash.Simple(); + + Reservations.Add(new NameReservation(token, name, id)); + reserved = true; } + + return (reserved, token); } - public NameReservationState(IPersistenceFactory<State> persistenceFactory, string id) - : base(persistenceFactory, typeof(NameReservationState), id) + public bool Remove(string? token) { + return Reservations.RemoveAll(x => x.Token == token) > 0; } + } + + public NameReservationState(IPersistenceFactory<State> persistenceFactory, string id) + : base(persistenceFactory, typeof(NameReservationState), id) + { + } - public NameReservationState(IPersistenceFactory<State> persistenceFactory, DomainId id) - : base(persistenceFactory, typeof(NameReservationState), id) + public NameReservationState(IPersistenceFactory<State> persistenceFactory, DomainId id) + : base(persistenceFactory, typeof(NameReservationState), id) + { + } + + public async Task<string?> ReserveAsync(DomainId id, string name, + CancellationToken ct = default) + { + try { + return await UpdateAsync(s => s.Reserve(id, name), ct: ct); } - - public async Task<string?> ReserveAsync(DomainId id, string name, - CancellationToken ct = default) + catch (InconsistentStateException) { - try - { - return await UpdateAsync(s => s.Reserve(id, name), ct: ct); - } - catch (InconsistentStateException) - { - return null; - } + return null; } + } - public async Task RemoveReservationAsync(string? token, - CancellationToken ct = default) + public async Task RemoveReservationAsync(string? token, + CancellationToken ct = default) + { + try { - try - { - await UpdateAsync(s => s.Remove(token), ct: ct); - } - catch (InconsistentStateException) - { - return; - } + await UpdateAsync(s => s.Remove(token), ct: ct); + } + catch (InconsistentStateException) + { + return; } } } diff --git a/backend/src/Squidex.Infrastructure/States/Persistence.cs b/backend/src/Squidex.Infrastructure/States/Persistence.cs index c8f13bd250..2d0ada6d30 100644 --- a/backend/src/Squidex.Infrastructure/States/Persistence.cs +++ b/backend/src/Squidex.Infrastructure/States/Persistence.cs @@ -9,244 +9,243 @@ #pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +internal sealed class Persistence<T> : IPersistence<T> { - internal sealed class Persistence<T> : IPersistence<T> + private readonly DomainId ownerKey; + private readonly HandleEvent? applyEvent; + private readonly HandleSnapshot<T>? applyState; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly ISnapshotStore<T> snapshotStore; + private readonly Lazy<string> streamName; + private readonly PersistenceMode persistenceMode; + private long versionSnapshot = EtagVersion.Empty; + private long versionEvents = EtagVersion.Empty; + private long version = EtagVersion.Empty; + + public long Version { - private readonly DomainId ownerKey; - private readonly HandleEvent? applyEvent; - private readonly HandleSnapshot<T>? applyState; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly ISnapshotStore<T> snapshotStore; - private readonly Lazy<string> streamName; - private readonly PersistenceMode persistenceMode; - private long versionSnapshot = EtagVersion.Empty; - private long versionEvents = EtagVersion.Empty; - private long version = EtagVersion.Empty; + get => version; + } - public long Version - { - get => version; - } + private bool UseSnapshots + { + get => (persistenceMode & PersistenceMode.Snapshots) == PersistenceMode.Snapshots; + } - private bool UseSnapshots - { - get => (persistenceMode & PersistenceMode.Snapshots) == PersistenceMode.Snapshots; - } + private bool UseEventSourcing + { + get => (persistenceMode & PersistenceMode.EventSourcing) == PersistenceMode.EventSourcing; + } - private bool UseEventSourcing - { - get => (persistenceMode & PersistenceMode.EventSourcing) == PersistenceMode.EventSourcing; - } + public bool IsSnapshotStale + { + get => UseSnapshots && UseEventSourcing && versionSnapshot < versionEvents; + } - public bool IsSnapshotStale - { - get => UseSnapshots && UseEventSourcing && versionSnapshot < versionEvents; - } + public Persistence(DomainId ownerKey, Type ownerType, + PersistenceMode persistenceMode, + IEventFormatter eventFormatter, + IEventStore eventStore, + IEventStreamNames eventStreamNames, + ISnapshotStore<T> snapshotStore, + HandleSnapshot<T>? applyState, + HandleEvent? applyEvent) + { + this.applyEvent = applyEvent; + this.applyState = applyState; + this.eventFormatter = eventFormatter; + this.eventStore = eventStore; + this.ownerKey = ownerKey; + this.persistenceMode = persistenceMode; + this.snapshotStore = snapshotStore; + + streamName = new Lazy<string>(() => eventStreamNames.GetStreamName(ownerType, ownerKey.ToString()!)); + } - public Persistence(DomainId ownerKey, Type ownerType, - PersistenceMode persistenceMode, - IEventFormatter eventFormatter, - IEventStore eventStore, - IEventStreamNames eventStreamNames, - ISnapshotStore<T> snapshotStore, - HandleSnapshot<T>? applyState, - HandleEvent? applyEvent) + public async Task DeleteAsync( + CancellationToken ct = default) + { + if (UseSnapshots) { - this.applyEvent = applyEvent; - this.applyState = applyState; - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; - this.ownerKey = ownerKey; - this.persistenceMode = persistenceMode; - this.snapshotStore = snapshotStore; + using (Telemetry.Activities.StartActivity("Persistence/ReadState")) + { + await snapshotStore.RemoveAsync(ownerKey, ct); + } - streamName = new Lazy<string>(() => eventStreamNames.GetStreamName(ownerType, ownerKey.ToString()!)); + versionSnapshot = EtagVersion.Empty; } - public async Task DeleteAsync( - CancellationToken ct = default) + if (UseEventSourcing) { - if (UseSnapshots) + using (Telemetry.Activities.StartActivity("Persistence/ReadEvents")) { - using (Telemetry.Activities.StartActivity("Persistence/ReadState")) - { - await snapshotStore.RemoveAsync(ownerKey, ct); - } - - versionSnapshot = EtagVersion.Empty; + await eventStore.DeleteStreamAsync(streamName.Value, ct); } - if (UseEventSourcing) - { - using (Telemetry.Activities.StartActivity("Persistence/ReadEvents")) - { - await eventStore.DeleteStreamAsync(streamName.Value, ct); - } + versionEvents = EtagVersion.Empty; + } - versionEvents = EtagVersion.Empty; - } + UpdateVersion(); + } - UpdateVersion(); + public async Task ReadAsync(long expectedVersion = EtagVersion.Any, + CancellationToken ct = default) + { + versionSnapshot = EtagVersion.Empty; + versionEvents = EtagVersion.Empty; + + if (UseSnapshots) + { + await ReadSnapshotAsync(ct); } - public async Task ReadAsync(long expectedVersion = EtagVersion.Any, - CancellationToken ct = default) + if (UseEventSourcing) { - versionSnapshot = EtagVersion.Empty; - versionEvents = EtagVersion.Empty; + await ReadEventsAsync(ct); + } - if (UseSnapshots) - { - await ReadSnapshotAsync(ct); - } + UpdateVersion(); - if (UseEventSourcing) + if (expectedVersion > EtagVersion.Any && expectedVersion != version) + { + if (version == EtagVersion.Empty) { - await ReadEventsAsync(ct); + throw new DomainObjectNotFoundException(ownerKey.ToString()!); } - - UpdateVersion(); - - if (expectedVersion > EtagVersion.Any && expectedVersion != version) + else { - if (version == EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(ownerKey.ToString()!); - } - else - { - throw new InconsistentStateException(version, expectedVersion); - } + throw new InconsistentStateException(version, expectedVersion); } } + } - private async Task ReadSnapshotAsync( - CancellationToken ct) - { - var (_, state, version, valid) = await snapshotStore.ReadAsync(ownerKey, ct); + private async Task ReadSnapshotAsync( + CancellationToken ct) + { + var (_, state, version, valid) = await snapshotStore.ReadAsync(ownerKey, ct); - version = Math.Max(version, EtagVersion.Empty); - versionSnapshot = version; + version = Math.Max(version, EtagVersion.Empty); + versionSnapshot = version; - if (valid) - { - versionEvents = version; - } + if (valid) + { + versionEvents = version; + } - if (applyState != null && version > EtagVersion.Empty && valid) - { - applyState(state, version); - } + if (applyState != null && version > EtagVersion.Empty && valid) + { + applyState(state, version); } + } - private async Task ReadEventsAsync( - CancellationToken ct) + private async Task ReadEventsAsync( + CancellationToken ct) + { + var events = await eventStore.QueryAsync(streamName.Value, versionEvents + 1, ct); + + var isStopped = false; + + foreach (var @event in events) { - var events = await eventStore.QueryAsync(streamName.Value, versionEvents + 1, ct); + var newVersion = versionEvents + 1; - var isStopped = false; + if (@event.EventStreamNumber != newVersion) + { + ThrowHelper.InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); + } - foreach (var @event in events) + if (!isStopped) { - var newVersion = versionEvents + 1; + var parsedEvent = eventFormatter.ParseIfKnown(@event); - if (@event.EventStreamNumber != newVersion) + if (applyEvent != null && parsedEvent != null) { - ThrowHelper.InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); + isStopped = !applyEvent(parsedEvent); } + } - if (!isStopped) - { - var parsedEvent = eventFormatter.ParseIfKnown(@event); + versionEvents++; + } + } - if (applyEvent != null && parsedEvent != null) - { - isStopped = !applyEvent(parsedEvent); - } - } + public async Task WriteSnapshotAsync(T state, + CancellationToken ct = default) + { + var oldVersion = versionSnapshot; - versionEvents++; - } + if (oldVersion == EtagVersion.Empty && UseEventSourcing) + { + oldVersion = versionEvents - 1; } - public async Task WriteSnapshotAsync(T state, - CancellationToken ct = default) + var newVersion = UseEventSourcing ? versionEvents : oldVersion + 1; + + if (newVersion == versionSnapshot) { - var oldVersion = versionSnapshot; + return; + } - if (oldVersion == EtagVersion.Empty && UseEventSourcing) + using (Telemetry.Activities.StartActivity("Persistence/WriteState")) + { + var job = new SnapshotWriteJob<T>(ownerKey, state, newVersion) { - oldVersion = versionEvents - 1; - } - - var newVersion = UseEventSourcing ? versionEvents : oldVersion + 1; + OldVersion = oldVersion + }; - if (newVersion == versionSnapshot) - { - return; - } + await snapshotStore.WriteAsync(job, ct); + } - using (Telemetry.Activities.StartActivity("Persistence/WriteState")) - { - var job = new SnapshotWriteJob<T>(ownerKey, state, newVersion) - { - OldVersion = oldVersion - }; + versionSnapshot = newVersion; - await snapshotStore.WriteAsync(job, ct); - } + UpdateVersion(); + } - versionSnapshot = newVersion; + public async Task WriteEventsAsync(IReadOnlyList<Envelope<IEvent>> events, + CancellationToken ct = default) + { + Guard.NotEmpty(events); - UpdateVersion(); - } + var oldVersion = EtagVersion.Any; - public async Task WriteEventsAsync(IReadOnlyList<Envelope<IEvent>> events, - CancellationToken ct = default) + if (UseEventSourcing) { - Guard.NotEmpty(events); + oldVersion = versionEvents; + } - var oldVersion = EtagVersion.Any; + var eventCommitId = Guid.NewGuid(); + var eventData = events.Select(x => eventFormatter.ToEventData(x, eventCommitId, true)).ToArray(); - if (UseEventSourcing) + try + { + using (Telemetry.Activities.StartActivity("Persistence/WriteEvents")) { - oldVersion = versionEvents; + await eventStore.AppendAsync(eventCommitId, streamName.Value, oldVersion, eventData, ct); } + } + catch (WrongEventVersionException ex) + { + throw new InconsistentStateException(ex.CurrentVersion, ex.ExpectedVersion, ex); + } - var eventCommitId = Guid.NewGuid(); - var eventData = events.Select(x => eventFormatter.ToEventData(x, eventCommitId, true)).ToArray(); - - try - { - using (Telemetry.Activities.StartActivity("Persistence/WriteEvents")) - { - await eventStore.AppendAsync(eventCommitId, streamName.Value, oldVersion, eventData, ct); - } - } - catch (WrongEventVersionException ex) - { - throw new InconsistentStateException(ex.CurrentVersion, ex.ExpectedVersion, ex); - } + versionEvents += eventData.Length; + } - versionEvents += eventData.Length; + private void UpdateVersion() + { + if (persistenceMode == PersistenceMode.Snapshots) + { + version = versionSnapshot; } - - private void UpdateVersion() + else if (persistenceMode == PersistenceMode.EventSourcing) { - if (persistenceMode == PersistenceMode.Snapshots) - { - version = versionSnapshot; - } - else if (persistenceMode == PersistenceMode.EventSourcing) - { - version = versionEvents; - } - else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) - { - version = Math.Max(versionEvents, versionSnapshot); - } + version = versionEvents; + } + else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) + { + version = Math.Max(versionEvents, versionSnapshot); } } } diff --git a/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs b/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs index 8385c79ff4..c63bd4143f 100644 --- a/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs +++ b/backend/src/Squidex.Infrastructure/States/PersistenceMode.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +[Flags] +public enum PersistenceMode { - [Flags] - public enum PersistenceMode - { - EventSourcing = 1, - Snapshots = 2, - SnapshotsAndEventSourcing = 3 - } + EventSourcing = 1, + Snapshots = 2, + SnapshotsAndEventSourcing = 3 } diff --git a/backend/src/Squidex.Infrastructure/States/SimpleState.cs b/backend/src/Squidex.Infrastructure/States/SimpleState.cs index 9781d852dd..d8eae40ccd 100644 --- a/backend/src/Squidex.Infrastructure/States/SimpleState.cs +++ b/backend/src/Squidex.Infrastructure/States/SimpleState.cs @@ -10,170 +10,169 @@ using Squidex.Infrastructure.Tasks; using Squidex.Log; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public class SimpleState<T> where T : class, new() { - public class SimpleState<T> where T : class, new() + private readonly AsyncLock? lockObject; + private readonly IPersistence<T> persistence; + private bool isLoaded; + private Instant lastWrite; + + public T Value { get; set; } = new T(); + + public long Version { - private readonly AsyncLock? lockObject; - private readonly IPersistence<T> persistence; - private bool isLoaded; - private Instant lastWrite; + get => persistence.Version; + } - public T Value { get; set; } = new T(); + public IClock Clock { get; set; } = SystemClock.Instance; - public long Version - { - get => persistence.Version; - } + public SimpleState(IPersistenceFactory<T> persistenceFactory, Type ownerType, string id, bool lockOperations = false) + : this(persistenceFactory, ownerType, DomainId.Create(id), lockOperations) + { + } - public IClock Clock { get; set; } = SystemClock.Instance; + public SimpleState(IPersistenceFactory<T> persistenceFactory, Type ownerType, DomainId id, bool lockOperations = false) + { + Guard.NotNull(persistenceFactory); - public SimpleState(IPersistenceFactory<T> persistenceFactory, Type ownerType, string id, bool lockOperations = false) - : this(persistenceFactory, ownerType, DomainId.Create(id), lockOperations) + persistence = persistenceFactory.WithSnapshots(ownerType, id, (state, version) => { - } + Value = state; + }); - public SimpleState(IPersistenceFactory<T> persistenceFactory, Type ownerType, DomainId id, bool lockOperations = false) + if (lockOperations) { - Guard.NotNull(persistenceFactory); - - persistence = persistenceFactory.WithSnapshots(ownerType, id, (state, version) => - { - Value = state; - }); - - if (lockOperations) - { - lockObject = new AsyncLock(); - } + lockObject = new AsyncLock(); } + } - public async Task LoadAsync( - CancellationToken ct = default) + public async Task LoadAsync( + CancellationToken ct = default) + { + using (await LockAsync()) { - using (await LockAsync()) - { - await LoadInternalAsync(ct); - } + await LoadInternalAsync(ct); } + } - public async Task ClearAsync( - CancellationToken ct = default) + public async Task ClearAsync( + CancellationToken ct = default) + { + using (await LockAsync()) { - using (await LockAsync()) - { - // Reset state first, in case the deletion fails. - Value = new T(); + // Reset state first, in case the deletion fails. + Value = new T(); - await persistence.DeleteAsync(ct); - } + await persistence.DeleteAsync(ct); } + } - public async Task WriteAsync(int ifNotWrittenWithinMs, - CancellationToken ct = default) + public async Task WriteAsync(int ifNotWrittenWithinMs, + CancellationToken ct = default) + { + using (await LockAsync()) { - using (await LockAsync()) - { - // Calculate the timestamp once. - var now = Clock.GetCurrentInstant(); + // Calculate the timestamp once. + var now = Clock.GetCurrentInstant(); - if (ifNotWrittenWithinMs > 0 && now.Minus(lastWrite).TotalMilliseconds < ifNotWrittenWithinMs) - { - return; - } + if (ifNotWrittenWithinMs > 0 && now.Minus(lastWrite).TotalMilliseconds < ifNotWrittenWithinMs) + { + return; + } - await persistence.WriteSnapshotAsync(Value, ct); + await persistence.WriteSnapshotAsync(Value, ct); - // Only update the last write property if it is successful. - lastWrite = now; - } + // Only update the last write property if it is successful. + lastWrite = now; } + } - public async Task WriteAsync( - CancellationToken ct = default) + public async Task WriteAsync( + CancellationToken ct = default) + { + using (await LockAsync()) { - using (await LockAsync()) - { - await persistence.WriteSnapshotAsync(Value, ct); + await persistence.WriteSnapshotAsync(Value, ct); - // Only update the last write property if it is successful. - lastWrite = Clock.GetCurrentInstant(); - } + // Only update the last write property if it is successful. + lastWrite = Clock.GetCurrentInstant(); } + } - public async Task WriteEventAsync(Envelope<IEvent> envelope, - CancellationToken ct = default) + public async Task WriteEventAsync(Envelope<IEvent> envelope, + CancellationToken ct = default) + { + using (await LockAsync()) { - using (await LockAsync()) - { - await persistence.WriteEventAsync(envelope, ct); + await persistence.WriteEventAsync(envelope, ct); - // Only update the last write property if it is successful. - lastWrite = Clock.GetCurrentInstant(); - } + // Only update the last write property if it is successful. + lastWrite = Clock.GetCurrentInstant(); } + } - public Task UpdateAsync(Func<T, bool> updater, int retries = 20, - CancellationToken ct = default) - { - return UpdateAsync(state => (updater(state), None.Value), retries, ct); - } + public Task UpdateAsync(Func<T, bool> updater, int retries = 20, + CancellationToken ct = default) + { + return UpdateAsync(state => (updater(state), None.Value), retries, ct); + } + + public async Task<TResult> UpdateAsync<TResult>(Func<T, (bool, TResult)> updater, int retries = 20, + CancellationToken ct = default) + { + Guard.GreaterEquals(retries, 1); + Guard.LessThan(retries, 100); - public async Task<TResult> UpdateAsync<TResult>(Func<T, (bool, TResult)> updater, int retries = 20, - CancellationToken ct = default) + using (await LockAsync()) { - Guard.GreaterEquals(retries, 1); - Guard.LessThan(retries, 100); + // Ensure that the state is loaded before we make the update. + if (!isLoaded) + { + await LoadInternalAsync(ct); + } - using (await LockAsync()) + for (var i = 0; i < retries; i++) { - // Ensure that the state is loaded before we make the update. - if (!isLoaded) + try { - await LoadInternalAsync(ct); - } + var (isChanged, result) = updater(Value); - for (var i = 0; i < retries; i++) - { - try + // If nothing has been changed, we can avoid the call to the database. + if (!isChanged) { - var (isChanged, result) = updater(Value); - - // If nothing has been changed, we can avoid the call to the database. - if (!isChanged) - { - return result; - } - - await WriteAsync(ct); return result; } - catch (InconsistentStateException) when (i < retries - 1) - { - await LoadInternalAsync(ct); - } - } - return default!; + await WriteAsync(ct); + return result; + } + catch (InconsistentStateException) when (i < retries - 1) + { + await LoadInternalAsync(ct); + } } + + return default!; } + } - private async Task LoadInternalAsync( - CancellationToken ct = default) - { - await persistence.ReadAsync(ct: ct); + private async Task LoadInternalAsync( + CancellationToken ct = default) + { + await persistence.ReadAsync(ct: ct); - isLoaded = true; - } + isLoaded = true; + } - private Task<IDisposable> LockAsync() + private Task<IDisposable> LockAsync() + { + if (lockObject != null) { - if (lockObject != null) - { - return lockObject.EnterAsync(); - } - - return Task.FromResult<IDisposable>(NoopDisposable.Instance); + return lockObject.EnterAsync(); } + + return Task.FromResult<IDisposable>(NoopDisposable.Instance); } } diff --git a/backend/src/Squidex.Infrastructure/States/Store.cs b/backend/src/Squidex.Infrastructure/States/Store.cs index d14c7ca8d4..bdd7a524e8 100644 --- a/backend/src/Squidex.Infrastructure/States/Store.cs +++ b/backend/src/Squidex.Infrastructure/States/Store.cs @@ -7,70 +7,69 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public sealed class Store<T> : IStore<T> { - public sealed class Store<T> : IStore<T> - { - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly IEventStreamNames eventStreamNames; - private readonly ISnapshotStore<T> snapshotStore; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly IEventStreamNames eventStreamNames; + private readonly ISnapshotStore<T> snapshotStore; - public ISnapshotStore<T> Snapshots => snapshotStore; + public ISnapshotStore<T> Snapshots => snapshotStore; - public Store( - IEventFormatter eventFormatter, - IEventStore eventStore, - IEventStreamNames eventStreamNames, - ISnapshotStore<T> snapshotStore) - { - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; - this.eventStreamNames = eventStreamNames; - this.snapshotStore = snapshotStore; - } + public Store( + IEventFormatter eventFormatter, + IEventStore eventStore, + IEventStreamNames eventStreamNames, + ISnapshotStore<T> snapshotStore) + { + this.eventFormatter = eventFormatter; + this.eventStore = eventStore; + this.eventStreamNames = eventStreamNames; + this.snapshotStore = snapshotStore; + } - public Task ClearSnapshotsAsync() - { - return snapshotStore.ClearAsync(); - } + public Task ClearSnapshotsAsync() + { + return snapshotStore.ClearAsync(); + } - public IBatchContext<T> WithBatchContext(Type owner) - { - return new BatchContext<T>(owner, - eventFormatter, - eventStore, - eventStreamNames, - snapshotStore); - } + public IBatchContext<T> WithBatchContext(Type owner) + { + return new BatchContext<T>(owner, + eventFormatter, + eventStore, + eventStreamNames, + snapshotStore); + } - public IPersistence<T> WithEventSourcing(Type owner, DomainId key, HandleEvent? applyEvent) - { - return CreatePersistence(owner, key, PersistenceMode.EventSourcing, null, applyEvent); - } + public IPersistence<T> WithEventSourcing(Type owner, DomainId key, HandleEvent? applyEvent) + { + return CreatePersistence(owner, key, PersistenceMode.EventSourcing, null, applyEvent); + } - public IPersistence<T> WithSnapshots(Type owner, DomainId key, HandleSnapshot<T>? applySnapshot) - { - return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); - } + public IPersistence<T> WithSnapshots(Type owner, DomainId key, HandleSnapshot<T>? applySnapshot) + { + return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); + } - public IPersistence<T> WithSnapshotsAndEventSourcing(Type owner, DomainId key, HandleSnapshot<T>? applySnapshot, HandleEvent? applyEvent) - { - return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); - } + public IPersistence<T> WithSnapshotsAndEventSourcing(Type owner, DomainId key, HandleSnapshot<T>? applySnapshot, HandleEvent? applyEvent) + { + return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); + } - private IPersistence<T> CreatePersistence(Type owner, DomainId key, PersistenceMode mode, HandleSnapshot<T>? applySnapshot, HandleEvent? applyEvent) - { - Guard.NotNull(key); + private IPersistence<T> CreatePersistence(Type owner, DomainId key, PersistenceMode mode, HandleSnapshot<T>? applySnapshot, HandleEvent? applyEvent) + { + Guard.NotNull(key); - return new Persistence<T>(key, owner, - mode, - eventFormatter, - eventStore, - eventStreamNames, - snapshotStore, - applySnapshot, - applyEvent); - } + return new Persistence<T>(key, owner, + mode, + eventFormatter, + eventStore, + eventStreamNames, + snapshotStore, + applySnapshot, + applyEvent); } } diff --git a/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs b/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs index cd9efa5f8c..f6866b4a38 100644 --- a/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public static class StoreExtensions { - public static class StoreExtensions + public static Task WriteEventAsync<T>(this IPersistence<T> persistence, Envelope<IEvent> @event, + CancellationToken ct = default) { - public static Task WriteEventAsync<T>(this IPersistence<T> persistence, Envelope<IEvent> @event, - CancellationToken ct = default) - { - return persistence.WriteEventsAsync(new[] { @event }, ct); - } + return persistence.WriteEventsAsync(new[] { @event }, ct); } } diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs index 3c97c6fb76..01fcd40bcc 100644 --- a/backend/src/Squidex.Infrastructure/StringExtensions.cs +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -11,129 +11,128 @@ using System.Text.Json; using System.Text.RegularExpressions; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class StringExtensions { - public static class StringExtensions + private static readonly Regex RegexEmail = new Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-||_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+([a-z]+|\d|-|\.{0,1}|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); + private static readonly Regex RegexProperty = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly JsonSerializerOptions JsonEscapeOptions = new JsonSerializerOptions { - private static readonly Regex RegexEmail = new Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-||_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+([a-z]+|\d|-|\.{0,1}|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); - private static readonly Regex RegexProperty = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - private static readonly JsonSerializerOptions JsonEscapeOptions = new JsonSerializerOptions - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; - public static string JsonEscape(this string value) - { - value = JsonSerializer.Serialize(value, JsonEscapeOptions); - value = value[1..^1]; + public static string JsonEscape(this string value) + { + value = JsonSerializer.Serialize(value, JsonEscapeOptions); + value = value[1..^1]; - return value; - } + return value; + } - public static bool IsEmail(this string? value) - { - return value != null && RegexEmail.IsMatch(value); - } + public static bool IsEmail(this string? value) + { + return value != null && RegexEmail.IsMatch(value); + } - public static bool IsPropertyName(this string? value) - { - return value != null && RegexProperty.IsMatch(value); - } + public static bool IsPropertyName(this string? value) + { + return value != null && RegexProperty.IsMatch(value); + } - public static string Or(this string? value, string fallback) - { - return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; - } + public static string Or(this string? value, string fallback) + { + return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; + } - public static string ToHexString(this byte[] data) + public static string ToHexString(this byte[] data) + { + unchecked { - unchecked - { - var hex = new char[data.Length * 2]; - - int n = 0; - for (int i = 0; i < data.Length; i++) - { - byte b = data[i]; - - byte b1 = (byte)(b >> 4); - byte b2 = (byte)(b & 0xF); + var hex = new char[data.Length * 2]; - hex[n++] = (b1 < 10) ? (char)('0' + b1) : (char)('A' + (b1 - 10)); - hex[n++] = (b2 < 10) ? (char)('0' + b2) : (char)('A' + (b2 - 10)); - } + int n = 0; + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; - return new string(hex); - } - } + byte b1 = (byte)(b >> 4); + byte b2 = (byte)(b & 0xF); - public static string JoinNonEmpty(string separator, params string?[] parts) - { - Guard.NotNull(separator); - - if (parts == null || parts.Length == 0) - { - return string.Empty; + hex[n++] = (b1 < 10) ? (char)('0' + b1) : (char)('A' + (b1 - 10)); + hex[n++] = (b2 < 10) ? (char)('0' + b2) : (char)('A' + (b2 - 10)); } - return string.Join(separator, parts.Where(x => !string.IsNullOrWhiteSpace(x))); + return new string(hex); } + } - public static string ToIso8601(this DateTime value) - { - return value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture); - } + public static string JoinNonEmpty(string separator, params string?[] parts) + { + Guard.NotNull(separator); - public static string TrimNonLetterOrDigit(this string value) + if (parts == null || parts.Length == 0) { - var span = value.AsSpan(); - - while (span.Length > 0) - { - if (char.IsLetterOrDigit(span[0])) - { - break; - } + return string.Empty; + } - span = span[1..]; - } + return string.Join(separator, parts.Where(x => !string.IsNullOrWhiteSpace(x))); + } - while (span.Length > 0) - { - if (char.IsLetterOrDigit(span[^1])) - { - break; - } + public static string ToIso8601(this DateTime value) + { + return value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture); + } - span = span[0..^1]; - } + public static string TrimNonLetterOrDigit(this string value) + { + var span = value.AsSpan(); - if (span.Length == value.Length) + while (span.Length > 0) + { + if (char.IsLetterOrDigit(span[0])) { - return value; + break; } - return new string(span); + span = span[1..]; } - public static StringBuilder AppendIfNotEmpty(this StringBuilder sb, char separator) + while (span.Length > 0) { - if (sb.Length > 0) + if (char.IsLetterOrDigit(span[^1])) { - sb.Append(separator); + break; } - return sb; + span = span[0..^1]; } - public static StringBuilder AppendIfNotEmpty(this StringBuilder sb, string separator) + if (span.Length == value.Length) { - if (sb.Length > 0) - { - sb.Append(separator); - } + return value; + } + + return new string(span); + } + + public static StringBuilder AppendIfNotEmpty(this StringBuilder sb, char separator) + { + if (sb.Length > 0) + { + sb.Append(separator); + } + + return sb; + } - return sb; + public static StringBuilder AppendIfNotEmpty(this StringBuilder sb, string separator) + { + if (sb.Length > 0) + { + sb.Append(separator); } + + return sb; } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs index 5e964a30ca..cb91af5569 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs @@ -7,97 +7,96 @@ using System.Threading.Channels; -namespace Squidex.Infrastructure.Tasks +namespace Squidex.Infrastructure.Tasks; + +public static class AsyncHelper { - public static class AsyncHelper - { - private static readonly TaskFactory TaskFactory = new - TaskFactory(CancellationToken.None, - TaskCreationOptions.None, - TaskContinuationOptions.None, - TaskScheduler.Default); + private static readonly TaskFactory TaskFactory = new + TaskFactory(CancellationToken.None, + TaskCreationOptions.None, + TaskContinuationOptions.None, + TaskScheduler.Default); - public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2) - { - await Task.WhenAll(task1, task2); + public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2) + { + await Task.WhenAll(task1, task2); #pragma warning disable MA0042 // Do not use blocking calls in an async method - return (task1.Result, task2.Result); + return (task1.Result, task2.Result); #pragma warning restore MA0042 // Do not use blocking calls in an async method - } + } - public static async Task<(T1, T2, T3)> WhenAll<T1, T2, T3>(Task<T1> task1, Task<T2> task2, Task<T3> task3) - { - await Task.WhenAll(task1, task2, task3); + public static async Task<(T1, T2, T3)> WhenAll<T1, T2, T3>(Task<T1> task1, Task<T2> task2, Task<T3> task3) + { + await Task.WhenAll(task1, task2, task3); #pragma warning disable MA0042 // Do not use blocking calls in an async method - return (task1.Result, task2.Result, task3.Result); + return (task1.Result, task2.Result, task3.Result); #pragma warning restore MA0042 // Do not use blocking calls in an async method - } + } - public static TResult Sync<TResult>(Func<Task<TResult>> func) - { - return TaskFactory - .StartNew(func).Unwrap() - .GetAwaiter() - .GetResult(); - } + public static TResult Sync<TResult>(Func<Task<TResult>> func) + { + return TaskFactory + .StartNew(func).Unwrap() + .GetAwaiter() + .GetResult(); + } - public static void Sync(Func<Task> func) - { - TaskFactory - .StartNew(func).Unwrap() - .GetAwaiter() - .GetResult(); - } - - public static void Batch<TIn>(this Channel<object> source, Channel<object> target, int batchSize, int timeout, - CancellationToken ct = default) + public static void Sync(Func<Task> func) + { + TaskFactory + .StartNew(func).Unwrap() + .GetAwaiter() + .GetResult(); + } + + public static void Batch<TIn>(this Channel<object> source, Channel<object> target, int batchSize, int timeout, + CancellationToken ct = default) + { + Task.Run(async () => { - Task.Run(async () => - { - var batch = new List<TIn>(batchSize); + var batch = new List<TIn>(batchSize); - // Just a marker object to force sending out new batches. - var force = new object(); + // Just a marker object to force sending out new batches. + var force = new object(); - await using var timer = new Timer(_ => source.Writer.TryWrite(force)); + await using var timer = new Timer(_ => source.Writer.TryWrite(force)); - async Task TrySendAsync() + async Task TrySendAsync() + { + if (batch.Count > 0) { - if (batch.Count > 0) - { - await target.Writer.WriteAsync(batch, ct); + await target.Writer.WriteAsync(batch, ct); - // Create a new batch, because the value is shared and might be processes by another concurrent task. - batch = new List<TIn>(); - } + // Create a new batch, because the value is shared and might be processes by another concurrent task. + batch = new List<TIn>(); } + } - // Exceptions usually that the process was stopped and the channel closed, therefore we do not catch them. - await foreach (var item in source.Reader.ReadAllAsync(ct)) + // Exceptions usually that the process was stopped and the channel closed, therefore we do not catch them. + await foreach (var item in source.Reader.ReadAllAsync(ct)) + { + if (ReferenceEquals(item, force)) { - if (ReferenceEquals(item, force)) - { - // Our item is the marker object from the timer. - await TrySendAsync(); - } - else if (item is TIn typed) - { - // The timeout just with the last event and should push events out if no further events are received. - timer.Change(timeout, Timeout.Infinite); + // Our item is the marker object from the timer. + await TrySendAsync(); + } + else if (item is TIn typed) + { + // The timeout just with the last event and should push events out if no further events are received. + timer.Change(timeout, Timeout.Infinite); - batch.Add(typed); + batch.Add(typed); - if (batch.Count >= batchSize) - { - await TrySendAsync(); - } + if (batch.Count >= batchSize) + { + await TrySendAsync(); } } + } - await TrySendAsync(); - }, ct).ContinueWith(x => target.Writer.TryComplete(x.Exception)); - } + await TrySendAsync(); + }, ct).ContinueWith(x => target.Writer.TryComplete(x.Exception)); } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs index 46a3149010..435dec09f3 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs @@ -5,51 +5,50 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Tasks +namespace Squidex.Infrastructure.Tasks; + +public sealed class AsyncLock : IDisposable { - public sealed class AsyncLock : IDisposable + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); + private readonly Disposable disposable; + + private sealed class Disposable : IDisposable { - private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); - private readonly Disposable disposable; + private readonly SemaphoreSlim semaphore; - private sealed class Disposable : IDisposable + public Disposable(SemaphoreSlim semaphore) { - private readonly SemaphoreSlim semaphore; - - public Disposable(SemaphoreSlim semaphore) - { - this.semaphore = semaphore; - } - - public void Dispose() - { - semaphore.Release(); - } + this.semaphore = semaphore; } - public AsyncLock() + public void Dispose() { - disposable = new Disposable(semaphore); + semaphore.Release(); } + } - public async Task<IDisposable> EnterAsync( - CancellationToken ct = default) - { - await semaphore.WaitAsync(ct); + public AsyncLock() + { + disposable = new Disposable(semaphore); + } - return disposable; - } + public async Task<IDisposable> EnterAsync( + CancellationToken ct = default) + { + await semaphore.WaitAsync(ct); - public IDisposable Enter() - { - semaphore.Wait(); + return disposable; + } - return disposable; - } + public IDisposable Enter() + { + semaphore.Wait(); - public void Dispose() - { - semaphore.Dispose(); - } + return disposable; + } + + public void Dispose() + { + semaphore.Dispose(); } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/LimitedConcurrencyLevelTaskScheduler.cs b/backend/src/Squidex.Infrastructure/Tasks/LimitedConcurrencyLevelTaskScheduler.cs index 5748ce0281..d29f8bec5a 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/LimitedConcurrencyLevelTaskScheduler.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/LimitedConcurrencyLevelTaskScheduler.cs @@ -5,127 +5,126 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Tasks +namespace Squidex.Infrastructure.Tasks; + +internal sealed class LimitedConcurrencyLevelTaskScheduler : TaskScheduler { - internal sealed class LimitedConcurrencyLevelTaskScheduler : TaskScheduler - { - [ThreadStatic] - private static bool currentThreadIsProcessingItems; - private readonly LinkedList<Task> tasks = new LinkedList<Task>(); - private readonly int maxDegreeOfParallelism; - private int delegatesQueuedOrRunning; + [ThreadStatic] + private static bool currentThreadIsProcessingItems; + private readonly LinkedList<Task> tasks = new LinkedList<Task>(); + private readonly int maxDegreeOfParallelism; + private int delegatesQueuedOrRunning; - public override int MaximumConcurrencyLevel => maxDegreeOfParallelism; + public override int MaximumConcurrencyLevel => maxDegreeOfParallelism; - public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) - { - Guard.GreaterEquals(maxDegreeOfParallelism, 1); + public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) + { + Guard.GreaterEquals(maxDegreeOfParallelism, 1); - this.maxDegreeOfParallelism = maxDegreeOfParallelism; - } + this.maxDegreeOfParallelism = maxDegreeOfParallelism; + } - protected override void QueueTask(Task task) + protected override void QueueTask(Task task) + { + lock (tasks) { - lock (tasks) - { - tasks.AddLast(task); + tasks.AddLast(task); - if (delegatesQueuedOrRunning < maxDegreeOfParallelism) - { - ++delegatesQueuedOrRunning; + if (delegatesQueuedOrRunning < maxDegreeOfParallelism) + { + ++delegatesQueuedOrRunning; - NotifyThreadPoolOfPendingWork(); - } + NotifyThreadPoolOfPendingWork(); } } + } - private void NotifyThreadPoolOfPendingWork() + private void NotifyThreadPoolOfPendingWork() + { + ThreadPool.UnsafeQueueUserWorkItem(_ => { - ThreadPool.UnsafeQueueUserWorkItem(_ => + currentThreadIsProcessingItems = true; + try { - currentThreadIsProcessingItems = true; - try + while (true) { - while (true) - { - Task task; + Task task; - lock (tasks) + lock (tasks) + { + // When there are no more items to be processed, + // note that we're done processing, and get out. + if (tasks.Count == 0) { - // When there are no more items to be processed, - // note that we're done processing, and get out. - if (tasks.Count == 0) - { - --delegatesQueuedOrRunning; - break; - } - - // Cannot be null because of previous check. - task = tasks.First!.Value; - tasks.RemoveFirst(); + --delegatesQueuedOrRunning; + break; } - TryExecuteTask(task); + // Cannot be null because of previous check. + task = tasks.First!.Value; + tasks.RemoveFirst(); } - } - finally - { - currentThreadIsProcessingItems = false; - } - }, null); - } - protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) - { - // If this thread isn't already processing a task, we don't support inlining - if (!currentThreadIsProcessingItems) - { - return false; + TryExecuteTask(task); + } } - - // If the task was previously queued, remove it from the queue - if (taskWasPreviouslyQueued) + finally { - // Try to run the next task from the tasks. - if (TryDequeue(task)) - { - return TryExecuteTask(task); - } - - return false; + currentThreadIsProcessingItems = false; } + }, null); + } - return TryExecuteTask(task); + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + // If this thread isn't already processing a task, we don't support inlining + if (!currentThreadIsProcessingItems) + { + return false; } - protected override bool TryDequeue(Task task) + // If the task was previously queued, remove it from the queue + if (taskWasPreviouslyQueued) { - lock (tasks) + // Try to run the next task from the tasks. + if (TryDequeue(task)) { - return tasks.Remove(task); + return TryExecuteTask(task); } + + return false; } - protected override IEnumerable<Task> GetScheduledTasks() + return TryExecuteTask(task); + } + + protected override bool TryDequeue(Task task) + { + lock (tasks) { - var lockTaken = false; - try - { - Monitor.TryEnter(tasks, ref lockTaken); + return tasks.Remove(task); + } + } - if (lockTaken) - { - return tasks; - } + protected override IEnumerable<Task> GetScheduledTasks() + { + var lockTaken = false; + try + { + Monitor.TryEnter(tasks, ref lockTaken); - throw new NotSupportedException(); + if (lockTaken) + { + return tasks; } - finally + + throw new NotSupportedException(); + } + finally + { + if (lockTaken) { - if (lockTaken) - { - Monitor.Exit(tasks); - } + Monitor.Exit(tasks); } } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs b/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs index e37687635f..1f70759450 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs @@ -7,110 +7,109 @@ using System.Threading.Tasks.Dataflow; -namespace Squidex.Infrastructure.Tasks +namespace Squidex.Infrastructure.Tasks; + +public sealed class PartitionedActionBlock<TInput> : ITargetBlock<TInput> { - public sealed class PartitionedActionBlock<TInput> : ITargetBlock<TInput> - { - private readonly ITargetBlock<TInput> distributor; - private readonly ActionBlock<TInput>[] workers; + private readonly ITargetBlock<TInput> distributor; + private readonly ActionBlock<TInput>[] workers; - public Task Completion - { - get => Task.WhenAll(workers.Select(x => x.Completion)); - } + public Task Completion + { + get => Task.WhenAll(workers.Select(x => x.Completion)); + } - public PartitionedActionBlock(Func<TInput, Task> action, Func<TInput, long> partitioner) - : this(action, partitioner, new ExecutionDataflowBlockOptions()) - { - } + public PartitionedActionBlock(Func<TInput, Task> action, Func<TInput, long> partitioner) + : this(action, partitioner, new ExecutionDataflowBlockOptions()) + { + } - public PartitionedActionBlock(Func<TInput, Task> action, Func<TInput, long> partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) - { - Guard.NotNull(action); - Guard.NotNull(partitioner); - Guard.NotNull(dataflowBlockOptions); - Guard.GreaterThan(dataflowBlockOptions.MaxDegreeOfParallelism, 1, nameof(dataflowBlockOptions.MaxDegreeOfParallelism)); + public PartitionedActionBlock(Func<TInput, Task> action, Func<TInput, long> partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) + { + Guard.NotNull(action); + Guard.NotNull(partitioner); + Guard.NotNull(dataflowBlockOptions); + Guard.GreaterThan(dataflowBlockOptions.MaxDegreeOfParallelism, 1, nameof(dataflowBlockOptions.MaxDegreeOfParallelism)); - workers = new ActionBlock<TInput>[dataflowBlockOptions.MaxDegreeOfParallelism]; + workers = new ActionBlock<TInput>[dataflowBlockOptions.MaxDegreeOfParallelism]; - for (var i = 0; i < dataflowBlockOptions.MaxDegreeOfParallelism; i++) - { - workers[i] = new ActionBlock<TInput>(action, new ExecutionDataflowBlockOptions - { - BoundedCapacity = dataflowBlockOptions.BoundedCapacity, - CancellationToken = dataflowBlockOptions.CancellationToken, - MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1, - TaskScheduler = dataflowBlockOptions.TaskScheduler - }); - } - - distributor = new ActionBlock<TInput>(async input => - { - try - { - var partition = Math.Abs(partitioner(input)) % workers.Length; - - await workers[partition].SendAsync(input); - } - catch (OperationCanceledException ex) - { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); - } - }, new ExecutionDataflowBlockOptions + for (var i = 0; i < dataflowBlockOptions.MaxDegreeOfParallelism; i++) + { + workers[i] = new ActionBlock<TInput>(action, new ExecutionDataflowBlockOptions { - BoundedCapacity = 1, + BoundedCapacity = dataflowBlockOptions.BoundedCapacity, + CancellationToken = dataflowBlockOptions.CancellationToken, MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1 + MaxMessagesPerTask = 1, + TaskScheduler = dataflowBlockOptions.TaskScheduler }); - - LinkCompletion(); } - public DataflowMessageStatus OfferMessage(DataflowMessageHeader messageHeader, TInput messageValue, ISourceBlock<TInput>? source, bool consumeToAccept) + distributor = new ActionBlock<TInput>(async input => { - return distributor.OfferMessage(messageHeader, messageValue, source, consumeToAccept); - } + try + { + var partition = Math.Abs(partitioner(input)) % workers.Length; - public void Complete() + await workers[partition].SendAsync(input); + } + catch (OperationCanceledException ex) + { + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); + } + }, new ExecutionDataflowBlockOptions { - distributor.Complete(); - } + BoundedCapacity = 1, + MaxDegreeOfParallelism = 1, + MaxMessagesPerTask = 1 + }); - public void Fault(Exception exception) - { - distributor.Fault(exception); - } + LinkCompletion(); + } + + public DataflowMessageStatus OfferMessage(DataflowMessageHeader messageHeader, TInput messageValue, ISourceBlock<TInput>? source, bool consumeToAccept) + { + return distributor.OfferMessage(messageHeader, messageValue, source, consumeToAccept); + } + + public void Complete() + { + distributor.Complete(); + } + + public void Fault(Exception exception) + { + distributor.Fault(exception); + } #pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void - private async void LinkCompletion() + private async void LinkCompletion() #pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void + { + try { - try - { - await distributor.Completion.ConfigureAwait(false); - } + await distributor.Completion.ConfigureAwait(false); + } #pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - catch + catch #pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body - { - // we do not want to change the stacktrace of the exception. - } + { + // we do not want to change the stacktrace of the exception. + } - if (distributor.Completion.IsFaulted && distributor.Completion.Exception != null) + if (distributor.Completion.IsFaulted && distributor.Completion.Exception != null) + { + foreach (var worker in workers) { - foreach (var worker in workers) - { - ((IDataflowBlock)worker).Fault(distributor.Completion.Exception); - } + ((IDataflowBlock)worker).Fault(distributor.Completion.Exception); } - else + } + else + { + foreach (var worker in workers) { - foreach (var worker in workers) - { - worker.Complete(); - } + worker.Complete(); } } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/ReentrantScheduler.cs b/backend/src/Squidex.Infrastructure/Tasks/ReentrantScheduler.cs index 1476bb4b72..ce2fcafe06 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/ReentrantScheduler.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/ReentrantScheduler.cs @@ -5,37 +5,36 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Tasks -{ +namespace Squidex.Infrastructure.Tasks; + #pragma warning disable MA0048 // File name must match type name - public delegate Task ReentrantSchedulerTask(CancellationToken ct); +public delegate Task ReentrantSchedulerTask(CancellationToken ct); #pragma warning restore MA0048 // File name must match type name - public sealed class ReentrantScheduler - { - private readonly TaskScheduler taskScheduler; +public sealed class ReentrantScheduler +{ + private readonly TaskScheduler taskScheduler; - public ReentrantScheduler(int maxDegreeOfParallelism) - { - taskScheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism); - } + public ReentrantScheduler(int maxDegreeOfParallelism) + { + taskScheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism); + } - public Task ScheduleAsync(ReentrantSchedulerTask action, - CancellationToken ct = default) - { - var inner = Task<Task>.Factory.StartNew(() => action(ct), ct, TaskCreationOptions.DenyChildAttach, taskScheduler); + public Task ScheduleAsync(ReentrantSchedulerTask action, + CancellationToken ct = default) + { + var inner = Task<Task>.Factory.StartNew(() => action(ct), ct, TaskCreationOptions.DenyChildAttach, taskScheduler); - return inner.Unwrap(); - } + return inner.Unwrap(); + } - public Task Schedule(Action action) + public Task Schedule(Action action) + { + return Task<bool>.Factory.StartNew(() => { - return Task<bool>.Factory.StartNew(() => - { - action(); + action(); - return false; - }, default, TaskCreationOptions.DenyChildAttach, taskScheduler); - } + return false; + }, default, TaskCreationOptions.DenyChildAttach, taskScheduler); } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs b/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs index b061b7d4ae..9067d5c2fd 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs @@ -5,120 +5,119 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Tasks -{ +namespace Squidex.Infrastructure.Tasks; + #pragma warning disable MA0048 // File name must match type name - public delegate Task SchedulerTask(CancellationToken ct); +public delegate Task SchedulerTask(CancellationToken ct); #pragma warning restore MA0048 // File name must match type name - public sealed class Scheduler - { - private readonly TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); - private readonly SemaphoreSlim semaphore; - private List<SchedulerTask>? tasks; - private int pendingTasks; +public sealed class Scheduler +{ + private readonly TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); + private readonly SemaphoreSlim semaphore; + private List<SchedulerTask>? tasks; + private int pendingTasks; - public Scheduler(int maxDegreeOfParallelism = 0) + public Scheduler(int maxDegreeOfParallelism = 0) + { + if (maxDegreeOfParallelism <= 0) { - if (maxDegreeOfParallelism <= 0) - { - maxDegreeOfParallelism = Environment.ProcessorCount * 2; - } + maxDegreeOfParallelism = Environment.ProcessorCount * 2; + } + + semaphore = new SemaphoreSlim(maxDegreeOfParallelism); + } - semaphore = new SemaphoreSlim(maxDegreeOfParallelism); + public void Schedule(SchedulerTask task) + { + if (pendingTasks < 0) + { + // Already completed. + return; } - public void Schedule(SchedulerTask task) + if (pendingTasks >= 1) { - if (pendingTasks < 0) - { - // Already completed. - return; - } + // If we already in a tasks we just queue it with the semaphore. + ScheduleTask(task, default).Forget(); + return; + } - if (pendingTasks >= 1) - { - // If we already in a tasks we just queue it with the semaphore. - ScheduleTask(task, default).Forget(); - return; - } + tasks ??= new List<SchedulerTask>(1); + tasks.Add(task); + } - tasks ??= new List<SchedulerTask>(1); - tasks.Add(task); + public async ValueTask CompleteAsync( + CancellationToken ct = default) + { + if (tasks == null || tasks.Count == 0) + { + return; } - public async ValueTask CompleteAsync( - CancellationToken ct = default) + // Use the value to indicate that the task have been started. + pendingTasks = 1; + try { - if (tasks == null || tasks.Count == 0) - { - return; - } + RunTasks(ct).AsTask().Forget(); - // Use the value to indicate that the task have been started. - pendingTasks = 1; - try - { - RunTasks(ct).AsTask().Forget(); + await tcs.Task; + } + finally + { + pendingTasks = -1; + } + } - await tcs.Task; - } - finally - { - pendingTasks = -1; - } + private async ValueTask RunTasks( + CancellationToken ct) + { + // If nothing needs to be done, we can just stop here. + if (tasks == null || tasks.Count == 0) + { + tcs.TrySetResult(true); + return; } - private async ValueTask RunTasks( - CancellationToken ct) + // Quick check to avoid the allocation of the list. + if (tasks.Count == 1) { - // If nothing needs to be done, we can just stop here. - if (tasks == null || tasks.Count == 0) - { - tcs.TrySetResult(true); - return; - } + await ScheduleTask(tasks[0], ct); + return; + } - // Quick check to avoid the allocation of the list. - if (tasks.Count == 1) - { - await ScheduleTask(tasks[0], ct); - return; - } + var runningTasks = new List<Task>(); - var runningTasks = new List<Task>(); + foreach (var validationTask in tasks) + { + runningTasks.Add(ScheduleTask(validationTask, ct)); + } - foreach (var validationTask in tasks) - { - runningTasks.Add(ScheduleTask(validationTask, ct)); - } + await Task.WhenAll(runningTasks); + } - await Task.WhenAll(runningTasks); - } + private async Task ScheduleTask(SchedulerTask task, + CancellationToken ct) + { + try + { + // Use the interlock to reduce degree of parallelization. + Interlocked.Increment(ref pendingTasks); - private async Task ScheduleTask(SchedulerTask task, - CancellationToken ct) + await semaphore.WaitAsync(ct); + await task(ct); + } + catch { - try - { - // Use the interlock to reduce degree of parallelization. - Interlocked.Increment(ref pendingTasks); + return; + } + finally + { + semaphore.Release(); - await semaphore.WaitAsync(ct); - await task(ct); - } - catch - { - return; - } - finally + if (Interlocked.Decrement(ref pendingTasks) <= 1) { - semaphore.Release(); - - if (Interlocked.Decrement(ref pendingTasks) <= 1) - { - tcs.TrySetResult(true); - } + tcs.TrySetResult(true); } } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs index ddb8a24701..87043ffdf1 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -7,75 +7,74 @@ using System.Threading.Tasks.Dataflow; -namespace Squidex.Infrastructure.Tasks +namespace Squidex.Infrastructure.Tasks; + +public static class TaskExtensions { - public static class TaskExtensions - { - private static readonly Action<Task> IgnoreTaskContinuation = t => { var ignored = t.Exception; }; + private static readonly Action<Task> IgnoreTaskContinuation = t => { var ignored = t.Exception; }; - public static void Forget(this Task task) + public static void Forget(this Task task) + { + if (task.IsCompleted) { - if (task.IsCompleted) - { #pragma warning disable IDE0059 // Unnecessary assignment of a value - var ignored = task.Exception; + var ignored = task.Exception; #pragma warning restore IDE0059 // Unnecessary assignment of a value - } - else - { - task.ContinueWith( - IgnoreTaskContinuation, - CancellationToken.None, - TaskContinuationOptions.OnlyOnFaulted | - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } } - - public static async Task<T> WithCancellation<T>(this Task<T> task, - CancellationToken cancellationToken) + else { - var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously); + task.ContinueWith( + IgnoreTaskContinuation, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted | + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } - await using (cancellationToken.Register(state => - { - ((TaskCompletionSource<object>)state!).TrySetResult(null!); - }, - tcs)) - { - var resultTask = await Task.WhenAny(task, tcs.Task); - if (resultTask == tcs.Task) - { - throw new OperationCanceledException(cancellationToken); - } + public static async Task<T> WithCancellation<T>(this Task<T> task, + CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously); - return await task; + await using (cancellationToken.Register(state => + { + ((TaskCompletionSource<object>)state!).TrySetResult(null!); + }, + tcs)) + { + var resultTask = await Task.WhenAny(task, tcs.Task); + if (resultTask == tcs.Task) + { + throw new OperationCanceledException(cancellationToken); } + + return await task; } + } #pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void - public static async void BidirectionalLinkTo<T>(this ISourceBlock<T> source, ITargetBlock<T> target) + public static async void BidirectionalLinkTo<T>(this ISourceBlock<T> source, ITargetBlock<T> target) #pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void + { + source.LinkTo(target, new DataflowLinkOptions { - source.LinkTo(target, new DataflowLinkOptions - { - PropagateCompletion = true - }); + PropagateCompletion = true + }); - try - { - await target.Completion.ConfigureAwait(false); - } - catch - { - // We do not want to change the stacktrace of the exception. - return; - } + try + { + await target.Completion.ConfigureAwait(false); + } + catch + { + // We do not want to change the stacktrace of the exception. + return; + } - if (target.Completion.IsFaulted && target.Completion.Exception != null) - { - source.Fault(target.Completion.Exception.Flatten()); - } + if (target.Completion.IsFaulted && target.Completion.Exception != null) + { + source.Fault(target.Completion.Exception.Flatten()); } } } diff --git a/backend/src/Squidex.Infrastructure/Telemetry.cs b/backend/src/Squidex.Infrastructure/Telemetry.cs index c8389022cf..ed8790885b 100644 --- a/backend/src/Squidex.Infrastructure/Telemetry.cs +++ b/backend/src/Squidex.Infrastructure/Telemetry.cs @@ -7,20 +7,19 @@ using System.Diagnostics; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class Telemetry { - public static class Telemetry - { - public static readonly ActivitySource Activities = new ActivitySource("Squidex"); + public static readonly ActivitySource Activities = new ActivitySource("Squidex"); - public static Activity? StartSubActivity(this ActivitySource activity, string name) + public static Activity? StartSubActivity(this ActivitySource activity, string name) + { + if (Activity.Current == null) { - if (Activity.Current == null) - { - return null; - } - - return activity.StartActivity(name); + return null; } + + return activity.StartActivity(name); } } diff --git a/backend/src/Squidex.Infrastructure/ThrowHelper.cs b/backend/src/Squidex.Infrastructure/ThrowHelper.cs index 5c1afad623..cdb42fa2e4 100644 --- a/backend/src/Squidex.Infrastructure/ThrowHelper.cs +++ b/backend/src/Squidex.Infrastructure/ThrowHelper.cs @@ -7,43 +7,42 @@ using Squidex.Infrastructure.Json; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public static class ThrowHelper { - public static class ThrowHelper + public static void ArgumentException(string message, string? paramName) + { + throw new ArgumentException(message, paramName); + } + + public static void ArgumentNullException(string? paramName) + { + throw new ArgumentNullException(paramName); + } + + public static void KeyNotFoundException(string? message = null) + { + throw new KeyNotFoundException(message); + } + + public static void InvalidOperationException(string? message = null) + { + throw new InvalidOperationException(message); + } + + public static void InvalidCastException(string? message = null) + { + throw new InvalidCastException(message); + } + + public static void JsonException(string? message = null, Exception? ex = null) + { + throw new JsonException(message, ex); + } + + public static void NotSupportedException(string? message = null) { - public static void ArgumentException(string message, string? paramName) - { - throw new ArgumentException(message, paramName); - } - - public static void ArgumentNullException(string? paramName) - { - throw new ArgumentNullException(paramName); - } - - public static void KeyNotFoundException(string? message = null) - { - throw new KeyNotFoundException(message); - } - - public static void InvalidOperationException(string? message = null) - { - throw new InvalidOperationException(message); - } - - public static void InvalidCastException(string? message = null) - { - throw new InvalidCastException(message); - } - - public static void JsonException(string? message = null, Exception? ex = null) - { - throw new JsonException(message, ex); - } - - public static void NotSupportedException(string? message = null) - { - throw new NotSupportedException(message); - } + throw new NotSupportedException(message); } } diff --git a/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs index 13e4dc8596..0867f7c2ac 100644 --- a/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs +++ b/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs @@ -5,103 +5,102 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Timers +namespace Squidex.Infrastructure.Timers; + +public sealed class CompletionTimer { - public sealed class CompletionTimer - { - private const int OneCallNotExecuted = 0; - private const int OneCallExecuted = 1; - private const int OneCallRequested = 2; - private readonly CancellationTokenSource stopToken = new CancellationTokenSource(); - private readonly Task runTask; - private int oneCallState; - private CancellationTokenSource? wakeupToken; + private const int OneCallNotExecuted = 0; + private const int OneCallExecuted = 1; + private const int OneCallRequested = 2; + private readonly CancellationTokenSource stopToken = new CancellationTokenSource(); + private readonly Task runTask; + private int oneCallState; + private CancellationTokenSource? wakeupToken; - public CompletionTimer(TimeSpan delay, Func<CancellationToken, Task> callback, TimeSpan initialDelay = default) - : this((int)delay.TotalMilliseconds, callback, (int)initialDelay.TotalMilliseconds) - { - } + public CompletionTimer(TimeSpan delay, Func<CancellationToken, Task> callback, TimeSpan initialDelay = default) + : this((int)delay.TotalMilliseconds, callback, (int)initialDelay.TotalMilliseconds) + { + } - public CompletionTimer(int delayInMs, Func<CancellationToken, Task> callback, int initialDelay = 0) - { - Guard.NotNull(callback); - Guard.GreaterThan(delayInMs, 0); + public CompletionTimer(int delayInMs, Func<CancellationToken, Task> callback, int initialDelay = 0) + { + Guard.NotNull(callback); + Guard.GreaterThan(delayInMs, 0); - runTask = RunInternalAsync(delayInMs, initialDelay, callback); - } + runTask = RunInternalAsync(delayInMs, initialDelay, callback); + } - public Task StopAsync() - { - stopToken.Cancel(); + public Task StopAsync() + { + stopToken.Cancel(); - return runTask; - } + return runTask; + } - public void SkipCurrentDelay() + public void SkipCurrentDelay() + { + if (!stopToken.IsCancellationRequested) { - if (!stopToken.IsCancellationRequested) - { - Interlocked.CompareExchange(ref oneCallState, OneCallRequested, OneCallNotExecuted); + Interlocked.CompareExchange(ref oneCallState, OneCallRequested, OneCallNotExecuted); - try - { - wakeupToken?.Cancel(); - } - catch (ObjectDisposedException) - { - return; - } + try + { + wakeupToken?.Cancel(); + } + catch (ObjectDisposedException) + { + return; } } + } - private async Task RunInternalAsync(int delay, int initialDelay, Func<CancellationToken, Task> callback) + private async Task RunInternalAsync(int delay, int initialDelay, Func<CancellationToken, Task> callback) + { + try { - try + if (initialDelay > 0) { - if (initialDelay > 0) - { - await WaitAsync(initialDelay).ConfigureAwait(false); - } + await WaitAsync(initialDelay).ConfigureAwait(false); + } - while (oneCallState == OneCallRequested || !stopToken.IsCancellationRequested) - { - await callback(stopToken.Token).ConfigureAwait(false); + while (oneCallState == OneCallRequested || !stopToken.IsCancellationRequested) + { + await callback(stopToken.Token).ConfigureAwait(false); - oneCallState = OneCallExecuted; + oneCallState = OneCallExecuted; - await WaitAsync(delay).ConfigureAwait(false); - } - } - catch - { - return; + await WaitAsync(delay).ConfigureAwait(false); } } + catch + { + return; + } + } - private async Task WaitAsync(int intervall) + private async Task WaitAsync(int intervall) + { + try { + wakeupToken = new CancellationTokenSource(); + try { - wakeupToken = new CancellationTokenSource(); - - try + using (wakeupToken) { - using (wakeupToken) + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, wakeupToken.Token)) { - using (var cts = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, wakeupToken.Token)) - { - await Task.Delay(intervall, cts.Token).ConfigureAwait(false); - } + await Task.Delay(intervall, cts.Token).ConfigureAwait(false); } } - finally - { - wakeupToken = null; - } } - catch (OperationCanceledException) + finally { + wakeupToken = null; } } + catch (OperationCanceledException) + { + } } } diff --git a/backend/src/Squidex.Infrastructure/Translations/ILocalizer.cs b/backend/src/Squidex.Infrastructure/Translations/ILocalizer.cs index 56554e2184..d6a4ce7c6f 100644 --- a/backend/src/Squidex.Infrastructure/Translations/ILocalizer.cs +++ b/backend/src/Squidex.Infrastructure/Translations/ILocalizer.cs @@ -7,10 +7,9 @@ using System.Globalization; -namespace Squidex.Infrastructure.Translations +namespace Squidex.Infrastructure.Translations; + +public interface ILocalizer { - public interface ILocalizer - { - (string Result, bool Found) Get(CultureInfo culture, string key, string fallback, object? args = null); - } + (string Result, bool Found) Get(CultureInfo culture, string key, string fallback, object? args = null); } diff --git a/backend/src/Squidex.Infrastructure/Translations/MissingKeys.cs b/backend/src/Squidex.Infrastructure/Translations/MissingKeys.cs index 790ab9e6c9..f43f31a8d0 100644 --- a/backend/src/Squidex.Infrastructure/Translations/MissingKeys.cs +++ b/backend/src/Squidex.Infrastructure/Translations/MissingKeys.cs @@ -5,36 +5,35 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Translations +namespace Squidex.Infrastructure.Translations; + +public sealed class MissingKeys { - public sealed class MissingKeys - { - private const string MissingFileName = "__missing.txt"; - private readonly object lockObject = new object(); - private readonly HashSet<string> missingTranslations; + private const string MissingFileName = "__missing.txt"; + private readonly object lockObject = new object(); + private readonly HashSet<string> missingTranslations; - public MissingKeys() + public MissingKeys() + { + if (File.Exists(MissingFileName)) { - if (File.Exists(MissingFileName)) - { - var missing = File.ReadAllLines(MissingFileName); + var missing = File.ReadAllLines(MissingFileName); - missingTranslations = new HashSet<string>(missing); - } - else - { - missingTranslations = new HashSet<string>(); - } + missingTranslations = new HashSet<string>(missing); + } + else + { + missingTranslations = new HashSet<string>(); } + } - public void Log(string key) + public void Log(string key) + { + lock (lockObject) { - lock (lockObject) + if (!missingTranslations.Add(key)) { - if (!missingTranslations.Add(key)) - { - File.AppendAllLines(MissingFileName, new[] { key }); - } + File.AppendAllLines(MissingFileName, new[] { key }); } } } diff --git a/backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs b/backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs index ade3f11cc8..6a33568f76 100644 --- a/backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs +++ b/backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs @@ -9,148 +9,147 @@ using System.Resources; using System.Text; -namespace Squidex.Infrastructure.Translations +namespace Squidex.Infrastructure.Translations; + +public sealed class ResourcesLocalizer : ILocalizer { - public sealed class ResourcesLocalizer : ILocalizer - { #if DEBUG - private static readonly MissingKeys MissingKeys = new MissingKeys(); + private static readonly MissingKeys MissingKeys = new MissingKeys(); #endif - private readonly ResourceManager resourceManager; + private readonly ResourceManager resourceManager; + + public ResourcesLocalizer(ResourceManager resourceManager) + { + this.resourceManager = resourceManager; + } + + public (string Result, bool Found) Get(CultureInfo culture, string key, string fallback, object? args = null) + { + Guard.NotNull(culture); + Guard.NotNullOrEmpty(key); + Guard.NotNull(fallback); + + var translation = GetCore(culture, key); - public ResourcesLocalizer(ResourceManager resourceManager) + if (translation == null) { - this.resourceManager = resourceManager; + return (fallback, false); } - public (string Result, bool Found) Get(CultureInfo culture, string key, string fallback, object? args = null) + if (args != null) { - Guard.NotNull(culture); - Guard.NotNullOrEmpty(key); - Guard.NotNull(fallback); + var argsType = args.GetType(); - var translation = GetCore(culture, key); + var sb = new StringBuilder(translation.Length); - if (translation == null) - { - return (fallback, false); - } + var span = translation.AsSpan(); - if (args != null) + while (span.Length > 0) { - var argsType = args.GetType(); - - var sb = new StringBuilder(translation.Length); - - var span = translation.AsSpan(); + var indexOfStart = span.IndexOf('{'); - while (span.Length > 0) + if (indexOfStart < 0) { - var indexOfStart = span.IndexOf('{'); - - if (indexOfStart < 0) - { - break; - } + break; + } - indexOfStart++; + indexOfStart++; - var indexOfEnd = span[indexOfStart..].IndexOf('}'); + var indexOfEnd = span[indexOfStart..].IndexOf('}'); - if (indexOfEnd < 0) - { - break; - } + if (indexOfEnd < 0) + { + break; + } - indexOfEnd += indexOfStart; + indexOfEnd += indexOfStart; - sb.Append(span[.. (indexOfStart - 1)]); + sb.Append(span[.. (indexOfStart - 1)]); - var variable = span[indexOfStart..indexOfEnd]; + var variable = span[indexOfStart..indexOfEnd]; - var shouldLower = false; - var shouldUpper = false; + var shouldLower = false; + var shouldUpper = false; - if (variable.Length > 0) + if (variable.Length > 0) + { + if (variable.EndsWith("|lower", StringComparison.OrdinalIgnoreCase)) { - if (variable.EndsWith("|lower", StringComparison.OrdinalIgnoreCase)) - { - variable = variable[..^6]; - shouldLower = true; - } + variable = variable[..^6]; + shouldLower = true; + } - if (variable.EndsWith("|upper", StringComparison.OrdinalIgnoreCase)) - { - variable = variable[..^6]; - shouldUpper = true; - } + if (variable.EndsWith("|upper", StringComparison.OrdinalIgnoreCase)) + { + variable = variable[..^6]; + shouldUpper = true; } + } - var variableName = variable.ToString(); - var variableValue = variableName; + var variableName = variable.ToString(); + var variableValue = variableName; - var property = argsType.GetProperty(variableName); + var property = argsType.GetProperty(variableName); - if (property != null) + if (property != null) + { + try { - try - { - var value = property.GetValue(args); + var value = property.GetValue(args); - if (value != null) - { - variableValue = Convert.ToString(value, culture) ?? variableName; - } - } - catch + if (value != null) { - variableValue = variableName; + variableValue = Convert.ToString(value, culture) ?? variableName; } } + catch + { + variableValue = variableName; + } + } - variableValue ??= variableName; + variableValue ??= variableName; - if (variableValue!.Length > 0) + if (variableValue!.Length > 0) + { + if (shouldLower && !char.IsLower(variableValue[0])) { - if (shouldLower && !char.IsLower(variableValue[0])) - { - sb.Append(char.ToLower(variableValue[0], CultureInfo.InvariantCulture)); + sb.Append(char.ToLower(variableValue[0], CultureInfo.InvariantCulture)); - sb.Append(variableValue.AsSpan()[1..]); - } - else if (shouldUpper && !char.IsUpper(variableValue[0])) - { - sb.Append(char.ToUpper(variableValue[0], CultureInfo.InvariantCulture)); - - sb.Append(variableValue.AsSpan()[1..]); - } - else - { - sb.Append(variableValue); - } + sb.Append(variableValue.AsSpan()[1..]); } + else if (shouldUpper && !char.IsUpper(variableValue[0])) + { + sb.Append(char.ToUpper(variableValue[0], CultureInfo.InvariantCulture)); - span = span[(indexOfEnd + 1)..]; + sb.Append(variableValue.AsSpan()[1..]); + } + else + { + sb.Append(variableValue); + } } - sb.Append(span); - - return (sb.ToString(), true); + span = span[(indexOfEnd + 1)..]; } - return (translation, true); + sb.Append(span); + + return (sb.ToString(), true); } - private string? GetCore(CultureInfo culture, string key) - { - var translation = resourceManager.GetString(key, culture); + return (translation, true); + } + + private string? GetCore(CultureInfo culture, string key) + { + var translation = resourceManager.GetString(key, culture); #if DEBUG - if (translation == null) - { - MissingKeys.Log(key); - } -#endif - return translation; + if (translation == null) + { + MissingKeys.Log(key); } +#endif + return translation; } } diff --git a/backend/src/Squidex.Infrastructure/Translations/T.cs b/backend/src/Squidex.Infrastructure/Translations/T.cs index 34283634ca..3eb6ae49ab 100644 --- a/backend/src/Squidex.Infrastructure/Translations/T.cs +++ b/backend/src/Squidex.Infrastructure/Translations/T.cs @@ -7,34 +7,33 @@ using System.Globalization; -namespace Squidex.Infrastructure.Translations +namespace Squidex.Infrastructure.Translations; + +public static class T { - public static class T + private static ILocalizer? localizer; + + public static void Setup(ILocalizer newLocalizer) { - private static ILocalizer? localizer; + localizer = newLocalizer; + } - public static void Setup(ILocalizer newLocalizer) - { - localizer = newLocalizer; - } + public static string Get(string key, object? args = null) + { + return Get(key, key, args); + } - public static string Get(string key, object? args = null) - { - return Get(key, key, args); - } + public static string Get(string key, string fallback, object? args = null) + { + Guard.NotNullOrEmpty(key); - public static string Get(string key, string fallback, object? args = null) + if (localizer == null) { - Guard.NotNullOrEmpty(key); - - if (localizer == null) - { - return key; - } + return key; + } - var (result, _) = localizer.Get(CultureInfo.CurrentUICulture, key, fallback, args); + var (result, _) = localizer.Get(CultureInfo.CurrentUICulture, key, fallback, args); - return result; - } + return result; } } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/ApiStats.cs b/backend/src/Squidex.Infrastructure/UsageTracking/ApiStats.cs index feb54f63d5..255421b809 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/ApiStats.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/ApiStats.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed record ApiStats(DateTime Date, long TotalCalls, double AverageElapsedMs, long TotalBytes); -} +namespace Squidex.Infrastructure.UsageTracking; + +public sealed record ApiStats(DateTime Date, long TotalCalls, double AverageElapsedMs, long TotalBytes); diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/ApiStatsSummary.cs b/backend/src/Squidex.Infrastructure/UsageTracking/ApiStatsSummary.cs index 3288f1245e..715fc34ca3 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/ApiStatsSummary.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/ApiStatsSummary.cs @@ -7,12 +7,11 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed record ApiStatsSummary( - double AverageElapsedMs, - long TotalCalls, - long TotalBytes, - long MonthCalls, - long MonthBytes); -} +namespace Squidex.Infrastructure.UsageTracking; + +public sealed record ApiStatsSummary( + double AverageElapsedMs, + long TotalCalls, + long TotalBytes, + long MonthCalls, + long MonthBytes); diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/ApiUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/ApiUsageTracker.cs index 9231466a7b..91f217e568 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/ApiUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/ApiUsageTracker.cs @@ -5,121 +5,120 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public sealed class ApiUsageTracker : IApiUsageTracker { - public sealed class ApiUsageTracker : IApiUsageTracker + public const string CounterTotalBytes = "TotalBytes"; + public const string CounterTotalCalls = "TotalCalls"; + public const string CounterTotalElapsedMs = "TotalElapsedMs"; + private readonly IUsageTracker usageTracker; + + public ApiUsageTracker(IUsageTracker usageTracker) { - public const string CounterTotalBytes = "TotalBytes"; - public const string CounterTotalCalls = "TotalCalls"; - public const string CounterTotalElapsedMs = "TotalElapsedMs"; - private readonly IUsageTracker usageTracker; + this.usageTracker = usageTracker; + } - public ApiUsageTracker(IUsageTracker usageTracker) - { - this.usageTracker = usageTracker; - } + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + var apiKey = GetKey(key); - public Task DeleteAsync(string key, - CancellationToken ct = default) - { - var apiKey = GetKey(key); + return usageTracker.DeleteAsync(apiKey, ct); + } - return usageTracker.DeleteAsync(apiKey, ct); - } + public async Task<long> GetMonthCallsAsync(string key, DateTime date, string? category, + CancellationToken ct = default) + { + var apiKey = GetKey(key); - public async Task<long> GetMonthCallsAsync(string key, DateTime date, string? category, - CancellationToken ct = default) - { - var apiKey = GetKey(key); + var counters = await usageTracker.GetForMonthAsync(apiKey, date, category, ct); - var counters = await usageTracker.GetForMonthAsync(apiKey, date, category, ct); + return counters.GetInt64(CounterTotalCalls); + } - return counters.GetInt64(CounterTotalCalls); - } + public async Task<long> GetMonthBytesAsync(string key, DateTime date, string? category, + CancellationToken ct = default) + { + var apiKey = GetKey(key); - public async Task<long> GetMonthBytesAsync(string key, DateTime date, string? category, - CancellationToken ct = default) - { - var apiKey = GetKey(key); + var counters = await usageTracker.GetForMonthAsync(apiKey, date, category, ct); - var counters = await usageTracker.GetForMonthAsync(apiKey, date, category, ct); + return counters.GetInt64(CounterTotalBytes); + } - return counters.GetInt64(CounterTotalBytes); - } + public Task TrackAsync(DateTime date, string key, string? category, double weight, long elapsedMs, long bytes, + CancellationToken ct = default) + { + var apiKey = GetKey(key); - public Task TrackAsync(DateTime date, string key, string? category, double weight, long elapsedMs, long bytes, - CancellationToken ct = default) + var counters = new Counters { - var apiKey = GetKey(key); + [CounterTotalCalls] = weight, + [CounterTotalElapsedMs] = elapsedMs, + [CounterTotalBytes] = bytes + }; - var counters = new Counters - { - [CounterTotalCalls] = weight, - [CounterTotalElapsedMs] = elapsedMs, - [CounterTotalBytes] = bytes - }; + return usageTracker.TrackAsync(date, apiKey, category, counters, ct); + } - return usageTracker.TrackAsync(date, apiKey, category, counters, ct); - } + public async Task<(ApiStatsSummary, Dictionary<string, List<ApiStats>> Details)> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + var apiKey = GetKey(key); - public async Task<(ApiStatsSummary, Dictionary<string, List<ApiStats>> Details)> QueryAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default) - { - var apiKey = GetKey(key); + var queries = await usageTracker.QueryAsync(apiKey, fromDate, toDate, ct); - var queries = await usageTracker.QueryAsync(apiKey, fromDate, toDate, ct); + var details = new Dictionary<string, List<ApiStats>>(); - var details = new Dictionary<string, List<ApiStats>>(); + var summaryBytes = 0L; + var summaryCalls = 0L; + var summaryElapsed = 0L; - var summaryBytes = 0L; - var summaryCalls = 0L; - var summaryElapsed = 0L; + foreach (var (category, usages) in queries) + { + var resultByCategory = new List<ApiStats>(); - foreach (var (category, usages) in queries) + foreach (var (date, counters) in usages) { - var resultByCategory = new List<ApiStats>(); + var dateBytes = counters.GetInt64(CounterTotalBytes); + var dateCalls = counters.GetInt64(CounterTotalCalls); + var dateElapsed = counters.GetInt64(CounterTotalElapsedMs); + var dateElapsedAvg = CalculateAverage(dateCalls, dateElapsed); - foreach (var (date, counters) in usages) - { - var dateBytes = counters.GetInt64(CounterTotalBytes); - var dateCalls = counters.GetInt64(CounterTotalCalls); - var dateElapsed = counters.GetInt64(CounterTotalElapsedMs); - var dateElapsedAvg = CalculateAverage(dateCalls, dateElapsed); + resultByCategory.Add(new ApiStats(date, dateCalls, dateElapsedAvg, dateBytes)); - resultByCategory.Add(new ApiStats(date, dateCalls, dateElapsedAvg, dateBytes)); - - summaryBytes += dateBytes; - summaryCalls += dateCalls; - summaryElapsed += dateElapsed; - } - - details[category] = resultByCategory; + summaryBytes += dateBytes; + summaryCalls += dateCalls; + summaryElapsed += dateElapsed; } - var summaryElapsedAvg = CalculateAverage(summaryCalls, summaryElapsed); + details[category] = resultByCategory; + } - var monthStats = await usageTracker.GetForMonthAsync(apiKey, DateTime.Today, null, ct); + var summaryElapsedAvg = CalculateAverage(summaryCalls, summaryElapsed); - var summary = new ApiStatsSummary( - summaryElapsedAvg, - summaryCalls, - summaryBytes, - monthStats.GetInt64(CounterTotalCalls), - monthStats.GetInt64(CounterTotalBytes)); + var monthStats = await usageTracker.GetForMonthAsync(apiKey, DateTime.Today, null, ct); - return (summary, details); - } + var summary = new ApiStatsSummary( + summaryElapsedAvg, + summaryCalls, + summaryBytes, + monthStats.GetInt64(CounterTotalCalls), + monthStats.GetInt64(CounterTotalBytes)); - private static double CalculateAverage(long calls, long elapsed) - { - return calls > 0 ? Math.Round((double)elapsed / calls, 2) : 0; - } + return (summary, details); + } - private static string GetKey(string key) - { - Guard.NotNullOrEmpty(key); + private static double CalculateAverage(long calls, long elapsed) + { + return calls > 0 ? Math.Round((double)elapsed / calls, 2) : 0; + } - return $"{key}_API"; - } + private static string GetKey(string key) + { + Guard.NotNullOrEmpty(key); + + return $"{key}_API"; } } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index e9b7702864..6e3fbc321d 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -9,196 +9,195 @@ using Microsoft.Extensions.Logging; using Squidex.Infrastructure.Timers; -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public sealed class BackgroundUsageTracker : DisposableObjectBase, IUsageTracker { - public sealed class BackgroundUsageTracker : DisposableObjectBase, IUsageTracker - { - private const int Intervall = 60 * 1000; - private readonly IUsageRepository usageRepository; - private readonly ILogger<BackgroundUsageTracker> log; - private readonly CompletionTimer timer; - private ConcurrentDictionary<(string Key, string Category, DateTime Date), Counters> jobs = new ConcurrentDictionary<(string Key, string Category, DateTime Date), Counters>(); + private const int Intervall = 60 * 1000; + private readonly IUsageRepository usageRepository; + private readonly ILogger<BackgroundUsageTracker> log; + private readonly CompletionTimer timer; + private ConcurrentDictionary<(string Key, string Category, DateTime Date), Counters> jobs = new ConcurrentDictionary<(string Key, string Category, DateTime Date), Counters>(); - public bool ForceWrite { get; set; } + public bool ForceWrite { get; set; } - public string FallbackCategory => "*"; + public string FallbackCategory => "*"; - public BackgroundUsageTracker(IUsageRepository usageRepository, - ILogger<BackgroundUsageTracker> log) - { - this.usageRepository = usageRepository; + public BackgroundUsageTracker(IUsageRepository usageRepository, + ILogger<BackgroundUsageTracker> log) + { + this.usageRepository = usageRepository; - this.log = log; + this.log = log; - timer = new CompletionTimer(Intervall, TrackAsync, Intervall); - } + timer = new CompletionTimer(Intervall, TrackAsync, Intervall); + } - protected override void DisposeObject(bool disposing) + protected override void DisposeObject(bool disposing) + { + if (disposing) { - if (disposing) - { - timer.StopAsync().Wait(); - } + timer.StopAsync().Wait(); } + } - public void Next() - { - ThrowIfDisposed(); + public void Next() + { + ThrowIfDisposed(); - timer.SkipCurrentDelay(); - } + timer.SkipCurrentDelay(); + } - private async Task TrackAsync( - CancellationToken ct) + private async Task TrackAsync( + CancellationToken ct) + { + try { - try + var localUsages = Interlocked.Exchange(ref jobs, new ConcurrentDictionary<(string Key, string Category, DateTime Date), Counters>()); + + if (!localUsages.IsEmpty) { - var localUsages = Interlocked.Exchange(ref jobs, new ConcurrentDictionary<(string Key, string Category, DateTime Date), Counters>()); + var updates = new UsageUpdate[localUsages.Count]; + var updateIndex = 0; - if (!localUsages.IsEmpty) + foreach (var (key, value) in localUsages) { - var updates = new UsageUpdate[localUsages.Count]; - var updateIndex = 0; - - foreach (var (key, value) in localUsages) + if (updateIndex >= updates.Length) { - if (updateIndex >= updates.Length) - { - break; - } - - updates[updateIndex].Key = key.Key; - updates[updateIndex].Category = key.Category; - updates[updateIndex].Counters = value; - updates[updateIndex].Date = key.Date; - - updateIndex++; + break; } - if (ForceWrite) - { - ct = default; - } + updates[updateIndex].Key = key.Key; + updates[updateIndex].Category = key.Category; + updates[updateIndex].Counters = value; + updates[updateIndex].Date = key.Date; - await usageRepository.TrackUsagesAsync(updates, ct); + updateIndex++; } - } - catch (Exception ex) - { - log.LogError(ex, "Failed to track usage in background."); - } - } - public Task DeleteAsync(string key, - CancellationToken ct = default) - { - Guard.NotNull(key); + if (ForceWrite) + { + ct = default; + } - return usageRepository.DeleteAsync(key, ct); + await usageRepository.TrackUsagesAsync(updates, ct); + } } - - public Task DeleteByKeyPatternAsync(string pattern, - CancellationToken ct = default) + catch (Exception ex) { - Guard.NotNull(pattern); - - return usageRepository.DeleteByKeyPatternAsync(pattern, ct); + log.LogError(ex, "Failed to track usage in background."); } + } - public Task TrackAsync(DateTime date, string key, string? category, Counters counters, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(key); - Guard.NotNull(counters); - - ThrowIfDisposed(); + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + Guard.NotNull(key); - category = GetCategory(category); + return usageRepository.DeleteAsync(key, ct); + } -#pragma warning disable MA0105 // Use the lambda parameters instead of using a closure - jobs.AddOrUpdate((key, category, date), counters, (k, p) => p.SumUp(counters)); -#pragma warning restore MA0105 // Use the lambda parameters instead of using a closure + public Task DeleteByKeyPatternAsync(string pattern, + CancellationToken ct = default) + { + Guard.NotNull(pattern); - return Task.CompletedTask; - } + return usageRepository.DeleteByKeyPatternAsync(pattern, ct); + } - public async Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(key); + public Task TrackAsync(DateTime date, string key, string? category, Counters counters, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(key); + Guard.NotNull(counters); - ThrowIfDisposed(); + ThrowIfDisposed(); - var result = new Dictionary<string, List<(DateTime Date, Counters Counters)>>(); + category = GetCategory(category); - var usageData = await usageRepository.QueryAsync(key, fromDate, toDate, ct); - var usageGroups = usageData.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); +#pragma warning disable MA0105 // Use the lambda parameters instead of using a closure + jobs.AddOrUpdate((key, category, date), counters, (k, p) => p.SumUp(counters)); +#pragma warning restore MA0105 // Use the lambda parameters instead of using a closure - if (usageGroups.Keys.Count == 0) - { - var enriched = new List<(DateTime Date, Counters Counters)>(); + return Task.CompletedTask; + } - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - enriched.Add((date, new Counters())); - } + public async Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(key); - result[FallbackCategory] = enriched; - } + ThrowIfDisposed(); - foreach (var (category, value) in usageGroups) - { - var enriched = new List<(DateTime Date, Counters Counters)>(); + var result = new Dictionary<string, List<(DateTime Date, Counters Counters)>>(); - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - var counters = value.Find(x => x.Date == date)?.Counters; + var usageData = await usageRepository.QueryAsync(key, fromDate, toDate, ct); + var usageGroups = usageData.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); - enriched.Add((date, counters ?? new Counters())); - } + if (usageGroups.Keys.Count == 0) + { + var enriched = new List<(DateTime Date, Counters Counters)>(); - result[category] = enriched; + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + enriched.Add((date, new Counters())); } - return result; + result[FallbackCategory] = enriched; } - public Task<Counters> GetForMonthAsync(string key, DateTime date, string? category, - CancellationToken ct = default) + foreach (var (category, value) in usageGroups) { - var dateFrom = new DateTime(date.Year, date.Month, 1); - var dateTo = dateFrom.AddMonths(1).AddDays(-1); + var enriched = new List<(DateTime Date, Counters Counters)>(); - return GetAsync(key, dateFrom, dateTo, category, ct); + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + var counters = value.Find(x => x.Date == date)?.Counters; + + enriched.Add((date, counters ?? new Counters())); + } + + result[category] = enriched; } - public async Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, - CancellationToken ct = default) - { - Guard.NotNullOrEmpty(key); + return result; + } - ThrowIfDisposed(); + public Task<Counters> GetForMonthAsync(string key, DateTime date, string? category, + CancellationToken ct = default) + { + var dateFrom = new DateTime(date.Year, date.Month, 1); + var dateTo = dateFrom.AddMonths(1).AddDays(-1); - var queried = await usageRepository.QueryAsync(key, fromDate, toDate, ct); + return GetAsync(key, dateFrom, dateTo, category, ct); + } - if (category != null) - { - queried = queried.Where(x => string.Equals(x.Category, category, StringComparison.OrdinalIgnoreCase)).ToList(); - } + public async Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(key); - var result = new Counters(); + ThrowIfDisposed(); - foreach (var usage in queried) - { - result.SumUp(usage.Counters); - } + var queried = await usageRepository.QueryAsync(key, fromDate, toDate, ct); - return result; + if (category != null) + { + queried = queried.Where(x => string.Equals(x.Category, category, StringComparison.OrdinalIgnoreCase)).ToList(); } - private string GetCategory(string? category) + var result = new Counters(); + + foreach (var usage in queried) { - return !string.IsNullOrWhiteSpace(category) ? category.Trim() : FallbackCategory; + result.SumUp(usage.Counters); } + + return result; + } + + private string GetCategory(string? category) + { + return !string.IsNullOrWhiteSpace(category) ? category.Trim() : FallbackCategory; } } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs index 05692da360..4de675b53d 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs @@ -7,82 +7,81 @@ using Microsoft.Extensions.Caching.Memory; -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public sealed class CachingUsageTracker : IUsageTracker { - public sealed class CachingUsageTracker : IUsageTracker + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); + private readonly IUsageTracker inner; + private readonly IMemoryCache cache; + + public string FallbackCategory => inner.FallbackCategory; + + public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); - private readonly IUsageTracker inner; - private readonly IMemoryCache cache; + this.inner = inner; + this.cache = cache; + } - public string FallbackCategory => inner.FallbackCategory; + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + Guard.NotNull(key); - public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) - { - this.inner = inner; - this.cache = cache; - } + return inner.DeleteAsync(key, ct); + } - public Task DeleteAsync(string key, - CancellationToken ct = default) - { - Guard.NotNull(key); + public Task DeleteByKeyPatternAsync(string pattern, + CancellationToken ct = default) + { + Guard.NotNull(pattern); - return inner.DeleteAsync(key, ct); - } + return inner.DeleteByKeyPatternAsync(pattern, ct); + } - public Task DeleteByKeyPatternAsync(string pattern, - CancellationToken ct = default) - { - Guard.NotNull(pattern); + public Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + Guard.NotNull(key); - return inner.DeleteByKeyPatternAsync(pattern, ct); - } + return inner.QueryAsync(key, fromDate, toDate, ct); + } - public Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default) - { - Guard.NotNull(key); + public Task TrackAsync(DateTime date, string key, string? category, Counters counters, + CancellationToken ct = default) + { + Guard.NotNull(key); - return inner.QueryAsync(key, fromDate, toDate, ct); - } + return inner.TrackAsync(date, key, category, counters, ct); + } - public Task TrackAsync(DateTime date, string key, string? category, Counters counters, - CancellationToken ct = default) - { - Guard.NotNull(key); + public Task<Counters> GetForMonthAsync(string key, DateTime date, string? category, + CancellationToken ct = default) + { + Guard.NotNull(key); - return inner.TrackAsync(date, key, category, counters, ct); - } + var cacheKey = $"{typeof(CachingUsageTracker)}_UsageForMonth_{key}_{date}_{category}"; - public Task<Counters> GetForMonthAsync(string key, DateTime date, string? category, - CancellationToken ct = default) + return cache.GetOrCreateAsync(cacheKey, entry => { - Guard.NotNull(key); + entry.AbsoluteExpirationRelativeToNow = CacheDuration; - var cacheKey = $"{typeof(CachingUsageTracker)}_UsageForMonth_{key}_{date}_{category}"; + return inner.GetForMonthAsync(key, date, category, ct); + }); + } - return cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; + public Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, + CancellationToken ct = default) + { + Guard.NotNull(key); - return inner.GetForMonthAsync(key, date, category, ct); - }); - } + var cacheKey = $"{typeof(CachingUsageTracker)}_Usage_{key}_{fromDate}_{toDate}_{category}"; - public Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, - CancellationToken ct = default) + return cache.GetOrCreateAsync(cacheKey, entry => { - Guard.NotNull(key); - - var cacheKey = $"{typeof(CachingUsageTracker)}_Usage_{key}_{fromDate}_{toDate}_{category}"; - - return cache.GetOrCreateAsync(cacheKey, entry => - { - entry.AbsoluteExpirationRelativeToNow = CacheDuration; + entry.AbsoluteExpirationRelativeToNow = CacheDuration; - return inner.GetAsync(key, fromDate, toDate, category, ct); - }); - } + return inner.GetAsync(key, fromDate, toDate, category, ct); + }); } } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/Counters.cs b/backend/src/Squidex.Infrastructure/UsageTracking/Counters.cs index e1a61b66a1..903c1468cc 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/Counters.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/Counters.cs @@ -5,58 +5,57 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public sealed class Counters : Dictionary<string, double> { - public sealed class Counters : Dictionary<string, double> + public Counters() { - public Counters() - { - } + } - public Counters(Counters source) - : base(source) - { - } + public Counters(Counters source) + : base(source) + { + } - public double Get(string name) + public double Get(string name) + { + if (name == null) { - if (name == null) - { - return 0; - } + return 0; + } - TryGetValue(name, out var value); + TryGetValue(name, out var value); - return value; - } + return value; + } - public long GetInt64(string name) + public long GetInt64(string name) + { + if (name == null) { - if (name == null) - { - return 0; - } + return 0; + } - TryGetValue(name, out var value); + TryGetValue(name, out var value); - return (long)value; - } + return (long)value; + } - public Counters SumUp(Counters counters) + public Counters SumUp(Counters counters) + { + foreach (var (key, value) in counters) { - foreach (var (key, value) in counters) - { - var newValue = value; + var newValue = value; - if (TryGetValue(key, out var temp)) - { - newValue += temp; - } - - this[key] = newValue; + if (TryGetValue(key, out var temp)) + { + newValue += temp; } - return this; + this[key] = newValue; } + + return this; } } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/IApiUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IApiUsageTracker.cs index b4a43a28ef..fd24129a87 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/IApiUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/IApiUsageTracker.cs @@ -5,23 +5,22 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public interface IApiUsageTracker { - public interface IApiUsageTracker - { - Task DeleteAsync(string key, - CancellationToken ct = default); + Task DeleteAsync(string key, + CancellationToken ct = default); - Task TrackAsync(DateTime date, string key, string? category, double weight, long elapsedMs, long bytes, - CancellationToken ct = default); + Task TrackAsync(DateTime date, string key, string? category, double weight, long elapsedMs, long bytes, + CancellationToken ct = default); - Task<long> GetMonthCallsAsync(string key, DateTime date, string? category, - CancellationToken ct = default); + Task<long> GetMonthCallsAsync(string key, DateTime date, string? category, + CancellationToken ct = default); - Task<long> GetMonthBytesAsync(string key, DateTime date, string? category, - CancellationToken ct = default); + Task<long> GetMonthBytesAsync(string key, DateTime date, string? category, + CancellationToken ct = default); - Task<(ApiStatsSummary, Dictionary<string, List<ApiStats>> Details)> QueryAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default); - } + Task<(ApiStatsSummary, Dictionary<string, List<ApiStats>> Details)> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs index cb1fdd54a8..d65ff15110 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs @@ -5,23 +5,22 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public interface IUsageRepository { - public interface IUsageRepository - { - Task TrackUsagesAsync(UsageUpdate update, - CancellationToken ct = default); + Task TrackUsagesAsync(UsageUpdate update, + CancellationToken ct = default); - Task TrackUsagesAsync(UsageUpdate[] updates, - CancellationToken ct = default); + Task TrackUsagesAsync(UsageUpdate[] updates, + CancellationToken ct = default); - Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default); + Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); - Task DeleteAsync(string key, - CancellationToken ct = default); + Task DeleteAsync(string key, + CancellationToken ct = default); - Task DeleteByKeyPatternAsync(string pattern, - CancellationToken ct = default); - } + Task DeleteByKeyPatternAsync(string pattern, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs index 9819d9452f..808eb1c00e 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -5,28 +5,27 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public interface IUsageTracker { - public interface IUsageTracker - { - string FallbackCategory { get; } + string FallbackCategory { get; } - Task TrackAsync(DateTime date, string key, string? category, Counters counters, - CancellationToken ct = default); + Task TrackAsync(DateTime date, string key, string? category, Counters counters, + CancellationToken ct = default); - Task<Counters> GetForMonthAsync(string key, DateTime date, string? category, - CancellationToken ct = default); + Task<Counters> GetForMonthAsync(string key, DateTime date, string? category, + CancellationToken ct = default); - Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, - CancellationToken ct = default); + Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, + CancellationToken ct = default); - Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate, - CancellationToken ct = default); + Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); - Task DeleteAsync(string key, - CancellationToken ct = default); + Task DeleteAsync(string key, + CancellationToken ct = default); - Task DeleteByKeyPatternAsync(string pattern, - CancellationToken ct = default); - } + Task DeleteByKeyPatternAsync(string pattern, + CancellationToken ct = default); } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs b/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs index 6ca2c93d22..cd3b12d534 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs @@ -7,7 +7,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed record StoredUsage(string? Category, DateTime Date, Counters Counters); -} +namespace Squidex.Infrastructure.UsageTracking; + +public sealed record StoredUsage(string? Category, DateTime Date, Counters Counters); diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs b/backend/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs index 7cdd9ecf2a..97dfb8d4ef 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs @@ -5,24 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public struct UsageUpdate { - public struct UsageUpdate - { - public DateTime Date; + public DateTime Date; - public string Key; + public string Key; - public string Category; + public string Category; - public Counters Counters; + public Counters Counters; - public UsageUpdate(DateTime date, string key, string category, Counters counters) - { - Key = key; - Category = category; - Counters = counters; - Date = date; - } + public UsageUpdate(DateTime date, string key, string category, Counters counters) + { + Key = key; + Category = category; + Counters = counters; + Date = date; } } diff --git a/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs index 1d90d8b0a7..fe615ec5aa 100644 --- a/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs @@ -9,21 +9,20 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class AbsoluteUrlAttribute : ValidationAttribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class AbsoluteUrlAttribute : ValidationAttribute + public override string FormatErrorMessage(string name) { - public override string FormatErrorMessage(string name) - { - var property = T.Get($"common.{name.ToCamelCase()}", name); + var property = T.Get($"common.{name.ToCamelCase()}", name); - return T.Get("annotations_AbsoluteUrl", new { property }); - } + return T.Get("annotations_AbsoluteUrl", new { property }); + } - public override bool IsValid(object? value) - { - return value is not Uri uri || uri.IsAbsoluteUri; - } + public override bool IsValid(object? value) + { + return value is not Uri uri || uri.IsAbsoluteUri; } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedCompareAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedCompareAttribute.cs index 13e3fdfde7..be294104f3 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedCompareAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedCompareAttribute.cs @@ -9,22 +9,21 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +public sealed class LocalizedCompareAttribute : CompareAttribute { - public sealed class LocalizedCompareAttribute : CompareAttribute + public LocalizedCompareAttribute(string otherProperty) + : base(otherProperty) { - public LocalizedCompareAttribute(string otherProperty) - : base(otherProperty) - { - } + } - public override string FormatErrorMessage(string name) - { - var property = T.Get($"common.{name.ToCamelCase()}", name); + public override string FormatErrorMessage(string name) + { + var property = T.Get($"common.{name.ToCamelCase()}", name); - var other = T.Get($"common.{OtherProperty.ToCamelCase()}", OtherProperty); + var other = T.Get($"common.{OtherProperty.ToCamelCase()}", OtherProperty); - return T.Get("annotations_Compare", base.FormatErrorMessage(name), new { name = property, other }); - } + return T.Get("annotations_Compare", base.FormatErrorMessage(name), new { name = property, other }); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedEmailAddressAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedEmailAddressAttribute.cs index 2b3f144ff0..f44c13be4d 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedEmailAddressAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedEmailAddressAttribute.cs @@ -9,21 +9,20 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +[AttributeUsage(AttributeTargets.Property)] +public class LocalizedEmailAddressAttribute : ValidationAttribute { - [AttributeUsage(AttributeTargets.Property)] - public class LocalizedEmailAddressAttribute : ValidationAttribute + public override bool IsValid(object? value) { - public override bool IsValid(object? value) - { - return value is not string s || s.IsEmail(); - } + return value is not string s || s.IsEmail(); + } - public override string FormatErrorMessage(string name) - { - var property = T.Get($"common.{name.ToCamelCase()}", name); + public override string FormatErrorMessage(string name) + { + var property = T.Get($"common.{name.ToCamelCase()}", name); - return T.Get("annotations_EmailAddress", base.FormatErrorMessage(name), new { name = property }); - } + return T.Get("annotations_EmailAddress", base.FormatErrorMessage(name), new { name = property }); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedRangeAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedRangeAttribute.cs index 44ab8a251e..b60085c1c0 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedRangeAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedRangeAttribute.cs @@ -9,29 +9,28 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class LocalizedRangeAttribute : RangeAttribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class LocalizedRangeAttribute : RangeAttribute + public LocalizedRangeAttribute(int minimum, int maximum) + : base(minimum, maximum) { - public LocalizedRangeAttribute(int minimum, int maximum) - : base(minimum, maximum) - { - } + } - public LocalizedRangeAttribute(double minimum, double maximum) - : base(minimum, maximum) - { - } + public LocalizedRangeAttribute(double minimum, double maximum) + : base(minimum, maximum) + { + } - public override string FormatErrorMessage(string name) - { - var property = T.Get($"common.{name.ToCamelCase()}", name); + public override string FormatErrorMessage(string name) + { + var property = T.Get($"common.{name.ToCamelCase()}", name); - var min = Minimum; - var max = Maximum; + var min = Minimum; + var max = Maximum; - return T.Get("annotations_Range", base.FormatErrorMessage(name), new { name = property, min, max }); - } + return T.Get("annotations_Range", base.FormatErrorMessage(name), new { name = property, min, max }); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedRegularExpressionAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedRegularExpressionAttribute.cs index 108d8ccdf5..924a4a2cd1 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedRegularExpressionAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedRegularExpressionAttribute.cs @@ -9,21 +9,20 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class LocalizedRegularExpressionAttribute : RegularExpressionAttribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class LocalizedRegularExpressionAttribute : RegularExpressionAttribute + public LocalizedRegularExpressionAttribute(string pattern) + : base(pattern) { - public LocalizedRegularExpressionAttribute(string pattern) - : base(pattern) - { - } + } - public override string FormatErrorMessage(string name) - { - var property = T.Get($"common.{name.ToCamelCase()}", name); + public override string FormatErrorMessage(string name) + { + var property = T.Get($"common.{name.ToCamelCase()}", name); - return T.Get("annotations_RegularExpression", base.FormatErrorMessage(name), new { name = property }); - } + return T.Get("annotations_RegularExpression", base.FormatErrorMessage(name), new { name = property }); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedRequiredAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedRequiredAttribute.cs index af3b95550f..1c31ceaf71 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedRequiredAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedRequiredAttribute.cs @@ -9,16 +9,15 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +[AttributeUsage(AttributeTargets.Property)] +public class LocalizedRequiredAttribute : RequiredAttribute { - [AttributeUsage(AttributeTargets.Property)] - public class LocalizedRequiredAttribute : RequiredAttribute + public override string FormatErrorMessage(string name) { - public override string FormatErrorMessage(string name) - { - var property = T.Get($"common.{name.ToCamelCase()}", name); + var property = T.Get($"common.{name.ToCamelCase()}", name); - return T.Get("annotations_Required", base.FormatErrorMessage(name), new { name = property }); - } + return T.Get("annotations_Required", base.FormatErrorMessage(name), new { name = property }); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedStringLengthAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedStringLengthAttribute.cs index f610dda351..92f7efae0e 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedStringLengthAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedStringLengthAttribute.cs @@ -9,30 +9,29 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +public sealed class LocalizedStringLengthAttribute : StringLengthAttribute { - public sealed class LocalizedStringLengthAttribute : StringLengthAttribute + public LocalizedStringLengthAttribute(int maximumLength) + : base(maximumLength) { - public LocalizedStringLengthAttribute(int maximumLength) - : base(maximumLength) - { - } - - public override string FormatErrorMessage(string name) - { - var property = T.Get($"common.{name.ToCamelCase()}", name); + } - var min = MinimumLength; - var max = MaximumLength; + public override string FormatErrorMessage(string name) + { + var property = T.Get($"common.{name.ToCamelCase()}", name); - var args = new { name = property, min, max }; + var min = MinimumLength; + var max = MaximumLength; - if (min > 0) - { - return T.Get("annotations_StringLengthMinimum", base.FormatErrorMessage(name), args); - } + var args = new { name = property, min, max }; - return T.Get("annotations_StringLength", base.FormatErrorMessage(name), args); + if (min > 0) + { + return T.Get("annotations_StringLengthMinimum", base.FormatErrorMessage(name), args); } + + return T.Get("annotations_StringLength", base.FormatErrorMessage(name), args); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/Not.cs b/backend/src/Squidex.Infrastructure/Validation/Not.cs index a5600295ac..e2b9b0f304 100644 --- a/backend/src/Squidex.Infrastructure/Validation/Not.cs +++ b/backend/src/Squidex.Infrastructure/Validation/Not.cs @@ -9,103 +9,102 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +public static class Not { - public static class Not + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Defined() { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Defined() - { - return T.Get("validation.requiredValue"); - } + return T.Get("validation.requiredValue"); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Defined(string propertyName) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Defined(string propertyName) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - return T.Get("validation.required", new { property }); - } + return T.Get("validation.required", new { property }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string BothDefined(string propertyName1, string propertyName2) - { - var property1 = T.Get($"common.{propertyName1.ToCamelCase()}", propertyName1); - var property2 = T.Get($"common.{propertyName2.ToCamelCase()}", propertyName2); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string BothDefined(string propertyName1, string propertyName2) + { + var property1 = T.Get($"common.{propertyName1.ToCamelCase()}", propertyName1); + var property2 = T.Get($"common.{propertyName2.ToCamelCase()}", propertyName2); - return T.Get("validation.requiredBoth", new { property1, property2 }); - } + return T.Get("validation.requiredBoth", new { property1, property2 }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ValidSlug(string propertyName) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ValidSlug(string propertyName) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - return T.Get("validation.slug", new { property }); - } + return T.Get("validation.slug", new { property }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ValidJavascriptName(string propertyName) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ValidJavascriptName(string propertyName) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - return T.Get("validation.javascriptProperty", new { property }); - } + return T.Get("validation.javascriptProperty", new { property }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GreaterThan(string propertyName, string otherName) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GreaterThan(string propertyName, string otherName) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - var other = T.Get($"common.{otherName.ToCamelCase()}", otherName); + var other = T.Get($"common.{otherName.ToCamelCase()}", otherName); - return T.Get("validation.greaterThan", new { property, other }); - } + return T.Get("validation.greaterThan", new { property, other }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GreaterEqualsThan(string propertyName, string otherName) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GreaterEqualsThan(string propertyName, string otherName) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - var other = T.Get($"common.{otherName.ToCamelCase()}", otherName); + var other = T.Get($"common.{otherName.ToCamelCase()}", otherName); - return T.Get("validation.greaterEqualsThan", new { property, other }); - } + return T.Get("validation.greaterEqualsThan", new { property, other }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string LessThan(string propertyName, string otherName) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string LessThan(string propertyName, string otherName) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - var other = T.Get($"common.{otherName.ToCamelCase()}", otherName); + var other = T.Get($"common.{otherName.ToCamelCase()}", otherName); - return T.Get("validation.lessThan", new { property, other }); - } + return T.Get("validation.lessThan", new { property, other }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string LessEqualsThan(string propertyName, string otherName) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string LessEqualsThan(string propertyName, string otherName) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - var other = T.Get($"common.{otherName.ToCamelCase()}", otherName); + var other = T.Get($"common.{otherName.ToCamelCase()}", otherName); - return T.Get("validation.lessEqualsThan", new { property, other }); - } + return T.Get("validation.lessEqualsThan", new { property, other }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Between<TField>(string propertyName, TField min, TField max) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Between<TField>(string propertyName, TField min, TField max) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - return T.Get("validation.between", new { property, min, max }); - } + return T.Get("validation.between", new { property, min, max }); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Valid(string propertyName) - { - var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Valid(string propertyName) + { + var property = T.Get($"common.{propertyName.ToCamelCase()}", propertyName); - return T.Get("validation.valid", new { property }); - } + return T.Get("validation.valid", new { property }); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/Validate.cs b/backend/src/Squidex.Infrastructure/Validation/Validate.cs index 839061e88c..449a26e544 100644 --- a/backend/src/Squidex.Infrastructure/Validation/Validate.cs +++ b/backend/src/Squidex.Infrastructure/Validation/Validate.cs @@ -7,46 +7,45 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +public static class Validate { - public static class Validate + public static void It(Action<AddValidation> action) { - public static void It(Action<AddValidation> action) - { - List<ValidationError>? errors = null; + List<ValidationError>? errors = null; - var addValidation = new AddValidation((m, p) => - { - errors ??= new List<ValidationError>(); - errors.Add(new ValidationError(m, p)); - }); + var addValidation = new AddValidation((m, p) => + { + errors ??= new List<ValidationError>(); + errors.Add(new ValidationError(m, p)); + }); - action(addValidation); + action(addValidation); - if (errors != null) - { - throw new ValidationException(errors); - } + if (errors != null) + { + throw new ValidationException(errors); } + } - public static async Task It(Func<AddValidation, Task> action) - { - List<ValidationError>? errors = null; + public static async Task It(Func<AddValidation, Task> action) + { + List<ValidationError>? errors = null; - var addValidation = new AddValidation((m, p) => - { - errors ??= new List<ValidationError>(); - errors.Add(new ValidationError(m, p)); - }); + var addValidation = new AddValidation((m, p) => + { + errors ??= new List<ValidationError>(); + errors.Add(new ValidationError(m, p)); + }); - await action(addValidation); + await action(addValidation); - if (errors != null) - { - throw new ValidationException(errors); - } + if (errors != null) + { + throw new ValidationException(errors); } } - - public delegate void AddValidation(string message, params string[] propertyNames); } + +public delegate void AddValidation(string message, params string[] propertyNames); diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs index a0015693ea..19c18863c8 100644 --- a/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationError.cs @@ -5,48 +5,47 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +[Serializable] +public sealed class ValidationError { - [Serializable] - public sealed class ValidationError - { - private readonly string message; - private readonly string[] propertyNames; + private readonly string message; + private readonly string[] propertyNames; - public string Message - { - get => message; - } + public string Message + { + get => message; + } - public IEnumerable<string> PropertyNames - { - get => propertyNames; - } + public IEnumerable<string> PropertyNames + { + get => propertyNames; + } - public ValidationError(string message, params string[] propertyNames) - { - Guard.NotNullOrEmpty(message); + public ValidationError(string message, params string[] propertyNames) + { + Guard.NotNullOrEmpty(message); - this.message = message; + this.message = message; - this.propertyNames = propertyNames ?? Array.Empty<string>(); - } + this.propertyNames = propertyNames ?? Array.Empty<string>(); + } - public ValidationError WithPrefix(string prefix) + public ValidationError WithPrefix(string prefix) + { + if (propertyNames.Length > 0) { - if (propertyNames.Length > 0) - { - return new ValidationError(Message, propertyNames.Select(x => $"{prefix}.{x}").ToArray()); - } - else - { - return new ValidationError(Message, prefix); - } + return new ValidationError(Message, propertyNames.Select(x => $"{prefix}.{x}").ToArray()); } - - public void AddTo(AddValidation e) + else { - e(Message, propertyNames); + return new ValidationError(Message, prefix); } } + + public void AddTo(AddValidation e) + { + e(Message, propertyNames); + } } diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs index 983b7c2752..dc0138c13d 100644 --- a/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs @@ -8,71 +8,70 @@ using System.Runtime.Serialization; using System.Text; -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +[Serializable] +public class ValidationException : DomainException { - [Serializable] - public class ValidationException : DomainException + private const string ValidationError = "VALIDATION_ERROR"; + + public IReadOnlyList<ValidationError> Errors { get; } + + public ValidationException(string error, Exception? inner = null) + : this(new ValidationError(error), inner) { - private const string ValidationError = "VALIDATION_ERROR"; + } - public IReadOnlyList<ValidationError> Errors { get; } + public ValidationException(ValidationError error, Exception? inner = null) + : this(new List<ValidationError> { error }, inner) + { + } - public ValidationException(string error, Exception? inner = null) - : this(new ValidationError(error), inner) - { - } + public ValidationException(IReadOnlyList<ValidationError> errors, Exception? inner = null) + : base(FormatMessage(errors), ValidationError, inner) + { + Errors = errors; + } - public ValidationException(ValidationError error, Exception? inner = null) - : this(new List<ValidationError> { error }, inner) - { - } + protected ValidationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Errors = (List<ValidationError>)info.GetValue(nameof(Errors), typeof(List<ValidationError>))!; + } - public ValidationException(IReadOnlyList<ValidationError> errors, Exception? inner = null) - : base(FormatMessage(errors), ValidationError, inner) - { - Errors = errors; - } + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Errors), Errors); - protected ValidationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Errors = (List<ValidationError>)info.GetValue(nameof(Errors), typeof(List<ValidationError>))!; - } + base.GetObjectData(info, context); + } - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Errors), Errors); + private static string FormatMessage(IReadOnlyList<ValidationError> errors) + { + Guard.NotNull(errors); - base.GetObjectData(info, context); - } + var sb = new StringBuilder(); - private static string FormatMessage(IReadOnlyList<ValidationError> errors) + for (var i = 0; i < errors.Count; i++) { - Guard.NotNull(errors); - - var sb = new StringBuilder(); + var error = errors[i]?.Message; - for (var i = 0; i < errors.Count; i++) + if (!string.IsNullOrWhiteSpace(error)) { - var error = errors[i]?.Message; + sb.Append(error); - if (!string.IsNullOrWhiteSpace(error)) + if (!error.EndsWith(".", StringComparison.OrdinalIgnoreCase)) { - sb.Append(error); - - if (!error.EndsWith(".", StringComparison.OrdinalIgnoreCase)) - { - sb.Append('.'); - } + sb.Append('.'); + } - if (i < errors.Count - 1) - { - sb.Append(' '); - } + if (i < errors.Count - 1) + { + sb.Append(' '); } } - - return sb.ToString(); } + + return sb.ToString(); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs index 1eac96a000..34b0323379 100644 --- a/backend/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationExtensions.cs @@ -10,39 +10,38 @@ #pragma warning disable RECS0026 // Possible unassigned object created by 'new' #pragma warning disable CA1806 // Do not ignore method results -namespace Squidex.Infrastructure.Validation +namespace Squidex.Infrastructure.Validation; + +public static class ValidationExtensions { - public static class ValidationExtensions + public static bool IsBetween<TValue>(this TValue value, TValue low, TValue high) where TValue : IComparable { - public static bool IsBetween<TValue>(this TValue value, TValue low, TValue high) where TValue : IComparable + return Comparer<TValue>.Default.Compare(low, value) <= 0 && Comparer<TValue>.Default.Compare(high, value) >= 0; + } + + public static bool IsEnumValue<TEnum>(this TEnum value) where TEnum : struct + { + try { - return Comparer<TValue>.Default.Compare(low, value) <= 0 && Comparer<TValue>.Default.Compare(high, value) >= 0; + return Enum.IsDefined(typeof(TEnum), value); } - - public static bool IsEnumValue<TEnum>(this TEnum value) where TEnum : struct + catch { - try - { - return Enum.IsDefined(typeof(TEnum), value); - } - catch - { - return false; - } + return false; } + } - public static bool IsValidRegex(this string value) + public static bool IsValidRegex(this string value) + { + try { - try - { - new Regex(value); + new Regex(value); - return true; - } - catch (ArgumentException) - { - return false; - } + return true; + } + catch (ArgumentException) + { + return false; } } } diff --git a/backend/src/Squidex.Infrastructure/ValueStopwatch.cs b/backend/src/Squidex.Infrastructure/ValueStopwatch.cs index 66af46944d..a1ea159cb1 100644 --- a/backend/src/Squidex.Infrastructure/ValueStopwatch.cs +++ b/backend/src/Squidex.Infrastructure/ValueStopwatch.cs @@ -7,50 +7,49 @@ using System.Diagnostics; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public readonly struct ValueStopwatch { - public readonly struct ValueStopwatch - { - private const long TicksPerMillisecond = 10000; - private const long TicksPerSecond = TicksPerMillisecond * 1000; + private const long TicksPerMillisecond = 10000; + private const long TicksPerSecond = TicksPerMillisecond * 1000; - private static readonly double TickFrequency; + private static readonly double TickFrequency; - private readonly long startTime; + private readonly long startTime; - static ValueStopwatch() + static ValueStopwatch() + { + if (Stopwatch.IsHighResolution) { - if (Stopwatch.IsHighResolution) - { - TickFrequency = (double)TicksPerSecond / Stopwatch.Frequency; - } + TickFrequency = (double)TicksPerSecond / Stopwatch.Frequency; } + } - public ValueStopwatch(long startTime) - { - this.startTime = startTime; - } + public ValueStopwatch(long startTime) + { + this.startTime = startTime; + } - public static ValueStopwatch StartNew() + public static ValueStopwatch StartNew() + { + return new ValueStopwatch(Stopwatch.GetTimestamp()); + } + + public long Stop() + { + var elapsed = Stopwatch.GetTimestamp() - startTime; + + if (elapsed < 0) { - return new ValueStopwatch(Stopwatch.GetTimestamp()); + return elapsed; } - public long Stop() + if (Stopwatch.IsHighResolution) { - var elapsed = Stopwatch.GetTimestamp() - startTime; - - if (elapsed < 0) - { - return elapsed; - } - - if (Stopwatch.IsHighResolution) - { - elapsed = unchecked((long)(elapsed * TickFrequency)); - } - - return elapsed / TicksPerMillisecond; + elapsed = unchecked((long)(elapsed * TickFrequency)); } + + return elapsed / TicksPerMillisecond; } } diff --git a/backend/src/Squidex.Web/ApiController.cs b/backend/src/Squidex.Web/ApiController.cs index 4cb86cd7ba..b9460989b6 100644 --- a/backend/src/Squidex.Web/ApiController.cs +++ b/backend/src/Squidex.Web/ApiController.cs @@ -16,117 +16,116 @@ using Squidex.Infrastructure.Translations; using Squidex.Shared; -namespace Squidex.Web +namespace Squidex.Web; + +[Area("api")] +[ApiController] +[ApiExceptionFilter] +[ApiModelValidation(false)] +[Route(Constants.PrefixApi)] +public abstract class ApiController : Controller { - [Area("api")] - [ApiController] - [ApiExceptionFilter] - [ApiModelValidation(false)] - [Route(Constants.PrefixApi)] - public abstract class ApiController : Controller - { - private readonly Lazy<Resources> resources; + private readonly Lazy<Resources> resources; - protected ICommandBus CommandBus { get; } + protected ICommandBus CommandBus { get; } - protected IAppEntity App + protected IAppEntity App + { + get { - get - { - var app = HttpContext.Features.Get<IAppFeature>()?.App; - - if (app == null) - { - ThrowHelper.InvalidOperationException("Not in a app context."); - return default!; - } + var app = HttpContext.Features.Get<IAppFeature>()?.App; - return app; + if (app == null) + { + ThrowHelper.InvalidOperationException("Not in a app context."); + return default!; } + + return app; } + } - protected ITeamEntity Team + protected ITeamEntity Team + { + get { - get - { - var team = HttpContext.Features.Get<ITeamFeature>()?.Team; - - if (team == null) - { - ThrowHelper.InvalidOperationException("Not in a team context."); - return default!; - } + var team = HttpContext.Features.Get<ITeamFeature>()?.Team; - return team; + if (team == null) + { + ThrowHelper.InvalidOperationException("Not in a team context."); + return default!; } + + return team; } + } - protected ISchemaEntity Schema + protected ISchemaEntity Schema + { + get { - get - { - var schema = HttpContext.Features.Get<ISchemaFeature>()?.Schema; + var schema = HttpContext.Features.Get<ISchemaFeature>()?.Schema; - if (schema == null) - { - ThrowHelper.InvalidOperationException("Not in a schema context."); - return default!; - } - - return schema; + if (schema == null) + { + ThrowHelper.InvalidOperationException("Not in a schema context."); + return default!; } + + return schema; } + } - protected string UserId + protected string UserId + { + get { - get - { - var subject = User.OpenIdSubject(); - - if (string.IsNullOrWhiteSpace(subject)) - { - throw new DomainForbiddenException(T.Get("common.httpOnlyAsUser")); - } + var subject = User.OpenIdSubject(); - return subject; + if (string.IsNullOrWhiteSpace(subject)) + { + throw new DomainForbiddenException(T.Get("common.httpOnlyAsUser")); } - } - protected bool IsFrontend - { - get => HttpContext.User.IsInClient(DefaultClients.Frontend); + return subject; } + } - protected string UserOrClientId - { - get => HttpContext.User.UserOrClientId()!; - } + protected bool IsFrontend + { + get => HttpContext.User.IsInClient(DefaultClients.Frontend); + } - protected Resources Resources - { - get => resources.Value; - } + protected string UserOrClientId + { + get => HttpContext.User.UserOrClientId()!; + } - protected Context Context - { - get => HttpContext.Context(); - } + protected Resources Resources + { + get => resources.Value; + } - protected DomainId AppId - { - get => App.Id; - } + protected Context Context + { + get => HttpContext.Context(); + } - protected DomainId TeamId - { - get => Team.Id; - } + protected DomainId AppId + { + get => App.Id; + } - protected ApiController(ICommandBus commandBus) - { - CommandBus = commandBus; + protected DomainId TeamId + { + get => Team.Id; + } - resources = new Lazy<Resources>(() => new Resources(this)); - } + protected ApiController(ICommandBus commandBus) + { + CommandBus = commandBus; + + resources = new Lazy<Resources>(() => new Resources(this)); } } diff --git a/backend/src/Squidex.Web/ApiCostsAttribute.cs b/backend/src/Squidex.Web/ApiCostsAttribute.cs index afae1d92ea..a91b05bd38 100644 --- a/backend/src/Squidex.Web/ApiCostsAttribute.cs +++ b/backend/src/Squidex.Web/ApiCostsAttribute.cs @@ -8,17 +8,16 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Web.Pipeline; -namespace Squidex.Web +namespace Squidex.Web; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class ApiCostsAttribute : ServiceFilterAttribute, IApiCostsFeature { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public sealed class ApiCostsAttribute : ServiceFilterAttribute, IApiCostsFeature - { - public double Costs { get; } + public double Costs { get; } - public ApiCostsAttribute(double costs) - : base(typeof(ApiCostsFilter)) - { - Costs = costs; - } + public ApiCostsAttribute(double costs) + : base(typeof(ApiCostsFilter)) + { + Costs = costs; } } diff --git a/backend/src/Squidex.Web/ApiExceptionConverter.cs b/backend/src/Squidex.Web/ApiExceptionConverter.cs index 18aa643cac..372f0170e9 100644 --- a/backend/src/Squidex.Web/ApiExceptionConverter.cs +++ b/backend/src/Squidex.Web/ApiExceptionConverter.cs @@ -14,191 +14,190 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Web +namespace Squidex.Web; + +public static class ApiExceptionConverter { - public static class ApiExceptionConverter + private static readonly Dictionary<int, string> Links = new Dictionary<int, string> { - private static readonly Dictionary<int, string> Links = new Dictionary<int, string> - { - [400] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1", - [401] = "https://www.rfc-editor.org/rfc/rfc7235#section-3.1", - [403] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.3", - [404] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.4", - [406] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.6", - [408] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.7", - [409] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.8", - [410] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.9", - [412] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.10", - [415] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.13", - [422] = "https://www.rfc-editor.org/rfc/rfc4918#section-11.2", - [500] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1" - }; - - public static (ErrorDto Error, Exception? Unhandled) ToErrorDto(int statusCode, HttpContext? httpContext) - { - var error = new ErrorDto { StatusCode = statusCode }; - - Enrich(httpContext, error); + [400] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1", + [401] = "https://www.rfc-editor.org/rfc/rfc7235#section-3.1", + [403] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.3", + [404] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.4", + [406] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.6", + [408] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.7", + [409] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.8", + [410] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.9", + [412] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.10", + [415] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.13", + [422] = "https://www.rfc-editor.org/rfc/rfc4918#section-11.2", + [500] = "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1" + }; + + public static (ErrorDto Error, Exception? Unhandled) ToErrorDto(int statusCode, HttpContext? httpContext) + { + var error = new ErrorDto { StatusCode = statusCode }; - return (error, null); - } + Enrich(httpContext, error); - public static (ErrorDto Error, Exception? Unhandled) ToErrorDto(this ProblemDetails problem, HttpContext? httpContext) - { - Guard.NotNull(problem); + return (error, null); + } - var error = CreateError(problem.Status ?? 500, problem.Title); + public static (ErrorDto Error, Exception? Unhandled) ToErrorDto(this ProblemDetails problem, HttpContext? httpContext) + { + Guard.NotNull(problem); - Enrich(httpContext, error); + var error = CreateError(problem.Status ?? 500, problem.Title); - return (error, null); - } + Enrich(httpContext, error); - public static (ErrorDto Error, Exception? Unhandled) ToErrorDto(this Exception exception, HttpContext? httpContext) - { - Guard.NotNull(exception); + return (error, null); + } - var result = CreateError(exception); + public static (ErrorDto Error, Exception? Unhandled) ToErrorDto(this Exception exception, HttpContext? httpContext) + { + Guard.NotNull(exception); - Enrich(httpContext, result.Error); + var result = CreateError(exception); - return result; - } + Enrich(httpContext, result.Error); - private static void Enrich(HttpContext? httpContext, ErrorDto error) - { - error.TraceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; + return result; + } - if (error.StatusCode == 0) - { - error.StatusCode = 500; - } + private static void Enrich(HttpContext? httpContext, ErrorDto error) + { + error.TraceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; - error.Type = Links.GetValueOrDefault(error.StatusCode); + if (error.StatusCode == 0) + { + error.StatusCode = 500; } - private static (ErrorDto Error, Exception? Unhandled) CreateError(Exception exception) + error.Type = Links.GetValueOrDefault(error.StatusCode); + } + + private static (ErrorDto Error, Exception? Unhandled) CreateError(Exception exception) + { + switch (exception) { - switch (exception) - { - case ValidationException ex: - { - var message = T.Get("common.httpValidationError"); + case ValidationException ex: + { + var message = T.Get("common.httpValidationError"); - return (CreateError(400, message, null, ToErrors(ex.Errors)), GetInner(exception)); - } + return (CreateError(400, message, null, ToErrors(ex.Errors)), GetInner(exception)); + } - case DomainObjectNotFoundException ex: - return (CreateError(404, ex.ErrorCode), GetInner(exception)); + case DomainObjectNotFoundException ex: + return (CreateError(404, ex.ErrorCode), GetInner(exception)); - case DomainObjectVersionException ex: - return (CreateError(412, ex.Message, ex.ErrorCode), GetInner(exception)); + case DomainObjectVersionException ex: + return (CreateError(412, ex.Message, ex.ErrorCode), GetInner(exception)); - case DomainObjectDeletedException ex: - return (CreateError(410, ex.Message, ex.ErrorCode), GetInner(exception)); + case DomainObjectDeletedException ex: + return (CreateError(410, ex.Message, ex.ErrorCode), GetInner(exception)); - case DomainObjectConflictException ex: - return (CreateError(409, ex.Message, ex.ErrorCode), GetInner(exception)); + case DomainObjectConflictException ex: + return (CreateError(409, ex.Message, ex.ErrorCode), GetInner(exception)); - case DomainForbiddenException ex: - return (CreateError(403, ex.Message, ex.ErrorCode), GetInner(exception)); + case DomainForbiddenException ex: + return (CreateError(403, ex.Message, ex.ErrorCode), GetInner(exception)); - case DomainException ex: - return (CreateError(400, ex.Message, ex.ErrorCode), GetInner(exception)); + case DomainException ex: + return (CreateError(400, ex.Message, ex.ErrorCode), GetInner(exception)); - case OperationCanceledException: - return (CreateError(408), null); + case OperationCanceledException: + return (CreateError(408), null); - case SecurityException ex: - return (CreateError(403), ex); + case SecurityException ex: + return (CreateError(403), ex); - case DecoderFallbackException ex: - return (CreateError(400, ex.Message), null); + case DecoderFallbackException ex: + return (CreateError(400, ex.Message), null); - case BadHttpRequestException ex: - return (CreateError(ex.StatusCode, ex.Message), null); + case BadHttpRequestException ex: + return (CreateError(ex.StatusCode, ex.Message), null); - default: - return (CreateError(500), exception); - } + default: + return (CreateError(500), exception); } + } - private static Exception? GetInner(Exception exception) - { - var current = exception; + private static Exception? GetInner(Exception exception) + { + var current = exception; - while (current != null) + while (current != null) + { + if (current is not DomainException) { - if (current is not DomainException) - { - return current; - } - - current = current.InnerException; + return current; } - return null; + current = current.InnerException; } - private static ErrorDto CreateError(int status, string? message = null, string? errorCode = null, IEnumerable<string>? details = null) - { - var error = new ErrorDto { StatusCode = status, Message = message }; - - if (!string.IsNullOrWhiteSpace(errorCode)) - { - error.ErrorCode = errorCode; - } + return null; + } - error.Details = details?.ToArray(); + private static ErrorDto CreateError(int status, string? message = null, string? errorCode = null, IEnumerable<string>? details = null) + { + var error = new ErrorDto { StatusCode = status, Message = message }; - return error; + if (!string.IsNullOrWhiteSpace(errorCode)) + { + error.ErrorCode = errorCode; } - public static IEnumerable<string> ToErrors(IEnumerable<ValidationError> errors) - { - static string FixPropertyName(string property) - { - property = property.Trim(); + error.Details = details?.ToArray(); - if (property.Length == 0) - { - return property; - } + return error; + } - var prevChar = 0; + public static IEnumerable<string> ToErrors(IEnumerable<ValidationError> errors) + { + static string FixPropertyName(string property) + { + property = property.Trim(); - var builder = new StringBuilder(property.Length); + if (property.Length == 0) + { + return property; + } - builder.Append(char.ToLowerInvariant(property[0])); + var prevChar = 0; - foreach (var character in property.Skip(1)) - { - if (prevChar == '.') - { - builder.Append(char.ToLowerInvariant(character)); - } - else - { - builder.Append(character); - } - - prevChar = character; - } + var builder = new StringBuilder(property.Length); - return builder.ToString(); - } + builder.Append(char.ToLowerInvariant(property[0])); - return errors.Select(e => + foreach (var character in property.Skip(1)) { - if (e.PropertyNames?.Any() == true) + if (prevChar == '.') { - return $"{string.Join(", ", e.PropertyNames.Select(FixPropertyName))}: {e.Message}"; + builder.Append(char.ToLowerInvariant(character)); } else { - return e.Message; + builder.Append(character); } - }); + + prevChar = character; + } + + return builder.ToString(); } + + return errors.Select(e => + { + if (e.PropertyNames?.Any() == true) + { + return $"{string.Join(", ", e.PropertyNames.Select(FixPropertyName))}: {e.Message}"; + } + else + { + return e.Message; + } + }); } } diff --git a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs index 95ed8a5a58..6db560fd7b 100644 --- a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs +++ b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs @@ -10,47 +10,46 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter, IAsyncActionFilter { - public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter, IAsyncActionFilter + public override Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { - public override Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + if (context.Result is ObjectResult { Value: ProblemDetails problem }) { - if (context.Result is ObjectResult { Value: ProblemDetails problem }) - { - var (error, _) = problem.ToErrorDto(context.HttpContext); - - context.Result = GetResult(error); - } + var (error, _) = problem.ToErrorDto(context.HttpContext); - return next(); + context.Result = GetResult(error); } - public void OnException(ExceptionContext context) - { - var (error, unhandled) = context.Exception.ToErrorDto(context.HttpContext); + return next(); + } - if (unhandled != null) - { - var log = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiExceptionFilterAttribute>>(); + public void OnException(ExceptionContext context) + { + var (error, unhandled) = context.Exception.ToErrorDto(context.HttpContext); - log.LogError(unhandled, "An unexpected exception has occurred."); - } + if (unhandled != null) + { + var log = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiExceptionFilterAttribute>>(); - context.Result = GetResult(error); + log.LogError(unhandled, "An unexpected exception has occurred."); } - private static IActionResult GetResult(ErrorDto error) + context.Result = GetResult(error); + } + + private static IActionResult GetResult(ErrorDto error) + { + if (error.StatusCode == 404) { - if (error.StatusCode == 404) - { - return new NotFoundResult(); - } - - return new ObjectResult(error) - { - StatusCode = error.StatusCode - }; + return new NotFoundResult(); } + + return new ObjectResult(error) + { + StatusCode = error.StatusCode + }; } } diff --git a/backend/src/Squidex.Web/ApiModelValidationAttribute.cs b/backend/src/Squidex.Web/ApiModelValidationAttribute.cs index 4f6abf1f7b..66fa7e5309 100644 --- a/backend/src/Squidex.Web/ApiModelValidationAttribute.cs +++ b/backend/src/Squidex.Web/ApiModelValidationAttribute.cs @@ -13,71 +13,70 @@ using Squidex.Infrastructure.Validation; using Squidex.Text; -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class ApiModelValidationAttribute : ActionFilterAttribute { - public sealed class ApiModelValidationAttribute : ActionFilterAttribute + private const string RequestBodyTooLarge = "Request body too large."; + private readonly bool allErrors; + + public ApiModelValidationAttribute(bool allErrors) { - private const string RequestBodyTooLarge = "Request body too large."; - private readonly bool allErrors; + this.allErrors = allErrors; + } - public ApiModelValidationAttribute(bool allErrors) + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context.ModelState.IsValid) { - this.allErrors = allErrors; + return; } - public override void OnActionExecuting(ActionExecutingContext context) - { - if (context.ModelState.IsValid) - { - return; - } + var errors = new List<ValidationError>(); - var errors = new List<ValidationError>(); - - foreach (var (key, value) in context.ModelState) + foreach (var (key, value) in context.ModelState) + { + if (value.ValidationState == ModelValidationState.Invalid) { - if (value.ValidationState == ModelValidationState.Invalid) + foreach (var error in value.Errors) { - foreach (var error in value.Errors) + if (error.ErrorMessage?.Contains(RequestBodyTooLarge, StringComparison.OrdinalIgnoreCase) == true) { - if (error.ErrorMessage?.Contains(RequestBodyTooLarge, StringComparison.OrdinalIgnoreCase) == true) - { - throw new BadHttpRequestException(error.ErrorMessage, 413); - } + throw new BadHttpRequestException(error.ErrorMessage, 413); } + } - if (string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrWhiteSpace(key)) + { + errors.Add(new ValidationError(T.Get("common.httpInvalidRequestFormat"))); + } + else + { + var properties = Array.Empty<string>(); + + if (!string.IsNullOrWhiteSpace(key)) { - errors.Add(new ValidationError(T.Get("common.httpInvalidRequestFormat"))); + properties = new[] { key.ToCamelCase() }; } - else - { - var properties = Array.Empty<string>(); - if (!string.IsNullOrWhiteSpace(key)) + foreach (var error in value.Errors) + { + if (!string.IsNullOrWhiteSpace(error.ErrorMessage) && allErrors) { - properties = new[] { key.ToCamelCase() }; + errors.Add(new ValidationError(error.ErrorMessage, properties)); } - - foreach (var error in value.Errors) + else if (error.Exception is JsonException jsonException) { - if (!string.IsNullOrWhiteSpace(error.ErrorMessage) && allErrors) - { - errors.Add(new ValidationError(error.ErrorMessage, properties)); - } - else if (error.Exception is JsonException jsonException) - { - errors.Add(new ValidationError(jsonException.Message)); - } + errors.Add(new ValidationError(jsonException.Message)); } } } } + } - if (errors.Count > 0) - { - throw new ValidationException(errors); - } + if (errors.Count > 0) + { + throw new ValidationException(errors); } } } diff --git a/backend/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs index 3ba03cdfa9..44f7596f70 100644 --- a/backend/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionAttribute.cs @@ -11,74 +11,73 @@ using Squidex.Infrastructure.Security; using Squidex.Shared; -namespace Squidex.Web +namespace Squidex.Web; + +public class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter { - public class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter - { - private readonly string[] permissionIds; + private readonly string[] permissionIds; - public IEnumerable<string> PermissionIds - { - get => permissionIds; - } + public IEnumerable<string> PermissionIds + { + get => permissionIds; + } - public ApiPermissionAttribute(params string[] ids) - { - AuthenticationSchemes = Constants.ApiSecurityScheme; + public ApiPermissionAttribute(params string[] ids) + { + AuthenticationSchemes = Constants.ApiSecurityScheme; - permissionIds = ids; - } + permissionIds = ids; + } - public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (permissionIds.Length > 0) { - if (permissionIds.Length > 0) - { - var permissions = context.HttpContext.Context().UserPermissions; + var permissions = context.HttpContext.Context().UserPermissions; - var hasPermission = false; + var hasPermission = false; - if (permissions != null) + if (permissions != null) + { + foreach (var id in permissionIds) { - foreach (var id in permissionIds) - { - var app = context.HttpContext.Features.Get<IAppFeature>()?.App.Name; + var app = context.HttpContext.Features.Get<IAppFeature>()?.App.Name; - if (string.IsNullOrWhiteSpace(app)) - { - app = Permission.Any; - } + if (string.IsNullOrWhiteSpace(app)) + { + app = Permission.Any; + } - var schema = context.HttpContext.Features.Get<ISchemaFeature>()?.Schema.SchemaDef.Name; + var schema = context.HttpContext.Features.Get<ISchemaFeature>()?.Schema.SchemaDef.Name; - if (string.IsNullOrWhiteSpace(schema)) - { - schema = Permission.Any; - } + if (string.IsNullOrWhiteSpace(schema)) + { + schema = Permission.Any; + } - var team = context.HttpContext.Features.Get<ITeamFeature>()?.Team.Id.ToString(); + var team = context.HttpContext.Features.Get<ITeamFeature>()?.Team.Id.ToString(); - if (string.IsNullOrWhiteSpace(team)) - { - team = Permission.Any; - } + if (string.IsNullOrWhiteSpace(team)) + { + team = Permission.Any; + } - if (permissions.Allows(id, app, schema, team)) - { - hasPermission = true; - break; - } + if (permissions.Allows(id, app, schema, team)) + { + hasPermission = true; + break; } } + } - if (!hasPermission) - { - context.Result = new StatusCodeResult(403); + if (!hasPermission) + { + context.Result = new StatusCodeResult(403); - return Task.CompletedTask; - } + return Task.CompletedTask; } - - return next(); } + + return next(); } } diff --git a/backend/src/Squidex.Web/ApiPermissionOrAnonymousAttribute.cs b/backend/src/Squidex.Web/ApiPermissionOrAnonymousAttribute.cs index 9175d9aa92..43df4c4f82 100644 --- a/backend/src/Squidex.Web/ApiPermissionOrAnonymousAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionOrAnonymousAttribute.cs @@ -7,13 +7,12 @@ using Microsoft.AspNetCore.Authorization; -namespace Squidex.Web +namespace Squidex.Web; + +public class ApiPermissionOrAnonymousAttribute : ApiPermissionAttribute, IAllowAnonymous { - public class ApiPermissionOrAnonymousAttribute : ApiPermissionAttribute, IAllowAnonymous + public ApiPermissionOrAnonymousAttribute(params string[] ids) + : base(ids) { - public ApiPermissionOrAnonymousAttribute(params string[] ids) - : base(ids) - { - } } } diff --git a/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs b/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs index dac4dc3123..bec968c427 100644 --- a/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs +++ b/backend/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs @@ -12,40 +12,39 @@ using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Assets; -namespace Squidex.Web +namespace Squidex.Web; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class AssetRequestSizeLimitAttribute : Attribute, IAuthorizationFilter, IRequestSizePolicy, IOrderedFilter { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public sealed class AssetRequestSizeLimitAttribute : Attribute, IAuthorizationFilter, IRequestSizePolicy, IOrderedFilter - { - public int Order { get; } = 900; + public int Order { get; } = 900; - public void OnAuthorization(AuthorizationFilterContext context) - { - var assetOptions = context.HttpContext.RequestServices.GetService<IOptions<AssetOptions>>(); + public void OnAuthorization(AuthorizationFilterContext context) + { + var assetOptions = context.HttpContext.RequestServices.GetService<IOptions<AssetOptions>>(); - var maxRequestBodySizeFeature = context.HttpContext.Features.Get<IHttpMaxRequestBodySizeFeature>(); + var maxRequestBodySizeFeature = context.HttpContext.Features.Get<IHttpMaxRequestBodySizeFeature>(); - if (maxRequestBodySizeFeature?.IsReadOnly == false) + if (maxRequestBodySizeFeature?.IsReadOnly == false) + { + if (assetOptions?.Value.MaxSize > 0) { - if (assetOptions?.Value.MaxSize > 0) - { - maxRequestBodySizeFeature.MaxRequestBodySize = assetOptions.Value.MaxSize; - } - else - { - maxRequestBodySizeFeature.MaxRequestBodySize = null; - } + maxRequestBodySizeFeature.MaxRequestBodySize = assetOptions.Value.MaxSize; } + else + { + maxRequestBodySizeFeature.MaxRequestBodySize = null; + } + } - if (assetOptions?.Value.MaxSize > 0) + if (assetOptions?.Value.MaxSize > 0) + { + var options = new FormOptions { - var options = new FormOptions - { - MultipartBodyLengthLimit = assetOptions.Value.MaxSize - }; + MultipartBodyLengthLimit = assetOptions.Value.MaxSize + }; - context.HttpContext.Features.Set<IFormFeature>(new FormFeature(context.HttpContext.Request, options)); - } + context.HttpContext.Features.Set<IFormFeature>(new FormFeature(context.HttpContext.Request, options)); } } } diff --git a/backend/src/Squidex.Web/ClearCookiesAttribute.cs b/backend/src/Squidex.Web/ClearCookiesAttribute.cs index 1df3cda2e4..e8167c529e 100644 --- a/backend/src/Squidex.Web/ClearCookiesAttribute.cs +++ b/backend/src/Squidex.Web/ClearCookiesAttribute.cs @@ -7,18 +7,17 @@ using Microsoft.AspNetCore.Mvc.Filters; -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class ClearCookiesAttribute : ActionFilterAttribute { - public sealed class ClearCookiesAttribute : ActionFilterAttribute + public override void OnActionExecuting(ActionExecutingContext context) { - public override void OnActionExecuting(ActionExecutingContext context) - { - var cookies = context.HttpContext.Response.Cookies; + var cookies = context.HttpContext.Response.Cookies; - foreach (var cookie in context.HttpContext.Request.Cookies.Keys) - { - cookies.Delete(cookie); - } + foreach (var cookie in context.HttpContext.Request.Cookies.Keys) + { + cookies.Delete(cookie); } } } diff --git a/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs index 844947f065..e1e4386d7f 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs @@ -12,57 +12,56 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public class ETagCommandMiddleware : ICommandMiddleware { - public class ETagCommandMiddleware : ICommandMiddleware + private readonly IHttpContextAccessor httpContextAccessor; + + public ETagCommandMiddleware(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { - private readonly IHttpContextAccessor httpContextAccessor; + var httpContext = httpContextAccessor.HttpContext; - public ETagCommandMiddleware(IHttpContextAccessor httpContextAccessor) + if (httpContext == null) { - this.httpContextAccessor = httpContextAccessor; + await next(context, ct); + return; } - public async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - var httpContext = httpContextAccessor.HttpContext; + var command = context.Command; - if (httpContext == null) + if (command.ExpectedVersion == EtagVersion.Auto) + { + if (httpContext.TryParseEtagVersion(HeaderNames.IfMatch, out var expectedVersion)) { - await next(context, ct); - return; + command.ExpectedVersion = expectedVersion; } - - var command = context.Command; - - if (command.ExpectedVersion == EtagVersion.Auto) + else { - if (httpContext.TryParseEtagVersion(HeaderNames.IfMatch, out var expectedVersion)) - { - command.ExpectedVersion = expectedVersion; - } - else - { - command.ExpectedVersion = EtagVersion.Any; - } + command.ExpectedVersion = EtagVersion.Any; } + } - await next(context, ct); + await next(context, ct); - if (context.PlainResult is CommandResult result) - { - SetResponsEtag(httpContext, result.NewVersion); - } - else if (context.PlainResult is IEntityWithVersion entity) - { - SetResponsEtag(httpContext, entity.Version); - } + if (context.PlainResult is CommandResult result) + { + SetResponsEtag(httpContext, result.NewVersion); } - - private static void SetResponsEtag(HttpContext httpContext, long version) + else if (context.PlainResult is IEntityWithVersion entity) { - httpContext.Response.Headers[HeaderNames.ETag] = version.ToString(CultureInfo.InvariantCulture); + SetResponsEtag(httpContext, entity.Version); } } + + private static void SetResponsEtag(HttpContext httpContext, long version) + { + httpContext.Response.Headers[HeaderNames.ETag] = version.ToString(CultureInfo.InvariantCulture); + } } diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs index 8fc6868979..88cec4598b 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs @@ -11,40 +11,39 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public class EnrichWithActorCommandMiddleware : ICommandMiddleware { - public class EnrichWithActorCommandMiddleware : ICommandMiddleware + private readonly IHttpContextAccessor httpContextAccessor; + + public EnrichWithActorCommandMiddleware(IHttpContextAccessor httpContextAccessor) { - private readonly IHttpContextAccessor httpContextAccessor; + this.httpContextAccessor = httpContextAccessor; + } - public EnrichWithActorCommandMiddleware(IHttpContextAccessor httpContextAccessor) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (httpContextAccessor.HttpContext == null) { - this.httpContextAccessor = httpContextAccessor; + return next(context, ct); } - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + if (context.Command is SquidexCommand squidexCommand) { - if (httpContextAccessor.HttpContext == null) - { - return next(context, ct); - } + var user = httpContextAccessor.HttpContext.User; - if (context.Command is SquidexCommand squidexCommand) + if (squidexCommand.Actor == null) { - var user = httpContextAccessor.HttpContext.User; - - if (squidexCommand.Actor == null) - { - var actorToken = user.Token(); - - squidexCommand.Actor = actorToken ?? throw new DomainForbiddenException("No actor with subject or client id available."); - } + var actorToken = user.Token(); - squidexCommand.User ??= httpContextAccessor.HttpContext.User; + squidexCommand.Actor = actorToken ?? throw new DomainForbiddenException("No actor with subject or client id available."); } - return next(context, ct); + squidexCommand.User ??= httpContextAccessor.HttpContext.User; } + + return next(context, ct); } } diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs index 7424eb2836..6eaebe17fa 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs @@ -10,41 +10,40 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public sealed class EnrichWithAppIdCommandMiddleware : ICommandMiddleware { - public sealed class EnrichWithAppIdCommandMiddleware : ICommandMiddleware - { - private readonly IContextProvider contextProvider; + private readonly IContextProvider contextProvider; - public EnrichWithAppIdCommandMiddleware(IContextProvider contextProvider) - { - this.contextProvider = contextProvider; - } + public EnrichWithAppIdCommandMiddleware(IContextProvider contextProvider) + { + this.contextProvider = contextProvider; + } - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is IAppCommand { AppId: null } appCommand) { - if (context.Command is IAppCommand { AppId: null } appCommand) - { - var appId = GetAppId(); + var appId = GetAppId(); - appCommand.AppId = appId; - } - - return next(context, ct); + appCommand.AppId = appId; } - private NamedId<DomainId> GetAppId() - { - var context = contextProvider.Context; + return next(context, ct); + } - if (context.App == null) - { - ThrowHelper.InvalidOperationException("Cannot resolve app."); - return default!; - } + private NamedId<DomainId> GetAppId() + { + var context = contextProvider.Context; - return context.App.NamedId(); + if (context.App == null) + { + ThrowHelper.InvalidOperationException("Cannot resolve app."); + return default!; } + + return context.App.NamedId(); } } diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs index 7db7646af8..7e821d6e8f 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs @@ -8,24 +8,23 @@ using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure.Commands; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public sealed class EnrichWithContentIdCommandMiddleware : ICommandMiddleware { - public sealed class EnrichWithContentIdCommandMiddleware : ICommandMiddleware - { - private const string SingletonId = "_schemaId_"; + private const string SingletonId = "_schemaId_"; - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is ContentCommand contentCommand and not CreateContent) { - if (context.Command is ContentCommand contentCommand and not CreateContent) + if (contentCommand.ContentId.ToString().Equals(SingletonId, StringComparison.Ordinal)) { - if (contentCommand.ContentId.ToString().Equals(SingletonId, StringComparison.Ordinal)) - { - contentCommand.ContentId = contentCommand.SchemaId.Id; - } + contentCommand.ContentId = contentCommand.SchemaId.Id; } - - return next(context, ct); } + + return next(context, ct); } } diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs index a3f41bd17e..af98e87d82 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -11,46 +11,45 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware { - public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware + private readonly IHttpContextAccessor httpContextAccessor; + + public EnrichWithSchemaIdCommandMiddleware(IHttpContextAccessor httpContextAccessor) { - private readonly IHttpContextAccessor httpContextAccessor; + this.httpContextAccessor = httpContextAccessor; + } - public EnrichWithSchemaIdCommandMiddleware(IHttpContextAccessor httpContextAccessor) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (httpContextAccessor.HttpContext == null) { - this.httpContextAccessor = httpContextAccessor; + return next(context, ct); } - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + if (context.Command is ISchemaCommand { SchemaId: null } schemaCommand) { - if (httpContextAccessor.HttpContext == null) - { - return next(context, ct); - } - - if (context.Command is ISchemaCommand { SchemaId: null } schemaCommand) - { - var schemaId = GetSchemaId(); - - schemaCommand.SchemaId = schemaId; - } + var schemaId = GetSchemaId(); - return next(context, ct); + schemaCommand.SchemaId = schemaId; } - private NamedId<DomainId> GetSchemaId() - { - var feature = httpContextAccessor.HttpContext?.Features.Get<ISchemaFeature>(); + return next(context, ct); + } - if (feature == null) - { - ThrowHelper.InvalidOperationException("Cannot resolve schema."); - return default!; - } + private NamedId<DomainId> GetSchemaId() + { + var feature = httpContextAccessor.HttpContext?.Features.Get<ISchemaFeature>(); - return feature.Schema.NamedId(); + if (feature == null) + { + ThrowHelper.InvalidOperationException("Cannot resolve schema."); + return default!; } + + return feature.Schema.NamedId(); } } diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithTeamIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithTeamIdCommandMiddleware.cs index c74ad539c6..3b9f909bd0 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithTeamIdCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithTeamIdCommandMiddleware.cs @@ -10,46 +10,45 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public sealed class EnrichWithTeamIdCommandMiddleware : ICommandMiddleware { - public sealed class EnrichWithTeamIdCommandMiddleware : ICommandMiddleware + private readonly IHttpContextAccessor httpContextAccessor; + + public EnrichWithTeamIdCommandMiddleware(IHttpContextAccessor httpContextAccessor) { - private readonly IHttpContextAccessor httpContextAccessor; + this.httpContextAccessor = httpContextAccessor; + } - public EnrichWithTeamIdCommandMiddleware(IHttpContextAccessor httpContextAccessor) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (httpContextAccessor.HttpContext == null) { - this.httpContextAccessor = httpContextAccessor; + return next(context, ct); } - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) + if (context.Command is ITeamCommand teamCommand && teamCommand.TeamId == default) { - if (httpContextAccessor.HttpContext == null) - { - return next(context, ct); - } - - if (context.Command is ITeamCommand teamCommand && teamCommand.TeamId == default) - { - var teamId = GetTeamId(); - - teamCommand.TeamId = teamId; - } + var teamId = GetTeamId(); - return next(context, ct); + teamCommand.TeamId = teamId; } - private DomainId GetTeamId() - { - var feature = httpContextAccessor.HttpContext?.Features.Get<ITeamFeature>(); + return next(context, ct); + } - if (feature == null) - { - ThrowHelper.InvalidOperationException("Cannot resolve team."); - return default!; - } + private DomainId GetTeamId() + { + var feature = httpContextAccessor.HttpContext?.Features.Get<ITeamFeature>(); - return feature.Team.Id; + if (feature == null) + { + ThrowHelper.InvalidOperationException("Cannot resolve team."); + return default!; } + + return feature.Team.Id; } } diff --git a/backend/src/Squidex.Web/Constants.cs b/backend/src/Squidex.Web/Constants.cs index 5c96007407..ce369cc007 100644 --- a/backend/src/Squidex.Web/Constants.cs +++ b/backend/src/Squidex.Web/Constants.cs @@ -8,30 +8,29 @@ using Squidex.Infrastructure; using Squidex.Shared; -namespace Squidex.Web +namespace Squidex.Web; + +public static class Constants { - public static class Constants - { - public const string SecurityDefinition = "squidex-oauth-auth"; + public const string SecurityDefinition = "squidex-oauth-auth"; - public const string ApiSecurityScheme = "API"; + public const string ApiSecurityScheme = "API"; - public const string PrefixApi = "/api"; + public const string PrefixApi = "/api"; - public const string PrefixIdentityServer = "/identity-server"; + public const string PrefixIdentityServer = "/identity-server"; - public const string ScopePermissions = "permissions"; + public const string ScopePermissions = "permissions"; - public const string ScopeProfile = "squidex-profile"; + public const string ScopeProfile = "squidex-profile"; - public const string ScopeRole = "role"; + public const string ScopeRole = "role"; - public const string ScopeApi = "squidex-api"; + public const string ScopeApi = "squidex-api"; - public static readonly string ClientFrontendId = DefaultClients.Frontend; + public static readonly string ClientFrontendId = DefaultClients.Frontend; - public static readonly string ClientInternalId = "squidex-internal"; + public static readonly string ClientInternalId = "squidex-internal"; - public static readonly string ClientInternalSecret = "squidex-internal".ToSha256Base64(); - } + public static readonly string ClientInternalSecret = "squidex-internal".ToSha256Base64(); } diff --git a/backend/src/Squidex.Web/ContextExtensions.cs b/backend/src/Squidex.Web/ContextExtensions.cs index be499e1bec..0a9b6e32ac 100644 --- a/backend/src/Squidex.Web/ContextExtensions.cs +++ b/backend/src/Squidex.Web/ContextExtensions.cs @@ -8,36 +8,35 @@ using Microsoft.AspNetCore.Http; using RequestContext = Squidex.Domain.Apps.Entities.Context; -namespace Squidex.Web +namespace Squidex.Web; + +public static class ContextExtensions { - public static class ContextExtensions + public static RequestContext Context(this HttpContext httpContext) { - public static RequestContext Context(this HttpContext httpContext) - { - var context = httpContext.Features.Get<RequestContext>(); - - if (context == null) - { - context = new RequestContext(httpContext.User, null!).WithHeaders(httpContext); + var context = httpContext.Features.Get<RequestContext>(); - httpContext.Features.Set(context); - } + if (context == null) + { + context = new RequestContext(httpContext.User, null!).WithHeaders(httpContext); - return context; + httpContext.Features.Set(context); } - public static RequestContext WithHeaders(this RequestContext context, HttpContext httpContext) + return context; + } + + public static RequestContext WithHeaders(this RequestContext context, HttpContext httpContext) + { + return context.Clone(builder => { - return context.Clone(builder => + foreach (var (key, value) in httpContext.Request.Headers) { - foreach (var (key, value) in httpContext.Request.Headers) + if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) { - if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) - { - builder.SetHeader(key, value.ToString()); - } + builder.SetHeader(key, value.ToString()); } - }); - } + } + }); } } diff --git a/backend/src/Squidex.Web/ContextProvider.cs b/backend/src/Squidex.Web/ContextProvider.cs index f64ca2d6ba..79fe4c9382 100644 --- a/backend/src/Squidex.Web/ContextProvider.cs +++ b/backend/src/Squidex.Web/ContextProvider.cs @@ -8,34 +8,33 @@ using Microsoft.AspNetCore.Http; using Squidex.Domain.Apps.Entities; -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class ContextProvider : IContextProvider { - public sealed class ContextProvider : IContextProvider - { - private readonly IHttpContextAccessor httpContextAccessor; - private readonly AsyncLocal<Context> asyncLocal = new AsyncLocal<Context>(); + private readonly IHttpContextAccessor httpContextAccessor; + private readonly AsyncLocal<Context> asyncLocal = new AsyncLocal<Context>(); - public Context Context + public Context Context + { + get { - get + if (httpContextAccessor.HttpContext == null) { - if (httpContextAccessor.HttpContext == null) + if (asyncLocal.Value == null) { - if (asyncLocal.Value == null) - { - asyncLocal.Value = Context.Anonymous(null!); - } - - return asyncLocal.Value; + asyncLocal.Value = Context.Anonymous(null!); } - return httpContextAccessor.HttpContext.Context(); + return asyncLocal.Value; } - } - public ContextProvider(IHttpContextAccessor httpContextAccessor) - { - this.httpContextAccessor = httpContextAccessor; + return httpContextAccessor.HttpContext.Context(); } } + + public ContextProvider(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } } diff --git a/backend/src/Squidex.Web/Deferred.cs b/backend/src/Squidex.Web/Deferred.cs index b185a9d4c4..3b82708528 100644 --- a/backend/src/Squidex.Web/Deferred.cs +++ b/backend/src/Squidex.Web/Deferred.cs @@ -7,34 +7,33 @@ using Squidex.Infrastructure; -namespace Squidex.Web +namespace Squidex.Web; + +public readonly struct Deferred { - public readonly struct Deferred - { - private readonly Lazy<Task<object>> value; + private readonly Lazy<Task<object>> value; - public Task<object> Value - { - get => value.Value; - } + public Task<object> Value + { + get => value.Value; + } - private Deferred(Func<Task<object>> value) - { - this.value = new Lazy<Task<object>>(value); - } + private Deferred(Func<Task<object>> value) + { + this.value = new Lazy<Task<object>>(value); + } - public static Deferred Response(Func<object> factory) - { - Guard.NotNull(factory); + public static Deferred Response(Func<object> factory) + { + Guard.NotNull(factory); - return new Deferred(() => Task.FromResult(factory())); - } + return new Deferred(() => Task.FromResult(factory())); + } - public static Deferred AsyncResponse<T>(Func<Task<T>> factory) - { - Guard.NotNull(factory); + public static Deferred AsyncResponse<T>(Func<Task<T>> factory) + { + Guard.NotNull(factory); - return new Deferred(async () => (await factory())!); - } + return new Deferred(async () => (await factory())!); } } diff --git a/backend/src/Squidex.Web/ETagExtensions.cs b/backend/src/Squidex.Web/ETagExtensions.cs index 061c79bb60..b1e2e423b5 100644 --- a/backend/src/Squidex.Web/ETagExtensions.cs +++ b/backend/src/Squidex.Web/ETagExtensions.cs @@ -12,74 +12,73 @@ using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; -namespace Squidex.Web +namespace Squidex.Web; + +public static class ETagExtensions { - public static class ETagExtensions + public static string ToEtag<T>(this IReadOnlyList<T> items) where T : IEntity, IEntityWithVersion { - public static string ToEtag<T>(this IReadOnlyList<T> items) where T : IEntity, IEntityWithVersion + using (Telemetry.Activities.StartActivity("CalculateEtag")) { - using (Telemetry.Activities.StartActivity("CalculateEtag")) - { - var hash = Create(items, 0); + var hash = Create(items, 0); - return hash; - } + return hash; } + } - public static string ToEtag<T>(this IResultList<T> entities) where T : IEntity, IEntityWithVersion + public static string ToEtag<T>(this IResultList<T> entities) where T : IEntity, IEntityWithVersion + { + using (Telemetry.Activities.StartActivity("CalculateEtag")) { - using (Telemetry.Activities.StartActivity("CalculateEtag")) - { - var hash = Create(entities, entities.Total); + var hash = Create(entities, entities.Total); - return hash; - } + return hash; } + } - private static string Create<T>(IReadOnlyList<T> entities, long total) where T : IEntity, IEntityWithVersion + private static string Create<T>(IReadOnlyList<T> entities, long total) where T : IEntity, IEntityWithVersion + { + using (var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256)) { - using (var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256)) - { - hasher.AppendData(BitConverter.GetBytes(total)); + hasher.AppendData(BitConverter.GetBytes(total)); - foreach (var item in entities) - { - hasher.AppendData(Encoding.Default.GetBytes(item.UniqueId.ToString())); - hasher.AppendData(BitConverter.GetBytes(item.Version)); - } + foreach (var item in entities) + { + hasher.AppendData(Encoding.Default.GetBytes(item.UniqueId.ToString())); + hasher.AppendData(BitConverter.GetBytes(item.Version)); + } - var cacheBuffer = hasher.GetHashAndReset(); - var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty, StringComparison.Ordinal).ToUpperInvariant(); + var cacheBuffer = hasher.GetHashAndReset(); + var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty, StringComparison.Ordinal).ToUpperInvariant(); - return cacheString; - } + return cacheString; } + } - public static string ToEtag<T>(this T entity) where T : IEntity, IEntityWithVersion - { - return entity.Version.ToString(CultureInfo.InvariantCulture); - } + public static string ToEtag<T>(this T entity) where T : IEntity, IEntityWithVersion + { + return entity.Version.ToString(CultureInfo.InvariantCulture); + } + + public static bool TryParseEtagVersion(this HttpContext httpContext, string header, out long version) + { + version = default; - public static bool TryParseEtagVersion(this HttpContext httpContext, string header, out long version) + if (httpContext.Request.Headers.TryGetString(header, out var etag)) { - version = default; + var span = etag.AsSpan(); - if (httpContext.Request.Headers.TryGetString(header, out var etag)) + if (span.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) { - var span = etag.AsSpan(); - - if (span.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) - { - span = span[2..]; - } - - if (long.TryParse(span, NumberStyles.Any, CultureInfo.InvariantCulture, out version)) - { - return true; - } + span = span[2..]; } - return false; + if (long.TryParse(span, NumberStyles.Any, CultureInfo.InvariantCulture, out version)) + { + return true; + } } + + return false; } } diff --git a/backend/src/Squidex.Web/ETagUtils.cs b/backend/src/Squidex.Web/ETagUtils.cs index 867d39f4b9..cadad5a56b 100644 --- a/backend/src/Squidex.Web/ETagUtils.cs +++ b/backend/src/Squidex.Web/ETagUtils.cs @@ -5,55 +5,54 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Web +namespace Squidex.Web; + +public static class ETagUtils { - public static class ETagUtils + public static string ToWeakEtag(string? etag) { - public static string ToWeakEtag(string? etag) - { - return $"W/{etag}"; - } + return $"W/{etag}"; + } - public static bool IsStrongEtag(string etag) - { - return !IsWeakEtag(etag.AsSpan()); - } + public static bool IsStrongEtag(string etag) + { + return !IsWeakEtag(etag.AsSpan()); + } - public static bool IsWeakEtag(string etag) - { - return IsWeakEtag(etag.AsSpan()); - } + public static bool IsWeakEtag(string etag) + { + return IsWeakEtag(etag.AsSpan()); + } - public static bool IsWeakEtag(ReadOnlySpan<char> etag) + public static bool IsWeakEtag(ReadOnlySpan<char> etag) + { + return etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase); + } + + public static bool IsSameEtag(string lhs, string rhs) + { + return IsSameEtag(lhs.AsSpan(), rhs.AsSpan()); + } + + public static bool IsSameEtag(ReadOnlySpan<char> lhs, ReadOnlySpan<char> rhs) + { + var isMatch = lhs.Equals(rhs, StringComparison.Ordinal); + + if (isMatch) { - return etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase); + return true; } - public static bool IsSameEtag(string lhs, string rhs) + if (lhs.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) { - return IsSameEtag(lhs.AsSpan(), rhs.AsSpan()); + lhs = lhs[2..]; } - public static bool IsSameEtag(ReadOnlySpan<char> lhs, ReadOnlySpan<char> rhs) + if (rhs.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) { - var isMatch = lhs.Equals(rhs, StringComparison.Ordinal); - - if (isMatch) - { - return true; - } - - if (lhs.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) - { - lhs = lhs[2..]; - } - - if (rhs.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) - { - rhs = rhs[2..]; - } - - return lhs.Equals(rhs, StringComparison.Ordinal); + rhs = rhs[2..]; } + + return lhs.Equals(rhs, StringComparison.Ordinal); } } diff --git a/backend/src/Squidex.Web/ErrorDto.cs b/backend/src/Squidex.Web/ErrorDto.cs index d517348bce..d065b0c691 100644 --- a/backend/src/Squidex.Web/ErrorDto.cs +++ b/backend/src/Squidex.Web/ErrorDto.cs @@ -8,27 +8,26 @@ using System.ComponentModel.DataAnnotations; using Squidex.Infrastructure.Validation; -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class ErrorDto { - public sealed class ErrorDto - { - [LocalizedRequired] - [Display(Description = "Error message.")] - public string? Message { get; set; } + [LocalizedRequired] + [Display(Description = "Error message.")] + public string? Message { get; set; } - [Display(Description = "The error code.")] - public string? ErrorCode { get; set; } + [Display(Description = "The error code.")] + public string? ErrorCode { get; set; } - [Display(Description = "The optional trace id.")] - public string? TraceId { get; set; } + [Display(Description = "The optional trace id.")] + public string? TraceId { get; set; } - [Display(Description = "Link to the error details.")] - public string? Type { get; set; } + [Display(Description = "Link to the error details.")] + public string? Type { get; set; } - [Display(Description = "Detailed error messages.")] - public string[]? Details { get; set; } + [Display(Description = "Detailed error messages.")] + public string[]? Details { get; set; } - [Display(Description = "Status code of the http response.")] - public int StatusCode { get; set; } = 400; - } + [Display(Description = "Status code of the http response.")] + public int StatusCode { get; set; } = 400; } diff --git a/backend/src/Squidex.Web/ExposedConfiguration.cs b/backend/src/Squidex.Web/ExposedConfiguration.cs index 5b62aa2b16..232c6cf8a6 100644 --- a/backend/src/Squidex.Web/ExposedConfiguration.cs +++ b/backend/src/Squidex.Web/ExposedConfiguration.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class ExposedConfiguration : Dictionary<string, string> { - public sealed class ExposedConfiguration : Dictionary<string, string> - { - } } diff --git a/backend/src/Squidex.Web/ExposedValues.cs b/backend/src/Squidex.Web/ExposedValues.cs index 64b80d0af2..a08c0fca94 100644 --- a/backend/src/Squidex.Web/ExposedValues.cs +++ b/backend/src/Squidex.Web/ExposedValues.cs @@ -10,51 +10,50 @@ using Microsoft.Extensions.Configuration; using Squidex.Infrastructure; -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class ExposedValues : Dictionary<string, string> { - public sealed class ExposedValues : Dictionary<string, string> + public ExposedValues() { - public ExposedValues() - { - } - - public ExposedValues(ExposedConfiguration configured, IConfiguration configuration, Assembly? assembly = null) - { - Guard.NotNull(configured); - Guard.NotNull(configuration); + } - foreach (var kvp in configured) - { - var value = configuration.GetValue<string>(kvp.Value); + public ExposedValues(ExposedConfiguration configured, IConfiguration configuration, Assembly? assembly = null) + { + Guard.NotNull(configured); + Guard.NotNull(configuration); - if (!string.IsNullOrWhiteSpace(value)) - { - this[kvp.Key] = value; - } - } + foreach (var kvp in configured) + { + var value = configuration.GetValue<string>(kvp.Value); - if (assembly != null) + if (!string.IsNullOrWhiteSpace(value)) { - if (!ContainsKey("version")) - { - this["version"] = assembly.GetName()!.Version!.ToString(); - } + this[kvp.Key] = value; } } - public override string ToString() + if (assembly != null) { - var sb = new StringBuilder(); - - foreach (var (key, value) in this) + if (!ContainsKey("version")) { - sb.AppendIfNotEmpty(", "); - sb.Append(key); - sb.Append(": "); - sb.Append(value); + this["version"] = assembly.GetName()!.Version!.ToString(); } + } + } - return sb.ToString(); + public override string ToString() + { + var sb = new StringBuilder(); + + foreach (var (key, value) in this) + { + sb.AppendIfNotEmpty(", "); + sb.Append(key); + sb.Append(": "); + sb.Append(value); } + + return sb.ToString(); } } diff --git a/backend/src/Squidex.Web/Extensions.cs b/backend/src/Squidex.Web/Extensions.cs index aeaa8d5147..d7b171cf40 100644 --- a/backend/src/Squidex.Web/Extensions.cs +++ b/backend/src/Squidex.Web/Extensions.cs @@ -11,67 +11,66 @@ using Microsoft.Extensions.Primitives; using Squidex.Infrastructure.Security; -namespace Squidex.Web +namespace Squidex.Web; + +public static class Extensions { - public static class Extensions + public static string? GetClientId(this ClaimsPrincipal principal) { - public static string? GetClientId(this ClaimsPrincipal principal) - { - var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; + var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; - return clientId?.GetClientParts().ClientId; - } + return clientId?.GetClientParts().ClientId; + } - public static (string? App, string? ClientId) GetClient(this ClaimsPrincipal principal) - { - var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; + public static (string? App, string? ClientId) GetClient(this ClaimsPrincipal principal) + { + var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; - return clientId.GetClientParts(); - } + return clientId.GetClientParts(); + } - public static (string? App, string? ClientId) GetClientParts(this string? clientId) + public static (string? App, string? ClientId) GetClientParts(this string? clientId) + { + if (clientId == null) { - if (clientId == null) - { - return (null, null); - } - - var parts = clientId.Split(':', '~'); - - if (parts.Length == 1) - { - return (null, parts[0]); - } - - if (parts.Length == 2) - { - return (parts[0], parts[1]); - } - return (null, null); } - public static bool IsUser(this ApiController controller, string userId) - { - var subject = controller.User.OpenIdSubject(); + var parts = clientId.Split(':', '~'); - return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); + if (parts.Length == 1) + { + return (null, parts[0]); } - public static bool TryGetString(this IHeaderDictionary headers, string header, [MaybeNullWhen(false)] out string result) + if (parts.Length == 2) { - result = null!; + return (parts[0], parts[1]); + } + + return (null, null); + } + + public static bool IsUser(this ApiController controller, string userId) + { + var subject = controller.User.OpenIdSubject(); + + return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); + } + + public static bool TryGetString(this IHeaderDictionary headers, string header, [MaybeNullWhen(false)] out string result) + { + result = null!; - if (headers.TryGetValue(header, out var value) && value != StringValues.Empty) + if (headers.TryGetValue(header, out var value) && value != StringValues.Empty) + { + if (!string.IsNullOrWhiteSpace(value)) { - if (!string.IsNullOrWhiteSpace(value)) - { - result = value; - return true; - } + result = value; + return true; } - - return false; } + + return false; } } diff --git a/backend/src/Squidex.Web/FileCallbackResult.cs b/backend/src/Squidex.Web/FileCallbackResult.cs index 9dda3b64dc..762ca3bdb0 100644 --- a/backend/src/Squidex.Web/FileCallbackResult.cs +++ b/backend/src/Squidex.Web/FileCallbackResult.cs @@ -13,34 +13,33 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Web -{ - public delegate Task FileCallback(Stream body, BytesRange range, - CancellationToken ct); +namespace Squidex.Web; - public sealed class FileCallbackResult : FileResult - { - public bool ErrorAs404 { get; set; } +public delegate Task FileCallback(Stream body, BytesRange range, + CancellationToken ct); - public bool SendInline { get; set; } +public sealed class FileCallbackResult : FileResult +{ + public bool ErrorAs404 { get; set; } - public long? FileSize { get; set; } + public bool SendInline { get; set; } - public FileCallback Callback { get; } + public long? FileSize { get; set; } - public FileCallbackResult(string contentType, FileCallback callback) - : base(contentType) - { - Guard.NotNull(callback); + public FileCallback Callback { get; } - Callback = callback; - } + public FileCallbackResult(string contentType, FileCallback callback) + : base(contentType) + { + Guard.NotNull(callback); - public override Task ExecuteResultAsync(ActionContext context) - { - var executor = context.HttpContext.RequestServices.GetRequiredService<FileCallbackResultExecutor>(); + Callback = callback; + } + + public override Task ExecuteResultAsync(ActionContext context) + { + var executor = context.HttpContext.RequestServices.GetRequiredService<FileCallbackResultExecutor>(); - return executor.ExecuteAsync(context, this); - } + return executor.ExecuteAsync(context, this); } } diff --git a/backend/src/Squidex.Web/FileExtensions.cs b/backend/src/Squidex.Web/FileExtensions.cs index 5c78edda61..2d65ba67bd 100644 --- a/backend/src/Squidex.Web/FileExtensions.cs +++ b/backend/src/Squidex.Web/FileExtensions.cs @@ -10,23 +10,22 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Web +namespace Squidex.Web; + +public static class FileExtensions { - public static class FileExtensions + public static AssetFile ToAssetFile(this IFormFile formFile) { - public static AssetFile ToAssetFile(this IFormFile formFile) + if (string.IsNullOrWhiteSpace(formFile.ContentType)) { - if (string.IsNullOrWhiteSpace(formFile.ContentType)) - { - throw new ValidationException(T.Get("common.httpContentTypeNotDefined")); - } - - if (string.IsNullOrWhiteSpace(formFile.FileName)) - { - throw new ValidationException(T.Get("common.httpFileNameNotDefined")); - } + throw new ValidationException(T.Get("common.httpContentTypeNotDefined")); + } - return new DelegateAssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream); + if (string.IsNullOrWhiteSpace(formFile.FileName)) + { + throw new ValidationException(T.Get("common.httpFileNameNotDefined")); } + + return new DelegateAssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream); } } diff --git a/backend/src/Squidex.Web/GraphQL/DummySchema.cs b/backend/src/Squidex.Web/GraphQL/DummySchema.cs index 9c18613cdc..e78b182656 100644 --- a/backend/src/Squidex.Web/GraphQL/DummySchema.cs +++ b/backend/src/Squidex.Web/GraphQL/DummySchema.cs @@ -7,13 +7,12 @@ using GraphQL.Types; -namespace Squidex.Web.GraphQL +namespace Squidex.Web.GraphQL; + +public sealed class DummySchema : Schema { - public sealed class DummySchema : Schema + public DummySchema() { - public DummySchema() - { - Query = new ObjectGraphType(); - } + Query = new ObjectGraphType(); } } diff --git a/backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs b/backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs index 968592ebd5..7c68db9ce2 100644 --- a/backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs +++ b/backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs @@ -11,17 +11,16 @@ using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents.GraphQL; -namespace Squidex.Web.GraphQL +namespace Squidex.Web.GraphQL; + +public sealed class DynamicUserContextBuilder : IUserContextBuilder { - public sealed class DynamicUserContextBuilder : IUserContextBuilder - { - private readonly ObjectFactory factory = ActivatorUtilities.CreateFactory(typeof(GraphQLExecutionContext), new[] { typeof(Context) }); + private readonly ObjectFactory factory = ActivatorUtilities.CreateFactory(typeof(GraphQLExecutionContext), new[] { typeof(Context) }); - public ValueTask<IDictionary<string, object?>?> BuildUserContextAsync(HttpContext context, object? payload) - { - var executionContext = (GraphQLExecutionContext)factory(context.RequestServices, new object[] { context.Context() }); + public ValueTask<IDictionary<string, object?>?> BuildUserContextAsync(HttpContext context, object? payload) + { + var executionContext = (GraphQLExecutionContext)factory(context.RequestServices, new object[] { context.Context() }); - return new ValueTask<IDictionary<string, object?>?>(executionContext); - } + return new ValueTask<IDictionary<string, object?>?>(executionContext); } } diff --git a/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs b/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs index 2f9d79989a..27e211f196 100644 --- a/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs +++ b/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs @@ -10,27 +10,26 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; -namespace Squidex.Web.GraphQL +namespace Squidex.Web.GraphQL; + +public sealed class GraphQLRunner { - public sealed class GraphQLRunner + private readonly GraphQLHttpMiddleware<DummySchema> middleware; + + public GraphQLRunner(IServiceProvider serviceProvider) { - private readonly GraphQLHttpMiddleware<DummySchema> middleware; + RequestDelegate next = x => Task.CompletedTask; - public GraphQLRunner(IServiceProvider serviceProvider) + var options = new GraphQLHttpMiddlewareOptions { - RequestDelegate next = x => Task.CompletedTask; - - var options = new GraphQLHttpMiddlewareOptions - { - DefaultResponseContentType = new MediaTypeHeaderValue("application/json") - }; + DefaultResponseContentType = new MediaTypeHeaderValue("application/json") + }; - middleware = ActivatorUtilities.CreateInstance<GraphQLHttpMiddleware<DummySchema>>(serviceProvider, next, options); - } + middleware = ActivatorUtilities.CreateInstance<GraphQLHttpMiddleware<DummySchema>>(serviceProvider, next, options); + } - public Task InvokeAsync(HttpContext context) - { - return middleware.InvokeAsync(context); - } + public Task InvokeAsync(HttpContext context) + { + return middleware.InvokeAsync(context); } } diff --git a/backend/src/Squidex.Web/IApiCostsFeature.cs b/backend/src/Squidex.Web/IApiCostsFeature.cs index b0cbcee8bf..784abd88d6 100644 --- a/backend/src/Squidex.Web/IApiCostsFeature.cs +++ b/backend/src/Squidex.Web/IApiCostsFeature.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Web +namespace Squidex.Web; + +public interface IApiCostsFeature { - public interface IApiCostsFeature - { - double Costs { get; } - } + double Costs { get; } } \ No newline at end of file diff --git a/backend/src/Squidex.Web/IAppFeature.cs b/backend/src/Squidex.Web/IAppFeature.cs index 77563cf70d..ae21db4c96 100644 --- a/backend/src/Squidex.Web/IAppFeature.cs +++ b/backend/src/Squidex.Web/IAppFeature.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Entities.Apps; -namespace Squidex.Web +namespace Squidex.Web; + +public interface IAppFeature { - public interface IAppFeature - { - IAppEntity App { get; } - } + IAppEntity App { get; } } diff --git a/backend/src/Squidex.Web/ISchemaFeature.cs b/backend/src/Squidex.Web/ISchemaFeature.cs index d3a539e90a..6bd785fb66 100644 --- a/backend/src/Squidex.Web/ISchemaFeature.cs +++ b/backend/src/Squidex.Web/ISchemaFeature.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Entities.Schemas; -namespace Squidex.Web +namespace Squidex.Web; + +public interface ISchemaFeature { - public interface ISchemaFeature - { - ISchemaEntity Schema { get; } - } + ISchemaEntity Schema { get; } } diff --git a/backend/src/Squidex.Web/ITeamFeature.cs b/backend/src/Squidex.Web/ITeamFeature.cs index aff13155b8..49bf190ed5 100644 --- a/backend/src/Squidex.Web/ITeamFeature.cs +++ b/backend/src/Squidex.Web/ITeamFeature.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Entities.Teams; -namespace Squidex.Web +namespace Squidex.Web; + +public interface ITeamFeature { - public interface ITeamFeature - { - ITeamEntity Team { get; } - } + ITeamEntity Team { get; } } diff --git a/backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs b/backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs index 81c3f90345..49615d7bc6 100644 --- a/backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs +++ b/backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs @@ -12,98 +12,97 @@ #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Web.Json +namespace Squidex.Web.Json; + +public class JsonInheritanceConverter<T> : InheritanceConverterBase<T> where T : notnull { - public class JsonInheritanceConverter<T> : InheritanceConverterBase<T> where T : notnull + private static readonly Lazy<Dictionary<string, Type>> DefaultMapping = new Lazy<Dictionary<string, Type>>(() => { - private static readonly Lazy<Dictionary<string, Type>> DefaultMapping = new Lazy<Dictionary<string, Type>>(() => - { - var baseName = typeof(T).Name; - - var result = new Dictionary<string, Type>(); + var baseName = typeof(T).Name; - void AddType(Type type) - { - var typeName = type.Name; + var result = new Dictionary<string, Type>(); - if (typeName.EndsWith(baseName, StringComparison.CurrentCulture)) - { - typeName = typeName[..^baseName.Length]; - } + void AddType(Type type) + { + var typeName = type.Name; - result[typeName] = type; + if (typeName.EndsWith(baseName, StringComparison.CurrentCulture)) + { + typeName = typeName[..^baseName.Length]; } - foreach (var attribute in typeof(T).GetCustomAttributes<KnownTypeAttribute>()) + result[typeName] = type; + } + + foreach (var attribute in typeof(T).GetCustomAttributes<KnownTypeAttribute>()) + { + if (attribute.Type != null) { - if (attribute.Type != null) + if (!attribute.Type.IsAbstract) { - if (!attribute.Type.IsAbstract) - { - AddType(attribute.Type); - } + AddType(attribute.Type); } - else if (!string.IsNullOrWhiteSpace(attribute.MethodName)) + } + else if (!string.IsNullOrWhiteSpace(attribute.MethodName)) + { + var method = typeof(T).GetMethod(attribute.MethodName); + + if (method != null && method.IsStatic) { - var method = typeof(T).GetMethod(attribute.MethodName); + var types = (IEnumerable<Type>)method.Invoke(null, Array.Empty<object>())!; - if (method != null && method.IsStatic) + foreach (var type in types) { - var types = (IEnumerable<Type>)method.Invoke(null, Array.Empty<object>())!; - - foreach (var type in types) + if (!type.IsAbstract) { - if (!type.IsAbstract) - { - AddType(type); - } + AddType(type); } } } } + } - return result; - }); + return result; + }); - private readonly IReadOnlyDictionary<string, Type> mapping; + private readonly IReadOnlyDictionary<string, Type> mapping; - public JsonInheritanceConverter() - : this(null, DefaultMapping.Value) - { - } + public JsonInheritanceConverter() + : this(null, DefaultMapping.Value) + { + } - public JsonInheritanceConverter(string? discriminatorName) - : this(discriminatorName, DefaultMapping.Value) - { - } + public JsonInheritanceConverter(string? discriminatorName) + : this(discriminatorName, DefaultMapping.Value) + { + } - public JsonInheritanceConverter(string? discriminatorName, IReadOnlyDictionary<string, Type> mapping) - : base(GetDiscriminatorName(discriminatorName)) - { - this.mapping = mapping ?? DefaultMapping.Value; - } + public JsonInheritanceConverter(string? discriminatorName, IReadOnlyDictionary<string, Type> mapping) + : base(GetDiscriminatorName(discriminatorName)) + { + this.mapping = mapping ?? DefaultMapping.Value; + } - private static string GetDiscriminatorName(string? discriminatorName) - { - var attribute = typeof(T).GetCustomAttribute<JsonInheritanceConverterAttribute>(); + private static string GetDiscriminatorName(string? discriminatorName) + { + var attribute = typeof(T).GetCustomAttribute<JsonInheritanceConverterAttribute>(); - return attribute?.DiscriminatorName ?? discriminatorName ?? "discriminator"; - } + return attribute?.DiscriminatorName ?? discriminatorName ?? "discriminator"; + } - public override Type GetDiscriminatorType(string name, Type typeToConvert) + public override Type GetDiscriminatorType(string name, Type typeToConvert) + { + if (!mapping.TryGetValue(name, out var type)) { - if (!mapping.TryGetValue(name, out var type)) - { - ThrowHelper.JsonException($"Could not find subtype of '{typeToConvert.Name}' with discriminator '{name}'."); - return default!; - } - - return type; + ThrowHelper.JsonException($"Could not find subtype of '{typeToConvert.Name}' with discriminator '{name}'."); + return default!; } - public override string GetDiscriminatorValue(Type type) - { - return mapping.FirstOrDefault(x => x.Value == type).Key ?? type.Name; - } + return type; + } + + public override string GetDiscriminatorValue(Type type) + { + return mapping.FirstOrDefault(x => x.Value == type).Key ?? type.Name; } } diff --git a/backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs b/backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs index 51763236e5..7194d465bb 100644 --- a/backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs +++ b/backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs @@ -7,16 +7,15 @@ using System.Text.Json.Serialization; -namespace Squidex.Web.Json +namespace Squidex.Web.Json; + +public sealed class JsonInheritanceConverterAttribute : JsonConverterAttribute { - public sealed class JsonInheritanceConverterAttribute : JsonConverterAttribute - { - public string DiscriminatorName { get; } + public string DiscriminatorName { get; } - public JsonInheritanceConverterAttribute(Type baseType, string discriminatorName = "$type") - : base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType)) - { - DiscriminatorName = discriminatorName; - } + public JsonInheritanceConverterAttribute(Type baseType, string discriminatorName = "$type") + : base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType)) + { + DiscriminatorName = discriminatorName; } } diff --git a/backend/src/Squidex.Web/Pipeline/AccessTokenQueryExtensions.cs b/backend/src/Squidex.Web/Pipeline/AccessTokenQueryExtensions.cs index 7caaf1c804..8626376e15 100644 --- a/backend/src/Squidex.Web/Pipeline/AccessTokenQueryExtensions.cs +++ b/backend/src/Squidex.Web/Pipeline/AccessTokenQueryExtensions.cs @@ -7,13 +7,12 @@ using Microsoft.AspNetCore.Builder; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public static class AccessTokenQueryExtensions { - public static class AccessTokenQueryExtensions + public static void UseAccessTokenQueryString(this IApplicationBuilder app) { - public static void UseAccessTokenQueryString(this IApplicationBuilder app) - { - app.UseMiddleware<AccessTokenQueryMiddleware>(); - } + app.UseMiddleware<AccessTokenQueryMiddleware>(); } } diff --git a/backend/src/Squidex.Web/Pipeline/AccessTokenQueryMiddleware.cs b/backend/src/Squidex.Web/Pipeline/AccessTokenQueryMiddleware.cs index c071cae43d..fa3f4a5478 100644 --- a/backend/src/Squidex.Web/Pipeline/AccessTokenQueryMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/AccessTokenQueryMiddleware.cs @@ -8,32 +8,31 @@ using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class AccessTokenQueryMiddleware { - public sealed class AccessTokenQueryMiddleware + private readonly RequestDelegate next; + + public AccessTokenQueryMiddleware(RequestDelegate next) { - private readonly RequestDelegate next; + this.next = next; + } - public AccessTokenQueryMiddleware(RequestDelegate next) - { - this.next = next; - } + public Task InvokeAsync(HttpContext context) + { + var request = context.Request; - public Task InvokeAsync(HttpContext context) + if (HasNoAuthHeader(request) && request.Query.TryGetValue("access_token", out var token)) { - var request = context.Request; - - if (HasNoAuthHeader(request) && request.Query.TryGetValue("access_token", out var token)) - { - request.Headers[HeaderNames.Authorization] = $"Bearer {token}"; - } - - return next(context); + request.Headers[HeaderNames.Authorization] = $"Bearer {token}"; } - private static bool HasNoAuthHeader(HttpRequest request) - { - return string.IsNullOrWhiteSpace(request.Headers[HeaderNames.Authorization]); - } + return next(context); + } + + private static bool HasNoAuthHeader(HttpRequest request) + { + return string.IsNullOrWhiteSpace(request.Headers[HeaderNames.Authorization]); } } diff --git a/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs b/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs index db2eea5685..42c857f1f9 100644 --- a/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs +++ b/backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs @@ -10,70 +10,69 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Squidex.Log; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class ActionContextLogAppender : ILogAppender { - public sealed class ActionContextLogAppender : ILogAppender + private readonly IHttpContextAccessor httpContextAccessor; + private readonly IActionContextAccessor actionContextAccessor; + + public ActionContextLogAppender(IHttpContextAccessor httpContextAccessor, IActionContextAccessor actionContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + this.actionContextAccessor = actionContextAccessor; + } + + public void Append(IObjectWriter writer, SemanticLogLevel logLevel, Exception? exception) { - private readonly IHttpContextAccessor httpContextAccessor; - private readonly IActionContextAccessor actionContextAccessor; + var httpContext = httpContextAccessor.HttpContext; - public ActionContextLogAppender(IHttpContextAccessor httpContextAccessor, IActionContextAccessor actionContextAccessor) + if (string.IsNullOrEmpty(httpContext?.Request?.Method)) { - this.httpContextAccessor = httpContextAccessor; - this.actionContextAccessor = actionContextAccessor; + return; } - public void Append(IObjectWriter writer, SemanticLogLevel logLevel, Exception? exception) - { - var httpContext = httpContextAccessor.HttpContext; + var actionContext = actionContextAccessor.ActionContext; + try + { if (string.IsNullOrEmpty(httpContext?.Request?.Method)) { return; } - var actionContext = actionContextAccessor.ActionContext; + var requestId = GetRequestId(httpContext); - try - { - if (string.IsNullOrEmpty(httpContext?.Request?.Method)) - { - return; - } + var logContext = (requestId, context: httpContext, actionContext); - var requestId = GetRequestId(httpContext); + writer.WriteObject("web", logContext, (ctx, w) => + { + w.WriteProperty("requestId", ctx.requestId); + w.WriteProperty("requestPath", ctx.context.Request.Path); + w.WriteProperty("requestMethod", ctx.context.Request.Method); - var logContext = (requestId, context: httpContext, actionContext); + var actionContext = ctx.actionContext; - writer.WriteObject("web", logContext, (ctx, w) => + if (actionContext != null) { - w.WriteProperty("requestId", ctx.requestId); - w.WriteProperty("requestPath", ctx.context.Request.Path); - w.WriteProperty("requestMethod", ctx.context.Request.Method); - - var actionContext = ctx.actionContext; - - if (actionContext != null) + w.WriteObject("routeValues", actionContext.ActionDescriptor.RouteValues, (routeValues, r) => { - w.WriteObject("routeValues", actionContext.ActionDescriptor.RouteValues, (routeValues, r) => + foreach (var (key, value) in routeValues) { - foreach (var (key, value) in routeValues) - { - r.WriteProperty(key, value); - } - }); - } - }); - } - catch - { - return; - } + r.WriteProperty(key, value); + } + }); + } + }); } - - private static string GetRequestId(HttpContext httpContext) + catch { - return Activity.Current?.Id ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString(); + return; } } + + private static string GetRequestId(HttpContext httpContext) + { + return Activity.Current?.Id ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString(); + } } diff --git a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs index 0480b4e60a..5c859e8426 100644 --- a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs @@ -11,59 +11,58 @@ using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer { - public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer + private readonly IUsageGate usageGate; + + public ApiCostsFilter(IUsageGate usageGate) { - private readonly IUsageGate usageGate; + this.usageGate = usageGate; + } - public ApiCostsFilter(IUsageGate usageGate) + IFilterMetadata IFilterContainer.FilterDefinition { get; set; } + + public ApiCostsAttribute FilterDefinition + { + get { - this.usageGate = usageGate; + return (ApiCostsAttribute)((IFilterContainer)this).FilterDefinition; } - - IFilterMetadata IFilterContainer.FilterDefinition { get; set; } - - public ApiCostsAttribute FilterDefinition + set { - get - { - return (ApiCostsAttribute)((IFilterContainer)this).FilterDefinition; - } - set - { - ((IFilterContainer)this).FilterDefinition = value; - } + ((IFilterContainer)this).FilterDefinition = value; } + } - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - context.HttpContext.Features.Set<IApiCostsFeature>(FilterDefinition); + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + context.HttpContext.Features.Set<IApiCostsFeature>(FilterDefinition); - var app = context.HttpContext.Context().App; + var app = context.HttpContext.Context().App; - if (app != null) + if (app != null) + { + if (FilterDefinition.Costs > 0) { - if (FilterDefinition.Costs > 0) + using (Telemetry.Activities.StartActivity("CheckUsage")) { - using (Telemetry.Activities.StartActivity("CheckUsage")) - { - var (_, clientId) = context.HttpContext.User.GetClient(); + var (_, clientId) = context.HttpContext.User.GetClient(); - var isBlocked = await usageGate.IsBlockedAsync(app, clientId, DateTime.Today, context.HttpContext.RequestAborted); + var isBlocked = await usageGate.IsBlockedAsync(app, clientId, DateTime.Today, context.HttpContext.RequestAborted); - if (isBlocked) - { - context.Result = new StatusCodeResult(429); - return; - } + if (isBlocked) + { + context.Result = new StatusCodeResult(429); + return; } } - - context.HttpContext.Response.Headers.Add("X-Costs", FilterDefinition.Costs.ToString(CultureInfo.InvariantCulture)); } - await next(); + context.HttpContext.Response.Headers.Add("X-Costs", FilterDefinition.Costs.ToString(CultureInfo.InvariantCulture)); } + + await next(); } } diff --git a/backend/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs b/backend/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs index 5ee2d0964c..05a2710ad9 100644 --- a/backend/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs +++ b/backend/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs @@ -10,22 +10,21 @@ using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class ApiPermissionUnifier : IClaimsTransformation { - public sealed class ApiPermissionUnifier : IClaimsTransformation + private const string AdministratorRole = "ADMINISTRATOR"; + + public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { - private const string AdministratorRole = "ADMINISTRATOR"; + var identity = principal.Identities.First(); - public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) + if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, AdministratorRole, StringComparison.OrdinalIgnoreCase)) { - var identity = principal.Identities.First(); - - if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, AdministratorRole, StringComparison.OrdinalIgnoreCase)) - { - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.Admin)); - } - - return Task.FromResult(principal); + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.Admin)); } + + return Task.FromResult(principal); } } diff --git a/backend/src/Squidex.Web/Pipeline/AppFeature.cs b/backend/src/Squidex.Web/Pipeline/AppFeature.cs index fe3add818d..1d2325c134 100644 --- a/backend/src/Squidex.Web/Pipeline/AppFeature.cs +++ b/backend/src/Squidex.Web/Pipeline/AppFeature.cs @@ -9,7 +9,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Web.Pipeline -{ - public sealed record AppFeature(IAppEntity App) : IAppFeature; -} +namespace Squidex.Web.Pipeline; + +public sealed record AppFeature(IAppEntity App) : IAppFeature; diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index 5468d0a06f..7b297f7227 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -17,168 +17,167 @@ using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class AppResolver : IAsyncActionFilter { - public sealed class AppResolver : IAsyncActionFilter + private readonly IAppProvider appProvider; + + public AppResolver(IAppProvider appProvider) { - private readonly IAppProvider appProvider; + this.appProvider = appProvider; + } - public AppResolver(IAppProvider appProvider) - { - this.appProvider = appProvider; - } + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var user = context.HttpContext.User; - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + if (context.RouteData.Values.TryGetValue("app", out var appValue)) { - var user = context.HttpContext.User; + var appName = appValue?.ToString(); - if (context.RouteData.Values.TryGetValue("app", out var appValue)) + if (string.IsNullOrWhiteSpace(appName)) { - var appName = appValue?.ToString(); + context.Result = new NotFoundResult(); + return; + } - if (string.IsNullOrWhiteSpace(appName)) - { - context.Result = new NotFoundResult(); - return; - } + var isFrontend = user.IsInClient(DefaultClients.Frontend); - var isFrontend = user.IsInClient(DefaultClients.Frontend); + var app = await appProvider.GetAppAsync(appName, !isFrontend, default); - var app = await appProvider.GetAppAsync(appName, !isFrontend, default); + if (app == null) + { + var log = context.HttpContext.RequestServices?.GetService<ILogger<AppResolver>>(); - if (app == null) - { - var log = context.HttpContext.RequestServices?.GetService<ILogger<AppResolver>>(); + log?.LogWarning("Cannot find app with the given name {name}.", appName); - log?.LogWarning("Cannot find app with the given name {name}.", appName); + context.Result = new NotFoundResult(); + return; + } - context.Result = new NotFoundResult(); - return; - } + string? clientId = null; + + var (role, permissions) = FindByOpenIdSubject(app, user, isFrontend); - string? clientId = null; + if (permissions == null) + { + (clientId, role, permissions) = FindByOpenIdClient(app, user, isFrontend); + } - var (role, permissions) = FindByOpenIdSubject(app, user, isFrontend); + if (permissions == null) + { + (clientId, role, permissions) = FindAnonymousClient(app, isFrontend); + } - if (permissions == null) + if (permissions != null) + { + var identity = user.Identities.First(); + + if (!string.IsNullOrWhiteSpace(role)) { - (clientId, role, permissions) = FindByOpenIdClient(app, user, isFrontend); + identity.AddClaim(new Claim(ClaimTypes.Role, role)); } - if (permissions == null) + foreach (var permission in permissions) { - (clientId, role, permissions) = FindAnonymousClient(app, isFrontend); + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id)); } - if (permissions != null) + if (user.Token() == null && clientId != null) { - var identity = user.Identities.First(); - - if (!string.IsNullOrWhiteSpace(role)) - { - identity.AddClaim(new Claim(ClaimTypes.Role, role)); - } - - foreach (var permission in permissions) - { - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id)); - } - - if (user.Token() == null && clientId != null) - { - identity.AddClaim(new Claim(OpenIdClaims.ClientId, clientId)); - } + identity.AddClaim(new Claim(OpenIdClaims.ClientId, clientId)); } + } - var requestContext = new Context(context.HttpContext.User, app).WithHeaders(context.HttpContext); + var requestContext = new Context(context.HttpContext.User, app).WithHeaders(context.HttpContext); - if (!AllowAnonymous(context) && !HasPermission(appName, requestContext)) + if (!AllowAnonymous(context) && !HasPermission(appName, requestContext)) + { + if (string.IsNullOrWhiteSpace(user.Identity?.AuthenticationType)) { - if (string.IsNullOrWhiteSpace(user.Identity?.AuthenticationType)) - { - context.Result = new UnauthorizedResult(); - } - else - { - var log = context.HttpContext.RequestServices?.GetService<ILogger<AppResolver>>(); - - log?.LogWarning("Authenticated user has no permission to access the app {name} with ID {id}.", - app.Id, - app.Name); - - context.Result = new NotFoundResult(); - } - - return; + context.Result = new UnauthorizedResult(); } + else + { + var log = context.HttpContext.RequestServices?.GetService<ILogger<AppResolver>>(); - context.HttpContext.Features.Set(requestContext); - context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app)); - context.HttpContext.Response.Headers.Add("X-AppId", app.Id.ToString()); - } + log?.LogWarning("Authenticated user has no permission to access the app {name} with ID {id}.", + app.Id, + app.Name); - await next(); - } + context.Result = new NotFoundResult(); + } - private static bool HasPermission(string appName, Context requestContext) - { - return requestContext.UserPermissions.Includes(PermissionIds.ForApp(PermissionIds.App, appName)); - } + return; + } - private static bool AllowAnonymous(ActionExecutingContext context) - { - return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); + context.HttpContext.Features.Set(requestContext); + context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app)); + context.HttpContext.Response.Headers.Add("X-AppId", app.Id.ToString()); } - private static (string?, string?, PermissionSet?) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user, bool isFrontend) - { - var (appName, clientId) = user.GetClient(); + await next(); + } - if (app.Name != appName || clientId == null) - { - return default; - } + private static bool HasPermission(string appName, Context requestContext) + { + return requestContext.UserPermissions.Includes(PermissionIds.ForApp(PermissionIds.App, appName)); + } - if (app.TryGetClientRole(clientId, isFrontend, out var role)) - { - return (clientId, role.Name, role.Permissions); - } + private static bool AllowAnonymous(ActionExecutingContext context) + { + return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); + } + + private static (string?, string?, PermissionSet?) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user, bool isFrontend) + { + var (appName, clientId) = user.GetClient(); + if (app.Name != appName || clientId == null) + { return default; } - private static (string?, string?, PermissionSet?) FindAnonymousClient(IAppEntity app, bool isFrontend) + if (app.TryGetClientRole(clientId, isFrontend, out var role)) { - var client = app.Clients.FirstOrDefault(x => x.Value.AllowAnonymous); + return (clientId, role.Name, role.Permissions); + } - if (client.Value == null) - { - return default; - } + return default; + } - if (app.TryGetRole(client.Value.Role, isFrontend, out var role)) - { - return (client.Key, role.Name, role.Permissions); - } + private static (string?, string?, PermissionSet?) FindAnonymousClient(IAppEntity app, bool isFrontend) + { + var client = app.Clients.FirstOrDefault(x => x.Value.AllowAnonymous); + if (client.Value == null) + { return default; } - private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user, bool isFrontend) + if (app.TryGetRole(client.Value.Role, isFrontend, out var role)) { - var subjectId = user.OpenIdSubject(); + return (client.Key, role.Name, role.Permissions); + } - if (subjectId == null) - { - return default; - } + return default; + } - if (app.TryGetContributorRole(subjectId, isFrontend, out var role)) - { - return (role.Name, role.Permissions); - } + private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user, bool isFrontend) + { + var subjectId = user.OpenIdSubject(); + if (subjectId == null) + { return default; } + + if (app.TryGetContributorRole(subjectId, isFrontend, out var role)) + { + return (role.Name, role.Permissions); + } + + return default; } } diff --git a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs index dff1b47762..2fa7d726a7 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs @@ -12,48 +12,47 @@ #pragma warning disable MA0073 // Avoid comparison with bool constant -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class CachingFilter : IAsyncActionFilter { - public sealed class CachingFilter : IAsyncActionFilter - { - private readonly CachingManager cachingManager; + private readonly CachingManager cachingManager; - public CachingFilter(CachingManager cachingManager) - { - this.cachingManager = cachingManager; - } + public CachingFilter(CachingManager cachingManager) + { + this.cachingManager = cachingManager; + } - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - var httpContext = context.HttpContext; + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var httpContext = context.HttpContext; - cachingManager.Start(httpContext); + cachingManager.Start(httpContext); - var resultContext = await next(); + var resultContext = await next(); - cachingManager.Finish(httpContext); + cachingManager.Finish(httpContext); - if (httpContext.Response.HasStarted == false && - httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag) && - IsCacheable(httpContext, etag)) - { - resultContext.Result = new StatusCodeResult(304); - } + if (httpContext.Response.HasStarted == false && + httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag) && + IsCacheable(httpContext, etag)) + { + resultContext.Result = new StatusCodeResult(304); } + } - private static bool IsCacheable(HttpContext httpContext, string etag) + private static bool IsCacheable(HttpContext httpContext, string etag) + { + if (!HttpMethods.IsGet(httpContext.Request.Method) || httpContext.Response.StatusCode != 200) { - if (!HttpMethods.IsGet(httpContext.Request.Method) || httpContext.Response.StatusCode != 200) - { - return false; - } - - if (!httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatchValue)) - { - return false; - } + return false; + } - return ETagUtils.IsSameEtag(noneMatchValue, etag); + if (!httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatchValue)) + { + return false; } + + return ETagUtils.IsSameEtag(noneMatchValue, etag); } } diff --git a/backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs b/backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs index b7bd0ff34d..83675bf2ba 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs @@ -10,60 +10,59 @@ using Microsoft.Net.Http.Headers; using Squidex.Infrastructure.Security; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class CachingKeysMiddleware { - public sealed class CachingKeysMiddleware + private readonly CachingOptions cachingOptions; + private readonly CachingManager cachingManager; + private readonly RequestDelegate next; + + public CachingKeysMiddleware(CachingManager cachingManager, IOptions<CachingOptions> cachingOptions, RequestDelegate next) { - private readonly CachingOptions cachingOptions; - private readonly CachingManager cachingManager; - private readonly RequestDelegate next; + this.cachingOptions = cachingOptions.Value; + this.cachingManager = cachingManager; - public CachingKeysMiddleware(CachingManager cachingManager, IOptions<CachingOptions> cachingOptions, RequestDelegate next) - { - this.cachingOptions = cachingOptions.Value; - this.cachingManager = cachingManager; + this.next = next; + } - this.next = next; - } + public async Task InvokeAsync(HttpContext context) + { + cachingManager.Start(context); - public async Task InvokeAsync(HttpContext context) + AppendAuthHeaders(context); + + context.Response.OnStarting(_ => { - cachingManager.Start(context); + var httpContext = (HttpContext)_; - AppendAuthHeaders(context); + cachingManager.Finish(httpContext); - context.Response.OnStarting(_ => + if (httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag)) { - var httpContext = (HttpContext)_; - - cachingManager.Finish(httpContext); - - if (httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag)) + if (!cachingOptions.StrongETag && !ETagUtils.IsWeakEtag(etag)) { - if (!cachingOptions.StrongETag && !ETagUtils.IsWeakEtag(etag)) - { - httpContext.Response.Headers[HeaderNames.ETag] = ETagUtils.ToWeakEtag(etag); - } + httpContext.Response.Headers[HeaderNames.ETag] = ETagUtils.ToWeakEtag(etag); } + } - return Task.CompletedTask; - }, context); + return Task.CompletedTask; + }, context); - await next(context); - } + await next(context); + } - private void AppendAuthHeaders(HttpContext httpContext) - { - cachingManager.AddHeader("Auth-State"); + private void AppendAuthHeaders(HttpContext httpContext) + { + cachingManager.AddHeader("Auth-State"); - if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdSubject())) - { - cachingManager.AddHeader(HeaderNames.Authorization); - } - else if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdClientId())) - { - cachingManager.AddHeader("Auth-ClientId"); - } + if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdSubject())) + { + cachingManager.AddHeader(HeaderNames.Authorization); + } + else if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdClientId())) + { + cachingManager.AddHeader("Auth-ClientId"); } } } diff --git a/backend/src/Squidex.Web/Pipeline/CachingManager.cs b/backend/src/Squidex.Web/Pipeline/CachingManager.cs index fdd43319ad..a24015c531 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingManager.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingManager.cs @@ -16,272 +16,271 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class CachingManager : IRequestCache { - public sealed class CachingManager : IRequestCache - { - public const string SurrogateKeySizeHeader = "X-SurrogateKeys"; - private const int MaxAllowedKeysSize = 20000; - private readonly ObjectPool<StringBuilder> stringBuilderPool; - private readonly CachingOptions cachingOptions; - private readonly IHttpContextAccessor httpContextAccessor; + public const string SurrogateKeySizeHeader = "X-SurrogateKeys"; + private const int MaxAllowedKeysSize = 20000; + private readonly ObjectPool<StringBuilder> stringBuilderPool; + private readonly CachingOptions cachingOptions; + private readonly IHttpContextAccessor httpContextAccessor; - internal sealed class CacheContext : IDisposable + internal sealed class CacheContext : IDisposable + { + private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + private readonly HashSet<string> keys = new HashSet<string>(); + private readonly HashSet<string> headers = new HashSet<string>(); + private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); + private readonly int maxKeysSize; + private bool hasDependency; + private bool isFinished; + + public CacheContext(int maxKeysSize) { - private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); - private readonly HashSet<string> keys = new HashSet<string>(); - private readonly HashSet<string> headers = new HashSet<string>(); - private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); - private readonly int maxKeysSize; - private bool hasDependency; - private bool isFinished; - - public CacheContext(int maxKeysSize) - { - this.maxKeysSize = maxKeysSize; - } + this.maxKeysSize = maxKeysSize; + } - public void Dispose() - { - hasher.Dispose(); + public void Dispose() + { + hasher.Dispose(); - slimLock.Dispose(); - } + slimLock.Dispose(); + } - public void AddDependency(string key, long version) + public void AddDependency(string key, long version) + { + if (!string.IsNullOrWhiteSpace(key)) { - if (!string.IsNullOrWhiteSpace(key)) + slimLock.EnterWriteLock(); + try { - slimLock.EnterWriteLock(); - try - { - keys.Add(key); + keys.Add(key); - hasher.AppendData(Encoding.Default.GetBytes(key)); - hasher.AppendData(BitConverter.GetBytes(version)); + hasher.AppendData(Encoding.Default.GetBytes(key)); + hasher.AppendData(BitConverter.GetBytes(version)); - hasDependency = true; - } - finally - { - slimLock.ExitWriteLock(); - } + hasDependency = true; + } + finally + { + slimLock.ExitWriteLock(); } } + } - public void AddDependency<T>(T value) - { - var formatted = value?.ToString(); + public void AddDependency<T>(T value) + { + var formatted = value?.ToString(); - if (formatted != null) + if (formatted != null) + { + slimLock.EnterWriteLock(); + try { - slimLock.EnterWriteLock(); - try - { - hasher.AppendData(Encoding.Default.GetBytes(formatted)); + hasher.AppendData(Encoding.Default.GetBytes(formatted)); - hasDependency = true; - } - finally - { - slimLock.ExitWriteLock(); - } + hasDependency = true; + } + finally + { + slimLock.ExitWriteLock(); } } + } - public void Finish(HttpResponse response, ObjectPool<StringBuilder> stringBuilderPool) + public void Finish(HttpResponse response, ObjectPool<StringBuilder> stringBuilderPool) + { + // Finish might be called multiple times. + if (isFinished) { - // Finish might be called multiple times. - if (isFinished) - { - return; - } + return; + } - // Set to finish before we start to ensure that we do not call it again in case of an error. - isFinished = true; + // Set to finish before we start to ensure that we do not call it again in case of an error. + isFinished = true; - if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag)) + if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag)) + { + using (Telemetry.Activities.StartActivity("CalculateEtag")) { - using (Telemetry.Activities.StartActivity("CalculateEtag")) - { - var cacheBuffer = hasher.GetHashAndReset(); - var cacheString = cacheBuffer.ToHexString(); + var cacheBuffer = hasher.GetHashAndReset(); + var cacheString = cacheBuffer.ToHexString(); - response.Headers.Add(HeaderNames.ETag, cacheString); - } + response.Headers.Add(HeaderNames.ETag, cacheString); } + } - if (keys.Count > 0 && maxKeysSize > 0) + if (keys.Count > 0 && maxKeysSize > 0) + { + var stringBuilder = stringBuilderPool.Get(); + try { - var stringBuilder = stringBuilderPool.Get(); - try + foreach (var key in keys) { - foreach (var key in keys) - { - var encoded = Uri.EscapeDataString(key); + var encoded = Uri.EscapeDataString(key); - if (stringBuilder.Length == 0) + if (stringBuilder.Length == 0) + { + if (stringBuilder.Length + encoded.Length > maxKeysSize) { - if (stringBuilder.Length + encoded.Length > maxKeysSize) - { - break; - } + break; } - else + } + else + { + if (stringBuilder.Length + encoded.Length + 1 > maxKeysSize) { - if (stringBuilder.Length + encoded.Length + 1 > maxKeysSize) - { - break; - } - - stringBuilder.Append(' '); + break; } - stringBuilder.Append(encoded); + stringBuilder.Append(' '); } - if (stringBuilder.Length > 0) - { - response.Headers["Surrogate-Key"] = stringBuilder.ToString(); - } + stringBuilder.Append(encoded); } - finally + + if (stringBuilder.Length > 0) { - stringBuilderPool.Return(stringBuilder); + response.Headers["Surrogate-Key"] = stringBuilder.ToString(); } } - - if (headers.Count > 0) + finally { - response.Headers[HeaderNames.Vary] = new StringValues(headers.ToArray()); + stringBuilderPool.Return(stringBuilder); } } - public void AddHeader(string header, StringValues values) + if (headers.Count > 0) { - if (!string.IsNullOrWhiteSpace(header)) - { - try - { - slimLock.EnterWriteLock(); - - headers.Add(header); - } - finally - { - slimLock.ExitWriteLock(); - } - - foreach (var value in values) - { - AddDependency(value); - } - } + response.Headers[HeaderNames.Vary] = new StringValues(headers.ToArray()); } } - public CachingManager(IHttpContextAccessor httpContextAccessor, IOptions<CachingOptions> cachingOptions) + public void AddHeader(string header, StringValues values) { - this.httpContextAccessor = httpContextAccessor; + if (!string.IsNullOrWhiteSpace(header)) + { + try + { + slimLock.EnterWriteLock(); - this.cachingOptions = cachingOptions.Value; + headers.Add(header); + } + finally + { + slimLock.ExitWriteLock(); + } - stringBuilderPool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy - { - MaximumRetainedCapacity = cachingOptions.Value.MaxSurrogateKeysSize - }); + foreach (var value in values) + { + AddDependency(value); + } + } } + } - public void Reset(HttpContext httpContext) - { - Guard.NotNull(httpContext); + public CachingManager(IHttpContextAccessor httpContextAccessor, IOptions<CachingOptions> cachingOptions) + { + this.httpContextAccessor = httpContextAccessor; - httpContext.Features.Set<CacheContext>(null); - } + this.cachingOptions = cachingOptions.Value; - public void Start(HttpContext httpContext) + stringBuilderPool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy { - Guard.NotNull(httpContext); + MaximumRetainedCapacity = cachingOptions.Value.MaxSurrogateKeysSize + }); + } + + public void Reset(HttpContext httpContext) + { + Guard.NotNull(httpContext); - var maxKeysSize = GetKeysSize(httpContext); + httpContext.Features.Set<CacheContext>(null); + } - // Ensure that we only add the cache context once. - if (httpContext.Features.Get<CacheContext>() != null) - { - return; - } + public void Start(HttpContext httpContext) + { + Guard.NotNull(httpContext); - httpContext.Features.Set(new CacheContext(maxKeysSize)); - } + var maxKeysSize = GetKeysSize(httpContext); - public void Finish(HttpContext httpContext) + // Ensure that we only add the cache context once. + if (httpContext.Features.Get<CacheContext>() != null) { - Guard.NotNull(httpContext); + return; + } - var cacheContext = httpContext.Features.Get<CacheContext>(); + httpContext.Features.Set(new CacheContext(maxKeysSize)); + } - // If the cache context has not been set it does not make sense to handle it now. - if (cacheContext == null) - { - return; - } + public void Finish(HttpContext httpContext) + { + Guard.NotNull(httpContext); - cacheContext.Finish(httpContext.Response, stringBuilderPool); - } + var cacheContext = httpContext.Features.Get<CacheContext>(); - private int GetKeysSize(HttpContext httpContext) + // If the cache context has not been set it does not make sense to handle it now. + if (cacheContext == null) { - var headers = httpContext.Request.Headers; + return; + } - if (!headers.TryGetValue(SurrogateKeySizeHeader, out var header) || TryParseHeader(header, out var size)) - { - size = cachingOptions.MaxSurrogateKeysSize; - } + cacheContext.Finish(httpContext.Response, stringBuilderPool); + } - return Math.Min(MaxAllowedKeysSize, size); - } + private int GetKeysSize(HttpContext httpContext) + { + var headers = httpContext.Request.Headers; - private static bool TryParseHeader(StringValues header, out int size) + if (!headers.TryGetValue(SurrogateKeySizeHeader, out var header) || TryParseHeader(header, out var size)) { - return !int.TryParse(header, NumberStyles.Integer, CultureInfo.InvariantCulture, out size); + size = cachingOptions.MaxSurrogateKeysSize; } - public void AddDependency(DomainId key, long version) - { - var cacheContext = httpContextAccessor.HttpContext?.Features.Get<CacheContext>(); + return Math.Min(MaxAllowedKeysSize, size); + } - // The cache context can be null if start has never been called. - cacheContext?.AddDependency(key.ToString(), version); - } + private static bool TryParseHeader(StringValues header, out int size) + { + return !int.TryParse(header, NumberStyles.Integer, CultureInfo.InvariantCulture, out size); + } - public void AddDependency<T>(T value) - { - var cacheContext = httpContextAccessor.HttpContext?.Features.Get<CacheContext>(); + public void AddDependency(DomainId key, long version) + { + var cacheContext = httpContextAccessor.HttpContext?.Features.Get<CacheContext>(); - // The cache context can be null if start has never been called. - cacheContext?.AddDependency(value); - } + // The cache context can be null if start has never been called. + cacheContext?.AddDependency(key.ToString(), version); + } - public void AddHeader(string header) - { - var httpContext = httpContextAccessor.HttpContext; + public void AddDependency<T>(T value) + { + var cacheContext = httpContextAccessor.HttpContext?.Features.Get<CacheContext>(); - if (httpContext == null) - { - return; - } + // The cache context can be null if start has never been called. + cacheContext?.AddDependency(value); + } - var cacheContext = httpContext.Features.Get<CacheContext>(); + public void AddHeader(string header) + { + var httpContext = httpContextAccessor.HttpContext; - // The cache context can be null if start has never been called. - if (cacheContext == null) - { - return; - } + if (httpContext == null) + { + return; + } - httpContext.Request.Headers.TryGetValue(header, out var value); + var cacheContext = httpContext.Features.Get<CacheContext>(); - cacheContext?.AddHeader(header, value); + // The cache context can be null if start has never been called. + if (cacheContext == null) + { + return; } + + httpContext.Request.Headers.TryGetValue(header, out var value); + + cacheContext?.AddHeader(header, value); } } diff --git a/backend/src/Squidex.Web/Pipeline/CachingOptions.cs b/backend/src/Squidex.Web/Pipeline/CachingOptions.cs index 780b8d58bd..cb321d48b8 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingOptions.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingOptions.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class CachingOptions { - public sealed class CachingOptions - { - public bool StrongETag { get; set; } + public bool StrongETag { get; set; } - public int MaxSurrogateKeysSize { get; set; } = 8000; - } + public int MaxSurrogateKeysSize { get; set; } = 8000; } diff --git a/backend/src/Squidex.Web/Pipeline/ContextFilter.cs b/backend/src/Squidex.Web/Pipeline/ContextFilter.cs index 9876d31a60..449eeb0ac5 100644 --- a/backend/src/Squidex.Web/Pipeline/ContextFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/ContextFilter.cs @@ -8,29 +8,28 @@ using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Domain.Apps.Entities; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class ContextFilter : IAsyncActionFilter { - public sealed class ContextFilter : IAsyncActionFilter + public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - var httpContext = context.HttpContext; + var httpContext = context.HttpContext; - var requestContext = - new Context(httpContext.User, null!).Clone(builder => + var requestContext = + new Context(httpContext.User, null!).Clone(builder => + { + foreach (var (key, value) in httpContext.Request.Headers) { - foreach (var (key, value) in httpContext.Request.Headers) + if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) { - if (key.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) - { - builder.SetHeader(key, value.ToString()); - } + builder.SetHeader(key, value.ToString()); } - }); + } + }); - httpContext.Features.Set(requestContext); + httpContext.Features.Set(requestContext); - return next(); - } + return next(); } } diff --git a/backend/src/Squidex.Web/Pipeline/DeferredActionFilter.cs b/backend/src/Squidex.Web/Pipeline/DeferredActionFilter.cs index fc1bb0728e..bba1a8086d 100644 --- a/backend/src/Squidex.Web/Pipeline/DeferredActionFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/DeferredActionFilter.cs @@ -8,18 +8,17 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class DeferredActionFilter : IAsyncActionFilter { - public sealed class DeferredActionFilter : IAsyncActionFilter + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - var resultContext = await next(); + var resultContext = await next(); - if (resultContext.Result is ObjectResult { Value: Deferred deferred } objectResult) - { - objectResult.Value = await deferred.Value; - } + if (resultContext.Result is ObjectResult { Value: Deferred deferred } objectResult) + { + objectResult.Value = await deferred.Value; } } } diff --git a/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs b/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs index 3a2f984df2..9333aef27c 100644 --- a/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs +++ b/backend/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs @@ -11,54 +11,53 @@ using Microsoft.Net.Http.Headers; using Squidex.Assets; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class FileCallbackResultExecutor : FileResultExecutorBase { - public sealed class FileCallbackResultExecutor : FileResultExecutorBase + public FileCallbackResultExecutor(ILoggerFactory loggerFactory) + : base(CreateLogger<FileCallbackResultExecutor>(loggerFactory)) { - public FileCallbackResultExecutor(ILoggerFactory loggerFactory) - : base(CreateLogger<FileCallbackResultExecutor>(loggerFactory)) - { - } + } - public async Task ExecuteAsync(ActionContext context, FileCallbackResult result) + public async Task ExecuteAsync(ActionContext context, FileCallbackResult result) + { + try { - try - { - var (range, _, serveBody) = SetHeadersAndLog(context, result, result.FileSize, result.FileSize != null); + var (range, _, serveBody) = SetHeadersAndLog(context, result, result.FileSize, result.FileSize != null); - if (!string.IsNullOrWhiteSpace(result.FileDownloadName) && result.SendInline) - { - var headerValue = new ContentDispositionHeaderValue("inline"); + if (!string.IsNullOrWhiteSpace(result.FileDownloadName) && result.SendInline) + { + var headerValue = new ContentDispositionHeaderValue("inline"); - headerValue.SetHttpFileName(result.FileDownloadName); + headerValue.SetHttpFileName(result.FileDownloadName); - context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = headerValue.ToString(); - } + context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = headerValue.ToString(); + } - if (serveBody) - { - var bytesRange = new BytesRange(range?.From, range?.To); + if (serveBody) + { + var bytesRange = new BytesRange(range?.From, range?.To); - await result.Callback(context.HttpContext.Response.Body, bytesRange, context.HttpContext.RequestAborted); - } + await result.Callback(context.HttpContext.Response.Body, bytesRange, context.HttpContext.RequestAborted); } - catch (OperationCanceledException) + } + catch (OperationCanceledException) + { + return; + } + catch (Exception e) + { + if (!context.HttpContext.Response.HasStarted && result.ErrorAs404) { - return; + context.HttpContext.Response.Headers.Clear(); + context.HttpContext.Response.StatusCode = 404; + + Logger.LogCritical(new EventId(99), e, "Failed to send result."); } - catch (Exception e) + else { - if (!context.HttpContext.Response.HasStarted && result.ErrorAs404) - { - context.HttpContext.Response.Headers.Clear(); - context.HttpContext.Response.StatusCode = 404; - - Logger.LogCritical(new EventId(99), e, "Failed to send result."); - } - else - { - throw; - } + throw; } } } diff --git a/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs b/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs index 78edcd0bed..4485b1fb8b 100644 --- a/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs @@ -8,23 +8,22 @@ using Microsoft.AspNetCore.Http; using Squidex.Caching; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class LocalCacheMiddleware { - public sealed class LocalCacheMiddleware - { - private readonly RequestDelegate next; + private readonly RequestDelegate next; - public LocalCacheMiddleware(RequestDelegate next) - { - this.next = next; - } + public LocalCacheMiddleware(RequestDelegate next) + { + this.next = next; + } - public async Task InvokeAsync(HttpContext context, ILocalCache localCache) + public async Task InvokeAsync(HttpContext context, ILocalCache localCache) + { + using (localCache.StartContext()) { - using (localCache.StartContext()) - { - await next(context); - } + await next(context); } } } diff --git a/backend/src/Squidex.Web/Pipeline/MeasureResultFilter.cs b/backend/src/Squidex.Web/Pipeline/MeasureResultFilter.cs index 6d66113ab2..5f7593c8ec 100644 --- a/backend/src/Squidex.Web/Pipeline/MeasureResultFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/MeasureResultFilter.cs @@ -8,24 +8,23 @@ using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Infrastructure; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class MeasureResultFilter : IAsyncResultFilter, IAsyncActionFilter { - public sealed class MeasureResultFilter : IAsyncResultFilter, IAsyncActionFilter + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + using (Telemetry.Activities.StartActivity("ExecuteAction")) { - using (Telemetry.Activities.StartActivity("ExecuteAction")) - { - await next(); - } + await next(); } + } - public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + using (Telemetry.Activities.StartActivity("ExecuteResult")) { - using (Telemetry.Activities.StartActivity("ExecuteResult")) - { - await next(); - } + await next(); } } } diff --git a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs index bcac2144d8..ad326fad2d 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs @@ -13,75 +13,74 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class RequestExceptionMiddleware { - public sealed class RequestExceptionMiddleware + private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor(); + private static readonly RouteData EmptyRouteData = new RouteData(); + private readonly RequestDelegate next; + + public RequestExceptionMiddleware(RequestDelegate next) { - private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor(); - private static readonly RouteData EmptyRouteData = new RouteData(); - private readonly RequestDelegate next; + this.next = next; + } - public RequestExceptionMiddleware(RequestDelegate next) + public async Task InvokeAsync(HttpContext context, IActionResultExecutor<ObjectResult> writer, + ILogger<RequestExceptionMiddleware> log) + { + if (TryGetErrorCode(context, out var statusCode) && IsErrorStatusCode(statusCode)) { - this.next = next; + var (error, _) = ApiExceptionConverter.ToErrorDto(statusCode, context); + + await WriteErrorAsync(context, error, writer); + return; } - public async Task InvokeAsync(HttpContext context, IActionResultExecutor<ObjectResult> writer, - ILogger<RequestExceptionMiddleware> log) + try { - if (TryGetErrorCode(context, out var statusCode) && IsErrorStatusCode(statusCode)) - { - var (error, _) = ApiExceptionConverter.ToErrorDto(statusCode, context); - - await WriteErrorAsync(context, error, writer); - return; - } - - try - { - await next(context); - } - catch (Exception ex) - { - log.LogError(ex, "An unexpected exception has occurred."); - - if (!context.Response.HasStarted) - { - var (error, _) = ex.ToErrorDto(context); - - await WriteErrorAsync(context, error, writer); - } - } + await next(context); + } + catch (Exception ex) + { + log.LogError(ex, "An unexpected exception has occurred."); - if (IsErrorStatusCode(context.Response.StatusCode) && !context.Response.HasStarted) + if (!context.Response.HasStarted) { - var (error, _) = ApiExceptionConverter.ToErrorDto(context.Response.StatusCode, context); + var (error, _) = ex.ToErrorDto(context); await WriteErrorAsync(context, error, writer); } } - private static async Task WriteErrorAsync(HttpContext context, ErrorDto error, IActionResultExecutor<ObjectResult> writer) + if (IsErrorStatusCode(context.Response.StatusCode) && !context.Response.HasStarted) { - var actionRouteData = context.GetRouteData() ?? EmptyRouteData; - var actionContext = new ActionContext(context, actionRouteData, EmptyActionDescriptor); + var (error, _) = ApiExceptionConverter.ToErrorDto(context.Response.StatusCode, context); - await writer.ExecuteAsync(actionContext, new ObjectResult(error) - { - StatusCode = error.StatusCode - }); + await WriteErrorAsync(context, error, writer); } + } + + private static async Task WriteErrorAsync(HttpContext context, ErrorDto error, IActionResultExecutor<ObjectResult> writer) + { + var actionRouteData = context.GetRouteData() ?? EmptyRouteData; + var actionContext = new ActionContext(context, actionRouteData, EmptyActionDescriptor); - private static bool TryGetErrorCode(HttpContext context, out int statusCode) + await writer.ExecuteAsync(actionContext, new ObjectResult(error) { - statusCode = 0; + StatusCode = error.StatusCode + }); + } - return context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, NumberStyles.Integer, CultureInfo.InvariantCulture, out statusCode); - } + private static bool TryGetErrorCode(HttpContext context, out int statusCode) + { + statusCode = 0; - private static bool IsErrorStatusCode(int statusCode) - { - return statusCode is >= 400 and < 600; - } + return context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, NumberStyles.Integer, CultureInfo.InvariantCulture, out statusCode); + } + + private static bool IsErrorStatusCode(int statusCode) + { + return statusCode is >= 400 and < 600; } } diff --git a/backend/src/Squidex.Web/Pipeline/RequestLogOptions.cs b/backend/src/Squidex.Web/Pipeline/RequestLogOptions.cs index e15eea34dd..32a1428699 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestLogOptions.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestLogOptions.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class RequestLogOptions { - public sealed class RequestLogOptions - { - public bool LogRequests { get; set; } - } + public bool LogRequests { get; set; } } diff --git a/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs index 8c7da4a44c..eba1e9c56d 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs @@ -11,75 +11,74 @@ using Squidex.Infrastructure.Security; using Squidex.Log; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class RequestLogPerformanceMiddleware { - public sealed class RequestLogPerformanceMiddleware - { - private readonly RequestLogOptions requestLogOptions; - private readonly RequestDelegate next; + private readonly RequestLogOptions requestLogOptions; + private readonly RequestDelegate next; - public RequestLogPerformanceMiddleware(RequestDelegate next, IOptions<RequestLogOptions> requestLogOptions) - { - this.requestLogOptions = requestLogOptions.Value; + public RequestLogPerformanceMiddleware(RequestDelegate next, IOptions<RequestLogOptions> requestLogOptions) + { + this.requestLogOptions = requestLogOptions.Value; - this.next = next; - } + this.next = next; + } - public async Task InvokeAsync(HttpContext context, ISemanticLog log) + public async Task InvokeAsync(HttpContext context, ISemanticLog log) + { + if (requestLogOptions.LogRequests) { - if (requestLogOptions.LogRequests) - { - var watch = ValueStopwatch.StartNew(); + var watch = ValueStopwatch.StartNew(); - try - { - await next(context); - } - finally - { - var elapsedMs = watch.Stop(); - - log.LogInformation((elapsedMs, context), (ctx, w) => - { - w.WriteProperty("message", "HTTP request executed."); - w.WriteProperty("elapsedRequestMs", ctx.elapsedMs); - w.WriteObject("filters", ctx.context, LogFilters); - }); - } - } - else + try { await next(context); } - } - - private static void LogFilters(HttpContext httpContext, IObjectWriter obj) - { - var app = httpContext.Context().App; - - if (app != null) + finally { - obj.WriteProperty("appId", app.Id.ToString()); - obj.WriteProperty("appName", app.Name); + var elapsedMs = watch.Stop(); + + log.LogInformation((elapsedMs, context), (ctx, w) => + { + w.WriteProperty("message", "HTTP request executed."); + w.WriteProperty("elapsedRequestMs", ctx.elapsedMs); + w.WriteObject("filters", ctx.context, LogFilters); + }); } + } + else + { + await next(context); + } + } - var userId = httpContext.User.OpenIdSubject(); + private static void LogFilters(HttpContext httpContext, IObjectWriter obj) + { + var app = httpContext.Context().App; - if (!string.IsNullOrWhiteSpace(userId)) - { - obj.WriteProperty(nameof(userId), userId); - } + if (app != null) + { + obj.WriteProperty("appId", app.Id.ToString()); + obj.WriteProperty("appName", app.Name); + } - var clientId = httpContext.User.OpenIdClientId(); + var userId = httpContext.User.OpenIdSubject(); - if (!string.IsNullOrWhiteSpace(clientId)) - { - obj.WriteProperty(nameof(clientId), clientId); - } + if (!string.IsNullOrWhiteSpace(userId)) + { + obj.WriteProperty(nameof(userId), userId); + } - var costs = httpContext.Features.Get<IApiCostsFeature>()?.Costs ?? 0; + var clientId = httpContext.User.OpenIdClientId(); - obj.WriteProperty(nameof(costs), costs); + if (!string.IsNullOrWhiteSpace(clientId)) + { + obj.WriteProperty(nameof(clientId), clientId); } + + var costs = httpContext.Features.Get<IApiCostsFeature>()?.Costs ?? 0; + + obj.WriteProperty(nameof(costs), costs); } } diff --git a/backend/src/Squidex.Web/Pipeline/SchemaFeature.cs b/backend/src/Squidex.Web/Pipeline/SchemaFeature.cs index c3dbd79cec..469eec90a5 100644 --- a/backend/src/Squidex.Web/Pipeline/SchemaFeature.cs +++ b/backend/src/Squidex.Web/Pipeline/SchemaFeature.cs @@ -9,7 +9,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Web.Pipeline -{ - public sealed record SchemaFeature(ISchemaEntity Schema) : ISchemaFeature; -} +namespace Squidex.Web.Pipeline; + +public sealed record SchemaFeature(ISchemaEntity Schema) : ISchemaFeature; diff --git a/backend/src/Squidex.Web/Pipeline/SchemaResolver.cs b/backend/src/Squidex.Web/Pipeline/SchemaResolver.cs index 0e075deb49..d9ded0b5f5 100644 --- a/backend/src/Squidex.Web/Pipeline/SchemaResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/SchemaResolver.cs @@ -14,70 +14,69 @@ using Squidex.Infrastructure.Security; using Squidex.Shared; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class SchemaResolver : IAsyncActionFilter { - public sealed class SchemaResolver : IAsyncActionFilter + private readonly IAppProvider appProvider; + + public SchemaResolver(IAppProvider appProvider) { - private readonly IAppProvider appProvider; + Guard.NotNull(appProvider); - public SchemaResolver(IAppProvider appProvider) - { - Guard.NotNull(appProvider); + this.appProvider = appProvider; + } - this.appProvider = appProvider; - } + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var appId = context.HttpContext.Features.Get<IAppFeature>()?.App.Id ?? default; - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + if (appId != default) { - var appId = context.HttpContext.Features.Get<IAppFeature>()?.App.Id ?? default; - - if (appId != default) + if (context.RouteData.Values.TryGetValue("schema", out var schemaValue)) { - if (context.RouteData.Values.TryGetValue("schema", out var schemaValue)) - { - var schemaIdOrName = schemaValue?.ToString(); - - if (string.IsNullOrWhiteSpace(schemaIdOrName)) - { - context.Result = new NotFoundResult(); - return; - } + var schemaIdOrName = schemaValue?.ToString(); - var schema = await GetSchemaAsync(appId, schemaIdOrName, context.HttpContext.User); + if (string.IsNullOrWhiteSpace(schemaIdOrName)) + { + context.Result = new NotFoundResult(); + return; + } - if (schema == null) - { - context.Result = new NotFoundResult(); - return; - } + var schema = await GetSchemaAsync(appId, schemaIdOrName, context.HttpContext.User); - if (context.ActionDescriptor.EndpointMetadata.Any(x => x is SchemaMustBePublishedAttribute) && !schema.SchemaDef.IsPublished) - { - context.Result = new NotFoundResult(); - return; - } + if (schema == null) + { + context.Result = new NotFoundResult(); + return; + } - context.HttpContext.Features.Set<ISchemaFeature>(new SchemaFeature(schema)); + if (context.ActionDescriptor.EndpointMetadata.Any(x => x is SchemaMustBePublishedAttribute) && !schema.SchemaDef.IsPublished) + { + context.Result = new NotFoundResult(); + return; } - } - await next(); + context.HttpContext.Features.Set<ISchemaFeature>(new SchemaFeature(schema)); + } } - private Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, string schemaIdOrName, ClaimsPrincipal user) - { - var canCache = !user.IsInClient(DefaultClients.Frontend); + await next(); + } - if (Guid.TryParse(schemaIdOrName, out var guid)) - { - var schemaId = DomainId.Create(guid); + private Task<ISchemaEntity?> GetSchemaAsync(DomainId appId, string schemaIdOrName, ClaimsPrincipal user) + { + var canCache = !user.IsInClient(DefaultClients.Frontend); - return appProvider.GetSchemaAsync(appId, schemaId, canCache); - } - else - { - return appProvider.GetSchemaAsync(appId, schemaIdOrName, canCache); - } + if (Guid.TryParse(schemaIdOrName, out var guid)) + { + var schemaId = DomainId.Create(guid); + + return appProvider.GetSchemaAsync(appId, schemaId, canCache); + } + else + { + return appProvider.GetSchemaAsync(appId, schemaIdOrName, canCache); } } } diff --git a/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs b/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs index 8f7fa3e834..dc3bb5dc7f 100644 --- a/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs @@ -8,38 +8,37 @@ using Microsoft.AspNetCore.Http; using Squidex.Domain.Users; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class SetupMiddleware { - public sealed class SetupMiddleware + private readonly RequestDelegate next; + private bool isUserFound; + + public SetupMiddleware(RequestDelegate next) { - private readonly RequestDelegate next; - private bool isUserFound; + this.next = next; + } - public SetupMiddleware(RequestDelegate next) + public async Task InvokeAsync(HttpContext context, IUserService userService) + { + if (context.Request.Query.ContainsKey("skip-setup")) { - this.next = next; + await next(context); + return; } - public async Task InvokeAsync(HttpContext context, IUserService userService) + if (!isUserFound && await userService.IsEmptyAsync(context.RequestAborted)) { - if (context.Request.Query.ContainsKey("skip-setup")) - { - await next(context); - return; - } - - if (!isUserFound && await userService.IsEmptyAsync(context.RequestAborted)) - { - var url = context.Request.PathBase.Add("/identity-server/setup"); - - context.Response.Redirect(url); - } - else - { - isUserFound = true; - - await next(context); - } + var url = context.Request.PathBase.Add("/identity-server/setup"); + + context.Response.Redirect(url); + } + else + { + isUserFound = true; + + await next(context); } } } diff --git a/backend/src/Squidex.Web/Pipeline/TeamFeature.cs b/backend/src/Squidex.Web/Pipeline/TeamFeature.cs index 9fecc71319..3aae18d1ee 100644 --- a/backend/src/Squidex.Web/Pipeline/TeamFeature.cs +++ b/backend/src/Squidex.Web/Pipeline/TeamFeature.cs @@ -9,7 +9,6 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Web.Pipeline -{ - public sealed record TeamFeature(ITeamEntity Team) : ITeamFeature; -} +namespace Squidex.Web.Pipeline; + +public sealed record TeamFeature(ITeamEntity Team) : ITeamFeature; diff --git a/backend/src/Squidex.Web/Pipeline/TeamResolver.cs b/backend/src/Squidex.Web/Pipeline/TeamResolver.cs index 42c86a908c..b8c7c87168 100644 --- a/backend/src/Squidex.Web/Pipeline/TeamResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/TeamResolver.cs @@ -17,89 +17,88 @@ using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class TeamResolver : IAsyncActionFilter { - public sealed class TeamResolver : IAsyncActionFilter + private readonly IAppProvider appProvider; + + public TeamResolver(IAppProvider appProvider) { - private readonly IAppProvider appProvider; + this.appProvider = appProvider; + } - public TeamResolver(IAppProvider appProvider) - { - this.appProvider = appProvider; - } + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var user = context.HttpContext.User; - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + if (context.RouteData.Values.TryGetValue("team", out var teamValue)) { - var user = context.HttpContext.User; + var teamId = teamValue?.ToString(); - if (context.RouteData.Values.TryGetValue("team", out var teamValue)) + if (string.IsNullOrWhiteSpace(teamId)) { - var teamId = teamValue?.ToString(); - - if (string.IsNullOrWhiteSpace(teamId)) - { - context.Result = new NotFoundResult(); - return; - } + context.Result = new NotFoundResult(); + return; + } - var team = await appProvider.GetTeamAsync(DomainId.Create(teamId), default); + var team = await appProvider.GetTeamAsync(DomainId.Create(teamId), default); - if (team == null) - { - var log = context.HttpContext.RequestServices?.GetService<ILogger<TeamResolver>>(); + if (team == null) + { + var log = context.HttpContext.RequestServices?.GetService<ILogger<TeamResolver>>(); - log?.LogWarning("Cannot find team with the given id {id}.", teamId); + log?.LogWarning("Cannot find team with the given id {id}.", teamId); - context.Result = new NotFoundResult(); - return; - } + context.Result = new NotFoundResult(); + return; + } - var subjectId = user.OpenIdSubject(); + var subjectId = user.OpenIdSubject(); - if (subjectId != null && team.Contributors.TryGetValue(subjectId, out var role)) - { - var identity = user.Identities.First(); + if (subjectId != null && team.Contributors.TryGetValue(subjectId, out var role)) + { + var identity = user.Identities.First(); - identity.AddClaim(new Claim(ClaimTypes.Role, role)); - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.ForApp(PermissionIds.TeamAdmin, team: team.Id.ToString()).Id)); - } + identity.AddClaim(new Claim(ClaimTypes.Role, role)); + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.ForApp(PermissionIds.TeamAdmin, team: team.Id.ToString()).Id)); + } - var requestContext = new Context(context.HttpContext.User, null!).WithHeaders(context.HttpContext); + var requestContext = new Context(context.HttpContext.User, null!).WithHeaders(context.HttpContext); - if (!AllowAnonymous(context) && !HasPermission(team.Id, requestContext)) + if (!AllowAnonymous(context) && !HasPermission(team.Id, requestContext)) + { + if (string.IsNullOrWhiteSpace(user.Identity?.AuthenticationType)) { - if (string.IsNullOrWhiteSpace(user.Identity?.AuthenticationType)) - { - context.Result = new UnauthorizedResult(); - } - else - { - var log = context.HttpContext.RequestServices?.GetService<ILogger<AppResolver>>(); - - log?.LogWarning("Authenticated user has no permission to access the team with ID {id}.", team.Id); + context.Result = new UnauthorizedResult(); + } + else + { + var log = context.HttpContext.RequestServices?.GetService<ILogger<AppResolver>>(); - context.Result = new NotFoundResult(); - } + log?.LogWarning("Authenticated user has no permission to access the team with ID {id}.", team.Id); - return; + context.Result = new NotFoundResult(); } - context.HttpContext.Features.Set(requestContext); - context.HttpContext.Features.Set<ITeamFeature>(new TeamFeature(team)); - context.HttpContext.Response.Headers.Add("X-TeamId", team.Id.ToString()); + return; } - await next(); + context.HttpContext.Features.Set(requestContext); + context.HttpContext.Features.Set<ITeamFeature>(new TeamFeature(team)); + context.HttpContext.Response.Headers.Add("X-TeamId", team.Id.ToString()); } - private static bool HasPermission(DomainId teamId, Context requestContext) - { - return requestContext.UserPermissions.Includes(PermissionIds.ForApp(PermissionIds.Team, team: teamId.ToString())); - } + await next(); + } - private static bool AllowAnonymous(ActionExecutingContext context) - { - return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); - } + private static bool HasPermission(DomainId teamId, Context requestContext) + { + return requestContext.UserPermissions.Includes(PermissionIds.ForApp(PermissionIds.Team, team: teamId.ToString())); + } + + private static bool AllowAnonymous(ActionExecutingContext context) + { + return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); } } diff --git a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs index 15a94f91a3..a8c2fa387c 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs @@ -13,88 +13,87 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class UsageMiddleware : IMiddleware { - public sealed class UsageMiddleware : IMiddleware + private readonly IAppLogStore usageLog; + private readonly IUsageGate usageGate; + + public IClock Clock { get; set; } = SystemClock.Instance; + + public UsageMiddleware(IAppLogStore usageLog, IUsageGate usageGate) { - private readonly IAppLogStore usageLog; - private readonly IUsageGate usageGate; + this.usageLog = usageLog; + this.usageGate = usageGate; + } - public IClock Clock { get; set; } = SystemClock.Instance; + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var usageBody = SetUsageBody(context); - public UsageMiddleware(IAppLogStore usageLog, IUsageGate usageGate) + var watch = ValueStopwatch.StartNew(); + try { - this.usageLog = usageLog; - this.usageGate = usageGate; + await next(context); } - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + finally { - var usageBody = SetUsageBody(context); - - var watch = ValueStopwatch.StartNew(); - try - { - await next(context); - } - finally + if (context.Response.StatusCode != StatusCodes.Status429TooManyRequests) { - if (context.Response.StatusCode != StatusCodes.Status429TooManyRequests) + var app = context.Features.Get<IAppFeature>()?.App; + + if (app != null) { - var app = context.Features.Get<IAppFeature>()?.App; + var bytes = usageBody.BytesWritten; + + if (context.Request.ContentLength != null) + { + bytes += context.Request.ContentLength.Value; + } + + var (_, clientId) = context.User.GetClient(); + + var request = default(RequestLog); + + request.Bytes = bytes; + request.CacheStatus = "MISS"; + request.CacheHits = 0; + request.Costs = context.Features.Get<IApiCostsFeature>()?.Costs ?? 0; + request.ElapsedMs = watch.Stop(); + request.RequestMethod = context.Request.Method; + request.RequestPath = context.Request.Path; + request.Timestamp = Clock.GetCurrentInstant(); + request.StatusCode = context.Response.StatusCode; + request.UserId = context.User.OpenIdSubject(); + request.UserClientId = clientId; + + // Do not flow cancellation token because it is too important. + await usageLog.LogAsync(app.Id, request, default); - if (app != null) + if (request.Costs > 0) { - var bytes = usageBody.BytesWritten; - - if (context.Request.ContentLength != null) - { - bytes += context.Request.ContentLength.Value; - } - - var (_, clientId) = context.User.GetClient(); - - var request = default(RequestLog); - - request.Bytes = bytes; - request.CacheStatus = "MISS"; - request.CacheHits = 0; - request.Costs = context.Features.Get<IApiCostsFeature>()?.Costs ?? 0; - request.ElapsedMs = watch.Stop(); - request.RequestMethod = context.Request.Method; - request.RequestPath = context.Request.Path; - request.Timestamp = Clock.GetCurrentInstant(); - request.StatusCode = context.Response.StatusCode; - request.UserId = context.User.OpenIdSubject(); - request.UserClientId = clientId; - - // Do not flow cancellation token because it is too important. - await usageLog.LogAsync(app.Id, request, default); - - if (request.Costs > 0) - { - var date = request.Timestamp.ToDateTimeUtc().Date; - - await usageGate.TrackRequestAsync(app, request.UserClientId, date, - request.Costs, - request.ElapsedMs, - request.Bytes, - default); - } + var date = request.Timestamp.ToDateTimeUtc().Date; + + await usageGate.TrackRequestAsync(app, request.UserClientId, date, + request.Costs, + request.ElapsedMs, + request.Bytes, + default); } } } } + } - private static UsageResponseBodyFeature SetUsageBody(HttpContext context) - { - var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>()!; + private static UsageResponseBodyFeature SetUsageBody(HttpContext context) + { + var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>()!; - var usageBody = new UsageResponseBodyFeature(originalBodyFeature); + var usageBody = new UsageResponseBodyFeature(originalBodyFeature); - context.Features.Set<IHttpResponseBodyFeature>(usageBody); + context.Features.Set<IHttpResponseBodyFeature>(usageBody); - return usageBody; - } + return usageBody; } } diff --git a/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs b/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs index 30e3ddabcf..86f564a473 100644 --- a/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs +++ b/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs @@ -7,54 +7,53 @@ using System.IO.Pipelines; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public sealed class UsagePipeWriter : PipeWriter { - public sealed class UsagePipeWriter : PipeWriter - { - private readonly PipeWriter inner; - private long bytesWritten; - - public long BytesWritten - { - get => bytesWritten; - } - - public UsagePipeWriter(PipeWriter inner) - { - this.inner = inner; - } - - public override void Advance(int bytes) - { - inner.Advance(bytes); - - bytesWritten += bytes; - } - - public override void CancelPendingFlush() - { - inner.CancelPendingFlush(); - } - - public override void Complete(Exception? exception = null) - { - inner.Complete(); - } - - public override ValueTask<FlushResult> FlushAsync( - CancellationToken cancellationToken = default) - { - return inner.FlushAsync(cancellationToken); - } - - public override Memory<byte> GetMemory(int sizeHint = 0) - { - return inner.GetMemory(sizeHint); - } - - public override Span<byte> GetSpan(int sizeHint = 0) - { - return inner.GetSpan(sizeHint); - } + private readonly PipeWriter inner; + private long bytesWritten; + + public long BytesWritten + { + get => bytesWritten; + } + + public UsagePipeWriter(PipeWriter inner) + { + this.inner = inner; + } + + public override void Advance(int bytes) + { + inner.Advance(bytes); + + bytesWritten += bytes; + } + + public override void CancelPendingFlush() + { + inner.CancelPendingFlush(); + } + + public override void Complete(Exception? exception = null) + { + inner.Complete(); + } + + public override ValueTask<FlushResult> FlushAsync( + CancellationToken cancellationToken = default) + { + return inner.FlushAsync(cancellationToken); + } + + public override Memory<byte> GetMemory(int sizeHint = 0) + { + return inner.GetMemory(sizeHint); + } + + public override Span<byte> GetSpan(int sizeHint = 0) + { + return inner.GetSpan(sizeHint); } } diff --git a/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs b/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs index cbfca0ce80..c143a502ae 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs @@ -8,71 +8,70 @@ using System.IO.Pipelines; using Microsoft.AspNetCore.Http.Features; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +internal sealed class UsageResponseBodyFeature : IHttpResponseBodyFeature { - internal sealed class UsageResponseBodyFeature : IHttpResponseBodyFeature + private readonly IHttpResponseBodyFeature inner; + private readonly UsageStream usageStream; + private readonly UsagePipeWriter usageWriter; + private long bytesWritten; + + public long BytesWritten { - private readonly IHttpResponseBodyFeature inner; - private readonly UsageStream usageStream; - private readonly UsagePipeWriter usageWriter; - private long bytesWritten; + get => bytesWritten + usageStream.BytesWritten + usageWriter.BytesWritten; + } - public long BytesWritten - { - get => bytesWritten + usageStream.BytesWritten + usageWriter.BytesWritten; - } + public Stream Stream + { + get => usageStream; + } - public Stream Stream - { - get => usageStream; - } + public PipeWriter Writer + { + get => usageWriter; + } - public PipeWriter Writer - { - get => usageWriter; - } + public UsageResponseBodyFeature(IHttpResponseBodyFeature inner) + { + usageStream = new UsageStream(inner.Stream); + usageWriter = new UsagePipeWriter(inner.Writer); - public UsageResponseBodyFeature(IHttpResponseBodyFeature inner) - { - usageStream = new UsageStream(inner.Stream); - usageWriter = new UsagePipeWriter(inner.Writer); + this.inner = inner; + } - this.inner = inner; - } + public Task StartAsync( + CancellationToken cancellationToken = default) + { + return inner.StartAsync(cancellationToken); + } - public Task StartAsync( - CancellationToken cancellationToken = default) - { - return inner.StartAsync(cancellationToken); - } + public Task CompleteAsync() + { + return inner.CompleteAsync(); + } - public Task CompleteAsync() - { - return inner.CompleteAsync(); - } + public void DisableBuffering() + { + inner.DisableBuffering(); + } + + public async Task SendFileAsync(string path, long offset, long? count, + CancellationToken cancellationToken = default) + { + await inner.SendFileAsync(path, offset, count, cancellationToken); - public void DisableBuffering() + if (count != null) { - inner.DisableBuffering(); + bytesWritten += count.Value; } - - public async Task SendFileAsync(string path, long offset, long? count, - CancellationToken cancellationToken = default) + else { - await inner.SendFileAsync(path, offset, count, cancellationToken); + var file = new FileInfo(path); - if (count != null) - { - bytesWritten += count.Value; - } - else + if (file.Exists) { - var file = new FileInfo(path); - - if (file.Exists) - { - bytesWritten += file.Length; - } + bytesWritten += file.Length; } } } diff --git a/backend/src/Squidex.Web/Pipeline/UsageStream.cs b/backend/src/Squidex.Web/Pipeline/UsageStream.cs index a5215be604..f39c7eaccd 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageStream.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageStream.cs @@ -7,124 +7,123 @@ #pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +internal sealed class UsageStream : Stream { - internal sealed class UsageStream : Stream - { - private readonly Stream inner; - private long bytesWritten; - - public long BytesWritten - { - get => bytesWritten; - } - - public override bool CanRead - { - get => false; - } - - public override bool CanSeek - { - get => false; - } - - public override bool CanWrite - { - get => inner.CanWrite; - } - - public override long Length - { - get => throw new NotSupportedException(); - } - - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - - public UsageStream(Stream inner) - { - this.inner = inner; - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - var result = inner.BeginWrite(buffer, offset, count, callback, state); - - bytesWritten += count; - - return result; - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, - CancellationToken cancellationToken) - { - await inner.WriteAsync(buffer, offset, count, cancellationToken); - - bytesWritten += count; - } - - public override void Write(byte[] buffer, int offset, int count) - { - inner.Write(buffer, offset, count); - - bytesWritten += count; - } - - public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, - CancellationToken cancellationToken = default) - { - await inner.WriteAsync(buffer, cancellationToken); - - bytesWritten += buffer.Length; - } - - public override void Write(ReadOnlySpan<byte> buffer) - { - inner.Write(buffer); - - bytesWritten += buffer.Length; - } - - public override void WriteByte(byte value) - { - inner.WriteByte(value); - - bytesWritten++; - } - - public override Task FlushAsync( - CancellationToken cancellationToken) - { - return inner.FlushAsync(cancellationToken); - } - - public override void Flush() - { - inner.Flush(); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - inner.EndWrite(asyncResult); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + private readonly Stream inner; + private long bytesWritten; + + public long BytesWritten + { + get => bytesWritten; + } + + public override bool CanRead + { + get => false; + } + + public override bool CanSeek + { + get => false; + } + + public override bool CanWrite + { + get => inner.CanWrite; + } + + public override long Length + { + get => throw new NotSupportedException(); + } + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public UsageStream(Stream inner) + { + this.inner = inner; + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + var result = inner.BeginWrite(buffer, offset, count, callback, state); + + bytesWritten += count; + + return result; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, + CancellationToken cancellationToken) + { + await inner.WriteAsync(buffer, offset, count, cancellationToken); + + bytesWritten += count; + } + + public override void Write(byte[] buffer, int offset, int count) + { + inner.Write(buffer, offset, count); + + bytesWritten += count; + } + + public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, + CancellationToken cancellationToken = default) + { + await inner.WriteAsync(buffer, cancellationToken); + + bytesWritten += buffer.Length; + } + + public override void Write(ReadOnlySpan<byte> buffer) + { + inner.Write(buffer); + + bytesWritten += buffer.Length; + } + + public override void WriteByte(byte value) + { + inner.WriteByte(value); + + bytesWritten++; + } + + public override Task FlushAsync( + CancellationToken cancellationToken) + { + return inner.FlushAsync(cancellationToken); + } + + public override void Flush() + { + inner.Flush(); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + inner.EndWrite(asyncResult); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); } } diff --git a/backend/src/Squidex.Web/Resource.cs b/backend/src/Squidex.Web/Resource.cs index b8d64c3113..bbefc07672 100644 --- a/backend/src/Squidex.Web/Resource.cs +++ b/backend/src/Squidex.Web/Resource.cs @@ -10,52 +10,51 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; -namespace Squidex.Web +namespace Squidex.Web; + +public abstract class Resource { - public abstract class Resource + [LocalizedRequired] + [Display(Description = "The links.")] + [JsonPropertyName("_links")] + public Dictionary<string, ResourceLink> Links { get; } = new Dictionary<string, ResourceLink>(); + + protected void AddSelfLink(string href) + { + AddGetLink("self", href); + } + + protected void AddGetLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "GET", href, metadata); + } + + protected void AddPatchLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "PATCH", href, metadata); + } + + protected void AddPostLink(string rel, string href, string? metadata = null) { - [LocalizedRequired] - [Display(Description = "The links.")] - [JsonPropertyName("_links")] - public Dictionary<string, ResourceLink> Links { get; } = new Dictionary<string, ResourceLink>(); - - protected void AddSelfLink(string href) - { - AddGetLink("self", href); - } - - protected void AddGetLink(string rel, string href, string? metadata = null) - { - AddLink(rel, "GET", href, metadata); - } - - protected void AddPatchLink(string rel, string href, string? metadata = null) - { - AddLink(rel, "PATCH", href, metadata); - } - - protected void AddPostLink(string rel, string href, string? metadata = null) - { - AddLink(rel, "POST", href, metadata); - } - - protected void AddPutLink(string rel, string href, string? metadata = null) - { - AddLink(rel, "PUT", href, metadata); - } - - protected void AddDeleteLink(string rel, string href, string? metadata = null) - { - AddLink(rel, "DELETE", href, metadata); - } - - protected void AddLink(string rel, string method, string href, string? metadata = null) - { - Guard.NotNullOrEmpty(rel); - Guard.NotNullOrEmpty(href); - Guard.NotNullOrEmpty(method); - - Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata }; - } + AddLink(rel, "POST", href, metadata); + } + + protected void AddPutLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "PUT", href, metadata); + } + + protected void AddDeleteLink(string rel, string href, string? metadata = null) + { + AddLink(rel, "DELETE", href, metadata); + } + + protected void AddLink(string rel, string method, string href, string? metadata = null) + { + Guard.NotNullOrEmpty(rel); + Guard.NotNullOrEmpty(href); + Guard.NotNullOrEmpty(method); + + Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata }; } } diff --git a/backend/src/Squidex.Web/ResourceLink.cs b/backend/src/Squidex.Web/ResourceLink.cs index 2de49ba6ec..1495915abc 100644 --- a/backend/src/Squidex.Web/ResourceLink.cs +++ b/backend/src/Squidex.Web/ResourceLink.cs @@ -8,19 +8,18 @@ using System.ComponentModel.DataAnnotations; using Squidex.Infrastructure.Validation; -namespace Squidex.Web +namespace Squidex.Web; + +public class ResourceLink { - public class ResourceLink - { - [LocalizedRequired] - [Display(Description = "The link url.")] - public string Href { get; set; } + [LocalizedRequired] + [Display(Description = "The link url.")] + public string Href { get; set; } - [LocalizedRequired] - [Display(Description = "The link method.")] - public string Method { get; set; } + [LocalizedRequired] + [Display(Description = "The link method.")] + public string Method { get; set; } - [Display(Description = "Additional data about the link.")] - public string? Metadata { get; set; } - } + [Display(Description = "Additional data about the link.")] + public string? Metadata { get; set; } } diff --git a/backend/src/Squidex.Web/Resources.cs b/backend/src/Squidex.Web/Resources.cs index 7c7cd947af..1a5481370f 100644 --- a/backend/src/Squidex.Web/Resources.cs +++ b/backend/src/Squidex.Web/Resources.cs @@ -10,249 +10,248 @@ using Squidex.Infrastructure.Security; using Squidex.Shared; -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class Resources { - public sealed class Resources - { - private readonly Dictionary<(string Id, string Schema), bool> permissions = new Dictionary<(string, string), bool>(); + private readonly Dictionary<(string Id, string Schema), bool> permissions = new Dictionary<(string, string), bool>(); + + // Contents + public bool CanReadContent(string schema) => Can(PermissionIds.AppContentsReadOwn, schema); + + public bool CanCreateContent(string schema) => Can(PermissionIds.AppContentsCreate, schema); - // Contents - public bool CanReadContent(string schema) => Can(PermissionIds.AppContentsReadOwn, schema); + public bool CanCreateContentVersion(string schema) => Can(PermissionIds.AppContentsVersionCreateOwn, schema); - public bool CanCreateContent(string schema) => Can(PermissionIds.AppContentsCreate, schema); + public bool CanDeleteContent(string schema) => Can(PermissionIds.AppContentsDeleteOwn, schema); - public bool CanCreateContentVersion(string schema) => Can(PermissionIds.AppContentsVersionCreateOwn, schema); + public bool CanDeleteContentVersion(string schema) => Can(PermissionIds.AppContentsVersionDeleteOwn, schema); - public bool CanDeleteContent(string schema) => Can(PermissionIds.AppContentsDeleteOwn, schema); + public bool CanChangeStatus(string schema) => Can(PermissionIds.AppContentsChangeStatus, schema); - public bool CanDeleteContentVersion(string schema) => Can(PermissionIds.AppContentsVersionDeleteOwn, schema); + public bool CanCancelContentStatus(string schema) => Can(PermissionIds.AppContentsChangeStatusCancelOwn, schema); - public bool CanChangeStatus(string schema) => Can(PermissionIds.AppContentsChangeStatus, schema); + public bool CanUpdateContent(string schema) => Can(PermissionIds.AppContentsUpdateOwn, schema); - public bool CanCancelContentStatus(string schema) => Can(PermissionIds.AppContentsChangeStatusCancelOwn, schema); + // Schemas + public bool CanUpdateSchema(string schema) => Can(PermissionIds.AppSchemasDelete, schema); - public bool CanUpdateContent(string schema) => Can(PermissionIds.AppContentsUpdateOwn, schema); + public bool CanUpdateSchemaScripts(string schema) => Can(PermissionIds.AppSchemasScripts, schema); - // Schemas - public bool CanUpdateSchema(string schema) => Can(PermissionIds.AppSchemasDelete, schema); + public bool CanPublishSchema(string schema) => Can(PermissionIds.AppSchemasPublish, schema); - public bool CanUpdateSchemaScripts(string schema) => Can(PermissionIds.AppSchemasScripts, schema); + public bool CanDeleteSchema(string schema) => Can(PermissionIds.AppSchemasDelete, schema); - public bool CanPublishSchema(string schema) => Can(PermissionIds.AppSchemasPublish, schema); + public bool CanCreateSchema => Can(PermissionIds.AppSchemasCreate); - public bool CanDeleteSchema(string schema) => Can(PermissionIds.AppSchemasDelete, schema); + public bool CanUpdateSettings => Can(PermissionIds.AppUpdateSettings); - public bool CanCreateSchema => Can(PermissionIds.AppSchemasCreate); + // Asset Scripts + public bool CanUpdateAssetsScripts => Can(PermissionIds.AppAssetsScriptsUpdate); - public bool CanUpdateSettings => Can(PermissionIds.AppUpdateSettings); + // Contributors + public bool CanAssignContributor => Can(PermissionIds.AppContributorsAssign); - // Asset Scripts - public bool CanUpdateAssetsScripts => Can(PermissionIds.AppAssetsScriptsUpdate); + public bool CanAssignTeamContributor => Can(PermissionIds.TeamContributorsAssign); - // Contributors - public bool CanAssignContributor => Can(PermissionIds.AppContributorsAssign); + public bool CanRevokeContributor => Can(PermissionIds.AppContributorsRevoke); - public bool CanAssignTeamContributor => Can(PermissionIds.TeamContributorsAssign); + public bool CanRevokeTeamContributor => Can(PermissionIds.TeamContributorsRevoke); - public bool CanRevokeContributor => Can(PermissionIds.AppContributorsRevoke); + // Workflows + public bool CanCreateWorkflow => Can(PermissionIds.AppWorkflowsCreate); - public bool CanRevokeTeamContributor => Can(PermissionIds.TeamContributorsRevoke); + public bool CanUpdateWorkflow => Can(PermissionIds.AppWorkflowsUpdate); - // Workflows - public bool CanCreateWorkflow => Can(PermissionIds.AppWorkflowsCreate); + public bool CanDeleteWorkflow => Can(PermissionIds.AppWorkflowsDelete); - public bool CanUpdateWorkflow => Can(PermissionIds.AppWorkflowsUpdate); + // Roles + public bool CanCreateRole => Can(PermissionIds.AppRolesCreate); - public bool CanDeleteWorkflow => Can(PermissionIds.AppWorkflowsDelete); + public bool CanUpdateRole => Can(PermissionIds.AppRolesUpdate); - // Roles - public bool CanCreateRole => Can(PermissionIds.AppRolesCreate); + public bool CanDeleteRole => Can(PermissionIds.AppRolesDelete); - public bool CanUpdateRole => Can(PermissionIds.AppRolesUpdate); + // Languages + public bool CanCreateLanguage => Can(PermissionIds.AppLanguagesCreate); - public bool CanDeleteRole => Can(PermissionIds.AppRolesDelete); + public bool CanUpdateLanguage => Can(PermissionIds.AppLanguagesUpdate); - // Languages - public bool CanCreateLanguage => Can(PermissionIds.AppLanguagesCreate); + public bool CanDeleteLanguage => Can(PermissionIds.AppLanguagesDelete); - public bool CanUpdateLanguage => Can(PermissionIds.AppLanguagesUpdate); + // Clients + public bool CanCreateClient => Can(PermissionIds.AppClientsCreate); - public bool CanDeleteLanguage => Can(PermissionIds.AppLanguagesDelete); + public bool CanUpdateClient => Can(PermissionIds.AppClientsUpdate); - // Clients - public bool CanCreateClient => Can(PermissionIds.AppClientsCreate); + public bool CanDeleteClient => Can(PermissionIds.AppClientsDelete); - public bool CanUpdateClient => Can(PermissionIds.AppClientsUpdate); + // Rules + public bool CanDisableRule => Can(PermissionIds.AppRulesDisable); - public bool CanDeleteClient => Can(PermissionIds.AppClientsDelete); + public bool CanCreateRule => Can(PermissionIds.AppRulesCreate); - // Rules - public bool CanDisableRule => Can(PermissionIds.AppRulesDisable); + public bool CanUpdateRule => Can(PermissionIds.AppRulesUpdate); - public bool CanCreateRule => Can(PermissionIds.AppRulesCreate); + public bool CanDeleteRule => Can(PermissionIds.AppRulesDelete); - public bool CanUpdateRule => Can(PermissionIds.AppRulesUpdate); + public bool CanReadRuleEvents => Can(PermissionIds.AppRulesEventsRead); - public bool CanDeleteRule => Can(PermissionIds.AppRulesDelete); + public bool CanUpdateRuleEvents => Can(PermissionIds.AppRulesEventsUpdate); - public bool CanReadRuleEvents => Can(PermissionIds.AppRulesEventsRead); + public bool CanRunRuleEvents => Can(PermissionIds.AppRulesEventsRun); - public bool CanUpdateRuleEvents => Can(PermissionIds.AppRulesEventsUpdate); + public bool CanDeleteRuleEvents => Can(PermissionIds.AppRulesEventsDelete); - public bool CanRunRuleEvents => Can(PermissionIds.AppRulesEventsRun); + // Users + public bool CanReadUsers => Can(PermissionIds.AdminUsersRead); - public bool CanDeleteRuleEvents => Can(PermissionIds.AppRulesEventsDelete); + public bool CanCreateUser => Can(PermissionIds.AdminUsersCreate); - // Users - public bool CanReadUsers => Can(PermissionIds.AdminUsersRead); + public bool CanLockUser => Can(PermissionIds.AdminUsersLock); - public bool CanCreateUser => Can(PermissionIds.AdminUsersCreate); + public bool CanUnlockUser => Can(PermissionIds.AdminUsersUnlock); - public bool CanLockUser => Can(PermissionIds.AdminUsersLock); + public bool CanUpdateUser => Can(PermissionIds.AdminUsersUpdate); - public bool CanUnlockUser => Can(PermissionIds.AdminUsersUnlock); + // Assets + public bool CanUploadAsset => Can(PermissionIds.AppAssetsUpload); - public bool CanUpdateUser => Can(PermissionIds.AdminUsersUpdate); + public bool CanCreateAsset => Can(PermissionIds.AppAssetsCreate); - // Assets - public bool CanUploadAsset => Can(PermissionIds.AppAssetsUpload); + public bool CanDeleteAsset => Can(PermissionIds.AppAssetsDelete); - public bool CanCreateAsset => Can(PermissionIds.AppAssetsCreate); + public bool CanUpdateAsset => Can(PermissionIds.AppAssetsUpdate); - public bool CanDeleteAsset => Can(PermissionIds.AppAssetsDelete); + public bool CanReadAssets => Can(PermissionIds.AppAssetsRead); - public bool CanUpdateAsset => Can(PermissionIds.AppAssetsUpdate); + // Events + public bool CanReadEvents => Can(PermissionIds.AdminEventsRead); - public bool CanReadAssets => Can(PermissionIds.AppAssetsRead); + public bool CanManageEvents => Can(PermissionIds.AdminEventsManage); - // Events - public bool CanReadEvents => Can(PermissionIds.AdminEventsRead); + // Plans + public bool CanChangePlan => Can(PermissionIds.AppPlansChange); - public bool CanManageEvents => Can(PermissionIds.AdminEventsManage); + public bool CanChangeTeamPlan => Can(PermissionIds.TeamPlansChange); - // Plans - public bool CanChangePlan => Can(PermissionIds.AppPlansChange); + // Backups + public bool CanRestoreBackup => Can(PermissionIds.AdminRestore); - public bool CanChangeTeamPlan => Can(PermissionIds.TeamPlansChange); + public bool CanCreateBackup => Can(PermissionIds.AppBackupsCreate); - // Backups - public bool CanRestoreBackup => Can(PermissionIds.AdminRestore); + public bool CanDeleteBackup => Can(PermissionIds.AppBackupsDelete); - public bool CanCreateBackup => Can(PermissionIds.AppBackupsCreate); + public bool CanDownloadBackup => Can(PermissionIds.AppBackupsDownload); - public bool CanDeleteBackup => Can(PermissionIds.AppBackupsDelete); + public Context Context { get; set; } - public bool CanDownloadBackup => Can(PermissionIds.AppBackupsDownload); + public string? App => GetAppName(); - public Context Context { get; set; } + public string? Schema => GetAppName(); - public string? App => GetAppName(); + public string? Team => GetTeamId().ToString(); - public string? Schema => GetAppName(); + public DomainId AppId => GetAppId(); - public string? Team => GetTeamId().ToString(); + public ApiController Controller { get; } + + public Resources(ApiController controller) + { + Controller = controller; + Context = controller.HttpContext.Context(); + } - public DomainId AppId => GetAppId(); + public string Url<T>(Func<T?, string> action, object? values = null) where T : ApiController + { + var url = Controller.Url(action, values); - public ApiController Controller { get; } + var basePath = Controller.HttpContext.Request.PathBase; - public Resources(ApiController controller) + if (url.StartsWith(Controller.HttpContext.Request.PathBase, StringComparison.OrdinalIgnoreCase)) { - Controller = controller; - Context = controller.HttpContext.Context(); + url = url[basePath.Value!.Length..]; } - public string Url<T>(Func<T?, string> action, object? values = null) where T : ApiController - { - var url = Controller.Url(action, values); + return url; + } - var basePath = Controller.HttpContext.Request.PathBase; + public bool IsUser(string userId) + { + var subject = Controller.User.OpenIdSubject(); - if (url.StartsWith(Controller.HttpContext.Request.PathBase, StringComparison.OrdinalIgnoreCase)) - { - url = url[basePath.Value!.Length..]; - } + return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); + } - return url; - } + public bool Includes(Permission permission, PermissionSet? additional = null) + { + return Context.UserPermissions.Includes(permission) || additional?.Includes(permission) == true; + } - public bool IsUser(string userId) - { - var subject = Controller.User.OpenIdSubject(); + public bool Can(string id) + { + return permissions.GetOrAdd((Id: id, string.Empty), k => IsAllowed(k.Id, Permission.Any, Permission.Any)); + } - return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); - } + public bool Can(string id, string schema) + { + return permissions.GetOrAdd((Id: id, Schema: schema), k => IsAllowed(k.Id, Permission.Any, k.Schema)); + } - public bool Includes(Permission permission, PermissionSet? additional = null) + public bool IsAllowed(string id, string app = Permission.Any, string schema = Permission.Any, string team = Permission.Any, PermissionSet? additional = null) + { + if (app == Permission.Any) { - return Context.UserPermissions.Includes(permission) || additional?.Includes(permission) == true; - } + var fallback = GetAppName(); - public bool Can(string id) - { - return permissions.GetOrAdd((Id: id, string.Empty), k => IsAllowed(k.Id, Permission.Any, Permission.Any)); + if (!string.IsNullOrWhiteSpace(fallback)) + { + app = fallback; + } } - public bool Can(string id, string schema) + if (schema == Permission.Any) { - return permissions.GetOrAdd((Id: id, Schema: schema), k => IsAllowed(k.Id, Permission.Any, k.Schema)); - } + var fallback = GetSchemaName(); - public bool IsAllowed(string id, string app = Permission.Any, string schema = Permission.Any, string team = Permission.Any, PermissionSet? additional = null) - { - if (app == Permission.Any) + if (!string.IsNullOrWhiteSpace(fallback)) { - var fallback = GetAppName(); - - if (!string.IsNullOrWhiteSpace(fallback)) - { - app = fallback; - } + schema = fallback; } + } - if (schema == Permission.Any) - { - var fallback = GetSchemaName(); - - if (!string.IsNullOrWhiteSpace(fallback)) - { - schema = fallback; - } - } + if (team == Permission.Any) + { + var fallback = GetTeamId(); - if (team == Permission.Any) + if (fallback != default) { - var fallback = GetTeamId(); - - if (fallback != default) - { - team = fallback.ToString(); - } + team = fallback.ToString(); } + } - var permission = PermissionIds.ForApp(id, app, schema, team); + var permission = PermissionIds.ForApp(id, app, schema, team); - return Context.UserPermissions.Allows(permission) || additional?.Allows(permission) == true; - } + return Context.UserPermissions.Allows(permission) || additional?.Allows(permission) == true; + } - private string? GetAppName() - { - return Controller.HttpContext.Context().App?.Name; - } + private string? GetAppName() + { + return Controller.HttpContext.Context().App?.Name; + } - private string? GetSchemaName() - { - return Controller.HttpContext.Features.Get<ISchemaFeature>()?.Schema.SchemaDef.Name; - } + private string? GetSchemaName() + { + return Controller.HttpContext.Features.Get<ISchemaFeature>()?.Schema.SchemaDef.Name; + } - private DomainId GetAppId() - { - return Controller.HttpContext.Context().App?.Id ?? default; - } + private DomainId GetAppId() + { + return Controller.HttpContext.Context().App?.Id ?? default; + } - private DomainId GetTeamId() - { - return Controller.HttpContext.Features.Get<ITeamFeature>()?.Team?.Id ?? default; - } + private DomainId GetTeamId() + { + return Controller.HttpContext.Features.Get<ITeamFeature>()?.Team?.Id ?? default; } } diff --git a/backend/src/Squidex.Web/SchemaMustBePublishedAttribute.cs b/backend/src/Squidex.Web/SchemaMustBePublishedAttribute.cs index aeee779e56..bcbe26cc0b 100644 --- a/backend/src/Squidex.Web/SchemaMustBePublishedAttribute.cs +++ b/backend/src/Squidex.Web/SchemaMustBePublishedAttribute.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Web +namespace Squidex.Web; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class SchemaMustBePublishedAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public sealed class SchemaMustBePublishedAttribute : Attribute - { - } } diff --git a/backend/src/Squidex.Web/Services/StringLocalizer.cs b/backend/src/Squidex.Web/Services/StringLocalizer.cs index 52b57c0986..4f28d7d316 100644 --- a/backend/src/Squidex.Web/Services/StringLocalizer.cs +++ b/backend/src/Squidex.Web/Services/StringLocalizer.cs @@ -10,99 +10,98 @@ using Squidex.Infrastructure.Translations; using Squidex.Text; -namespace Squidex.Web.Services +namespace Squidex.Web.Services; + +public sealed class StringLocalizer : IStringLocalizer, IStringLocalizerFactory { - public sealed class StringLocalizer : IStringLocalizer, IStringLocalizerFactory - { - private readonly ILocalizer translationService; - private readonly CultureInfo? culture; + private readonly ILocalizer translationService; + private readonly CultureInfo? culture; - public LocalizedString this[string name] - { - get => this[name, null!]; - } + public LocalizedString this[string name] + { + get => this[name, null!]; + } - public LocalizedString this[string name, params object[] arguments] + public LocalizedString this[string name, params object[] arguments] + { + get { - get + if (string.IsNullOrWhiteSpace(name)) { - if (string.IsNullOrWhiteSpace(name)) - { - return new LocalizedString(name, name, false); - } + return new LocalizedString(name, name, false); + } - var currentCulture = culture ?? CultureInfo.CurrentUICulture; + var currentCulture = culture ?? CultureInfo.CurrentUICulture; - TranslateProperty(name, arguments, currentCulture); + TranslateProperty(name, arguments, currentCulture); - var (result, found) = translationService.Get(currentCulture, $"dotnet_{name}", name); + var (result, found) = translationService.Get(currentCulture, $"dotnet_{name}", name); - if (arguments != null && found) + if (arguments != null && found) + { + try { - try - { - result = string.Format(currentCulture, result, arguments); - } - catch (FormatException) - { - return new LocalizedString(name, name, true); - } + result = string.Format(currentCulture, result, arguments); + } + catch (FormatException) + { + return new LocalizedString(name, name, true); } - - return new LocalizedString(name, result, !found); } + + return new LocalizedString(name, result, !found); } + } - private void TranslateProperty(string name, object[] arguments, CultureInfo currentCulture) + private void TranslateProperty(string name, object[] arguments, CultureInfo currentCulture) + { + if (arguments?.Length == 1 && IsValidationError(name)) { - if (arguments?.Length == 1 && IsValidationError(name)) - { - var key = $"common.{arguments[0].ToString()?.ToCamelCase()}"; + var key = $"common.{arguments[0].ToString()?.ToCamelCase()}"; - var (result, found) = translationService.Get(currentCulture, key, name); + var (result, found) = translationService.Get(currentCulture, key, name); - if (found) - { - arguments[0] = result; - } + if (found) + { + arguments[0] = result; } } + } - public StringLocalizer(ILocalizer translationService) - : this(translationService, null) - { - } + public StringLocalizer(ILocalizer translationService) + : this(translationService, null) + { + } - private StringLocalizer(ILocalizer translationService, CultureInfo? culture) - { - this.translationService = translationService; + private StringLocalizer(ILocalizer translationService, CultureInfo? culture) + { + this.translationService = translationService; - this.culture = culture; - } + this.culture = culture; + } - public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) - { - return Enumerable.Empty<LocalizedString>(); - } + public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) + { + return Enumerable.Empty<LocalizedString>(); + } - public IStringLocalizer WithCulture(CultureInfo culture) - { - return new StringLocalizer(translationService, culture); - } + public IStringLocalizer WithCulture(CultureInfo culture) + { + return new StringLocalizer(translationService, culture); + } - public IStringLocalizer Create(string baseName, string location) - { - return this; - } + public IStringLocalizer Create(string baseName, string location) + { + return this; + } - public IStringLocalizer Create(Type resourceSource) - { - return this; - } + public IStringLocalizer Create(Type resourceSource) + { + return this; + } - private static bool IsValidationError(string name) - { - return name.Contains("annotations_", StringComparison.OrdinalIgnoreCase); - } + private static bool IsValidationError(string name) + { + return name.Contains("annotations_", StringComparison.OrdinalIgnoreCase); } } diff --git a/backend/src/Squidex.Web/Services/UrlGenerator.cs b/backend/src/Squidex.Web/Services/UrlGenerator.cs index 260f398166..3b1ae62652 100644 --- a/backend/src/Squidex.Web/Services/UrlGenerator.cs +++ b/backend/src/Squidex.Web/Services/UrlGenerator.cs @@ -13,158 +13,157 @@ using Squidex.Infrastructure; using IGenericUrlGenerator = Squidex.Hosting.IUrlGenerator; -namespace Squidex.Web.Services +namespace Squidex.Web.Services; + +public sealed class UrlGenerator : IUrlGenerator { - public sealed class UrlGenerator : IUrlGenerator + private readonly IAssetFileStore assetFileStore; + private readonly IGenericUrlGenerator urlGenerator; + private readonly AssetOptions assetOptions; + private readonly ContentOptions contentOptions; + + public UrlGenerator(IGenericUrlGenerator urlGenerator, IAssetFileStore assetFileStore, + IOptions<AssetOptions> assetOptions, + IOptions<ContentOptions> contentOptions) { - private readonly IAssetFileStore assetFileStore; - private readonly IGenericUrlGenerator urlGenerator; - private readonly AssetOptions assetOptions; - private readonly ContentOptions contentOptions; + this.contentOptions = contentOptions.Value; + this.assetFileStore = assetFileStore; + this.assetOptions = assetOptions.Value; + this.urlGenerator = urlGenerator; + } - public UrlGenerator(IGenericUrlGenerator urlGenerator, IAssetFileStore assetFileStore, - IOptions<AssetOptions> assetOptions, - IOptions<ContentOptions> contentOptions) + public string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType) + { + if (assetType != AssetType.Image) { - this.contentOptions = contentOptions.Value; - this.assetFileStore = assetFileStore; - this.assetOptions = assetOptions.Value; - this.urlGenerator = urlGenerator; + return null; } - public string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType) - { - if (assetType != AssetType.Image) - { - return null; - } - - return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}?width=100&mode=Max"); - } + return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}?width=100&mode=Max"); + } - public string AssetContentCDNBase() - { - return assetOptions.CDN ?? string.Empty; - } + public string AssetContentCDNBase() + { + return assetOptions.CDN ?? string.Empty; + } - public string AssetContentBase() - { - return urlGenerator.BuildUrl("api/assets/"); - } + public string AssetContentBase() + { + return urlGenerator.BuildUrl("api/assets/"); + } - public string AssetContentBase(string appName) - { - return urlGenerator.BuildUrl($"api/assets/{appName}/"); - } + public string AssetContentBase(string appName) + { + return urlGenerator.BuildUrl($"api/assets/{appName}/"); + } - public string AssetContent(NamedId<DomainId> appId, DomainId assetId) - { - return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{assetId}"); - } + public string AssetContent(NamedId<DomainId> appId, DomainId assetId) + { + return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{assetId}"); + } - public string AssetContent(NamedId<DomainId> appId, string idOrSlug) - { - return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}"); - } + public string AssetContent(NamedId<DomainId> appId, string idOrSlug) + { + return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}"); + } - public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion) - { - return assetFileStore.GeneratePublicUrl(appId.Id, assetId, fileVersion, null); - } + public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion) + { + return assetFileStore.GeneratePublicUrl(appId.Id, assetId, fileVersion, null); + } - public string AssetsUI(NamedId<DomainId> appId, string? @ref = null) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/assets", false) + @ref != null ? $"?ref={@ref}" : string.Empty; - } + public string AssetsUI(NamedId<DomainId> appId, string? @ref = null) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/assets", false) + @ref != null ? $"?ref={@ref}" : string.Empty; + } - public string BackupsUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/settings/backups", false); - } + public string BackupsUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/settings/backups", false); + } - public string ClientsUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/settings/clients", false); - } + public string ClientsUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/settings/clients", false); + } - public string ContentCDNBase() - { - return contentOptions.CDN ?? string.Empty; - } + public string ContentCDNBase() + { + return contentOptions.CDN ?? string.Empty; + } - public string ContentBase() - { - return urlGenerator.BuildUrl("api/content/", false); - } + public string ContentBase() + { + return urlGenerator.BuildUrl("api/content/", false); + } - public string ContentsUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}", false); - } + public string ContentsUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}", false); + } - public string ContentUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId, DomainId contentId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history", false); - } + public string ContentUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId, DomainId contentId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history", false); + } - public string ContributorsUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/settings/contributors", false); - } + public string ContributorsUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/settings/contributors", false); + } - public string DashboardUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}", false); - } + public string DashboardUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}", false); + } - public string LanguagesUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/settings/languages", false); - } + public string LanguagesUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/settings/languages", false); + } - public string PatternsUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/settings/patterns", false); - } + public string PatternsUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/settings/patterns", false); + } - public string PlansUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/settings/plans", false); - } + public string PlansUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/settings/plans", false); + } - public string RolesUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/settings/roles", false); - } + public string RolesUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/settings/roles", false); + } - public string Root() - { - return urlGenerator.BuildUrl(); - } + public string Root() + { + return urlGenerator.BuildUrl(); + } - public string RulesUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/rules", false); - } + public string RulesUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/rules", false); + } - public string SchemasUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/schemas", false); - } + public string SchemasUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/schemas", false); + } - public string SchemaUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/schemas/{schemaId.Name}", false); - } + public string SchemaUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/schemas/{schemaId.Name}", false); + } - public string WorkflowsUI(NamedId<DomainId> appId) - { - return urlGenerator.BuildUrl($"app/{appId.Name}/settings/workflows", false); - } + public string WorkflowsUI(NamedId<DomainId> appId) + { + return urlGenerator.BuildUrl($"app/{appId.Name}/settings/workflows", false); + } - public string UI() - { - return urlGenerator.BuildUrl("app", false); - } + public string UI() + { + return urlGenerator.BuildUrl("app", false); } } diff --git a/backend/src/Squidex.Web/SquidexWeb.cs b/backend/src/Squidex.Web/SquidexWeb.cs index 7c2358d5d1..908c5519dc 100644 --- a/backend/src/Squidex.Web/SquidexWeb.cs +++ b/backend/src/Squidex.Web/SquidexWeb.cs @@ -9,10 +9,9 @@ #pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class SquidexWeb { - public sealed class SquidexWeb - { - public static readonly Assembly Assembly = typeof(SquidexWeb).Assembly; - } + public static readonly Assembly Assembly = typeof(SquidexWeb).Assembly; } diff --git a/backend/src/Squidex.Web/UrlDecodeRouteParamsAttribute.cs b/backend/src/Squidex.Web/UrlDecodeRouteParamsAttribute.cs index a515a9808b..e6abddbf02 100644 --- a/backend/src/Squidex.Web/UrlDecodeRouteParamsAttribute.cs +++ b/backend/src/Squidex.Web/UrlDecodeRouteParamsAttribute.cs @@ -8,18 +8,17 @@ using System.Web; using Microsoft.AspNetCore.Mvc.Filters; -namespace Squidex.Web +namespace Squidex.Web; + +public class UrlDecodeRouteParamsAttribute : ActionFilterAttribute { - public class UrlDecodeRouteParamsAttribute : ActionFilterAttribute + public override void OnActionExecuting(ActionExecutingContext context) { - public override void OnActionExecuting(ActionExecutingContext context) + foreach (var (key, value) in context.ActionArguments.ToList()) { - foreach (var (key, value) in context.ActionArguments.ToList()) + if (value is string text) { - if (value is string text) - { - context.ActionArguments[key] = HttpUtility.UrlDecode(text); - } + context.ActionArguments[key] = HttpUtility.UrlDecode(text); } } } diff --git a/backend/src/Squidex.Web/UrlHelperExtensions.cs b/backend/src/Squidex.Web/UrlHelperExtensions.cs index a45fc55e60..ac8dccb948 100644 --- a/backend/src/Squidex.Web/UrlHelperExtensions.cs +++ b/backend/src/Squidex.Web/UrlHelperExtensions.cs @@ -9,37 +9,36 @@ #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Web +namespace Squidex.Web; + +public static class UrlHelperExtensions { - public static class UrlHelperExtensions + private static class NameOf<T> { - private static class NameOf<T> - { - public static readonly string Controller; - - static NameOf() - { - const string suffix = "Controller"; + public static readonly string Controller; - var name = typeof(T).Name; + static NameOf() + { + const string suffix = "Controller"; - if (name.EndsWith(suffix, StringComparison.Ordinal)) - { - name = name[..^suffix.Length]; - } + var name = typeof(T).Name; - Controller = name; + if (name.EndsWith(suffix, StringComparison.Ordinal)) + { + name = name[..^suffix.Length]; } - } - public static string Url<T>(this IUrlHelper urlHelper, Func<T?, string> action, object? values = null) where T : Controller - { - return urlHelper.Action(action(null), NameOf<T>.Controller, values)!; + Controller = name; } + } - public static string Url<T>(this Controller controller, Func<T?, string> action, object? values = null) where T : Controller - { - return controller.Url.Url(action, values); - } + public static string Url<T>(this IUrlHelper urlHelper, Func<T?, string> action, object? values = null) where T : Controller + { + return urlHelper.Action(action(null), NameOf<T>.Controller, values)!; + } + + public static string Url<T>(this Controller controller, Func<T?, string> action, object? values = null) where T : Controller + { + return controller.Url.Url(action, values); } } diff --git a/backend/src/Squidex.Web/UsageOptions.cs b/backend/src/Squidex.Web/UsageOptions.cs index 0a001710c7..e4ca16db69 100644 --- a/backend/src/Squidex.Web/UsageOptions.cs +++ b/backend/src/Squidex.Web/UsageOptions.cs @@ -7,10 +7,9 @@ using Squidex.Domain.Apps.Entities.Billing; -namespace Squidex.Web +namespace Squidex.Web; + +public sealed class UsageOptions { - public sealed class UsageOptions - { - public Plan[] Plans { get; set; } - } + public Plan[] Plans { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs index 336b14d465..f06c833d07 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs @@ -11,43 +11,42 @@ using Squidex.Hosting; using Squidex.Web; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class CommonProcessor : IDocumentProcessor { - public sealed class CommonProcessor : IDocumentProcessor + private readonly string version; + private readonly string logoBackground = "#3f83df"; + private readonly string logoUrl; + + private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation { - private readonly string version; - private readonly string logoBackground = "#3f83df"; - private readonly string logoUrl; + Url = "https://docs.squidex.io" + }; - private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation - { - Url = "https://docs.squidex.io" - }; + public CommonProcessor(ExposedValues exposedValues, IUrlGenerator urlGenerator) + { + logoUrl = urlGenerator.BuildUrl("images/logo-white.png", false); - public CommonProcessor(ExposedValues exposedValues, IUrlGenerator urlGenerator) + if (!exposedValues.TryGetValue("version", out version!) || version == null) { - logoUrl = urlGenerator.BuildUrl("images/logo-white.png", false); - - if (!exposedValues.TryGetValue("version", out version!) || version == null) - { - version = "1.0"; - } + version = "1.0"; } + } - public void Process(DocumentProcessorContext context) + public void Process(DocumentProcessorContext context) + { + context.Document.Info.Version = version; + context.Document.Info.ExtensionData = new Dictionary<string, object> { - context.Document.Info.Version = version; - context.Document.Info.ExtensionData = new Dictionary<string, object> + ["x-logo"] = new { - ["x-logo"] = new - { - url = logoUrl, - backgroundStyle = string.Empty, - backgroundColor = logoBackground - } - }; - - context.Document.ExternalDocumentation = documentation; - } + url = logoUrl, + backgroundStyle = string.Empty, + backgroundColor = logoBackground + } + }; + + context.Document.ExternalDocumentation = documentation; } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs index 739eb24b63..96505be813 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs @@ -12,60 +12,59 @@ using NSwag.Generation.Processors.Contexts; using Squidex.Web; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class ErrorDtoProcessor : IDocumentProcessor { - public sealed class ErrorDtoProcessor : IDocumentProcessor + public void Process(DocumentProcessorContext context) { - public void Process(DocumentProcessorContext context) - { - var errorSchema = GetErrorSchema(context); + var errorSchema = GetErrorSchema(context); - foreach (var operation in context.Document.Paths.Values.SelectMany(x => x.Values)) - { - AddErrorResponses(operation, errorSchema); + foreach (var operation in context.Document.Paths.Values.SelectMany(x => x.Values)) + { + AddErrorResponses(operation, errorSchema); - CleanupResponses(operation); - } + CleanupResponses(operation); } + } - private static void AddErrorResponses(OpenApiOperation operation, JsonSchema errorSchema) + private static void AddErrorResponses(OpenApiOperation operation, JsonSchema errorSchema) + { + if (!operation.Responses.ContainsKey("500")) { - if (!operation.Responses.ContainsKey("500")) - { - const string description = "Operation failed."; + const string description = "Operation failed."; - var response = new OpenApiResponse { Description = description, Schema = errorSchema }; + var response = new OpenApiResponse { Description = description, Schema = errorSchema }; - operation.Responses["500"] = response; - } + operation.Responses["500"] = response; + } - foreach (var (code, response) in operation.Responses) + foreach (var (code, response) in operation.Responses) + { + if (code != "404" && code.StartsWith("4", StringComparison.OrdinalIgnoreCase) && response.Schema == null) { - if (code != "404" && code.StartsWith("4", StringComparison.OrdinalIgnoreCase) && response.Schema == null) - { - response.Schema = errorSchema; - } + response.Schema = errorSchema; } } + } - private static void CleanupResponses(OpenApiOperation operation) + private static void CleanupResponses(OpenApiOperation operation) + { + foreach (var (code, response) in operation.Responses.ToList()) { - foreach (var (code, response) in operation.Responses.ToList()) + if (string.IsNullOrWhiteSpace(response.Description) || + response.Description?.Contains("=>", StringComparison.Ordinal) == true || + response.Description?.Contains("=>", StringComparison.Ordinal) == true) { - if (string.IsNullOrWhiteSpace(response.Description) || - response.Description?.Contains("=>", StringComparison.Ordinal) == true || - response.Description?.Contains("=>", StringComparison.Ordinal) == true) - { - operation.Responses.Remove(code); - } + operation.Responses.Remove(code); } } + } - private static JsonSchema GetErrorSchema(DocumentProcessorContext context) - { - var errorType = typeof(ErrorDto).ToContextualType(); + private static JsonSchema GetErrorSchema(DocumentProcessorContext context) + { + var errorType = typeof(ErrorDto).ToContextualType(); - return context.SchemaGenerator.GenerateWithReference<JsonSchema>(errorType, context.SchemaResolver); - } + return context.SchemaGenerator.GenerateWithReference<JsonSchema>(errorType, context.SchemaResolver); } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs index 6cb476b812..446ad1778e 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs @@ -9,23 +9,22 @@ using NSwag.Generation.Processors; using NSwag.Generation.Processors.Contexts; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class FixProcessor : IOperationProcessor { - public sealed class FixProcessor : IOperationProcessor - { - private static readonly JsonSchema StringSchema = new JsonSchema { Type = JsonObjectType.String }; + private static readonly JsonSchema StringSchema = new JsonSchema { Type = JsonObjectType.String }; - public bool Process(OperationProcessorContext context) + public bool Process(OperationProcessorContext context) + { + foreach (var (_, parameter) in context.Parameters) { - foreach (var (_, parameter) in context.Parameters) + if (parameter.IsRequired && parameter.Schema is { Type: JsonObjectType.String }) { - if (parameter.IsRequired && parameter.Schema is { Type: JsonObjectType.String }) - { - parameter.Schema = StringSchema; - } + parameter.Schema = StringSchema; } - - return true; } + + return true; } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs index 6eaabac3f8..63c4e4b2c6 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs @@ -19,139 +19,138 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public static class OpenApiServices { - public static class OpenApiServices + public static void AddSquidexOpenApiSettings(this IServiceCollection services) { - public static void AddSquidexOpenApiSettings(this IServiceCollection services) - { - services.AddSingletonAs<ErrorDtoProcessor>() - .As<IDocumentProcessor>(); + services.AddSingletonAs<ErrorDtoProcessor>() + .As<IDocumentProcessor>(); - services.AddSingletonAs<RuleActionProcessor>() - .As<IDocumentProcessor>(); + services.AddSingletonAs<RuleActionProcessor>() + .As<IDocumentProcessor>(); - services.AddSingletonAs<CommonProcessor>() - .As<IDocumentProcessor>(); + services.AddSingletonAs<CommonProcessor>() + .As<IDocumentProcessor>(); - services.AddSingletonAs<XmlTagProcessor>() - .As<IDocumentProcessor>(); + services.AddSingletonAs<XmlTagProcessor>() + .As<IDocumentProcessor>(); - services.AddSingletonAs<SecurityProcessor>() - .As<IDocumentProcessor>(); + services.AddSingletonAs<SecurityProcessor>() + .As<IDocumentProcessor>(); - services.AddSingletonAs<ScopesProcessor>() - .As<IOperationProcessor>(); + services.AddSingletonAs<ScopesProcessor>() + .As<IOperationProcessor>(); - services.AddSingletonAs<FixProcessor>() - .As<IOperationProcessor>(); + services.AddSingletonAs<FixProcessor>() + .As<IOperationProcessor>(); - services.AddSingletonAs<TagByGroupNameProcessor>() - .As<IOperationProcessor>(); + services.AddSingletonAs<TagByGroupNameProcessor>() + .As<IOperationProcessor>(); - services.AddSingletonAs<XmlResponseTypesProcessor>() - .As<IOperationProcessor>(); + services.AddSingletonAs<XmlResponseTypesProcessor>() + .As<IOperationProcessor>(); - services.AddSingletonAs<JsonSchemaGenerator>() - .AsSelf(); + services.AddSingletonAs<JsonSchemaGenerator>() + .AsSelf(); - services.AddSingletonAs<OpenApiSchemaGenerator>() - .AsSelf(); - - services.AddSingleton(c => - { - var settings = new JsonSchemaGeneratorSettings(); + services.AddSingletonAs<OpenApiSchemaGenerator>() + .AsSelf(); - ConfigureSchemaSettings(settings, true); - - return settings; - }); + services.AddSingleton(c => + { + var settings = new JsonSchemaGeneratorSettings(); - services.AddSingleton(c => - { - var settings = new OpenApiDocumentGeneratorSettings(); + ConfigureSchemaSettings(settings, true); - ConfigureSchemaSettings(settings, true); + return settings; + }); - foreach (var processor in c.GetRequiredService<IEnumerable<IDocumentProcessor>>()) - { - settings.DocumentProcessors.Add(processor); - } + services.AddSingleton(c => + { + var settings = new OpenApiDocumentGeneratorSettings(); - return settings; - }); + ConfigureSchemaSettings(settings, true); - services.AddOpenApiDocument(settings => + foreach (var processor in c.GetRequiredService<IEnumerable<IDocumentProcessor>>()) { - settings.Title = "Squidex API"; - - ConfigureSchemaSettings(settings); + settings.DocumentProcessors.Add(processor); + } - settings.OperationProcessors.Add(new QueryParamsProcessor("/api/apps/{app}/assets")); - }); - } + return settings; + }); - private static void ConfigureSchemaSettings(JsonSchemaGeneratorSettings settings, bool flatten = false) + services.AddOpenApiDocument(settings => { - settings.AllowReferencesWithProperties = true; + settings.Title = "Squidex API"; - settings.ReflectionService = new ReflectionServices(); + ConfigureSchemaSettings(settings); - settings.TypeMappers = new List<ITypeMapper> - { - CreateStringMap<DomainId>(), - CreateStringMap<Instant>(JsonFormatStrings.DateTime), - CreateStringMap<LocalDate>(JsonFormatStrings.Date), - CreateStringMap<LocalDateTime>(JsonFormatStrings.DateTime), - CreateStringMap<Language>(), - CreateStringMap<NamedId<DomainId>>(), - CreateStringMap<NamedId<Guid>>(), - CreateStringMap<NamedId<string>>(), - CreateStringMap<RefToken>(), - CreateStringMap<Status>(), - - CreateObjectMap<JsonObject>(), - CreateObjectMap<AssetMetadata>(), - - CreateAnyMap<JsonDocument>(), - CreateAnyMap<JsonValue>(), - CreateAnyMap<FilterNode<JsonValue>>() - }; + settings.OperationProcessors.Add(new QueryParamsProcessor("/api/apps/{app}/assets")); + }); + } + + private static void ConfigureSchemaSettings(JsonSchemaGeneratorSettings settings, bool flatten = false) + { + settings.AllowReferencesWithProperties = true; - settings.SchemaType = SchemaType.OpenApi3; + settings.ReflectionService = new ReflectionServices(); - settings.FlattenInheritanceHierarchy = flatten; - } + settings.TypeMappers = new List<ITypeMapper> + { + CreateStringMap<DomainId>(), + CreateStringMap<Instant>(JsonFormatStrings.DateTime), + CreateStringMap<LocalDate>(JsonFormatStrings.Date), + CreateStringMap<LocalDateTime>(JsonFormatStrings.DateTime), + CreateStringMap<Language>(), + CreateStringMap<NamedId<DomainId>>(), + CreateStringMap<NamedId<Guid>>(), + CreateStringMap<NamedId<string>>(), + CreateStringMap<RefToken>(), + CreateStringMap<Status>(), + + CreateObjectMap<JsonObject>(), + CreateObjectMap<AssetMetadata>(), + + CreateAnyMap<JsonDocument>(), + CreateAnyMap<JsonValue>(), + CreateAnyMap<FilterNode<JsonValue>>() + }; + + settings.SchemaType = SchemaType.OpenApi3; + + settings.FlattenInheritanceHierarchy = flatten; + } - private static ITypeMapper CreateObjectMap<T>() + private static ITypeMapper CreateObjectMap<T>() + { + return new PrimitiveTypeMapper(typeof(T), schema => { - return new PrimitiveTypeMapper(typeof(T), schema => - { - schema.Type = JsonObjectType.Object; + schema.Type = JsonObjectType.Object; - schema.AdditionalPropertiesSchema = new JsonSchema - { - Description = "Any" - }; - }); - } + schema.AdditionalPropertiesSchema = new JsonSchema + { + Description = "Any" + }; + }); + } - private static ITypeMapper CreateStringMap<T>(string? format = null) + private static ITypeMapper CreateStringMap<T>(string? format = null) + { + return new PrimitiveTypeMapper(typeof(T), schema => { - return new PrimitiveTypeMapper(typeof(T), schema => - { - schema.Type = JsonObjectType.String; + schema.Type = JsonObjectType.String; - schema.Format = format; - }); - } + schema.Format = format; + }); + } - private static ITypeMapper CreateAnyMap<T>() + private static ITypeMapper CreateAnyMap<T>() + { + return new PrimitiveTypeMapper(typeof(T), schema => { - return new PrimitiveTypeMapper(typeof(T), schema => - { - schema.Type = JsonObjectType.None; - }); - } + schema.Type = JsonObjectType.None; + }); } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs index 0d984f7e96..d157a180e8 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs @@ -9,85 +9,84 @@ using NSwag; using Squidex.Domain.Apps.Core; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public static class QueryExtensions { - public static class QueryExtensions + public static void AddQuery(this OpenApiOperation operation, bool supportSearch) { - public static void AddQuery(this OpenApiOperation operation, bool supportSearch) + var stringSchema = new JsonSchema { - var stringSchema = new JsonSchema - { - Type = JsonObjectType.String - }; + Type = JsonObjectType.String + }; - var numberSchema = new JsonSchema - { - Type = JsonObjectType.Number - }; + var numberSchema = new JsonSchema + { + Type = JsonObjectType.Number + }; - void AddParameterQuery(OpenApiParameter parameter) + void AddParameterQuery(OpenApiParameter parameter) + { + if (operation.Parameters.Any(x => x.Name == parameter.Name && x.Kind == OpenApiParameterKind.Query)) { - if (operation.Parameters.Any(x => x.Name == parameter.Name && x.Kind == OpenApiParameterKind.Query)) - { - return; - } - - parameter.Kind = OpenApiParameterKind.Query; - - operation.Parameters.Add(parameter); + return; } - if (supportSearch) - { - AddParameterQuery(new OpenApiParameter - { - Schema = stringSchema, - Name = "$search", - Description = FieldDescriptions.QuerySkip - }); - } + parameter.Kind = OpenApiParameterKind.Query; - AddParameterQuery(new OpenApiParameter - { - Schema = numberSchema, - Name = "$top", - Description = FieldDescriptions.QueryTop - }); + operation.Parameters.Add(parameter); + } + if (supportSearch) + { AddParameterQuery(new OpenApiParameter { - Schema = numberSchema, - Name = "$skip", + Schema = stringSchema, + Name = "$search", Description = FieldDescriptions.QuerySkip }); + } - AddParameterQuery(new OpenApiParameter - { - Schema = stringSchema, - Name = "$orderby", - Description = FieldDescriptions.QueryOrderBy - }); + AddParameterQuery(new OpenApiParameter + { + Schema = numberSchema, + Name = "$top", + Description = FieldDescriptions.QueryTop + }); - AddParameterQuery(new OpenApiParameter - { - Schema = stringSchema, - Name = "$filter", - Description = FieldDescriptions.QueryFilter - }); + AddParameterQuery(new OpenApiParameter + { + Schema = numberSchema, + Name = "$skip", + Description = FieldDescriptions.QuerySkip + }); - AddParameterQuery(new OpenApiParameter - { - Schema = stringSchema, - Name = "q", - Description = FieldDescriptions.QueryQ - }); + AddParameterQuery(new OpenApiParameter + { + Schema = stringSchema, + Name = "$orderby", + Description = FieldDescriptions.QueryOrderBy + }); - AddParameterQuery(new OpenApiParameter - { - Schema = stringSchema, - Name = "ids", - Description = FieldDescriptions.QueryIds - }); - } + AddParameterQuery(new OpenApiParameter + { + Schema = stringSchema, + Name = "$filter", + Description = FieldDescriptions.QueryFilter + }); + + AddParameterQuery(new OpenApiParameter + { + Schema = stringSchema, + Name = "q", + Description = FieldDescriptions.QueryQ + }); + + AddParameterQuery(new OpenApiParameter + { + Schema = stringSchema, + Name = "ids", + Description = FieldDescriptions.QueryIds + }); } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryParamsProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryParamsProcessor.cs index 769e89cac7..050603ac48 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryParamsProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/QueryParamsProcessor.cs @@ -9,27 +9,26 @@ using NSwag.Generation.Processors; using NSwag.Generation.Processors.Contexts; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class QueryParamsProcessor : IOperationProcessor { - public sealed class QueryParamsProcessor : IOperationProcessor - { - private readonly string path; + private readonly string path; - public QueryParamsProcessor(string path) - { - this.path = path; - } + public QueryParamsProcessor(string path) + { + this.path = path; + } - public bool Process(OperationProcessorContext context) + public bool Process(OperationProcessorContext context) + { + if (context.OperationDescription.Path == path && context.OperationDescription.Method == OpenApiOperationMethod.Get) { - if (context.OperationDescription.Path == path && context.OperationDescription.Method == OpenApiOperationMethod.Get) - { - var operation = context.OperationDescription.Operation; + var operation = context.OperationDescription.Operation; - operation.AddQuery(false); - } - - return true; + operation.AddQuery(false); } + + return true; } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ReflectionServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ReflectionServices.cs index 96ef390201..8d01254419 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/ReflectionServices.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ReflectionServices.cs @@ -9,30 +9,29 @@ using NJsonSchema.Generation; using Squidex.Infrastructure.Collections; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public class ReflectionServices : DefaultReflectionService { - public class ReflectionServices : DefaultReflectionService + protected override bool IsArrayType(ContextualType contextualType) { - protected override bool IsArrayType(ContextualType contextualType) + if (contextualType.Type.IsGenericType && + contextualType.Type.GetGenericTypeDefinition() == typeof(ReadonlyList<>)) { - if (contextualType.Type.IsGenericType && - contextualType.Type.GetGenericTypeDefinition() == typeof(ReadonlyList<>)) - { - return true; - } - - return base.IsArrayType(contextualType); + return true; } - protected override bool IsDictionaryType(ContextualType contextualType) - { - if (contextualType.Type.IsGenericType && - contextualType.Type.GetGenericTypeDefinition() == typeof(ReadonlyDictionary<,>)) - { - return true; - } + return base.IsArrayType(contextualType); + } - return base.IsDictionaryType(contextualType); + protected override bool IsDictionaryType(ContextualType contextualType) + { + if (contextualType.Type.IsGenericType && + contextualType.Type.GetGenericTypeDefinition() == typeof(ReadonlyDictionary<,>)) + { + return true; } + + return base.IsDictionaryType(contextualType); } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs index 374e76941a..6b719e97c2 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs @@ -12,42 +12,41 @@ using NSwag.Generation.Processors.Contexts; using Squidex.Web; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class ScopesProcessor : IOperationProcessor { - public sealed class ScopesProcessor : IOperationProcessor + public bool Process(OperationProcessorContext context) { - public bool Process(OperationProcessorContext context) - { - context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>(); + context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>(); - var permissionAttribute = context.MethodInfo.GetCustomAttribute<ApiPermissionAttribute>(); + var permissionAttribute = context.MethodInfo.GetCustomAttribute<ApiPermissionAttribute>(); - if (permissionAttribute != null) + if (permissionAttribute != null) + { + context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement { - context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement - { - [Constants.SecurityDefinition] = permissionAttribute.PermissionIds - }); - } - else + [Constants.SecurityDefinition] = permissionAttribute.PermissionIds + }); + } + else + { + var authorizeAttributes = + context.MethodInfo.GetCustomAttributes<AuthorizeAttribute>(true).Union( + context.MethodInfo.DeclaringType!.GetCustomAttributes<AuthorizeAttribute>(true)) + .ToArray(); + + if (authorizeAttributes.Any()) { - var authorizeAttributes = - context.MethodInfo.GetCustomAttributes<AuthorizeAttribute>(true).Union( - context.MethodInfo.DeclaringType!.GetCustomAttributes<AuthorizeAttribute>(true)) - .ToArray(); + var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles!.Split(',')).Distinct().ToList(); - if (authorizeAttributes.Any()) + context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement { - var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles!.Split(',')).Distinct().ToList(); - - context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement - { - [Constants.SecurityDefinition] = scopes - }); - } + [Constants.SecurityDefinition] = scopes + }); } - - return true; } + + return true; } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs index af8550073c..eca5c110dd 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs @@ -10,51 +10,50 @@ using Squidex.Hosting; using Squidex.Web; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class SecurityProcessor : SecurityDefinitionAppender { - public sealed class SecurityProcessor : SecurityDefinitionAppender + public SecurityProcessor(IUrlGenerator urlGenerator) + : base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlGenerator)) { - public SecurityProcessor(IUrlGenerator urlGenerator) - : base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlGenerator)) - { - } + } - private static OpenApiSecurityScheme CreateOAuthSchema(IUrlGenerator urlGenerator) + private static OpenApiSecurityScheme CreateOAuthSchema(IUrlGenerator urlGenerator) + { + var security = new OpenApiSecurityScheme { - var security = new OpenApiSecurityScheme - { - Type = OpenApiSecuritySchemeType.OAuth2 - }; + Type = OpenApiSecuritySchemeType.OAuth2 + }; - var tokenUrl = urlGenerator.BuildUrl($"/{Constants.PrefixIdentityServer}/connect/token", false); + var tokenUrl = urlGenerator.BuildUrl($"/{Constants.PrefixIdentityServer}/connect/token", false); - security.TokenUrl = tokenUrl; + security.TokenUrl = tokenUrl; - SetupDescription(security, tokenUrl); - SetupFlow(security); - SetupScopes(security); + SetupDescription(security, tokenUrl); + SetupFlow(security); + SetupScopes(security); - return security; - } + return security; + } - private static void SetupFlow(OpenApiSecurityScheme security) - { - security.Flow = OpenApiOAuth2Flow.Application; - } + private static void SetupFlow(OpenApiSecurityScheme security) + { + security.Flow = OpenApiOAuth2Flow.Application; + } - private static void SetupScopes(OpenApiSecurityScheme security) + private static void SetupScopes(OpenApiSecurityScheme security) + { + security.Scopes = new Dictionary<string, string> { - security.Scopes = new Dictionary<string, string> - { - [Constants.ScopeApi] = "Read and write access to the API" - }; - } + [Constants.ScopeApi] = "Read and write access to the API" + }; + } - private static void SetupDescription(OpenApiSecurityScheme securityScheme, string tokenUrl) - { - var securityText = Properties.Resources.OpenApiSecurity.Replace("<TOKEN_URL>", tokenUrl, StringComparison.Ordinal); + private static void SetupDescription(OpenApiSecurityScheme securityScheme, string tokenUrl) + { + var securityText = Properties.Resources.OpenApiSecurity.Replace("<TOKEN_URL>", tokenUrl, StringComparison.Ordinal); - securityScheme.Description = securityText; - } + securityScheme.Description = securityText; } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs index 93060205d7..dee2edf208 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs @@ -10,24 +10,23 @@ using NSwag.Generation.Processors; using NSwag.Generation.Processors.Contexts; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class TagByGroupNameProcessor : IOperationProcessor { - public sealed class TagByGroupNameProcessor : IOperationProcessor + public bool Process(OperationProcessorContext context) { - public bool Process(OperationProcessorContext context) - { - var groupName = context.ControllerType.GetCustomAttribute<ApiExplorerSettingsAttribute>()?.GroupName; + var groupName = context.ControllerType.GetCustomAttribute<ApiExplorerSettingsAttribute>()?.GroupName; - if (!string.IsNullOrWhiteSpace(groupName)) - { - context.OperationDescription.Operation.Tags = new List<string> { groupName }; + if (!string.IsNullOrWhiteSpace(groupName)) + { + context.OperationDescription.Operation.Tags = new List<string> { groupName }; - return true; - } - else - { - return false; - } + return true; + } + else + { + return false; } } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs index dffc8d6239..9e739b214c 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs @@ -12,44 +12,43 @@ using NSwag.Generation.Processors.Contexts; using Squidex.Infrastructure; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class XmlResponseTypesProcessor : IOperationProcessor { - public sealed class XmlResponseTypesProcessor : IOperationProcessor - { - private static readonly Regex ResponseRegex = new Regex("(?<Code>[0-9]{3})[\\s]*=((>)|>)[\\s]*(?<Description>.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly Regex ResponseRegex = new Regex("(?<Code>[0-9]{3})[\\s]*=((>)|>)[\\s]*(?<Description>.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - public bool Process(OperationProcessorContext context) - { - var operation = context.OperationDescription.Operation; + public bool Process(OperationProcessorContext context) + { + var operation = context.OperationDescription.Operation; - var returnsDescription = context.MethodInfo.GetXmlDocsTag("returns"); + var returnsDescription = context.MethodInfo.GetXmlDocsTag("returns"); - if (!string.IsNullOrWhiteSpace(returnsDescription)) + if (!string.IsNullOrWhiteSpace(returnsDescription)) + { + foreach (var match in ResponseRegex.Matches(returnsDescription).OfType<Match>()) { - foreach (var match in ResponseRegex.Matches(returnsDescription).OfType<Match>()) - { - var statusCode = match.Groups["Code"].Value; - - if (!operation.Responses.TryGetValue(statusCode, out var response)) - { - response = new OpenApiResponse(); + var statusCode = match.Groups["Code"].Value; - operation.Responses[statusCode] = response; - } + if (!operation.Responses.TryGetValue(statusCode, out var response)) + { + response = new OpenApiResponse(); - var description = match.Groups["Description"].Value; + operation.Responses[statusCode] = response; + } - if (description.Contains("=>", StringComparison.Ordinal)) - { - ThrowHelper.InvalidOperationException("Description not formatted correcly."); - return default!; - } + var description = match.Groups["Description"].Value; - response.Description = description; + if (description.Contains("=>", StringComparison.Ordinal)) + { + ThrowHelper.InvalidOperationException("Description not formatted correcly."); + return default!; } - } - return true; + response.Description = description; + } } + + return true; } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs index 9d4c938a97..083b1ec816 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs @@ -11,43 +11,42 @@ using NSwag.Generation.Processors; using NSwag.Generation.Processors.Contexts; -namespace Squidex.Areas.Api.Config.OpenApi +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class XmlTagProcessor : IDocumentProcessor { - public sealed class XmlTagProcessor : IDocumentProcessor + public void Process(DocumentProcessorContext context) { - public void Process(DocumentProcessorContext context) + try { - try + foreach (var controllerType in context.ControllerTypes) { - foreach (var controllerType in context.ControllerTypes) + var attribute = controllerType.GetCustomAttribute<ApiExplorerSettingsAttribute>(); + + if (attribute != null) { - var attribute = controllerType.GetCustomAttribute<ApiExplorerSettingsAttribute>(); + var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); - if (attribute != null) + if (tag != null) { - var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName); + var description = controllerType.GetXmlDocsSummary(); - if (tag != null) + if (description != null) { - var description = controllerType.GetXmlDocsSummary(); + tag.Description ??= string.Empty; - if (description != null) + if (!tag.Description.Contains(description, StringComparison.Ordinal)) { - tag.Description ??= string.Empty; - - if (!tag.Description.Contains(description, StringComparison.Ordinal)) - { - tag.Description += "\n\n" + description; - } + tag.Description += "\n\n" + description; } } } } } - finally - { - XmlDocs.ClearCache(); - } + } + finally + { + XmlDocs.ClearCache(); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs index 7448c381ab..8c8225f81d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs @@ -12,77 +12,76 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppAssetsController : ApiController { + public AppAssetsController(ICommandBus commandBus) + : base(commandBus) + { + } + /// <summary> - /// Update and query apps. + /// Get the app asset scripts. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppAssetsController : ApiController + /// <param name="app">The name of the app to get the asset scripts for.</param> + /// <returns> + /// 200 => Asset scripts returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/assets/scripts")] + [ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetSScriptsRead)] + [ApiCosts(0)] + public IActionResult GetAssetScripts(string app) { - public AppAssetsController(ICommandBus commandBus) - : base(commandBus) + var response = Deferred.Response(() => { - } + return GetResponse(App); + }); - /// <summary> - /// Get the app asset scripts. - /// </summary> - /// <param name="app">The name of the app to get the asset scripts for.</param> - /// <returns> - /// 200 => Asset scripts returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/assets/scripts")] - [ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetSScriptsRead)] - [ApiCosts(0)] - public IActionResult GetAssetScripts(string app) - { - var response = Deferred.Response(() => - { - return GetResponse(App); - }); - - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Update the app asset scripts. - /// </summary> - /// <param name="app">The name of the app to update.</param> - /// <param name="request">The values to update.</param> - /// <returns> - /// 200 => Asset scripts updated. - /// 400 => Asset request not valid. - /// 404 => App not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/assets/scripts")] - [ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsScriptsUpdate)] - [ApiCosts(0)] - public async Task<IActionResult> PutAssetScripts(string app, [FromBody] UpdateAssetScriptsDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); + /// <summary> + /// Update the app asset scripts. + /// </summary> + /// <param name="app">The name of the app to update.</param> + /// <param name="request">The values to update.</param> + /// <returns> + /// 200 => Asset scripts updated. + /// 400 => Asset request not valid. + /// 404 => App not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/assets/scripts")] + [ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsScriptsUpdate)] + [ApiCosts(0)] + public async Task<IActionResult> PutAssetScripts(string app, [FromBody] UpdateAssetScriptsDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); - return Ok(response); - } + return Ok(response); + } - private async Task<AssetScriptsDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + private async Task<AssetScriptsDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var result = context.Result<IAppEntity>(); - var response = GetResponse(result); + var result = context.Result<IAppEntity>(); + var response = GetResponse(result); - return response; - } + return response; + } - private AssetScriptsDto GetResponse(IAppEntity result) - { - return AssetScriptsDto.FromDomain(result, Resources); - } + private AssetScriptsDto GetResponse(IAppEntity result) + { + return AssetScriptsDto.FromDomain(result, Resources); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 8191db0818..241524abab 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -14,142 +14,141 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppClientsController : ApiController { + public AppClientsController(ICommandBus commandBus) + : base(commandBus) + { + } + /// <summary> - /// Update and query apps. + /// Get app clients. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppClientsController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Clients returned. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// Gets all configured clients for the app with the specified name. + /// </remarks> + [HttpGet] + [Route("apps/{app}/clients/")] + [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppClientsRead)] + [ApiCosts(0)] + public IActionResult GetClients(string app) { - public AppClientsController(ICommandBus commandBus) - : base(commandBus) - { - } - - /// <summary> - /// Get app clients. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Clients returned. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// Gets all configured clients for the app with the specified name. - /// </remarks> - [HttpGet] - [Route("apps/{app}/clients/")] - [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppClientsRead)] - [ApiCosts(0)] - public IActionResult GetClients(string app) + var response = Deferred.Response(() => { - var response = Deferred.Response(() => - { - return GetResponse(App); - }); - - Response.Headers[HeaderNames.ETag] = App.ToEtag(); - - return Ok(response); - } - - /// <summary> - /// Create a new app client. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">Client object that needs to be added to the app.</param> - /// <returns> - /// 201 => Client created. - /// 400 => Client request not valid. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// Create a new client for the app with the specified name. - /// The client secret is auto generated on the server and returned. The client does not expire, the access token is valid for 30 days. - /// </remarks> - [HttpPost] - [Route("apps/{app}/clients/")] - [ProducesResponseType(typeof(ClientsDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppClientsCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostClient(string app, [FromBody] CreateClientDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(command); - - return CreatedAtAction(nameof(GetClients), new { app }, response); - } - - /// <summary> - /// Updates an app client. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the client that must be updated.</param> - /// <param name="request">Client object that needs to be updated.</param> - /// <returns> - /// 200 => Client updated. - /// 400 => Client request not valid. - /// 404 => Client or app not found. - /// </returns> - /// <remarks> - /// Only the display name can be changed, create a new client if necessary. - /// </remarks> - [HttpPut] - [Route("apps/{app}/clients/{id}/")] - [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppClientsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutClient(string app, string id, [FromBody] UpdateClientDto request) - { - var command = request.ToCommand(id); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Revoke an app client. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the client that must be deleted.</param> - /// <returns> - /// 200 => Client deleted. - /// 404 => Client or app not found. - /// </returns> - /// <remarks> - /// The application that uses this client credentials cannot access the API after it has been revoked. - /// </remarks> - [HttpDelete] - [Route("apps/{app}/clients/{id}/")] - [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppClientsDelete)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteClient(string app, string id) - { - var command = new RevokeClient { Id = id }; + return GetResponse(App); + }); - var response = await InvokeCommandAsync(command); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); - return Ok(response); - } + return Ok(response); + } - private async Task<ClientsDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + /// <summary> + /// Create a new app client. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">Client object that needs to be added to the app.</param> + /// <returns> + /// 201 => Client created. + /// 400 => Client request not valid. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// Create a new client for the app with the specified name. + /// The client secret is auto generated on the server and returned. The client does not expire, the access token is valid for 30 days. + /// </remarks> + [HttpPost] + [Route("apps/{app}/clients/")] + [ProducesResponseType(typeof(ClientsDto), 201)] + [ApiPermissionOrAnonymous(PermissionIds.AppClientsCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostClient(string app, [FromBody] CreateClientDto request) + { + var command = request.ToCommand(); - var result = context.Result<IAppEntity>(); - var response = GetResponse(result); + var response = await InvokeCommandAsync(command); - return response; - } + return CreatedAtAction(nameof(GetClients), new { app }, response); + } - private ClientsDto GetResponse(IAppEntity app) - { - return ClientsDto.FromApp(app, Resources); - } + /// <summary> + /// Updates an app client. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the client that must be updated.</param> + /// <param name="request">Client object that needs to be updated.</param> + /// <returns> + /// 200 => Client updated. + /// 400 => Client request not valid. + /// 404 => Client or app not found. + /// </returns> + /// <remarks> + /// Only the display name can be changed, create a new client if necessary. + /// </remarks> + [HttpPut] + [Route("apps/{app}/clients/{id}/")] + [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppClientsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutClient(string app, string id, [FromBody] UpdateClientDto request) + { + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Revoke an app client. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the client that must be deleted.</param> + /// <returns> + /// 200 => Client deleted. + /// 404 => Client or app not found. + /// </returns> + /// <remarks> + /// The application that uses this client credentials cannot access the API after it has been revoked. + /// </remarks> + [HttpDelete] + [Route("apps/{app}/clients/{id}/")] + [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppClientsDelete)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteClient(string app, string id) + { + var command = new RevokeClient { Id = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + private async Task<ClientsDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + var result = context.Result<IAppEntity>(); + var response = GetResponse(result); + + return response; + } + + private ClientsDto GetResponse(IAppEntity app) + { + return ClientsDto.FromApp(app, Resources); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 11dc28d83f..38fcef7de5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -17,137 +17,136 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppContributorsController : ApiController { + private readonly IUsageGate usageGate; + private readonly IUserResolver userResolver; + + public AppContributorsController(ICommandBus commandBus, IUsageGate usageGate, IUserResolver userResolver) + : base(commandBus) + { + this.usageGate = usageGate; + this.userResolver = userResolver; + } + /// <summary> - /// Update and query apps. + /// Get app contributors. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppContributorsController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Contributors returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/contributors/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContributorsRead)] + [ApiCosts(0)] + public IActionResult GetContributors(string app) { - private readonly IUsageGate usageGate; - private readonly IUserResolver userResolver; - - public AppContributorsController(ICommandBus commandBus, IUsageGate usageGate, IUserResolver userResolver) - : base(commandBus) + var response = Deferred.AsyncResponse(() => { - this.usageGate = usageGate; - this.userResolver = userResolver; - } + return GetResponseAsync(App, false); + }); - /// <summary> - /// Get app contributors. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Contributors returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/contributors/")] - [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContributorsRead)] - [ApiCosts(0)] - public IActionResult GetContributors(string app) - { - var response = Deferred.AsyncResponse(() => - { - return GetResponseAsync(App, false); - }); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); - Response.Headers[HeaderNames.ETag] = App.ToEtag(); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Assign contributor to app. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">Contributor object that needs to be added to the app.</param> + /// <returns> + /// 201 => Contributor assigned to app. + /// 400 => Contributor request not valid. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/contributors/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)] + [ApiPermissionOrAnonymous(PermissionIds.AppContributorsAssign)] + [ApiCosts(1)] + public async Task<IActionResult> PostContributor(string app, [FromBody] AssignContributorDto request) + { + var command = SimpleMapper.Map(request, new AssignContributor()); - /// <summary> - /// Assign contributor to app. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">Contributor object that needs to be added to the app.</param> - /// <returns> - /// 201 => Contributor assigned to app. - /// 400 => Contributor request not valid. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/contributors/")] - [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)] - [ApiPermissionOrAnonymous(PermissionIds.AppContributorsAssign)] - [ApiCosts(1)] - public async Task<IActionResult> PostContributor(string app, [FromBody] AssignContributorDto request) - { - var command = SimpleMapper.Map(request, new AssignContributor()); + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return CreatedAtAction(nameof(GetContributors), new { app }, response); + } - return CreatedAtAction(nameof(GetContributors), new { app }, response); - } + /// <summary> + /// Remove yourself. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Contributor removed. + /// 404 => Contributor or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/contributors/me/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(1)] + public async Task<IActionResult> DeleteMyself(string app) + { + var command = new RemoveContributor { ContributorId = UserId }; - /// <summary> - /// Remove yourself. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Contributor removed. - /// 404 => Contributor or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/contributors/me/")] - [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(1)] - public async Task<IActionResult> DeleteMyself(string app) - { - var command = new RemoveContributor { ContributorId = UserId }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Remove contributor. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the contributor.</param> + /// <returns> + /// 200 => Contributor removed. + /// 404 => Contributor or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/contributors/{id}/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContributorsRevoke)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteContributor(string app, string id) + { + var command = new RemoveContributor { ContributorId = id }; - /// <summary> - /// Remove contributor. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the contributor.</param> - /// <returns> - /// 200 => Contributor removed. - /// 404 => Contributor or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/contributors/{id}/")] - [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContributorsRevoke)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteContributor(string app, string id) - { - var command = new RemoveContributor { ContributorId = id }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + private async Task<ContributorsDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - private async Task<ContributorsDto> InvokeCommandAsync(ICommand command) + if (context.PlainResult is InvitedResult<IAppEntity> invited) { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - if (context.PlainResult is InvitedResult<IAppEntity> invited) - { - return await GetResponseAsync(invited.Entity, true); - } - else - { - return await GetResponseAsync(context.Result<IAppEntity>(), false); - } + return await GetResponseAsync(invited.Entity, true); } - - private async Task<ContributorsDto> GetResponseAsync(IAppEntity app, bool invited) + else { - var (plan, _, _) = await usageGate.GetPlanForAppAsync(app, HttpContext.RequestAborted); - - return await ContributorsDto.FromDomainAsync(app, Resources, userResolver, plan, invited); + return await GetResponseAsync(context.Result<IAppEntity>(), false); } } + + private async Task<ContributorsDto> GetResponseAsync(IAppEntity app, bool invited) + { + var (plan, _, _) = await usageGate.GetPlanForAppAsync(app, HttpContext.RequestAborted); + + return await ContributorsDto.FromDomainAsync(app, Resources, userResolver, plan, invited); + } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs index 825f20cf9d..1a13b3bbc9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs @@ -14,143 +14,142 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppImageController : ApiController { + private readonly IAppImageStore appImageStore; + private readonly IAssetStore assetStore; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + + public AppImageController(ICommandBus commandBus, + IAppImageStore appImageStore, + IAssetStore assetStore, + IAssetThumbnailGenerator assetThumbnailGenerator) + : base(commandBus) + { + this.appImageStore = appImageStore; + this.assetStore = assetStore; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + /// <summary> - /// Update and query apps. + /// Get the app image. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppImageController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => App image found and content or (resized) image returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [AllowAnonymous] + [ApiCosts(0)] + public IActionResult GetImage(string app) { - private readonly IAppImageStore appImageStore; - private readonly IAssetStore assetStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - - public AppImageController(ICommandBus commandBus, - IAppImageStore appImageStore, - IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator) - : base(commandBus) + if (App.Image == null) { - this.appImageStore = appImageStore; - this.assetStore = assetStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; + return NotFound(); } - /// <summary> - /// Get the app image. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => App image found and content or (resized) image returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] - [AllowAnonymous] - [ApiCosts(0)] - public IActionResult GetImage(string app) - { - if (App.Image == null) - { - return NotFound(); - } + var etag = App.Image.Etag; - var etag = App.Image.Etag; + Response.Headers[HeaderNames.ETag] = etag; - Response.Headers[HeaderNames.ETag] = etag; + var callback = new FileCallback(async (body, range, ct) => + { + var resizedAsset = $"{App.Id}_{etag}_Resized"; - var callback = new FileCallback(async (body, range, ct) => + try { - var resizedAsset = $"{App.Id}_{etag}_Resized"; - - try - { - await assetStore.DownloadAsync(resizedAsset, body, ct: ct); - } - catch (AssetNotFoundException) - { - await ResizeAsync(resizedAsset, App.Image.MimeType, body, ct); - } - }); - - return new FileCallbackResult(App.Image.MimeType, callback) + await assetStore.DownloadAsync(resizedAsset, body, ct: ct); + } + catch (AssetNotFoundException) { - ErrorAs404 = true - }; - } + await ResizeAsync(resizedAsset, App.Image.MimeType, body, ct); + } + }); - private async Task ResizeAsync(string resizedAsset, string mimeType, Stream target, - CancellationToken ct) + return new FileCallbackResult(App.Image.MimeType, callback) { + ErrorAs404 = true + }; + } + + private async Task ResizeAsync(string resizedAsset, string mimeType, Stream target, + CancellationToken ct) + { #pragma warning disable MA0040 // Flow the cancellation token - using var activity = Telemetry.Activities.StartActivity("Resize"); + using var activity = Telemetry.Activities.StartActivity("Resize"); - await using var assetOriginal = new TempAssetFile(resizedAsset, mimeType, 0); - await using var assetResized = new TempAssetFile(resizedAsset, mimeType, 0); + await using var assetOriginal = new TempAssetFile(resizedAsset, mimeType, 0); + await using var assetResized = new TempAssetFile(resizedAsset, mimeType, 0); - var resizeOptions = new ResizeOptions - { - TargetWidth = 50, - TargetHeight = 50 - }; + var resizeOptions = new ResizeOptions + { + TargetWidth = 50, + TargetHeight = 50 + }; - using (Telemetry.Activities.StartActivity("Read")) + using (Telemetry.Activities.StartActivity("Read")) + { + await using (var originalStream = assetOriginal.OpenWrite()) { - await using (var originalStream = assetOriginal.OpenWrite()) - { - await appImageStore.DownloadAsync(App.Id, originalStream, ct); - } + await appImageStore.DownloadAsync(App.Id, originalStream, ct); } + } - using (Telemetry.Activities.StartActivity("Resize")) + using (Telemetry.Activities.StartActivity("Resize")) + { + try { - try + await using (var originalStream = assetOriginal.OpenRead()) { - await using (var originalStream = assetOriginal.OpenRead()) + await using (var resizeStream = assetResized.OpenWrite()) { - await using (var resizeStream = assetResized.OpenWrite()) - { - await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, mimeType, resizeStream, resizeOptions, ct); - } - } - } - catch - { - await using (var originalStream = assetOriginal.OpenRead()) - { - await using (var resizeStream = assetResized.OpenWrite()) - { - await originalStream.CopyToAsync(resizeStream); - } + await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, mimeType, resizeStream, resizeOptions, ct); } } } - - using (Telemetry.Activities.StartActivity("Save")) + catch { - try + await using (var originalStream = assetOriginal.OpenRead()) { - await using (var resizeStream = assetResized.OpenRead()) + await using (var resizeStream = assetResized.OpenWrite()) { - await assetStore.UploadAsync(resizedAsset, resizeStream); + await originalStream.CopyToAsync(resizeStream); } } - catch (AssetAlreadyExistsException) - { - return; - } } + } - using (Telemetry.Activities.StartActivity("Write")) + using (Telemetry.Activities.StartActivity("Save")) + { + try { await using (var resizeStream = assetResized.OpenRead()) { - await resizeStream.CopyToAsync(target, ct); + await assetStore.UploadAsync(resizedAsset, resizeStream); } } -#pragma warning restore MA0040 // Flow the cancellation token + catch (AssetAlreadyExistsException) + { + return; + } } + + using (Telemetry.Activities.StartActivity("Write")) + { + await using (var resizeStream = assetResized.OpenRead()) + { + await resizeStream.CopyToAsync(target, ct); + } + } +#pragma warning restore MA0040 // Flow the cancellation token } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 0c03036481..4750bb40c7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -15,142 +15,141 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppLanguagesController : ApiController { + public AppLanguagesController(ICommandBus commandBus) + : base(commandBus) + { + } + /// <summary> - /// Update and query apps. + /// Get app languages. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppLanguagesController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Languages returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/languages/")] + [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppLanguagesRead)] + [ApiCosts(0)] + public IActionResult GetLanguages(string app) { - public AppLanguagesController(ICommandBus commandBus) - : base(commandBus) + var response = Deferred.Response(() => { - } + return GetResponse(App); + }); - /// <summary> - /// Get app languages. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Languages returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/languages/")] - [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppLanguagesRead)] - [ApiCosts(0)] - public IActionResult GetLanguages(string app) - { - var response = Deferred.Response(() => - { - return GetResponse(App); - }); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); - Response.Headers[HeaderNames.ETag] = App.ToEtag(); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Attaches an app language. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">The language to add to the app.</param> + /// <returns> + /// 201 => Language created. + /// 400 => Language request not valid. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/languages/")] + [ProducesResponseType(typeof(AppLanguagesDto), 201)] + [ApiPermissionOrAnonymous(PermissionIds.AppLanguagesCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostLanguage(string app, [FromBody] AddLanguageDto request) + { + var command = request.ToCommand(); - /// <summary> - /// Attaches an app language. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The language to add to the app.</param> - /// <returns> - /// 201 => Language created. - /// 400 => Language request not valid. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/languages/")] - [ProducesResponseType(typeof(AppLanguagesDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppLanguagesCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostLanguage(string app, [FromBody] AddLanguageDto request) - { - var command = request.ToCommand(); + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return CreatedAtAction(nameof(GetLanguages), new { app }, response); + } - return CreatedAtAction(nameof(GetLanguages), new { app }, response); - } + /// <summary> + /// Updates an app language. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="language">The language to update.</param> + /// <param name="request">The language object.</param> + /// <returns> + /// 200 => Language updated. + /// 400 => Language request not valid. + /// 404 => Language or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/languages/{language}/")] + [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppLanguagesUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutLanguage(string app, string language, [FromBody] UpdateLanguageDto request) + { + var command = request.ToCommand(ParseLanguage(language)); - /// <summary> - /// Updates an app language. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="language">The language to update.</param> - /// <param name="request">The language object.</param> - /// <returns> - /// 200 => Language updated. - /// 400 => Language request not valid. - /// 404 => Language or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/languages/{language}/")] - [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppLanguagesUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutLanguage(string app, string language, [FromBody] UpdateLanguageDto request) - { - var command = request.ToCommand(ParseLanguage(language)); + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Deletes an app language. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="language">The language to delete from the app.</param> + /// <returns> + /// 200 => Language deleted. + /// 400 => Language is master language. + /// 404 => Language or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/languages/{language}/")] + [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppLanguagesDelete)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteLanguage(string app, string language) + { + var command = new RemoveLanguage { Language = ParseLanguage(language) }; - /// <summary> - /// Deletes an app language. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="language">The language to delete from the app.</param> - /// <returns> - /// 200 => Language deleted. - /// 400 => Language is master language. - /// 404 => Language or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/languages/{language}/")] - [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppLanguagesDelete)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteLanguage(string app, string language) - { - var command = new RemoveLanguage { Language = ParseLanguage(language) }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + private async Task<AppLanguagesDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - private async Task<AppLanguagesDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + var result = context.Result<IAppEntity>(); + var response = GetResponse(result); - var result = context.Result<IAppEntity>(); - var response = GetResponse(result); + return response; + } - return response; - } + private AppLanguagesDto GetResponse(IAppEntity result) + { + return AppLanguagesDto.FromDomain(result, Resources); + } - private AppLanguagesDto GetResponse(IAppEntity result) + private static Language ParseLanguage(string language) + { + try { - return AppLanguagesDto.FromDomain(result, Resources); + return Language.GetLanguage(language); } - - private static Language ParseLanguage(string language) + catch (NotSupportedException) { - try - { - return Language.GetLanguage(language); - } - catch (NotSupportedException) - { - throw new DomainObjectNotFoundException(language); - } + throw new DomainObjectNotFoundException(language); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index d1fefd7fcf..c62f0d6ec7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs @@ -15,160 +15,159 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppRolesController : ApiController { + private readonly RolePermissionsProvider permissionsProvider; + + public AppRolesController(ICommandBus commandBus, RolePermissionsProvider permissionsProvider) + : base(commandBus) + { + this.permissionsProvider = permissionsProvider; + } + /// <summary> - /// Update and query apps. + /// Get app roles. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppRolesController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Roles returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/roles/")] + [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRolesRead)] + [ApiCosts(0)] + public IActionResult GetRoles(string app) { - private readonly RolePermissionsProvider permissionsProvider; - - public AppRolesController(ICommandBus commandBus, RolePermissionsProvider permissionsProvider) - : base(commandBus) - { - this.permissionsProvider = permissionsProvider; - } - - /// <summary> - /// Get app roles. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Roles returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/roles/")] - [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRolesRead)] - [ApiCosts(0)] - public IActionResult GetRoles(string app) - { - var response = Deferred.Response(() => - { - return GetResponse(App); - }); - - Response.Headers[HeaderNames.ETag] = App.ToEtag(); - - return Ok(response); - } - - /// <summary> - /// Get app permissions. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => App permissions returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/roles/permissions")] - [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRolesRead)] - [ApiCosts(0)] - public IActionResult GetPermissions(string app) - { - var response = Deferred.AsyncResponse(() => - { - return permissionsProvider.GetPermissionsAsync(App); - }); - - Response.Headers[HeaderNames.ETag] = string.Concat(response).ToSha256Base64(); - - return Ok(response); - } - - /// <summary> - /// Add role to app. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">Role object that needs to be added to the app.</param> - /// <returns> - /// 201 => Role created. - /// 400 => Role request not valid. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/roles/")] - [ProducesResponseType(typeof(RolesDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppRolesCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostRole(string app, [FromBody] AddRoleDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(command); - - return CreatedAtAction(nameof(GetRoles), new { app }, response); - } - - /// <summary> - /// Update an app role. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="roleName">The name of the role to be updated.</param> - /// <param name="request">Role to be updated for the app.</param> - /// <returns> - /// 200 => Role updated. - /// 400 => Role request not valid. - /// 404 => Role or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/roles/{roleName}/")] - [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRolesUpdate)] - [ApiCosts(1)] - [UrlDecodeRouteParams] - public async Task<IActionResult> PutRole(string app, string roleName, [FromBody] UpdateRoleDto request) - { - var command = request.ToCommand(roleName); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Remove role from app. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="roleName">The name of the role.</param> - /// <returns> - /// 200 => Role deleted. - /// 400 => Role is in use by contributor or client or a default role. - /// 404 => Role or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/roles/{roleName}/")] - [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRolesDelete)] - [ApiCosts(1)] - [UrlDecodeRouteParams] - public async Task<IActionResult> DeleteRole(string app, string roleName) + var response = Deferred.Response(() => { - var command = new DeleteRole { Name = roleName }; + return GetResponse(App); + }); - var response = await InvokeCommandAsync(command); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); - return Ok(response); - } + return Ok(response); + } - private async Task<RolesDto> InvokeCommandAsync(ICommand command) + /// <summary> + /// Get app permissions. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => App permissions returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/roles/permissions")] + [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRolesRead)] + [ApiCosts(0)] + public IActionResult GetPermissions(string app) + { + var response = Deferred.AsyncResponse(() => { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return permissionsProvider.GetPermissionsAsync(App); + }); - var result = context.Result<IAppEntity>(); - var response = GetResponse(result); + Response.Headers[HeaderNames.ETag] = string.Concat(response).ToSha256Base64(); - return response; - } + return Ok(response); + } - private RolesDto GetResponse(IAppEntity result) - { - return RolesDto.FromDomain(result, Resources); - } + /// <summary> + /// Add role to app. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">Role object that needs to be added to the app.</param> + /// <returns> + /// 201 => Role created. + /// 400 => Role request not valid. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/roles/")] + [ProducesResponseType(typeof(RolesDto), 201)] + [ApiPermissionOrAnonymous(PermissionIds.AppRolesCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostRole(string app, [FromBody] AddRoleDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(command); + + return CreatedAtAction(nameof(GetRoles), new { app }, response); + } + + /// <summary> + /// Update an app role. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="roleName">The name of the role to be updated.</param> + /// <param name="request">Role to be updated for the app.</param> + /// <returns> + /// 200 => Role updated. + /// 400 => Role request not valid. + /// 404 => Role or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/roles/{roleName}/")] + [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRolesUpdate)] + [ApiCosts(1)] + [UrlDecodeRouteParams] + public async Task<IActionResult> PutRole(string app, string roleName, [FromBody] UpdateRoleDto request) + { + var command = request.ToCommand(roleName); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Remove role from app. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="roleName">The name of the role.</param> + /// <returns> + /// 200 => Role deleted. + /// 400 => Role is in use by contributor or client or a default role. + /// 404 => Role or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/roles/{roleName}/")] + [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRolesDelete)] + [ApiCosts(1)] + [UrlDecodeRouteParams] + public async Task<IActionResult> DeleteRole(string app, string roleName) + { + var command = new DeleteRole { Name = roleName }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + private async Task<RolesDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + var result = context.Result<IAppEntity>(); + var response = GetResponse(result); + + return response; + } + + private RolesDto GetResponse(IAppEntity result) + { + return RolesDto.FromDomain(result, Resources); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs index df0603a926..ff0dd84343 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs @@ -12,77 +12,76 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppSettingsController : ApiController { + public AppSettingsController(ICommandBus commandBus) + : base(commandBus) + { + } + /// <summary> - /// Update and query apps. + /// Get the app settings. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppSettingsController : ApiController + /// <param name="app">The name of the app to get the settings for.</param> + /// <returns> + /// 200 => App settings returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/settings")] + [ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(0)] + public IActionResult GetSettings(string app) { - public AppSettingsController(ICommandBus commandBus) - : base(commandBus) + var response = Deferred.Response(() => { - } + return GetResponse(App); + }); - /// <summary> - /// Get the app settings. - /// </summary> - /// <param name="app">The name of the app to get the settings for.</param> - /// <returns> - /// 200 => App settings returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/settings")] - [ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(0)] - public IActionResult GetSettings(string app) - { - var response = Deferred.Response(() => - { - return GetResponse(App); - }); - - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Update the app settings. - /// </summary> - /// <param name="app">The name of the app to update.</param> - /// <param name="request">The values to update.</param> - /// <returns> - /// 200 => App updated. - /// 400 => App request not valid. - /// 404 => App not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/settings")] - [ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppUpdateSettings)] - [ApiCosts(0)] - public async Task<IActionResult> PutSettings(string app, [FromBody] UpdateAppSettingsDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); + /// <summary> + /// Update the app settings. + /// </summary> + /// <param name="app">The name of the app to update.</param> + /// <param name="request">The values to update.</param> + /// <returns> + /// 200 => App updated. + /// 400 => App request not valid. + /// 404 => App not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/settings")] + [ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppUpdateSettings)] + [ApiCosts(0)] + public async Task<IActionResult> PutSettings(string app, [FromBody] UpdateAppSettingsDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); - return Ok(response); - } + return Ok(response); + } - private async Task<AppSettingsDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + private async Task<AppSettingsDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var result = context.Result<IAppEntity>(); - var response = GetResponse(result); + var result = context.Result<IAppEntity>(); + var response = GetResponse(result); - return response; - } + return response; + } - private AppSettingsDto GetResponse(IAppEntity result) - { - return AppSettingsDto.FromDomain(result, Resources); - } + private AppSettingsDto GetResponse(IAppEntity result) + { + return AppSettingsDto.FromDomain(result, Resources); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index c8bc747d27..26ebf8e841 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -16,132 +16,131 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppWorkflowsController : ApiController { + private readonly IWorkflowsValidator workflowsValidator; + + public AppWorkflowsController(ICommandBus commandBus, IWorkflowsValidator workflowsValidator) + : base(commandBus) + { + this.workflowsValidator = workflowsValidator; + } + /// <summary> - /// Update and query apps. + /// Get app workflow. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppWorkflowsController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Workflows returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/workflows/")] + [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppWorkflowsRead)] + [ApiCosts(0)] + public IActionResult GetWorkflows(string app) { - private readonly IWorkflowsValidator workflowsValidator; - - public AppWorkflowsController(ICommandBus commandBus, IWorkflowsValidator workflowsValidator) - : base(commandBus) - { - this.workflowsValidator = workflowsValidator; - } - - /// <summary> - /// Get app workflow. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Workflows returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/workflows/")] - [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppWorkflowsRead)] - [ApiCosts(0)] - public IActionResult GetWorkflows(string app) - { - var response = Deferred.AsyncResponse(() => - { - return GetResponse(App); - }); - - Response.Headers[HeaderNames.ETag] = App.ToEtag(); - - return Ok(response); - } - - /// <summary> - /// Create a workflow. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The new workflow.</param> - /// <returns> - /// 200 => Workflow created. - /// 400 => Workflow request not valid. - /// 404 => Workflow or app not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/workflows/")] - [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppWorkflowsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PostWorkflow(string app, [FromBody] AddWorkflowDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Update a workflow. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the workflow to update.</param> - /// <param name="request">The new workflow.</param> - /// <returns> - /// 200 => Workflow updated. - /// 400 => Workflow request not valid. - /// 404 => Workflow or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/workflows/{id}")] - [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppWorkflowsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutWorkflow(string app, DomainId id, [FromBody] UpdateWorkflowDto request) - { - var command = request.ToCommand(id); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Delete a workflow. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the workflow to update.</param> - /// <returns> - /// 200 => Workflow deleted. - /// 404 => Workflow or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/workflows/{id}")] - [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppWorkflowsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteWorkflow(string app, DomainId id) + var response = Deferred.AsyncResponse(() => { - var command = new DeleteWorkflow { WorkflowId = id }; + return GetResponse(App); + }); - var response = await InvokeCommandAsync(command); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); - return Ok(response); - } + return Ok(response); + } - private async Task<WorkflowsDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + /// <summary> + /// Create a workflow. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">The new workflow.</param> + /// <returns> + /// 200 => Workflow created. + /// 400 => Workflow request not valid. + /// 404 => Workflow or app not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/workflows/")] + [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppWorkflowsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PostWorkflow(string app, [FromBody] AddWorkflowDto request) + { + var command = request.ToCommand(); - var result = context.Result<IAppEntity>(); - var response = await GetResponse(result); + var response = await InvokeCommandAsync(command); - return response; - } + return Ok(response); + } - private async Task<WorkflowsDto> GetResponse(IAppEntity result) - { - return await WorkflowsDto.FromAppAsync(workflowsValidator, result, Resources); - } + /// <summary> + /// Update a workflow. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the workflow to update.</param> + /// <param name="request">The new workflow.</param> + /// <returns> + /// 200 => Workflow updated. + /// 400 => Workflow request not valid. + /// 404 => Workflow or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/workflows/{id}")] + [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppWorkflowsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutWorkflow(string app, DomainId id, [FromBody] UpdateWorkflowDto request) + { + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Delete a workflow. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the workflow to update.</param> + /// <returns> + /// 200 => Workflow deleted. + /// 404 => Workflow or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/workflows/{id}")] + [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppWorkflowsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteWorkflow(string app, DomainId id) + { + var command = new DeleteWorkflow { WorkflowId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + private async Task<WorkflowsDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + var result = context.Result<IAppEntity>(); + var response = await GetResponse(result); + + return response; + } + + private async Task<WorkflowsDto> GetResponse(IAppEntity result) + { + return await WorkflowsDto.FromAppAsync(workflowsValidator, result, Resources); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 577f42f217..164851699d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -18,271 +18,270 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps +namespace Squidex.Areas.Api.Controllers.Apps; + +/// <summary> +/// Update and query apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Apps))] +public sealed class AppsController : ApiController { + private readonly IAppProvider appProvider; + + public AppsController(ICommandBus commandBus, IAppProvider appProvider) + : base(commandBus) + { + this.appProvider = appProvider; + } + /// <summary> - /// Update and query apps. + /// Get your apps. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Apps))] - public sealed class AppsController : ApiController + /// <returns> + /// 200 => Apps returned. + /// </returns> + /// <remarks> + /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). + /// You will retrieve all apps, where you are assigned as a contributor. + /// </remarks> + [HttpGet] + [Route("apps/")] + [ProducesResponseType(typeof(AppDto[]), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public async Task<IActionResult> GetApps() { - private readonly IAppProvider appProvider; + var userOrClientId = UserOrClientId!; + var userPermissions = Resources.Context.UserPermissions; - public AppsController(ICommandBus commandBus, IAppProvider appProvider) - : base(commandBus) - { - this.appProvider = appProvider; - } + var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions, HttpContext.RequestAborted); - /// <summary> - /// Get your apps. - /// </summary> - /// <returns> - /// 200 => Apps returned. - /// </returns> - /// <remarks> - /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). - /// You will retrieve all apps, where you are assigned as a contributor. - /// </remarks> - [HttpGet] - [Route("apps/")] - [ProducesResponseType(typeof(AppDto[]), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(0)] - public async Task<IActionResult> GetApps() + var response = Deferred.Response(() => { - var userOrClientId = UserOrClientId!; - var userPermissions = Resources.Context.UserPermissions; + return apps.OrderBy(x => x.Name).Select(a => AppDto.FromDomain(a, userOrClientId, IsFrontend, Resources)).ToArray(); + }); - var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions, HttpContext.RequestAborted); + Response.Headers[HeaderNames.ETag] = apps.ToEtag(); - var response = Deferred.Response(() => - { - return apps.OrderBy(x => x.Name).Select(a => AppDto.FromDomain(a, userOrClientId, IsFrontend, Resources)).ToArray(); - }); - - Response.Headers[HeaderNames.ETag] = apps.ToEtag(); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Get team apps. + /// </summary> + /// <param name="team">The ID of the team.</param> + /// <returns> + /// 200 => Apps returned. + /// </returns> + /// <remarks> + /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). + /// You will retrieve all apps, where you are assigned as a contributor. + /// </remarks> + [HttpGet] + [Route("teams/{team}/apps")] + [ProducesResponseType(typeof(AppDto[]), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public async Task<IActionResult> GetTeamApps(string team) + { + var apps = await appProvider.GetTeamAppsAsync(Team.Id, HttpContext.RequestAborted); - /// <summary> - /// Get team apps. - /// </summary> - /// <param name="team">The ID of the team.</param> - /// <returns> - /// 200 => Apps returned. - /// </returns> - /// <remarks> - /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). - /// You will retrieve all apps, where you are assigned as a contributor. - /// </remarks> - [HttpGet] - [Route("teams/{team}/apps")] - [ProducesResponseType(typeof(AppDto[]), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(0)] - public async Task<IActionResult> GetTeamApps(string team) + var response = Deferred.Response(() => { - var apps = await appProvider.GetTeamAppsAsync(Team.Id, HttpContext.RequestAborted); + return apps.OrderBy(x => x.Name).Select(a => AppDto.FromDomain(a, UserOrClientId, IsFrontend, Resources)).ToArray(); + }); - var response = Deferred.Response(() => - { - return apps.OrderBy(x => x.Name).Select(a => AppDto.FromDomain(a, UserOrClientId, IsFrontend, Resources)).ToArray(); - }); + Response.Headers[HeaderNames.ETag] = apps.ToEtag(); - Response.Headers[HeaderNames.ETag] = apps.ToEtag(); - - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Get an app by name. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Apps returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}")] - [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(0)] - public IActionResult GetApp(string app) + /// <summary> + /// Get an app by name. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Apps returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}")] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public IActionResult GetApp(string app) + { + var response = Deferred.Response(() => { - var response = Deferred.Response(() => - { - var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); + var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); - return AppDto.FromDomain(App, UserOrClientId, IsFrontend, Resources); - }); + return AppDto.FromDomain(App, UserOrClientId, IsFrontend, Resources); + }); - Response.Headers[HeaderNames.ETag] = App.ToEtag(); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Create a new app. - /// </summary> - /// <param name="request">The app object that needs to be added to Squidex.</param> - /// <returns> - /// 201 => App created. - /// 400 => App request not valid. - /// 409 => App name is already in use. - /// </returns> - /// <remarks> - /// You can only create an app when you are authenticated as a user (OpenID implicit flow). - /// You will be assigned as owner of the new app automatically. - /// </remarks> - [HttpPost] - [Route("apps/")] - [ProducesResponseType(typeof(AppDto), 201)] - [ApiPermission] - [ApiCosts(0)] - public async Task<IActionResult> PostApp([FromBody] CreateAppDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); + /// <summary> + /// Create a new app. + /// </summary> + /// <param name="request">The app object that needs to be added to Squidex.</param> + /// <returns> + /// 201 => App created. + /// 400 => App request not valid. + /// 409 => App name is already in use. + /// </returns> + /// <remarks> + /// You can only create an app when you are authenticated as a user (OpenID implicit flow). + /// You will be assigned as owner of the new app automatically. + /// </remarks> + [HttpPost] + [Route("apps/")] + [ProducesResponseType(typeof(AppDto), 201)] + [ApiPermission] + [ApiCosts(0)] + public async Task<IActionResult> PostApp([FromBody] CreateAppDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); - return CreatedAtAction(nameof(GetApps), response); - } + return CreatedAtAction(nameof(GetApps), response); + } - /// <summary> - /// Update the app. - /// </summary> - /// <param name="app">The name of the app to update.</param> - /// <param name="request">The values to update.</param> - /// <returns> - /// 200 => App updated. - /// 400 => App request not valid. - /// 404 => App not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/")] - [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppUpdate)] - [ApiCosts(0)] - public async Task<IActionResult> PutApp(string app, [FromBody] UpdateAppDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); + /// <summary> + /// Update the app. + /// </summary> + /// <param name="app">The name of the app to update.</param> + /// <param name="request">The values to update.</param> + /// <returns> + /// 200 => App updated. + /// 400 => App request not valid. + /// 404 => App not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/")] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppUpdate)] + [ApiCosts(0)] + public async Task<IActionResult> PutApp(string app, [FromBody] UpdateAppDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Transfer the app. - /// </summary> - /// <param name="app">The name of the app to update.</param> - /// <param name="request">The team information.</param> - /// <returns> - /// 200 => App transferred. - /// 400 => App request not valid. - /// 404 => App not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/team")] - [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppTransfer)] - [ApiCosts(0)] - public async Task<IActionResult> PutAppTeam(string app, [FromBody] TransferToTeamDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); + /// <summary> + /// Transfer the app. + /// </summary> + /// <param name="app">The name of the app to update.</param> + /// <param name="request">The team information.</param> + /// <returns> + /// 200 => App transferred. + /// 400 => App request not valid. + /// 404 => App not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/team")] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppTransfer)] + [ApiCosts(0)] + public async Task<IActionResult> PutAppTeam(string app, [FromBody] TransferToTeamDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Upload the app image. - /// </summary> - /// <param name="app">The name of the app to update.</param> - /// <param name="file">The file to upload.</param> - /// <returns> - /// 200 => App image uploaded. - /// 400 => App request not valid. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppImageUpload)] - [ApiCosts(0)] - public async Task<IActionResult> UploadImage(string app, IFormFile file) - { - var response = await InvokeCommandAsync(CreateCommand(file)); + /// <summary> + /// Upload the app image. + /// </summary> + /// <param name="app">The name of the app to update.</param> + /// <param name="file">The file to upload.</param> + /// <returns> + /// 200 => App image uploaded. + /// 400 => App request not valid. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppImageUpload)] + [ApiCosts(0)] + public async Task<IActionResult> UploadImage(string app, IFormFile file) + { + var response = await InvokeCommandAsync(CreateCommand(file)); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Remove the app image. - /// </summary> - /// <param name="app">The name of the app to update.</param> - /// <returns> - /// 200 => App image removed. - /// 404 => App not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppImageDelete)] - [ApiCosts(0)] - public async Task<IActionResult> DeleteImage(string app) - { - var response = await InvokeCommandAsync(new RemoveAppImage()); + /// <summary> + /// Remove the app image. + /// </summary> + /// <param name="app">The name of the app to update.</param> + /// <returns> + /// 200 => App image removed. + /// 404 => App not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/image")] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppImageDelete)] + [ApiCosts(0)] + public async Task<IActionResult> DeleteImage(string app) + { + var response = await InvokeCommandAsync(new RemoveAppImage()); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Delete the app. - /// </summary> - /// <param name="app">The name of the app to delete.</param> - /// <returns> - /// 204 => App deleted. - /// 404 => App not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/")] - [ApiPermission(PermissionIds.AppDelete)] - [ApiCosts(0)] - public async Task<IActionResult> DeleteApp(string app) - { - var command = new DeleteApp(); + /// <summary> + /// Delete the app. + /// </summary> + /// <param name="app">The name of the app to delete.</param> + /// <returns> + /// 204 => App deleted. + /// 404 => App not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/")] + [ApiPermission(PermissionIds.AppDelete)] + [ApiCosts(0)] + public async Task<IActionResult> DeleteApp(string app) + { + var command = new DeleteApp(); - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - return NoContent(); - } + return NoContent(); + } - private Task<AppDto> InvokeCommandAsync(ICommand command) + private Task<AppDto> InvokeCommandAsync(ICommand command) + { + return InvokeCommandAsync(command, x => { - return InvokeCommandAsync(command, x => - { - return AppDto.FromDomain(x, UserOrClientId, IsFrontend, Resources); - }); - } + return AppDto.FromDomain(x, UserOrClientId, IsFrontend, Resources); + }); + } - private async Task<T> InvokeCommandAsync<T>(ICommand command, Func<IAppEntity, T> converter) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + private async Task<T> InvokeCommandAsync<T>(ICommand command, Func<IAppEntity, T> converter) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var result = context.Result<IAppEntity>(); - var response = converter(result); + var result = context.Result<IAppEntity>(); + var response = converter(result); - return response; - } + return response; + } - private UploadAppImage CreateCommand(IFormFile? file) + private UploadAppImage CreateCommand(IFormFile? file) + { + if (file == null || Request.Form.Files.Count != 1) { - if (file == null || Request.Form.Files.Count != 1) - { - var error = T.Get("validation.onlyOneFile"); + var error = T.Get("validation.onlyOneFile"); - throw new ValidationException(error); - } - - return new UploadAppImage { File = file.ToAssetFile() }; + throw new ValidationException(error); } + + return new UploadAppImage { File = file.ToAssetFile() }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs index 5eae2298cf..30039d6a0f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs @@ -10,19 +10,18 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class AddLanguageDto { - public sealed class AddLanguageDto - { - /// <summary> - /// The language to add. - /// </summary> - [LocalizedRequired] - public Language Language { get; set; } + /// <summary> + /// The language to add. + /// </summary> + [LocalizedRequired] + public Language Language { get; set; } - public AddLanguage ToCommand() - { - return SimpleMapper.Map(this, new AddLanguage()); - } + public AddLanguage ToCommand() + { + return SimpleMapper.Map(this, new AddLanguage()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs index 6740773662..edc96be929 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs @@ -8,19 +8,18 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class AddRoleDto { - public sealed class AddRoleDto - { - /// <summary> - /// The role name. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The role name. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - public AddRole ToCommand() - { - return new AddRole { Name = Name }; - } + public AddRole ToCommand() + { + return new AddRole { Name = Name }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs index ae97660e83..c214492648 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs @@ -8,19 +8,18 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class AddWorkflowDto { - public sealed class AddWorkflowDto - { - /// <summary> - /// The name of the workflow. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The name of the workflow. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - public AddWorkflow ToCommand() - { - return new AddWorkflow { Name = Name }; - } + public AddWorkflow ToCommand() + { + return new AddWorkflow { Name = Name }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index 69846ae84e..1d3a895364 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -24,254 +24,253 @@ #pragma warning disable RECS0033 // Convert 'if' to '||' expression -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class AppDto : Resource { - public sealed class AppDto : Resource + /// <summary> + /// The ID of the app. + /// </summary> + public DomainId Id { get; set; } + + /// <summary> + /// The name of the app. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Name { get; set; } + + /// <summary> + /// The optional label of the app. + /// </summary> + public string? Label { get; set; } + + /// <summary> + /// The optional description of the app. + /// </summary> + public string? Description { get; set; } + + /// <summary> + /// The version of the app. + /// </summary> + public long Version { get; set; } + + /// <summary> + /// The timestamp when the app has been created. + /// </summary> + public Instant Created { get; set; } + + /// <summary> + /// The timestamp when the app has been modified last. + /// </summary> + public Instant LastModified { get; set; } + + /// <summary> + /// The ID of the team. + /// </summary> + public DomainId? TeamId { get; set; } + + /// <summary> + /// The permission level of the user. + /// </summary> + public IEnumerable<string> Permissions { get; set; } = Array.Empty<string>(); + + /// <summary> + /// Indicates if the user can access the api. + /// </summary> + [Obsolete("Use 'roleProperties' field now.")] + public bool CanAccessApi { get; set; } + + /// <summary> + /// Indicates if the user can access at least one content. + /// </summary> + public bool CanAccessContent { get; set; } + + /// <summary> + /// The role name of the user. + /// </summary> + public string? RoleName { get; set; } + + /// <summary> + /// The properties from the role. + /// </summary> + [LocalizedRequired] + public JsonObject RoleProperties { get; set; } + + public static AppDto FromDomain(IAppEntity app, string userId, bool isFrontend, Resources resources) { - /// <summary> - /// The ID of the app. - /// </summary> - public DomainId Id { get; set; } - - /// <summary> - /// The name of the app. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Name { get; set; } - - /// <summary> - /// The optional label of the app. - /// </summary> - public string? Label { get; set; } - - /// <summary> - /// The optional description of the app. - /// </summary> - public string? Description { get; set; } - - /// <summary> - /// The version of the app. - /// </summary> - public long Version { get; set; } - - /// <summary> - /// The timestamp when the app has been created. - /// </summary> - public Instant Created { get; set; } - - /// <summary> - /// The timestamp when the app has been modified last. - /// </summary> - public Instant LastModified { get; set; } - - /// <summary> - /// The ID of the team. - /// </summary> - public DomainId? TeamId { get; set; } - - /// <summary> - /// The permission level of the user. - /// </summary> - public IEnumerable<string> Permissions { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Indicates if the user can access the api. - /// </summary> - [Obsolete("Use 'roleProperties' field now.")] - public bool CanAccessApi { get; set; } - - /// <summary> - /// Indicates if the user can access at least one content. - /// </summary> - public bool CanAccessContent { get; set; } - - /// <summary> - /// The role name of the user. - /// </summary> - public string? RoleName { get; set; } - - /// <summary> - /// The properties from the role. - /// </summary> - [LocalizedRequired] - public JsonObject RoleProperties { get; set; } - - public static AppDto FromDomain(IAppEntity app, string userId, bool isFrontend, Resources resources) + var result = SimpleMapper.Map(app, new AppDto()); + + var permissions = PermissionSet.Empty; + + var isContributor = false; + + if (app.TryGetContributorRole(userId, isFrontend, out var role)) + { + isContributor = true; + + result.RoleName = role.Name; + result.RoleProperties = role.Properties; + result.Permissions = permissions.ToIds(); + + permissions = role.Permissions; + } + else if (app.TryGetClientRole(userId, isFrontend, out role)) + { + result.RoleName = role.Name; + result.RoleProperties = role.Properties; + result.Permissions = permissions.ToIds(); + + permissions = role.Permissions; + } + else { - var result = SimpleMapper.Map(app, new AppDto()); - - var permissions = PermissionSet.Empty; - - var isContributor = false; - - if (app.TryGetContributorRole(userId, isFrontend, out var role)) - { - isContributor = true; - - result.RoleName = role.Name; - result.RoleProperties = role.Properties; - result.Permissions = permissions.ToIds(); - - permissions = role.Permissions; - } - else if (app.TryGetClientRole(userId, isFrontend, out role)) - { - result.RoleName = role.Name; - result.RoleProperties = role.Properties; - result.Permissions = permissions.ToIds(); - - permissions = role.Permissions; - } - else - { - result.RoleProperties = new JsonObject(); - } - - foreach (var (key, value) in resources.Context.UserPrincipal.Claims.GetUIProperties(app.Name)) - { - result.RoleProperties[key] = JsonValue.Create(value); - } - - if (resources.Includes(PermissionIds.ForApp(PermissionIds.AppContents, app.Name), permissions)) - { - result.CanAccessContent = true; - } - - return result.CreateLinks(app, resources, permissions, isContributor); + result.RoleProperties = new JsonObject(); } - private AppDto CreateLinks(IAppEntity app, Resources resources, PermissionSet permissions, bool isContributor) + foreach (var (key, value) in resources.Context.UserPrincipal.Claims.GetUIProperties(app.Name)) { - var values = new { app = Name }; - - AddGetLink("ping", - resources.Url<PingController>(x => nameof(x.GetAppPing), values)); - - if (app.Image != null) - { - AddGetLink("image", - resources.Url<AppImageController>(x => nameof(x.GetImage), values)); - } - - if (isContributor) - { - AddDeleteLink("leave", - resources.Url<AppContributorsController>(x => nameof(x.DeleteMyself), values)); - } - - if (resources.IsAllowed(PermissionIds.AppDelete, Name, additional: permissions)) - { - AddDeleteLink("delete", - resources.Url<AppsController>(x => nameof(x.DeleteApp), values)); - } - - if (resources.IsAllowed(PermissionIds.AppTransfer, Name, additional: permissions)) - { - AddPutLink("transfer", - resources.Url<AppsController>(x => nameof(x.PutAppTeam), values)); - } - - if (resources.IsAllowed(PermissionIds.AppUpdate, Name, additional: permissions)) - { - AddPutLink("update", - resources.Url<AppsController>(x => nameof(x.PutApp), values)); - } - - if (resources.IsAllowed(PermissionIds.AppAssetsRead, Name, additional: permissions)) - { - AddGetLink("assets", - resources.Url<AssetsController>(x => nameof(x.GetAssets), values)); - } - - if (resources.IsAllowed(PermissionIds.AppBackupsRead, Name, additional: permissions)) - { - AddGetLink("backups", - resources.Url<BackupsController>(x => nameof(x.GetBackups), values)); - } - - if (resources.IsAllowed(PermissionIds.AppClientsRead, Name, additional: permissions)) - { - AddGetLink("clients", - resources.Url<AppClientsController>(x => nameof(x.GetClients), values)); - } - - if (resources.IsAllowed(PermissionIds.AppContributorsRead, Name, additional: permissions)) - { - AddGetLink("contributors", - resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); - } - - if (resources.IsAllowed(PermissionIds.AppLanguagesRead, Name, additional: permissions)) - { - AddGetLink("languages", - resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values)); - } - - if (resources.IsAllowed(PermissionIds.AppPlansRead, Name, additional: permissions)) - { - AddGetLink("plans", - resources.Url<AppPlansController>(x => nameof(x.GetPlans), values)); - } - - if (resources.IsAllowed(PermissionIds.AppRolesRead, Name, additional: permissions)) - { - AddGetLink("roles", - resources.Url<AppRolesController>(x => nameof(x.GetRoles), values)); - } - - if (resources.IsAllowed(PermissionIds.AppRulesRead, Name, additional: permissions)) - { - AddGetLink("rules", - resources.Url<RulesController>(x => nameof(x.GetRules), values)); - } - - if (resources.IsAllowed(PermissionIds.AppSchemasRead, Name, additional: permissions)) - { - AddGetLink("schemas", - resources.Url<SchemasController>(x => nameof(x.GetSchemas), values)); - } - - if (resources.IsAllowed(PermissionIds.AppWorkflowsRead, Name, additional: permissions)) - { - AddGetLink("workflows", - resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values)); - } - - if (resources.IsAllowed(PermissionIds.AppSchemasCreate, Name, additional: permissions)) - { - AddPostLink("schemas/create", - resources.Url<SchemasController>(x => nameof(x.PostSchema), values)); - } - - if (resources.IsAllowed(PermissionIds.AppAssetsCreate, Name, additional: permissions)) - { - AddPostLink("assets/create", - resources.Url<SchemasController>(x => nameof(x.PostSchema), values)); - } - - if (resources.IsAllowed(PermissionIds.AppImageUpload, Name, additional: permissions)) - { - AddPostLink("image/upload", - resources.Url<AppsController>(x => nameof(x.UploadImage), values)); - } - - if (resources.IsAllowed(PermissionIds.AppImageDelete, Name, additional: permissions)) - { - AddDeleteLink("image/delete", - resources.Url<AppsController>(x => nameof(x.DeleteImage), values)); - } - - if (resources.IsAllowed(PermissionIds.AppAssetsScriptsUpdate, Name, additional: permissions)) - { - AddDeleteLink("assets/scripts", - resources.Url<AppAssetsController>(x => nameof(x.GetAssetScripts), values)); - } - - AddGetLink("settings", - resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values)); - - return this; + result.RoleProperties[key] = JsonValue.Create(value); } + + if (resources.Includes(PermissionIds.ForApp(PermissionIds.AppContents, app.Name), permissions)) + { + result.CanAccessContent = true; + } + + return result.CreateLinks(app, resources, permissions, isContributor); + } + + private AppDto CreateLinks(IAppEntity app, Resources resources, PermissionSet permissions, bool isContributor) + { + var values = new { app = Name }; + + AddGetLink("ping", + resources.Url<PingController>(x => nameof(x.GetAppPing), values)); + + if (app.Image != null) + { + AddGetLink("image", + resources.Url<AppImageController>(x => nameof(x.GetImage), values)); + } + + if (isContributor) + { + AddDeleteLink("leave", + resources.Url<AppContributorsController>(x => nameof(x.DeleteMyself), values)); + } + + if (resources.IsAllowed(PermissionIds.AppDelete, Name, additional: permissions)) + { + AddDeleteLink("delete", + resources.Url<AppsController>(x => nameof(x.DeleteApp), values)); + } + + if (resources.IsAllowed(PermissionIds.AppTransfer, Name, additional: permissions)) + { + AddPutLink("transfer", + resources.Url<AppsController>(x => nameof(x.PutAppTeam), values)); + } + + if (resources.IsAllowed(PermissionIds.AppUpdate, Name, additional: permissions)) + { + AddPutLink("update", + resources.Url<AppsController>(x => nameof(x.PutApp), values)); + } + + if (resources.IsAllowed(PermissionIds.AppAssetsRead, Name, additional: permissions)) + { + AddGetLink("assets", + resources.Url<AssetsController>(x => nameof(x.GetAssets), values)); + } + + if (resources.IsAllowed(PermissionIds.AppBackupsRead, Name, additional: permissions)) + { + AddGetLink("backups", + resources.Url<BackupsController>(x => nameof(x.GetBackups), values)); + } + + if (resources.IsAllowed(PermissionIds.AppClientsRead, Name, additional: permissions)) + { + AddGetLink("clients", + resources.Url<AppClientsController>(x => nameof(x.GetClients), values)); + } + + if (resources.IsAllowed(PermissionIds.AppContributorsRead, Name, additional: permissions)) + { + AddGetLink("contributors", + resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); + } + + if (resources.IsAllowed(PermissionIds.AppLanguagesRead, Name, additional: permissions)) + { + AddGetLink("languages", + resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values)); + } + + if (resources.IsAllowed(PermissionIds.AppPlansRead, Name, additional: permissions)) + { + AddGetLink("plans", + resources.Url<AppPlansController>(x => nameof(x.GetPlans), values)); + } + + if (resources.IsAllowed(PermissionIds.AppRolesRead, Name, additional: permissions)) + { + AddGetLink("roles", + resources.Url<AppRolesController>(x => nameof(x.GetRoles), values)); + } + + if (resources.IsAllowed(PermissionIds.AppRulesRead, Name, additional: permissions)) + { + AddGetLink("rules", + resources.Url<RulesController>(x => nameof(x.GetRules), values)); + } + + if (resources.IsAllowed(PermissionIds.AppSchemasRead, Name, additional: permissions)) + { + AddGetLink("schemas", + resources.Url<SchemasController>(x => nameof(x.GetSchemas), values)); + } + + if (resources.IsAllowed(PermissionIds.AppWorkflowsRead, Name, additional: permissions)) + { + AddGetLink("workflows", + resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values)); + } + + if (resources.IsAllowed(PermissionIds.AppSchemasCreate, Name, additional: permissions)) + { + AddPostLink("schemas/create", + resources.Url<SchemasController>(x => nameof(x.PostSchema), values)); + } + + if (resources.IsAllowed(PermissionIds.AppAssetsCreate, Name, additional: permissions)) + { + AddPostLink("assets/create", + resources.Url<SchemasController>(x => nameof(x.PostSchema), values)); + } + + if (resources.IsAllowed(PermissionIds.AppImageUpload, Name, additional: permissions)) + { + AddPostLink("image/upload", + resources.Url<AppsController>(x => nameof(x.UploadImage), values)); + } + + if (resources.IsAllowed(PermissionIds.AppImageDelete, Name, additional: permissions)) + { + AddDeleteLink("image/delete", + resources.Url<AppsController>(x => nameof(x.DeleteImage), values)); + } + + if (resources.IsAllowed(PermissionIds.AppAssetsScriptsUpdate, Name, additional: permissions)) + { + AddDeleteLink("assets/scripts", + resources.Url<AppAssetsController>(x => nameof(x.GetAssetScripts), values)); + } + + AddGetLink("settings", + resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values)); + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs index 12377c141b..cd73b3286a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs @@ -11,72 +11,71 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class AppLanguageDto : Resource { - public sealed class AppLanguageDto : Resource - { - /// <summary> - /// The iso code of the language. - /// </summary> - [LocalizedRequired] - public string Iso2Code { get; set; } + /// <summary> + /// The iso code of the language. + /// </summary> + [LocalizedRequired] + public string Iso2Code { get; set; } - /// <summary> - /// The english name of the language. - /// </summary> - [LocalizedRequired] - public string EnglishName { get; set; } + /// <summary> + /// The english name of the language. + /// </summary> + [LocalizedRequired] + public string EnglishName { get; set; } - /// <summary> - /// The fallback languages. - /// </summary> - [LocalizedRequired] - public Language[] Fallback { get; set; } + /// <summary> + /// The fallback languages. + /// </summary> + [LocalizedRequired] + public Language[] Fallback { get; set; } - /// <summary> - /// Indicates if the language is the master language. - /// </summary> - public bool IsMaster { get; set; } + /// <summary> + /// Indicates if the language is the master language. + /// </summary> + public bool IsMaster { get; set; } - /// <summary> - /// Indicates if the language is optional. - /// </summary> - public bool IsOptional { get; set; } + /// <summary> + /// Indicates if the language is optional. + /// </summary> + public bool IsOptional { get; set; } - public static AppLanguageDto FromDomain(Language language, LanguageConfig config, LanguagesConfig languages) + public static AppLanguageDto FromDomain(Language language, LanguageConfig config, LanguagesConfig languages) + { + var result = new AppLanguageDto { - var result = new AppLanguageDto - { - EnglishName = language.EnglishName, - IsMaster = languages.IsMaster(language), - IsOptional = languages.IsOptional(language), - Iso2Code = language.Iso2Code, - Fallback = config.Fallbacks.ToArray() - }; + EnglishName = language.EnglishName, + IsMaster = languages.IsMaster(language), + IsOptional = languages.IsOptional(language), + Iso2Code = language.Iso2Code, + Fallback = config.Fallbacks.ToArray() + }; - return result; - } + return result; + } - public AppLanguageDto CreateLinks(Resources resources, IAppEntity app) - { - var values = new { app = resources.App, language = Iso2Code }; + public AppLanguageDto CreateLinks(Resources resources, IAppEntity app) + { + var values = new { app = resources.App, language = Iso2Code }; - if (!IsMaster) + if (!IsMaster) + { + if (resources.CanUpdateLanguage) { - if (resources.CanUpdateLanguage) - { - AddPutLink("update", - resources.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values)); - } - - if (resources.CanDeleteLanguage && app.Languages.Languages.Count > 1) - { - AddDeleteLink("delete", - resources.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values)); - } + AddPutLink("update", + resources.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values)); } - return this; + if (resources.CanDeleteLanguage && app.Languages.Languages.Count > 1) + { + AddDeleteLink("delete", + resources.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values)); + } } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs index d0902d3181..94c78e1b5e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs @@ -9,45 +9,44 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class AppLanguagesDto : Resource { - public sealed class AppLanguagesDto : Resource - { - /// <summary> - /// The languages. - /// </summary> - [LocalizedRequired] - public AppLanguageDto[] Items { get; set; } + /// <summary> + /// The languages. + /// </summary> + [LocalizedRequired] + public AppLanguageDto[] Items { get; set; } - public static AppLanguagesDto FromDomain(IAppEntity app, Resources resources) - { - var config = app.Languages; - - var result = new AppLanguagesDto - { - Items = config.Languages - .Select(x => AppLanguageDto.FromDomain(x.Key, x.Value, config)) - .Select(x => x.CreateLinks(resources, app)) - .OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code) - .ToArray() - }; - - return result.CreateLinks(resources); - } + public static AppLanguagesDto FromDomain(IAppEntity app, Resources resources) + { + var config = app.Languages; - private AppLanguagesDto CreateLinks(Resources resources) + var result = new AppLanguagesDto { - var values = new { app = resources.App }; + Items = config.Languages + .Select(x => AppLanguageDto.FromDomain(x.Key, x.Value, config)) + .Select(x => x.CreateLinks(resources, app)) + .OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code) + .ToArray() + }; + + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values)); + private AppLanguagesDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanCreateLanguage) - { - AddPostLink("create", - resources.Url<AppLanguagesController>(x => nameof(x.PostLanguage), values)); - } + AddSelfLink(resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values)); - return this; + if (resources.CanCreateLanguage) + { + AddPostLink("create", + resources.Url<AppLanguagesController>(x => nameof(x.PostLanguage), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs index 8ae2cef856..f9b34d804d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs @@ -9,66 +9,65 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class AppSettingsDto : Resource - { - /// <summary> - /// The configured app patterns. - /// </summary> - [LocalizedRequired] - public PatternDto[] Patterns { get; set; } - - /// <summary> - /// The configured UI editors. - /// </summary> - [LocalizedRequired] - public EditorDto[] Editors { get; set; } +namespace Squidex.Areas.Api.Controllers.Apps.Models; - /// <summary> - /// Hide the scheduler for content items. - /// </summary> - public bool HideScheduler { get; set; } +public sealed class AppSettingsDto : Resource +{ + /// <summary> + /// The configured app patterns. + /// </summary> + [LocalizedRequired] + public PatternDto[] Patterns { get; set; } - /// <summary> - /// Hide the datetime mode button. - /// </summary> - public bool HideDateTimeModeButton { get; set; } + /// <summary> + /// The configured UI editors. + /// </summary> + [LocalizedRequired] + public EditorDto[] Editors { get; set; } - /// <summary> - /// The version of the app. - /// </summary> - public long Version { get; set; } + /// <summary> + /// Hide the scheduler for content items. + /// </summary> + public bool HideScheduler { get; set; } - public static AppSettingsDto FromDomain(IAppEntity app, Resources resources) - { - var settings = app.Settings; + /// <summary> + /// Hide the datetime mode button. + /// </summary> + public bool HideDateTimeModeButton { get; set; } - var result = new AppSettingsDto - { - Editors = settings.Editors.Select(EditorDto.FromDomain).ToArray(), - HideDateTimeModeButton = settings.HideDateTimeModeButton, - HideScheduler = settings.HideScheduler, - Patterns = settings.Patterns.Select(PatternDto.FromPattern).ToArray(), - Version = app.Version - }; + /// <summary> + /// The version of the app. + /// </summary> + public long Version { get; set; } - return result.CreateLinks(resources); - } + public static AppSettingsDto FromDomain(IAppEntity app, Resources resources) + { + var settings = app.Settings; - private AppSettingsDto CreateLinks(Resources resources) + var result = new AppSettingsDto { - var values = new { app = resources.App }; + Editors = settings.Editors.Select(EditorDto.FromDomain).ToArray(), + HideDateTimeModeButton = settings.HideDateTimeModeButton, + HideScheduler = settings.HideScheduler, + Patterns = settings.Patterns.Select(PatternDto.FromPattern).ToArray(), + Version = app.Version + }; + + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values)); + private AppSettingsDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanUpdateSettings) - { - AddPutLink("update", - resources.Url<AppSettingsController>(x => nameof(x.PutSettings), values)); - } + AddSelfLink(resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values)); - return this; + if (resources.CanUpdateSettings) + { + AddPutLink("update", + resources.Url<AppSettingsController>(x => nameof(x.PutSettings), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs index d4adadd976..503d0f550b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs @@ -9,60 +9,59 @@ using Squidex.Infrastructure.Reflection; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class AssetScriptsDto : Resource - { - /// <summary> - /// The script that is executed when creating an asset. - /// </summary> - public string? Create { get; init; } +namespace Squidex.Areas.Api.Controllers.Apps.Models; - /// <summary> - /// The script that is executed when updating a content. - /// </summary> - public string? Update { get; init; } +public sealed class AssetScriptsDto : Resource +{ + /// <summary> + /// The script that is executed when creating an asset. + /// </summary> + public string? Create { get; init; } - /// <summary> - /// The script that is executed when annotating a content. - /// </summary> - public string? Annotate { get; init; } + /// <summary> + /// The script that is executed when updating a content. + /// </summary> + public string? Update { get; init; } - /// <summary> - /// The script that is executed when moving a content. - /// </summary> - public string? Move { get; init; } + /// <summary> + /// The script that is executed when annotating a content. + /// </summary> + public string? Annotate { get; init; } - /// <summary> - /// The script that is executed when deleting a content. - /// </summary> - public string? Delete { get; init; } + /// <summary> + /// The script that is executed when moving a content. + /// </summary> + public string? Move { get; init; } - /// <summary> - /// The version of the app. - /// </summary> - public long Version { get; set; } + /// <summary> + /// The script that is executed when deleting a content. + /// </summary> + public string? Delete { get; init; } - public static AssetScriptsDto FromDomain(IAppEntity app, Resources resources) - { - var result = SimpleMapper.Map(app.AssetScripts, new AssetScriptsDto()); + /// <summary> + /// The version of the app. + /// </summary> + public long Version { get; set; } - return result.CreateLinks(resources); - } + public static AssetScriptsDto FromDomain(IAppEntity app, Resources resources) + { + var result = SimpleMapper.Map(app.AssetScripts, new AssetScriptsDto()); - private AssetScriptsDto CreateLinks(Resources resources) - { - var values = new { app = resources.App }; + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values)); + private AssetScriptsDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanUpdateAssetsScripts) - { - AddPutLink("update", - resources.Url<AppAssetsController>(x => nameof(x.PutAssetScripts), values)); - } + AddSelfLink(resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values)); - return this; + if (resources.CanUpdateAssetsScripts) + { + AddPutLink("update", + resources.Url<AppAssetsController>(x => nameof(x.PutAssetScripts), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs index e1ffbd008a..667d91d060 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs @@ -10,72 +10,71 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class ClientDto : Resource - { - /// <summary> - /// The client id. - /// </summary> - [LocalizedRequired] - public string Id { get; set; } +namespace Squidex.Areas.Api.Controllers.Apps.Models; - /// <summary> - /// The client secret. - /// </summary> - [LocalizedRequired] - public string Secret { get; set; } +public sealed class ClientDto : Resource +{ + /// <summary> + /// The client id. + /// </summary> + [LocalizedRequired] + public string Id { get; set; } - /// <summary> - /// The client name. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The client secret. + /// </summary> + [LocalizedRequired] + public string Secret { get; set; } - /// <summary> - /// The role of the client. - /// </summary> - public string? Role { get; set; } + /// <summary> + /// The client name. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - /// <summary> - /// The number of allowed api calls per month for this client. - /// </summary> - public long ApiCallsLimit { get; set; } + /// <summary> + /// The role of the client. + /// </summary> + public string? Role { get; set; } - /// <summary> - /// The number of allowed api traffic bytes per month for this client. - /// </summary> - public long ApiTrafficLimit { get; set; } + /// <summary> + /// The number of allowed api calls per month for this client. + /// </summary> + public long ApiCallsLimit { get; set; } - /// <summary> - /// True to allow anonymous access without an access token for this client. - /// </summary> - public bool AllowAnonymous { get; set; } + /// <summary> + /// The number of allowed api traffic bytes per month for this client. + /// </summary> + public long ApiTrafficLimit { get; set; } - public static ClientDto FromClient(string id, AppClient client) - { - var result = SimpleMapper.Map(client, new ClientDto { Id = id }); + /// <summary> + /// True to allow anonymous access without an access token for this client. + /// </summary> + public bool AllowAnonymous { get; set; } - return result; - } + public static ClientDto FromClient(string id, AppClient client) + { + var result = SimpleMapper.Map(client, new ClientDto { Id = id }); - public ClientDto CreateLinks(Resources resources) - { - var values = new { app = resources.App, id = Id }; + return result; + } - if (resources.CanUpdateClient) - { - AddPutLink("update", - resources.Url<AppClientsController>(x => nameof(x.PutClient), values)); - } + public ClientDto CreateLinks(Resources resources) + { + var values = new { app = resources.App, id = Id }; - if (resources.CanDeleteClient) - { - AddDeleteLink("delete", - resources.Url<AppClientsController>(x => nameof(x.DeleteClient), values)); - } + if (resources.CanUpdateClient) + { + AddPutLink("update", + resources.Url<AppClientsController>(x => nameof(x.PutClient), values)); + } - return this; + if (resources.CanDeleteClient) + { + AddDeleteLink("delete", + resources.Url<AppClientsController>(x => nameof(x.DeleteClient), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs index b592200754..7fbfd2afbf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs @@ -9,42 +9,41 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class ClientsDto : Resource { - public sealed class ClientsDto : Resource - { - /// <summary> - /// The clients. - /// </summary> - [LocalizedRequired] - public ClientDto[] Items { get; set; } + /// <summary> + /// The clients. + /// </summary> + [LocalizedRequired] + public ClientDto[] Items { get; set; } - public static ClientsDto FromApp(IAppEntity app, Resources resources) + public static ClientsDto FromApp(IAppEntity app, Resources resources) + { + var result = new ClientsDto { - var result = new ClientsDto - { - Items = app.Clients - .Select(x => ClientDto.FromClient(x.Key, x.Value)) - .Select(x => x.CreateLinks(resources)) - .ToArray() - }; - - return result.CreateLinks(resources); - } + Items = app.Clients + .Select(x => ClientDto.FromClient(x.Key, x.Value)) + .Select(x => x.CreateLinks(resources)) + .ToArray() + }; - private ClientsDto CreateLinks(Resources resources) - { - var values = new { app = resources.App }; + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<AppClientsController>(x => nameof(x.GetClients), values)); + private ClientsDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanCreateClient) - { - AddPostLink("create", - resources.Url<AppClientsController>(x => nameof(x.PostClient), values)); - } + AddSelfLink(resources.Url<AppClientsController>(x => nameof(x.GetClients), values)); - return this; + if (resources.CanCreateClient) + { + AddPostLink("create", + resources.Url<AppClientsController>(x => nameof(x.PostClient), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs index 4a8b18ab4d..5fcbbe83c3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs @@ -9,25 +9,24 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class CreateAppDto { - public sealed class CreateAppDto - { - /// <summary> - /// The name of the app. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Name { get; set; } + /// <summary> + /// The name of the app. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Name { get; set; } - /// <summary> - /// Initialize the app with the inbuilt template. - /// </summary> - public string? Template { get; set; } + /// <summary> + /// Initialize the app with the inbuilt template. + /// </summary> + public string? Template { get; set; } - public CreateApp ToCommand() - { - return SimpleMapper.Map(this, new CreateApp()); - } + public CreateApp ToCommand() + { + return SimpleMapper.Map(this, new CreateApp()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs index d4fc227daa..f61630761c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs @@ -9,20 +9,19 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class CreateClientDto { - public sealed class CreateClientDto - { - /// <summary> - /// The ID of the client. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Id { get; set; } + /// <summary> + /// The ID of the client. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Id { get; set; } - public AttachClient ToCommand() - { - return SimpleMapper.Map(this, new AttachClient()); - } + public AttachClient ToCommand() + { + return SimpleMapper.Map(this, new AttachClient()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/EditorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/EditorDto.cs index 8f38d94fd2..9814af8a4e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/EditorDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/EditorDto.cs @@ -9,32 +9,31 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class EditorDto { - public sealed class EditorDto - { - /// <summary> - /// The name of the editor. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The name of the editor. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - /// <summary> - /// The url to the editor. - /// </summary> - [LocalizedRequired] - public string Url { get; set; } + /// <summary> + /// The url to the editor. + /// </summary> + [LocalizedRequired] + public string Url { get; set; } - public static EditorDto FromDomain(Editor editor) - { - var result = SimpleMapper.Map(editor, new EditorDto()); + public static EditorDto FromDomain(Editor editor) + { + var result = SimpleMapper.Map(editor, new EditorDto()); - return result; - } + return result; + } - public Editor ToEditor() - { - return new Editor(Name, Url); - } + public Editor ToEditor() + { + return new Editor(Name, Url); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs index 015de04ee8..8ae33136ff 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs @@ -9,40 +9,39 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class PatternDto { - public sealed class PatternDto + /// <summary> + /// The name of the suggestion. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } + + /// <summary> + /// The regex pattern. + /// </summary> + [LocalizedRequired] + public string Regex { get; set; } + + /// <summary> + /// The regex message. + /// </summary> + public string? Message { get; set; } + + public static PatternDto FromPattern(Pattern pattern) { - /// <summary> - /// The name of the suggestion. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } - - /// <summary> - /// The regex pattern. - /// </summary> - [LocalizedRequired] - public string Regex { get; set; } - - /// <summary> - /// The regex message. - /// </summary> - public string? Message { get; set; } - - public static PatternDto FromPattern(Pattern pattern) - { - var result = SimpleMapper.Map(pattern, new PatternDto()); + var result = SimpleMapper.Map(pattern, new PatternDto()); - return result; - } + return result; + } - public Pattern ToPattern() + public Pattern ToPattern() + { + return new Pattern(Name, Regex) { - return new Pattern(Name, Regex) - { - Message = Message - }; - } + Message = Message + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs index bfb510ebc0..ae7f4d9e0f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs @@ -11,88 +11,87 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class RoleDto : Resource { - public sealed class RoleDto : Resource + /// <summary> + /// The role name. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } + + /// <summary> + /// The number of clients with this role. + /// </summary> + public int NumClients { get; set; } + + /// <summary> + /// The number of contributors with this role. + /// </summary> + public int NumContributors { get; set; } + + /// <summary> + /// Indicates if the role is an builtin default role. + /// </summary> + public bool IsDefaultRole { get; set; } + + /// <summary> + /// Associated list of permissions. + /// </summary> + [LocalizedRequired] + public string[] Permissions { get; set; } + + /// <summary> + /// Associated list of UI properties. + /// </summary> + [LocalizedRequired] + public JsonObject Properties { get; set; } + + public static RoleDto FromDomain(Role role, IAppEntity app) { - /// <summary> - /// The role name. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } - - /// <summary> - /// The number of clients with this role. - /// </summary> - public int NumClients { get; set; } - - /// <summary> - /// The number of contributors with this role. - /// </summary> - public int NumContributors { get; set; } - - /// <summary> - /// Indicates if the role is an builtin default role. - /// </summary> - public bool IsDefaultRole { get; set; } - - /// <summary> - /// Associated list of permissions. - /// </summary> - [LocalizedRequired] - public string[] Permissions { get; set; } - - /// <summary> - /// Associated list of UI properties. - /// </summary> - [LocalizedRequired] - public JsonObject Properties { get; set; } - - public static RoleDto FromDomain(Role role, IAppEntity app) + var result = new RoleDto { - var result = new RoleDto - { - Name = role.Name, - NumClients = GetNumClients(role, app), - NumContributors = GetNumContributors(role, app), - Permissions = role.Permissions.ToIds().ToArray(), - Properties = role.Properties, - IsDefaultRole = role.IsDefault - }; - - return result; - } + Name = role.Name, + NumClients = GetNumClients(role, app), + NumContributors = GetNumContributors(role, app), + Permissions = role.Permissions.ToIds().ToArray(), + Properties = role.Properties, + IsDefaultRole = role.IsDefault + }; - private static int GetNumContributors(Role role, IAppEntity app) - { - return app.Contributors.Count(x => role.Equals(x.Value)); - } + return result; + } - private static int GetNumClients(Role role, IAppEntity app) - { - return app.Clients.Count(x => role.Equals(x.Value.Role)); - } + private static int GetNumContributors(Role role, IAppEntity app) + { + return app.Contributors.Count(x => role.Equals(x.Value)); + } - public RoleDto CreateLinks(Resources resources) - { - var values = new { app = resources.App, roleName = Name }; + private static int GetNumClients(Role role, IAppEntity app) + { + return app.Clients.Count(x => role.Equals(x.Value.Role)); + } - if (!IsDefaultRole) + public RoleDto CreateLinks(Resources resources) + { + var values = new { app = resources.App, roleName = Name }; + + if (!IsDefaultRole) + { + if (resources.CanUpdateRole) { - if (resources.CanUpdateRole) - { - AddPutLink("update", - resources.Url<AppRolesController>(x => nameof(x.PutRole), values)); - } - - if (resources.CanDeleteRole && NumClients == 0 && NumContributors == 0) - { - AddDeleteLink("delete", - resources.Url<AppRolesController>(x => nameof(x.DeleteRole), values)); - } + AddPutLink("update", + resources.Url<AppRolesController>(x => nameof(x.PutRole), values)); } - return this; + if (resources.CanDeleteRole && NumClients == 0 && NumContributors == 0) + { + AddDeleteLink("delete", + resources.Url<AppRolesController>(x => nameof(x.DeleteRole), values)); + } } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs index 319f7b5927..da896bee00 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs @@ -9,44 +9,43 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models -{ - public sealed class RolesDto : Resource - { - /// <summary> - /// The roles. - /// </summary> - [LocalizedRequired] - public RoleDto[] Items { get; set; } +namespace Squidex.Areas.Api.Controllers.Apps.Models; - public static RolesDto FromDomain(IAppEntity app, Resources resources) - { - var result = new RolesDto - { - Items = - app.Roles.All - .Select(x => RoleDto.FromDomain(x, app)) - .Select(x => x.CreateLinks(resources)) - .OrderBy(x => x.Name) - .ToArray() - }; - - return result.CreateLinks(resources); - } +public sealed class RolesDto : Resource +{ + /// <summary> + /// The roles. + /// </summary> + [LocalizedRequired] + public RoleDto[] Items { get; set; } - private RolesDto CreateLinks(Resources resources) + public static RolesDto FromDomain(IAppEntity app, Resources resources) + { + var result = new RolesDto { - var values = new { app = resources.App }; + Items = + app.Roles.All + .Select(x => RoleDto.FromDomain(x, app)) + .Select(x => x.CreateLinks(resources)) + .OrderBy(x => x.Name) + .ToArray() + }; + + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<AppRolesController>(x => nameof(x.GetRoles), values)); + private RolesDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanCreateRole) - { - AddPostLink("create", - resources.Url<AppRolesController>(x => nameof(x.PostRole), values)); - } + AddSelfLink(resources.Url<AppRolesController>(x => nameof(x.GetRoles), values)); - return this; + if (resources.CanCreateRole) + { + AddPostLink("create", + resources.Url<AppRolesController>(x => nameof(x.PostRole), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/TransferToTeamDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/TransferToTeamDto.cs index f69aceb47d..f0561bac9e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/TransferToTeamDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/TransferToTeamDto.cs @@ -9,18 +9,17 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class TransferToTeamDto { - public sealed class TransferToTeamDto - { - /// <summary> - /// The ID of the team. - /// </summary> - public DomainId? TeamId { get; set; } + /// <summary> + /// The ID of the team. + /// </summary> + public DomainId? TeamId { get; set; } - public TransferToTeam ToCommand() - { - return SimpleMapper.Map(this, new TransferToTeam()); - } + public TransferToTeam ToCommand() + { + return SimpleMapper.Map(this, new TransferToTeam()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs index 13a356e80d..fa31d72d1b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppDto.cs @@ -8,23 +8,22 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class UpdateAppDto { - public sealed class UpdateAppDto - { - /// <summary> - /// The optional label of your app. - /// </summary> - public string? Label { get; set; } + /// <summary> + /// The optional label of your app. + /// </summary> + public string? Label { get; set; } - /// <summary> - /// The optional description of your app. - /// </summary> - public string? Description { get; set; } + /// <summary> + /// The optional description of your app. + /// </summary> + public string? Description { get; set; } - public UpdateApp ToCommand() - { - return SimpleMapper.Map(this, new UpdateApp()); - } + public UpdateApp ToCommand() + { + return SimpleMapper.Map(this, new UpdateApp()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppSettingsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppSettingsDto.cs index 43ba95cd3b..36ae9c0563 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppSettingsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppSettingsDto.cs @@ -10,44 +10,43 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Collections; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class UpdateAppSettingsDto { - public sealed class UpdateAppSettingsDto - { - /// <summary> - /// The configured app patterns. - /// </summary> - [Required] - public PatternDto[] Patterns { get; set; } + /// <summary> + /// The configured app patterns. + /// </summary> + [Required] + public PatternDto[] Patterns { get; set; } - /// <summary> - /// The configured UI editors. - /// </summary> - [Required] - public EditorDto[] Editors { get; set; } + /// <summary> + /// The configured UI editors. + /// </summary> + [Required] + public EditorDto[] Editors { get; set; } - /// <summary> - /// Hide the scheduler for content items. - /// </summary> - public bool HideScheduler { get; set; } + /// <summary> + /// Hide the scheduler for content items. + /// </summary> + public bool HideScheduler { get; set; } - /// <summary> - /// Hide the datetime mode button. - /// </summary> - public bool HideDateTimeModeButton { get; set; } + /// <summary> + /// Hide the datetime mode button. + /// </summary> + public bool HideDateTimeModeButton { get; set; } - public UpdateAppSettings ToCommand() + public UpdateAppSettings ToCommand() + { + return new UpdateAppSettings { - return new UpdateAppSettings + Settings = new AppSettings { - Settings = new AppSettings - { - Editors = Editors?.Select(x => x.ToEditor()).ToReadonlyList()!, - HideScheduler = HideScheduler, - HideDateTimeModeButton = HideDateTimeModeButton, - Patterns = Patterns?.Select(x => x.ToPattern()).ToReadonlyList()! - } - }; - } + Editors = Editors?.Select(x => x.ToEditor()).ToReadonlyList()!, + HideScheduler = HideScheduler, + HideDateTimeModeButton = HideDateTimeModeButton, + Patterns = Patterns?.Select(x => x.ToPattern()).ToReadonlyList()! + } + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAssetScriptsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAssetScriptsDto.cs index 9f8632483d..70cd0cd425 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAssetScriptsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAssetScriptsDto.cs @@ -9,40 +9,39 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class UpdateAssetScriptsDto { - public sealed class UpdateAssetScriptsDto + /// <summary> + /// The script that is executed when creating an asset. + /// </summary> + public string? Create { get; init; } + + /// <summary> + /// The script that is executed when updating a content. + /// </summary> + public string? Update { get; init; } + + /// <summary> + /// The script that is executed when annotating a content. + /// </summary> + public string? Annotate { get; init; } + + /// <summary> + /// The script that is executed when moving a content. + /// </summary> + public string? Move { get; init; } + + /// <summary> + /// The script that is executed when deleting a content. + /// </summary> + public string? Delete { get; init; } + + public ConfigureAssetScripts ToCommand() { - /// <summary> - /// The script that is executed when creating an asset. - /// </summary> - public string? Create { get; init; } - - /// <summary> - /// The script that is executed when updating a content. - /// </summary> - public string? Update { get; init; } - - /// <summary> - /// The script that is executed when annotating a content. - /// </summary> - public string? Annotate { get; init; } - - /// <summary> - /// The script that is executed when moving a content. - /// </summary> - public string? Move { get; init; } - - /// <summary> - /// The script that is executed when deleting a content. - /// </summary> - public string? Delete { get; init; } - - public ConfigureAssetScripts ToCommand() - { - var scripts = SimpleMapper.Map(this, new AssetScripts()); - - return new ConfigureAssetScripts { Scripts = scripts }; - } + var scripts = SimpleMapper.Map(this, new AssetScripts()); + + return new ConfigureAssetScripts { Scripts = scripts }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs index 090e4701f5..062d1d5bfc 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs @@ -9,39 +9,38 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class UpdateClientDto { - public sealed class UpdateClientDto + /// <summary> + /// The new display name of the client. + /// </summary> + [LocalizedStringLength(20)] + public string? Name { get; set; } + + /// <summary> + /// The role of the client. + /// </summary> + public string? Role { get; set; } + + /// <summary> + /// True to allow anonymous access without an access token for this client. + /// </summary> + public bool? AllowAnonymous { get; set; } + + /// <summary> + /// The number of allowed api calls per month for this client. + /// </summary> + public long? ApiCallsLimit { get; set; } + + /// <summary> + /// The number of allowed api traffic bytes per month for this client. + /// </summary> + public long? ApiTrafficLimit { get; set; } + + public UpdateClient ToCommand(string clientId) { - /// <summary> - /// The new display name of the client. - /// </summary> - [LocalizedStringLength(20)] - public string? Name { get; set; } - - /// <summary> - /// The role of the client. - /// </summary> - public string? Role { get; set; } - - /// <summary> - /// True to allow anonymous access without an access token for this client. - /// </summary> - public bool? AllowAnonymous { get; set; } - - /// <summary> - /// The number of allowed api calls per month for this client. - /// </summary> - public long? ApiCallsLimit { get; set; } - - /// <summary> - /// The number of allowed api traffic bytes per month for this client. - /// </summary> - public long? ApiTrafficLimit { get; set; } - - public UpdateClient ToCommand(string clientId) - { - return SimpleMapper.Map(this, new UpdateClient { Id = clientId }); - } + return SimpleMapper.Map(this, new UpdateClient { Id = clientId }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs index 32367c839f..b75ff187e5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs @@ -9,28 +9,27 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class UpdateLanguageDto { - public sealed class UpdateLanguageDto - { - /// <summary> - /// Set the value to true to make the language the master. - /// </summary> - public bool? IsMaster { get; set; } + /// <summary> + /// Set the value to true to make the language the master. + /// </summary> + public bool? IsMaster { get; set; } - /// <summary> - /// Set the value to true to make the language optional. - /// </summary> - public bool IsOptional { get; set; } + /// <summary> + /// Set the value to true to make the language optional. + /// </summary> + public bool IsOptional { get; set; } - /// <summary> - /// Optional fallback languages. - /// </summary> - public Language[]? Fallback { get; set; } + /// <summary> + /// Optional fallback languages. + /// </summary> + public Language[]? Fallback { get; set; } - public UpdateLanguage ToCommand(Language language) - { - return SimpleMapper.Map(this, new UpdateLanguage { Language = language }); - } + public UpdateLanguage ToCommand(Language language) + { + return SimpleMapper.Map(this, new UpdateLanguage { Language = language }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs index c45b1387ae..48f270435f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs @@ -10,24 +10,23 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class UpdateRoleDto { - public sealed class UpdateRoleDto - { - /// <summary> - /// Associated list of permissions. - /// </summary> - [LocalizedRequired] - public string[] Permissions { get; set; } + /// <summary> + /// Associated list of permissions. + /// </summary> + [LocalizedRequired] + public string[] Permissions { get; set; } - /// <summary> - /// Associated list of UI properties. - /// </summary> - public JsonObject Properties { get; set; } + /// <summary> + /// Associated list of UI properties. + /// </summary> + public JsonObject Properties { get; set; } - public UpdateRole ToCommand(string name) - { - return SimpleMapper.Map(this, new UpdateRole { Name = name }); - } + public UpdateRole ToCommand(string name) + { + return SimpleMapper.Map(this, new UpdateRole { Name = name }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs index f3aeae67cc..d3e6c30c2e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs @@ -11,43 +11,42 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class UpdateWorkflowDto { - public sealed class UpdateWorkflowDto + /// <summary> + /// The name of the workflow. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// The workflow steps. + /// </summary> + [LocalizedRequired] + public Dictionary<Status, WorkflowStepDto> Steps { get; set; } + + /// <summary> + /// The schema ids. + /// </summary> + public ReadonlyList<DomainId>? SchemaIds { get; set; } + + /// <summary> + /// The initial step. + /// </summary> + [LocalizedRequired] + public Status Initial { get; set; } + + public UpdateWorkflow ToCommand(DomainId id) { - /// <summary> - /// The name of the workflow. - /// </summary> - public string? Name { get; set; } - - /// <summary> - /// The workflow steps. - /// </summary> - [LocalizedRequired] - public Dictionary<Status, WorkflowStepDto> Steps { get; set; } - - /// <summary> - /// The schema ids. - /// </summary> - public ReadonlyList<DomainId>? SchemaIds { get; set; } - - /// <summary> - /// The initial step. - /// </summary> - [LocalizedRequired] - public Status Initial { get; set; } - - public UpdateWorkflow ToCommand(DomainId id) - { - var workflow = new Workflow( - Initial, - Steps?.ToReadonlyDictionary( - x => x.Key, - x => x.Value?.ToWorkflowStep()!), - SchemaIds, - Name); - - return new UpdateWorkflow { WorkflowId = id, Workflow = workflow }; - } + var workflow = new Workflow( + Initial, + Steps?.ToReadonlyDictionary( + x => x.Key, + x => x.Value?.ToWorkflowStep()!), + SchemaIds, + Name); + + return new UpdateWorkflow { WorkflowId = id, Workflow = workflow }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs index 2872821338..5db5506258 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs @@ -12,66 +12,65 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class WorkflowDto : Resource { - public sealed class WorkflowDto : Resource - { - /// <summary> - /// The workflow id. - /// </summary> - public DomainId Id { get; set; } + /// <summary> + /// The workflow id. + /// </summary> + public DomainId Id { get; set; } - /// <summary> - /// The name of the workflow. - /// </summary> - public string? Name { get; set; } + /// <summary> + /// The name of the workflow. + /// </summary> + public string? Name { get; set; } - /// <summary> - /// The workflow steps. - /// </summary> - [LocalizedRequired] - public Dictionary<Status, WorkflowStepDto> Steps { get; set; } + /// <summary> + /// The workflow steps. + /// </summary> + [LocalizedRequired] + public Dictionary<Status, WorkflowStepDto> Steps { get; set; } - /// <summary> - /// The schema ids. - /// </summary> - public ReadonlyList<DomainId>? SchemaIds { get; set; } + /// <summary> + /// The schema ids. + /// </summary> + public ReadonlyList<DomainId>? SchemaIds { get; set; } - /// <summary> - /// The initial step. - /// </summary> - public Status Initial { get; set; } + /// <summary> + /// The initial step. + /// </summary> + public Status Initial { get; set; } - public static WorkflowDto FromDomain(DomainId id, Workflow workflow) + public static WorkflowDto FromDomain(DomainId id, Workflow workflow) + { + var result = SimpleMapper.Map(workflow, new WorkflowDto { - var result = SimpleMapper.Map(workflow, new WorkflowDto - { - Steps = workflow.Steps.ToDictionary( - x => x.Key, - x => WorkflowStepDto.FromDomain(x.Value)), - Id = id - }); + Steps = workflow.Steps.ToDictionary( + x => x.Key, + x => WorkflowStepDto.FromDomain(x.Value)), + Id = id + }); - return result; - } - - public WorkflowDto CreateLinks(Resources resources) - { - var values = new { app = resources.App, id = Id }; + return result; + } - if (resources.CanUpdateWorkflow) - { - AddPutLink("update", - resources.Url<AppWorkflowsController>(x => nameof(x.PutWorkflow), values)); - } + public WorkflowDto CreateLinks(Resources resources) + { + var values = new { app = resources.App, id = Id }; - if (resources.CanDeleteWorkflow) - { - AddDeleteLink("delete", - resources.Url<AppWorkflowsController>(x => nameof(x.DeleteWorkflow), values)); - } + if (resources.CanUpdateWorkflow) + { + AddPutLink("update", + resources.Url<AppWorkflowsController>(x => nameof(x.PutWorkflow), values)); + } - return this; + if (resources.CanDeleteWorkflow) + { + AddDeleteLink("delete", + resources.Url<AppWorkflowsController>(x => nameof(x.DeleteWorkflow), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs index c5990d677e..5bf48be962 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs @@ -11,71 +11,70 @@ using Squidex.Infrastructure.Validation; using NoUpdateType = Squidex.Domain.Apps.Core.Contents.NoUpdate; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class WorkflowStepDto { - public sealed class WorkflowStepDto - { - /// <summary> - /// The transitions. - /// </summary> - [LocalizedRequired] - public Dictionary<Status, WorkflowTransitionDto> Transitions { get; set; } + /// <summary> + /// The transitions. + /// </summary> + [LocalizedRequired] + public Dictionary<Status, WorkflowTransitionDto> Transitions { get; set; } - /// <summary> - /// The optional color. - /// </summary> - public string? Color { get; set; } + /// <summary> + /// The optional color. + /// </summary> + public string? Color { get; set; } - /// <summary> - /// True if the content should be validated when moving to this step. - /// </summary> - public bool Validate { get; set; } + /// <summary> + /// True if the content should be validated when moving to this step. + /// </summary> + public bool Validate { get; set; } - /// <summary> - /// Indicates if updates should not be allowed. - /// </summary> - public bool NoUpdate { get; set; } + /// <summary> + /// Indicates if updates should not be allowed. + /// </summary> + public bool NoUpdate { get; set; } - /// <summary> - /// Optional expression that must evaluate to true when you want to prevent updates. - /// </summary> - public string? NoUpdateExpression { get; set; } + /// <summary> + /// Optional expression that must evaluate to true when you want to prevent updates. + /// </summary> + public string? NoUpdateExpression { get; set; } - /// <summary> - /// Optional list of roles to restrict the updates for users with these roles. - /// </summary> - public string[]? NoUpdateRoles { get; set; } + /// <summary> + /// Optional list of roles to restrict the updates for users with these roles. + /// </summary> + public string[]? NoUpdateRoles { get; set; } - public static WorkflowStepDto FromDomain(WorkflowStep step) + public static WorkflowStepDto FromDomain(WorkflowStep step) + { + var result = SimpleMapper.Map(step, new WorkflowStepDto { - var result = SimpleMapper.Map(step, new WorkflowStepDto - { - Transitions = step.Transitions.ToDictionary( - y => y.Key, - y => WorkflowTransitionDto.FromDomain(y.Value)) - }); - - if (step.NoUpdate != null) - { - result.NoUpdate = true; - result.NoUpdateExpression = step.NoUpdate.Expression; - result.NoUpdateRoles = step.NoUpdate.Roles?.ToArray(); - } - - return result; - } + Transitions = step.Transitions.ToDictionary( + y => y.Key, + y => WorkflowTransitionDto.FromDomain(y.Value)) + }); - public WorkflowStep ToWorkflowStep() + if (step.NoUpdate != null) { - return new WorkflowStep( - Transitions?.ToReadonlyDictionary( - y => y.Key, - y => y.Value?.ToWorkflowTransition()!), - Color, - NoUpdate ? - NoUpdateType.When(NoUpdateExpression, NoUpdateRoles) : - null, - Validate); + result.NoUpdate = true; + result.NoUpdateExpression = step.NoUpdate.Expression; + result.NoUpdateRoles = step.NoUpdate.Roles?.ToArray(); } + + return result; + } + + public WorkflowStep ToWorkflowStep() + { + return new WorkflowStep( + Transitions?.ToReadonlyDictionary( + y => y.Key, + y => y.Value?.ToWorkflowTransition()!), + Color, + NoUpdate ? + NoUpdateType.When(NoUpdateExpression, NoUpdateRoles) : + null, + Validate); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs index b30781d84d..8b9a094232 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs @@ -7,30 +7,29 @@ using Squidex.Domain.Apps.Core.Contents; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class WorkflowTransitionDto { - public sealed class WorkflowTransitionDto - { - /// <summary> - /// The optional expression. - /// </summary> - public string? Expression { get; set; } + /// <summary> + /// The optional expression. + /// </summary> + public string? Expression { get; set; } - /// <summary> - /// The optional restricted role. - /// </summary> - public string[]? Roles { get; set; } + /// <summary> + /// The optional restricted role. + /// </summary> + public string[]? Roles { get; set; } - public static WorkflowTransitionDto FromDomain(WorkflowTransition transition) - { - var result = new WorkflowTransitionDto { Expression = transition.Expression, Roles = transition.Roles?.ToArray() }; + public static WorkflowTransitionDto FromDomain(WorkflowTransition transition) + { + var result = new WorkflowTransitionDto { Expression = transition.Expression, Roles = transition.Roles?.ToArray() }; - return result; - } + return result; + } - public WorkflowTransition ToWorkflowTransition() - { - return WorkflowTransition.When(Expression, Roles); - } + public WorkflowTransition ToWorkflowTransition() + { + return WorkflowTransition.When(Expression, Roles); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs index f34fc8fc39..66752456cd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs @@ -10,53 +10,52 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models; + +public sealed class WorkflowsDto : Resource { - public sealed class WorkflowsDto : Resource + /// <summary> + /// The workflow. + /// </summary> + [LocalizedRequired] + public WorkflowDto[] Items { get; set; } + + /// <summary> + /// The errros that should be fixed. + /// </summary> + [LocalizedRequired] + public string[] Errors { get; set; } + + public static async Task<WorkflowsDto> FromAppAsync(IWorkflowsValidator workflowsValidator, IAppEntity app, Resources resources) { - /// <summary> - /// The workflow. - /// </summary> - [LocalizedRequired] - public WorkflowDto[] Items { get; set; } - - /// <summary> - /// The errros that should be fixed. - /// </summary> - [LocalizedRequired] - public string[] Errors { get; set; } - - public static async Task<WorkflowsDto> FromAppAsync(IWorkflowsValidator workflowsValidator, IAppEntity app, Resources resources) + var result = new WorkflowsDto { - var result = new WorkflowsDto - { - Items = - app.Workflows - .Select(x => WorkflowDto.FromDomain(x.Key, x.Value)) - .Select(x => x.CreateLinks(resources)) - .ToArray() - }; - - var errors = await workflowsValidator.ValidateAsync(app.Id, app.Workflows); + Items = + app.Workflows + .Select(x => WorkflowDto.FromDomain(x.Key, x.Value)) + .Select(x => x.CreateLinks(resources)) + .ToArray() + }; - result.Errors = errors.ToArray(); + var errors = await workflowsValidator.ValidateAsync(app.Id, app.Workflows); - return result.CreateLinks(resources); - } + result.Errors = errors.ToArray(); - private WorkflowsDto CreateLinks(Resources resources) - { - var values = new { app = resources.App }; + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values)); + private WorkflowsDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanCreateWorkflow) - { - AddPostLink("create", - resources.Url<AppWorkflowsController>(x => nameof(x.PostWorkflow), values)); - } + AddSelfLink(resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values)); - return this; + if (resources.CanCreateWorkflow) + { + AddPostLink("create", + resources.Url<AppWorkflowsController>(x => nameof(x.PostWorkflow), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 7ec783cd47..d8ff8cf5c8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -18,255 +18,254 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Assets +namespace Squidex.Areas.Api.Controllers.Assets; + +/// <summary> +/// Uploads and retrieves assets. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Assets))] +public sealed class AssetContentController : ApiController { + private readonly IAssetFileStore assetFileStore; + private readonly IAssetQueryService assetQuery; + private readonly IAssetLoader assetLoader; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + + public AssetContentController( + ICommandBus commandBus, + IAssetFileStore assetFileStore, + IAssetQueryService assetQuery, + IAssetLoader assetLoader, + IAssetThumbnailGenerator assetThumbnailGenerator) + : base(commandBus) + { + this.assetFileStore = assetFileStore; + this.assetQuery = assetQuery; + this.assetLoader = assetLoader; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + /// <summary> - /// Uploads and retrieves assets. + /// Get the asset content. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Assets))] - public sealed class AssetContentController : ApiController + /// <param name="app">The name of the app.</param> + /// <param name="idOrSlug">The id or slug of the asset.</param> + /// <param name="request">The request parameters.</param> + /// <param name="more">Optional suffix that can be used to seo-optimize the link to the image Has not effect.</param> + /// <returns> + /// 200 => Asset found and content or (resized) image returned. + /// 404 => Asset or app not found. + /// </returns> + [HttpGet] + [Route("assets/{app}/{idOrSlug}/{*more}")] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0.5)] + [AllowAnonymous] + public async Task<IActionResult> GetAssetContentBySlug(string app, string idOrSlug, AssetContentQueryDto request, string? more = null) { - private readonly IAssetFileStore assetFileStore; - private readonly IAssetQueryService assetQuery; - private readonly IAssetLoader assetLoader; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - - public AssetContentController( - ICommandBus commandBus, - IAssetFileStore assetFileStore, - IAssetQueryService assetQuery, - IAssetLoader assetLoader, - IAssetThumbnailGenerator assetThumbnailGenerator) - : base(commandBus) + var requestContext = Context.Clone(b => b.WithoutAssetEnrichment()); + + var asset = await assetQuery.FindAsync(requestContext, DomainId.Create(idOrSlug), ct: HttpContext.RequestAborted); + + if (asset == null) { - this.assetFileStore = assetFileStore; - this.assetQuery = assetQuery; - this.assetLoader = assetLoader; - this.assetThumbnailGenerator = assetThumbnailGenerator; + asset = await assetQuery.FindBySlugAsync(requestContext, idOrSlug, HttpContext.RequestAborted); } - /// <summary> - /// Get the asset content. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="idOrSlug">The id or slug of the asset.</param> - /// <param name="request">The request parameters.</param> - /// <param name="more">Optional suffix that can be used to seo-optimize the link to the image Has not effect.</param> - /// <returns> - /// 200 => Asset found and content or (resized) image returned. - /// 404 => Asset or app not found. - /// </returns> - [HttpGet] - [Route("assets/{app}/{idOrSlug}/{*more}")] - [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(0.5)] - [AllowAnonymous] - public async Task<IActionResult> GetAssetContentBySlug(string app, string idOrSlug, AssetContentQueryDto request, string? more = null) - { - var requestContext = Context.Clone(b => b.WithoutAssetEnrichment()); + return await DeliverAssetAsync(requestContext, asset, request); + } + + /// <summary> + /// Get the asset content. + /// </summary> + /// <param name="id">The ID of the asset.</param> + /// <param name="request">The request parameters.</param> + /// <returns> + /// 200 => Asset found and content or (resized) image returned. + /// 404 => Asset or app not found. + /// </returns> + [HttpGet] + [Route("assets/{id}/")] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0.5)] + [AllowAnonymous] + [Obsolete("Use overload with app name")] + public async Task<IActionResult> GetAssetContent(DomainId id, AssetContentQueryDto request) + { + var requestContext = Context.Clone(b => b.WithoutAssetEnrichment()); - var asset = await assetQuery.FindAsync(requestContext, DomainId.Create(idOrSlug), ct: HttpContext.RequestAborted); + var asset = await assetQuery.FindGlobalAsync(requestContext, id, HttpContext.RequestAborted); - if (asset == null) - { - asset = await assetQuery.FindBySlugAsync(requestContext, idOrSlug, HttpContext.RequestAborted); - } + return await DeliverAssetAsync(requestContext, asset, request); + } - return await DeliverAssetAsync(requestContext, asset, request); - } + private async Task<IActionResult> DeliverAssetAsync(Context context, IAssetEntity? asset, AssetContentQueryDto request) + { + request ??= new AssetContentQueryDto(); - /// <summary> - /// Get the asset content. - /// </summary> - /// <param name="id">The ID of the asset.</param> - /// <param name="request">The request parameters.</param> - /// <returns> - /// 200 => Asset found and content or (resized) image returned. - /// 404 => Asset or app not found. - /// </returns> - [HttpGet] - [Route("assets/{id}/")] - [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(0.5)] - [AllowAnonymous] - [Obsolete("Use overload with app name")] - public async Task<IActionResult> GetAssetContent(DomainId id, AssetContentQueryDto request) + if (asset == null) { - var requestContext = Context.Clone(b => b.WithoutAssetEnrichment()); + return NotFound(); + } - var asset = await assetQuery.FindGlobalAsync(requestContext, id, HttpContext.RequestAborted); + if (asset.IsProtected && !Resources.CanReadAssets) + { + Response.Headers[HeaderNames.CacheControl] = "public,max-age=0"; - return await DeliverAssetAsync(requestContext, asset, request); + return StatusCode(403); } - private async Task<IActionResult> DeliverAssetAsync(Context context, IAssetEntity? asset, AssetContentQueryDto request) + if (asset != null && request.Version > EtagVersion.Any && asset.Version != request.Version) { - request ??= new AssetContentQueryDto(); - - if (asset == null) + if (context.App != null) { - return NotFound(); + asset = await assetQuery.FindAsync(context, asset.Id, request.Version, HttpContext.RequestAborted); } - - if (asset.IsProtected && !Resources.CanReadAssets) + else { - Response.Headers[HeaderNames.CacheControl] = "public,max-age=0"; - - return StatusCode(403); + // Fallback for old endpoint. Does not set the surrogate key. + asset = await assetLoader.GetAsync(asset.AppId.Id, asset.Id, request.Version, HttpContext.RequestAborted); } + } - if (asset != null && request.Version > EtagVersion.Any && asset.Version != request.Version) - { - if (context.App != null) - { - asset = await assetQuery.FindAsync(context, asset.Id, request.Version, HttpContext.RequestAborted); - } - else - { - // Fallback for old endpoint. Does not set the surrogate key. - asset = await assetLoader.GetAsync(asset.AppId.Id, asset.Id, request.Version, HttpContext.RequestAborted); - } - } + if (asset == null) + { + return NotFound(); + } - if (asset == null) - { - return NotFound(); - } + Response.Headers[HeaderNames.ETag] = asset.FileVersion.ToString(CultureInfo.InvariantCulture); - Response.Headers[HeaderNames.ETag] = asset.FileVersion.ToString(CultureInfo.InvariantCulture); + if (request.CacheDuration > 0) + { + Response.Headers[HeaderNames.CacheControl] = $"public,max-age={request.CacheDuration}"; + } - if (request.CacheDuration > 0) - { - Response.Headers[HeaderNames.CacheControl] = $"public,max-age={request.CacheDuration}"; - } + var resizeOptions = request.ToResizeOptions(asset, assetThumbnailGenerator, HttpContext.Request); - var resizeOptions = request.ToResizeOptions(asset, assetThumbnailGenerator, HttpContext.Request); + var contentLength = (long?)null; + var contentCallback = (FileCallback?)null; + var contentType = asset.MimeType; - var contentLength = (long?)null; - var contentCallback = (FileCallback?)null; - var contentType = asset.MimeType; + if (asset.Type == AssetType.Image && assetThumbnailGenerator.IsResizable(asset.MimeType, resizeOptions, out var destinationMimeType)) + { + contentType = destinationMimeType!; - if (asset.Type == AssetType.Image && assetThumbnailGenerator.IsResizable(asset.MimeType, resizeOptions, out var destinationMimeType)) + contentCallback = async (body, range, ct) => { - contentType = destinationMimeType!; + var suffix = resizeOptions.ToString(); - contentCallback = async (body, range, ct) => + if (request.Force) { - var suffix = resizeOptions.ToString(); - - if (request.Force) + using (Telemetry.Activities.StartActivity("Resize")) + { + await ResizeAsync(asset, suffix, body, resizeOptions, true, ct); + } + } + else + { + try { - using (Telemetry.Activities.StartActivity("Resize")) - { - await ResizeAsync(asset, suffix, body, resizeOptions, true, ct); - } + await DownloadAsync(asset, body, suffix, range, ct); } - else + catch (AssetNotFoundException) { - try - { - await DownloadAsync(asset, body, suffix, range, ct); - } - catch (AssetNotFoundException) - { - await ResizeAsync(asset, suffix, body, resizeOptions, false, ct); - } + await ResizeAsync(asset, suffix, body, resizeOptions, false, ct); } - }; - } - else - { - contentLength = asset.FileSize; - - contentCallback = async (body, range, ct) => - { - await DownloadAsync(asset, body, null, range, ct); - }; - } + } + }; + } + else + { + contentLength = asset.FileSize; - return new FileCallbackResult(contentType, contentCallback) + contentCallback = async (body, range, ct) => { - EnableRangeProcessing = contentLength > 0, - ErrorAs404 = true, - FileDownloadName = asset.FileName, - FileSize = contentLength, - LastModified = asset.LastModified.ToDateTimeOffset(), - SendInline = request.Download != 1 + await DownloadAsync(asset, body, null, range, ct); }; } - private async Task DownloadAsync(IAssetEntity asset, Stream bodyStream, string? suffix, BytesRange range, - CancellationToken ct) + return new FileCallbackResult(contentType, contentCallback) { - await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, bodyStream, range, ct); - } + EnableRangeProcessing = contentLength > 0, + ErrorAs404 = true, + FileDownloadName = asset.FileName, + FileSize = contentLength, + LastModified = asset.LastModified.ToDateTimeOffset(), + SendInline = request.Download != 1 + }; + } - private async Task ResizeAsync(IAssetEntity asset, string suffix, Stream target, ResizeOptions resizeOptions, bool overwrite, - CancellationToken ct) - { + private async Task DownloadAsync(IAssetEntity asset, Stream bodyStream, string? suffix, BytesRange range, + CancellationToken ct) + { + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, bodyStream, range, ct); + } + + private async Task ResizeAsync(IAssetEntity asset, string suffix, Stream target, ResizeOptions resizeOptions, bool overwrite, + CancellationToken ct) + { #pragma warning disable MA0040 // Flow the cancellation token - using var activity = Telemetry.Activities.StartActivity("Resize"); + using var activity = Telemetry.Activities.StartActivity("Resize"); - await using var assetOriginal = new TempAssetFile(asset.FileName, asset.MimeType, 0); - await using var assetResized = new TempAssetFile(asset.FileName, asset.MimeType, 0); + await using var assetOriginal = new TempAssetFile(asset.FileName, asset.MimeType, 0); + await using var assetResized = new TempAssetFile(asset.FileName, asset.MimeType, 0); - using (Telemetry.Activities.StartActivity("Read")) + using (Telemetry.Activities.StartActivity("Read")) + { + await using (var originalStream = assetOriginal.OpenWrite()) { - await using (var originalStream = assetOriginal.OpenWrite()) - { - await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, originalStream); - } + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, originalStream); } + } - using (Telemetry.Activities.StartActivity("Resize")) + using (Telemetry.Activities.StartActivity("Resize")) + { + try { - try + await using (var originalStream = assetOriginal.OpenRead()) { - await using (var originalStream = assetOriginal.OpenRead()) + await using (var resizeStream = assetResized.OpenWrite()) { - await using (var resizeStream = assetResized.OpenWrite()) - { - await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, asset.MimeType, resizeStream, resizeOptions); - } - } - } - catch - { - await using (var originalStream = assetOriginal.OpenRead()) - { - await using (var resizeStream = assetResized.OpenWrite()) - { - await originalStream.CopyToAsync(resizeStream); - } + await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, asset.MimeType, resizeStream, resizeOptions); } } } - - using (Telemetry.Activities.StartActivity("Save")) + catch { - try + await using (var originalStream = assetOriginal.OpenRead()) { - await using (var resizeStream = assetResized.OpenRead()) + await using (var resizeStream = assetResized.OpenWrite()) { - await assetFileStore.UploadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, resizeStream, overwrite); + await originalStream.CopyToAsync(resizeStream); } } - catch (AssetAlreadyExistsException) - { - return; - } } + } - using (Telemetry.Activities.StartActivity("Write")) + using (Telemetry.Activities.StartActivity("Save")) + { + try { await using (var resizeStream = assetResized.OpenRead()) { - await resizeStream.CopyToAsync(target, ct); + await assetFileStore.UploadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, resizeStream, overwrite); } } -#pragma warning restore MA0040 // Flow the cancellation token + catch (AssetAlreadyExistsException) + { + return; + } + } + + using (Telemetry.Activities.StartActivity("Write")) + { + await using (var resizeStream = assetResized.OpenRead()) + { + await resizeStream.CopyToAsync(target, ct); + } } +#pragma warning restore MA0040 // Flow the cancellation token } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs index d8353dc3ee..4e05d1b756 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs @@ -17,180 +17,179 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Assets +namespace Squidex.Areas.Api.Controllers.Assets; + +/// <summary> +/// Uploads and retrieves assets. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Assets))] +public sealed class AssetFoldersController : ApiController { + private readonly IAssetQueryService assetQuery; + + public AssetFoldersController(ICommandBus commandBus, IAssetQueryService assetQuery) + : base(commandBus) + { + this.assetQuery = assetQuery; + } + /// <summary> - /// Uploads and retrieves assets. + /// Get asset folders. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Assets))] - public sealed class AssetFoldersController : ApiController + /// <param name="app">The name of the app.</param> + /// <param name="parentId">The optional parent folder id.</param> + /// <param name="scope">The scope of the query.</param> + /// <returns> + /// 200 => Asset folders returned. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// Get all asset folders for the app. + /// </remarks> + [HttpGet] + [Route("apps/{app}/assets/folders", Order = -1)] + [ProducesResponseType(typeof(AssetFoldersDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] + [ApiCosts(1)] + public async Task<IActionResult> GetAssetFolders(string app, [FromQuery] DomainId parentId, [FromQuery] AssetFolderScope scope = AssetFolderScope.PathAndItems) { - private readonly IAssetQueryService assetQuery; + var (folders, path) = await AsyncHelper.WhenAll( + GetAssetFoldersAsync(parentId, scope), + GetAssetPathAsync(parentId, scope)); - public AssetFoldersController(ICommandBus commandBus, IAssetQueryService assetQuery) - : base(commandBus) + var response = Deferred.Response(() => { - this.assetQuery = assetQuery; - } + return AssetFoldersDto.FromDomain(folders, path, Resources); + }); - /// <summary> - /// Get asset folders. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="parentId">The optional parent folder id.</param> - /// <param name="scope">The scope of the query.</param> - /// <returns> - /// 200 => Asset folders returned. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// Get all asset folders for the app. - /// </remarks> - [HttpGet] - [Route("apps/{app}/assets/folders", Order = -1)] - [ProducesResponseType(typeof(AssetFoldersDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] - [ApiCosts(1)] - public async Task<IActionResult> GetAssetFolders(string app, [FromQuery] DomainId parentId, [FromQuery] AssetFolderScope scope = AssetFolderScope.PathAndItems) - { - var (folders, path) = await AsyncHelper.WhenAll( - GetAssetFoldersAsync(parentId, scope), - GetAssetPathAsync(parentId, scope)); + Response.Headers[HeaderNames.ETag] = folders.ToEtag(); - var response = Deferred.Response(() => - { - return AssetFoldersDto.FromDomain(folders, path, Resources); - }); + return Ok(response); + } - Response.Headers[HeaderNames.ETag] = folders.ToEtag(); + /// <summary> + /// Create an asset folder. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">The asset folder object that needs to be added to the App.</param> + /// <returns> + /// 201 => Asset folder created. + /// 400 => Asset folder request not valid. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/assets/folders", Order = -1)] + [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status201Created)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PostAssetFolder(string app, [FromBody] CreateAssetFolderDto request) + { + var command = request.ToCommand(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Create an asset folder. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The asset folder object that needs to be added to the App.</param> - /// <returns> - /// 201 => Asset folder created. - /// 400 => Asset folder request not valid. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/assets/folders", Order = -1)] - [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status201Created)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PostAssetFolder(string app, [FromBody] CreateAssetFolderDto request) - { - var command = request.ToCommand(); + return CreatedAtAction(nameof(GetAssetFolders), new { parentId = request.ParentId, app }, response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Update an asset folder. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the asset folder.</param> + /// <param name="request">The asset folder object that needs to updated.</param> + /// <returns> + /// 200 => Asset folder updated. + /// 400 => Asset folder request not valid. + /// 404 => Asset folder or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/assets/folders/{id}/", Order = -1)] + [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutAssetFolder(string app, DomainId id, [FromBody] RenameAssetFolderDto request) + { + var command = request.ToCommand(id); - return CreatedAtAction(nameof(GetAssetFolders), new { parentId = request.ParentId, app }, response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Update an asset folder. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the asset folder.</param> - /// <param name="request">The asset folder object that needs to updated.</param> - /// <returns> - /// 200 => Asset folder updated. - /// 400 => Asset folder request not valid. - /// 404 => Asset folder or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/assets/folders/{id}/", Order = -1)] - [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutAssetFolder(string app, DomainId id, [FromBody] RenameAssetFolderDto request) - { - var command = request.ToCommand(id); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Move an asset folder. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the asset folder.</param> + /// <param name="request">The asset folder object that needs to updated.</param> + /// <returns> + /// 200 => Asset folder moved. + /// 400 => Asset folder request not valid. + /// 404 => Asset folder or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/assets/folders/{id}/parent", Order = -1)] + [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutAssetFolderParent(string app, DomainId id, [FromBody] MoveAssetFolderDto request) + { + var command = request.ToCommand(id); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Move an asset folder. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the asset folder.</param> - /// <param name="request">The asset folder object that needs to updated.</param> - /// <returns> - /// 200 => Asset folder moved. - /// 400 => Asset folder request not valid. - /// 404 => Asset folder or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/assets/folders/{id}/parent", Order = -1)] - [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutAssetFolderParent(string app, DomainId id, [FromBody] MoveAssetFolderDto request) - { - var command = request.ToCommand(id); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Delete an asset folder. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the asset folder to delete.</param> + /// <returns> + /// 204 => Asset folder deleted. + /// 404 => Asset folder or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/assets/folders/{id}/", Order = -1)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteAssetFolder(string app, DomainId id) + { + var command = new DeleteAssetFolder { AssetFolderId = id }; - return Ok(response); - } + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - /// <summary> - /// Delete an asset folder. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the asset folder to delete.</param> - /// <returns> - /// 204 => Asset folder deleted. - /// 404 => Asset folder or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/assets/folders/{id}/", Order = -1)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteAssetFolder(string app, DomainId id) - { - var command = new DeleteAssetFolder { AssetFolderId = id }; + return NoContent(); + } - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + private async Task<AssetFolderDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - return NoContent(); - } + return AssetFolderDto.FromDomain(context.Result<IAssetFolderEntity>(), Resources); + } - private async Task<AssetFolderDto> InvokeCommandAsync(ICommand command) + private Task<IReadOnlyList<IAssetFolderEntity>> GetAssetPathAsync(DomainId parentId, AssetFolderScope scope) + { + if (scope == AssetFolderScope.Items) { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - return AssetFolderDto.FromDomain(context.Result<IAssetFolderEntity>(), Resources); + return Task.FromResult<IReadOnlyList<IAssetFolderEntity>>(ReadonlyList.Empty<IAssetFolderEntity>()); } - private Task<IReadOnlyList<IAssetFolderEntity>> GetAssetPathAsync(DomainId parentId, AssetFolderScope scope) - { - if (scope == AssetFolderScope.Items) - { - return Task.FromResult<IReadOnlyList<IAssetFolderEntity>>(ReadonlyList.Empty<IAssetFolderEntity>()); - } - - return assetQuery.FindAssetFolderAsync(Context.App.Id, parentId, HttpContext.RequestAborted); - } + return assetQuery.FindAssetFolderAsync(Context.App.Id, parentId, HttpContext.RequestAborted); + } - private Task<IResultList<IAssetFolderEntity>> GetAssetFoldersAsync(DomainId parentId, AssetFolderScope scope) + private Task<IResultList<IAssetFolderEntity>> GetAssetFoldersAsync(DomainId parentId, AssetFolderScope scope) + { + if (scope == AssetFolderScope.Path) { - if (scope == AssetFolderScope.Path) - { - return Task.FromResult(ResultList.Empty<IAssetFolderEntity>()); - } - - return assetQuery.QueryAssetFoldersAsync(Context, parentId, HttpContext.RequestAborted); + return Task.FromResult(ResultList.Empty<IAssetFolderEntity>()); } + + return assetQuery.QueryAssetFoldersAsync(Context, parentId, HttpContext.RequestAborted); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 263c50694b..38160e8b8b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -24,471 +24,470 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Assets +namespace Squidex.Areas.Api.Controllers.Assets; + +/// <summary> +/// Uploads and retrieves assets. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Assets))] +public sealed class AssetsController : ApiController { + private readonly IUsageGate usageGate; + private readonly IAssetQueryService assetQuery; + private readonly IAssetUsageTracker assetUsageTracker; + private readonly ITagService tagService; + private readonly AssetTusRunner assetTusRunner; + + public AssetsController( + ICommandBus commandBus, + IUsageGate usageGate, + IAssetQueryService assetQuery, + IAssetUsageTracker assetUsageTracker, + ITagService tagService, + AssetTusRunner assetTusRunner) + : base(commandBus) + { + this.usageGate = usageGate; + this.assetQuery = assetQuery; + this.assetUsageTracker = assetUsageTracker; + this.assetTusRunner = assetTusRunner; + this.tagService = tagService; + } + /// <summary> - /// Uploads and retrieves assets. + /// Get assets tags. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Assets))] - public sealed class AssetsController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Assets tags returned. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// Get all tags for assets. + /// </remarks> + [HttpGet] + [Route("apps/{app}/assets/tags")] + [ProducesResponseType(typeof(Dictionary<string, int>), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] + [ApiCosts(1)] + public async Task<IActionResult> GetTags(string app) { - private readonly IUsageGate usageGate; - private readonly IAssetQueryService assetQuery; - private readonly IAssetUsageTracker assetUsageTracker; - private readonly ITagService tagService; - private readonly AssetTusRunner assetTusRunner; - - public AssetsController( - ICommandBus commandBus, - IUsageGate usageGate, - IAssetQueryService assetQuery, - IAssetUsageTracker assetUsageTracker, - ITagService tagService, - AssetTusRunner assetTusRunner) - : base(commandBus) - { - this.usageGate = usageGate; - this.assetQuery = assetQuery; - this.assetUsageTracker = assetUsageTracker; - this.assetTusRunner = assetTusRunner; - this.tagService = tagService; - } + var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets, HttpContext.RequestAborted); - /// <summary> - /// Get assets tags. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Assets tags returned. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// Get all tags for assets. - /// </remarks> - [HttpGet] - [Route("apps/{app}/assets/tags")] - [ProducesResponseType(typeof(Dictionary<string, int>), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] - [ApiCosts(1)] - public async Task<IActionResult> GetTags(string app) - { - var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets, HttpContext.RequestAborted); + Response.Headers[HeaderNames.ETag] = tags.Version.ToString(CultureInfo.InvariantCulture); - Response.Headers[HeaderNames.ETag] = tags.Version.ToString(CultureInfo.InvariantCulture); + return Ok(tags); + } - return Ok(tags); - } + /// <summary> + /// Rename an asset tag. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="name">The tag to return.</param> + /// <param name="request">The required request object.</param> + /// <returns> + /// 200 => Asset tag renamed and new tags returned. + /// 404 => App not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/assets/tags/{name}")] + [ProducesResponseType(typeof(Dictionary<string, int>), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutTag(string app, string name, [FromBody] RenameTagDto request) + { + await tagService.RenameTagAsync(AppId, TagGroups.Assets, Uri.UnescapeDataString(name), request.TagName, HttpContext.RequestAborted); - /// <summary> - /// Rename an asset tag. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="name">The tag to return.</param> - /// <param name="request">The required request object.</param> - /// <returns> - /// 200 => Asset tag renamed and new tags returned. - /// 404 => App not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/assets/tags/{name}")] - [ProducesResponseType(typeof(Dictionary<string, int>), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutTag(string app, string name, [FromBody] RenameTagDto request) - { - await tagService.RenameTagAsync(AppId, TagGroups.Assets, Uri.UnescapeDataString(name), request.TagName, HttpContext.RequestAborted); + return await GetTags(app); + } - return await GetTags(app); - } + /// <summary> + /// Get assets. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="parentId">The optional parent folder id.</param> + /// <param name="ids">The optional asset ids.</param> + /// <param name="q">The optional json query.</param> + /// <returns> + /// 200 => Assets returned. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// Get all assets for the app. + /// </remarks> + [HttpGet] + [Route("apps/{app}/assets/")] + [ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] + [ApiCosts(1)] + public async Task<IActionResult> GetAssets(string app, [FromQuery] DomainId? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null) + { + var assets = await assetQuery.QueryAsync(Context, parentId, CreateQuery(ids, q), HttpContext.RequestAborted); - /// <summary> - /// Get assets. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="parentId">The optional parent folder id.</param> - /// <param name="ids">The optional asset ids.</param> - /// <param name="q">The optional json query.</param> - /// <returns> - /// 200 => Assets returned. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// Get all assets for the app. - /// </remarks> - [HttpGet] - [Route("apps/{app}/assets/")] - [ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] - [ApiCosts(1)] - public async Task<IActionResult> GetAssets(string app, [FromQuery] DomainId? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null) + var response = Deferred.Response(() => { - var assets = await assetQuery.QueryAsync(Context, parentId, CreateQuery(ids, q), HttpContext.RequestAborted); + return AssetsDto.FromDomain(assets, Resources); + }); - var response = Deferred.Response(() => - { - return AssetsDto.FromDomain(assets, Resources); - }); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Get assets. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="query">The required query object.</param> + /// <returns> + /// 200 => Assets returned. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// Get all assets for the app. + /// </remarks> + [HttpPost] + [Route("apps/{app}/assets/query")] + [ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] + [ApiCosts(1)] + public async Task<IActionResult> GetAssetsPost(string app, [FromBody] QueryDto query) + { + var assets = await assetQuery.QueryAsync(Context, query?.ParentId, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); - /// <summary> - /// Get assets. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="query">The required query object.</param> - /// <returns> - /// 200 => Assets returned. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// Get all assets for the app. - /// </remarks> - [HttpPost] - [Route("apps/{app}/assets/query")] - [ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] - [ApiCosts(1)] - public async Task<IActionResult> GetAssetsPost(string app, [FromBody] QueryDto query) + var response = Deferred.Response(() => { - var assets = await assetQuery.QueryAsync(Context, query?.ParentId, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); + return AssetsDto.FromDomain(assets, Resources); + }); - var response = Deferred.Response(() => - { - return AssetsDto.FromDomain(assets, Resources); - }); + return Ok(response); + } - return Ok(response); + /// <summary> + /// Get an asset by id. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the asset to retrieve.</param> + /// <returns> + /// 200 => Asset found. + /// 404 => Asset or app not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/assets/{id}/")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] + [ApiCosts(1)] + public async Task<IActionResult> GetAsset(string app, DomainId id) + { + var asset = await assetQuery.FindAsync(Context, id, ct: HttpContext.RequestAborted); + + if (asset == null) + { + return NotFound(); } - /// <summary> - /// Get an asset by id. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the asset to retrieve.</param> - /// <returns> - /// 200 => Asset found. - /// 404 => Asset or app not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] - [ApiCosts(1)] - public async Task<IActionResult> GetAsset(string app, DomainId id) + var response = Deferred.Response(() => { - var asset = await assetQuery.FindAsync(Context, id, ct: HttpContext.RequestAborted); + return AssetDto.FromDomain(asset, Resources); + }); - if (asset == null) - { - return NotFound(); - } + return Ok(response); + } - var response = Deferred.Response(() => - { - return AssetDto.FromDomain(asset, Resources); - }); + /// <summary> + /// Upload a new asset. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">The request parameters.</param> + /// <returns> + /// 201 => Asset created. + /// 400 => Asset request not valid. + /// 413 => Asset exceeds the maximum upload size. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. + /// </remarks> + [HttpPost] + [Route("apps/{app}/assets/")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status201Created)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostAsset(string app, CreateAssetDto request) + { + var command = request.ToCommand(await CheckAssetFileAsync(request.File)); - return Ok(response); - } + var response = await InvokeCommandAsync(command); + + return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); + } + + /// <summary> + /// Upload a new asset using tus.io. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 201 => Asset created. + /// 400 => Asset request not valid. + /// 413 => Asset exceeds the maximum upload size. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// Use the tus protocol to upload an asset. + /// </remarks> + [OpenApiIgnore] + [Route("apps/{app}/assets/tus/{**fileId}")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status201Created)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostAssetTus(string app) + { + var url = Url.Action(null, new { app, fileId = (object?)null })!; - /// <summary> - /// Upload a new asset. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The request parameters.</param> - /// <returns> - /// 201 => Asset created. - /// 400 => Asset request not valid. - /// 413 => Asset exceeds the maximum upload size. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. - /// </remarks> - [HttpPost] - [Route("apps/{app}/assets/")] - [ProducesResponseType(typeof(AssetDto), StatusCodes.Status201Created)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostAsset(string app, CreateAssetDto request) + var (result, file) = await assetTusRunner.InvokeAsync(HttpContext, url); + + if (file != null) { - var command = request.ToCommand(await CheckAssetFileAsync(request.File)); + var command = UpsertAssetDto.ToCommand(file); var response = await InvokeCommandAsync(command); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); } - /// <summary> - /// Upload a new asset using tus.io. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 201 => Asset created. - /// 400 => Asset request not valid. - /// 413 => Asset exceeds the maximum upload size. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// Use the tus protocol to upload an asset. - /// </remarks> - [OpenApiIgnore] - [Route("apps/{app}/assets/tus/{**fileId}")] - [ProducesResponseType(typeof(AssetDto), StatusCodes.Status201Created)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostAssetTus(string app) - { - var url = Url.Action(null, new { app, fileId = (object?)null })!; - - var (result, file) = await assetTusRunner.InvokeAsync(HttpContext, url); - - if (file != null) - { - var command = UpsertAssetDto.ToCommand(file); + return result; + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Bulk update assets. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">The bulk update request.</param> + /// <returns> + /// 200 => Assets created, update or delete. + /// 400 => Assets request not valid. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/assets/bulk")] + [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] + [ApiCosts(5)] + public async Task<IActionResult> BulkUpdateAssets(string app, [FromBody] BulkUpdateAssetsDto request) + { + var command = request.ToCommand(); - return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); - } + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - return result; - } + var result = context.Result<BulkUpdateResult>(); + var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); - /// <summary> - /// Bulk update assets. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The bulk update request.</param> - /// <returns> - /// 200 => Assets created, update or delete. - /// 400 => Assets request not valid. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/assets/bulk")] - [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsRead)] - [ApiCosts(5)] - public async Task<IActionResult> BulkUpdateAssets(string app, [FromBody] BulkUpdateAssetsDto request) - { - var command = request.ToCommand(); + return Ok(response); + } - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + /// <summary> + /// Upsert an asset. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The optional custom asset id.</param> + /// <param name="request">The request parameters.</param> + /// <returns> + /// 200 => Asset created or updated. + /// 400 => Asset request not valid. + /// 413 => Asset exceeds the maximum upload size. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. + /// </remarks> + [HttpPost] + [Route("apps/{app}/assets/{id}")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostUpsertAsset(string app, DomainId id, UpsertAssetDto request) + { + var command = request.ToCommand(id, await CheckAssetFileAsync(request.File)); - var result = context.Result<BulkUpdateResult>(); - var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); + var response = await InvokeCommandAsync(command); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Upsert an asset. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The optional custom asset id.</param> - /// <param name="request">The request parameters.</param> - /// <returns> - /// 200 => Asset created or updated. - /// 400 => Asset request not valid. - /// 413 => Asset exceeds the maximum upload size. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. - /// </remarks> - [HttpPost] - [Route("apps/{app}/assets/{id}")] - [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostUpsertAsset(string app, DomainId id, UpsertAssetDto request) - { - var command = request.ToCommand(id, await CheckAssetFileAsync(request.File)); + /// <summary> + /// Replace asset content. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the asset.</param> + /// <param name="file">The file to upload.</param> + /// <returns> + /// 200 => Asset updated. + /// 400 => Asset request not valid. + /// 413 => Asset exceeds the maximum upload size. + /// 404 => Asset or app not found. + /// </returns> + /// <remarks> + /// Use multipart request to upload an asset. + /// </remarks> + [HttpPut] + [Route("apps/{app}/assets/{id}/content/")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpload)] + [ApiCosts(1)] + public async Task<IActionResult> PutAssetContent(string app, DomainId id, IFormFile file) + { + var command = new UpdateAsset { File = await CheckAssetFileAsync(file), AssetId = id }; - var response = await InvokeCommandAsync(command); + var response = await InvokeCommandAsync(command); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Replace asset content. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the asset.</param> - /// <param name="file">The file to upload.</param> - /// <returns> - /// 200 => Asset updated. - /// 400 => Asset request not valid. - /// 413 => Asset exceeds the maximum upload size. - /// 404 => Asset or app not found. - /// </returns> - /// <remarks> - /// Use multipart request to upload an asset. - /// </remarks> - [HttpPut] - [Route("apps/{app}/assets/{id}/content/")] - [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpload)] - [ApiCosts(1)] - public async Task<IActionResult> PutAssetContent(string app, DomainId id, IFormFile file) - { - var command = new UpdateAsset { File = await CheckAssetFileAsync(file), AssetId = id }; + /// <summary> + /// Update an asset. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the asset.</param> + /// <param name="request">The asset object that needs to updated.</param> + /// <returns> + /// 200 => Asset updated. + /// 400 => Asset request not valid. + /// 404 => Asset or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/assets/{id}/")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutAsset(string app, DomainId id, [FromBody] AnnotateAssetDto request) + { + var command = request.ToCommand(id); - var response = await InvokeCommandAsync(command); + var response = await InvokeCommandAsync(command); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Update an asset. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the asset.</param> - /// <param name="request">The asset object that needs to updated.</param> - /// <returns> - /// 200 => Asset updated. - /// 400 => Asset request not valid. - /// 404 => Asset or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutAsset(string app, DomainId id, [FromBody] AnnotateAssetDto request) - { - var command = request.ToCommand(id); + /// <summary> + /// Moves the asset. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the asset.</param> + /// <param name="request">The asset object that needs to updated.</param> + /// <returns> + /// 200 => Asset moved. + /// 400 => Asset request not valid. + /// 404 => Asset or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/assets/{id}/parent")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] + [AssetRequestSizeLimit] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutAssetParent(string app, DomainId id, [FromBody] MoveAssetDto request) + { + var command = request.ToCommand(id); - var response = await InvokeCommandAsync(command); + var response = await InvokeCommandAsync(command); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Moves the asset. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the asset.</param> - /// <param name="request">The asset object that needs to updated.</param> - /// <returns> - /// 200 => Asset moved. - /// 400 => Asset request not valid. - /// 404 => Asset or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/assets/{id}/parent")] - [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] - [AssetRequestSizeLimit] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutAssetParent(string app, DomainId id, [FromBody] MoveAssetDto request) - { - var command = request.ToCommand(id); + /// <summary> + /// Delete an asset. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the asset to delete.</param> + /// <param name="request">The request parameters.</param> + /// <returns> + /// 204 => Asset deleted. + /// 404 => Asset or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/assets/{id}/")] + [ApiPermissionOrAnonymous(PermissionIds.AppAssetsDelete)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteAsset(string app, DomainId id, DeleteAssetDto request) + { + var command = request.ToCommand(id); - var response = await InvokeCommandAsync(command); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - return Ok(response); - } + return NoContent(); + } - /// <summary> - /// Delete an asset. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the asset to delete.</param> - /// <param name="request">The request parameters.</param> - /// <returns> - /// 204 => Asset deleted. - /// 404 => Asset or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/assets/{id}/")] - [ApiPermissionOrAnonymous(PermissionIds.AppAssetsDelete)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteAsset(string app, DomainId id, DeleteAssetDto request) - { - var command = request.ToCommand(id); + [HttpGet] + [Route("apps/{app}/assets/completion")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + [OpenApiIgnore] + public IActionResult GetScriptCompletion(string app, string schema, + [FromServices] ScriptingCompleter completer) + { + var completion = completer.AssetScript(); - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return Ok(completion); + } - return NoContent(); - } + [HttpGet] + [Route("apps/{app}/assets/completion/trigger")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + [OpenApiIgnore] + public IActionResult GetScriptTriggerCompletion(string app, string schema, + [FromServices] ScriptingCompleter completer) + { + var completion = completer.AssetTrigger(); - [HttpGet] - [Route("apps/{app}/assets/completion")] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - [OpenApiIgnore] - public IActionResult GetScriptCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) - { - var completion = completer.AssetScript(); + return Ok(completion); + } - return Ok(completion); - } + private async Task<AssetDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - [HttpGet] - [Route("apps/{app}/assets/completion/trigger")] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - [OpenApiIgnore] - public IActionResult GetScriptTriggerCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) + if (context.PlainResult is AssetDuplicate created) { - var completion = completer.AssetTrigger(); - - return Ok(completion); + return AssetDto.FromDomain(created.Asset, Resources, true); } - - private async Task<AssetDto> InvokeCommandAsync(ICommand command) + else { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - if (context.PlainResult is AssetDuplicate created) - { - return AssetDto.FromDomain(created.Asset, Resources, true); - } - else - { - return AssetDto.FromDomain(context.Result<IEnrichedAssetEntity>(), Resources); - } + return AssetDto.FromDomain(context.Result<IEnrichedAssetEntity>(), Resources); } + } - private async Task<AssetFile> CheckAssetFileAsync(IFormFile? file) + private async Task<AssetFile> CheckAssetFileAsync(IFormFile? file) + { + if (file == null || Request.Form.Files.Count != 1) { - if (file == null || Request.Form.Files.Count != 1) - { - var error = T.Get("validation.onlyOneFile"); - - throw new ValidationException(error); - } + var error = T.Get("validation.onlyOneFile"); - var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + throw new ValidationException(error); + } - var currentSize = await assetUsageTracker.GetTotalSizeByAppAsync(AppId, HttpContext.RequestAborted); + var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); - if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + file.Length) - { - var error = new ValidationError(T.Get("assets.maxSizeReached")); + var currentSize = await assetUsageTracker.GetTotalSizeByAppAsync(AppId, HttpContext.RequestAborted); - throw new ValidationException(error); - } + if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + file.Length) + { + var error = new ValidationError(T.Get("assets.maxSizeReached")); - return file.ToAssetFile(); + throw new ValidationException(error); } - private Q CreateQuery(string? ids, string? q) - { - return Q.Empty - .WithIds(ids) - .WithJsonQuery(q) - .WithODataQuery(Request.QueryString.ToString()); - } + return file.ToAssetFile(); + } + + private Q CreateQuery(string? ids, string? q) + { + return Q.Empty + .WithIds(ids) + .WithJsonQuery(q) + .WithODataQuery(Request.QueryString.ToString()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs index b617f285f2..f9393ba5a7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs @@ -10,38 +10,37 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class AnnotateAssetDto { - public sealed class AnnotateAssetDto + /// <summary> + /// The new name of the asset. + /// </summary> + public string? FileName { get; set; } + + /// <summary> + /// The new slug of the asset. + /// </summary> + public string? Slug { get; set; } + + /// <summary> + /// True, when the asset is not public. + /// </summary> + public bool? IsProtected { get; set; } + + /// <summary> + /// The new asset tags. + /// </summary> + public HashSet<string>? Tags { get; set; } + + /// <summary> + /// The asset metadata. + /// </summary> + public AssetMetadata? Metadata { get; set; } + + public AnnotateAsset ToCommand(DomainId id) { - /// <summary> - /// The new name of the asset. - /// </summary> - public string? FileName { get; set; } - - /// <summary> - /// The new slug of the asset. - /// </summary> - public string? Slug { get; set; } - - /// <summary> - /// True, when the asset is not public. - /// </summary> - public bool? IsProtected { get; set; } - - /// <summary> - /// The new asset tags. - /// </summary> - public HashSet<string>? Tags { get; set; } - - /// <summary> - /// The asset metadata. - /// </summary> - public AssetMetadata? Metadata { get; set; } - - public AnnotateAsset ToCommand(DomainId id) - { - return SimpleMapper.Map(this, new AnnotateAsset { AssetId = id }); - } + return SimpleMapper.Map(this, new AnnotateAsset { AssetId = id }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs index 2ba1981d78..1140d9d4b2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs @@ -11,164 +11,163 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class AssetContentQueryDto { - public sealed class AssetContentQueryDto + /// <summary> + /// The optional version of the asset. + /// </summary> + [FromQuery(Name = "version")] + public long Version { get; set; } = EtagVersion.Any; + + /// <summary> + /// The cache duration in seconds. + /// </summary> + [FromQuery(Name = "cache")] + public long CacheDuration { get; set; } + + /// <summary> + /// Set it to 0 to prevent download. + /// </summary> + [FromQuery(Name = "download")] + public int Download { get; set; } = 0; + + /// <summary> + /// The target width of the asset, if it is an image. + /// </summary> + [FromQuery(Name = "width")] + public int? Width { get; set; } + + /// <summary> + /// The target height of the asset, if it is an image. + /// </summary> + [FromQuery(Name = "height")] + public int? Height { get; set; } + + /// <summary> + /// Optional image quality, it is is an jpeg image. + /// </summary> + [FromQuery(Name = "quality")] + public int? Quality { get; set; } + + /// <summary> + /// The resize mode when the width and height is defined. + /// </summary> + [FromQuery(Name = "mode")] + public ResizeMode? Mode { get; set; } + + /// <summary> + /// Optional background color. + /// </summary> + [FromQuery(Name = "bg")] + public string? Background { get; set; } + + /// <summary> + /// Override the y focus point. + /// </summary> + [FromQuery(Name = "focusX")] + public float? FocusX { get; set; } + + /// <summary> + /// Override the x focus point. + /// </summary> + [FromQuery(Name = "focusY")] + public float? FocusY { get; set; } + + /// <summary> + /// True to ignore the asset focus point if any. + /// </summary> + [FromQuery(Name = "nofocus")] + public bool IgnoreFocus { get; set; } + + /// <summary> + /// True to use auto format. + /// </summary> + [FromQuery(Name = "auto")] + public bool Auto { get; set; } + + /// <summary> + /// True to force a new resize even if it already stored. + /// </summary> + [FromQuery(Name = "force")] + public bool Force { get; set; } + + /// <summary> + /// True to force a new resize even if it already stored. + /// </summary> + [FromQuery(Name = "format")] + public ImageFormat? Format { get; set; } + + public ResizeOptions ToResizeOptions(IAssetEntity asset, IAssetThumbnailGenerator assetThumbnailGenerator, HttpRequest request) { - /// <summary> - /// The optional version of the asset. - /// </summary> - [FromQuery(Name = "version")] - public long Version { get; set; } = EtagVersion.Any; - - /// <summary> - /// The cache duration in seconds. - /// </summary> - [FromQuery(Name = "cache")] - public long CacheDuration { get; set; } - - /// <summary> - /// Set it to 0 to prevent download. - /// </summary> - [FromQuery(Name = "download")] - public int Download { get; set; } = 0; - - /// <summary> - /// The target width of the asset, if it is an image. - /// </summary> - [FromQuery(Name = "width")] - public int? Width { get; set; } - - /// <summary> - /// The target height of the asset, if it is an image. - /// </summary> - [FromQuery(Name = "height")] - public int? Height { get; set; } - - /// <summary> - /// Optional image quality, it is is an jpeg image. - /// </summary> - [FromQuery(Name = "quality")] - public int? Quality { get; set; } - - /// <summary> - /// The resize mode when the width and height is defined. - /// </summary> - [FromQuery(Name = "mode")] - public ResizeMode? Mode { get; set; } - - /// <summary> - /// Optional background color. - /// </summary> - [FromQuery(Name = "bg")] - public string? Background { get; set; } - - /// <summary> - /// Override the y focus point. - /// </summary> - [FromQuery(Name = "focusX")] - public float? FocusX { get; set; } - - /// <summary> - /// Override the x focus point. - /// </summary> - [FromQuery(Name = "focusY")] - public float? FocusY { get; set; } - - /// <summary> - /// True to ignore the asset focus point if any. - /// </summary> - [FromQuery(Name = "nofocus")] - public bool IgnoreFocus { get; set; } - - /// <summary> - /// True to use auto format. - /// </summary> - [FromQuery(Name = "auto")] - public bool Auto { get; set; } - - /// <summary> - /// True to force a new resize even if it already stored. - /// </summary> - [FromQuery(Name = "force")] - public bool Force { get; set; } - - /// <summary> - /// True to force a new resize even if it already stored. - /// </summary> - [FromQuery(Name = "format")] - public ImageFormat? Format { get; set; } - - public ResizeOptions ToResizeOptions(IAssetEntity asset, IAssetThumbnailGenerator assetThumbnailGenerator, HttpRequest request) - { - Guard.NotNull(asset); + Guard.NotNull(asset); - var result = SimpleMapper.Map(this, new ResizeOptions()); + var result = SimpleMapper.Map(this, new ResizeOptions()); - var (x, y) = GetFocusPoint(asset); + var (x, y) = GetFocusPoint(asset); - result.FocusX = x; - result.FocusY = y; - result.TargetWidth = Width; - result.TargetHeight = Height; - result.Format = GetFormat(asset, assetThumbnailGenerator, request); + result.FocusX = x; + result.FocusY = y; + result.TargetWidth = Width; + result.TargetHeight = Height; + result.Format = GetFormat(asset, assetThumbnailGenerator, request); - return result; + return result; + } + + private ImageFormat? GetFormat(IAssetEntity asset, IAssetThumbnailGenerator assetThumbnailGenerator, HttpRequest request) + { + if (Format.HasValue || !Auto) + { + return Format; } - private ImageFormat? GetFormat(IAssetEntity asset, IAssetThumbnailGenerator assetThumbnailGenerator, HttpRequest request) + bool Accepts(string mimeType) { - if (Format.HasValue || !Auto) + if (string.Equals(asset.MimeType, mimeType, StringComparison.OrdinalIgnoreCase)) { - return Format; + return false; } - bool Accepts(string mimeType) - { - if (string.Equals(asset.MimeType, mimeType, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + request.Headers.TryGetValue("Accept", out var accept); - request.Headers.TryGetValue("Accept", out var accept); - - return accept.Any(x => x.Contains(mimeType, StringComparison.OrdinalIgnoreCase)) && assetThumbnailGenerator.CanReadAndWrite(mimeType); - } + return accept.Any(x => x.Contains(mimeType, StringComparison.OrdinalIgnoreCase)) && assetThumbnailGenerator.CanReadAndWrite(mimeType); + } #if ENABLE_AVIF - if (Accepts("image/avif")) - { - return ImageFormat.AVIF; - } + if (Accepts("image/avif")) + { + return ImageFormat.AVIF; + } #endif - if (Accepts("image/webp")) - { - return ImageFormat.WEBP; - } - - return Format; + if (Accepts("image/webp")) + { + return ImageFormat.WEBP; } - private (float?, float?) GetFocusPoint(IAssetEntity asset) - { - if (IgnoreFocus) - { - return (null, null); - } + return Format; + } - if (FocusX != null && FocusY != null) - { - return (FocusX.Value, FocusY.Value); - } + private (float?, float?) GetFocusPoint(IAssetEntity asset) + { + if (IgnoreFocus) + { + return (null, null); + } - var focusX = asset.Metadata.GetFocusX(); - var focusY = asset.Metadata.GetFocusY(); + if (FocusX != null && FocusY != null) + { + return (FocusX.Value, FocusY.Value); + } - if (focusX != null && focusY != null) - { - return (focusX.Value, focusY.Value); - } + var focusX = asset.Metadata.GetFocusX(); + var focusY = asset.Metadata.GetFocusY(); - return (null, null); + if (focusX != null && focusY != null) + { + return (focusX.Value, focusY.Value); } + + return (null, null); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index 2da7a3a5fa..a2dfe2f6b4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -14,224 +14,223 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class AssetDto : Resource { - public sealed class AssetDto : Resource + /// <summary> + /// The ID of the asset. + /// </summary> + public DomainId Id { get; set; } + + /// <summary> + /// The ID of the parent folder. Empty for files without parent. + /// </summary> + public DomainId ParentId { get; set; } + + /// <summary> + /// The file name. + /// </summary> + [LocalizedRequired] + public string FileName { get; set; } + + /// <summary> + /// The file hash. + /// </summary> + public string? FileHash { get; set; } + + /// <summary> + /// True, when the asset is not public. + /// </summary> + public bool IsProtected { get; set; } + + /// <summary> + /// The slug. + /// </summary> + [LocalizedRequired] + public string Slug { get; set; } + + /// <summary> + /// The mime type. + /// </summary> + [LocalizedRequired] + public string MimeType { get; set; } + + /// <summary> + /// The file type. + /// </summary> + [LocalizedRequired] + public string FileType { get; set; } + + /// <summary> + /// The formatted text representation of the metadata. + /// </summary> + [LocalizedRequired] + public string MetadataText { get; set; } + + /// <summary> + /// The UI token. + /// </summary> + public string? EditToken { get; set; } + + /// <summary> + /// The asset metadata. + /// </summary> + [LocalizedRequired] + public AssetMetadata Metadata { get; set; } + + /// <summary> + /// The asset tags. + /// </summary> + [LocalizedRequired] + public HashSet<string>? Tags { get; set; } + + /// <summary> + /// The size of the file in bytes. + /// </summary> + public long FileSize { get; set; } + + /// <summary> + /// The version of the file. + /// </summary> + public long FileVersion { get; set; } + + /// <summary> + /// The type of the asset. + /// </summary> + public AssetType Type { get; set; } + + /// <summary> + /// The user that has created the schema. + /// </summary> + [LocalizedRequired] + public RefToken CreatedBy { get; set; } + + /// <summary> + /// The user that has updated the asset. + /// </summary> + [LocalizedRequired] + public RefToken LastModifiedBy { get; set; } + + /// <summary> + /// The date and time when the asset has been created. + /// </summary> + public Instant Created { get; set; } + + /// <summary> + /// The date and time when the asset has been modified last. + /// </summary> + public Instant LastModified { get; set; } + + /// <summary> + /// The version of the asset. + /// </summary> + public long Version { get; set; } + + /// <summary> + /// The metadata. + /// </summary> + [JsonPropertyName("_meta")] + public AssetMeta? Meta { get; set; } + + /// <summary> + /// Determines of the created file is an image. + /// </summary> + [Obsolete("Use 'type' field now.")] + public bool IsImage { - /// <summary> - /// The ID of the asset. - /// </summary> - public DomainId Id { get; set; } - - /// <summary> - /// The ID of the parent folder. Empty for files without parent. - /// </summary> - public DomainId ParentId { get; set; } - - /// <summary> - /// The file name. - /// </summary> - [LocalizedRequired] - public string FileName { get; set; } - - /// <summary> - /// The file hash. - /// </summary> - public string? FileHash { get; set; } - - /// <summary> - /// True, when the asset is not public. - /// </summary> - public bool IsProtected { get; set; } - - /// <summary> - /// The slug. - /// </summary> - [LocalizedRequired] - public string Slug { get; set; } - - /// <summary> - /// The mime type. - /// </summary> - [LocalizedRequired] - public string MimeType { get; set; } - - /// <summary> - /// The file type. - /// </summary> - [LocalizedRequired] - public string FileType { get; set; } - - /// <summary> - /// The formatted text representation of the metadata. - /// </summary> - [LocalizedRequired] - public string MetadataText { get; set; } - - /// <summary> - /// The UI token. - /// </summary> - public string? EditToken { get; set; } - - /// <summary> - /// The asset metadata. - /// </summary> - [LocalizedRequired] - public AssetMetadata Metadata { get; set; } - - /// <summary> - /// The asset tags. - /// </summary> - [LocalizedRequired] - public HashSet<string>? Tags { get; set; } - - /// <summary> - /// The size of the file in bytes. - /// </summary> - public long FileSize { get; set; } - - /// <summary> - /// The version of the file. - /// </summary> - public long FileVersion { get; set; } - - /// <summary> - /// The type of the asset. - /// </summary> - public AssetType Type { get; set; } - - /// <summary> - /// The user that has created the schema. - /// </summary> - [LocalizedRequired] - public RefToken CreatedBy { get; set; } - - /// <summary> - /// The user that has updated the asset. - /// </summary> - [LocalizedRequired] - public RefToken LastModifiedBy { get; set; } - - /// <summary> - /// The date and time when the asset has been created. - /// </summary> - public Instant Created { get; set; } - - /// <summary> - /// The date and time when the asset has been modified last. - /// </summary> - public Instant LastModified { get; set; } - - /// <summary> - /// The version of the asset. - /// </summary> - public long Version { get; set; } - - /// <summary> - /// The metadata. - /// </summary> - [JsonPropertyName("_meta")] - public AssetMeta? Meta { get; set; } - - /// <summary> - /// Determines of the created file is an image. - /// </summary> - [Obsolete("Use 'type' field now.")] - public bool IsImage - { - get => Type == AssetType.Image; - } + get => Type == AssetType.Image; + } - /// <summary> - /// The width of the image in pixels if the asset is an image. - /// </summary> - [Obsolete("Use 'metadata' field now.")] - public int? PixelWidth - { - get => Metadata.GetPixelWidth(); - } + /// <summary> + /// The width of the image in pixels if the asset is an image. + /// </summary> + [Obsolete("Use 'metadata' field now.")] + public int? PixelWidth + { + get => Metadata.GetPixelWidth(); + } - /// <summary> - /// The height of the image in pixels if the asset is an image. - /// </summary> - [Obsolete("Use 'metadata' field now.")] - public int? PixelHeight - { - get => Metadata.GetPixelHeight(); - } + /// <summary> + /// The height of the image in pixels if the asset is an image. + /// </summary> + [Obsolete("Use 'metadata' field now.")] + public int? PixelHeight + { + get => Metadata.GetPixelHeight(); + } - public static AssetDto FromDomain(IEnrichedAssetEntity asset, Resources resources, bool isDuplicate = false) - { - var result = SimpleMapper.Map(asset, new AssetDto()); + public static AssetDto FromDomain(IEnrichedAssetEntity asset, Resources resources, bool isDuplicate = false) + { + var result = SimpleMapper.Map(asset, new AssetDto()); - result.Tags = asset.TagNames; + result.Tags = asset.TagNames; - if (isDuplicate) + if (isDuplicate) + { + result.Meta = new AssetMeta { - result.Meta = new AssetMeta - { - IsDuplicate = "true" - }; - } - - result.FileType = asset.FileName.FileType(); - - return result.CreateLinks(resources); + IsDuplicate = "true" + }; } - private AssetDto CreateLinks(Resources resources) - { - var app = resources.App; + result.FileType = asset.FileName.FileType(); - var values = new { app, id = Id }; + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<AssetsController>(x => nameof(x.GetAsset), values)); + private AssetDto CreateLinks(Resources resources) + { + var app = resources.App; - if (resources.CanUpdateAsset) - { - AddPutLink("update", - resources.Url<AssetsController>(x => nameof(x.PutAsset), values)); - } + var values = new { app, id = Id }; - if (resources.CanUpdateAsset) - { - AddPutLink("move", - resources.Url<AssetsController>(x => nameof(x.PutAssetParent), values)); - } + AddSelfLink(resources.Url<AssetsController>(x => nameof(x.GetAsset), values)); - if (resources.CanUploadAsset) - { - AddPutLink("upload", - resources.Url<AssetsController>(x => nameof(x.PutAssetContent), values)); - } + if (resources.CanUpdateAsset) + { + AddPutLink("update", + resources.Url<AssetsController>(x => nameof(x.PutAsset), values)); + } - if (resources.CanDeleteAsset) - { - AddDeleteLink("delete", - resources.Url<AssetsController>(x => nameof(x.DeleteAsset), values)); - } + if (resources.CanUpdateAsset) + { + AddPutLink("move", + resources.Url<AssetsController>(x => nameof(x.PutAssetParent), values)); + } - if (!string.IsNullOrWhiteSpace(Slug)) - { - var idValues = new { app, idOrSlug = Id, more = Slug }; + if (resources.CanUploadAsset) + { + AddPutLink("upload", + resources.Url<AssetsController>(x => nameof(x.PutAssetContent), values)); + } - AddGetLink("content", - resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), idValues)); + if (resources.CanDeleteAsset) + { + AddDeleteLink("delete", + resources.Url<AssetsController>(x => nameof(x.DeleteAsset), values)); + } - var slugValues = new { app, idOrSlug = Slug }; + if (!string.IsNullOrWhiteSpace(Slug)) + { + var idValues = new { app, idOrSlug = Id, more = Slug }; - AddGetLink("content/slug", - resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), slugValues)); - } - else - { - var idValues = new { app, idOrSlug = Id }; + AddGetLink("content", + resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), idValues)); - AddGetLink("content", - resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), idValues)); - } + var slugValues = new { app, idOrSlug = Slug }; - return this; + AddGetLink("content/slug", + resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), slugValues)); } + else + { + var idValues = new { app, idOrSlug = Id }; + + AddGetLink("content", + resources.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), idValues)); + } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs index da5b02f955..0340c0cfb0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs @@ -11,58 +11,57 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Assets.Models -{ - public sealed class AssetFolderDto : Resource - { - /// <summary> - /// The ID of the asset. - /// </summary> - public DomainId Id { get; set; } +namespace Squidex.Areas.Api.Controllers.Assets.Models; - /// <summary> - /// The ID of the parent folder. Empty for files without parent. - /// </summary> - public DomainId ParentId { get; set; } +public sealed class AssetFolderDto : Resource +{ + /// <summary> + /// The ID of the asset. + /// </summary> + public DomainId Id { get; set; } - /// <summary> - /// The folder name. - /// </summary> - [LocalizedRequired] - public string FolderName { get; set; } + /// <summary> + /// The ID of the parent folder. Empty for files without parent. + /// </summary> + public DomainId ParentId { get; set; } - /// <summary> - /// The version of the asset folder. - /// </summary> - public long Version { get; set; } + /// <summary> + /// The folder name. + /// </summary> + [LocalizedRequired] + public string FolderName { get; set; } - public static AssetFolderDto FromDomain(IAssetFolderEntity asset, Resources resources) - { - var result = SimpleMapper.Map(asset, new AssetFolderDto()); + /// <summary> + /// The version of the asset folder. + /// </summary> + public long Version { get; set; } - return result.CreateLinks(resources); - } + public static AssetFolderDto FromDomain(IAssetFolderEntity asset, Resources resources) + { + var result = SimpleMapper.Map(asset, new AssetFolderDto()); - private AssetFolderDto CreateLinks(Resources resources) - { - var values = new { app = resources.App, id = Id }; + return result.CreateLinks(resources); + } - if (resources.CanUpdateAsset) - { - AddPutLink("update", - resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolder), values)); + private AssetFolderDto CreateLinks(Resources resources) + { + var values = new { app = resources.App, id = Id }; - AddPutLink("move", - resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolderParent), values)); - } + if (resources.CanUpdateAsset) + { + AddPutLink("update", + resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolder), values)); - if (resources.CanUpdateAsset) - { - AddDeleteLink("delete", - resources.Url<AssetFoldersController>(x => nameof(x.DeleteAssetFolder), values)); - } + AddPutLink("move", + resources.Url<AssetFoldersController>(x => nameof(x.PutAssetFolderParent), values)); + } - return this; + if (resources.CanUpdateAsset) + { + AddDeleteLink("delete", + resources.Url<AssetFoldersController>(x => nameof(x.DeleteAssetFolder), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderScope.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderScope.cs index a99f152b3e..32cdb176db 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderScope.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderScope.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public enum AssetFolderScope { - public enum AssetFolderScope - { - PathAndItems, - Path, - Items - } + PathAndItems, + Path, + Items } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs index 9539a26398..5233cc10a1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs @@ -10,53 +10,52 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class AssetFoldersDto : Resource { - public sealed class AssetFoldersDto : Resource + /// <summary> + /// The total number of assets. + /// </summary> + public long Total { get; set; } + + /// <summary> + /// The assets folders. + /// </summary> + [LocalizedRequired] + public AssetFolderDto[] Items { get; set; } + + /// <summary> + /// The path to the current folder. + /// </summary> + [LocalizedRequired] + public AssetFolderDto[] Path { get; set; } + + public static AssetFoldersDto FromDomain(IResultList<IAssetFolderEntity> assetFolders, IEnumerable<IAssetFolderEntity> path, Resources resources) { - /// <summary> - /// The total number of assets. - /// </summary> - public long Total { get; set; } - - /// <summary> - /// The assets folders. - /// </summary> - [LocalizedRequired] - public AssetFolderDto[] Items { get; set; } - - /// <summary> - /// The path to the current folder. - /// </summary> - [LocalizedRequired] - public AssetFolderDto[] Path { get; set; } - - public static AssetFoldersDto FromDomain(IResultList<IAssetFolderEntity> assetFolders, IEnumerable<IAssetFolderEntity> path, Resources resources) + var result = new AssetFoldersDto { - var result = new AssetFoldersDto - { - Total = assetFolders.Total, - Items = assetFolders.Select(x => AssetFolderDto.FromDomain(x, resources)).ToArray() - }; - - result.Path = path.Select(x => AssetFolderDto.FromDomain(x, resources)).ToArray(); + Total = assetFolders.Total, + Items = assetFolders.Select(x => AssetFolderDto.FromDomain(x, resources)).ToArray() + }; - return result.CreateLinks(resources); - } + result.Path = path.Select(x => AssetFolderDto.FromDomain(x, resources)).ToArray(); - private AssetFoldersDto CreateLinks(Resources resources) - { - var values = new { app = resources.App }; + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<AssetFoldersController>(x => nameof(x.GetAssetFolders), values)); + private AssetFoldersDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanUpdateAsset) - { - AddPostLink("create", - resources.Url<AssetFoldersController>(x => nameof(x.PostAssetFolder), values)); - } + AddSelfLink(resources.Url<AssetFoldersController>(x => nameof(x.GetAssetFolders), values)); - return this; + if (resources.CanUpdateAsset) + { + AddPostLink("create", + resources.Url<AssetFoldersController>(x => nameof(x.PostAssetFolder), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMeta.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMeta.cs index a29693a096..ce4eabb8fe 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMeta.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMeta.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class AssetMeta { - public sealed class AssetMeta - { - /// <summary> - /// Indicates whether the asset is a duplicate. - /// </summary> - public string IsDuplicate { get; set; } - } + /// <summary> + /// Indicates whether the asset is a duplicate. + /// </summary> + public string IsDuplicate { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs index 2a605fa1b3..4efa4b699c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs @@ -10,56 +10,55 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class AssetsDto : Resource { - public sealed class AssetsDto : Resource - { - /// <summary> - /// The total number of assets. - /// </summary> - public long Total { get; set; } + /// <summary> + /// The total number of assets. + /// </summary> + public long Total { get; set; } - /// <summary> - /// The assets. - /// </summary> - [LocalizedRequired] - public AssetDto[] Items { get; set; } + /// <summary> + /// The assets. + /// </summary> + [LocalizedRequired] + public AssetDto[] Items { get; set; } - public static AssetsDto FromDomain(IResultList<IEnrichedAssetEntity> assets, Resources resources) + public static AssetsDto FromDomain(IResultList<IEnrichedAssetEntity> assets, Resources resources) + { + var result = new AssetsDto { - var result = new AssetsDto - { - Total = assets.Total, - Items = assets.Select(x => AssetDto.FromDomain(x, resources)).ToArray() - }; + Total = assets.Total, + Items = assets.Select(x => AssetDto.FromDomain(x, resources)).ToArray() + }; - return result.CreateLinks(resources); - } + return result.CreateLinks(resources); + } - private AssetsDto CreateLinks(Resources resources) - { - var values = new { app = resources.App }; + private AssetsDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - AddSelfLink(resources.Url<AssetsController>(x => nameof(x.GetAssets), values)); + AddSelfLink(resources.Url<AssetsController>(x => nameof(x.GetAssets), values)); - if (resources.CanCreateAsset) - { - AddPostLink("create", - resources.Url<AssetsController>(x => nameof(x.PostAsset), values)); - } + if (resources.CanCreateAsset) + { + AddPostLink("create", + resources.Url<AssetsController>(x => nameof(x.PostAsset), values)); + } - if (resources.CanUpdateAsset) - { - var tagValue = new { values.app, name = "tag" }; + if (resources.CanUpdateAsset) + { + var tagValue = new { values.app, name = "tag" }; - AddPutLink("tags/rename", - resources.Url<AssetsController>(x => nameof(x.PutTag), tagValue)); - } + AddPutLink("tags/rename", + resources.Url<AssetsController>(x => nameof(x.PutTag), tagValue)); + } - AddGetLink("tags", - resources.Url<AssetsController>(x => nameof(x.GetTags), values)); + AddGetLink("tags", + resources.Url<AssetsController>(x => nameof(x.GetTags), values)); - return this; - } + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs index dd9fbc108f..51afdbb16a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs @@ -9,38 +9,37 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class BulkUpdateAssetsDto { - public sealed class BulkUpdateAssetsDto + /// <summary> + /// The contents to update or insert. + /// </summary> + [LocalizedRequired] + public BulkUpdateAssetsJobDto[]? Jobs { get; set; } + + /// <summary> + /// True to check referrers of deleted assets. + /// </summary> + public bool CheckReferrers { get; set; } + + /// <summary> + /// True to turn off costly validation: Folder checks. Default: true. + /// </summary> + public bool OptimizeValidation { get; set; } = true; + + /// <summary> + /// True to turn off scripting for faster inserts. Default: true. + /// </summary> + public bool DoNotScript { get; set; } = true; + + public BulkUpdateAssets ToCommand() { - /// <summary> - /// The contents to update or insert. - /// </summary> - [LocalizedRequired] - public BulkUpdateAssetsJobDto[]? Jobs { get; set; } - - /// <summary> - /// True to check referrers of deleted assets. - /// </summary> - public bool CheckReferrers { get; set; } - - /// <summary> - /// True to turn off costly validation: Folder checks. Default: true. - /// </summary> - public bool OptimizeValidation { get; set; } = true; - - /// <summary> - /// True to turn off scripting for faster inserts. Default: true. - /// </summary> - public bool DoNotScript { get; set; } = true; - - public BulkUpdateAssets ToCommand() - { - var result = SimpleMapper.Map(this, new BulkUpdateAssets()); - - result.Jobs = Jobs?.Select(x => x.ToJob())?.ToArray(); - - return result; - } + var result = SimpleMapper.Map(this, new BulkUpdateAssets()); + + result.Jobs = Jobs?.Select(x => x.ToJob())?.ToArray(); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs index b1ead93c19..51e4c4a7f9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsJobDto.cs @@ -10,63 +10,62 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public class BulkUpdateAssetsJobDto { - public class BulkUpdateAssetsJobDto - { - /// <summary> - /// An optional ID of the asset to update. - /// </summary> - public DomainId Id { get; set; } + /// <summary> + /// An optional ID of the asset to update. + /// </summary> + public DomainId Id { get; set; } - /// <summary> - /// The update type. - /// </summary> - public BulkUpdateAssetType Type { get; set; } + /// <summary> + /// The update type. + /// </summary> + public BulkUpdateAssetType Type { get; set; } - /// <summary> - /// The parent folder id. - /// </summary> - public DomainId ParentId { get; set; } + /// <summary> + /// The parent folder id. + /// </summary> + public DomainId ParentId { get; set; } - /// <summary> - /// The new name of the asset. - /// </summary> - public string? FileName { get; set; } + /// <summary> + /// The new name of the asset. + /// </summary> + public string? FileName { get; set; } - /// <summary> - /// The new slug of the asset. - /// </summary> - public string? Slug { get; set; } + /// <summary> + /// The new slug of the asset. + /// </summary> + public string? Slug { get; set; } - /// <summary> - /// True, when the asset is not public. - /// </summary> - public bool? IsProtected { get; set; } + /// <summary> + /// True, when the asset is not public. + /// </summary> + public bool? IsProtected { get; set; } - /// <summary> - /// The new asset tags. - /// </summary> - public HashSet<string>? Tags { get; set; } + /// <summary> + /// The new asset tags. + /// </summary> + public HashSet<string>? Tags { get; set; } - /// <summary> - /// The asset metadata. - /// </summary> - public AssetMetadata? Metadata { get; set; } + /// <summary> + /// The asset metadata. + /// </summary> + public AssetMetadata? Metadata { get; set; } - /// <summary> - /// True to delete the asset permanently. - /// </summary> - public bool Permanent { get; set; } + /// <summary> + /// True to delete the asset permanently. + /// </summary> + public bool Permanent { get; set; } - /// <summary> - /// The expected version. - /// </summary> - public long ExpectedVersion { get; set; } = EtagVersion.Any; + /// <summary> + /// The expected version. + /// </summary> + public long ExpectedVersion { get; set; } = EtagVersion.Any; - public BulkUpdateJob ToJob() - { - return SimpleMapper.Map(this, new BulkUpdateJob()); - } + public BulkUpdateJob ToJob() + { + return SimpleMapper.Map(this, new BulkUpdateJob()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs index 5bd1993804..51ff124caf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs @@ -11,43 +11,42 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class CreateAssetDto { - public sealed class CreateAssetDto + /// <summary> + /// The file to upload. + /// </summary> + public IFormFile File { get; set; } + + /// <summary> + /// The optional parent folder id. + /// </summary> + [FromQuery] + public DomainId ParentId { get; set; } + + /// <summary> + /// The optional custom asset id. + /// </summary> + [FromQuery] + public DomainId? Id { get; set; } + + /// <summary> + /// True to duplicate the asset, event if the file has been uploaded. + /// </summary> + [FromQuery] + public bool Duplicate { get; set; } + + public CreateAsset ToCommand(AssetFile file) { - /// <summary> - /// The file to upload. - /// </summary> - public IFormFile File { get; set; } - - /// <summary> - /// The optional parent folder id. - /// </summary> - [FromQuery] - public DomainId ParentId { get; set; } - - /// <summary> - /// The optional custom asset id. - /// </summary> - [FromQuery] - public DomainId? Id { get; set; } - - /// <summary> - /// True to duplicate the asset, event if the file has been uploaded. - /// </summary> - [FromQuery] - public bool Duplicate { get; set; } - - public CreateAsset ToCommand(AssetFile file) - { - var command = SimpleMapper.Map(this, new CreateAsset { File = file }); - - if (Id != null && Id.Value != default && !string.IsNullOrWhiteSpace(Id.Value.ToString())) - { - command.AssetId = Id.Value; - } + var command = SimpleMapper.Map(this, new CreateAsset { File = file }); - return command; + if (Id != null && Id.Value != default && !string.IsNullOrWhiteSpace(Id.Value.ToString())) + { + command.AssetId = Id.Value; } + + return command; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs index f1f18987aa..8fb230ff2f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs @@ -10,24 +10,23 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class CreateAssetFolderDto { - public sealed class CreateAssetFolderDto - { - /// <summary> - /// The name of the folder. - /// </summary> - [LocalizedRequired] - public string FolderName { get; set; } + /// <summary> + /// The name of the folder. + /// </summary> + [LocalizedRequired] + public string FolderName { get; set; } - /// <summary> - /// The ID of the parent folder. - /// </summary> - public DomainId ParentId { get; set; } + /// <summary> + /// The ID of the parent folder. + /// </summary> + public DomainId ParentId { get; set; } - public CreateAssetFolder ToCommand() - { - return SimpleMapper.Map(this, new CreateAssetFolder()); - } + public CreateAssetFolder ToCommand() + { + return SimpleMapper.Map(this, new CreateAssetFolder()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/DeleteAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/DeleteAssetDto.cs index 5c2b068e65..dde0198970 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/DeleteAssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/DeleteAssetDto.cs @@ -10,25 +10,24 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class DeleteAssetDto { - public sealed class DeleteAssetDto - { - /// <summary> - /// True to check referrers of this asset. - /// </summary> - [FromQuery] - public bool CheckReferrers { get; set; } + /// <summary> + /// True to check referrers of this asset. + /// </summary> + [FromQuery] + public bool CheckReferrers { get; set; } - /// <summary> - /// True to delete the asset permanently. - /// </summary> - [FromQuery] - public bool Permanent { get; set; } + /// <summary> + /// True to delete the asset permanently. + /// </summary> + [FromQuery] + public bool Permanent { get; set; } - public DeleteAsset ToCommand(DomainId id) - { - return SimpleMapper.Map(this, new DeleteAsset { AssetId = id }); - } + public DeleteAsset ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new DeleteAsset { AssetId = id }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetDto.cs index 50e9dc946c..26d58844af 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetDto.cs @@ -9,18 +9,17 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class MoveAssetDto { - public sealed class MoveAssetDto - { - /// <summary> - /// The parent folder id. - /// </summary> - public DomainId ParentId { get; set; } + /// <summary> + /// The parent folder id. + /// </summary> + public DomainId ParentId { get; set; } - public MoveAsset ToCommand(DomainId id) - { - return SimpleMapper.Map(this, new MoveAsset { AssetId = id }); - } + public MoveAsset ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new MoveAsset { AssetId = id }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetFolderDto.cs index 75782d5539..396ca0d0f0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetFolderDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetFolderDto.cs @@ -9,18 +9,17 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class MoveAssetFolderDto { - public sealed class MoveAssetFolderDto - { - /// <summary> - /// The parent folder id. - /// </summary> - public DomainId ParentId { get; set; } + /// <summary> + /// The parent folder id. + /// </summary> + public DomainId ParentId { get; set; } - public MoveAssetFolder ToCommand(DomainId id) - { - return SimpleMapper.Map(this, new MoveAssetFolder { AssetFolderId = id }); - } + public MoveAssetFolder ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new MoveAssetFolder { AssetFolderId = id }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs index 24e0076850..a5f2853c52 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs @@ -10,19 +10,18 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models; + +public sealed class RenameAssetFolderDto { - public sealed class RenameAssetFolderDto - { - /// <summary> - /// The name of the folder. - /// </summary> - [LocalizedRequired] - public string FolderName { get; set; } + /// <summary> + /// The name of the folder. + /// </summary> + [LocalizedRequired] + public string FolderName { get; set; } - public RenameAssetFolder ToCommand(DomainId id) - { - return SimpleMapper.Map(this, new RenameAssetFolder { AssetFolderId = id }); - } + public RenameAssetFolder ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new RenameAssetFolder { AssetFolderId = id }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/UpsertAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/UpsertAssetDto.cs index 5f28e4510e..48fe38b90a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/UpsertAssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/UpsertAssetDto.cs @@ -11,67 +11,66 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Assets.Models -{ - public sealed class UpsertAssetDto - { - /// <summary> - /// The file to upload. - /// </summary> - public IFormFile File { get; set; } - - /// <summary> - /// The optional parent folder id. - /// </summary> - [FromQuery] - public DomainId ParentId { get; set; } +namespace Squidex.Areas.Api.Controllers.Assets.Models; - /// <summary> - /// True to duplicate the asset, event if the file has been uploaded. - /// </summary> - [FromQuery] - public bool Duplicate { get; set; } +public sealed class UpsertAssetDto +{ + /// <summary> + /// The file to upload. + /// </summary> + public IFormFile File { get; set; } - public static UpsertAsset ToCommand(AssetTusFile file) - { - var command = new UpsertAsset { File = file }; + /// <summary> + /// The optional parent folder id. + /// </summary> + [FromQuery] + public DomainId ParentId { get; set; } - bool TryGetString(string key, out string result) - { - result = null!; + /// <summary> + /// True to duplicate the asset, event if the file has been uploaded. + /// </summary> + [FromQuery] + public bool Duplicate { get; set; } - var value = file.Metadata.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)).Value; + public static UpsertAsset ToCommand(AssetTusFile file) + { + var command = new UpsertAsset { File = file }; - if (!string.IsNullOrWhiteSpace(value)) - { - result = value; - return true; - } + bool TryGetString(string key, out string result) + { + result = null!; - return false; - } + var value = file.Metadata.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)).Value; - if (TryGetString("id", out var id)) + if (!string.IsNullOrWhiteSpace(value)) { - command.AssetId = DomainId.Create(id); + result = value; + return true; } - if (TryGetString("parentId", out var parentId)) - { - command.ParentId = DomainId.Create(parentId); - } + return false; + } - if (TryGetString("duplicate", out var duplicate) && bool.TryParse(duplicate, out var parsed)) - { - command.Duplicate = parsed; - } + if (TryGetString("id", out var id)) + { + command.AssetId = DomainId.Create(id); + } - return command; + if (TryGetString("parentId", out var parentId)) + { + command.ParentId = DomainId.Create(parentId); } - public UpsertAsset ToCommand(DomainId id, AssetFile file) + if (TryGetString("duplicate", out var duplicate) && bool.TryParse(duplicate, out var parsed)) { - return SimpleMapper.Map(this, new UpsertAsset { File = file, AssetId = id }); + command.Duplicate = parsed; } + + return command; + } + + public UpsertAsset ToCommand(DomainId id, AssetFile file) + { + return SimpleMapper.Map(this, new UpsertAsset { File = file, AssetId = id }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/AssignContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/AssignContributorDto.cs index 88cbaf143e..8132e98782 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/AssignContributorDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/AssignContributorDto.cs @@ -8,24 +8,23 @@ using Squidex.Infrastructure.Validation; using Roles = Squidex.Domain.Apps.Core.Apps.Role; -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class AssignContributorDto { - public sealed class AssignContributorDto - { - /// <summary> - /// The id or email of the user to add to the app. - /// </summary> - [LocalizedRequired] - public string ContributorId { get; set; } + /// <summary> + /// The id or email of the user to add to the app. + /// </summary> + [LocalizedRequired] + public string ContributorId { get; set; } - /// <summary> - /// The role of the contributor. - /// </summary> - public string? Role { get; set; } = Roles.Developer; + /// <summary> + /// The role of the contributor. + /// </summary> + public string? Role { get; set; } = Roles.Developer; - /// <summary> - /// Set to true to invite the user if he does not exist. - /// </summary> - public bool Invite { get; set; } - } + /// <summary> + /// Set to true to invite the user if he does not exist. + /// </summary> + public bool Invite { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index 2e66fbf733..066e1c42f0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs @@ -12,89 +12,88 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Backups +namespace Squidex.Areas.Api.Controllers.Backups; + +/// <summary> +/// Update and query backups for app. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Backups))] +public class BackupContentController : ApiController { + private readonly IBackupArchiveStore backupArchiveStore; + private readonly IBackupService backupservice; + + public BackupContentController(ICommandBus commandBus, + IBackupArchiveStore backupArchiveStore, + IBackupService backupservice) + : base(commandBus) + { + this.backupArchiveStore = backupArchiveStore; + this.backupservice = backupservice; + } + /// <summary> - /// Update and query backups for app. + /// Get the backup content. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Backups))] - public class BackupContentController : ApiController + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the backup.</param> + /// <returns> + /// 200 => Backup found and content returned. + /// 404 => Backup or app not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/backups/{id}")] + [ResponseCache(Duration = 3600 * 24 * 30)] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [ApiCosts(0)] + [AllowAnonymous] + public Task<IActionResult> GetBackupContent(string app, DomainId id) { - private readonly IBackupArchiveStore backupArchiveStore; - private readonly IBackupService backupservice; + return GetBackupAsync(AppId, app, id); + } - public BackupContentController(ICommandBus commandBus, - IBackupArchiveStore backupArchiveStore, - IBackupService backupservice) - : base(commandBus) - { - this.backupArchiveStore = backupArchiveStore; - this.backupservice = backupservice; - } + /// <summary> + /// Get the backup content. + /// </summary> + /// <param name="id">The ID of the backup.</param> + /// <param name="appId">The ID of the app.</param> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Backup found and content returned. + /// 404 => Backup or app not found. + /// </returns> + [HttpGet] + [Route("apps/backups/{id}")] + [ResponseCache(Duration = 3600 * 24 * 30)] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [ApiCosts(0)] + [AllowAnonymous] + public Task<IActionResult> GetBackupContentV2(DomainId id, [FromQuery] DomainId appId = default, [FromQuery] string app = "") + { + return GetBackupAsync(appId, app, id); + } - /// <summary> - /// Get the backup content. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the backup.</param> - /// <returns> - /// 200 => Backup found and content returned. - /// 404 => Backup or app not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/backups/{id}")] - [ResponseCache(Duration = 3600 * 24 * 30)] - [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] - [ApiCosts(0)] - [AllowAnonymous] - public Task<IActionResult> GetBackupContent(string app, DomainId id) - { - return GetBackupAsync(AppId, app, id); - } + private async Task<IActionResult> GetBackupAsync(DomainId appId, string app, DomainId id) + { + var backup = await backupservice.GetBackupAsync(appId, id, HttpContext.RequestAborted); - /// <summary> - /// Get the backup content. - /// </summary> - /// <param name="id">The ID of the backup.</param> - /// <param name="appId">The ID of the app.</param> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Backup found and content returned. - /// 404 => Backup or app not found. - /// </returns> - [HttpGet] - [Route("apps/backups/{id}")] - [ResponseCache(Duration = 3600 * 24 * 30)] - [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] - [ApiCosts(0)] - [AllowAnonymous] - public Task<IActionResult> GetBackupContentV2(DomainId id, [FromQuery] DomainId appId = default, [FromQuery] string app = "") + if (backup is not { Status: JobStatus.Completed }) { - return GetBackupAsync(appId, app, id); + return NotFound(); } - private async Task<IActionResult> GetBackupAsync(DomainId appId, string app, DomainId id) - { - var backup = await backupservice.GetBackupAsync(appId, id, HttpContext.RequestAborted); - - if (backup is not { Status: JobStatus.Completed }) - { - return NotFound(); - } - - var fileName = $"backup-{app}-{backup.Started:yyyy-MM-dd_HH-mm-ss}.zip"; + var fileName = $"backup-{app}-{backup.Started:yyyy-MM-dd_HH-mm-ss}.zip"; - var callback = new FileCallback((body, range, ct) => - { - return backupArchiveStore.DownloadAsync(id, body, ct); - }); + var callback = new FileCallback((body, range, ct) => + { + return backupArchiveStore.DownloadAsync(id, body, ct); + }); - return new FileCallbackResult("application/zip", callback) - { - FileDownloadName = fileName, - FileSize = null, - ErrorAs404 = true - }; - } + return new FileCallbackResult("application/zip", callback) + { + FileDownloadName = fileName, + FileSize = null, + ErrorAs404 = true + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs index 547cbe1129..2df6b84477 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs @@ -14,84 +14,83 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Backups +namespace Squidex.Areas.Api.Controllers.Backups; + +/// <summary> +/// Update and query backups for apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Backups))] +public class BackupsController : ApiController { + private readonly IBackupService backupService; + + public BackupsController(ICommandBus commandBus, IBackupService backupService) + : base(commandBus) + { + this.backupService = backupService; + } + /// <summary> - /// Update and query backups for apps. + /// Get all backup jobs. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Backups))] - public class BackupsController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Backups returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/backups/")] + [ProducesResponseType(typeof(BackupJobsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppBackupsRead)] + [ApiCosts(0)] + public async Task<IActionResult> GetBackups(string app) { - private readonly IBackupService backupService; - - public BackupsController(ICommandBus commandBus, IBackupService backupService) - : base(commandBus) - { - this.backupService = backupService; - } + var jobs = await backupService.GetBackupsAsync(AppId, HttpContext.RequestAborted); - /// <summary> - /// Get all backup jobs. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Backups returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/backups/")] - [ProducesResponseType(typeof(BackupJobsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppBackupsRead)] - [ApiCosts(0)] - public async Task<IActionResult> GetBackups(string app) - { - var jobs = await backupService.GetBackupsAsync(AppId, HttpContext.RequestAborted); + var response = BackupJobsDto.FromDomain(jobs, Resources); - var response = BackupJobsDto.FromDomain(jobs, Resources); - - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Start a new backup. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 204 => Backup started. - /// 400 => Backup contingent reached. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/backups/")] - [ProducesResponseType(typeof(BackupJobDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppBackupsCreate)] - [ApiCosts(0)] - public async Task<IActionResult> PostBackup(string app) - { - await backupService.StartBackupAsync(App.Id, User.Token()!, HttpContext.RequestAborted); + /// <summary> + /// Start a new backup. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 204 => Backup started. + /// 400 => Backup contingent reached. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/backups/")] + [ProducesResponseType(typeof(BackupJobDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppBackupsCreate)] + [ApiCosts(0)] + public async Task<IActionResult> PostBackup(string app) + { + await backupService.StartBackupAsync(App.Id, User.Token()!, HttpContext.RequestAborted); - return NoContent(); - } + return NoContent(); + } - /// <summary> - /// Delete a backup. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the backup to delete.</param> - /// <returns> - /// 204 => Backup deleted. - /// 404 => Backup or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/backups/{id}")] - [ProducesResponseType(typeof(BackupJobDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppBackupsDelete)] - [ApiCosts(0)] - public async Task<IActionResult> DeleteBackup(string app, DomainId id) - { - await backupService.DeleteBackupAsync(AppId, id, HttpContext.RequestAborted); + /// <summary> + /// Delete a backup. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the backup to delete.</param> + /// <returns> + /// 204 => Backup deleted. + /// 404 => Backup or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/backups/{id}")] + [ProducesResponseType(typeof(BackupJobDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppBackupsDelete)] + [ApiCosts(0)] + public async Task<IActionResult> DeleteBackup(string app, DomainId id) + { + await backupService.DeleteBackupAsync(AppId, id, HttpContext.RequestAborted); - return NoContent(); - } + return NoContent(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs index 323aecb35d..fbf41decf5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs @@ -11,66 +11,65 @@ using Squidex.Infrastructure.Reflection; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Backups.Models +namespace Squidex.Areas.Api.Controllers.Backups.Models; + +public sealed class BackupJobDto : Resource { - public sealed class BackupJobDto : Resource - { - /// <summary> - /// The ID of the backup job. - /// </summary> - public DomainId Id { get; set; } + /// <summary> + /// The ID of the backup job. + /// </summary> + public DomainId Id { get; set; } - /// <summary> - /// The time when the job has been started. - /// </summary> - public Instant Started { get; set; } + /// <summary> + /// The time when the job has been started. + /// </summary> + public Instant Started { get; set; } - /// <summary> - /// The time when the job has been stopped. - /// </summary> - public Instant? Stopped { get; set; } + /// <summary> + /// The time when the job has been stopped. + /// </summary> + public Instant? Stopped { get; set; } - /// <summary> - /// The number of handled events. - /// </summary> - public int HandledEvents { get; set; } + /// <summary> + /// The number of handled events. + /// </summary> + public int HandledEvents { get; set; } - /// <summary> - /// The number of handled assets. - /// </summary> - public int HandledAssets { get; set; } + /// <summary> + /// The number of handled assets. + /// </summary> + public int HandledAssets { get; set; } - /// <summary> - /// The status of the operation. - /// </summary> - public JobStatus Status { get; set; } + /// <summary> + /// The status of the operation. + /// </summary> + public JobStatus Status { get; set; } - public static BackupJobDto FromDomain(IBackupJob backup, Resources resources) - { - var result = SimpleMapper.Map(backup, new BackupJobDto()); + public static BackupJobDto FromDomain(IBackupJob backup, Resources resources) + { + var result = SimpleMapper.Map(backup, new BackupJobDto()); - return result.CreateLinks(resources); - } + return result.CreateLinks(resources); + } - private BackupJobDto CreateLinks(Resources resources) + private BackupJobDto CreateLinks(Resources resources) + { + if (resources.CanDeleteBackup) { - if (resources.CanDeleteBackup) - { - var values = new { app = resources.App, id = Id }; - - AddDeleteLink("delete", - resources.Url<BackupsController>(x => nameof(x.DeleteBackup), values)); - } + var values = new { app = resources.App, id = Id }; - if (resources.CanDownloadBackup) - { - var values = new { app = resources.App, appId = resources.AppId, id = Id }; + AddDeleteLink("delete", + resources.Url<BackupsController>(x => nameof(x.DeleteBackup), values)); + } - AddGetLink("download", - resources.Url<BackupContentController>(x => nameof(x.GetBackupContentV2), values)); - } + if (resources.CanDownloadBackup) + { + var values = new { app = resources.App, appId = resources.AppId, id = Id }; - return this; + AddGetLink("download", + resources.Url<BackupContentController>(x => nameof(x.GetBackupContentV2), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs index 917155cce9..f354cf8efd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs @@ -9,39 +9,38 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Backups.Models +namespace Squidex.Areas.Api.Controllers.Backups.Models; + +public sealed class BackupJobsDto : Resource { - public sealed class BackupJobsDto : Resource - { - /// <summary> - /// The backups. - /// </summary> - [LocalizedRequired] - public BackupJobDto[] Items { get; set; } + /// <summary> + /// The backups. + /// </summary> + [LocalizedRequired] + public BackupJobDto[] Items { get; set; } - public static BackupJobsDto FromDomain(IEnumerable<IBackupJob> backups, Resources resources) + public static BackupJobsDto FromDomain(IEnumerable<IBackupJob> backups, Resources resources) + { + var result = new BackupJobsDto { - var result = new BackupJobsDto - { - Items = backups.Select(x => BackupJobDto.FromDomain(x, resources)).ToArray() - }; - - return result.CreateLinks(resources); - } + Items = backups.Select(x => BackupJobDto.FromDomain(x, resources)).ToArray() + }; - private BackupJobsDto CreateLinks(Resources resources) - { - var values = new { app = resources.App }; + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<BackupsController>(x => nameof(x.GetBackups), values)); + private BackupJobsDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanCreateBackup) - { - AddPostLink("create", - resources.Url<BackupsController>(x => nameof(x.PostBackup), values)); - } + AddSelfLink(resources.Url<BackupsController>(x => nameof(x.GetBackups), values)); - return this; + if (resources.CanCreateBackup) + { + AddPostLink("create", + resources.Url<BackupsController>(x => nameof(x.PostBackup), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs index 76084dcfce..8af76c419b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreJobDto.cs @@ -10,40 +10,39 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Backups.Models +namespace Squidex.Areas.Api.Controllers.Backups.Models; + +public sealed class RestoreJobDto { - public sealed class RestoreJobDto + /// <summary> + /// The uri to load from. + /// </summary> + [LocalizedRequired] + public Uri Url { get; set; } + + /// <summary> + /// The status log. + /// </summary> + [LocalizedRequired] + public List<string> Log { get; set; } + + /// <summary> + /// The time when the job has been started. + /// </summary> + public Instant Started { get; set; } + + /// <summary> + /// The time when the job has been stopped. + /// </summary> + public Instant? Stopped { get; set; } + + /// <summary> + /// The status of the operation. + /// </summary> + public JobStatus Status { get; set; } + + public static RestoreJobDto FromDomain(IRestoreJob job) { - /// <summary> - /// The uri to load from. - /// </summary> - [LocalizedRequired] - public Uri Url { get; set; } - - /// <summary> - /// The status log. - /// </summary> - [LocalizedRequired] - public List<string> Log { get; set; } - - /// <summary> - /// The time when the job has been started. - /// </summary> - public Instant Started { get; set; } - - /// <summary> - /// The time when the job has been stopped. - /// </summary> - public Instant? Stopped { get; set; } - - /// <summary> - /// The status of the operation. - /// </summary> - public JobStatus Status { get; set; } - - public static RestoreJobDto FromDomain(IRestoreJob job) - { - return SimpleMapper.Map(job, new RestoreJobDto()); - } + return SimpleMapper.Map(job, new RestoreJobDto()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs index 53646c344c..fc33f7ed8d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs @@ -7,20 +7,19 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Backups.Models +namespace Squidex.Areas.Api.Controllers.Backups.Models; + +public sealed class RestoreRequestDto { - public sealed class RestoreRequestDto - { - /// <summary> - /// The name of the app. - /// </summary> - [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string? Name { get; set; } + /// <summary> + /// The name of the app. + /// </summary> + [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string? Name { get; set; } - /// <summary> - /// The url to the restore file. - /// </summary> - [LocalizedRequired] - public Uri Url { get; set; } - } + /// <summary> + /// The url to the restore file. + /// </summary> + [LocalizedRequired] + public Uri Url { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs index 779badd41d..af3c960688 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs @@ -13,62 +13,61 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Backups +namespace Squidex.Areas.Api.Controllers.Backups; + +/// <summary> +/// Update and query backups for apps. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Backups))] +[ApiModelValidation(true)] +public class RestoreController : ApiController { + private readonly IBackupService backupService; + + public RestoreController(ICommandBus commandBus, IBackupService backupService) + : base(commandBus) + { + this.backupService = backupService; + } + /// <summary> - /// Update and query backups for apps. + /// Get current restore status. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Backups))] - [ApiModelValidation(true)] - public class RestoreController : ApiController + /// <returns> + /// 200 => Status returned. + /// </returns> + [HttpGet] + [Route("apps/restore/")] + [ProducesResponseType(typeof(RestoreJobDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminRestore)] + public async Task<IActionResult> GetRestoreJob() { - private readonly IBackupService backupService; + var job = await backupService.GetRestoreAsync(HttpContext.RequestAborted); - public RestoreController(ICommandBus commandBus, IBackupService backupService) - : base(commandBus) + if (job == null) { - this.backupService = backupService; + return NotFound(); } - /// <summary> - /// Get current restore status. - /// </summary> - /// <returns> - /// 200 => Status returned. - /// </returns> - [HttpGet] - [Route("apps/restore/")] - [ProducesResponseType(typeof(RestoreJobDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminRestore)] - public async Task<IActionResult> GetRestoreJob() - { - var job = await backupService.GetRestoreAsync(HttpContext.RequestAborted); - - if (job == null) - { - return NotFound(); - } - - var response = RestoreJobDto.FromDomain(job); + var response = RestoreJobDto.FromDomain(job); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Restore a backup. - /// </summary> - /// <param name="request">The backup to restore.</param> - /// <returns> - /// 204 => Restore operation started. - /// </returns> - [HttpPost] - [Route("apps/restore/")] - [ApiPermission(PermissionIds.AdminRestore)] - public async Task<IActionResult> PostRestoreJob([FromBody] RestoreRequestDto request) - { - await backupService.StartRestoreAsync(User.Token()!, request.Url, request.Name); + /// <summary> + /// Restore a backup. + /// </summary> + /// <param name="request">The backup to restore.</param> + /// <returns> + /// 204 => Restore operation started. + /// </returns> + [HttpPost] + [Route("apps/restore/")] + [ApiPermission(PermissionIds.AdminRestore)] + public async Task<IActionResult> PostRestoreJob([FromBody] RestoreRequestDto request) + { + await backupService.StartRestoreAsync(User.Token()!, request.Url, request.Name); - return NoContent(); - } + return NoContent(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs b/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs index ea7dce6623..4dcc57827f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/BulkResultDto.cs @@ -10,36 +10,35 @@ using Squidex.Infrastructure.Reflection; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class BulkResultDto { - public sealed class BulkResultDto + /// <summary> + /// The error when the bulk job failed. + /// </summary> + public ErrorDto? Error { get; set; } + + /// <summary> + /// The index of the bulk job where the result belongs to. The order can change. + /// </summary> + public int JobIndex { get; set; } + + /// <summary> + /// The ID of the entity that has been handled successfully or not. + /// </summary> + public DomainId? Id { get; set; } + + /// <summary> + /// The ID of the entity that has been handled successfully or not. + /// </summary> + [Obsolete("Use 'id' field now.")] + public DomainId? ContentId => Id; + + public static BulkResultDto FromDomain(BulkUpdateResultItem result, HttpContext httpContext) { - /// <summary> - /// The error when the bulk job failed. - /// </summary> - public ErrorDto? Error { get; set; } - - /// <summary> - /// The index of the bulk job where the result belongs to. The order can change. - /// </summary> - public int JobIndex { get; set; } - - /// <summary> - /// The ID of the entity that has been handled successfully or not. - /// </summary> - public DomainId? Id { get; set; } - - /// <summary> - /// The ID of the entity that has been handled successfully or not. - /// </summary> - [Obsolete("Use 'id' field now.")] - public DomainId? ContentId => Id; - - public static BulkResultDto FromDomain(BulkUpdateResultItem result, HttpContext httpContext) - { - var error = result.Exception?.ToErrorDto(httpContext).Error; - - return SimpleMapper.Map(result, new BulkResultDto { Error = error }); - } + var error = result.Exception?.ToErrorDto(httpContext).Error; + + return SimpleMapper.Map(result, new BulkResultDto { Error = error }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index 0182a6284a..ad19cc4376 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -16,161 +16,160 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Comments +namespace Squidex.Areas.Api.Controllers.Comments; + +/// <summary> +/// Update and query comments for any kind of app resource. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Comments))] +public sealed class CommentsController : ApiController { + private readonly ICommentsLoader commentsLoader; + private readonly IWatchingService watchingService; + + public CommentsController(ICommandBus commandBus, ICommentsLoader commentsLoader, + IWatchingService watchingService) + : base(commandBus) + { + this.commentsLoader = commentsLoader; + + this.watchingService = watchingService; + } + /// <summary> - /// Update and query comments for any kind of app resource. + /// Get all watching users.. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Comments))] - public sealed class CommentsController : ApiController + /// <param name="app">The name of the app.</param> + /// <param name="resource">The path to the resource.</param> + /// <returns> + /// 200 => Watching users returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/watching/{*resource}")] + [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(0)] + public async Task<IActionResult> GetWatchingUsers(string app, string? resource = null) { - private readonly ICommentsLoader commentsLoader; - private readonly IWatchingService watchingService; + var result = await watchingService.GetWatchingUsersAsync(App.Id, resource, UserId, HttpContext.RequestAborted); - public CommentsController(ICommandBus commandBus, ICommentsLoader commentsLoader, - IWatchingService watchingService) - : base(commandBus) - { - this.commentsLoader = commentsLoader; - - this.watchingService = watchingService; - } - - /// <summary> - /// Get all watching users.. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="resource">The path to the resource.</param> - /// <returns> - /// 200 => Watching users returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/watching/{*resource}")] - [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(0)] - public async Task<IActionResult> GetWatchingUsers(string app, string? resource = null) - { - var result = await watchingService.GetWatchingUsersAsync(App.Id, resource, UserId, HttpContext.RequestAborted); - - return Ok(result); - } - - /// <summary> - /// Get all comments. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="commentsId">The ID of the comments.</param> - /// <param name="version">The current version.</param> - /// <remarks> - /// When passing in a version you can retrieve all updates since then. - /// </remarks> - /// <returns> - /// 200 => Comments returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/comments/{commentsId}")] - [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppCommentsRead)] - [ApiCosts(0)] - public async Task<IActionResult> GetComments(string app, DomainId commentsId, [FromQuery] long version = EtagVersion.Any) - { - var result = await commentsLoader.GetCommentsAsync(Id(commentsId), version, HttpContext.RequestAborted); - - var response = Deferred.Response(() => - { - return CommentsDto.FromDomain(result); - }); - - Response.Headers[HeaderNames.ETag] = result.Version.ToString(CultureInfo.InvariantCulture); - - return Ok(response); - } - - /// <summary> - /// Create a new comment. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="commentsId">The ID of the comments.</param> - /// <param name="request">The comment object that needs to created.</param> - /// <returns> - /// 201 => Comment created. - /// 400 => Comment request not valid. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/comments/{commentsId}")] - [ProducesResponseType(typeof(CommentDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppCommentsCreate)] - [ApiCosts(0)] - public async Task<IActionResult> PostComment(string app, DomainId commentsId, [FromBody] UpsertCommentDto request) - { - var command = request.ToCreateCommand(commentsId); - - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - var response = CommentDto.FromDomain(command); - - return CreatedAtAction(nameof(GetComments), new { app, commentsId }, response); - } - - /// <summary> - /// Update a comment. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="commentsId">The ID of the comments.</param> - /// <param name="commentId">The ID of the comment.</param> - /// <param name="request">The comment object that needs to updated.</param> - /// <returns> - /// 204 => Comment updated. - /// 400 => Comment request not valid. - /// 404 => Comment or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/comments/{commentsId}/{commentId}")] - [ApiPermissionOrAnonymous(PermissionIds.AppCommentsUpdate)] - [ApiCosts(0)] - public async Task<IActionResult> PutComment(string app, DomainId commentsId, DomainId commentId, [FromBody] UpsertCommentDto request) - { - var command = request.ToUpdateComment(commentsId, commentId); - - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - return NoContent(); - } - - /// <summary> - /// Delete a comment. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="commentsId">The ID of the comments.</param> - /// <param name="commentId">The ID of the comment.</param> - /// <returns> - /// 204 => Comment deleted. - /// 404 => Comment or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/comments/{commentsId}/{commentId}")] - [ApiPermissionOrAnonymous(PermissionIds.AppCommentsDelete)] - [ApiCosts(0)] - public async Task<IActionResult> DeleteComment(string app, DomainId commentsId, DomainId commentId) + return Ok(result); + } + + /// <summary> + /// Get all comments. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="commentsId">The ID of the comments.</param> + /// <param name="version">The current version.</param> + /// <remarks> + /// When passing in a version you can retrieve all updates since then. + /// </remarks> + /// <returns> + /// 200 => Comments returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/comments/{commentsId}")] + [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppCommentsRead)] + [ApiCosts(0)] + public async Task<IActionResult> GetComments(string app, DomainId commentsId, [FromQuery] long version = EtagVersion.Any) + { + var result = await commentsLoader.GetCommentsAsync(Id(commentsId), version, HttpContext.RequestAborted); + + var response = Deferred.Response(() => { - var command = new DeleteComment - { - CommentsId = commentsId, - CommentId = commentId - }; + return CommentsDto.FromDomain(result); + }); - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + Response.Headers[HeaderNames.ETag] = result.Version.ToString(CultureInfo.InvariantCulture); - return NoContent(); - } + return Ok(response); + } - private DomainId Id(DomainId commentsId) + /// <summary> + /// Create a new comment. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="commentsId">The ID of the comments.</param> + /// <param name="request">The comment object that needs to created.</param> + /// <returns> + /// 201 => Comment created. + /// 400 => Comment request not valid. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/comments/{commentsId}")] + [ProducesResponseType(typeof(CommentDto), 201)] + [ApiPermissionOrAnonymous(PermissionIds.AppCommentsCreate)] + [ApiCosts(0)] + public async Task<IActionResult> PostComment(string app, DomainId commentsId, [FromBody] UpsertCommentDto request) + { + var command = request.ToCreateCommand(commentsId); + + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + var response = CommentDto.FromDomain(command); + + return CreatedAtAction(nameof(GetComments), new { app, commentsId }, response); + } + + /// <summary> + /// Update a comment. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="commentsId">The ID of the comments.</param> + /// <param name="commentId">The ID of the comment.</param> + /// <param name="request">The comment object that needs to updated.</param> + /// <returns> + /// 204 => Comment updated. + /// 400 => Comment request not valid. + /// 404 => Comment or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/comments/{commentsId}/{commentId}")] + [ApiPermissionOrAnonymous(PermissionIds.AppCommentsUpdate)] + [ApiCosts(0)] + public async Task<IActionResult> PutComment(string app, DomainId commentsId, DomainId commentId, [FromBody] UpsertCommentDto request) + { + var command = request.ToUpdateComment(commentsId, commentId); + + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + return NoContent(); + } + + /// <summary> + /// Delete a comment. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="commentsId">The ID of the comments.</param> + /// <param name="commentId">The ID of the comment.</param> + /// <returns> + /// 204 => Comment deleted. + /// 404 => Comment or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/comments/{commentsId}/{commentId}")] + [ApiPermissionOrAnonymous(PermissionIds.AppCommentsDelete)] + [ApiCosts(0)] + public async Task<IActionResult> DeleteComment(string app, DomainId commentsId, DomainId commentId) + { + var command = new DeleteComment { - return DomainId.Combine(App.Id, commentsId); - } + CommentsId = commentsId, + CommentId = commentId + }; + + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + return NoContent(); + } + + private DomainId Id(DomainId commentsId) + { + return DomainId.Combine(App.Id, commentsId); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs index 60a2eb5f5f..4c2a1492fa 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs @@ -12,50 +12,49 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Comments.Models +namespace Squidex.Areas.Api.Controllers.Comments.Models; + +public sealed class CommentDto { - public sealed class CommentDto + /// <summary> + /// The ID of the comment. + /// </summary> + public DomainId Id { get; set; } + + /// <summary> + /// The time when the comment was created or updated last. + /// </summary> + [LocalizedRequired] + public Instant Time { get; set; } + + /// <summary> + /// The user who created or updated the comment. + /// </summary> + [LocalizedRequired] + public RefToken User { get; set; } + + /// <summary> + /// The text of the comment. + /// </summary> + [LocalizedRequired] + public string Text { get; set; } + + /// <summary> + /// The url where the comment is created. + /// </summary> + public Uri? Url { get; set; } + + public static CommentDto FromDomain(Comment comment) + { + var result = SimpleMapper.Map(comment, new CommentDto()); + + return result; + } + + public static CommentDto FromDomain(CreateComment command) { - /// <summary> - /// The ID of the comment. - /// </summary> - public DomainId Id { get; set; } - - /// <summary> - /// The time when the comment was created or updated last. - /// </summary> - [LocalizedRequired] - public Instant Time { get; set; } - - /// <summary> - /// The user who created or updated the comment. - /// </summary> - [LocalizedRequired] - public RefToken User { get; set; } - - /// <summary> - /// The text of the comment. - /// </summary> - [LocalizedRequired] - public string Text { get; set; } - - /// <summary> - /// The url where the comment is created. - /// </summary> - public Uri? Url { get; set; } - - public static CommentDto FromDomain(Comment comment) - { - var result = SimpleMapper.Map(comment, new CommentDto()); - - return result; - } - - public static CommentDto FromDomain(CreateComment command) - { - var time = SystemClock.Instance.GetCurrentInstant(); - - return SimpleMapper.Map(command, new CommentDto { Id = command.CommentId, User = command.Actor, Time = time }); - } + var time = SystemClock.Instance.GetCurrentInstant(); + + return SimpleMapper.Map(command, new CommentDto { Id = command.CommentId, User = command.Actor, Time = time }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs index 447a361ecb..e2046e6168 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs @@ -8,41 +8,40 @@ using Squidex.Domain.Apps.Entities.Comments; using Squidex.Infrastructure; -namespace Squidex.Areas.Api.Controllers.Comments.Models +namespace Squidex.Areas.Api.Controllers.Comments.Models; + +public sealed class CommentsDto { - public sealed class CommentsDto + /// <summary> + /// The created comments including the updates. + /// </summary> + public CommentDto[]? CreatedComments { get; set; } + + /// <summary> + /// The updates comments since the last version. + /// </summary> + public CommentDto[]? UpdatedComments { get; set; } + + /// <summary> + /// The deleted comments since the last version. + /// </summary> + public List<DomainId>? DeletedComments { get; set; } + + /// <summary> + /// The current version. + /// </summary> + public long Version { get; set; } + + public static CommentsDto FromDomain(CommentsResult comments) { - /// <summary> - /// The created comments including the updates. - /// </summary> - public CommentDto[]? CreatedComments { get; set; } - - /// <summary> - /// The updates comments since the last version. - /// </summary> - public CommentDto[]? UpdatedComments { get; set; } - - /// <summary> - /// The deleted comments since the last version. - /// </summary> - public List<DomainId>? DeletedComments { get; set; } - - /// <summary> - /// The current version. - /// </summary> - public long Version { get; set; } - - public static CommentsDto FromDomain(CommentsResult comments) + var result = new CommentsDto { - var result = new CommentsDto - { - CreatedComments = comments.CreatedComments.Select(CommentDto.FromDomain).ToArray(), - UpdatedComments = comments.UpdatedComments.Select(CommentDto.FromDomain).ToArray(), - DeletedComments = comments.DeletedComments, - Version = comments.Version - }; - - return result; - } + CreatedComments = comments.CreatedComments.Select(CommentDto.FromDomain).ToArray(), + UpdatedComments = comments.UpdatedComments.Select(CommentDto.FromDomain).ToArray(), + DeletedComments = comments.DeletedComments, + Version = comments.Version + }; + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs index 22f78f54f4..21024690b2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs @@ -10,36 +10,35 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Comments.Models +namespace Squidex.Areas.Api.Controllers.Comments.Models; + +public sealed class UpsertCommentDto { - public sealed class UpsertCommentDto - { - /// <summary> - /// The comment text. - /// </summary> - [LocalizedRequired] - public string Text { get; set; } + /// <summary> + /// The comment text. + /// </summary> + [LocalizedRequired] + public string Text { get; set; } - /// <summary> - /// The url where the comment is created. - /// </summary> - public Uri? Url { get; set; } + /// <summary> + /// The url where the comment is created. + /// </summary> + public Uri? Url { get; set; } - public CreateComment ToCreateCommand(DomainId commentsId) + public CreateComment ToCreateCommand(DomainId commentsId) + { + return SimpleMapper.Map(this, new CreateComment { - return SimpleMapper.Map(this, new CreateComment - { - CommentsId = commentsId - }); - } + CommentsId = commentsId + }); + } - public UpdateComment ToUpdateComment(DomainId commentsId, DomainId commentId) + public UpdateComment ToUpdateComment(DomainId commentsId, DomainId commentId) + { + return SimpleMapper.Map(this, new UpdateComment { - return SimpleMapper.Map(this, new UpdateComment - { - CommentsId = commentsId, - CommentId = commentId - }); - } + CommentsId = commentsId, + CommentId = commentId + }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs index 9446cffe6e..b10aa5b550 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs @@ -17,87 +17,86 @@ using Squidex.Infrastructure.Translations; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Comments.Notifications +namespace Squidex.Areas.Api.Controllers.Comments.Notifications; + +/// <summary> +/// Update and query user notifications. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Notifications))] +public sealed class UserNotificationsController : ApiController { + private readonly ICommentsLoader commentsLoader; + + public UserNotificationsController(ICommandBus commandBus, ICommentsLoader commentsLoader) + : base(commandBus) + { + this.commentsLoader = commentsLoader; + } + /// <summary> - /// Update and query user notifications. + /// Get all notifications. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Notifications))] - public sealed class UserNotificationsController : ApiController + /// <param name="userId">The user id.</param> + /// <param name="version">The current version.</param> + /// <remarks> + /// When passing in a version you can retrieve all updates since then. + /// </remarks> + /// <returns> + /// 200 => All comments returned. + /// </returns> + [HttpGet] + [Route("users/{userId}/notifications")] + [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] + [ApiPermission] + public async Task<IActionResult> GetNotifications(DomainId userId, [FromQuery] long version = EtagVersion.Any) { - private readonly ICommentsLoader commentsLoader; + CheckPermissions(userId); - public UserNotificationsController(ICommandBus commandBus, ICommentsLoader commentsLoader) - : base(commandBus) - { - this.commentsLoader = commentsLoader; - } + var result = await commentsLoader.GetCommentsAsync(userId, version, HttpContext.RequestAborted); - /// <summary> - /// Get all notifications. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="version">The current version.</param> - /// <remarks> - /// When passing in a version you can retrieve all updates since then. - /// </remarks> - /// <returns> - /// 200 => All comments returned. - /// </returns> - [HttpGet] - [Route("users/{userId}/notifications")] - [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] - [ApiPermission] - public async Task<IActionResult> GetNotifications(DomainId userId, [FromQuery] long version = EtagVersion.Any) + var response = Deferred.Response(() => { - CheckPermissions(userId); + return CommentsDto.FromDomain(result); + }); - var result = await commentsLoader.GetCommentsAsync(userId, version, HttpContext.RequestAborted); + Response.Headers[HeaderNames.ETag] = result.Version.ToString(CultureInfo.InvariantCulture); - var response = Deferred.Response(() => - { - return CommentsDto.FromDomain(result); - }); - - Response.Headers[HeaderNames.ETag] = result.Version.ToString(CultureInfo.InvariantCulture); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Delete a notification. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="commentId">The ID of the comment.</param> + /// <returns> + /// 204 => Comment deleted. + /// 404 => Comment not found. + /// </returns> + [HttpDelete] + [Route("users/{userId}/notifications/{commentId}")] + [ApiPermission] + public async Task<IActionResult> DeleteComment(DomainId userId, DomainId commentId) + { + CheckPermissions(userId); - /// <summary> - /// Delete a notification. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="commentId">The ID of the comment.</param> - /// <returns> - /// 204 => Comment deleted. - /// 404 => Comment not found. - /// </returns> - [HttpDelete] - [Route("users/{userId}/notifications/{commentId}")] - [ApiPermission] - public async Task<IActionResult> DeleteComment(DomainId userId, DomainId commentId) + var commmand = new DeleteComment { - CheckPermissions(userId); + AppId = CommentsCommand.NoApp, + CommentsId = userId, + CommentId = commentId + }; - var commmand = new DeleteComment - { - AppId = CommentsCommand.NoApp, - CommentsId = userId, - CommentId = commentId - }; + await CommandBus.PublishAsync(commmand, HttpContext.RequestAborted); - await CommandBus.PublishAsync(commmand, HttpContext.RequestAborted); - - return NoContent(); - } + return NoContent(); + } - private void CheckPermissions(DomainId userId) + private void CheckPermissions(DomainId userId) + { + if (!string.Equals(userId.ToString(), User.OpenIdSubject(), StringComparison.Ordinal)) { - if (!string.Equals(userId.ToString(), User.OpenIdSubject(), StringComparison.Ordinal)) - { - throw new DomainForbiddenException(T.Get("comments.noPermissions")); - } + throw new DomainForbiddenException(T.Get("comments.noPermissions")); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs index 8993af6f02..9f87fd6b5f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs @@ -12,73 +12,72 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Contents +namespace Squidex.Areas.Api.Controllers.Contents; + +public sealed class ContentOpenApiController : ApiController { - public sealed class ContentOpenApiController : ApiController - { - private readonly IAppProvider appProvider; - private readonly SchemasOpenApiGenerator schemasOpenApiGenerator; + private readonly IAppProvider appProvider; + private readonly SchemasOpenApiGenerator schemasOpenApiGenerator; - public ContentOpenApiController(ICommandBus commandBus, IAppProvider appProvider, - SchemasOpenApiGenerator schemasOpenApiGenerator) - : base(commandBus) - { - this.appProvider = appProvider; - this.schemasOpenApiGenerator = schemasOpenApiGenerator; - } + public ContentOpenApiController(ICommandBus commandBus, IAppProvider appProvider, + SchemasOpenApiGenerator schemasOpenApiGenerator) + : base(commandBus) + { + this.appProvider = appProvider; + this.schemasOpenApiGenerator = schemasOpenApiGenerator; + } - [HttpGet] - [Route("content/{app}/docs/")] - [ApiCosts(0)] - [AllowAnonymous] - public IActionResult Docs(string app) + [HttpGet] + [Route("content/{app}/docs/")] + [ApiCosts(0)] + [AllowAnonymous] + public IActionResult Docs(string app) + { + var vm = new DocsVM { - var vm = new DocsVM - { - Specification = $"~/api/content/{app}/swagger/v1/swagger.json" - }; + Specification = $"~/api/content/{app}/swagger/v1/swagger.json" + }; - return View(nameof(Docs), vm); - } + return View(nameof(Docs), vm); + } - [HttpGet] - [Route("content/{app}/docs/flat/")] - [ApiCosts(0)] - [AllowAnonymous] - public IActionResult DocsFlat(string app) + [HttpGet] + [Route("content/{app}/docs/flat/")] + [ApiCosts(0)] + [AllowAnonymous] + public IActionResult DocsFlat(string app) + { + var vm = new DocsVM { - var vm = new DocsVM - { - Specification = $"~/api/content/{app}/flat/swagger/v1/swagger.json" - }; + Specification = $"~/api/content/{app}/flat/swagger/v1/swagger.json" + }; - return View(nameof(Docs), vm); - } + return View(nameof(Docs), vm); + } - [HttpGet] - [Route("content/{app}/swagger/v1/swagger.json")] - [ApiCosts(0)] - [AllowAnonymous] - public async Task<IActionResult> GetOpenApi(string app) - { - var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); + [HttpGet] + [Route("content/{app}/swagger/v1/swagger.json")] + [ApiCosts(0)] + [AllowAnonymous] + public async Task<IActionResult> GetOpenApi(string app) + { + var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); - var openApiDocument = await schemasOpenApiGenerator.GenerateAsync(HttpContext, App, schemas, false); + var openApiDocument = await schemasOpenApiGenerator.GenerateAsync(HttpContext, App, schemas, false); - return Content(openApiDocument.ToJson(), "application/json"); - } + return Content(openApiDocument.ToJson(), "application/json"); + } - [HttpGet] - [Route("content/{app}/flat/swagger/v1/swagger.json")] - [ApiCosts(0)] - [AllowAnonymous] - public async Task<IActionResult> GetFlatOpenApi(string app) - { - var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); + [HttpGet] + [Route("content/{app}/flat/swagger/v1/swagger.json")] + [ApiCosts(0)] + [AllowAnonymous] + public async Task<IActionResult> GetFlatOpenApi(string app) + { + var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); - var openApiDocument = await schemasOpenApiGenerator.GenerateAsync(HttpContext, App, schemas, true); + var openApiDocument = await schemasOpenApiGenerator.GenerateAsync(HttpContext, App, schemas, true); - return Content(openApiDocument.ToJson(), "application/json"); - } + return Content(openApiDocument.ToJson(), "application/json"); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 4f9eeac105..1ea706f43e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -16,578 +16,577 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Contents +namespace Squidex.Areas.Api.Controllers.Contents; + +[SchemaMustBePublished] +public sealed class ContentsController : ApiController { - [SchemaMustBePublished] - public sealed class ContentsController : ApiController + private readonly IContentQueryService contentQuery; + private readonly IContentWorkflow contentWorkflow; + + public ContentsController(ICommandBus commandBus, + IContentQueryService contentQuery, + IContentWorkflow contentWorkflow) + : base(commandBus) { - private readonly IContentQueryService contentQuery; - private readonly IContentWorkflow contentWorkflow; + this.contentQuery = contentQuery; + this.contentWorkflow = contentWorkflow; + } - public ContentsController(ICommandBus commandBus, - IContentQueryService contentQuery, - IContentWorkflow contentWorkflow) - : base(commandBus) - { - this.contentQuery = contentQuery; - this.contentWorkflow = contentWorkflow; - } + /// <summary> + /// Queries contents. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="ids">The optional ids of the content to fetch.</param> + /// <param name="q">The optional json query.</param> + /// <returns> + /// 200 => Contents retunred. + /// 404 => Schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpGet] + [Route("content/{app}/{schema}/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task<IActionResult> GetContents(string app, string schema, [FromQuery] string? ids = null, [FromQuery] string? q = null) + { + var contents = await contentQuery.QueryAsync(Context, schema, CreateQuery(ids, q), HttpContext.RequestAborted); - /// <summary> - /// Queries contents. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="ids">The optional ids of the content to fetch.</param> - /// <param name="q">The optional json query.</param> - /// <returns> - /// 200 => Contents retunred. - /// 404 => Schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpGet] - [Route("content/{app}/{schema}/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - public async Task<IActionResult> GetContents(string app, string schema, [FromQuery] string? ids = null, [FromQuery] string? q = null) + var response = Deferred.AsyncResponse(() => { - var contents = await contentQuery.QueryAsync(Context, schema, CreateQuery(ids, q), HttpContext.RequestAborted); + return ContentsDto.FromContentsAsync(contents, Resources, Schema, contentWorkflow); + }); - var response = Deferred.AsyncResponse(() => - { - return ContentsDto.FromContentsAsync(contents, Resources, Schema, contentWorkflow); - }); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Queries contents. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="query">The required query object.</param> + /// <returns> + /// 200 => Contents returned. + /// 404 => Schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPost] + [Route("content/{app}/{schema}/query")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task<IActionResult> GetContentsPost(string app, string schema, [FromBody] QueryDto query) + { + var contents = await contentQuery.QueryAsync(Context, schema, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); - /// <summary> - /// Queries contents. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="query">The required query object.</param> - /// <returns> - /// 200 => Contents returned. - /// 404 => Schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPost] - [Route("content/{app}/{schema}/query")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - public async Task<IActionResult> GetContentsPost(string app, string schema, [FromBody] QueryDto query) + var response = Deferred.AsyncResponse(() => { - var contents = await contentQuery.QueryAsync(Context, schema, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); + return ContentsDto.FromContentsAsync(contents, Resources, Schema, contentWorkflow); + }); - var response = Deferred.AsyncResponse(() => - { - return ContentsDto.FromContentsAsync(contents, Resources, Schema, contentWorkflow); - }); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Get a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content to fetch.</param> + /// <param name="version">The optional version.</param> + /// <returns> + /// 200 => Content returned. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpGet] + [Route("content/{app}/{schema}/{id}/")] + [ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task<IActionResult> GetContent(string app, string schema, DomainId id, long version = EtagVersion.Any) + { + var content = await contentQuery.FindAsync(Context, schema, id, version, HttpContext.RequestAborted); - /// <summary> - /// Get a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content to fetch.</param> - /// <param name="version">The optional version.</param> - /// <returns> - /// 200 => Content returned. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpGet] - [Route("content/{app}/{schema}/{id}/")] - [ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - public async Task<IActionResult> GetContent(string app, string schema, DomainId id, long version = EtagVersion.Any) + if (content == null) { - var content = await contentQuery.FindAsync(Context, schema, id, version, HttpContext.RequestAborted); - - if (content == null) - { - return NotFound(); - } - - var response = Deferred.Response(() => - { - return ContentDto.FromDomain(content, Resources); - }); - - return Ok(response); + return NotFound(); } - /// <summary> - /// Get a content item validity. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content to fetch.</param> - /// <returns> - /// 204 => Content is valid. - /// 400 => Content not valid. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpGet] - [Route("content/{app}/{schema}/{id}/validity")] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - public async Task<IActionResult> GetContentValidity(string app, string schema, DomainId id) + var response = Deferred.Response(() => { - var command = new ValidateContent { ContentId = id }; + return ContentDto.FromDomain(content, Resources); + }); - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return Ok(response); + } - return NoContent(); - } + /// <summary> + /// Get a content item validity. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content to fetch.</param> + /// <returns> + /// 204 => Content is valid. + /// 400 => Content not valid. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpGet] + [Route("content/{app}/{schema}/{id}/validity")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task<IActionResult> GetContentValidity(string app, string schema, DomainId id) + { + var command = new ValidateContent { ContentId = id }; - /// <summary> - /// Get all references of a content. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content to fetch.</param> - /// <param name="q">The optional json query.</param> - /// <returns> - /// 200 => Contents returned. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpGet] - [Route("content/{app}/{schema}/{id}/references")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - public async Task<IActionResult> GetReferences(string app, string schema, DomainId id, [FromQuery] string? q = null) - { - var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id), HttpContext.RequestAborted); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var response = Deferred.AsyncResponse(() => - { - return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); - }); + return NoContent(); + } - return Ok(response); - } + /// <summary> + /// Get all references of a content. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content to fetch.</param> + /// <param name="q">The optional json query.</param> + /// <returns> + /// 200 => Contents returned. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpGet] + [Route("content/{app}/{schema}/{id}/references")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task<IActionResult> GetReferences(string app, string schema, DomainId id, [FromQuery] string? q = null) + { + var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id), HttpContext.RequestAborted); - /// <summary> - /// Get a referencing contents of a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content to fetch.</param> - /// <param name="q">The optional json query.</param> - /// <returns> - /// 200 => Content returned. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpGet] - [Route("content/{app}/{schema}/{id}/referencing")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - public async Task<IActionResult> GetReferencing(string app, string schema, DomainId id, [FromQuery] string? q = null) + var response = Deferred.AsyncResponse(() => { - var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id), HttpContext.RequestAborted); + return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); + }); - var response = Deferred.AsyncResponse(() => - { - return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); - }); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Get a referencing contents of a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content to fetch.</param> + /// <param name="q">The optional json query.</param> + /// <returns> + /// 200 => Content returned. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpGet] + [Route("content/{app}/{schema}/{id}/referencing")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task<IActionResult> GetReferencing(string app, string schema, DomainId id, [FromQuery] string? q = null) + { + var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id), HttpContext.RequestAborted); - /// <summary> - /// Get a content by version. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content to fetch.</param> - /// <param name="version">The version fo the content to fetch.</param> - /// <returns> - /// 200 => Content version returned. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpGet] - [Route("content/{app}/{schema}/{id}/{version}/")] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] - [ApiCosts(1)] - [Obsolete("Use ID endpoint with version query.")] - public async Task<IActionResult> GetContentVersion(string app, string schema, DomainId id, int version) + var response = Deferred.AsyncResponse(() => { - var content = await contentQuery.FindAsync(Context, schema, id, version, HttpContext.RequestAborted); + return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); + }); - if (content == null) - { - return NotFound(); - } + return Ok(response); + } - var response = Deferred.Response(() => - { - return ContentDto.FromDomain(content, Resources).Data; - }); + /// <summary> + /// Get a content by version. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content to fetch.</param> + /// <param name="version">The version fo the content to fetch.</param> + /// <returns> + /// 200 => Content version returned. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpGet] + [Route("content/{app}/{schema}/{id}/{version}/")] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] + [ApiCosts(1)] + [Obsolete("Use ID endpoint with version query.")] + public async Task<IActionResult> GetContentVersion(string app, string schema, DomainId id, int version) + { + var content = await contentQuery.FindAsync(Context, schema, id, version, HttpContext.RequestAborted); - return Ok(response); + if (content == null) + { + return NotFound(); } - /// <summary> - /// Create a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The request parameters.</param> - /// <returns> - /// 201 => Content created. - /// 400 => Content request not valid. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPost] - [Route("content/{app}/{schema}/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status201Created)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostContent(string app, string schema, CreateContentDto request) + var response = Deferred.Response(() => { - var command = request.ToCommand(); + return ContentDto.FromDomain(content, Resources).Data; + }); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return CreatedAtAction(nameof(GetContent), new { app, schema, id = command.ContentId }, response); - } + /// <summary> + /// Create a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The request parameters.</param> + /// <returns> + /// 201 => Content created. + /// 400 => Content request not valid. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPost] + [Route("content/{app}/{schema}/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status201Created)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostContent(string app, string schema, CreateContentDto request) + { + var command = request.ToCommand(); - /// <summary> - /// Import content items. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The import request.</param> - /// <returns> - /// 200 => Contents created. - /// 400 => Content request not valid. - /// 404 => Content references, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPost] - [Route("content/{app}/{schema}/import")] - [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsCreate)] - [ApiCosts(5)] - [Obsolete("Use bulk endpoint now.")] - public async Task<IActionResult> PostContents(string app, string schema, [FromBody] ImportContentsDto request) - { - var command = request.ToCommand(); + var response = await InvokeCommandAsync(command); - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return CreatedAtAction(nameof(GetContent), new { app, schema, id = command.ContentId }, response); + } - var result = context.Result<BulkUpdateResult>(); - var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); + /// <summary> + /// Import content items. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The import request.</param> + /// <returns> + /// 200 => Contents created. + /// 400 => Content request not valid. + /// 404 => Content references, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPost] + [Route("content/{app}/{schema}/import")] + [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsCreate)] + [ApiCosts(5)] + [Obsolete("Use bulk endpoint now.")] + public async Task<IActionResult> PostContents(string app, string schema, [FromBody] ImportContentsDto request) + { + var command = request.ToCommand(); - return Ok(response); - } + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - /// <summary> - /// Bulk update content items. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The bulk update request.</param> - /// <returns> - /// 201 => Contents created, update or delete. - /// 400 => Contents request not valid. - /// 404 => Contents references, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPost] - [Route("content/{app}/{schema}/bulk")] - [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] - [ApiCosts(5)] - public async Task<IActionResult> BulkUpdateContents(string app, string schema, [FromBody] BulkUpdateContentsDto request) - { - var command = request.ToCommand(false); + var result = context.Result<BulkUpdateResult>(); + var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return Ok(response); + } - var result = context.Result<BulkUpdateResult>(); - var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); + /// <summary> + /// Bulk update content items. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The bulk update request.</param> + /// <returns> + /// 201 => Contents created, update or delete. + /// 400 => Contents request not valid. + /// 404 => Contents references, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPost] + [Route("content/{app}/{schema}/bulk")] + [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] + [ApiCosts(5)] + public async Task<IActionResult> BulkUpdateContents(string app, string schema, [FromBody] BulkUpdateContentsDto request) + { + var command = request.ToCommand(false); - return Ok(response); - } + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - /// <summary> - /// Upsert a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content item to update.</param> - /// <param name="request">The request parameters.</param> - /// <returns> - /// 200 => Content created or updated. - /// 400 => Content request not valid. - /// 404 => Content references, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPost] - [Route("content/{app}/{schema}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpsert)] - [ApiCosts(1)] - public async Task<IActionResult> PostUpsertContent(string app, string schema, DomainId id, UpsertContentDto request) - { - var command = request.ToCommand(id); + var result = context.Result<BulkUpdateResult>(); + var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Upsert a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content item to update.</param> + /// <param name="request">The request parameters.</param> + /// <returns> + /// 200 => Content created or updated. + /// 400 => Content request not valid. + /// 404 => Content references, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPost] + [Route("content/{app}/{schema}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpsert)] + [ApiCosts(1)] + public async Task<IActionResult> PostUpsertContent(string app, string schema, DomainId id, UpsertContentDto request) + { + var command = request.ToCommand(id); - /// <summary> - /// Update a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content item to update.</param> - /// <param name="request">The full data for the content item.</param> - /// <returns> - /// 200 => Content updated. - /// 400 => Content request not valid. - /// 404 => Content references, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPut] - [Route("content/{app}/{schema}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpdateOwn)] - [ApiCosts(1)] - public async Task<IActionResult> PutContent(string app, string schema, DomainId id, [FromBody] ContentData request) - { - var command = new UpdateContent { ContentId = id, Data = request }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Update a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content item to update.</param> + /// <param name="request">The full data for the content item.</param> + /// <returns> + /// 200 => Content updated. + /// 400 => Content request not valid. + /// 404 => Content references, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPut] + [Route("content/{app}/{schema}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpdateOwn)] + [ApiCosts(1)] + public async Task<IActionResult> PutContent(string app, string schema, DomainId id, [FromBody] ContentData request) + { + var command = new UpdateContent { ContentId = id, Data = request }; - /// <summary> - /// Patchs a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content item to patch.</param> - /// <param name="request">The patch for the content item.</param> - /// <returns> - /// 200 => Content patched. - /// 400 => Content request not valid. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPatch] - [Route("content/{app}/{schema}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpdateOwn)] - [ApiCosts(1)] - public async Task<IActionResult> PatchContent(string app, string schema, DomainId id, [FromBody] ContentData request) - { - var command = new PatchContent { ContentId = id, Data = request }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Patchs a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content item to patch.</param> + /// <param name="request">The patch for the content item.</param> + /// <returns> + /// 200 => Content patched. + /// 400 => Content request not valid. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPatch] + [Route("content/{app}/{schema}/{id}/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsUpdateOwn)] + [ApiCosts(1)] + public async Task<IActionResult> PatchContent(string app, string schema, DomainId id, [FromBody] ContentData request) + { + var command = new PatchContent { ContentId = id, Data = request }; - /// <summary> - /// Change status of a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content item to change.</param> - /// <param name="request">The status request.</param> - /// <returns> - /// 200 => Content status changed. - /// 400 => Content request not valid. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPut] - [Route("content/{app}/{schema}/{id}/status/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsChangeStatusOwn)] - [ApiCosts(1)] - public async Task<IActionResult> PutContentStatus(string app, string schema, DomainId id, [FromBody] ChangeStatusDto request) - { - var command = request.ToCommand(id); + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Change status of a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content item to change.</param> + /// <param name="request">The status request.</param> + /// <returns> + /// 200 => Content status changed. + /// 400 => Content request not valid. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPut] + [Route("content/{app}/{schema}/{id}/status/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsChangeStatusOwn)] + [ApiCosts(1)] + public async Task<IActionResult> PutContentStatus(string app, string schema, DomainId id, [FromBody] ChangeStatusDto request) + { + var command = request.ToCommand(id); - /// <summary> - /// Cancel status change of a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content item to cancel.</param> - /// <returns> - /// 200 => Content status change cancelled. - /// 400 => Content request not valid. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpDelete] - [Route("content/{app}/{schema}/{id}/status/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsChangeStatusOwn)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteContentStatus(string app, string schema, DomainId id) - { - var command = new CancelContentSchedule { ContentId = id }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Cancel status change of a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content item to cancel.</param> + /// <returns> + /// 200 => Content status change cancelled. + /// 400 => Content request not valid. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpDelete] + [Route("content/{app}/{schema}/{id}/status/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsChangeStatusOwn)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteContentStatus(string app, string schema, DomainId id) + { + var command = new CancelContentSchedule { ContentId = id }; - /// <summary> - /// Create a new draft version. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content item to create the draft for.</param> - /// <returns> - /// 200 => Content draft created. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPost] - [Route("content/{app}/{schema}/{id}/draft/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsVersionCreateOwn)] - [ApiCosts(1)] - public async Task<IActionResult> CreateDraft(string app, string schema, DomainId id) - { - var command = new CreateContentDraft { ContentId = id }; + var response = await InvokeCommandAsync(command); + + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Create a new draft version. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content item to create the draft for.</param> + /// <returns> + /// 200 => Content draft created. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPost] + [Route("content/{app}/{schema}/{id}/draft/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsVersionCreateOwn)] + [ApiCosts(1)] + public async Task<IActionResult> CreateDraft(string app, string schema, DomainId id) + { + var command = new CreateContentDraft { ContentId = id }; - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Delete the draft version. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content item to delete the draft from.</param> - /// <returns> - /// 200 => Content draft deleted. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpDelete] - [Route("content/{app}/{schema}/{id}/draft/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsVersionDeleteOwn)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteVersion(string app, string schema, DomainId id) - { - var command = new DeleteContentDraft { ContentId = id }; + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Delete the draft version. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content item to delete the draft from.</param> + /// <returns> + /// 200 => Content draft deleted. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpDelete] + [Route("content/{app}/{schema}/{id}/draft/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsVersionDeleteOwn)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteVersion(string app, string schema, DomainId id) + { + var command = new DeleteContentDraft { ContentId = id }; - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Delete a content item. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the content item to delete.</param> - /// <param name="request">The request parameters.</param> - /// <returns> - /// 204 => Content deleted. - /// 400 => Content cannot be deleted. - /// 404 => Content, schema or app not found. - /// </returns> - /// <remarks> - /// You can create an generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpDelete] - [Route("content/{app}/{schema}/{id}/")] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsDeleteOwn)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteContent(string app, string schema, DomainId id, DeleteContentDto request) - { - var command = request.ToCommand(id); + return Ok(response); + } + + /// <summary> + /// Delete a content item. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the content item to delete.</param> + /// <param name="request">The request parameters.</param> + /// <returns> + /// 204 => Content deleted. + /// 400 => Content cannot be deleted. + /// 404 => Content, schema or app not found. + /// </returns> + /// <remarks> + /// You can create an generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpDelete] + [Route("content/{app}/{schema}/{id}/")] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsDeleteOwn)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteContent(string app, string schema, DomainId id, DeleteContentDto request) + { + var command = request.ToCommand(id); - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - return NoContent(); - } + return NoContent(); + } - private async Task<ContentDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + private async Task<ContentDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var result = context.Result<IEnrichedContentEntity>(); - var response = ContentDto.FromDomain(result, Resources); + var result = context.Result<IEnrichedContentEntity>(); + var response = ContentDto.FromDomain(result, Resources); - return response; - } + return response; + } - private Q CreateQuery(string? ids, string? q) - { - return Q.Empty - .WithIds(ids) - .WithJsonQuery(q) - .WithODataQuery(Request.QueryString.ToString()); - } + private Q CreateQuery(string? ids, string? q) + { + return Q.Empty + .WithIds(ids) + .WithJsonQuery(q) + .WithODataQuery(Request.QueryString.ToString()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs index c2ec9c737d..c22fd94f61 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs @@ -14,135 +14,134 @@ using Squidex.Web; using Squidex.Web.GraphQL; -namespace Squidex.Areas.Api.Controllers.Contents +namespace Squidex.Areas.Api.Controllers.Contents; + +[SchemaMustBePublished] +public sealed class ContentsSharedController : ApiController { - [SchemaMustBePublished] - public sealed class ContentsSharedController : ApiController + private readonly IContentQueryService contentQuery; + private readonly IContentWorkflow contentWorkflow; + private readonly GraphQLRunner graphQLRunner; + + public ContentsSharedController(ICommandBus commandBus, + IContentQueryService contentQuery, + IContentWorkflow contentWorkflow, + GraphQLRunner graphQLRunner) + : base(commandBus) { - private readonly IContentQueryService contentQuery; - private readonly IContentWorkflow contentWorkflow; - private readonly GraphQLRunner graphQLRunner; + this.contentQuery = contentQuery; + this.contentWorkflow = contentWorkflow; + this.graphQLRunner = graphQLRunner; + } - public ContentsSharedController(ICommandBus commandBus, - IContentQueryService contentQuery, - IContentWorkflow contentWorkflow, - GraphQLRunner graphQLRunner) - : base(commandBus) - { - this.contentQuery = contentQuery; - this.contentWorkflow = contentWorkflow; - this.graphQLRunner = graphQLRunner; - } + /// <summary> + /// GraphQL endpoint. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Contents returned or mutated. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpGet] + [HttpPost] + [Route("content/{app}/graphql/")] + [Route("content/{app}/graphql/batch")] + [ApiPermissionOrAnonymous] + [ApiCosts(2)] + public Task GetGraphQL(string app) + { + return graphQLRunner.InvokeAsync(HttpContext); + } - /// <summary> - /// GraphQL endpoint. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Contents returned or mutated. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpGet] - [HttpPost] - [Route("content/{app}/graphql/")] - [Route("content/{app}/graphql/batch")] - [ApiPermissionOrAnonymous] - [ApiCosts(2)] - public Task GetGraphQL(string app) - { - return graphQLRunner.InvokeAsync(HttpContext); - } + /// <summary> + /// Queries contents. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="query">The required query object.</param> + /// <returns> + /// 200 => Contents returned. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpGet] + [Route("content/{app}/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task<IActionResult> GetAllContents(string app, AllContentsByGetDto query) + { + var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); - /// <summary> - /// Queries contents. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="query">The required query object.</param> - /// <returns> - /// 200 => Contents returned. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpGet] - [Route("content/{app}/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - public async Task<IActionResult> GetAllContents(string app, AllContentsByGetDto query) + var response = Deferred.AsyncResponse(() => { - var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); + return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); + }); - var response = Deferred.AsyncResponse(() => - { - return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); - }); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Queries contents. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="query">The required query object.</param> + /// <returns> + /// 200 => Contents returned. + /// 404 => App not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPost] + [Route("content/{app}/")] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] AllContentsByPostDto query) + { + var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); - /// <summary> - /// Queries contents. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="query">The required query object.</param> - /// <returns> - /// 200 => Contents returned. - /// 404 => App not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPost] - [Route("content/{app}/")] - [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] AllContentsByPostDto query) + var response = Deferred.AsyncResponse(() => { - var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); - - var response = Deferred.AsyncResponse(() => - { - return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); - }); + return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); + }); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Bulk update content items. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The bulk update request.</param> - /// <returns> - /// 201 => Contents created, update or delete. - /// 400 => Contents request not valid. - /// 404 => Contents references, schema or app not found. - /// </returns> - /// <remarks> - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// </remarks> - [HttpPost] - [Route("content/{app}/bulk")] - [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] - [ApiCosts(5)] - public async Task<IActionResult> BulkUpdateContents(string app, string schema, [FromBody] BulkUpdateContentsDto request) - { - var command = request.ToCommand(true); + /// <summary> + /// Bulk update content items. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The bulk update request.</param> + /// <returns> + /// 201 => Contents created, update or delete. + /// 400 => Contents request not valid. + /// 404 => Contents references, schema or app not found. + /// </returns> + /// <remarks> + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// </remarks> + [HttpPost] + [Route("content/{app}/bulk")] + [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] + [ApiCosts(5)] + public async Task<IActionResult> BulkUpdateContents(string app, string schema, [FromBody] BulkUpdateContentsDto request) + { + var command = request.ToCommand(true); - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var result = context.Result<BulkUpdateResult>(); - var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); + var result = context.Result<BulkUpdateResult>(); + var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); - return Ok(response); - } + return Ok(response); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs index 4e823cc39a..a2eff7df60 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs @@ -16,181 +16,180 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; -namespace Squidex.Areas.Api.Controllers.Contents.Generator +namespace Squidex.Areas.Api.Controllers.Contents.Generator; + +internal sealed class Builder { - internal sealed class Builder - { - public string AppName { get; } + public string AppName { get; } - public JsonSchema ChangeStatusSchema { get; } + public JsonSchema ChangeStatusSchema { get; } - public JsonSchema BulkResponseSchema { get; } + public JsonSchema BulkResponseSchema { get; } - public JsonSchema BulkRequestSchema { get; } + public JsonSchema BulkRequestSchema { get; } - public JsonSchema QuerySchema { get; } + public JsonSchema QuerySchema { get; } - public OpenApiDocument OpenApiDocument { get; } + public OpenApiDocument OpenApiDocument { get; } - public OpenApiSchemaResolver OpenApiSchemaResolver { get; } + public OpenApiSchemaResolver OpenApiSchemaResolver { get; } - internal Builder(IAppEntity app, - OpenApiDocument document, - OpenApiSchemaResolver schemaResolver, - OpenApiSchemaGenerator schemaGenerator) - { - AppName = app.Name; + internal Builder(IAppEntity app, + OpenApiDocument document, + OpenApiSchemaResolver schemaResolver, + OpenApiSchemaGenerator schemaGenerator) + { + AppName = app.Name; - OpenApiDocument = document; - OpenApiSchemaResolver = schemaResolver; + OpenApiDocument = document; + OpenApiSchemaResolver = schemaResolver; - ChangeStatusSchema = CreateSchema<ChangeStatusDto>(schemaResolver, schemaGenerator); - BulkRequestSchema = CreateSchema<BulkUpdateContentsDto>(schemaResolver, schemaGenerator); - BulkResponseSchema = CreateSchema<BulkResultDto>(schemaResolver, schemaGenerator); - QuerySchema = CreateSchema<QueryDto>(schemaResolver, schemaGenerator); - } + ChangeStatusSchema = CreateSchema<ChangeStatusDto>(schemaResolver, schemaGenerator); + BulkRequestSchema = CreateSchema<BulkUpdateContentsDto>(schemaResolver, schemaGenerator); + BulkResponseSchema = CreateSchema<BulkResultDto>(schemaResolver, schemaGenerator); + QuerySchema = CreateSchema<QueryDto>(schemaResolver, schemaGenerator); + } - private static JsonSchema CreateSchema<T>(OpenApiSchemaResolver schemaResolver, OpenApiSchemaGenerator schemaGenerator) - { - var contextualType = typeof(T).ToContextualType(); + private static JsonSchema CreateSchema<T>(OpenApiSchemaResolver schemaResolver, OpenApiSchemaGenerator schemaGenerator) + { + var contextualType = typeof(T).ToContextualType(); - return schemaGenerator.GenerateWithReference<JsonSchema>(contextualType, schemaResolver); - } + return schemaGenerator.GenerateWithReference<JsonSchema>(contextualType, schemaResolver); + } - public OperationsBuilder Shared() + public OperationsBuilder Shared() + { + var dataSchema = RegisterReference("DataDto", _ => { - var dataSchema = RegisterReference("DataDto", _ => - { - return JsonSchema.CreateAnySchema(); - }); - - var contentSchema = RegisterReference("ContentDto", _ => - { - return ContentJsonSchema.Build(dataSchema, true); - }); + return JsonSchema.CreateAnySchema(); + }); - var contentsSchema = RegisterReference("ContentResultDto", _ => - { - return BuildResult(contentSchema); - }); - - var path = $"/api/content/{AppName}"; + var contentSchema = RegisterReference("ContentDto", _ => + { + return ContentJsonSchema.Build(dataSchema, true); + }); - var builder = new OperationsBuilder - { - ContentSchema = contentSchema, - ContentsSchema = contentsSchema, - DataSchema = dataSchema, - Path = path, - Parent = this, - SchemaDisplayName = "__Shared", - SchemaName = "__Shared", - SchemaTypeName = "__Shared" - }; + var contentsSchema = RegisterReference("ContentResultDto", _ => + { + return BuildResult(contentSchema); + }); - builder.AddTag("API endpoints for operations across all schemas."); + var path = $"/api/content/{AppName}"; - return builder; - } - - public OperationsBuilder Schema(Schema schema, PartitionResolver partitionResolver, ResolvedComponents components, bool flat) + var builder = new OperationsBuilder { - var typeName = schema.TypeName(); - - var dataSchema = RegisterReference($"{typeName}DataDto", _ => - { - return schema.BuildJsonSchemaDynamic(partitionResolver, components, CreateReference, false, true); - }); + ContentSchema = contentSchema, + ContentsSchema = contentsSchema, + DataSchema = dataSchema, + Path = path, + Parent = this, + SchemaDisplayName = "__Shared", + SchemaName = "__Shared", + SchemaTypeName = "__Shared" + }; + + builder.AddTag("API endpoints for operations across all schemas."); + + return builder; + } - var contentDataSchema = dataSchema; + public OperationsBuilder Schema(Schema schema, PartitionResolver partitionResolver, ResolvedComponents components, bool flat) + { + var typeName = schema.TypeName(); - if (flat) - { - contentDataSchema = RegisterReference($"{typeName}FlatDataDto", _ => - { - return schema.BuildJsonSchemaFlat(partitionResolver, components, CreateReference, false, true); - }); - } + var dataSchema = RegisterReference($"{typeName}DataDto", _ => + { + return schema.BuildJsonSchemaDynamic(partitionResolver, components, CreateReference, false, true); + }); - var contentSchema = RegisterReference($"{typeName}ContentDto", _ => - { - return ContentJsonSchema.Build(contentDataSchema, true); - }); + var contentDataSchema = dataSchema; - var contentsSchema = RegisterReference($"{typeName}ContentResultDto", _ => + if (flat) + { + contentDataSchema = RegisterReference($"{typeName}FlatDataDto", _ => { - return BuildResult(contentSchema); + return schema.BuildJsonSchemaFlat(partitionResolver, components, CreateReference, false, true); }); - - var path = $"/api/content/{AppName}/{schema.Name}"; - - var builder = new OperationsBuilder - { - ContentSchema = contentSchema, - ContentsSchema = contentsSchema, - DataSchema = dataSchema, - Path = path, - Parent = this, - SchemaDisplayName = schema.DisplayName(), - SchemaName = schema.Name, - SchemaTypeName = typeName - }; - - builder.AddTag("API endpoints for [schema] content items."); - - return builder; } - private JsonSchema RegisterReference(string name, Func<string, JsonSchema> creator) + var contentSchema = RegisterReference($"{typeName}ContentDto", _ => { - name = char.ToUpperInvariant(name[0]) + name[1..]; + return ContentJsonSchema.Build(contentDataSchema, true); + }); - var reference = OpenApiDocument.Definitions.GetOrAdd(name, creator); + var contentsSchema = RegisterReference($"{typeName}ContentResultDto", _ => + { + return BuildResult(contentSchema); + }); - return new JsonSchema - { - Reference = reference - }; - } + var path = $"/api/content/{AppName}/{schema.Name}"; - private (JsonSchema, JsonSchema?) CreateReference(string name) + var builder = new OperationsBuilder { - name = char.ToUpperInvariant(name[0]) + name[1..]; + ContentSchema = contentSchema, + ContentsSchema = contentsSchema, + DataSchema = dataSchema, + Path = path, + Parent = this, + SchemaDisplayName = schema.DisplayName(), + SchemaName = schema.Name, + SchemaTypeName = typeName + }; + + builder.AddTag("API endpoints for [schema] content items."); + + return builder; + } - if (OpenApiDocument.Definitions.TryGetValue(name, out var definition)) - { - var reference = new JsonSchema - { - Reference = definition - }; + private JsonSchema RegisterReference(string name, Func<string, JsonSchema> creator) + { + name = char.ToUpperInvariant(name[0]) + name[1..]; - return (reference, null); - } + var reference = OpenApiDocument.Definitions.GetOrAdd(name, creator); - definition = JsonTypeBuilder.Object(); + return new JsonSchema + { + Reference = reference + }; + } - OpenApiDocument.Definitions.Add(name, definition); + private (JsonSchema, JsonSchema?) CreateReference(string name) + { + name = char.ToUpperInvariant(name[0]) + name[1..]; - return (new JsonSchema + if (OpenApiDocument.Definitions.TryGetValue(name, out var definition)) + { + var reference = new JsonSchema { Reference = definition - }, definition); + }; + + return (reference, null); } - private static JsonSchema BuildResult(JsonSchema contentSchema) + definition = JsonTypeBuilder.Object(); + + OpenApiDocument.Definitions.Add(name, definition); + + return (new JsonSchema { - return new JsonSchema + Reference = definition + }, definition); + } + + private static JsonSchema BuildResult(JsonSchema contentSchema) + { + return new JsonSchema + { + AllowAdditionalProperties = false, + Properties = { - AllowAdditionalProperties = false, - Properties = - { - ["total"] = JsonTypeBuilder.NumberProperty( - FieldDescriptions.ContentsTotal, true), - ["items"] = JsonTypeBuilder.ArrayProperty(contentSchema, - FieldDescriptions.ContentsItems, true) - }, - Type = JsonObjectType.Object - }; - } + ["total"] = JsonTypeBuilder.NumberProperty( + FieldDescriptions.ContentsTotal, true), + ["items"] = JsonTypeBuilder.ArrayProperty(contentSchema, + FieldDescriptions.ContentsItems, true) + }, + Type = JsonObjectType.Object + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs index 8aee6fce7b..258bfc6fb1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs @@ -14,147 +14,146 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Contents.Generator +namespace Squidex.Areas.Api.Controllers.Contents.Generator; + +internal sealed class OperationBuilder { - internal sealed class OperationBuilder + private readonly OpenApiOperation operation = new OpenApiOperation(); + private readonly OperationsBuilder operations; + + public OperationBuilder(OperationsBuilder operations, OpenApiOperation operation) + { + this.operations = operations; + this.operation = operation; + } + + public OperationBuilder Operation(string name) { - private readonly OpenApiOperation operation = new OpenApiOperation(); - private readonly OperationsBuilder operations; + operation.OperationId = $"{name}{operations.SchemaTypeName}Content"; - public OperationBuilder(OperationsBuilder operations, OpenApiOperation operation) + return this; + } + + public OperationBuilder OperationSummary(string summary) + { + if (!string.IsNullOrWhiteSpace(summary)) { - this.operations = operations; - this.operation = operation; + operation.Summary = operations.FormatText(summary); } - public OperationBuilder Operation(string name) - { - operation.OperationId = $"{name}{operations.SchemaTypeName}Content"; + return this; + } - return this; + public OperationBuilder Describe(string description) + { + if (!string.IsNullOrWhiteSpace(description)) + { + operation.Description = description; } - public OperationBuilder OperationSummary(string summary) - { - if (!string.IsNullOrWhiteSpace(summary)) - { - operation.Summary = operations.FormatText(summary); - } + return this; + } - return this; - } + public OperationBuilder HasId() + { + HasPath("id", JsonObjectType.String, FieldDescriptions.EntityId); - public OperationBuilder Describe(string description) - { - if (!string.IsNullOrWhiteSpace(description)) - { - operation.Description = description; - } + Responds(404, "Content item not found."); - return this; - } + return this; + } - public OperationBuilder HasId() + private OperationBuilder AddParameter(string name, JsonSchema schema, OpenApiParameterKind kind, string? description) + { + var parameter = new OpenApiParameter { - HasPath("id", JsonObjectType.String, FieldDescriptions.EntityId); - - Responds(404, "Content item not found."); + Kind = kind, + Schema = schema, + Name = name, + }; - return this; + if (!string.IsNullOrWhiteSpace(description)) + { + parameter.Description = operations.FormatText(description); } - private OperationBuilder AddParameter(string name, JsonSchema schema, OpenApiParameterKind kind, string? description) + if (kind != OpenApiParameterKind.Query) { - var parameter = new OpenApiParameter - { - Kind = kind, - Schema = schema, - Name = name, - }; + parameter.IsRequired = true; + parameter.IsNullableRaw = false; + } - if (!string.IsNullOrWhiteSpace(description)) - { - parameter.Description = operations.FormatText(description); - } + operation.Parameters.Add(parameter); - if (kind != OpenApiParameterKind.Query) - { - parameter.IsRequired = true; - parameter.IsNullableRaw = false; - } + return this; + } - operation.Parameters.Add(parameter); + public OperationBuilder HasQueryOptions(bool supportSearch) + { + operation.AddQuery(true); - return this; - } + return this; + } - public OperationBuilder HasQueryOptions(bool supportSearch) - { - operation.AddQuery(true); + public OperationBuilder Deprecated() + { + operation.IsDeprecated = true; - return this; - } + return this; + } - public OperationBuilder Deprecated() - { - operation.IsDeprecated = true; + public OperationBuilder HasQuery(string name, JsonObjectType type, string description) + { + var jsonSchema = new JsonSchema { Type = type }; - return this; - } + return AddParameter(name, jsonSchema, OpenApiParameterKind.Query, description); + } - public OperationBuilder HasQuery(string name, JsonObjectType type, string description) - { - var jsonSchema = new JsonSchema { Type = type }; + public OperationBuilder HasPath(string name, JsonObjectType type, string description, string? format = null) + { + var jsonSchema = new JsonSchema { Type = type, Format = format }; - return AddParameter(name, jsonSchema, OpenApiParameterKind.Query, description); - } + return AddParameter(name, jsonSchema, OpenApiParameterKind.Path, description); + } - public OperationBuilder HasPath(string name, JsonObjectType type, string description, string? format = null) - { - var jsonSchema = new JsonSchema { Type = type, Format = format }; + public OperationBuilder HasBody(string name, JsonSchema schema, string? description = null) + { + var jsonSchema = schema; - return AddParameter(name, jsonSchema, OpenApiParameterKind.Path, description); - } + return AddParameter(name, jsonSchema, OpenApiParameterKind.Body, description); + } - public OperationBuilder HasBody(string name, JsonSchema schema, string? description = null) + public OperationBuilder Responds(int statusCode, string description, JsonSchema? schema = null) + { + var response = new OpenApiResponse { - var jsonSchema = schema; + Description = description + }; - return AddParameter(name, jsonSchema, OpenApiParameterKind.Body, description); - } - - public OperationBuilder Responds(int statusCode, string description, JsonSchema? schema = null) + if (schema != null && statusCode == 204) { - var response = new OpenApiResponse - { - Description = description - }; + ThrowHelper.ArgumentException("Invalid status code.", nameof(statusCode)); + } - if (schema != null && statusCode == 204) - { - ThrowHelper.ArgumentException("Invalid status code.", nameof(statusCode)); - } + response.Schema = schema; - response.Schema = schema; + operation.Responses.Add(statusCode.ToString(CultureInfo.InvariantCulture), response); - operation.Responses.Add(statusCode.ToString(CultureInfo.InvariantCulture), response); + return this; + } - return this; - } + public OperationBuilder RequirePermission(string permissionId) + { + var fullId = PermissionIds.ForApp(permissionId, operations.Parent.AppName, operations.SchemaName).Id; - public OperationBuilder RequirePermission(string permissionId) + operation.Security = new List<OpenApiSecurityRequirement> { - var fullId = PermissionIds.ForApp(permissionId, operations.Parent.AppName, operations.SchemaName).Id; - - operation.Security = new List<OpenApiSecurityRequirement> + new OpenApiSecurityRequirement { - new OpenApiSecurityRequirement - { - [Constants.SecurityDefinition] = new[] { fullId } - } - }; + [Constants.SecurityDefinition] = new[] { fullId } + } + }; - return this; - } + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs index 5e15ebae1e..bf6de0d09d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs @@ -9,55 +9,54 @@ using NSwag; using Squidex.Infrastructure; -namespace Squidex.Areas.Api.Controllers.Contents.Generator +namespace Squidex.Areas.Api.Controllers.Contents.Generator; + +internal sealed class OperationsBuilder { - internal sealed class OperationsBuilder - { - public Builder Parent { get; init; } + public Builder Parent { get; init; } - public string Path { get; init; } + public string Path { get; init; } - public string SchemaName { get; init; } + public string SchemaName { get; init; } - public string SchemaTypeName { get; init; } + public string SchemaTypeName { get; init; } - public string SchemaDisplayName { get; init; } + public string SchemaDisplayName { get; init; } - public JsonSchema ContentSchema { get; init; } + public JsonSchema ContentSchema { get; init; } - public JsonSchema ContentsSchema { get; init; } + public JsonSchema ContentsSchema { get; init; } - public JsonSchema DataSchema { get; init; } + public JsonSchema DataSchema { get; init; } - public string? FormatText(string text) - { - return text?.Replace("[schema]", $"'{SchemaDisplayName}'", StringComparison.Ordinal); - } + public string? FormatText(string text) + { + return text?.Replace("[schema]", $"'{SchemaDisplayName}'", StringComparison.Ordinal); + } - public void AddTag(string description) - { - var tag = new OpenApiTag { Name = SchemaTypeName, Description = FormatText(description) }; + public void AddTag(string description) + { + var tag = new OpenApiTag { Name = SchemaTypeName, Description = FormatText(description) }; - Parent.OpenApiDocument.Tags.Add(tag); - } + Parent.OpenApiDocument.Tags.Add(tag); + } - public OperationBuilder AddOperation(string method, string path) - { - var tag = SchemaTypeName; + public OperationBuilder AddOperation(string method, string path) + { + var tag = SchemaTypeName; - var operation = new OpenApiOperation + var operation = new OpenApiOperation + { + Tags = new List<string> { - Tags = new List<string> - { - tag - } - }; + tag + } + }; - var operations = Parent.OpenApiDocument.Paths.GetOrAddNew($"{Path}{path}"); + var operations = Parent.OpenApiDocument.Paths.GetOrAddNew($"{Path}{path}"); - operations[method] = operation; + operations[method] = operation; - return new OperationBuilder(this, operation); - } + return new OperationBuilder(this, operation); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs index 5a2e5a70e5..0e6f577d51 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs @@ -19,246 +19,245 @@ using IRequestUrlGenerator = Squidex.Hosting.IUrlGenerator; using SchemaDefType = Squidex.Domain.Apps.Core.Schemas.SchemaType; -namespace Squidex.Areas.Api.Controllers.Contents.Generator +namespace Squidex.Areas.Api.Controllers.Contents.Generator; + +public sealed class SchemasOpenApiGenerator { - public sealed class SchemasOpenApiGenerator + private readonly IAppProvider appProvider; + private readonly OpenApiDocumentGeneratorSettings schemaSettings; + private readonly OpenApiSchemaGenerator schemaGenerator; + private readonly IRequestUrlGenerator urlGenerator; + private readonly IRequestCache requestCache; + + public SchemasOpenApiGenerator( + IAppProvider appProvider, + OpenApiDocumentGeneratorSettings schemaSettings, + OpenApiSchemaGenerator schemaGenerator, + IRequestUrlGenerator urlGenerator, + IRequestCache requestCache) { - private readonly IAppProvider appProvider; - private readonly OpenApiDocumentGeneratorSettings schemaSettings; - private readonly OpenApiSchemaGenerator schemaGenerator; - private readonly IRequestUrlGenerator urlGenerator; - private readonly IRequestCache requestCache; - - public SchemasOpenApiGenerator( - IAppProvider appProvider, - OpenApiDocumentGeneratorSettings schemaSettings, - OpenApiSchemaGenerator schemaGenerator, - IRequestUrlGenerator urlGenerator, - IRequestCache requestCache) - { - this.appProvider = appProvider; - this.urlGenerator = urlGenerator; - this.schemaSettings = schemaSettings; - this.schemaGenerator = schemaGenerator; - this.requestCache = requestCache; - } - - public async Task<OpenApiDocument> GenerateAsync(HttpContext httpContext, IAppEntity app, IEnumerable<ISchemaEntity> schemas, bool flat) - { - var document = CreateApiDocument(httpContext, app); - - var schemaResolver = new OpenApiSchemaResolver(document, schemaSettings); + this.appProvider = appProvider; + this.urlGenerator = urlGenerator; + this.schemaSettings = schemaSettings; + this.schemaGenerator = schemaGenerator; + this.requestCache = requestCache; + } - requestCache.AddDependency(app.UniqueId, app.Version); + public async Task<OpenApiDocument> GenerateAsync(HttpContext httpContext, IAppEntity app, IEnumerable<ISchemaEntity> schemas, bool flat) + { + var document = CreateApiDocument(httpContext, app); - foreach (var schema in schemas) - { - requestCache.AddDependency(schema.UniqueId, schema.Version); - } + var schemaResolver = new OpenApiSchemaResolver(document, schemaSettings); - var builder = new Builder(app, document, schemaResolver, schemaGenerator); + requestCache.AddDependency(app.UniqueId, app.Version); - var validSchemas = - schemas.Where(x => - x.SchemaDef.IsPublished && - x.SchemaDef.Type != SchemaDefType.Component && - x.SchemaDef.Fields.Count > 0); + foreach (var schema in schemas) + { + requestCache.AddDependency(schema.UniqueId, schema.Version); + } - var partitionResolver = app.PartitionResolver(); + var builder = new Builder(app, document, schemaResolver, schemaGenerator); - foreach (var schema in validSchemas) - { - var components = await appProvider.GetComponentsAsync(schema, httpContext.RequestAborted); + var validSchemas = + schemas.Where(x => + x.SchemaDef.IsPublished && + x.SchemaDef.Type != SchemaDefType.Component && + x.SchemaDef.Fields.Count > 0); - GenerateSchemaOperations(builder.Schema(schema.SchemaDef, partitionResolver, components, flat)); - } + var partitionResolver = app.PartitionResolver(); - GenerateSharedOperations(builder.Shared()); + foreach (var schema in validSchemas) + { + var components = await appProvider.GetComponentsAsync(schema, httpContext.RequestAborted); - var context = - new DocumentProcessorContext(document, - Enumerable.Empty<Type>(), - Enumerable.Empty<Type>(), - schemaResolver, - schemaGenerator, - schemaSettings); + GenerateSchemaOperations(builder.Schema(schema.SchemaDef, partitionResolver, components, flat)); + } - foreach (var processor in schemaSettings.DocumentProcessors) - { - processor.Process(context); - } + GenerateSharedOperations(builder.Shared()); - return document; - } + var context = + new DocumentProcessorContext(document, + Enumerable.Empty<Type>(), + Enumerable.Empty<Type>(), + schemaResolver, + schemaGenerator, + schemaSettings); - private static void GenerateSharedOperations(OperationsBuilder builder) + foreach (var processor in schemaSettings.DocumentProcessors) { - builder.AddOperation(OpenApiOperationMethod.Get, "/") - .RequirePermission(PermissionIds.AppContentsReadOwn) - .Operation("Query") - .OperationSummary("Query contents across all schemas.") - .HasQuery("ids", JsonObjectType.String, "Comma-separated list of content IDs.") - .Responds(200, "Content items retrieved.", builder.ContentsSchema) - .Responds(400, "Query not valid."); - - builder.AddOperation(OpenApiOperationMethod.Post, "/bulk") - .RequirePermission(PermissionIds.AppContentsReadOwn) - .Operation("Bulk") - .OperationSummary("Bulk update content items across all schemas.") - .HasBody("request", builder.Parent.BulkRequestSchema, null) - .Responds(200, "Contents created, update or delete.", builder.Parent.BulkResponseSchema) - .Responds(400, "Contents request not valid."); + processor.Process(context); } - private static void GenerateSchemaOperations(OperationsBuilder builder) - { - builder.AddOperation(OpenApiOperationMethod.Get, "/") - .RequirePermission(PermissionIds.AppContentsReadOwn) - .Operation("Query") - .OperationSummary("Query [schema] contents items.") - .Describe(Resources.OpenApiSchemaQuery) - .HasQueryOptions(true) - .Responds(200, "Content items retrieved.", builder.ContentsSchema) - .Responds(400, "Content query not valid."); - - builder.AddOperation(OpenApiOperationMethod.Post, "/query") - .RequirePermission(PermissionIds.AppContentsReadOwn) - .Operation("QueryPost") - .OperationSummary("Query [schema] contents items using Post.") - .HasBody("query", builder.Parent.QuerySchema, null) - .Responds(200, "Content items retrieved.", builder.ContentsSchema) - .Responds(400, "Content query not valid."); - - builder.AddOperation(OpenApiOperationMethod.Get, "/{id}") - .RequirePermission(PermissionIds.AppContentsReadOwn) - .Operation("Get") - .OperationSummary("Get a [schema] content item.") - .HasQuery("version", JsonObjectType.Number, FieldDescriptions.EntityVersion) - .HasId() - .Responds(200, "Content item returned.", builder.ContentSchema); - - builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/{version}") - .RequirePermission(PermissionIds.AppContentsReadOwn) - .Deprecated() - .Operation("GetVersioned") - .OperationSummary("Get a [schema] content item by id and version.") - .HasPath("version", JsonObjectType.Number, FieldDescriptions.EntityVersion) - .HasId() - .Responds(200, "Content item returned.", builder.DataSchema); - - builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/validity") - .RequirePermission(PermissionIds.AppContentsReadOwn) - .Operation("Validate") - .OperationSummary("Validates a [schema] content item.") - .HasId() - .Responds(200, "Content item is valid.") - .Responds(400, "Content item is not valid."); - - builder.AddOperation(OpenApiOperationMethod.Post, "/") - .RequirePermission(PermissionIds.AppContentsCreate) - .Operation("Create") - .OperationSummary("Create a [schema] content item.") - .HasQuery("publish", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPublish) - .HasQuery("id", JsonObjectType.String, FieldDescriptions.ContentRequestOptionalId) - .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) - .Responds(201, "Content item created", builder.ContentSchema) - .Responds(400, "Content data not valid."); - - builder.AddOperation(OpenApiOperationMethod.Post, "/bulk") - .RequirePermission(PermissionIds.AppContentsReadOwn) - .Operation("Bulk") - .OperationSummary("Bulk update content items.") - .HasBody("request", builder.Parent.BulkRequestSchema, null) - .Responds(200, "Contents created, update or delete.", builder.Parent.BulkResponseSchema) - .Responds(400, "Contents request not valid."); - - builder.AddOperation(OpenApiOperationMethod.Post, "/{id}") - .RequirePermission(PermissionIds.AppContentsUpsert) - .Operation("Upsert") - .OperationSummary("Upsert a [schema] content item.") - .HasQuery("patch", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPatch) - .HasQuery("publish", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPublish) - .HasId() - .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) - .Responds(200, "Content item created or updated.", builder.ContentSchema) - .Responds(400, "Content data not valid."); - - builder.AddOperation(OpenApiOperationMethod.Put, "/{id}") - .RequirePermission(PermissionIds.AppContentsUpdateOwn) - .Operation("Update") - .OperationSummary("Update a [schema] content item.") - .HasId() - .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) - .Responds(200, "Content item updated.", builder.ContentSchema) - .Responds(400, "Content data not valid."); - - builder.AddOperation(OpenApiOperationMethod.Patch, "/{id}") - .RequirePermission(PermissionIds.AppContentsUpdateOwn) - .Operation("Patch") - .OperationSummary("Patch a [schema] content item.") - .HasId() - .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) - .Responds(200, "Content item updated.", builder.ContentSchema) - .Responds(400, "Content data not valid."); - - builder.AddOperation(OpenApiOperationMethod.Put, "/{id}/status") - .RequirePermission(PermissionIds.AppContentsChangeStatusOwn) - .Operation("Change") - .OperationSummary("Change the status of a [schema] content item.") - .HasId() - .HasBody("request", builder.Parent.ChangeStatusSchema, "The request to change content status.") - .Responds(200, "Content status updated.", builder.ContentSchema) - .Responds(400, "Content status not valid."); - - builder.AddOperation(OpenApiOperationMethod.Delete, "/{id}") - .RequirePermission(PermissionIds.AppContentsDeleteOwn) - .Operation("Delete") - .OperationSummary("Delete a [schema] content item.") - .HasQuery("permanent", JsonObjectType.Boolean, FieldDescriptions.EntityRequestDeletePermanent) - .HasId() - .Responds(204, "Content item deleted"); - } + return document; + } - private OpenApiDocument CreateApiDocument(HttpContext context, IAppEntity app) - { - var appName = app.Name; + private static void GenerateSharedOperations(OperationsBuilder builder) + { + builder.AddOperation(OpenApiOperationMethod.Get, "/") + .RequirePermission(PermissionIds.AppContentsReadOwn) + .Operation("Query") + .OperationSummary("Query contents across all schemas.") + .HasQuery("ids", JsonObjectType.String, "Comma-separated list of content IDs.") + .Responds(200, "Content items retrieved.", builder.ContentsSchema) + .Responds(400, "Query not valid."); + + builder.AddOperation(OpenApiOperationMethod.Post, "/bulk") + .RequirePermission(PermissionIds.AppContentsReadOwn) + .Operation("Bulk") + .OperationSummary("Bulk update content items across all schemas.") + .HasBody("request", builder.Parent.BulkRequestSchema, null) + .Responds(200, "Contents created, update or delete.", builder.Parent.BulkResponseSchema) + .Responds(400, "Contents request not valid."); + } + + private static void GenerateSchemaOperations(OperationsBuilder builder) + { + builder.AddOperation(OpenApiOperationMethod.Get, "/") + .RequirePermission(PermissionIds.AppContentsReadOwn) + .Operation("Query") + .OperationSummary("Query [schema] contents items.") + .Describe(Resources.OpenApiSchemaQuery) + .HasQueryOptions(true) + .Responds(200, "Content items retrieved.", builder.ContentsSchema) + .Responds(400, "Content query not valid."); + + builder.AddOperation(OpenApiOperationMethod.Post, "/query") + .RequirePermission(PermissionIds.AppContentsReadOwn) + .Operation("QueryPost") + .OperationSummary("Query [schema] contents items using Post.") + .HasBody("query", builder.Parent.QuerySchema, null) + .Responds(200, "Content items retrieved.", builder.ContentsSchema) + .Responds(400, "Content query not valid."); + + builder.AddOperation(OpenApiOperationMethod.Get, "/{id}") + .RequirePermission(PermissionIds.AppContentsReadOwn) + .Operation("Get") + .OperationSummary("Get a [schema] content item.") + .HasQuery("version", JsonObjectType.Number, FieldDescriptions.EntityVersion) + .HasId() + .Responds(200, "Content item returned.", builder.ContentSchema); + + builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/{version}") + .RequirePermission(PermissionIds.AppContentsReadOwn) + .Deprecated() + .Operation("GetVersioned") + .OperationSummary("Get a [schema] content item by id and version.") + .HasPath("version", JsonObjectType.Number, FieldDescriptions.EntityVersion) + .HasId() + .Responds(200, "Content item returned.", builder.DataSchema); + + builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/validity") + .RequirePermission(PermissionIds.AppContentsReadOwn) + .Operation("Validate") + .OperationSummary("Validates a [schema] content item.") + .HasId() + .Responds(200, "Content item is valid.") + .Responds(400, "Content item is not valid."); + + builder.AddOperation(OpenApiOperationMethod.Post, "/") + .RequirePermission(PermissionIds.AppContentsCreate) + .Operation("Create") + .OperationSummary("Create a [schema] content item.") + .HasQuery("publish", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPublish) + .HasQuery("id", JsonObjectType.String, FieldDescriptions.ContentRequestOptionalId) + .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) + .Responds(201, "Content item created", builder.ContentSchema) + .Responds(400, "Content data not valid."); + + builder.AddOperation(OpenApiOperationMethod.Post, "/bulk") + .RequirePermission(PermissionIds.AppContentsReadOwn) + .Operation("Bulk") + .OperationSummary("Bulk update content items.") + .HasBody("request", builder.Parent.BulkRequestSchema, null) + .Responds(200, "Contents created, update or delete.", builder.Parent.BulkResponseSchema) + .Responds(400, "Contents request not valid."); + + builder.AddOperation(OpenApiOperationMethod.Post, "/{id}") + .RequirePermission(PermissionIds.AppContentsUpsert) + .Operation("Upsert") + .OperationSummary("Upsert a [schema] content item.") + .HasQuery("patch", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPatch) + .HasQuery("publish", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPublish) + .HasId() + .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) + .Responds(200, "Content item created or updated.", builder.ContentSchema) + .Responds(400, "Content data not valid."); + + builder.AddOperation(OpenApiOperationMethod.Put, "/{id}") + .RequirePermission(PermissionIds.AppContentsUpdateOwn) + .Operation("Update") + .OperationSummary("Update a [schema] content item.") + .HasId() + .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) + .Responds(200, "Content item updated.", builder.ContentSchema) + .Responds(400, "Content data not valid."); + + builder.AddOperation(OpenApiOperationMethod.Patch, "/{id}") + .RequirePermission(PermissionIds.AppContentsUpdateOwn) + .Operation("Patch") + .OperationSummary("Patch a [schema] content item.") + .HasId() + .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) + .Responds(200, "Content item updated.", builder.ContentSchema) + .Responds(400, "Content data not valid."); + + builder.AddOperation(OpenApiOperationMethod.Put, "/{id}/status") + .RequirePermission(PermissionIds.AppContentsChangeStatusOwn) + .Operation("Change") + .OperationSummary("Change the status of a [schema] content item.") + .HasId() + .HasBody("request", builder.Parent.ChangeStatusSchema, "The request to change content status.") + .Responds(200, "Content status updated.", builder.ContentSchema) + .Responds(400, "Content status not valid."); + + builder.AddOperation(OpenApiOperationMethod.Delete, "/{id}") + .RequirePermission(PermissionIds.AppContentsDeleteOwn) + .Operation("Delete") + .OperationSummary("Delete a [schema] content item.") + .HasQuery("permanent", JsonObjectType.Boolean, FieldDescriptions.EntityRequestDeletePermanent) + .HasId() + .Responds(204, "Content item deleted"); + } - var scheme = - string.Equals(context.Request.Scheme, "http", StringComparison.OrdinalIgnoreCase) ? - OpenApiSchema.Http : - OpenApiSchema.Https; + private OpenApiDocument CreateApiDocument(HttpContext context, IAppEntity app) + { + var appName = app.Name; + + var scheme = + string.Equals(context.Request.Scheme, "http", StringComparison.OrdinalIgnoreCase) ? + OpenApiSchema.Http : + OpenApiSchema.Https; - var document = new OpenApiDocument + var document = new OpenApiDocument + { + Schemes = new List<OpenApiSchema> { - Schemes = new List<OpenApiSchema> - { - scheme - }, - Consumes = new List<string> - { - "application/json" - }, - Produces = new List<string> - { - "application/json" - }, - Info = new OpenApiInfo - { - Title = $"Squidex Content API for '{appName}' App", - Description = - Resources.OpenApiContentDescription - .Replace("[REDOC_LINK_NORMAL]", urlGenerator.BuildUrl($"api/content/{app.Name}/docs"), StringComparison.Ordinal) - .Replace("[REDOC_LINK_SIMPLE]", urlGenerator.BuildUrl($"api/content/{app.Name}/docs/flat"), StringComparison.Ordinal) - }, - SchemaType = SchemaType.OpenApi3 - }; - - if (!string.IsNullOrWhiteSpace(context.Request.Host.Value)) + scheme + }, + Consumes = new List<string> { - document.Host = context.Request.Host.Value; - } - - return document; + "application/json" + }, + Produces = new List<string> + { + "application/json" + }, + Info = new OpenApiInfo + { + Title = $"Squidex Content API for '{appName}' App", + Description = + Resources.OpenApiContentDescription + .Replace("[REDOC_LINK_NORMAL]", urlGenerator.BuildUrl($"api/content/{app.Name}/docs"), StringComparison.Ordinal) + .Replace("[REDOC_LINK_SIMPLE]", urlGenerator.BuildUrl($"api/content/{app.Name}/docs/flat"), StringComparison.Ordinal) + }, + SchemaType = SchemaType.OpenApi3 + }; + + if (!string.IsNullOrWhiteSpace(context.Request.Host.Value)) + { + document.Host = context.Request.Host.Value; } + + return document; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs index 655e0ac1cc..2a2eab4ee0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs @@ -11,41 +11,40 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class AllContentsByGetDto { - public sealed class AllContentsByGetDto + /// <summary> + /// The list of ids to query. + /// </summary> + [FromQuery(Name = "ids")] + public string? Ids { get; set; } + + /// <summary> + /// The start of the schedule. + /// </summary> + [FromQuery] + public Instant? ScheduledFrom { get; set; } + + /// <summary> + /// The end of the schedule. + /// </summary> + [FromQuery] + public Instant? ScheduledTo { get; set; } + + public Q ToQuery() { - /// <summary> - /// The list of ids to query. - /// </summary> - [FromQuery(Name = "ids")] - public string? Ids { get; set; } - - /// <summary> - /// The start of the schedule. - /// </summary> - [FromQuery] - public Instant? ScheduledFrom { get; set; } - - /// <summary> - /// The end of the schedule. - /// </summary> - [FromQuery] - public Instant? ScheduledTo { get; set; } - - public Q ToQuery() + if (!string.IsNullOrWhiteSpace(Ids)) { - if (!string.IsNullOrWhiteSpace(Ids)) - { - return Q.Empty.WithIds(Ids); - } - - if (ScheduledFrom != null && ScheduledTo != null) - { - return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value); - } + return Q.Empty.WithIds(Ids); + } - throw new ValidationException(T.Get("contents.invalidAllQuery")); + if (ScheduledFrom != null && ScheduledTo != null) + { + return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value); } + + throw new ValidationException(T.Get("contents.invalidAllQuery")); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs index 46b597611f..675837531f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs @@ -11,38 +11,37 @@ using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class AllContentsByPostDto { - public sealed class AllContentsByPostDto + /// <summary> + /// The list of ids to query. + /// </summary> + public DomainId[]? Ids { get; set; } + + /// <summary> + /// The start of the schedule. + /// </summary> + public Instant? ScheduledFrom { get; set; } + + /// <summary> + /// The end of the schedule. + /// </summary> + public Instant? ScheduledTo { get; set; } + + public Q ToQuery() { - /// <summary> - /// The list of ids to query. - /// </summary> - public DomainId[]? Ids { get; set; } - - /// <summary> - /// The start of the schedule. - /// </summary> - public Instant? ScheduledFrom { get; set; } - - /// <summary> - /// The end of the schedule. - /// </summary> - public Instant? ScheduledTo { get; set; } - - public Q ToQuery() + if (Ids?.Length > 0) { - if (Ids?.Length > 0) - { - return Q.Empty.WithIds(Ids); - } - - if (ScheduledFrom != null && ScheduledTo != null) - { - return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value); - } + return Q.Empty.WithIds(Ids); + } - throw new ValidationException(T.Get("contents.invalidAllQuery")); + if (ScheduledFrom != null && ScheduledTo != null) + { + return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value); } + + throw new ValidationException(T.Get("contents.invalidAllQuery")); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs index c42e7d8d6b..7a03546e31 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs @@ -10,71 +10,70 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class BulkUpdateContentsDto { - public sealed class BulkUpdateContentsDto - { - /// <summary> - /// The contents to update or insert. - /// </summary> - [LocalizedRequired] - public BulkUpdateContentsJobDto[]? Jobs { get; set; } + /// <summary> + /// The contents to update or insert. + /// </summary> + [LocalizedRequired] + public BulkUpdateContentsJobDto[]? Jobs { get; set; } - /// <summary> - /// True to automatically publish the content. - /// </summary> - [Obsolete("Use 'jobs.status' fields now.")] - public bool Publish { get; set; } + /// <summary> + /// True to automatically publish the content. + /// </summary> + [Obsolete("Use 'jobs.status' fields now.")] + public bool Publish { get; set; } - /// <summary> - /// True to turn off scripting for faster inserts. Default: true. - /// </summary> - public bool DoNotScript { get; set; } = true; + /// <summary> + /// True to turn off scripting for faster inserts. Default: true. + /// </summary> + public bool DoNotScript { get; set; } = true; - /// <summary> - /// True to turn off validation for faster inserts. Default: false. - /// </summary> - public bool DoNotValidate { get; set; } + /// <summary> + /// True to turn off validation for faster inserts. Default: false. + /// </summary> + public bool DoNotValidate { get; set; } - /// <summary> - /// True to turn off validation of workflow rules. Default: false. - /// </summary> - public bool DoNotValidateWorkflow { get; set; } + /// <summary> + /// True to turn off validation of workflow rules. Default: false. + /// </summary> + public bool DoNotValidateWorkflow { get; set; } - /// <summary> - /// True to check referrers of deleted contents. - /// </summary> - public bool CheckReferrers { get; set; } + /// <summary> + /// True to check referrers of deleted contents. + /// </summary> + public bool CheckReferrers { get; set; } - /// <summary> - /// True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. - /// </summary> - public bool OptimizeValidation { get; set; } = true; + /// <summary> + /// True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. + /// </summary> + public bool OptimizeValidation { get; set; } = true; - public BulkUpdateContents ToCommand(bool setSchema) - { - var result = SimpleMapper.Map(this, new BulkUpdateContents()); + public BulkUpdateContents ToCommand(bool setSchema) + { + var result = SimpleMapper.Map(this, new BulkUpdateContents()); - result.Jobs = Jobs?.Select(x => - { - var job = x.ToJob(); + result.Jobs = Jobs?.Select(x => + { + var job = x.ToJob(); #pragma warning disable CS0618 // Type or member is obsolete - if (Publish) - { - job.Status = Status.Published; - } -#pragma warning restore CS0618 // Type or member is obsolete - - return job; - }).ToArray(); - - if (setSchema) + if (Publish) { - result.SchemaId = BulkUpdateContents.NoSchema; + job.Status = Status.Published; } +#pragma warning restore CS0618 // Type or member is obsolete - return result; + return job; + }).ToArray(); + + if (setSchema) + { + result.SchemaId = BulkUpdateContents.NoSchema; } + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs index b9d00d4d9a..7d29f59e73 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs @@ -11,68 +11,67 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public class BulkUpdateContentsJobDto { - public class BulkUpdateContentsJobDto - { - /// <summary> - /// An optional query to identify the content to update. - /// </summary> - public QueryJsonDto? Query { get; set; } + /// <summary> + /// An optional query to identify the content to update. + /// </summary> + public QueryJsonDto? Query { get; set; } - /// <summary> - /// An optional ID of the content to update. - /// </summary> - public DomainId? Id { get; set; } + /// <summary> + /// An optional ID of the content to update. + /// </summary> + public DomainId? Id { get; set; } - /// <summary> - /// The data of the content when type is set to 'Upsert', 'Create', 'Update' or 'Patch. - /// </summary> - public ContentData? Data { get; set; } + /// <summary> + /// The data of the content when type is set to 'Upsert', 'Create', 'Update' or 'Patch. + /// </summary> + public ContentData? Data { get; set; } - /// <summary> - /// The new status when the type is set to 'ChangeStatus' or 'Upsert'. - /// </summary> - public Status? Status { get; set; } + /// <summary> + /// The new status when the type is set to 'ChangeStatus' or 'Upsert'. + /// </summary> + public Status? Status { get; set; } - /// <summary> - /// The due time. - /// </summary> - public Instant? DueTime { get; set; } + /// <summary> + /// The due time. + /// </summary> + public Instant? DueTime { get; set; } - /// <summary> - /// The update type. - /// </summary> - public BulkUpdateContentType Type { get; set; } + /// <summary> + /// The update type. + /// </summary> + public BulkUpdateContentType Type { get; set; } - /// <summary> - /// The optional schema id or name. - /// </summary> - public string? Schema { get; set; } + /// <summary> + /// The optional schema id or name. + /// </summary> + public string? Schema { get; set; } - /// <summary> - /// Makes the update as patch. - /// </summary> - public bool Patch { get; set; } + /// <summary> + /// Makes the update as patch. + /// </summary> + public bool Patch { get; set; } - /// <summary> - /// True to delete the content permanently. - /// </summary> - public bool Permanent { get; set; } + /// <summary> + /// True to delete the content permanently. + /// </summary> + public bool Permanent { get; set; } - /// <summary> - /// The number of expected items. Set it to a higher number to update multiple items when a query is defined. - /// </summary> - public long ExpectedCount { get; set; } = 1; + /// <summary> + /// The number of expected items. Set it to a higher number to update multiple items when a query is defined. + /// </summary> + public long ExpectedCount { get; set; } = 1; - /// <summary> - /// The expected version. - /// </summary> - public long ExpectedVersion { get; set; } = EtagVersion.Any; + /// <summary> + /// The expected version. + /// </summary> + public long ExpectedVersion { get; set; } = EtagVersion.Any; - public BulkUpdateJob ToJob() - { - return SimpleMapper.Map(this, new BulkUpdateJob { Query = Query }); - } + public BulkUpdateJob ToJob() + { + return SimpleMapper.Map(this, new BulkUpdateJob { Query = Query }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs index a22931c299..1724fab54b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs @@ -12,29 +12,28 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class ChangeStatusDto { - public sealed class ChangeStatusDto - { - /// <summary> - /// The new status. - /// </summary> - [LocalizedRequired] - public Status Status { get; set; } + /// <summary> + /// The new status. + /// </summary> + [LocalizedRequired] + public Status Status { get; set; } - /// <summary> - /// The due time. - /// </summary> - public Instant? DueTime { get; set; } + /// <summary> + /// The due time. + /// </summary> + public Instant? DueTime { get; set; } - /// <summary> - /// True to check referrers of this content. - /// </summary> - public bool CheckReferrers { get; set; } + /// <summary> + /// True to check referrers of this content. + /// </summary> + public bool CheckReferrers { get; set; } - public ChangeContentStatus ToCommand(DomainId id) - { - return SimpleMapper.Map(this, new ChangeContentStatus { ContentId = id }); - } + public ChangeContentStatus ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new ChangeContentStatus { ContentId = id }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index dce83f2ee9..60dd562554 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -15,213 +15,212 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class ContentDto : Resource { - public sealed class ContentDto : Resource + /// <summary> + /// The if of the content item. + /// </summary> + public DomainId Id { get; set; } + + /// <summary> + /// The user that has created the content item. + /// </summary> + [LocalizedRequired] + public RefToken CreatedBy { get; set; } + + /// <summary> + /// The user that has updated the content item. + /// </summary> + [LocalizedRequired] + public RefToken LastModifiedBy { get; set; } + + /// <summary> + /// The data of the content item. + /// </summary> + [LocalizedRequired] + public object Data { get; set; } + + /// <summary> + /// The reference data for the frontend UI. + /// </summary> + public ContentData? ReferenceData { get; set; } + + /// <summary> + /// The date and time when the content item has been created. + /// </summary> + public Instant Created { get; set; } + + /// <summary> + /// The date and time when the content item has been modified last. + /// </summary> + public Instant LastModified { get; set; } + + /// <summary> + /// The status of the content. + /// </summary> + public Status Status { get; set; } + + /// <summary> + /// The new status of the content. + /// </summary> + public Status? NewStatus { get; set; } + + /// <summary> + /// The color of the status. + /// </summary> + public string StatusColor { get; set; } + + /// <summary> + /// The color of the new status. + /// </summary> + public string? NewStatusColor { get; set; } + + /// <summary> + /// The UI token. + /// </summary> + public string? EditToken { get; set; } + + /// <summary> + /// The scheduled status. + /// </summary> + public ScheduleJobDto? ScheduleJob { get; set; } + + /// <summary> + /// The ID of the schema. + /// </summary> + public DomainId SchemaId { get; set; } + + /// <summary> + /// The name of the schema. + /// </summary> + public string? SchemaName { get; set; } + + /// <summary> + /// The display name of the schema. + /// </summary> + public string? SchemaDisplayName { get; set; } + + /// <summary> + /// The reference fields. + /// </summary> + public FieldDto[]? ReferenceFields { get; set; } + + /// <summary> + /// Indicates whether the content is deleted. + /// </summary> + public bool IsDeleted { get; set; } + + /// <summary> + /// The version of the content. + /// </summary> + public long Version { get; set; } + + public static ContentDto FromDomain(IEnrichedContentEntity content, Resources resources) { - /// <summary> - /// The if of the content item. - /// </summary> - public DomainId Id { get; set; } - - /// <summary> - /// The user that has created the content item. - /// </summary> - [LocalizedRequired] - public RefToken CreatedBy { get; set; } - - /// <summary> - /// The user that has updated the content item. - /// </summary> - [LocalizedRequired] - public RefToken LastModifiedBy { get; set; } - - /// <summary> - /// The data of the content item. - /// </summary> - [LocalizedRequired] - public object Data { get; set; } - - /// <summary> - /// The reference data for the frontend UI. - /// </summary> - public ContentData? ReferenceData { get; set; } - - /// <summary> - /// The date and time when the content item has been created. - /// </summary> - public Instant Created { get; set; } - - /// <summary> - /// The date and time when the content item has been modified last. - /// </summary> - public Instant LastModified { get; set; } - - /// <summary> - /// The status of the content. - /// </summary> - public Status Status { get; set; } - - /// <summary> - /// The new status of the content. - /// </summary> - public Status? NewStatus { get; set; } - - /// <summary> - /// The color of the status. - /// </summary> - public string StatusColor { get; set; } - - /// <summary> - /// The color of the new status. - /// </summary> - public string? NewStatusColor { get; set; } - - /// <summary> - /// The UI token. - /// </summary> - public string? EditToken { get; set; } - - /// <summary> - /// The scheduled status. - /// </summary> - public ScheduleJobDto? ScheduleJob { get; set; } - - /// <summary> - /// The ID of the schema. - /// </summary> - public DomainId SchemaId { get; set; } - - /// <summary> - /// The name of the schema. - /// </summary> - public string? SchemaName { get; set; } - - /// <summary> - /// The display name of the schema. - /// </summary> - public string? SchemaDisplayName { get; set; } - - /// <summary> - /// The reference fields. - /// </summary> - public FieldDto[]? ReferenceFields { get; set; } - - /// <summary> - /// Indicates whether the content is deleted. - /// </summary> - public bool IsDeleted { get; set; } - - /// <summary> - /// The version of the content. - /// </summary> - public long Version { get; set; } - - public static ContentDto FromDomain(IEnrichedContentEntity content, Resources resources) + var response = SimpleMapper.Map(content, new ContentDto { - var response = SimpleMapper.Map(content, new ContentDto - { - SchemaId = content.SchemaId.Id, - SchemaName = content.SchemaId.Name - }); - - if (resources.Context.ShouldFlatten()) - { - response.Data = content.Data.ToFlatten(); - } - else - { - response.Data = content.Data; - } + SchemaId = content.SchemaId.Id, + SchemaName = content.SchemaId.Name + }); - if (content.ReferenceFields != null) - { - response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromDomain).ToArray(); - } - - if (content.ScheduleJob != null) - { - response.ScheduleJob = new ScheduleJobDto - { - Color = content.ScheduledStatusColor! - }; + if (resources.Context.ShouldFlatten()) + { + response.Data = content.Data.ToFlatten(); + } + else + { + response.Data = content.Data; + } - SimpleMapper.Map(content.ScheduleJob, response.ScheduleJob); - } + if (content.ReferenceFields != null) + { + response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromDomain).ToArray(); + } - if (response.IsDeleted) + if (content.ScheduleJob != null) + { + response.ScheduleJob = new ScheduleJobDto { - return response; - } + Color = content.ScheduledStatusColor! + }; - return response.CreateLinksAsync(content, resources, content.SchemaId.Name); + SimpleMapper.Map(content.ScheduleJob, response.ScheduleJob); } - private ContentDto CreateLinksAsync(IEnrichedContentEntity content, Resources resources, string schema) + if (response.IsDeleted) { - var app = resources.App; + return response; + } - var values = new { app, schema, id = Id }; + return response.CreateLinksAsync(content, resources, content.SchemaId.Name); + } - AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContent), values)); + private ContentDto CreateLinksAsync(IEnrichedContentEntity content, Resources resources, string schema) + { + var app = resources.App; - if (Version > 0) - { - var versioned = new { app, schema, values.id, version = Version - 1 }; + var values = new { app, schema, id = Id }; - AddGetLink("previous", - resources.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned)); - } + AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContent), values)); - if (NewStatus != null) - { - if (resources.CanDeleteContentVersion(schema)) - { - AddDeleteLink("draft/delete", - resources.Url<ContentsController>(x => nameof(x.DeleteVersion), values)); - } - } - else if (Status == Status.Published) - { - if (resources.CanCreateContentVersion(schema)) - { - AddPostLink("draft/create", - resources.Url<ContentsController>(x => nameof(x.CreateDraft), values)); - } - } + if (Version > 0) + { + var versioned = new { app, schema, values.id, version = Version - 1 }; - if (content.NextStatuses != null && resources.CanChangeStatus(schema)) - { - foreach (var next in content.NextStatuses) - { - AddPutLink($"status/{next.Status}", resources.Url<ContentsController>(x => nameof(x.PutContentStatus), values), next.Color); - } - } + AddGetLink("previous", + resources.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned)); + } - if (content.ScheduleJob != null && resources.CanCancelContentStatus(schema)) + if (NewStatus != null) + { + if (resources.CanDeleteContentVersion(schema)) { - AddDeleteLink($"cancel", resources.Url<ContentsController>(x => nameof(x.DeleteContentStatus), values)); + AddDeleteLink("draft/delete", + resources.Url<ContentsController>(x => nameof(x.DeleteVersion), values)); } - - if (!content.IsSingleton && resources.CanDeleteContent(schema)) + } + else if (Status == Status.Published) + { + if (resources.CanCreateContentVersion(schema)) { - AddDeleteLink("delete", - resources.Url<ContentsController>(x => nameof(x.DeleteContent), values)); + AddPostLink("draft/create", + resources.Url<ContentsController>(x => nameof(x.CreateDraft), values)); } + } - if (content.CanUpdate && resources.CanUpdateContent(schema)) + if (content.NextStatuses != null && resources.CanChangeStatus(schema)) + { + foreach (var next in content.NextStatuses) { - AddPatchLink("patch", - resources.Url<ContentsController>(x => nameof(x.PatchContent), values)); + AddPutLink($"status/{next.Status}", resources.Url<ContentsController>(x => nameof(x.PutContentStatus), values), next.Color); } + } - if (content.CanUpdate && resources.CanUpdateContent(schema)) - { - AddPutLink("update", - resources.Url<ContentsController>(x => nameof(x.PutContent), values)); - } + if (content.ScheduleJob != null && resources.CanCancelContentStatus(schema)) + { + AddDeleteLink($"cancel", resources.Url<ContentsController>(x => nameof(x.DeleteContentStatus), values)); + } - return this; + if (!content.IsSingleton && resources.CanDeleteContent(schema)) + { + AddDeleteLink("delete", + resources.Url<ContentsController>(x => nameof(x.DeleteContent), values)); } + + if (content.CanUpdate && resources.CanUpdateContent(schema)) + { + AddPatchLink("patch", + resources.Url<ContentsController>(x => nameof(x.PatchContent), values)); + } + + if (content.CanUpdate && resources.CanUpdateContent(schema)) + { + AddPutLink("update", + resources.Url<ContentsController>(x => nameof(x.PutContent), values)); + } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 79e08087ef..9909158c60 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -11,71 +11,70 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class ContentsDto : Resource { - public sealed class ContentsDto : Resource - { - /// <summary> - /// The total number of content items. - /// </summary> - public long Total { get; set; } - - /// <summary> - /// The content items. - /// </summary> - [LocalizedRequired] - public ContentDto[] Items { get; set; } - - /// <summary> - /// The possible statuses. - /// </summary> - [LocalizedRequired] - public StatusInfoDto[] Statuses { get; set; } - - public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents, Resources resources, - ISchemaEntity? schema, IContentWorkflow workflow) - { - var result = new ContentsDto - { - Total = contents.Total, - Items = contents.Select(x => ContentDto.FromDomain(x, resources)).ToArray() - }; + /// <summary> + /// The total number of content items. + /// </summary> + public long Total { get; set; } - if (schema != null) - { - await result.AssignStatusesAsync(workflow, schema); + /// <summary> + /// The content items. + /// </summary> + [LocalizedRequired] + public ContentDto[] Items { get; set; } - await result.CreateLinksAsync(resources, workflow, schema); - } + /// <summary> + /// The possible statuses. + /// </summary> + [LocalizedRequired] + public StatusInfoDto[] Statuses { get; set; } - return result; - } + public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents, Resources resources, + ISchemaEntity? schema, IContentWorkflow workflow) + { + var result = new ContentsDto + { + Total = contents.Total, + Items = contents.Select(x => ContentDto.FromDomain(x, resources)).ToArray() + }; - private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema) + if (schema != null) { - var allStatuses = await workflow.GetAllAsync(schema); + await result.AssignStatusesAsync(workflow, schema); - Statuses = allStatuses.Select(StatusInfoDto.FromDomain).ToArray(); + await result.CreateLinksAsync(resources, workflow, schema); } - private async Task CreateLinksAsync(Resources resources, IContentWorkflow workflow, ISchemaEntity schema) - { - var values = new { app = resources.App, schema = schema.SchemaDef.Name }; + return result; + } - AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContents), values)); + private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema) + { + var allStatuses = await workflow.GetAllAsync(schema); - if (resources.CanCreateContent(values.schema)) - { - AddPostLink("create", - resources.Url<ContentsController>(x => nameof(x.PostContent), values)); + Statuses = allStatuses.Select(StatusInfoDto.FromDomain).ToArray(); + } + + private async Task CreateLinksAsync(Resources resources, IContentWorkflow workflow, ISchemaEntity schema) + { + var values = new { app = resources.App, schema = schema.SchemaDef.Name }; + + AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContents), values)); - if (resources.CanChangeStatus(values.schema) && await workflow.CanPublishInitialAsync(schema, resources.Context.UserPrincipal)) - { - var publishValues = new { values.app, values.schema, publish = true }; + if (resources.CanCreateContent(values.schema)) + { + AddPostLink("create", + resources.Url<ContentsController>(x => nameof(x.PostContent), values)); + + if (resources.CanChangeStatus(values.schema) && await workflow.CanPublishInitialAsync(schema, resources.Context.UserPrincipal)) + { + var publishValues = new { values.app, values.schema, publish = true }; - AddPostLink("create/publish", - resources.Url<ContentsController>(x => nameof(x.PostContent), publishValues)); - } + AddPostLink("create/publish", + resources.Url<ContentsController>(x => nameof(x.PostContent), publishValues)); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs index e4f4513e64..dbacabff1b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/CreateContentDto.cs @@ -11,56 +11,55 @@ using Squidex.Infrastructure; using StatusType = Squidex.Domain.Apps.Core.Contents.Status; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public class CreateContentDto { - public class CreateContentDto - { - /// <summary> - /// The full data for the content item. - /// </summary> - [FromBody] - public ContentData Data { get; set; } + /// <summary> + /// The full data for the content item. + /// </summary> + [FromBody] + public ContentData Data { get; set; } - /// <summary> - /// The initial status. - /// </summary> - [FromQuery] - public StatusType? Status { get; set; } + /// <summary> + /// The initial status. + /// </summary> + [FromQuery] + public StatusType? Status { get; set; } - /// <summary> - /// The optional custom content id. - /// </summary> - [FromQuery] - public DomainId? Id { get; set; } + /// <summary> + /// The optional custom content id. + /// </summary> + [FromQuery] + public DomainId? Id { get; set; } - /// <summary> - /// True to automatically publish the content. - /// </summary> - [FromQuery] - [Obsolete("Use 'status' query string now.")] - public bool Publish { get; set; } + /// <summary> + /// True to automatically publish the content. + /// </summary> + [FromQuery] + [Obsolete("Use 'status' query string now.")] + public bool Publish { get; set; } - public CreateContent ToCommand() - { - var command = new CreateContent { Data = Data! }; + public CreateContent ToCommand() + { + var command = new CreateContent { Data = Data! }; - if (Id != null && Id.Value != default && !string.IsNullOrWhiteSpace(Id.Value.ToString())) - { - command.ContentId = Id.Value; - } + if (Id != null && Id.Value != default && !string.IsNullOrWhiteSpace(Id.Value.ToString())) + { + command.ContentId = Id.Value; + } - if (Status != null) - { - command.Status = Status; - } + if (Status != null) + { + command.Status = Status; + } #pragma warning disable CS0618 // Type or member is obsolete - else if (Publish) - { - command.Status = StatusType.Published; - } + else if (Publish) + { + command.Status = StatusType.Published; + } #pragma warning restore CS0618 // Type or member is obsolete - return command; - } + return command; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/DeleteContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/DeleteContentDto.cs index 7d2ff83fa5..36d33f30b7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/DeleteContentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/DeleteContentDto.cs @@ -10,25 +10,24 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class DeleteContentDto { - public sealed class DeleteContentDto - { - /// <summary> - /// True to check referrers of this content. - /// </summary> - [FromQuery] - public bool CheckReferrers { get; set; } + /// <summary> + /// True to check referrers of this content. + /// </summary> + [FromQuery] + public bool CheckReferrers { get; set; } - /// <summary> - /// True to delete the content permanently. - /// </summary> - [FromQuery] - public bool Permanent { get; set; } + /// <summary> + /// True to delete the content permanently. + /// </summary> + [FromQuery] + public bool Permanent { get; set; } - public DeleteContent ToCommand(DomainId id) - { - return SimpleMapper.Map(this, new DeleteContent { ContentId = id }); - } + public DeleteContent ToCommand(DomainId id) + { + return SimpleMapper.Map(this, new DeleteContent { ContentId = id }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs index b9144d3f4c..23a346af05 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs @@ -10,55 +10,54 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class ImportContentsDto { - public sealed class ImportContentsDto + /// <summary> + /// The data to import. + /// </summary> + [LocalizedRequired] + public List<ContentData> Datas { get; set; } + + /// <summary> + /// True to automatically publish the content. + /// </summary> + [Obsolete("Use bulk endpoint now.")] + public bool Publish { get; set; } + + /// <summary> + /// True to turn off scripting for faster inserts. Default: true. + /// </summary> + public bool DoNotScript { get; set; } = true; + + /// <summary> + /// True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. + /// </summary> + public bool OptimizeValidation { get; set; } = true; + + public BulkUpdateContents ToCommand() { - /// <summary> - /// The data to import. - /// </summary> - [LocalizedRequired] - public List<ContentData> Datas { get; set; } - - /// <summary> - /// True to automatically publish the content. - /// </summary> - [Obsolete("Use bulk endpoint now.")] - public bool Publish { get; set; } - - /// <summary> - /// True to turn off scripting for faster inserts. Default: true. - /// </summary> - public bool DoNotScript { get; set; } = true; + var result = SimpleMapper.Map(this, new BulkUpdateContents()); - /// <summary> - /// True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. - /// </summary> - public bool OptimizeValidation { get; set; } = true; - - public BulkUpdateContents ToCommand() + result.Jobs = Datas?.Select(x => { - var result = SimpleMapper.Map(this, new BulkUpdateContents()); - - result.Jobs = Datas?.Select(x => + var job = new BulkUpdateJob { - var job = new BulkUpdateJob - { - Type = BulkUpdateContentType.Create, - Data = x - }; + Type = BulkUpdateContentType.Create, + Data = x + }; #pragma warning disable CS0618 // Type or member is obsolete - if (Publish) - { - job.Status = Status.Published; - } + if (Publish) + { + job.Status = Status.Published; + } #pragma warning restore CS0618 // Type or member is obsolete - return job; - }).ToArray(); + return job; + }).ToArray(); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs index b50b178d5c..307234a7d4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs @@ -10,34 +10,33 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class ScheduleJobDto { - public sealed class ScheduleJobDto - { - /// <summary> - /// The ID of the schedule job. - /// </summary> - public DomainId Id { get; set; } + /// <summary> + /// The ID of the schedule job. + /// </summary> + public DomainId Id { get; set; } - /// <summary> - /// The new status. - /// </summary> - public Status Status { get; set; } + /// <summary> + /// The new status. + /// </summary> + public Status Status { get; set; } - /// <summary> - /// The target date and time when the content should be scheduled. - /// </summary> - public Instant DueTime { get; set; } + /// <summary> + /// The target date and time when the content should be scheduled. + /// </summary> + public Instant DueTime { get; set; } - /// <summary> - /// The color of the scheduled status. - /// </summary> - public string Color { get; set; } + /// <summary> + /// The color of the scheduled status. + /// </summary> + public string Color { get; set; } - /// <summary> - /// The user who schedule the content. - /// </summary> - [LocalizedRequired] - public RefToken ScheduledBy { get; set; } - } + /// <summary> + /// The user who schedule the content. + /// </summary> + [LocalizedRequired] + public RefToken ScheduledBy { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs index aae009db4c..7206b4d1e8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs @@ -8,27 +8,26 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class StatusInfoDto { - public sealed class StatusInfoDto - { - /// <summary> - /// The name of the status. - /// </summary> - [LocalizedRequired] - public Status Status { get; set; } + /// <summary> + /// The name of the status. + /// </summary> + [LocalizedRequired] + public Status Status { get; set; } - /// <summary> - /// The color of the status. - /// </summary> - [LocalizedRequired] - public string Color { get; set; } + /// <summary> + /// The color of the status. + /// </summary> + [LocalizedRequired] + public string Color { get; set; } - public static StatusInfoDto FromDomain(StatusInfo statusInfo) - { - var result = new StatusInfoDto { Status = statusInfo.Status, Color = statusInfo.Color }; + public static StatusInfoDto FromDomain(StatusInfo statusInfo) + { + var result = new StatusInfoDto { Status = statusInfo.Status, Color = statusInfo.Color }; - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs index c4eed4cdf0..96cd69c51b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs @@ -12,47 +12,46 @@ using Squidex.Infrastructure.Reflection; using StatusType = Squidex.Domain.Apps.Core.Contents.Status; -namespace Squidex.Areas.Api.Controllers.Contents.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public class UpsertContentDto { - public class UpsertContentDto + /// <summary> + /// The full data for the content item. + /// </summary> + [FromBody] + public ContentData Data { get; set; } + + /// <summary> + /// The initial status. + /// </summary> + [FromQuery] + public StatusType? Status { get; set; } + + /// <summary> + /// Makes the update as patch. + /// </summary> + [FromQuery] + public bool Patch { get; set; } + + /// <summary> + /// True to automatically publish the content. + /// </summary> + [FromQuery] + [Obsolete("Use 'status' query string now.")] + public bool Publish { get; set; } + + public UpsertContent ToCommand(DomainId id) { - /// <summary> - /// The full data for the content item. - /// </summary> - [FromBody] - public ContentData Data { get; set; } - - /// <summary> - /// The initial status. - /// </summary> - [FromQuery] - public StatusType? Status { get; set; } - - /// <summary> - /// Makes the update as patch. - /// </summary> - [FromQuery] - public bool Patch { get; set; } - - /// <summary> - /// True to automatically publish the content. - /// </summary> - [FromQuery] - [Obsolete("Use 'status' query string now.")] - public bool Publish { get; set; } - - public UpsertContent ToCommand(DomainId id) - { - var command = SimpleMapper.Map(this, new UpsertContent { ContentId = id }); + var command = SimpleMapper.Map(this, new UpsertContent { ContentId = id }); #pragma warning disable CS0618 // Type or member is obsolete - if (command.Status == null && Publish) - { - command.Status = StatusType.Published; - } + if (command.Status == null && Publish) + { + command.Status = StatusType.Published; + } #pragma warning restore CS0618 // Type or member is obsolete - return command; - } + return command; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/ContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/ContributorDto.cs index 57772ec80f..3c86e5109a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/ContributorDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/ContributorDto.cs @@ -13,109 +13,108 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class ContributorDto : Resource { - public sealed class ContributorDto : Resource + /// <summary> + /// The ID of the user that contributes to the app. + /// </summary> + [LocalizedRequired] + public string ContributorId { get; set; } + + /// <summary> + /// The display name. + /// </summary> + [LocalizedRequired] + public string ContributorName { get; set; } + + /// <summary> + /// The email address. + /// </summary> + [LocalizedRequired] + public string ContributorEmail { get; set; } + + /// <summary> + /// The role of the contributor. + /// </summary> + public string? Role { get; set; } + + public static ContributorDto FromDomain(string id, string role) { - /// <summary> - /// The ID of the user that contributes to the app. - /// </summary> - [LocalizedRequired] - public string ContributorId { get; set; } - - /// <summary> - /// The display name. - /// </summary> - [LocalizedRequired] - public string ContributorName { get; set; } - - /// <summary> - /// The email address. - /// </summary> - [LocalizedRequired] - public string ContributorEmail { get; set; } - - /// <summary> - /// The role of the contributor. - /// </summary> - public string? Role { get; set; } - - public static ContributorDto FromDomain(string id, string role) - { - var result = new ContributorDto { ContributorId = id, Role = role }; + var result = new ContributorDto { ContributorId = id, Role = role }; - return result; - } + return result; + } - public ContributorDto CreateUser(IDictionary<string, IUser> users) + public ContributorDto CreateUser(IDictionary<string, IUser> users) + { + if (users.TryGetValue(ContributorId, out var user)) + { + ContributorName = user.Claims.DisplayName()!; + ContributorEmail = user.Email; + } + else { - if (users.TryGetValue(ContributorId, out var user)) - { - ContributorName = user.Claims.DisplayName()!; - ContributorEmail = user.Email; - } - else - { - ContributorName = T.Get("common.notFoundValue"); - } + ContributorName = T.Get("common.notFoundValue"); + } + + return this; + } + public ContributorDto CreateAppLinks(Resources resources) + { + if (resources.IsUser(ContributorId)) + { return this; } - public ContributorDto CreateAppLinks(Resources resources) - { - if (resources.IsUser(ContributorId)) - { - return this; - } + var app = resources.App; - var app = resources.App; + if (resources.CanAssignContributor) + { + var values = new { app }; - if (resources.CanAssignContributor) - { - var values = new { app }; + AddPostLink("update", + resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values)); + } - AddPostLink("update", - resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values)); - } + if (resources.CanRevokeContributor) + { + var values = new { app, id = ContributorId }; - if (resources.CanRevokeContributor) - { - var values = new { app, id = ContributorId }; + AddDeleteLink("delete", + resources.Url<AppContributorsController>(x => nameof(x.DeleteContributor), values)); + } - AddDeleteLink("delete", - resources.Url<AppContributorsController>(x => nameof(x.DeleteContributor), values)); - } + return this; + } + public ContributorDto CreateTeamLinks(Resources resources) + { + if (resources.IsUser(ContributorId)) + { return this; } - public ContributorDto CreateTeamLinks(Resources resources) - { - if (resources.IsUser(ContributorId)) - { - return this; - } - - var team = resources.Team; - - if (resources.CanAssignTeamContributor) - { - var values = new { team }; + var team = resources.Team; - AddPostLink("update", - resources.Url<TeamContributorsController>(x => nameof(x.PostContributor), values)); - } + if (resources.CanAssignTeamContributor) + { + var values = new { team }; - if (resources.CanRevokeTeamContributor) - { - var values = new { team, id = ContributorId }; + AddPostLink("update", + resources.Url<TeamContributorsController>(x => nameof(x.PostContributor), values)); + } - AddDeleteLink("delete", - resources.Url<TeamContributorsController>(x => nameof(x.DeleteContributor), values)); - } + if (resources.CanRevokeTeamContributor) + { + var values = new { team, id = ContributorId }; - return this; + AddDeleteLink("delete", + resources.Url<TeamContributorsController>(x => nameof(x.DeleteContributor), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/ContributorsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/ContributorsDto.cs index f2532f7671..cd0319aea8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/ContributorsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/ContributorsDto.cs @@ -15,110 +15,109 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class ContributorsDto : Resource { - public sealed class ContributorsDto : Resource + /// <summary> + /// The contributors. + /// </summary> + [LocalizedRequired] + public ContributorDto[] Items { get; set; } + + /// <summary> + /// The maximum number of allowed contributors. + /// </summary> + public long MaxContributors { get; set; } + + /// <summary> + /// The metadata to provide information about this request. + /// </summary> + [JsonPropertyName("_meta")] + public ContributorsMetadata? Metadata { get; set; } + + public static async Task<ContributorsDto> FromDomainAsync(IAppEntity app, Resources resources, IUserResolver userResolver, Plan plan, bool invited) { - /// <summary> - /// The contributors. - /// </summary> - [LocalizedRequired] - public ContributorDto[] Items { get; set; } - - /// <summary> - /// The maximum number of allowed contributors. - /// </summary> - public long MaxContributors { get; set; } - - /// <summary> - /// The metadata to provide information about this request. - /// </summary> - [JsonPropertyName("_meta")] - public ContributorsMetadata? Metadata { get; set; } - - public static async Task<ContributorsDto> FromDomainAsync(IAppEntity app, Resources resources, IUserResolver userResolver, Plan plan, bool invited) - { - var users = await userResolver.QueryManyAsync(app.Contributors.Keys.ToArray()); - - var result = new ContributorsDto - { - Items = app.Contributors - .Select(x => ContributorDto.FromDomain(x.Key, x.Value)) - .Select(x => x.CreateUser(users)) - .Select(x => x.CreateAppLinks(resources)) - .OrderBy(x => x.ContributorName) - .ToArray() - }; + var users = await userResolver.QueryManyAsync(app.Contributors.Keys.ToArray()); - result.CreateInvited(invited); - result.CreatePlan(plan); + var result = new ContributorsDto + { + Items = app.Contributors + .Select(x => ContributorDto.FromDomain(x.Key, x.Value)) + .Select(x => x.CreateUser(users)) + .Select(x => x.CreateAppLinks(resources)) + .OrderBy(x => x.ContributorName) + .ToArray() + }; + + result.CreateInvited(invited); + result.CreatePlan(plan); + + return result.CreateAppLinks(resources); + } - return result.CreateAppLinks(resources); - } + public static async Task<ContributorsDto> FromDomainAsync(ITeamEntity team, Resources resources, IUserResolver userResolver, bool invited) + { + var users = await userResolver.QueryManyAsync(team.Contributors.Keys.ToArray()); - public static async Task<ContributorsDto> FromDomainAsync(ITeamEntity team, Resources resources, IUserResolver userResolver, bool invited) + var result = new ContributorsDto { - var users = await userResolver.QueryManyAsync(team.Contributors.Keys.ToArray()); - - var result = new ContributorsDto - { - Items = team.Contributors - .Select(x => ContributorDto.FromDomain(x.Key, x.Value)) - .Select(x => x.CreateUser(users)) - .Select(x => x.CreateTeamLinks(resources)) - .OrderBy(x => x.ContributorName) - .ToArray() - }; + Items = team.Contributors + .Select(x => ContributorDto.FromDomain(x.Key, x.Value)) + .Select(x => x.CreateUser(users)) + .Select(x => x.CreateTeamLinks(resources)) + .OrderBy(x => x.ContributorName) + .ToArray() + }; - result.CreateInvited(invited); + result.CreateInvited(invited); - return result.CreateTeamLinks(resources); - } + return result.CreateTeamLinks(resources); + } - private void CreatePlan(Plan plan) - { - MaxContributors = plan.MaxContributors; - } + private void CreatePlan(Plan plan) + { + MaxContributors = plan.MaxContributors; + } - private void CreateInvited(bool isInvited) + private void CreateInvited(bool isInvited) + { + if (isInvited) { - if (isInvited) + Metadata = new ContributorsMetadata { - Metadata = new ContributorsMetadata - { - IsInvited = "true" - }; - } + IsInvited = "true" + }; } + } - private ContributorsDto CreateAppLinks(Resources resources) - { - var values = new { app = resources.App }; - - AddSelfLink(resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); + private ContributorsDto CreateAppLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanAssignContributor && (MaxContributors < 0 || Items.Length < MaxContributors)) - { - AddPostLink("create", - resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values)); - } + AddSelfLink(resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); - return this; + if (resources.CanAssignContributor && (MaxContributors < 0 || Items.Length < MaxContributors)) + { + AddPostLink("create", + resources.Url<AppContributorsController>(x => nameof(x.PostContributor), values)); } - private ContributorsDto CreateTeamLinks(Resources resources) - { - var values = new { team = resources.Team }; + return this; + } - AddSelfLink(resources.Url<TeamContributorsController>(x => nameof(x.GetContributors), values)); + private ContributorsDto CreateTeamLinks(Resources resources) + { + var values = new { team = resources.Team }; - if (resources.CanAssignTeamContributor) - { - AddPostLink("create", - resources.Url<TeamContributorsController>(x => nameof(x.PostContributor), values)); - } + AddSelfLink(resources.Url<TeamContributorsController>(x => nameof(x.GetContributors), values)); - return this; + if (resources.CanAssignTeamContributor) + { + AddPostLink("create", + resources.Url<TeamContributorsController>(x => nameof(x.PostContributor), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/ContributorsMetadata.cs b/backend/src/Squidex/Areas/Api/Controllers/ContributorsMetadata.cs index fd9a00ec53..1f766f5eec 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/ContributorsMetadata.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/ContributorsMetadata.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class ContributorsMetadata { - public sealed class ContributorsMetadata - { - /// <summary> - /// Indicates whether the user has been invited. - /// </summary> - public string IsInvited { get; set; } - } + /// <summary> + /// Indicates whether the user has been invited. + /// </summary> + public string IsInvited { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs index 9bb17a37d9..c1f9e6269b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs @@ -11,64 +11,63 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Diagnostics +namespace Squidex.Areas.Api.Controllers.Diagnostics; + +/// <summary> +/// Makes a diagnostics request. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Diagnostics))] +public sealed class DiagnosticsController : ApiController { + private readonly Diagnoser dumper; + + public DiagnosticsController(ICommandBus commandBus, Diagnoser dumper) + : base(commandBus) + { + this.dumper = dumper; + } + /// <summary> - /// Makes a diagnostics request. + /// Creates a dump and writes it into storage.. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Diagnostics))] - public sealed class DiagnosticsController : ApiController + /// <returns> + /// 204 => Dump created successful. + /// 501 => Not configured. + /// </returns> + [HttpGet] + [Route("diagnostics/dump")] + [ApiPermissionOrAnonymous(PermissionIds.Admin)] + public async Task<IActionResult> GetDump() { - private readonly Diagnoser dumper; + var success = await dumper.CreateDumpAsync(HttpContext.RequestAborted); - public DiagnosticsController(ICommandBus commandBus, Diagnoser dumper) - : base(commandBus) + if (!success) { - this.dumper = dumper; + return StatusCode(501); } - /// <summary> - /// Creates a dump and writes it into storage.. - /// </summary> - /// <returns> - /// 204 => Dump created successful. - /// 501 => Not configured. - /// </returns> - [HttpGet] - [Route("diagnostics/dump")] - [ApiPermissionOrAnonymous(PermissionIds.Admin)] - public async Task<IActionResult> GetDump() - { - var success = await dumper.CreateDumpAsync(HttpContext.RequestAborted); - - if (!success) - { - return StatusCode(501); - } + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Creates a gc dump and writes it into storage. + /// </summary> + /// <returns> + /// 204 => Dump created successful. + /// 501 => Not configured. + /// </returns> + [HttpGet] + [Route("diagnostics/gcdump")] + [ApiPermissionOrAnonymous(PermissionIds.Admin)] + public async Task<IActionResult> GetGCDump() + { + var success = await dumper.CreateGCDumpAsync(HttpContext.RequestAborted); - /// <summary> - /// Creates a gc dump and writes it into storage. - /// </summary> - /// <returns> - /// 204 => Dump created successful. - /// 501 => Not configured. - /// </returns> - [HttpGet] - [Route("diagnostics/gcdump")] - [ApiPermissionOrAnonymous(PermissionIds.Admin)] - public async Task<IActionResult> GetGCDump() + if (!success) { - var success = await dumper.CreateGCDumpAsync(HttpContext.RequestAborted); - - if (!success) - { - return StatusCode(501); - } - - return NoContent(); + return StatusCode(501); } + + return NoContent(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs index 9fe8e9f010..49bfe771de 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs @@ -9,25 +9,24 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Docs +namespace Squidex.Areas.Api.Controllers.Docs; + +public sealed class DocsController : ApiController { - public sealed class DocsController : ApiController + public DocsController(ICommandBus commandBus) + : base(commandBus) { - public DocsController(ICommandBus commandBus) - : base(commandBus) - { - } + } - [HttpGet] - [Route("docs/")] - public IActionResult Docs() + [HttpGet] + [Route("docs/")] + public IActionResult Docs() + { + var vm = new DocsVM { - var vm = new DocsVM - { - Specification = "~/api/swagger/v1/swagger.json" - }; + Specification = "~/api/swagger/v1/swagger.json" + }; - return View(nameof(Docs), vm); - } + return View(nameof(Docs), vm); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/DocsVM.cs b/backend/src/Squidex/Areas/Api/Controllers/DocsVM.cs index c616c74c62..8c082c33c4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/DocsVM.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/DocsVM.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class DocsVM { - public sealed class DocsVM - { - public string Specification { get; set; } - } + public string Specification { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index ee20802237..cfc3e498fc 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -12,102 +12,101 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.EventConsumers +namespace Squidex.Areas.Api.Controllers.EventConsumers; + +/// <summary> +/// Update and query event consumers. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(EventConsumers))] +public sealed class EventConsumersController : ApiController { + private readonly IEventConsumerManager eventConsumerManager; + + public EventConsumersController(ICommandBus commandBus, IEventConsumerManager eventConsumerManager) + : base(commandBus) + { + this.eventConsumerManager = eventConsumerManager; + } + + /// <summary> + /// Get event consumers. + /// </summary> + /// <returns> + /// 200 => Event consumers returned. + /// </returns> + [HttpGet] + [Route("event-consumers/")] + [ProducesResponseType(typeof(EventConsumersDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminEventsRead)] + public async Task<IActionResult> GetEventConsumers() + { + var eventConsumers = await eventConsumerManager.GetConsumersAsync(HttpContext.RequestAborted); + + var response = EventConsumersDto.FromDomain(eventConsumers, Resources); + + return Ok(response); + } + + /// <summary> + /// Start an event consumer. + /// </summary> + /// <param name="consumerName">The name of the event consumer.</param> + /// <returns> + /// 200 => Event consumer started asynchronously. + /// 404 => Event consumer not found. + /// </returns> + [HttpPut] + [Route("event-consumers/{consumerName}/start/")] + [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminEventsManage)] + public async Task<IActionResult> StartEventConsumer(string consumerName) + { + var eventConsumer = await eventConsumerManager.StartAsync(consumerName, HttpContext.RequestAborted); + + var response = EventConsumerDto.FromDomain(eventConsumer, Resources); + + return Ok(response); + } + + /// <summary> + /// Stop an event consumer. + /// </summary> + /// <param name="consumerName">The name of the event consumer.</param> + /// <returns> + /// 200 => Event consumer stopped asynchronously. + /// 404 => Event consumer not found. + /// </returns> + [HttpPut] + [Route("event-consumers/{consumerName}/stop/")] + [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminEventsManage)] + public async Task<IActionResult> StopEventConsumer(string consumerName) + { + var eventConsumer = await eventConsumerManager.StopAsync(consumerName, HttpContext.RequestAborted); + + var response = EventConsumerDto.FromDomain(eventConsumer, Resources); + + return Ok(response); + } + /// <summary> - /// Update and query event consumers. + /// Reset an event consumer. /// </summary> - [ApiExplorerSettings(GroupName = nameof(EventConsumers))] - public sealed class EventConsumersController : ApiController + /// <param name="consumerName">The name of the event consumer.</param> + /// <returns> + /// 200 => Event consumer resetted asynchronously. + /// 404 => Event consumer not found. + /// </returns> + [HttpPut] + [Route("event-consumers/{consumerName}/reset/")] + [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminEventsManage)] + public async Task<IActionResult> ResetEventConsumer(string consumerName) { - private readonly IEventConsumerManager eventConsumerManager; - - public EventConsumersController(ICommandBus commandBus, IEventConsumerManager eventConsumerManager) - : base(commandBus) - { - this.eventConsumerManager = eventConsumerManager; - } - - /// <summary> - /// Get event consumers. - /// </summary> - /// <returns> - /// 200 => Event consumers returned. - /// </returns> - [HttpGet] - [Route("event-consumers/")] - [ProducesResponseType(typeof(EventConsumersDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminEventsRead)] - public async Task<IActionResult> GetEventConsumers() - { - var eventConsumers = await eventConsumerManager.GetConsumersAsync(HttpContext.RequestAborted); - - var response = EventConsumersDto.FromDomain(eventConsumers, Resources); - - return Ok(response); - } - - /// <summary> - /// Start an event consumer. - /// </summary> - /// <param name="consumerName">The name of the event consumer.</param> - /// <returns> - /// 200 => Event consumer started asynchronously. - /// 404 => Event consumer not found. - /// </returns> - [HttpPut] - [Route("event-consumers/{consumerName}/start/")] - [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminEventsManage)] - public async Task<IActionResult> StartEventConsumer(string consumerName) - { - var eventConsumer = await eventConsumerManager.StartAsync(consumerName, HttpContext.RequestAborted); - - var response = EventConsumerDto.FromDomain(eventConsumer, Resources); - - return Ok(response); - } - - /// <summary> - /// Stop an event consumer. - /// </summary> - /// <param name="consumerName">The name of the event consumer.</param> - /// <returns> - /// 200 => Event consumer stopped asynchronously. - /// 404 => Event consumer not found. - /// </returns> - [HttpPut] - [Route("event-consumers/{consumerName}/stop/")] - [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminEventsManage)] - public async Task<IActionResult> StopEventConsumer(string consumerName) - { - var eventConsumer = await eventConsumerManager.StopAsync(consumerName, HttpContext.RequestAborted); - - var response = EventConsumerDto.FromDomain(eventConsumer, Resources); - - return Ok(response); - } - - /// <summary> - /// Reset an event consumer. - /// </summary> - /// <param name="consumerName">The name of the event consumer.</param> - /// <returns> - /// 200 => Event consumer resetted asynchronously. - /// 404 => Event consumer not found. - /// </returns> - [HttpPut] - [Route("event-consumers/{consumerName}/reset/")] - [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminEventsManage)] - public async Task<IActionResult> ResetEventConsumer(string consumerName) - { - var eventConsumer = await eventConsumerManager.ResetAsync(consumerName, HttpContext.RequestAborted); - - var response = EventConsumerDto.FromDomain(eventConsumer, Resources); - - return Ok(response); - } + var eventConsumer = await eventConsumerManager.ResetAsync(consumerName, HttpContext.RequestAborted); + + var response = EventConsumerDto.FromDomain(eventConsumer, Resources); + + return Ok(response); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs index 05db4eb50e..58ceec1b3a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs @@ -10,73 +10,72 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.EventConsumers.Models +namespace Squidex.Areas.Api.Controllers.EventConsumers.Models; + +public sealed class EventConsumerDto : Resource { - public sealed class EventConsumerDto : Resource - { - /// <summary> - /// Indicates if the event consumer has been started. - /// </summary> - public bool IsStopped { get; set; } + /// <summary> + /// Indicates if the event consumer has been started. + /// </summary> + public bool IsStopped { get; set; } - /// <summary> - /// Indicates if the event consumer is resetting at the moment. - /// </summary> - public bool IsResetting { get; set; } + /// <summary> + /// Indicates if the event consumer is resetting at the moment. + /// </summary> + public bool IsResetting { get; set; } - /// <summary> - /// The number of handled events. - /// </summary> - public int Count { get; set; } + /// <summary> + /// The number of handled events. + /// </summary> + public int Count { get; set; } - /// <summary> - /// The name of the event consumer. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The name of the event consumer. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - /// <summary> - /// The error details if the event consumer has been stopped after a failure. - /// </summary> - public string? Error { get; set; } + /// <summary> + /// The error details if the event consumer has been stopped after a failure. + /// </summary> + public string? Error { get; set; } - /// <summary> - /// The position within the vent stream. - /// </summary> - public string? Position { get; set; } + /// <summary> + /// The position within the vent stream. + /// </summary> + public string? Position { get; set; } - public static EventConsumerDto FromDomain(EventConsumerInfo eventConsumerInfo, Resources resources) - { - var result = SimpleMapper.Map(eventConsumerInfo, new EventConsumerDto()); + public static EventConsumerDto FromDomain(EventConsumerInfo eventConsumerInfo, Resources resources) + { + var result = SimpleMapper.Map(eventConsumerInfo, new EventConsumerDto()); - return result.CreateLinks(resources); - } + return result.CreateLinks(resources); + } - private EventConsumerDto CreateLinks(Resources resources) + private EventConsumerDto CreateLinks(Resources resources) + { + if (resources.CanManageEvents) { - if (resources.CanManageEvents) - { - var values = new { consumerName = Name }; + var values = new { consumerName = Name }; - if (!IsResetting) - { - AddPutLink("reset", - resources.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values)); - } - - if (IsStopped) - { - AddPutLink("start", - resources.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values)); - } - else - { - AddPutLink("stop", - resources.Url<EventConsumersController>(x => nameof(x.StopEventConsumer), values)); - } + if (!IsResetting) + { + AddPutLink("reset", + resources.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values)); } - return this; + if (IsStopped) + { + AddPutLink("start", + resources.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values)); + } + else + { + AddPutLink("stop", + resources.Url<EventConsumersController>(x => nameof(x.StopEventConsumer), values)); + } } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs index 1c3ece7c73..dd60d60dd1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs @@ -8,30 +8,29 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.EventConsumers.Models +namespace Squidex.Areas.Api.Controllers.EventConsumers.Models; + +public sealed class EventConsumersDto : Resource { - public sealed class EventConsumersDto : Resource - { - /// <summary> - /// The event consumers. - /// </summary> - public EventConsumerDto[] Items { get; set; } + /// <summary> + /// The event consumers. + /// </summary> + public EventConsumerDto[] Items { get; set; } - public static EventConsumersDto FromDomain(IEnumerable<EventConsumerInfo> items, Resources resources) + public static EventConsumersDto FromDomain(IEnumerable<EventConsumerInfo> items, Resources resources) + { + var result = new EventConsumersDto { - var result = new EventConsumersDto - { - Items = items.Select(x => EventConsumerDto.FromDomain(x, resources)).ToArray() - }; + Items = items.Select(x => EventConsumerDto.FromDomain(x, resources)).ToArray() + }; - return result.CreateLinks(resources); - } + return result.CreateLinks(resources); + } - private EventConsumersDto CreateLinks(Resources resources) - { - AddSelfLink(resources.Url<EventConsumersController>(c => nameof(c.GetEventConsumers))); + private EventConsumersDto CreateLinks(Resources resources) + { + AddSelfLink(resources.Url<EventConsumersController>(c => nameof(c.GetEventConsumers))); - return this; - } + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index 76ad219c14..7c08306446 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -12,66 +12,65 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.History +namespace Squidex.Areas.Api.Controllers.History; + +/// <summary> +/// Readonly API to get an event stream. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(History))] +public sealed class HistoryController : ApiController { + private readonly IHistoryService historyService; + + public HistoryController(ICommandBus commandBus, IHistoryService historyService) + : base(commandBus) + { + this.historyService = historyService; + } + /// <summary> - /// Readonly API to get an event stream. + /// Get historical events. /// </summary> - [ApiExplorerSettings(GroupName = nameof(History))] - public sealed class HistoryController : ApiController + /// <param name="app">The name of the app.</param> + /// <param name="channel">The name of the channel.</param> + /// <returns> + /// 200 => Events returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/history/")] + [ProducesResponseType(typeof(HistoryEventDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppHistory)] + [ApiCosts(0.1)] + public async Task<IActionResult> GetAppHistory(string app, string channel) { - private readonly IHistoryService historyService; - - public HistoryController(ICommandBus commandBus, IHistoryService historyService) - : base(commandBus) - { - this.historyService = historyService; - } + var events = await historyService.QueryByChannelAsync(AppId, channel, 100, HttpContext.RequestAborted); - /// <summary> - /// Get historical events. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="channel">The name of the channel.</param> - /// <returns> - /// 200 => Events returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/history/")] - [ProducesResponseType(typeof(HistoryEventDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppHistory)] - [ApiCosts(0.1)] - public async Task<IActionResult> GetAppHistory(string app, string channel) - { - var events = await historyService.QueryByChannelAsync(AppId, channel, 100, HttpContext.RequestAborted); + var response = events.Select(HistoryEventDto.FromDomain).Where(x => x.Message != null).ToArray(); - var response = events.Select(HistoryEventDto.FromDomain).Where(x => x.Message != null).ToArray(); - - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Get historical events for a team. - /// </summary> - /// <param name="team">The ID of the team.</param> - /// <param name="channel">The name of the channel.</param> - /// <returns> - /// 200 => Events returned. - /// 404 => Team not found. - /// </returns> - [HttpGet] - [Route("teams/{team}/history/")] - [ProducesResponseType(typeof(HistoryEventDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamHistory)] - [ApiCosts(0.1)] - public async Task<IActionResult> GetTeamHistory(string team, string channel) - { - var events = await historyService.QueryByChannelAsync(TeamId, channel, 100, HttpContext.RequestAborted); + /// <summary> + /// Get historical events for a team. + /// </summary> + /// <param name="team">The ID of the team.</param> + /// <param name="channel">The name of the channel.</param> + /// <returns> + /// 200 => Events returned. + /// 404 => Team not found. + /// </returns> + [HttpGet] + [Route("teams/{team}/history/")] + [ProducesResponseType(typeof(HistoryEventDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamHistory)] + [ApiCosts(0.1)] + public async Task<IActionResult> GetTeamHistory(string team, string channel) + { + var events = await historyService.QueryByChannelAsync(TeamId, channel, 100, HttpContext.RequestAborted); - var response = events.Select(HistoryEventDto.FromDomain).Where(x => x.Message != null).ToArray(); + var response = events.Select(HistoryEventDto.FromDomain).Where(x => x.Message != null).ToArray(); - return Ok(response); - } + return Ok(response); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs index 404b344527..2d20248b36 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs @@ -11,48 +11,47 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.History.Models +namespace Squidex.Areas.Api.Controllers.History.Models; + +public sealed class HistoryEventDto { - public sealed class HistoryEventDto + /// <summary> + /// The message for the event. + /// </summary> + [LocalizedRequired] + public string Message { get; set; } + + /// <summary> + /// The type of the original event. + /// </summary> + [LocalizedRequired] + public string EventType { get; set; } + + /// <summary> + /// The user who called the action. + /// </summary> + [LocalizedRequired] + public string Actor { get; set; } + + /// <summary> + /// Gets a unique id for the event. + /// </summary> + public DomainId EventId { get; set; } + + /// <summary> + /// The time when the event happened. + /// </summary> + public Instant Created { get; set; } + + /// <summary> + /// The version identifier. + /// </summary> + public long Version { get; set; } + + public static HistoryEventDto FromDomain(ParsedHistoryEvent historyEvent) { - /// <summary> - /// The message for the event. - /// </summary> - [LocalizedRequired] - public string Message { get; set; } - - /// <summary> - /// The type of the original event. - /// </summary> - [LocalizedRequired] - public string EventType { get; set; } - - /// <summary> - /// The user who called the action. - /// </summary> - [LocalizedRequired] - public string Actor { get; set; } - - /// <summary> - /// Gets a unique id for the event. - /// </summary> - public DomainId EventId { get; set; } - - /// <summary> - /// The time when the event happened. - /// </summary> - public Instant Created { get; set; } - - /// <summary> - /// The version identifier. - /// </summary> - public long Version { get; set; } - - public static HistoryEventDto FromDomain(ParsedHistoryEvent historyEvent) - { - var result = SimpleMapper.Map(historyEvent, new HistoryEventDto { EventId = historyEvent.Id }); - - return result; - } + var result = SimpleMapper.Map(historyEvent, new HistoryEventDto { EventId = historyEvent.Id }); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/LanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/LanguageDto.cs index 4354e1ee39..69737fcb76 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/LanguageDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/LanguageDto.cs @@ -9,33 +9,32 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class LanguageDto { - public sealed class LanguageDto - { - /// <summary> - /// The iso code of the language. - /// </summary> - [LocalizedRequired] - public string Iso2Code { get; set; } + /// <summary> + /// The iso code of the language. + /// </summary> + [LocalizedRequired] + public string Iso2Code { get; set; } - /// <summary> - /// The english name of the language. - /// </summary> - [LocalizedRequired] - public string EnglishName { get; set; } + /// <summary> + /// The english name of the language. + /// </summary> + [LocalizedRequired] + public string EnglishName { get; set; } - /// <summary> - /// The native name of the language. - /// </summary> - [LocalizedRequired] - public string NativeName { get; set; } + /// <summary> + /// The native name of the language. + /// </summary> + [LocalizedRequired] + public string NativeName { get; set; } - public static LanguageDto FromDomain(Language language) - { - var result = SimpleMapper.Map(language, new LanguageDto()); + public static LanguageDto FromDomain(Language language) + { + var result = SimpleMapper.Map(language, new LanguageDto()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs index 879902d63b..94cbfc60f4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs @@ -11,42 +11,41 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Languages +namespace Squidex.Areas.Api.Controllers.Languages; + +/// <summary> +/// Readonly API for supported languages. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Languages))] +public sealed class LanguagesController : ApiController { + public LanguagesController(ICommandBus commandBus) + : base(commandBus) + { + } + /// <summary> - /// Readonly API for supported languages. + /// Get supported languages. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Languages))] - public sealed class LanguagesController : ApiController + /// <remarks> + /// Provide a list of supported language codes, following the ISO2Code standard. + /// </remarks> + /// <returns> + /// 200 => Supported language codes returned. + /// </returns> + [HttpGet] + [Route("languages/")] + [ProducesResponseType(typeof(LanguageDto[]), StatusCodes.Status200OK)] + [ApiPermission] + public IActionResult GetLanguages() { - public LanguagesController(ICommandBus commandBus) - : base(commandBus) - { - } - - /// <summary> - /// Get supported languages. - /// </summary> - /// <remarks> - /// Provide a list of supported language codes, following the ISO2Code standard. - /// </remarks> - /// <returns> - /// 200 => Supported language codes returned. - /// </returns> - [HttpGet] - [Route("languages/")] - [ProducesResponseType(typeof(LanguageDto[]), StatusCodes.Status200OK)] - [ApiPermission] - public IActionResult GetLanguages() + var response = Deferred.Response(() => { - var response = Deferred.Response(() => - { - return Language.AllLanguages.Select(LanguageDto.FromDomain).ToArray(); - }); + return Language.AllLanguages.Select(LanguageDto.FromDomain).ToArray(); + }); - Response.Headers[HeaderNames.ETag] = "1"; + Response.Headers[HeaderNames.ETag] = "1"; - return Ok(response); - } + return Ok(response); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs index 839eb54964..80c66a9942 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs @@ -7,20 +7,19 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.News.Models +namespace Squidex.Areas.Api.Controllers.News.Models; + +public sealed class FeatureDto { - public sealed class FeatureDto - { - /// <summary> - /// The name of the feature. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The name of the feature. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - /// <summary> - /// The description text. - /// </summary> - [LocalizedRequired] - public string Text { get; set; } - } + /// <summary> + /// The description text. + /// </summary> + [LocalizedRequired] + public string Text { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs index 28307a0d67..f25f84f516 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/Models/FeaturesDto.cs @@ -7,19 +7,18 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.News.Models +namespace Squidex.Areas.Api.Controllers.News.Models; + +public class FeaturesDto { - public class FeaturesDto - { - /// <summary> - /// The latest features. - /// </summary> - [LocalizedRequired] - public List<FeatureDto> Features { get; } = new List<FeatureDto>(); + /// <summary> + /// The latest features. + /// </summary> + [LocalizedRequired] + public List<FeatureDto> Features { get; } = new List<FeatureDto>(); - /// <summary> - /// The recent version. - /// </summary> - public int Version { get; set; } - } + /// <summary> + /// The recent version. + /// </summary> + public int Version { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs index 6467423d16..eca4806d34 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs @@ -5,22 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.News +namespace Squidex.Areas.Api.Controllers.News; + +public sealed class MyNewsOptions { - public sealed class MyNewsOptions - { - public string AppName { get; set; } + public string AppName { get; set; } - public string ClientId { get; set; } + public string ClientId { get; set; } - public string ClientSecret { get; set; } + public string ClientSecret { get; set; } - public bool IsConfigured() - { - return - !string.IsNullOrWhiteSpace(AppName) && - !string.IsNullOrWhiteSpace(ClientId) && - !string.IsNullOrWhiteSpace(ClientSecret); - } + public bool IsConfigured() + { + return + !string.IsNullOrWhiteSpace(AppName) && + !string.IsNullOrWhiteSpace(ClientId) && + !string.IsNullOrWhiteSpace(ClientSecret); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs index 9b0cea2af4..9bdb066421 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs @@ -11,38 +11,37 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.News +namespace Squidex.Areas.Api.Controllers.News; + +/// <summary> +/// Readonly API for news items. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(News))] +public sealed class NewsController : ApiController { + private readonly FeaturesService featuresService; + + public NewsController(ICommandBus commandBus, FeaturesService featuresService) + : base(commandBus) + { + this.featuresService = featuresService; + } + /// <summary> - /// Readonly API for news items. + /// Get features since version. /// </summary> - [ApiExplorerSettings(GroupName = nameof(News))] - public sealed class NewsController : ApiController + /// <param name="version">The latest received version.</param> + /// <returns> + /// 200 => Latest features returned. + /// </returns> + [HttpGet] + [Route("news/features/")] + [ProducesResponseType(typeof(FeaturesDto), StatusCodes.Status200OK)] + [ApiPermission] + public async Task<IActionResult> GetNews([FromQuery] int version = 0) { - private readonly FeaturesService featuresService; - - public NewsController(ICommandBus commandBus, FeaturesService featuresService) - : base(commandBus) - { - this.featuresService = featuresService; - } - - /// <summary> - /// Get features since version. - /// </summary> - /// <param name="version">The latest received version.</param> - /// <returns> - /// 200 => Latest features returned. - /// </returns> - [HttpGet] - [Route("news/features/")] - [ProducesResponseType(typeof(FeaturesDto), StatusCodes.Status200OK)] - [ApiPermission] - public async Task<IActionResult> GetNews([FromQuery] int version = 0) - { - var features = await featuresService.GetFeaturesAsync(version, HttpContext.RequestAborted); + var features = await featuresService.GetFeaturesAsync(version, HttpContext.RequestAborted); - return Ok(features); - } + return Ok(features); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs index 6f503426c7..ff29c6e94e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs @@ -11,74 +11,73 @@ #pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body -namespace Squidex.Areas.Api.Controllers.News.Service +namespace Squidex.Areas.Api.Controllers.News.Service; + +public sealed class FeaturesService { - public sealed class FeaturesService - { - private const int FeatureVersion = 21; - private readonly QueryContext flatten = QueryContext.Default.Flatten(); - private readonly IContentsClient<NewsEntity, FeatureDto> client; + private const int FeatureVersion = 21; + private readonly QueryContext flatten = QueryContext.Default.Flatten(); + private readonly IContentsClient<NewsEntity, FeatureDto> client; - public sealed class NewsEntity : Content<FeatureDto> - { - } + public sealed class NewsEntity : Content<FeatureDto> + { + } - public FeaturesService(IOptions<MyNewsOptions> options) + public FeaturesService(IOptions<MyNewsOptions> options) + { + if (options.Value.IsConfigured()) { - if (options.Value.IsConfigured()) + var squidexOptions = new SquidexOptions { - var squidexOptions = new SquidexOptions - { - AppName = options.Value.AppName, - ClientId = options.Value.ClientId, - ClientSecret = options.Value.ClientSecret, - Url = "https://cloud.squidex.io" - }; + AppName = options.Value.AppName, + ClientId = options.Value.ClientId, + ClientSecret = options.Value.ClientSecret, + Url = "https://cloud.squidex.io" + }; - var clientManager = new SquidexClientManager(squidexOptions); + var clientManager = new SquidexClientManager(squidexOptions); - client = clientManager.CreateContentsClient<NewsEntity, FeatureDto>("feature-news"); - } + client = clientManager.CreateContentsClient<NewsEntity, FeatureDto>("feature-news"); } + } - public async Task<FeaturesDto> GetFeaturesAsync(int version = 0, - CancellationToken ct = default) + public async Task<FeaturesDto> GetFeaturesAsync(int version = 0, + CancellationToken ct = default) + { + var result = new FeaturesDto { - var result = new FeaturesDto - { - Version = version - }; + Version = version + }; - if (client != null && version < FeatureVersion) + if (client != null && version < FeatureVersion) + { + try { - try - { - var query = new ContentQuery(); + var query = new ContentQuery(); - if (version == 0) - { - query.Filter = $"data/version/iv eq {FeatureVersion}"; - } - else - { - query.Filter = $"data/version/iv le {FeatureVersion} and data/version/iv gt {version}"; - } + if (version == 0) + { + query.Filter = $"data/version/iv eq {FeatureVersion}"; + } + else + { + query.Filter = $"data/version/iv le {FeatureVersion} and data/version/iv gt {version}"; + } - var features = await client.GetAsync(query, flatten, ct); + var features = await client.GetAsync(query, flatten, ct); - result.Features.AddRange(features.Items.Select(x => x.Data).ToList()); + result.Features.AddRange(features.Items.Select(x => x.Data).ToList()); - if (features.Items.Count > 0) - { - result.Version = features.Items.Max(x => x.Version); - } - } - catch + if (features.Items.Count > 0) { + result.Version = features.Items.Max(x => x.Version); } } - - return result; + catch + { + } } + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs index 50c5faff8e..bb7e5467b7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs @@ -10,69 +10,68 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Ping +namespace Squidex.Areas.Api.Controllers.Ping; + +/// <summary> +/// Makes a ping request. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Ping))] +public sealed class PingController : ApiController { + private readonly ExposedValues exposedValues; + + public PingController(ICommandBus commandBus, ExposedValues exposedValues) + : base(commandBus) + { + this.exposedValues = exposedValues; + } + /// <summary> - /// Makes a ping request. + /// Get API information. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Ping))] - public sealed class PingController : ApiController + /// <returns> + /// 200 => Infos returned. + /// </returns> + [HttpGet] + [ProducesResponseType(typeof(ExposedValues), StatusCodes.Status200OK)] + [Route("info/")] + public IActionResult GetInfo() { - private readonly ExposedValues exposedValues; - - public PingController(ICommandBus commandBus, ExposedValues exposedValues) - : base(commandBus) - { - this.exposedValues = exposedValues; - } - - /// <summary> - /// Get API information. - /// </summary> - /// <returns> - /// 200 => Infos returned. - /// </returns> - [HttpGet] - [ProducesResponseType(typeof(ExposedValues), StatusCodes.Status200OK)] - [Route("info/")] - public IActionResult GetInfo() - { - return Ok(exposedValues); - } + return Ok(exposedValues); + } - /// <summary> - /// Get ping status of the API. - /// </summary> - /// <returns> - /// 204 => Service ping successful. - /// </returns> - /// <remarks> - /// Can be used to test, if the Squidex API is alive and responding. - /// </remarks> - [HttpGet] - [Route("ping/")] - public IActionResult GetPing() - { - return NoContent(); - } + /// <summary> + /// Get ping status of the API. + /// </summary> + /// <returns> + /// 204 => Service ping successful. + /// </returns> + /// <remarks> + /// Can be used to test, if the Squidex API is alive and responding. + /// </remarks> + [HttpGet] + [Route("ping/")] + public IActionResult GetPing() + { + return NoContent(); + } - /// <summary> - /// Get ping status. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 204 => Service ping successful. - /// </returns> - /// <remarks> - /// Can be used to test, if the Squidex API is alive and responding. - /// </remarks> - [HttpGet] - [Route("ping/{app}/")] - [ApiPermissionOrAnonymous(PermissionIds.AppPing)] - [ApiCosts(0)] - public IActionResult GetAppPing(string app) - { - return NoContent(); - } + /// <summary> + /// Get ping status. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 204 => Service ping successful. + /// </returns> + /// <remarks> + /// Can be used to test, if the Squidex API is alive and responding. + /// </remarks> + [HttpGet] + [Route("ping/{app}/")] + [ApiPermissionOrAnonymous(PermissionIds.AppPing)] + [ApiCosts(0)] + public IActionResult GetAppPing(string app) + { + return NoContent(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 29ba8e1fe3..7dd2a7ac0e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -16,117 +16,116 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Plans +namespace Squidex.Areas.Api.Controllers.Plans; + +/// <summary> +/// Update and query plans. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Plans))] +public sealed class AppPlansController : ApiController { + private readonly IBillingPlans billingPlans; + private readonly IBillingManager billingManager; + private readonly IUsageGate usageGate; + + public AppPlansController(ICommandBus commandBus, + IUsageGate usageGate, + IBillingPlans billingPlans, + IBillingManager billingManager) + : base(commandBus) + { + this.billingPlans = billingPlans; + this.billingManager = billingManager; + this.usageGate = usageGate; + } + /// <summary> - /// Update and query plans. + /// Get app plan information. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Plans))] - public sealed class AppPlansController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => App plan information returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/plans/")] + [ProducesResponseType(typeof(PlansDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppPlansRead)] + [ApiCosts(0)] + public IActionResult GetPlans(string app) { - private readonly IBillingPlans billingPlans; - private readonly IBillingManager billingManager; - private readonly IUsageGate usageGate; - - public AppPlansController(ICommandBus commandBus, - IUsageGate usageGate, - IBillingPlans billingPlans, - IBillingManager billingManager) - : base(commandBus) - { - this.billingPlans = billingPlans; - this.billingManager = billingManager; - this.usageGate = usageGate; - } - - /// <summary> - /// Get app plan information. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => App plan information returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/plans/")] - [ProducesResponseType(typeof(PlansDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppPlansRead)] - [ApiCosts(0)] - public IActionResult GetPlans(string app) + var response = Deferred.AsyncResponse(async () => { - var response = Deferred.AsyncResponse(async () => - { - var plans = billingPlans.GetAvailablePlans(); + var plans = billingPlans.GetAvailablePlans(); - var (plan, link, referral) = - await AsyncHelper.WhenAll( - usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted), - billingManager.GetPortalLinkAsync(UserId, App, HttpContext.RequestAborted), - billingManager.GetReferralInfoAsync(UserId, App, HttpContext.RequestAborted)); + var (plan, link, referral) = + await AsyncHelper.WhenAll( + usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted), + billingManager.GetPortalLinkAsync(UserId, App, HttpContext.RequestAborted), + billingManager.GetReferralInfoAsync(UserId, App, HttpContext.RequestAborted)); - var planOwner = App.Plan?.Owner.Identifier; + var planOwner = App.Plan?.Owner.Identifier; - PlansLockedReason GetLocked() + PlansLockedReason GetLocked() + { + if (plan.TeamId != null) + { + return PlansLockedReason.ManagedByTeam; + } + else if (!Resources.CanChangePlan) + { + return PlansLockedReason.NoPermission; + } + else if (planOwner != null && !string.Equals(planOwner, UserId, StringComparison.Ordinal)) { - if (plan.TeamId != null) - { - return PlansLockedReason.ManagedByTeam; - } - else if (!Resources.CanChangePlan) - { - return PlansLockedReason.NoPermission; - } - else if (planOwner != null && !string.Equals(planOwner, UserId, StringComparison.Ordinal)) - { - return PlansLockedReason.NotOwner; - } - - return PlansLockedReason.None; + return PlansLockedReason.NotOwner; } - return PlansDto.FromDomain( - plans.ToArray(), - planOwner, - plan.PlanId, - referral, - link, - GetLocked()); - }); + return PlansLockedReason.None; + } - Response.Headers[HeaderNames.ETag] = App.ToEtag(); + return PlansDto.FromDomain( + plans.ToArray(), + planOwner, + plan.PlanId, + referral, + link, + GetLocked()); + }); - return Ok(response); - } + Response.Headers[HeaderNames.ETag] = App.ToEtag(); - /// <summary> - /// Change the app plan. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">Plan object that needs to be changed.</param> - /// <returns> - /// 200 => Plan changed or redirect url returned. - /// 400 => Plan not owned by user. - /// 404 => App not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/plan/")] - [ProducesResponseType(typeof(PlanChangedDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppPlansChange)] - [ApiCosts(0)] - public async Task<IActionResult> PutPlan(string app, [FromBody] ChangePlanDto request) - { - var command = SimpleMapper.Map(request, new ChangePlan()); + return Ok(response); + } - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + /// <summary> + /// Change the app plan. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">Plan object that needs to be changed.</param> + /// <returns> + /// 200 => Plan changed or redirect url returned. + /// 400 => Plan not owned by user. + /// 404 => App not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/plan/")] + [ProducesResponseType(typeof(PlanChangedDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppPlansChange)] + [ApiCosts(0)] + public async Task<IActionResult> PutPlan(string app, [FromBody] ChangePlanDto request) + { + var command = SimpleMapper.Map(request, new ChangePlan()); - string? redirectUri = null; + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - if (context.PlainResult is PlanChangedResult result) - { - redirectUri = result.RedirectUri?.ToString(); - } + string? redirectUri = null; - return Ok(new PlanChangedDto { RedirectUri = redirectUri }); + if (context.PlainResult is PlanChangedResult result) + { + redirectUri = result.RedirectUri?.ToString(); } + + return Ok(new PlanChangedDto { RedirectUri = redirectUri }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs index d337a89969..7b30b256c0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models; + +public sealed class ChangePlanDto { - public sealed class ChangePlanDto - { - /// <summary> - /// The new plan id. - /// </summary> - [LocalizedRequired] - public string PlanId { get; set; } - } + /// <summary> + /// The new plan id. + /// </summary> + [LocalizedRequired] + public string PlanId { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs index 403dcd3819..6b2d7c3ced 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models; + +public sealed class PlanChangedDto { - public sealed class PlanChangedDto - { - /// <summary> - /// Optional redirect uri. - /// </summary> - public string? RedirectUri { get; set; } - } + /// <summary> + /// Optional redirect uri. + /// </summary> + public string? RedirectUri { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs index a7fbbdf9ba..b849f46612 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs @@ -9,73 +9,72 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models; + +public sealed class PlanDto { - public sealed class PlanDto - { - /// <summary> - /// The ID of the plan. - /// </summary> - [LocalizedRequired] - public string Id { get; set; } + /// <summary> + /// The ID of the plan. + /// </summary> + [LocalizedRequired] + public string Id { get; set; } - /// <summary> - /// The name of the plan. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The name of the plan. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - /// <summary> - /// The monthly costs of the plan. - /// </summary> - [LocalizedRequired] - public string Costs { get; set; } + /// <summary> + /// The monthly costs of the plan. + /// </summary> + [LocalizedRequired] + public string Costs { get; set; } - /// <summary> - /// An optional confirm text for the monthly subscription. - /// </summary> - public string? ConfirmText { get; set; } + /// <summary> + /// An optional confirm text for the monthly subscription. + /// </summary> + public string? ConfirmText { get; set; } - /// <summary> - /// An optional confirm text for the yearly subscription. - /// </summary> - public string? YearlyConfirmText { get; set; } + /// <summary> + /// An optional confirm text for the yearly subscription. + /// </summary> + public string? YearlyConfirmText { get; set; } - /// <summary> - /// The yearly costs of the plan. - /// </summary> - public string? YearlyCosts { get; set; } + /// <summary> + /// The yearly costs of the plan. + /// </summary> + public string? YearlyCosts { get; set; } - /// <summary> - /// The yearly ID of the plan. - /// </summary> - public string? YearlyId { get; set; } + /// <summary> + /// The yearly ID of the plan. + /// </summary> + public string? YearlyId { get; set; } - /// <summary> - /// The maximum number of API traffic. - /// </summary> - public long MaxApiBytes { get; set; } + /// <summary> + /// The maximum number of API traffic. + /// </summary> + public long MaxApiBytes { get; set; } - /// <summary> - /// The maximum number of API calls. - /// </summary> - public long MaxApiCalls { get; set; } + /// <summary> + /// The maximum number of API calls. + /// </summary> + public long MaxApiCalls { get; set; } - /// <summary> - /// The maximum allowed asset size. - /// </summary> - public long MaxAssetSize { get; set; } + /// <summary> + /// The maximum allowed asset size. + /// </summary> + public long MaxAssetSize { get; set; } - /// <summary> - /// The maximum number of contributors. - /// </summary> - public int MaxContributors { get; set; } + /// <summary> + /// The maximum number of contributors. + /// </summary> + public int MaxContributors { get; set; } - public static PlanDto FromDomain(Plan plan) - { - var result = SimpleMapper.Map(plan, new PlanDto()); + public static PlanDto FromDomain(Plan plan) + { + var result = SimpleMapper.Map(plan, new PlanDto()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansDto.cs index 4076021a1a..ccee5e311e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansDto.cs @@ -8,58 +8,57 @@ using Squidex.Domain.Apps.Entities.Billing; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models; + +public sealed class PlansDto { - public sealed class PlansDto - { - /// <summary> - /// The available plans. - /// </summary> - [LocalizedRequired] - public PlanDto[] Plans { get; set; } + /// <summary> + /// The available plans. + /// </summary> + [LocalizedRequired] + public PlanDto[] Plans { get; set; } - /// <summary> - /// The current plan id. - /// </summary> - public string? CurrentPlanId { get; set; } + /// <summary> + /// The current plan id. + /// </summary> + public string? CurrentPlanId { get; set; } - /// <summary> - /// The plan owner. - /// </summary> - public string? PlanOwner { get; set; } + /// <summary> + /// The plan owner. + /// </summary> + public string? PlanOwner { get; set; } - /// <summary> - /// The link to the management portal. - /// </summary> - public Uri? PortalLink { get; set; } + /// <summary> + /// The link to the management portal. + /// </summary> + public Uri? PortalLink { get; set; } - /// <summary> - /// The referral management. - /// </summary> - public ReferralInfo? Referral { get; set; } + /// <summary> + /// The referral management. + /// </summary> + public ReferralInfo? Referral { get; set; } - /// <summary> - /// The reason why the plan cannot be changed. - /// </summary> - public PlansLockedReason Locked { get; set; } + /// <summary> + /// The reason why the plan cannot be changed. + /// </summary> + public PlansLockedReason Locked { get; set; } - public static PlansDto FromDomain(Plan[] plans, string? owner, string? planId, ReferralInfo? referral, Uri? link, PlansLockedReason locked) + public static PlansDto FromDomain(Plan[] plans, string? owner, string? planId, ReferralInfo? referral, Uri? link, PlansLockedReason locked) + { + var result = new PlansDto { - var result = new PlansDto - { - Locked = locked, - CurrentPlanId = planId, - Plans = plans.Select(PlanDto.FromDomain).ToArray(), - PlanOwner = owner, - Referral = referral - }; + Locked = locked, + CurrentPlanId = planId, + Plans = plans.Select(PlanDto.FromDomain).ToArray(), + PlanOwner = owner, + Referral = referral + }; - if (locked == PlansLockedReason.None) - { - result.PortalLink = link; - } - - return result; + if (locked == PlansLockedReason.None) + { + result.PortalLink = link; } + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansLockedReason.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansLockedReason.cs index 2322034fe7..a685f87e55 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansLockedReason.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlansLockedReason.cs @@ -5,28 +5,27 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models; + +public enum PlansLockedReason { - public enum PlansLockedReason - { - /// <summary> - /// The user can change the plan - /// </summary> - None, + /// <summary> + /// The user can change the plan + /// </summary> + None, - /// <summary> - /// The user is not the owner. - /// </summary> - NotOwner, + /// <summary> + /// The user is not the owner. + /// </summary> + NotOwner, - /// <summary> - /// The user does not have permission to change the plan - /// </summary> - NoPermission, + /// <summary> + /// The user does not have permission to change the plan + /// </summary> + NoPermission, - /// <summary> - /// The plan is managed by the team. - /// </summary> - ManagedByTeam - } + /// <summary> + /// The plan is managed by the team. + /// </summary> + ManagedByTeam } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs index 19f0eef010..7fb516ad6e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs @@ -16,105 +16,104 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Plans +namespace Squidex.Areas.Api.Controllers.Plans; + +/// <summary> +/// Update and query plans. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Plans))] +public sealed class TeamPlansController : ApiController { + private readonly IUsageGate appUsageGate; + private readonly IBillingPlans billingPlans; + private readonly IBillingManager billingManager; + + public TeamPlansController(ICommandBus commandBus, + IUsageGate appUsageGate, + IBillingPlans billingPlans, + IBillingManager billingManager) + : base(commandBus) + { + this.appUsageGate = appUsageGate; + this.billingPlans = billingPlans; + this.billingManager = billingManager; + } + /// <summary> - /// Update and query plans. + /// Get team plan information. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Plans))] - public sealed class TeamPlansController : ApiController + /// <param name="team">The name of the team.</param> + /// <returns> + /// 200 => Team plan information returned. + /// 404 => Team not found. + /// </returns> + [HttpGet] + [Route("teams/{team}/plans/")] + [ProducesResponseType(typeof(PlansDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamPlansRead)] + [ApiCosts(0)] + public IActionResult GetTeamPlans(string team) { - private readonly IUsageGate appUsageGate; - private readonly IBillingPlans billingPlans; - private readonly IBillingManager billingManager; - - public TeamPlansController(ICommandBus commandBus, - IUsageGate appUsageGate, - IBillingPlans billingPlans, - IBillingManager billingManager) - : base(commandBus) + var response = Deferred.AsyncResponse(async () => { - this.appUsageGate = appUsageGate; - this.billingPlans = billingPlans; - this.billingManager = billingManager; - } + var plans = billingPlans.GetAvailablePlans(); - /// <summary> - /// Get team plan information. - /// </summary> - /// <param name="team">The name of the team.</param> - /// <returns> - /// 200 => Team plan information returned. - /// 404 => Team not found. - /// </returns> - [HttpGet] - [Route("teams/{team}/plans/")] - [ProducesResponseType(typeof(PlansDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamPlansRead)] - [ApiCosts(0)] - public IActionResult GetTeamPlans(string team) - { - var response = Deferred.AsyncResponse(async () => - { - var plans = billingPlans.GetAvailablePlans(); - - var (plan, link, referral) = - await AsyncHelper.WhenAll( - appUsageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted), - billingManager.GetPortalLinkAsync(UserId, Team, HttpContext.RequestAborted), - billingManager.GetReferralInfoAsync(UserId, Team, HttpContext.RequestAborted)); + var (plan, link, referral) = + await AsyncHelper.WhenAll( + appUsageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted), + billingManager.GetPortalLinkAsync(UserId, Team, HttpContext.RequestAborted), + billingManager.GetReferralInfoAsync(UserId, Team, HttpContext.RequestAborted)); - PlansLockedReason GetLocked() + PlansLockedReason GetLocked() + { + if (!Resources.CanChangeTeamPlan) { - if (!Resources.CanChangeTeamPlan) - { - return PlansLockedReason.NoPermission; - } - - return PlansLockedReason.None; + return PlansLockedReason.NoPermission; } - return PlansDto.FromDomain( - plans.ToArray(), null, - plan.PlanId, - referral, - link, - GetLocked()); - }); + return PlansLockedReason.None; + } - Response.Headers[HeaderNames.ETag] = Team.ToEtag(); + return PlansDto.FromDomain( + plans.ToArray(), null, + plan.PlanId, + referral, + link, + GetLocked()); + }); - return Ok(response); - } + Response.Headers[HeaderNames.ETag] = Team.ToEtag(); - /// <summary> - /// Change the team plan. - /// </summary> - /// <param name="team">The name of the team.</param> - /// <param name="request">Plan object that needs to be changed.</param> - /// <returns> - /// 200 => Plan changed or redirect url returned. - /// 404 => Team not found. - /// </returns> - [HttpPut] - [Route("teams/{team}/plan/")] - [ProducesResponseType(typeof(PlanChangedDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamPlansChange)] - [ApiCosts(0)] - public async Task<IActionResult> PutTeamPlan(string team, [FromBody] ChangePlanDto request) - { - var command = SimpleMapper.Map(request, new ChangePlan()); + return Ok(response); + } - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + /// <summary> + /// Change the team plan. + /// </summary> + /// <param name="team">The name of the team.</param> + /// <param name="request">Plan object that needs to be changed.</param> + /// <returns> + /// 200 => Plan changed or redirect url returned. + /// 404 => Team not found. + /// </returns> + [HttpPut] + [Route("teams/{team}/plan/")] + [ProducesResponseType(typeof(PlanChangedDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamPlansChange)] + [ApiCosts(0)] + public async Task<IActionResult> PutTeamPlan(string team, [FromBody] ChangePlanDto request) + { + var command = SimpleMapper.Map(request, new ChangePlan()); - string? redirectUri = null; + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - if (context.PlainResult is PlanChangedResult result) - { - redirectUri = result.RedirectUri?.ToString(); - } + string? redirectUri = null; - return Ok(new PlanChangedDto { RedirectUri = redirectUri }); + if (context.PlainResult is PlanChangedResult result) + { + redirectUri = result.RedirectUri?.ToString(); } + + return Ok(new PlanChangedDto { RedirectUri = redirectUri }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs index 664933ba55..923093a585 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs @@ -10,51 +10,50 @@ using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class QueryDto { - public sealed class QueryDto + /// <summary> + /// The optional list of ids to query. + /// </summary> + public DomainId[]? Ids { get; set; } + + /// <summary> + /// The optional odata query. + /// </summary> + public string? OData { get; set; } + + /// <summary> + /// The optional json query. + /// </summary> + [JsonPropertyName("q")] + public JsonDocument? JsonQuery { get; set; } + + /// <summary> + /// The parent id (for assets). + /// </summary> + public DomainId? ParentId { get; set; } + + public Q ToQuery() { - /// <summary> - /// The optional list of ids to query. - /// </summary> - public DomainId[]? Ids { get; set; } - - /// <summary> - /// The optional odata query. - /// </summary> - public string? OData { get; set; } - - /// <summary> - /// The optional json query. - /// </summary> - [JsonPropertyName("q")] - public JsonDocument? JsonQuery { get; set; } - - /// <summary> - /// The parent id (for assets). - /// </summary> - public DomainId? ParentId { get; set; } - - public Q ToQuery() - { - var result = Q.Empty; - - if (Ids != null) - { - result = result.WithIds(Ids); - } + var result = Q.Empty; - if (OData != null) - { - result = result.WithODataQuery(OData); - } + if (Ids != null) + { + result = result.WithIds(Ids); + } - if (JsonQuery != null) - { - result = result.WithJsonQuery(JsonQuery.RootElement.ToString()); - } + if (OData != null) + { + result = result.WithODataQuery(OData); + } - return result; + if (JsonQuery != null) + { + result = result.WithJsonQuery(JsonQuery.RootElement.ToString()); } + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/QueryJsonDto.cs b/backend/src/Squidex/Areas/Api/Controllers/QueryJsonDto.cs index 4e8bead886..1be6500e01 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/QueryJsonDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/QueryJsonDto.cs @@ -9,10 +9,9 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries; -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +[JsonSchemaFlatten] +public sealed class QueryJsonDto : Query<JsonValue> { - [JsonSchemaFlatten] - public sealed class QueryJsonDto : Query<JsonValue> - { - } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/RenameTagDto.cs b/backend/src/Squidex/Areas/Api/Controllers/RenameTagDto.cs index 0d261f9685..3e8ebebe8f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/RenameTagDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/RenameTagDto.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Areas.Api.Controllers; + +public sealed class RenameTagDto { - public sealed class RenameTagDto - { - /// <summary> - /// The new name for the tag. - /// </summary> - [LocalizedRequired] - public string TagName { get; set; } - } + /// <summary> + /// The new name for the tag. + /// </summary> + [LocalizedRequired] + public string TagName { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs index a66c8d7fa7..a953f4a730 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -10,51 +10,50 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters +namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters; + +public sealed class RuleTriggerDtoFactory : IRuleTriggerVisitor<RuleTriggerDto> { - public sealed class RuleTriggerDtoFactory : IRuleTriggerVisitor<RuleTriggerDto> - { - private static readonly RuleTriggerDtoFactory Instance = new RuleTriggerDtoFactory(); - - private RuleTriggerDtoFactory() - { - } - - public static RuleTriggerDto Create(RuleTrigger properties) - { - return properties.Accept(Instance); - } - - public RuleTriggerDto Visit(AssetChangedTriggerV2 trigger) - { - return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto()); - } - - public RuleTriggerDto Visit(CommentTrigger trigger) - { - return SimpleMapper.Map(trigger, new CommentRuleTriggerDto()); - } - - public RuleTriggerDto Visit(ManualTrigger trigger) - { - return SimpleMapper.Map(trigger, new ManualRuleTriggerDto()); - } - - public RuleTriggerDto Visit(SchemaChangedTrigger trigger) - { - return SimpleMapper.Map(trigger, new SchemaChangedRuleTriggerDto()); - } - - public RuleTriggerDto Visit(UsageTrigger trigger) - { - return SimpleMapper.Map(trigger, new UsageRuleTriggerDto()); - } - - public RuleTriggerDto Visit(ContentChangedTriggerV2 trigger) - { - var schemas = trigger.Schemas?.Select(ContentChangedRuleTriggerSchemaDto.FromDomain).ToArray(); - - return new ContentChangedRuleTriggerDto { Schemas = schemas, HandleAll = trigger.HandleAll }; - } + private static readonly RuleTriggerDtoFactory Instance = new RuleTriggerDtoFactory(); + + private RuleTriggerDtoFactory() + { + } + + public static RuleTriggerDto Create(RuleTrigger properties) + { + return properties.Accept(Instance); + } + + public RuleTriggerDto Visit(AssetChangedTriggerV2 trigger) + { + return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto()); + } + + public RuleTriggerDto Visit(CommentTrigger trigger) + { + return SimpleMapper.Map(trigger, new CommentRuleTriggerDto()); + } + + public RuleTriggerDto Visit(ManualTrigger trigger) + { + return SimpleMapper.Map(trigger, new ManualRuleTriggerDto()); + } + + public RuleTriggerDto Visit(SchemaChangedTrigger trigger) + { + return SimpleMapper.Map(trigger, new SchemaChangedRuleTriggerDto()); + } + + public RuleTriggerDto Visit(UsageTrigger trigger) + { + return SimpleMapper.Map(trigger, new UsageRuleTriggerDto()); + } + + public RuleTriggerDto Visit(ContentChangedTriggerV2 trigger) + { + var schemas = trigger.Schemas?.Select(ContentChangedRuleTriggerSchemaDto.FromDomain).ToArray(); + + return new ContentChangedRuleTriggerDto { Schemas = schemas, HandleAll = trigger.HandleAll }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs index 738e6c9f07..0aca4035a3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs @@ -10,31 +10,30 @@ using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class CreateRuleDto { - public sealed class CreateRuleDto - { - /// <summary> - /// The trigger properties. - /// </summary> - [LocalizedRequired] - public RuleTriggerDto Trigger { get; set; } + /// <summary> + /// The trigger properties. + /// </summary> + [LocalizedRequired] + public RuleTriggerDto Trigger { get; set; } - /// <summary> - /// The action properties. - /// </summary> - [LocalizedRequired] - [JsonConverter(typeof(RuleActionConverter))] - public RuleAction Action { get; set; } + /// <summary> + /// The action properties. + /// </summary> + [LocalizedRequired] + [JsonConverter(typeof(RuleActionConverter))] + public RuleAction Action { get; set; } - public Rule ToRule() - { - return new Rule(Trigger.ToTrigger(), Action); - } + public Rule ToRule() + { + return new Rule(Trigger.ToTrigger(), Action); + } - public CreateRule ToCommand() - { - return new CreateRule { Action = Action, Trigger = Trigger?.ToTrigger() }; - } + public CreateRule ToCommand() + { + return new CreateRule { Action = Action, Trigger = Trigger?.ToTrigger() }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs index 7c8b3098d4..8270e31198 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs @@ -8,15 +8,14 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Web.Json; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class RuleActionConverter : JsonInheritanceConverter<RuleAction> { - public sealed class RuleActionConverter : JsonInheritanceConverter<RuleAction> - { - public static IReadOnlyDictionary<string, Type> Mapping { get; set; } + public static IReadOnlyDictionary<string, Type> Mapping { get; set; } - public RuleActionConverter() - : base("actionType", Mapping) - { - } + public RuleActionConverter() + : base("actionType", Mapping) + { } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs index f119e32911..c5f5deba20 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs @@ -12,70 +12,69 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class RuleActionProcessor : IDocumentProcessor { - public sealed class RuleActionProcessor : IDocumentProcessor + private readonly RuleTypeProvider ruleRegistry; + + public RuleActionProcessor(RuleTypeProvider ruleRegistry) { - private readonly RuleTypeProvider ruleRegistry; + this.ruleRegistry = ruleRegistry; + } - public RuleActionProcessor(RuleTypeProvider ruleRegistry) + public void Process(DocumentProcessorContext context) + { + try { - this.ruleRegistry = ruleRegistry; - } + var schema = context.SchemaResolver.GetSchema(typeof(RuleAction), false); - public void Process(DocumentProcessorContext context) - { - try + if (schema != null) { - var schema = context.SchemaResolver.GetSchema(typeof(RuleAction), false); + var converter = new RuleActionConverter(); - if (schema != null) + schema.DiscriminatorObject = new OpenApiDiscriminator { - var converter = new RuleActionConverter(); + PropertyName = converter.DiscriminatorName, - schema.DiscriminatorObject = new OpenApiDiscriminator - { - PropertyName = converter.DiscriminatorName, - - // The converter must be set so that NJsonSchema can get the allowed types for that. - JsonInheritanceConverter = converter, - }; + // The converter must be set so that NJsonSchema can get the allowed types for that. + JsonInheritanceConverter = converter, + }; - schema.Properties[converter.DiscriminatorName] = new JsonSchemaProperty - { - Type = JsonObjectType.String, - IsRequired = true, - IsNullableRaw = true - }; + schema.Properties[converter.DiscriminatorName] = new JsonSchemaProperty + { + Type = JsonObjectType.String, + IsRequired = true, + IsNullableRaw = true + }; - // The types come from another assembly so we have to fix it here. - foreach (var (key, value) in ruleRegistry.Actions) - { - var derivedSchema = context.SchemaGenerator.Generate<JsonSchema>(value.Type.ToContextualType(), context.SchemaResolver); + // The types come from another assembly so we have to fix it here. + foreach (var (key, value) in ruleRegistry.Actions) + { + var derivedSchema = context.SchemaGenerator.Generate<JsonSchema>(value.Type.ToContextualType(), context.SchemaResolver); - var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key; + var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key; - if (oldName != null) - { - context.Document.Definitions.Remove(oldName); - context.Document.Definitions.Add($"{key}RuleActionDto", derivedSchema); - } + if (oldName != null) + { + context.Document.Definitions.Remove(oldName); + context.Document.Definitions.Add($"{key}RuleActionDto", derivedSchema); } - - RemoveFreezable(context, schema); } - } - catch (KeyNotFoundException) - { - return; + + RemoveFreezable(context, schema); } } - - private static void RemoveFreezable(DocumentProcessorContext context, JsonSchema schema) + catch (KeyNotFoundException) { - context.Document.Definitions.Remove("Freezable"); - - schema.AllOf.Clear(); + return; } } + + private static void RemoveFreezable(DocumentProcessorContext context, JsonSchema schema) + { + context.Document.Definitions.Remove("Freezable"); + + schema.AllOf.Clear(); + } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs index 642e24c775..314025b56c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs @@ -16,152 +16,151 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class RuleDto : Resource { - public sealed class RuleDto : Resource + /// <summary> + /// The ID of the rule. + /// </summary> + public DomainId Id { get; set; } + + /// <summary> + /// The user that has created the rule. + /// </summary> + [LocalizedRequired] + public RefToken CreatedBy { get; set; } + + /// <summary> + /// The user that has updated the rule. + /// </summary> + [LocalizedRequired] + public RefToken LastModifiedBy { get; set; } + + /// <summary> + /// The date and time when the rule has been created. + /// </summary> + public Instant Created { get; set; } + + /// <summary> + /// The date and time when the rule has been modified last. + /// </summary> + public Instant LastModified { get; set; } + + /// <summary> + /// The version of the rule. + /// </summary> + public long Version { get; set; } + + /// <summary> + /// Determines if the rule is enabled. + /// </summary> + public bool IsEnabled { get; set; } + + /// <summary> + /// Optional rule name. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// The trigger properties. + /// </summary> + [LocalizedRequired] + public RuleTriggerDto Trigger { get; set; } + + /// <summary> + /// The action properties. + /// </summary> + [LocalizedRequired] + [JsonConverter(typeof(RuleActionConverter))] + public RuleAction Action { get; set; } + + /// <summary> + /// The number of completed executions. + /// </summary> + public int NumSucceeded { get; set; } + + /// <summary> + /// The number of failed executions. + /// </summary> + public int NumFailed { get; set; } + + /// <summary> + /// The date and time when the rule was executed the last time. + /// </summary> + public Instant? LastExecuted { get; set; } + + public static RuleDto FromDomain(IEnrichedRuleEntity rule, bool canRun, IRuleRunnerService ruleRunnerService, Resources resources) { - /// <summary> - /// The ID of the rule. - /// </summary> - public DomainId Id { get; set; } - - /// <summary> - /// The user that has created the rule. - /// </summary> - [LocalizedRequired] - public RefToken CreatedBy { get; set; } - - /// <summary> - /// The user that has updated the rule. - /// </summary> - [LocalizedRequired] - public RefToken LastModifiedBy { get; set; } - - /// <summary> - /// The date and time when the rule has been created. - /// </summary> - public Instant Created { get; set; } - - /// <summary> - /// The date and time when the rule has been modified last. - /// </summary> - public Instant LastModified { get; set; } - - /// <summary> - /// The version of the rule. - /// </summary> - public long Version { get; set; } - - /// <summary> - /// Determines if the rule is enabled. - /// </summary> - public bool IsEnabled { get; set; } - - /// <summary> - /// Optional rule name. - /// </summary> - public string? Name { get; set; } - - /// <summary> - /// The trigger properties. - /// </summary> - [LocalizedRequired] - public RuleTriggerDto Trigger { get; set; } - - /// <summary> - /// The action properties. - /// </summary> - [LocalizedRequired] - [JsonConverter(typeof(RuleActionConverter))] - public RuleAction Action { get; set; } - - /// <summary> - /// The number of completed executions. - /// </summary> - public int NumSucceeded { get; set; } - - /// <summary> - /// The number of failed executions. - /// </summary> - public int NumFailed { get; set; } - - /// <summary> - /// The date and time when the rule was executed the last time. - /// </summary> - public Instant? LastExecuted { get; set; } - - public static RuleDto FromDomain(IEnrichedRuleEntity rule, bool canRun, IRuleRunnerService ruleRunnerService, Resources resources) + var result = new RuleDto(); + + SimpleMapper.Map(rule, result); + SimpleMapper.Map(rule.RuleDef, result); + + if (rule.RuleDef.Trigger != null) { - var result = new RuleDto(); + result.Trigger = RuleTriggerDtoFactory.Create(rule.RuleDef.Trigger); + } + + return result.CreateLinks(resources, rule, canRun, ruleRunnerService); + } - SimpleMapper.Map(rule, result); - SimpleMapper.Map(rule.RuleDef, result); + private RuleDto CreateLinks(Resources resources, IEnrichedRuleEntity rule, bool canRun, IRuleRunnerService ruleRunnerService) + { + var values = new { app = resources.App, id = Id }; - if (rule.RuleDef.Trigger != null) + if (resources.CanDisableRule) + { + if (IsEnabled) { - result.Trigger = RuleTriggerDtoFactory.Create(rule.RuleDef.Trigger); + AddPutLink("disable", + resources.Url<RulesController>(x => nameof(x.DisableRule), values)); + } + else + { + AddPutLink("enable", + resources.Url<RulesController>(x => nameof(x.EnableRule), values)); } - - return result.CreateLinks(resources, rule, canRun, ruleRunnerService); } - private RuleDto CreateLinks(Resources resources, IEnrichedRuleEntity rule, bool canRun, IRuleRunnerService ruleRunnerService) + if (resources.CanUpdateRule) { - var values = new { app = resources.App, id = Id }; + AddPutLink("update", + resources.Url<RulesController>(x => nameof(x.PutRule), values)); + } - if (resources.CanDisableRule) - { - if (IsEnabled) - { - AddPutLink("disable", - resources.Url<RulesController>(x => nameof(x.DisableRule), values)); - } - else - { - AddPutLink("enable", - resources.Url<RulesController>(x => nameof(x.EnableRule), values)); - } - } + if (resources.CanRunRuleEvents) + { + AddPutLink("trigger", + resources.Url<RulesController>(x => nameof(x.TriggerRule), values)); - if (resources.CanUpdateRule) + if (canRun && ruleRunnerService.CanRunRule(rule)) { - AddPutLink("update", - resources.Url<RulesController>(x => nameof(x.PutRule), values)); + AddPutLink("run", + resources.Url<RulesController>(x => nameof(x.PutRuleRun), values)); } - if (resources.CanRunRuleEvents) + if (canRun && ruleRunnerService.CanRunFromSnapshots(rule)) { - AddPutLink("trigger", - resources.Url<RulesController>(x => nameof(x.TriggerRule), values)); - - if (canRun && ruleRunnerService.CanRunRule(rule)) - { - AddPutLink("run", - resources.Url<RulesController>(x => nameof(x.PutRuleRun), values)); - } - - if (canRun && ruleRunnerService.CanRunFromSnapshots(rule)) - { - var snaphshotValues = new { values.app, values.id, fromSnapshots = true }; - - AddPutLink("run/snapshots", - resources.Url<RulesController>(x => nameof(x.PutRuleRun), snaphshotValues)); - } - } + var snaphshotValues = new { values.app, values.id, fromSnapshots = true }; - if (resources.CanReadRuleEvents) - { - AddGetLink("logs", - resources.Url<RulesController>(x => nameof(x.GetEvents), values)); + AddPutLink("run/snapshots", + resources.Url<RulesController>(x => nameof(x.PutRuleRun), snaphshotValues)); } + } - if (resources.CanDeleteRule) - { - AddDeleteLink("delete", - resources.Url<RulesController>(x => nameof(x.DeleteRule), values)); - } + if (resources.CanReadRuleEvents) + { + AddGetLink("logs", + resources.Url<RulesController>(x => nameof(x.GetEvents), values)); + } - return this; + if (resources.CanDeleteRule) + { + AddDeleteLink("delete", + resources.Url<RulesController>(x => nameof(x.DeleteRule), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs index 9ea19585af..95aaa52b51 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs @@ -9,55 +9,54 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class RuleElementDto { - public sealed class RuleElementDto + /// <summary> + /// Describes the action or trigger type. + /// </summary> + [LocalizedRequired] + public string Description { get; set; } + + /// <summary> + /// The label for the action or trigger type. + /// </summary> + [LocalizedRequired] + public string Display { get; set; } + + /// <summary> + /// Optional title. + /// </summary> + public string? Title { get; set; } + + /// <summary> + /// The color for the icon. + /// </summary> + public string? IconColor { get; set; } + + /// <summary> + /// The image for the icon. + /// </summary> + public string? IconImage { get; set; } + + /// <summary> + /// The optional link to the product that is integrated. + /// </summary> + public string? ReadMore { get; set; } + + /// <summary> + /// The properties. + /// </summary> + [LocalizedRequired] + public RuleElementPropertyDto[] Properties { get; set; } + + public static RuleElementDto FromDomain(RuleActionDefinition definition) { - /// <summary> - /// Describes the action or trigger type. - /// </summary> - [LocalizedRequired] - public string Description { get; set; } - - /// <summary> - /// The label for the action or trigger type. - /// </summary> - [LocalizedRequired] - public string Display { get; set; } - - /// <summary> - /// Optional title. - /// </summary> - public string? Title { get; set; } - - /// <summary> - /// The color for the icon. - /// </summary> - public string? IconColor { get; set; } - - /// <summary> - /// The image for the icon. - /// </summary> - public string? IconImage { get; set; } - - /// <summary> - /// The optional link to the product that is integrated. - /// </summary> - public string? ReadMore { get; set; } - - /// <summary> - /// The properties. - /// </summary> - [LocalizedRequired] - public RuleElementPropertyDto[] Properties { get; set; } - - public static RuleElementDto FromDomain(RuleActionDefinition definition) - { - var result = SimpleMapper.Map(definition, new RuleElementDto()); - - result.Properties = definition.Properties.Select(x => SimpleMapper.Map(x, new RuleElementPropertyDto())).ToArray(); - - return result; - } + var result = SimpleMapper.Map(definition, new RuleElementDto()); + + result.Properties = definition.Properties.Select(x => SimpleMapper.Map(x, new RuleElementPropertyDto())).ToArray(); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs index b4ef3188ef..c8cde27c48 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs @@ -8,46 +8,45 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class RuleElementPropertyDto { - public sealed class RuleElementPropertyDto - { - /// <summary> - /// The html editor. - /// </summary> - [LocalizedRequired] - public RuleFieldEditor Editor { get; set; } - - /// <summary> - /// The name of the editor. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } - - /// <summary> - /// The label to use. - /// </summary> - [LocalizedRequired] - public string Display { get; set; } - - /// <summary> - /// The options, if the editor is a dropdown. - /// </summary> - public string[]? Options { get; set; } - - /// <summary> - /// The optional description. - /// </summary> - public string? Description { get; set; } - - /// <summary> - /// Indicates if the property is formattable. - /// </summary> - public bool IsFormattable { get; set; } - - /// <summary> - /// Indicates if the property is required. - /// </summary> - public bool IsRequired { get; set; } - } + /// <summary> + /// The html editor. + /// </summary> + [LocalizedRequired] + public RuleFieldEditor Editor { get; set; } + + /// <summary> + /// The name of the editor. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } + + /// <summary> + /// The label to use. + /// </summary> + [LocalizedRequired] + public string Display { get; set; } + + /// <summary> + /// The options, if the editor is a dropdown. + /// </summary> + public string[]? Options { get; set; } + + /// <summary> + /// The optional description. + /// </summary> + public string? Description { get; set; } + + /// <summary> + /// Indicates if the property is formattable. + /// </summary> + public bool IsFormattable { get; set; } + + /// <summary> + /// Indicates if the property is required. + /// </summary> + public bool IsRequired { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs index aa55007168..b85f4585b7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs @@ -13,84 +13,83 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class RuleEventDto : Resource { - public sealed class RuleEventDto : Resource + /// <summary> + /// The ID of the event. + /// </summary> + public DomainId Id { get; set; } + + /// <summary> + /// The time when the event has been created. + /// </summary> + public Instant Created { get; set; } + + /// <summary> + /// The description. + /// </summary> + [LocalizedRequired] + public string Description { get; set; } + + /// <summary> + /// The name of the event. + /// </summary> + [LocalizedRequired] + public string EventName { get; set; } + + /// <summary> + /// The last dump. + /// </summary> + public string? LastDump { get; set; } + + /// <summary> + /// The number of calls. + /// </summary> + public int NumCalls { get; set; } + + /// <summary> + /// The next attempt. + /// </summary> + public Instant? NextAttempt { get; set; } + + /// <summary> + /// The result of the event. + /// </summary> + public RuleResult Result { get; set; } + + /// <summary> + /// The result of the job. + /// </summary> + public RuleJobResult JobResult { get; set; } + + public static RuleEventDto FromDomain(IRuleEventEntity ruleEvent, Resources resources) { - /// <summary> - /// The ID of the event. - /// </summary> - public DomainId Id { get; set; } - - /// <summary> - /// The time when the event has been created. - /// </summary> - public Instant Created { get; set; } - - /// <summary> - /// The description. - /// </summary> - [LocalizedRequired] - public string Description { get; set; } - - /// <summary> - /// The name of the event. - /// </summary> - [LocalizedRequired] - public string EventName { get; set; } - - /// <summary> - /// The last dump. - /// </summary> - public string? LastDump { get; set; } - - /// <summary> - /// The number of calls. - /// </summary> - public int NumCalls { get; set; } - - /// <summary> - /// The next attempt. - /// </summary> - public Instant? NextAttempt { get; set; } - - /// <summary> - /// The result of the event. - /// </summary> - public RuleResult Result { get; set; } - - /// <summary> - /// The result of the job. - /// </summary> - public RuleJobResult JobResult { get; set; } - - public static RuleEventDto FromDomain(IRuleEventEntity ruleEvent, Resources resources) - { - var result = new RuleEventDto(); - - SimpleMapper.Map(ruleEvent, result); - SimpleMapper.Map(ruleEvent.Job, result); + var result = new RuleEventDto(); - return result.CreateLinks(resources); - } + SimpleMapper.Map(ruleEvent, result); + SimpleMapper.Map(ruleEvent.Job, result); - private RuleEventDto CreateLinks(Resources resources) - { - var values = new { app = resources.App, id = Id }; + return result.CreateLinks(resources); + } - if (resources.CanUpdateRuleEvents) - { - AddPutLink("update", - resources.Url<RulesController>(x => nameof(x.PutEvent), values)); - } + private RuleEventDto CreateLinks(Resources resources) + { + var values = new { app = resources.App, id = Id }; - if (resources.CanDeleteRuleEvents && NextAttempt != null) - { - AddDeleteLink("cancel", - resources.Url<RulesController>(x => nameof(x.DeleteEvent), values)); - } + if (resources.CanUpdateRuleEvents) + { + AddPutLink("update", + resources.Url<RulesController>(x => nameof(x.PutEvent), values)); + } - return this; + if (resources.CanDeleteRuleEvents && NextAttempt != null) + { + AddDeleteLink("cancel", + resources.Url<RulesController>(x => nameof(x.DeleteEvent), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs index 0531765da5..fef750ad89 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs @@ -10,55 +10,54 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class RuleEventsDto : Resource { - public sealed class RuleEventsDto : Resource + /// <summary> + /// The rule events. + /// </summary> + [LocalizedRequired] + public RuleEventDto[] Items { get; set; } + + /// <summary> + /// The total number of rule events. + /// </summary> + public long Total { get; set; } + + public static RuleEventsDto FromDomain(IResultList<IRuleEventEntity> ruleEvents, Resources resources, DomainId? ruleId) { - /// <summary> - /// The rule events. - /// </summary> - [LocalizedRequired] - public RuleEventDto[] Items { get; set; } - - /// <summary> - /// The total number of rule events. - /// </summary> - public long Total { get; set; } - - public static RuleEventsDto FromDomain(IResultList<IRuleEventEntity> ruleEvents, Resources resources, DomainId? ruleId) + var result = new RuleEventsDto { - var result = new RuleEventsDto - { - Total = ruleEvents.Total, - Items = ruleEvents.Select(x => RuleEventDto.FromDomain(x, resources)).ToArray() - }; + Total = ruleEvents.Total, + Items = ruleEvents.Select(x => RuleEventDto.FromDomain(x, resources)).ToArray() + }; - return result.CreateLinks(resources, ruleId); - } + return result.CreateLinks(resources, ruleId); + } - private RuleEventsDto CreateLinks(Resources resources, DomainId? ruleId) - { - var values = new { app = resources.App }; + private RuleEventsDto CreateLinks(Resources resources, DomainId? ruleId) + { + var values = new { app = resources.App }; - AddSelfLink(resources.Url<RulesController>(x => nameof(x.GetEvents), values)); + AddSelfLink(resources.Url<RulesController>(x => nameof(x.GetEvents), values)); - if (resources.CanDeleteRuleEvents) + if (resources.CanDeleteRuleEvents) + { + if (ruleId != null) { - if (ruleId != null) - { - var routeValues = new { values.app, id = ruleId }; + var routeValues = new { values.app, id = ruleId }; - AddDeleteLink("cancel", - resources.Url<RulesController>(x => nameof(x.DeleteRuleEvents), routeValues)); - } - else - { - AddDeleteLink("cancel", - resources.Url<RulesController>(x => nameof(x.DeleteEvents), values)); - } + AddDeleteLink("cancel", + resources.Url<RulesController>(x => nameof(x.DeleteRuleEvents), routeValues)); + } + else + { + AddDeleteLink("cancel", + resources.Url<RulesController>(x => nameof(x.DeleteEvents), values)); } - - return this; } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs index 377a107da0..f3695c0839 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs @@ -9,19 +9,18 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Web.Json; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +[JsonInheritanceConverter(typeof(RuleTriggerDto), "triggerType")] +[KnownType(nameof(Subtypes))] +public abstract class RuleTriggerDto { - [JsonInheritanceConverter(typeof(RuleTriggerDto), "triggerType")] - [KnownType(nameof(Subtypes))] - public abstract class RuleTriggerDto - { - public abstract RuleTrigger ToTrigger(); + public abstract RuleTrigger ToTrigger(); - public static Type[] Subtypes() - { - var type = typeof(RuleTriggerDto); + public static Type[] Subtypes() + { + var type = typeof(RuleTriggerDto); - return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); - } + return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs index 76e025f161..ec2ac11437 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs @@ -11,61 +11,60 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Rules.Models -{ - public sealed class RulesDto : Resource - { - /// <summary> - /// The rules. - /// </summary> - [LocalizedRequired] - public RuleDto[] Items { get; set; } +namespace Squidex.Areas.Api.Controllers.Rules.Models; - /// <summary> - /// The ID of the rule that is currently rerunning. - /// </summary> - public DomainId? RunningRuleId { get; set; } +public sealed class RulesDto : Resource +{ + /// <summary> + /// The rules. + /// </summary> + [LocalizedRequired] + public RuleDto[] Items { get; set; } - public static async Task<RulesDto> FromRulesAsync(IEnumerable<IEnrichedRuleEntity> items, IRuleRunnerService ruleRunnerService, Resources resources) - { - var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(resources.Context.App.Id); - var runningAvailable = runningRuleId != default; + /// <summary> + /// The ID of the rule that is currently rerunning. + /// </summary> + public DomainId? RunningRuleId { get; set; } - var result = new RulesDto - { - Items = items.Select(x => RuleDto.FromDomain(x, runningAvailable, ruleRunnerService, resources)).ToArray() - }; + public static async Task<RulesDto> FromRulesAsync(IEnumerable<IEnrichedRuleEntity> items, IRuleRunnerService ruleRunnerService, Resources resources) + { + var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(resources.Context.App.Id); + var runningAvailable = runningRuleId != default; - result.RunningRuleId = runningRuleId; + var result = new RulesDto + { + Items = items.Select(x => RuleDto.FromDomain(x, runningAvailable, ruleRunnerService, resources)).ToArray() + }; - return result.CreateLinks(resources, runningRuleId); - } + result.RunningRuleId = runningRuleId; - private RulesDto CreateLinks(Resources resources, DomainId? runningRuleId) - { - var values = new { app = resources.App }; + return result.CreateLinks(resources, runningRuleId); + } - AddSelfLink(resources.Url<RulesController>(x => nameof(x.GetRules), values)); + private RulesDto CreateLinks(Resources resources, DomainId? runningRuleId) + { + var values = new { app = resources.App }; - if (resources.CanCreateRule) - { - AddPostLink("create", - resources.Url<RulesController>(x => nameof(x.PostRule), values)); - } + AddSelfLink(resources.Url<RulesController>(x => nameof(x.GetRules), values)); - if (resources.CanReadRuleEvents) - { - AddGetLink("events", - resources.Url<RulesController>(x => nameof(x.GetEvents), values)); - } + if (resources.CanCreateRule) + { + AddPostLink("create", + resources.Url<RulesController>(x => nameof(x.PostRule), values)); + } - if (resources.CanDeleteRuleEvents && runningRuleId != null) - { - AddDeleteLink("run/cancel", - resources.Url<RulesController>(x => nameof(x.DeleteRuleRun), values)); - } + if (resources.CanReadRuleEvents) + { + AddGetLink("events", + resources.Url<RulesController>(x => nameof(x.GetEvents), values)); + } - return this; + if (resources.CanDeleteRuleEvents && runningRuleId != null) + { + AddDeleteLink("run/cancel", + resources.Url<RulesController>(x => nameof(x.DeleteRuleRun), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs index 853d607508..1ae6bab6da 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs @@ -10,70 +10,69 @@ using Squidex.Domain.Apps.Entities.Rules.Runner; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed record SimulatedRuleEventDto { - public sealed record SimulatedRuleEventDto - { - /// <summary> - /// The unique event id. - /// </summary> - [Required] - public Guid EventId { get; init; } + /// <summary> + /// The unique event id. + /// </summary> + [Required] + public Guid EventId { get; init; } - /// <summary> - /// The name of the event. - /// </summary> - [Required] - public string EventName { get; set; } + /// <summary> + /// The name of the event. + /// </summary> + [Required] + public string EventName { get; set; } - /// <summary> - /// The source event. - /// </summary> - [Required] - public object Event { get; set; } + /// <summary> + /// The source event. + /// </summary> + [Required] + public object Event { get; set; } - /// <summary> - /// The enriched event. - /// </summary> - public object? EnrichedEvent { get; set; } + /// <summary> + /// The enriched event. + /// </summary> + public object? EnrichedEvent { get; set; } - /// <summary> - /// The data for the action. - /// </summary> - public string? ActionName { get; set; } + /// <summary> + /// The data for the action. + /// </summary> + public string? ActionName { get; set; } - /// <summary> - /// The name of the action. - /// </summary> - public string? ActionData { get; set; } + /// <summary> + /// The name of the action. + /// </summary> + public string? ActionData { get; set; } - /// <summary> - /// The name of the event. - /// </summary> - public string? Error { get; set; } + /// <summary> + /// The name of the event. + /// </summary> + public string? Error { get; set; } - /// <summary> - /// The reason why the event has been skipped. - /// </summary> - [Required] - public List<SkipReason> SkipReasons { get; set; } + /// <summary> + /// The reason why the event has been skipped. + /// </summary> + [Required] + public List<SkipReason> SkipReasons { get; set; } - public static SimulatedRuleEventDto FromDomain(SimulatedRuleEvent ruleEvent) + public static SimulatedRuleEventDto FromDomain(SimulatedRuleEvent ruleEvent) + { + var result = SimpleMapper.Map(ruleEvent, new SimulatedRuleEventDto { - var result = SimpleMapper.Map(ruleEvent, new SimulatedRuleEventDto - { - SkipReasons = new List<SkipReason>() - }); + SkipReasons = new List<SkipReason>() + }); - foreach (var reason in Enum.GetValues<SkipReason>()) + foreach (var reason in Enum.GetValues<SkipReason>()) + { + if (reason != SkipReason.None && ruleEvent.SkipReason.HasFlag(reason)) { - if (reason != SkipReason.None && ruleEvent.SkipReason.HasFlag(reason)) - { - result.SkipReasons.Add(reason); - } + result.SkipReasons.Add(reason); } - - return result; } + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventsDto.cs index 8521848686..af1231e1c4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventsDto.cs @@ -9,30 +9,29 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class SimulatedRuleEventsDto : Resource { - public sealed class SimulatedRuleEventsDto : Resource - { - /// <summary> - /// The simulated rule events. - /// </summary> - [LocalizedRequired] - public SimulatedRuleEventDto[] Items { get; set; } + /// <summary> + /// The simulated rule events. + /// </summary> + [LocalizedRequired] + public SimulatedRuleEventDto[] Items { get; set; } - /// <summary> - /// The total number of simulated rule events. - /// </summary> - public long Total { get; set; } + /// <summary> + /// The total number of simulated rule events. + /// </summary> + public long Total { get; set; } - public static SimulatedRuleEventsDto FromDomain(IList<SimulatedRuleEvent> events) + public static SimulatedRuleEventsDto FromDomain(IList<SimulatedRuleEvent> events) + { + var result = new SimulatedRuleEventsDto { - var result = new SimulatedRuleEventsDto - { - Total = events.Count, - Items = events.Select(SimulatedRuleEventDto.FromDomain).ToArray() - }; + Total = events.Count, + Items = events.Select(SimulatedRuleEventDto.FromDomain).ToArray() + }; - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs index 38f0843baa..9e1f986624 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs @@ -9,18 +9,17 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; + +public sealed class AssetChangedRuleTriggerDto : RuleTriggerDto { - public sealed class AssetChangedRuleTriggerDto : RuleTriggerDto - { - /// <summary> - /// Javascript condition when to trigger. - /// </summary> - public string? Condition { get; set; } + /// <summary> + /// Javascript condition when to trigger. + /// </summary> + public string? Condition { get; set; } - public override RuleTrigger ToTrigger() - { - return SimpleMapper.Map(this, new AssetChangedTriggerV2()); - } + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new AssetChangedTriggerV2()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CommentRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CommentRuleTriggerDto.cs index 30650444de..36b67c4565 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CommentRuleTriggerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/CommentRuleTriggerDto.cs @@ -9,18 +9,17 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; + +public class CommentRuleTriggerDto : RuleTriggerDto { - public class CommentRuleTriggerDto : RuleTriggerDto - { - /// <summary> - /// Javascript condition when to trigger. - /// </summary> - public string? Condition { get; set; } + /// <summary> + /// Javascript condition when to trigger. + /// </summary> + public string? Condition { get; set; } - public override RuleTrigger ToTrigger() - { - return SimpleMapper.Map(this, new CommentTrigger()); - } + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new CommentTrigger()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs index 36363b77fb..301746f0bf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerDto.cs @@ -9,25 +9,24 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Collections; -namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; + +public sealed class ContentChangedRuleTriggerDto : RuleTriggerDto { - public sealed class ContentChangedRuleTriggerDto : RuleTriggerDto - { - /// <summary> - /// The schema settings. - /// </summary> - public ContentChangedRuleTriggerSchemaDto[]? Schemas { get; set; } + /// <summary> + /// The schema settings. + /// </summary> + public ContentChangedRuleTriggerSchemaDto[]? Schemas { get; set; } - /// <summary> - /// Determines whether the trigger should handle all content changes events. - /// </summary> - public bool HandleAll { get; set; } + /// <summary> + /// Determines whether the trigger should handle all content changes events. + /// </summary> + public bool HandleAll { get; set; } - public override RuleTrigger ToTrigger() - { - var schemas = Schemas?.Select(x => x.ToTrigger()).ToReadonlyList(); + public override RuleTrigger ToTrigger() + { + var schemas = Schemas?.Select(x => x.ToTrigger()).ToReadonlyList(); - return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas }; - } + return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs index 0e8e2ccacf..8a9de3abf2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs @@ -9,30 +9,29 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; + +public sealed class ContentChangedRuleTriggerSchemaDto { - public sealed class ContentChangedRuleTriggerSchemaDto - { - /// <summary> - /// The ID of the schema. - /// </summary> - public DomainId SchemaId { get; set; } + /// <summary> + /// The ID of the schema. + /// </summary> + public DomainId SchemaId { get; set; } - /// <summary> - /// Javascript condition when to trigger. - /// </summary> - public string? Condition { get; set; } + /// <summary> + /// Javascript condition when to trigger. + /// </summary> + public string? Condition { get; set; } - public ContentChangedTriggerSchemaV2 ToTrigger() - { - return SimpleMapper.Map(this, new ContentChangedTriggerSchemaV2()); - } + public ContentChangedTriggerSchemaV2 ToTrigger() + { + return SimpleMapper.Map(this, new ContentChangedTriggerSchemaV2()); + } - public static ContentChangedRuleTriggerSchemaDto FromDomain(ContentChangedTriggerSchemaV2 trigger) - { - var result = SimpleMapper.Map(trigger, new ContentChangedRuleTriggerSchemaDto()); + public static ContentChangedRuleTriggerSchemaDto FromDomain(ContentChangedTriggerSchemaV2 trigger) + { + var result = SimpleMapper.Map(trigger, new ContentChangedRuleTriggerSchemaDto()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs index f381f5c84e..8f87a32670 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ManualRuleTriggerDto.cs @@ -9,13 +9,12 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; + +public sealed class ManualRuleTriggerDto : RuleTriggerDto { - public sealed class ManualRuleTriggerDto : RuleTriggerDto + public override RuleTrigger ToTrigger() { - public override RuleTrigger ToTrigger() - { - return SimpleMapper.Map(this, new ManualTrigger()); - } + return SimpleMapper.Map(this, new ManualTrigger()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs index 8db92c01b7..906225f6ff 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs @@ -9,18 +9,17 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; + +public sealed class SchemaChangedRuleTriggerDto : RuleTriggerDto { - public sealed class SchemaChangedRuleTriggerDto : RuleTriggerDto - { - /// <summary> - /// Javascript condition when to trigger. - /// </summary> - public string? Condition { get; set; } + /// <summary> + /// Javascript condition when to trigger. + /// </summary> + public string? Condition { get; set; } - public override RuleTrigger ToTrigger() - { - return SimpleMapper.Map(this, new SchemaChangedTrigger()); - } + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new SchemaChangedTrigger()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs index 0bd27eec0b..f12af9bbe4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs @@ -10,24 +10,23 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers; + +public sealed class UsageRuleTriggerDto : RuleTriggerDto { - public sealed class UsageRuleTriggerDto : RuleTriggerDto - { - /// <summary> - /// The number of monthly api calls. - /// </summary> - public int Limit { get; set; } + /// <summary> + /// The number of monthly api calls. + /// </summary> + public int Limit { get; set; } - /// <summary> - /// The number of days to check or null for the current month. - /// </summary> - [LocalizedRange(1, 30)] - public int? NumDays { get; set; } + /// <summary> + /// The number of days to check or null for the current month. + /// </summary> + [LocalizedRange(1, 30)] + public int? NumDays { get; set; } - public override RuleTrigger ToTrigger() - { - return SimpleMapper.Map(this, new UsageTrigger()); - } + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new UsageTrigger()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs index ec7fbdd6ad..e2c86f5a8c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs @@ -11,38 +11,37 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models; + +public sealed class UpdateRuleDto { - public sealed class UpdateRuleDto + /// <summary> + /// Optional rule name. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// The trigger properties. + /// </summary> + public RuleTriggerDto? Trigger { get; set; } + + /// <summary> + /// The action properties. + /// </summary> + [JsonConverter(typeof(RuleActionConverter))] + public RuleAction? Action { get; set; } + + /// <summary> + /// Enable or disable the rule. + /// </summary> + public bool? IsEnabled { get; set; } + + public UpdateRule ToCommand(DomainId id) { - /// <summary> - /// Optional rule name. - /// </summary> - public string? Name { get; set; } - - /// <summary> - /// The trigger properties. - /// </summary> - public RuleTriggerDto? Trigger { get; set; } - - /// <summary> - /// The action properties. - /// </summary> - [JsonConverter(typeof(RuleActionConverter))] - public RuleAction? Action { get; set; } - - /// <summary> - /// Enable or disable the rule. - /// </summary> - public bool? IsEnabled { get; set; } - - public UpdateRule ToCommand(DomainId id) - { - var command = SimpleMapper.Map(this, new UpdateRule { RuleId = id }); - - command.Trigger = Trigger?.ToTrigger(); - - return command; - } + var command = SimpleMapper.Map(this, new UpdateRule { RuleId = id }); + + command.Trigger = Trigger?.ToTrigger(); + + return command; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index b5eb8fb2df..4ab53ca885 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -24,504 +24,503 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Rules +namespace Squidex.Areas.Api.Controllers.Rules; + +/// <summary> +/// Update and query information about rules. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Rules))] +public sealed class RulesController : ApiController { + private readonly EventJsonSchemaGenerator eventJsonSchemaGenerator; + private readonly IAppProvider appProvider; + private readonly IRuleEventRepository ruleEventsRepository; + private readonly IRuleQueryService ruleQuery; + private readonly IRuleRunnerService ruleRunnerService; + private readonly RuleTypeProvider ruleRegistry; + + public RulesController(ICommandBus commandBus, + IAppProvider appProvider, + IRuleEventRepository ruleEventsRepository, + IRuleQueryService ruleQuery, + IRuleRunnerService ruleRunnerService, + RuleTypeProvider ruleRegistry, + EventJsonSchemaGenerator eventJsonSchemaGenerator) + : base(commandBus) + { + this.appProvider = appProvider; + this.ruleEventsRepository = ruleEventsRepository; + this.ruleQuery = ruleQuery; + this.ruleRunnerService = ruleRunnerService; + this.ruleRegistry = ruleRegistry; + this.eventJsonSchemaGenerator = eventJsonSchemaGenerator; + } + /// <summary> - /// Update and query information about rules. + /// Get supported rule actions. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Rules))] - public sealed class RulesController : ApiController + /// <returns> + /// 200 => Rule actions returned. + /// </returns> + [HttpGet] + [Route("rules/actions/")] + [ProducesResponseType(typeof(Dictionary<string, RuleElementDto>), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public IActionResult GetActions() { - private readonly EventJsonSchemaGenerator eventJsonSchemaGenerator; - private readonly IAppProvider appProvider; - private readonly IRuleEventRepository ruleEventsRepository; - private readonly IRuleQueryService ruleQuery; - private readonly IRuleRunnerService ruleRunnerService; - private readonly RuleTypeProvider ruleRegistry; - - public RulesController(ICommandBus commandBus, - IAppProvider appProvider, - IRuleEventRepository ruleEventsRepository, - IRuleQueryService ruleQuery, - IRuleRunnerService ruleRunnerService, - RuleTypeProvider ruleRegistry, - EventJsonSchemaGenerator eventJsonSchemaGenerator) - : base(commandBus) - { - this.appProvider = appProvider; - this.ruleEventsRepository = ruleEventsRepository; - this.ruleQuery = ruleQuery; - this.ruleRunnerService = ruleRunnerService; - this.ruleRegistry = ruleRegistry; - this.eventJsonSchemaGenerator = eventJsonSchemaGenerator; - } + var etag = string.Concat(ruleRegistry.Actions.Select(x => x.Key)).ToSha256Base64(); - /// <summary> - /// Get supported rule actions. - /// </summary> - /// <returns> - /// 200 => Rule actions returned. - /// </returns> - [HttpGet] - [Route("rules/actions/")] - [ProducesResponseType(typeof(Dictionary<string, RuleElementDto>), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(0)] - public IActionResult GetActions() + var response = Deferred.Response(() => { - var etag = string.Concat(ruleRegistry.Actions.Select(x => x.Key)).ToSha256Base64(); + return ruleRegistry.Actions.ToDictionary(x => x.Key, x => RuleElementDto.FromDomain(x.Value)); + }); - var response = Deferred.Response(() => - { - return ruleRegistry.Actions.ToDictionary(x => x.Key, x => RuleElementDto.FromDomain(x.Value)); - }); + Response.Headers[HeaderNames.ETag] = etag; - Response.Headers[HeaderNames.ETag] = etag; + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Get rules. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Rules returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/rules/")] + [ProducesResponseType(typeof(RulesDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesRead)] + [ApiCosts(1)] + public async Task<IActionResult> GetRules(string app) + { + var rules = await ruleQuery.QueryAsync(Context, HttpContext.RequestAborted); - /// <summary> - /// Get rules. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Rules returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/rules/")] - [ProducesResponseType(typeof(RulesDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesRead)] - [ApiCosts(1)] - public async Task<IActionResult> GetRules(string app) + var response = Deferred.AsyncResponse(() => { - var rules = await ruleQuery.QueryAsync(Context, HttpContext.RequestAborted); + return RulesDto.FromRulesAsync(rules, ruleRunnerService, Resources); + }); - var response = Deferred.AsyncResponse(() => - { - return RulesDto.FromRulesAsync(rules, ruleRunnerService, Resources); - }); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Create a new rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">The rule object that needs to be added to the app.</param> + /// <returns> + /// 201 => Rule created. + /// 400 => Rule request not valid. + /// 404 => App not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/rules/")] + [ProducesResponseType(typeof(RuleDto), 201)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostRule(string app, [FromBody] CreateRuleDto request) + { + var command = request.ToCommand(); - /// <summary> - /// Create a new rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The rule object that needs to be added to the app.</param> - /// <returns> - /// 201 => Rule created. - /// 400 => Rule request not valid. - /// 404 => App not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/rules/")] - [ProducesResponseType(typeof(RuleDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostRule(string app, [FromBody] CreateRuleDto request) - { - var command = request.ToCommand(); + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return CreatedAtAction(nameof(GetRules), new { app }, response); + } - return CreatedAtAction(nameof(GetRules), new { app }, response); - } + /// <summary> + /// Cancel the current run. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 204 => Rule run cancelled. + /// </returns> + [HttpDelete] + [Route("apps/{app}/rules/run")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteRuleRun(string app) + { + await ruleRunnerService.CancelAsync(App.Id, HttpContext.RequestAborted); - /// <summary> - /// Cancel the current run. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 204 => Rule run cancelled. - /// </returns> - [HttpDelete] - [Route("apps/{app}/rules/run")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteRuleRun(string app) - { - await ruleRunnerService.CancelAsync(App.Id, HttpContext.RequestAborted); + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Update a rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the rule to update.</param> + /// <param name="request">The rule object that needs to be added to the app.</param> + /// <returns> + /// 200 => Rule updated. + /// 400 => Rule request not valid. + /// 404 => Rule or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/rules/{id}/")] + [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutRule(string app, DomainId id, [FromBody] UpdateRuleDto request) + { + var command = request.ToCommand(id); - /// <summary> - /// Update a rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the rule to update.</param> - /// <param name="request">The rule object that needs to be added to the app.</param> - /// <returns> - /// 200 => Rule updated. - /// 400 => Rule request not valid. - /// 404 => Rule or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/rules/{id}/")] - [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutRule(string app, DomainId id, [FromBody] UpdateRuleDto request) - { - var command = request.ToCommand(id); + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Enable a rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the rule to enable.</param> + /// <returns> + /// 200 => Rule enabled. + /// 404 => Rule or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/rules/{id}/enable/")] + [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesDisable)] + [ApiCosts(1)] + public async Task<IActionResult> EnableRule(string app, DomainId id) + { + var command = new EnableRule { RuleId = id }; - /// <summary> - /// Enable a rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the rule to enable.</param> - /// <returns> - /// 200 => Rule enabled. - /// 404 => Rule or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/rules/{id}/enable/")] - [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesDisable)] - [ApiCosts(1)] - public async Task<IActionResult> EnableRule(string app, DomainId id) - { - var command = new EnableRule { RuleId = id }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Disable a rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the rule to disable.</param> + /// <returns> + /// 200 => Rule disabled. + /// 404 => Rule or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/rules/{id}/disable/")] + [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesDisable)] + [ApiCosts(1)] + public async Task<IActionResult> DisableRule(string app, DomainId id) + { + var command = new DisableRule { RuleId = id }; - /// <summary> - /// Disable a rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the rule to disable.</param> - /// <returns> - /// 200 => Rule disabled. - /// 404 => Rule or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/rules/{id}/disable/")] - [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesDisable)] - [ApiCosts(1)] - public async Task<IActionResult> DisableRule(string app, DomainId id) - { - var command = new DisableRule { RuleId = id }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Trigger a rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the rule to disable.</param> + /// <returns> + /// 204 => Rule triggered. + /// 404 => Rule or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/rules/{id}/trigger/")] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRun)] + [ApiCosts(1)] + public async Task<IActionResult> TriggerRule(string app, DomainId id) + { + var command = new TriggerRule { RuleId = id }; - /// <summary> - /// Trigger a rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the rule to disable.</param> - /// <returns> - /// 204 => Rule triggered. - /// 404 => Rule or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/rules/{id}/trigger/")] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRun)] - [ApiCosts(1)] - public async Task<IActionResult> TriggerRule(string app, DomainId id) - { - var command = new TriggerRule { RuleId = id }; + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Run a rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the rule to run.</param> + /// <param name="fromSnapshots">Runs the rule from snapeshots if possible.</param> + /// <returns> + /// 204 => Rule started. + /// </returns> + [HttpPut] + [Route("apps/{app}/rules/{id}/run")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRun)] + [ApiCosts(1)] + public async Task<IActionResult> PutRuleRun(string app, DomainId id, [FromQuery] bool fromSnapshots = false) + { + await ruleRunnerService.RunAsync(App.Id, id, fromSnapshots, HttpContext.RequestAborted); - /// <summary> - /// Run a rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the rule to run.</param> - /// <param name="fromSnapshots">Runs the rule from snapeshots if possible.</param> - /// <returns> - /// 204 => Rule started. - /// </returns> - [HttpPut] - [Route("apps/{app}/rules/{id}/run")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRun)] - [ApiCosts(1)] - public async Task<IActionResult> PutRuleRun(string app, DomainId id, [FromQuery] bool fromSnapshots = false) - { - await ruleRunnerService.RunAsync(App.Id, id, fromSnapshots, HttpContext.RequestAborted); + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Cancels all rule events. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the rule to cancel.</param> + /// <returns> + /// 204 => Rule events cancelled. + /// </returns> + [HttpDelete] + [Route("apps/{app}/rules/{id}/events/")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsDelete)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteRuleEvents(string app, DomainId id) + { + await ruleEventsRepository.CancelByRuleAsync(id, HttpContext.RequestAborted); - /// <summary> - /// Cancels all rule events. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the rule to cancel.</param> - /// <returns> - /// 204 => Rule events cancelled. - /// </returns> - [HttpDelete] - [Route("apps/{app}/rules/{id}/events/")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsDelete)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteRuleEvents(string app, DomainId id) - { - await ruleEventsRepository.CancelByRuleAsync(id, HttpContext.RequestAborted); + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Simulate a rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">The rule to simulate.</param> + /// <returns> + /// 200 => Rule simulated. + /// 404 => Rule or app not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/rules/simulate/")] + [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRead)] + [ApiCosts(5)] + public async Task<IActionResult> Simulate(string app, [FromBody] CreateRuleDto request) + { + var rule = request.ToRule(); - /// <summary> - /// Simulate a rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The rule to simulate.</param> - /// <returns> - /// 200 => Rule simulated. - /// 404 => Rule or app not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/rules/simulate/")] - [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRead)] - [ApiCosts(5)] - public async Task<IActionResult> Simulate(string app, [FromBody] CreateRuleDto request) - { - var rule = request.ToRule(); + var simulation = await ruleRunnerService.SimulateAsync(App.NamedId(), DomainId.Empty, rule, HttpContext.RequestAborted); - var simulation = await ruleRunnerService.SimulateAsync(App.NamedId(), DomainId.Empty, rule, HttpContext.RequestAborted); + var response = SimulatedRuleEventsDto.FromDomain(simulation); - var response = SimulatedRuleEventsDto.FromDomain(simulation); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Simulate a rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the rule to simulate.</param> + /// <returns> + /// 200 => Rule simulated. + /// 404 => Rule or app not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/rules/{id}/simulate/")] + [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRead)] + [ApiCosts(5)] + public async Task<IActionResult> Simulate(string app, DomainId id) + { + var rule = await appProvider.GetRuleAsync(AppId, id, HttpContext.RequestAborted); - /// <summary> - /// Simulate a rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the rule to simulate.</param> - /// <returns> - /// 200 => Rule simulated. - /// 404 => Rule or app not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/rules/{id}/simulate/")] - [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRead)] - [ApiCosts(5)] - public async Task<IActionResult> Simulate(string app, DomainId id) + if (rule == null) { - var rule = await appProvider.GetRuleAsync(AppId, id, HttpContext.RequestAborted); + return NotFound(); + } - if (rule == null) - { - return NotFound(); - } + var simulation = await ruleRunnerService.SimulateAsync(rule, HttpContext.RequestAborted); - var simulation = await ruleRunnerService.SimulateAsync(rule, HttpContext.RequestAborted); + var response = SimulatedRuleEventsDto.FromDomain(simulation); - var response = SimulatedRuleEventsDto.FromDomain(simulation); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Delete a rule. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The ID of the rule to delete.</param> + /// <returns> + /// 204 => Rule deleted. + /// 404 => Rule or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/rules/{id}/")] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesDelete)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteRule(string app, DomainId id) + { + var command = new DeleteRule { RuleId = id }; - /// <summary> - /// Delete a rule. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The ID of the rule to delete.</param> - /// <returns> - /// 204 => Rule deleted. - /// 404 => Rule or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/rules/{id}/")] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesDelete)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteRule(string app, DomainId id) - { - var command = new DeleteRule { RuleId = id }; + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Get rule events. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="ruleId">The optional rule id to filter to events.</param> + /// <param name="skip">The number of events to skip.</param> + /// <param name="take">The number of events to take.</param> + /// <returns> + /// 200 => Rule events returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/rules/events/")] + [ProducesResponseType(typeof(RuleEventsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRead)] + [ApiCosts(0)] + public async Task<IActionResult> GetEvents(string app, [FromQuery] DomainId? ruleId = null, [FromQuery] int skip = 0, [FromQuery] int take = 20) + { + var ruleEvents = await ruleEventsRepository.QueryByAppAsync(AppId, ruleId, skip, take, HttpContext.RequestAborted); - /// <summary> - /// Get rule events. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="ruleId">The optional rule id to filter to events.</param> - /// <param name="skip">The number of events to skip.</param> - /// <param name="take">The number of events to take.</param> - /// <returns> - /// 200 => Rule events returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/rules/events/")] - [ProducesResponseType(typeof(RuleEventsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsRead)] - [ApiCosts(0)] - public async Task<IActionResult> GetEvents(string app, [FromQuery] DomainId? ruleId = null, [FromQuery] int skip = 0, [FromQuery] int take = 20) - { - var ruleEvents = await ruleEventsRepository.QueryByAppAsync(AppId, ruleId, skip, take, HttpContext.RequestAborted); + var response = RuleEventsDto.FromDomain(ruleEvents, Resources, ruleId); - var response = RuleEventsDto.FromDomain(ruleEvents, Resources, ruleId); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Retry the event immediately. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The event to enqueue.</param> + /// <returns> + /// 204 => Rule enqueued. + /// 404 => App or rule event not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/rules/events/{id}/")] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsUpdate)] + [ApiCosts(0)] + public async Task<IActionResult> PutEvent(string app, DomainId id) + { + var ruleEvent = await ruleEventsRepository.FindAsync(id, HttpContext.RequestAborted); - /// <summary> - /// Retry the event immediately. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The event to enqueue.</param> - /// <returns> - /// 204 => Rule enqueued. - /// 404 => App or rule event not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/rules/events/{id}/")] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsUpdate)] - [ApiCosts(0)] - public async Task<IActionResult> PutEvent(string app, DomainId id) + if (ruleEvent == null) { - var ruleEvent = await ruleEventsRepository.FindAsync(id, HttpContext.RequestAborted); + return NotFound(); + } - if (ruleEvent == null) - { - return NotFound(); - } + await ruleEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant(), HttpContext.RequestAborted); - await ruleEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant(), HttpContext.RequestAborted); + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Cancels all events. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 204 => Events cancelled. + /// </returns> + [HttpDelete] + [Route("apps/{app}/rules/events/")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsDelete)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteEvents(string app) + { + await ruleEventsRepository.CancelByAppAsync(App.Id, HttpContext.RequestAborted); - /// <summary> - /// Cancels all events. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 204 => Events cancelled. - /// </returns> - [HttpDelete] - [Route("apps/{app}/rules/events/")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsDelete)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteEvents(string app) - { - await ruleEventsRepository.CancelByAppAsync(App.Id, HttpContext.RequestAborted); + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Cancels an event. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="id">The event to enqueue.</param> + /// <returns> + /// 204 => Rule deqeued. + /// 404 => App or rule event not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/rules/events/{id}/")] + [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsDelete)] + [ApiCosts(0)] + public async Task<IActionResult> DeleteEvent(string app, DomainId id) + { + var ruleEvent = await ruleEventsRepository.FindAsync(id, HttpContext.RequestAborted); - /// <summary> - /// Cancels an event. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="id">The event to enqueue.</param> - /// <returns> - /// 204 => Rule deqeued. - /// 404 => App or rule event not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/rules/events/{id}/")] - [ApiPermissionOrAnonymous(PermissionIds.AppRulesEventsDelete)] - [ApiCosts(0)] - public async Task<IActionResult> DeleteEvent(string app, DomainId id) + if (ruleEvent == null) { - var ruleEvent = await ruleEventsRepository.FindAsync(id, HttpContext.RequestAborted); + return NotFound(); + } - if (ruleEvent == null) - { - return NotFound(); - } + await ruleEventsRepository.CancelByRuleAsync(id, HttpContext.RequestAborted); - await ruleEventsRepository.CancelByRuleAsync(id, HttpContext.RequestAborted); + return NoContent(); + } - return NoContent(); - } + /// <summary> + /// Provide a list of all event types that are used in rules. + /// </summary> + /// <returns> + /// 200 => Rule events returned. + /// </returns> + [HttpGet] + [Route("rules/eventtypes")] + [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] + [AllowAnonymous] + public IActionResult GetEventTypes() + { + var types = eventJsonSchemaGenerator.AllTypes; - /// <summary> - /// Provide a list of all event types that are used in rules. - /// </summary> - /// <returns> - /// 200 => Rule events returned. - /// </returns> - [HttpGet] - [Route("rules/eventtypes")] - [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] - [AllowAnonymous] - public IActionResult GetEventTypes() - { - var types = eventJsonSchemaGenerator.AllTypes; + return Ok(types); + } - return Ok(types); - } + /// <summary> + /// Provide the json schema for the event with the specified name. + /// </summary> + /// <param name="type">The type name of the event.</param> + /// <returns> + /// 200 => Rule event type found. + /// 404 => Rule event not found. + /// </returns> + [HttpGet] + [Route("rules/eventtypes/{type}")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [AllowAnonymous] + public IActionResult GetEventSchema(string type) + { + var schema = eventJsonSchemaGenerator.GetSchema(type); - /// <summary> - /// Provide the json schema for the event with the specified name. - /// </summary> - /// <param name="type">The type name of the event.</param> - /// <returns> - /// 200 => Rule event type found. - /// 404 => Rule event not found. - /// </returns> - [HttpGet] - [Route("rules/eventtypes/{type}")] - [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [AllowAnonymous] - public IActionResult GetEventSchema(string type) + if (schema == null) { - var schema = eventJsonSchemaGenerator.GetSchema(type); - - if (schema == null) - { - return NotFound(); - } - - return Content(schema.ToJson(), "application/json"); + return NotFound(); } - [HttpGet] - [Route("apps/{app}/rules/completion/{triggerType}")] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - [OpenApiIgnore] - public IActionResult GetScriptCompletion(string app, string triggerType, - [FromServices] ScriptingCompleter completer) - { - var completion = completer.Trigger(triggerType); + return Content(schema.ToJson(), "application/json"); + } - return Ok(completion); - } + [HttpGet] + [Route("apps/{app}/rules/completion/{triggerType}")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + [OpenApiIgnore] + public IActionResult GetScriptCompletion(string app, string triggerType, + [FromServices] ScriptingCompleter completer) + { + var completion = completer.Trigger(triggerType); - private async Task<RuleDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return Ok(completion); + } - var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id, HttpContext.RequestAborted); + private async Task<RuleDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var result = context.Result<IEnrichedRuleEntity>(); - var response = RuleDto.FromDomain(result, runningRuleId == null, ruleRunnerService, Resources); + var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id, HttpContext.RequestAborted); - return response; - } + var result = context.Result<IEnrichedRuleEntity>(); + var response = RuleDto.FromDomain(result, runningRuleId == null, ruleRunnerService, Resources); + + return response; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs index 0696b1e237..50605073c4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs @@ -9,31 +9,30 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class AddFieldDto { - public sealed class AddFieldDto - { - /// <summary> - /// The name of the field. Must be unique within the schema. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$")] - public string Name { get; set; } + /// <summary> + /// The name of the field. Must be unique within the schema. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$")] + public string Name { get; set; } - /// <summary> - /// Determines the optional partitioning of the field. - /// </summary> - public string? Partitioning { get; set; } + /// <summary> + /// Determines the optional partitioning of the field. + /// </summary> + public string? Partitioning { get; set; } - /// <summary> - /// The field properties. - /// </summary> - [LocalizedRequired] - public FieldPropertiesDto Properties { get; set; } + /// <summary> + /// The field properties. + /// </summary> + [LocalizedRequired] + public FieldPropertiesDto Properties { get; set; } - public AddField ToCommand(long? parentId = null) - { - return SimpleMapper.Map(this, new AddField { ParentFieldId = parentId, Properties = Properties.ToProperties() }); - } + public AddField ToCommand(long? parentId = null) + { + return SimpleMapper.Map(this, new AddField { ParentFieldId = parentId, Properties = Properties.ToProperties() }); } } \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs index 6e3764d19b..e4a2eac1ae 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ChangeCategoryDto.cs @@ -8,18 +8,17 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class ChangeCategoryDto { - public sealed class ChangeCategoryDto - { - /// <summary> - /// The name of the category. - /// </summary> - public string? Name { get; set; } + /// <summary> + /// The name of the category. + /// </summary> + public string? Name { get; set; } - public ChangeCategory ToCommand() - { - return SimpleMapper.Map(this, new ChangeCategory()); - } + public ChangeCategory ToCommand() + { + return SimpleMapper.Map(this, new ChangeCategory()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs index 7791ce71f4..7346bf531d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs @@ -7,21 +7,20 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class ConfigureFieldRulesDto { - public sealed class ConfigureFieldRulesDto - { - /// <summary> - /// The field rules to configure. - /// </summary> - public FieldRuleDto[]? FieldRules { get; set; } + /// <summary> + /// The field rules to configure. + /// </summary> + public FieldRuleDto[]? FieldRules { get; set; } - public ConfigureFieldRules ToCommand() + public ConfigureFieldRules ToCommand() + { + return new ConfigureFieldRules { - return new ConfigureFieldRules - { - FieldRules = FieldRules?.Select(x => x.ToCommand()).ToArray() - }; - } + FieldRules = FieldRules?.Select(x => x.ToCommand()).ToArray() + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs index 1a6df76712..398dde954d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs @@ -8,16 +8,15 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Collections; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class ConfigurePreviewUrlsDto : Dictionary<string, string> { - public sealed class ConfigurePreviewUrlsDto : Dictionary<string, string> + public ConfigurePreviewUrls ToCommand() { - public ConfigurePreviewUrls ToCommand() + return new ConfigurePreviewUrls { - return new ConfigurePreviewUrls - { - PreviewUrls = new Dictionary<string, string>(this).ToReadonlyDictionary() - }; - } + PreviewUrls = new Dictionary<string, string>(this).ToReadonlyDictionary() + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureUIFieldsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureUIFieldsDto.cs index 2fd9bf8f29..dc6d4f56e9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureUIFieldsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureUIFieldsDto.cs @@ -9,23 +9,22 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class ConfigureUIFieldsDto { - public sealed class ConfigureUIFieldsDto - { - /// <summary> - /// The name of fields that are used in content lists. - /// </summary> - public FieldNames? FieldsInLists { get; set; } + /// <summary> + /// The name of fields that are used in content lists. + /// </summary> + public FieldNames? FieldsInLists { get; set; } - /// <summary> - /// The name of fields that are used in content references. - /// </summary> - public FieldNames? FieldsInReferences { get; set; } + /// <summary> + /// The name of fields that are used in content references. + /// </summary> + public FieldNames? FieldsInReferences { get; set; } - public ConfigureUIFields ToCommand() - { - return SimpleMapper.Map(this, new ConfigureUIFields()); - } + public ConfigureUIFields ToCommand() + { + return SimpleMapper.Map(this, new ConfigureUIFields()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs index 17d85661e3..68a16d1369 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs @@ -10,84 +10,83 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters; + +internal sealed class FieldPropertiesDtoFactory : IFieldPropertiesVisitor<FieldPropertiesDto, None> { - internal sealed class FieldPropertiesDtoFactory : IFieldPropertiesVisitor<FieldPropertiesDto, None> + private static readonly FieldPropertiesDtoFactory Instance = new FieldPropertiesDtoFactory(); + + private FieldPropertiesDtoFactory() + { + } + + public static FieldPropertiesDto Create(FieldProperties properties) + { + return properties.Accept(Instance, None.Value); + } + + public FieldPropertiesDto Visit(ArrayFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new ArrayFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(AssetsFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new AssetsFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(BooleanFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new BooleanFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(ComponentFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new ComponentFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(ComponentsFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new ComponentsFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(DateTimeFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new DateTimeFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(GeolocationFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new GeolocationFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(JsonFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new JsonFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(NumberFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new NumberFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(ReferencesFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new ReferencesFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(StringFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new StringFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(TagsFieldProperties properties, None args) + { + return SimpleMapper.Map(properties, new TagsFieldPropertiesDto()); + } + + public FieldPropertiesDto Visit(UIFieldProperties properties, None args) { - private static readonly FieldPropertiesDtoFactory Instance = new FieldPropertiesDtoFactory(); - - private FieldPropertiesDtoFactory() - { - } - - public static FieldPropertiesDto Create(FieldProperties properties) - { - return properties.Accept(Instance, None.Value); - } - - public FieldPropertiesDto Visit(ArrayFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new ArrayFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(AssetsFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new AssetsFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(BooleanFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new BooleanFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(ComponentFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new ComponentFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(ComponentsFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new ComponentsFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(DateTimeFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new DateTimeFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(GeolocationFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new GeolocationFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(JsonFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new JsonFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(NumberFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new NumberFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(ReferencesFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new ReferencesFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(StringFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new StringFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(TagsFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new TagsFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(UIFieldProperties properties, None args) - { - return SimpleMapper.Map(properties, new UIFieldPropertiesDto()); - } + return SimpleMapper.Map(properties, new UIFieldPropertiesDto()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs index 7d607f7eab..29726796c4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs @@ -9,41 +9,40 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class CreateSchemaDto : UpsertSchemaDto { - public sealed class CreateSchemaDto : UpsertSchemaDto - { - /// <summary> - /// The name of the schema. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Name { get; set; } + /// <summary> + /// The name of the schema. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Name { get; set; } - /// <summary> - /// The type of the schema. - /// </summary> - public SchemaType Type { get; set; } + /// <summary> + /// The type of the schema. + /// </summary> + public SchemaType Type { get; set; } - /// <summary> - /// Set to true to allow a single content item only. - /// </summary> - [Obsolete("Use 'type' field now.")] - public bool IsSingleton + /// <summary> + /// Set to true to allow a single content item only. + /// </summary> + [Obsolete("Use 'type' field now.")] + public bool IsSingleton + { + get => Type == SchemaType.Singleton; + set { - get => Type == SchemaType.Singleton; - set + if (value) { - if (value) - { - Type = SchemaType.Singleton; - } + Type = SchemaType.Singleton; } } + } - public CreateSchema ToCommand() - { - return ToCommand(this, new CreateSchema()); - } + public CreateSchema ToCommand() + { + return ToCommand(this, new CreateSchema()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs index 8b4f933dac..df4059a22a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs @@ -11,158 +11,157 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class FieldDto : Resource { - public sealed class FieldDto : Resource + /// <summary> + /// The ID of the field. + /// </summary> + public long FieldId { get; set; } + + /// <summary> + /// The name of the field. Must be unique within the schema. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Name { get; set; } + + /// <summary> + /// Defines if the field is hidden. + /// </summary> + public bool IsHidden { get; set; } + + /// <summary> + /// Defines if the field is locked. + /// </summary> + public bool IsLocked { get; set; } + + /// <summary> + /// Defines if the field is disabled. + /// </summary> + public bool IsDisabled { get; set; } + + /// <summary> + /// Defines the partitioning of the field. + /// </summary> + [LocalizedRequired] + public string Partitioning { get; set; } + + /// <summary> + /// The field properties. + /// </summary> + [LocalizedRequired] + public FieldPropertiesDto Properties { get; set; } + + /// <summary> + /// The nested fields. + /// </summary> + public List<NestedFieldDto>? Nested { get; set; } + + public static NestedFieldDto FromDomain(NestedField field) { - /// <summary> - /// The ID of the field. - /// </summary> - public long FieldId { get; set; } - - /// <summary> - /// The name of the field. Must be unique within the schema. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Name { get; set; } - - /// <summary> - /// Defines if the field is hidden. - /// </summary> - public bool IsHidden { get; set; } - - /// <summary> - /// Defines if the field is locked. - /// </summary> - public bool IsLocked { get; set; } - - /// <summary> - /// Defines if the field is disabled. - /// </summary> - public bool IsDisabled { get; set; } - - /// <summary> - /// Defines the partitioning of the field. - /// </summary> - [LocalizedRequired] - public string Partitioning { get; set; } - - /// <summary> - /// The field properties. - /// </summary> - [LocalizedRequired] - public FieldPropertiesDto Properties { get; set; } - - /// <summary> - /// The nested fields. - /// </summary> - public List<NestedFieldDto>? Nested { get; set; } - - public static NestedFieldDto FromDomain(NestedField field) - { - var properties = FieldPropertiesDtoFactory.Create(field.RawProperties); + var properties = FieldPropertiesDtoFactory.Create(field.RawProperties); - var result = - SimpleMapper.Map(field, - new NestedFieldDto - { - FieldId = field.Id, - Properties = properties - }); + var result = + SimpleMapper.Map(field, + new NestedFieldDto + { + FieldId = field.Id, + Properties = properties + }); - return result; - } + return result; + } - public static FieldDto FromDomain(RootField field) - { - var properties = FieldPropertiesDtoFactory.Create(field.RawProperties); - - var result = - SimpleMapper.Map(field, - new FieldDto - { - FieldId = field.Id, - Properties = properties, - Partitioning = field.Partitioning.Key - }); - - if (field is IArrayField arrayField) - { - result.Nested = new List<NestedFieldDto>(); + public static FieldDto FromDomain(RootField field) + { + var properties = FieldPropertiesDtoFactory.Create(field.RawProperties); - foreach (var nestedField in arrayField.Fields) + var result = + SimpleMapper.Map(field, + new FieldDto { - result.Nested.Add(FromDomain(nestedField)); - } - } - - return result; - } + FieldId = field.Id, + Properties = properties, + Partitioning = field.Partitioning.Key + }); - public void CreateLinks(Resources resources, string schema, bool allowUpdate) + if (field is IArrayField arrayField) { - allowUpdate = allowUpdate && !IsLocked; + result.Nested = new List<NestedFieldDto>(); - if (allowUpdate) + foreach (var nestedField in arrayField.Fields) { - var values = new { app = resources.App, schema, id = FieldId }; + result.Nested.Add(FromDomain(nestedField)); + } + } - AddPutLink("update", - resources.Url<SchemaFieldsController>(x => nameof(x.PutField), values)); + return result; + } - if (IsHidden) - { - AddPutLink("show", - resources.Url<SchemaFieldsController>(x => nameof(x.ShowField), values)); - } - else - { - AddPutLink("hide", - resources.Url<SchemaFieldsController>(x => nameof(x.HideField), values)); - } + public void CreateLinks(Resources resources, string schema, bool allowUpdate) + { + allowUpdate = allowUpdate && !IsLocked; - if (IsDisabled) - { - AddPutLink("enable", - resources.Url<SchemaFieldsController>(x => nameof(x.EnableField), values)); - } - else - { - AddPutLink("disable", - resources.Url<SchemaFieldsController>(x => nameof(x.DisableField), values)); - } + if (allowUpdate) + { + var values = new { app = resources.App, schema, id = FieldId }; - if (Nested != null) - { - var parentValues = new { values.app, values.schema, parentId = FieldId }; + AddPutLink("update", + resources.Url<SchemaFieldsController>(x => nameof(x.PutField), values)); - AddPostLink("fields/add", - resources.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues)); + if (IsHidden) + { + AddPutLink("show", + resources.Url<SchemaFieldsController>(x => nameof(x.ShowField), values)); + } + else + { + AddPutLink("hide", + resources.Url<SchemaFieldsController>(x => nameof(x.HideField), values)); + } - if (Nested.Count > 0) - { - AddPutLink("fields/order", - resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedFieldOrdering), parentValues)); - } - } + if (IsDisabled) + { + AddPutLink("enable", + resources.Url<SchemaFieldsController>(x => nameof(x.EnableField), values)); + } + else + { + AddPutLink("disable", + resources.Url<SchemaFieldsController>(x => nameof(x.DisableField), values)); + } - if (!IsLocked) + if (Nested != null) + { + var parentValues = new { values.app, values.schema, parentId = FieldId }; + + AddPostLink("fields/add", + resources.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues)); + + if (Nested.Count > 0) { - AddPutLink("lock", - resources.Url<SchemaFieldsController>(x => nameof(x.LockField), values)); + AddPutLink("fields/order", + resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedFieldOrdering), parentValues)); } + } - AddDeleteLink("delete", - resources.Url<SchemaFieldsController>(x => nameof(x.DeleteField), values)); + if (!IsLocked) + { + AddPutLink("lock", + resources.Url<SchemaFieldsController>(x => nameof(x.LockField), values)); } - if (Nested != null) + AddDeleteLink("delete", + resources.Url<SchemaFieldsController>(x => nameof(x.DeleteField), values)); + } + + if (Nested != null) + { + foreach (var nested in Nested) { - foreach (var nested in Nested) - { - nested.CreateLinks(resources, schema, FieldId, allowUpdate); - } + nested.CreateLinks(resources, schema, FieldId, allowUpdate); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs index 947f053b46..409d487442 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -11,62 +11,61 @@ using Squidex.Infrastructure.Validation; using Squidex.Web.Json; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +[JsonInheritanceConverter(typeof(FieldPropertiesDto), "fieldType")] +[KnownType(nameof(Subtypes))] +public abstract class FieldPropertiesDto { - [JsonInheritanceConverter(typeof(FieldPropertiesDto), "fieldType")] - [KnownType(nameof(Subtypes))] - public abstract class FieldPropertiesDto - { - /// <summary> - /// Optional label for the editor. - /// </summary> - [LocalizedStringLength(100)] - public string? Label { get; set; } + /// <summary> + /// Optional label for the editor. + /// </summary> + [LocalizedStringLength(100)] + public string? Label { get; set; } - /// <summary> - /// Hints to describe the field. - /// </summary> - [LocalizedStringLength(1000)] - public string? Hints { get; set; } + /// <summary> + /// Hints to describe the field. + /// </summary> + [LocalizedStringLength(1000)] + public string? Hints { get; set; } - /// <summary> - /// Placeholder to show when no value has been entered. - /// </summary> - [LocalizedStringLength(100)] - public string? Placeholder { get; set; } + /// <summary> + /// Placeholder to show when no value has been entered. + /// </summary> + [LocalizedStringLength(100)] + public string? Placeholder { get; set; } - /// <summary> - /// Indicates if the field is required. - /// </summary> - public bool IsRequired { get; set; } + /// <summary> + /// Indicates if the field is required. + /// </summary> + public bool IsRequired { get; set; } - /// <summary> - /// Indicates if the field is required when publishing. - /// </summary> - public bool IsRequiredOnPublish { get; set; } + /// <summary> + /// Indicates if the field is required when publishing. + /// </summary> + public bool IsRequiredOnPublish { get; set; } - /// <summary> - /// Indicates if the field should be rendered with half width only. - /// </summary> - public bool IsHalfWidth { get; set; } + /// <summary> + /// Indicates if the field should be rendered with half width only. + /// </summary> + public bool IsHalfWidth { get; set; } - /// <summary> - /// Optional url to the editor. - /// </summary> - public string? EditorUrl { get; set; } + /// <summary> + /// Optional url to the editor. + /// </summary> + public string? EditorUrl { get; set; } - /// <summary> - /// Tags for automation processes. - /// </summary> - public ReadonlyList<string>? Tags { get; set; } + /// <summary> + /// Tags for automation processes. + /// </summary> + public ReadonlyList<string>? Tags { get; set; } - public abstract FieldProperties ToProperties(); + public abstract FieldProperties ToProperties(); - public static Type[] Subtypes() - { - var type = typeof(FieldPropertiesDto); + public static Type[] Subtypes() + { + var type = typeof(FieldPropertiesDto); - return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); - } + return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldRuleDto.cs index 5a24219ed8..4278ef57b7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldRuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldRuleDto.cs @@ -10,35 +10,34 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class FieldRuleDto { - public sealed class FieldRuleDto - { - /// <summary> - /// The action to perform when the condition is met. - /// </summary> - [LocalizedRequired] - public FieldRuleAction Action { get; set; } + /// <summary> + /// The action to perform when the condition is met. + /// </summary> + [LocalizedRequired] + public FieldRuleAction Action { get; set; } - /// <summary> - /// The field to update. - /// </summary> - [LocalizedRequired] - public string Field { get; set; } + /// <summary> + /// The field to update. + /// </summary> + [LocalizedRequired] + public string Field { get; set; } - /// <summary> - /// The condition. - /// </summary> - public string? Condition { get; set; } + /// <summary> + /// The condition. + /// </summary> + public string? Condition { get; set; } - public static FieldRuleDto FromDomain(FieldRule fieldRule) - { - return SimpleMapper.Map(fieldRule, new FieldRuleDto()); - } + public static FieldRuleDto FromDomain(FieldRule fieldRule) + { + return SimpleMapper.Map(fieldRule, new FieldRuleDto()); + } - public FieldRuleCommand ToCommand() - { - return SimpleMapper.Map(this, new FieldRuleCommand()); - } + public FieldRuleCommand ToCommand() + { + return SimpleMapper.Map(this, new FieldRuleCommand()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs index 68f412b52d..5d7f2bae62 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs @@ -9,30 +9,29 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class ArrayFieldPropertiesDto : FieldPropertiesDto { - public sealed class ArrayFieldPropertiesDto : FieldPropertiesDto - { - /// <summary> - /// The minimum allowed items for the field value. - /// </summary> - public int? MinItems { get; set; } + /// <summary> + /// The minimum allowed items for the field value. + /// </summary> + public int? MinItems { get; set; } - /// <summary> - /// The maximum allowed items for the field value. - /// </summary> - public int? MaxItems { get; set; } + /// <summary> + /// The maximum allowed items for the field value. + /// </summary> + public int? MaxItems { get; set; } - /// <summary> - /// The fields that must be unique. - /// </summary> - public ReadonlyList<string>? UniqueFields { get; set; } + /// <summary> + /// The fields that must be unique. + /// </summary> + public ReadonlyList<string>? UniqueFields { get; set; } - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new ArrayFieldProperties()); + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new ArrayFieldProperties()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs index 2004ef6d44..cd7dd6b2ad 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs @@ -10,125 +10,124 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class AssetsFieldPropertiesDto : FieldPropertiesDto { - public sealed class AssetsFieldPropertiesDto : FieldPropertiesDto + /// <summary> + /// The preview mode for the asset. + /// </summary> + public AssetPreviewMode PreviewMode { get; set; } + + /// <summary> + /// The language specific default value as a list of asset ids. + /// </summary> + public LocalizedValue<ReadonlyList<string>?>? DefaultValues { get; set; } + + /// <summary> + /// The default value as a list of asset ids. + /// </summary> + public ReadonlyList<string>? DefaultValue { get; set; } + + /// <summary> + /// The initial id to the folder. + /// </summary> + public string? FolderId { get; set; } + + /// <summary> + /// The minimum allowed items for the field value. + /// </summary> + public int? MinItems { get; set; } + + /// <summary> + /// The maximum allowed items for the field value. + /// </summary> + public int? MaxItems { get; set; } + + /// <summary> + /// The minimum file size in bytes. + /// </summary> + public int? MinSize { get; set; } + + /// <summary> + /// The maximum file size in bytes. + /// </summary> + public int? MaxSize { get; set; } + + /// <summary> + /// The minimum image width in pixels. + /// </summary> + public int? MinWidth { get; set; } + + /// <summary> + /// The maximum image width in pixels. + /// </summary> + public int? MaxWidth { get; set; } + + /// <summary> + /// The minimum image height in pixels. + /// </summary> + public int? MinHeight { get; set; } + + /// <summary> + /// The maximum image height in pixels. + /// </summary> + public int? MaxHeight { get; set; } + + /// <summary> + /// The image aspect width in pixels. + /// </summary> + public int? AspectWidth { get; set; } + + /// <summary> + /// The image aspect height in pixels. + /// </summary> + public int? AspectHeight { get; set; } + + /// <summary> + /// The expected type. + /// </summary> + public AssetType? ExpectedType { get; set; } + + /// <summary> + /// True to resolve first asset in the content list. + /// </summary> + public bool ResolveFirst { get; set; } + + /// <summary> + /// True to resolve first image in the content list. + /// </summary> + [Obsolete("Use 'expectedType' field now")] + public bool MustBeImage + { + get => ExpectedType == AssetType.Image; + set => ExpectedType = value ? AssetType.Image : ExpectedType; + } + + /// <summary> + /// True to resolve first image in the content list. + /// </summary> + [Obsolete("Use 'resolveFirst' field now")] + public bool ResolveImage + { + get => ResolveFirst; + set => ResolveFirst = value; + } + + /// <summary> + /// The allowed file extensions. + /// </summary> + public ReadonlyList<string>? AllowedExtensions { get; set; } + + /// <summary> + /// True, if duplicate values are allowed. + /// </summary> + public bool AllowDuplicates { get; set; } + + public override FieldProperties ToProperties() { - /// <summary> - /// The preview mode for the asset. - /// </summary> - public AssetPreviewMode PreviewMode { get; set; } - - /// <summary> - /// The language specific default value as a list of asset ids. - /// </summary> - public LocalizedValue<ReadonlyList<string>?>? DefaultValues { get; set; } - - /// <summary> - /// The default value as a list of asset ids. - /// </summary> - public ReadonlyList<string>? DefaultValue { get; set; } - - /// <summary> - /// The initial id to the folder. - /// </summary> - public string? FolderId { get; set; } - - /// <summary> - /// The minimum allowed items for the field value. - /// </summary> - public int? MinItems { get; set; } - - /// <summary> - /// The maximum allowed items for the field value. - /// </summary> - public int? MaxItems { get; set; } - - /// <summary> - /// The minimum file size in bytes. - /// </summary> - public int? MinSize { get; set; } - - /// <summary> - /// The maximum file size in bytes. - /// </summary> - public int? MaxSize { get; set; } - - /// <summary> - /// The minimum image width in pixels. - /// </summary> - public int? MinWidth { get; set; } - - /// <summary> - /// The maximum image width in pixels. - /// </summary> - public int? MaxWidth { get; set; } - - /// <summary> - /// The minimum image height in pixels. - /// </summary> - public int? MinHeight { get; set; } - - /// <summary> - /// The maximum image height in pixels. - /// </summary> - public int? MaxHeight { get; set; } - - /// <summary> - /// The image aspect width in pixels. - /// </summary> - public int? AspectWidth { get; set; } - - /// <summary> - /// The image aspect height in pixels. - /// </summary> - public int? AspectHeight { get; set; } - - /// <summary> - /// The expected type. - /// </summary> - public AssetType? ExpectedType { get; set; } - - /// <summary> - /// True to resolve first asset in the content list. - /// </summary> - public bool ResolveFirst { get; set; } - - /// <summary> - /// True to resolve first image in the content list. - /// </summary> - [Obsolete("Use 'expectedType' field now")] - public bool MustBeImage - { - get => ExpectedType == AssetType.Image; - set => ExpectedType = value ? AssetType.Image : ExpectedType; - } - - /// <summary> - /// True to resolve first image in the content list. - /// </summary> - [Obsolete("Use 'resolveFirst' field now")] - public bool ResolveImage - { - get => ResolveFirst; - set => ResolveFirst = value; - } - - /// <summary> - /// The allowed file extensions. - /// </summary> - public ReadonlyList<string>? AllowedExtensions { get; set; } - - /// <summary> - /// True, if duplicate values are allowed. - /// </summary> - public bool AllowDuplicates { get; set; } - - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new AssetsFieldProperties()); - - return result; - } + var result = SimpleMapper.Map(this, new AssetsFieldProperties()); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs index 3f8985511f..c50f0377de 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs @@ -8,35 +8,34 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class BooleanFieldPropertiesDto : FieldPropertiesDto { - public sealed class BooleanFieldPropertiesDto : FieldPropertiesDto + /// <summary> + /// The language specific default value for the field value. + /// </summary> + public LocalizedValue<bool?>? DefaultValues { get; set; } + + /// <summary> + /// The default value for the field value. + /// </summary> + public bool? DefaultValue { get; set; } + + /// <summary> + /// Indicates that the inline editor is enabled for this field. + /// </summary> + public bool InlineEditable { get; set; } + + /// <summary> + /// The editor that is used to manage this field. + /// </summary> + public BooleanFieldEditor Editor { get; set; } + + public override FieldProperties ToProperties() { - /// <summary> - /// The language specific default value for the field value. - /// </summary> - public LocalizedValue<bool?>? DefaultValues { get; set; } - - /// <summary> - /// The default value for the field value. - /// </summary> - public bool? DefaultValue { get; set; } - - /// <summary> - /// Indicates that the inline editor is enabled for this field. - /// </summary> - public bool InlineEditable { get; set; } - - /// <summary> - /// The editor that is used to manage this field. - /// </summary> - public BooleanFieldEditor Editor { get; set; } - - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new BooleanFieldProperties()); - - return result; - } + var result = SimpleMapper.Map(this, new BooleanFieldProperties()); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs index 78d3834722..38f5b6c82f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentFieldPropertiesDto.cs @@ -10,20 +10,19 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class ComponentFieldPropertiesDto : FieldPropertiesDto { - public sealed class ComponentFieldPropertiesDto : FieldPropertiesDto - { - /// <summary> - /// The ID of the embedded schemas. - /// </summary> - public ReadonlyList<DomainId>? SchemaIds { get; set; } + /// <summary> + /// The ID of the embedded schemas. + /// </summary> + public ReadonlyList<DomainId>? SchemaIds { get; set; } - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new ComponentFieldProperties()); + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new ComponentFieldProperties()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs index 88ea347715..dfb553dd7b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs @@ -10,35 +10,34 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class ComponentsFieldPropertiesDto : FieldPropertiesDto { - public sealed class ComponentsFieldPropertiesDto : FieldPropertiesDto + /// <summary> + /// The minimum allowed items for the field value. + /// </summary> + public int? MinItems { get; set; } + + /// <summary> + /// The maximum allowed items for the field value. + /// </summary> + public int? MaxItems { get; set; } + + /// <summary> + /// The ID of the embedded schemas. + /// </summary> + public ReadonlyList<DomainId>? SchemaIds { get; set; } + + /// <summary> + /// The fields that must be unique. + /// </summary> + public ReadonlyList<string>? UniqueFields { get; set; } + + public override FieldProperties ToProperties() { - /// <summary> - /// The minimum allowed items for the field value. - /// </summary> - public int? MinItems { get; set; } - - /// <summary> - /// The maximum allowed items for the field value. - /// </summary> - public int? MaxItems { get; set; } - - /// <summary> - /// The ID of the embedded schemas. - /// </summary> - public ReadonlyList<DomainId>? SchemaIds { get; set; } - - /// <summary> - /// The fields that must be unique. - /// </summary> - public ReadonlyList<string>? UniqueFields { get; set; } - - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new ComponentsFieldProperties()); - - return result; - } + var result = SimpleMapper.Map(this, new ComponentsFieldProperties()); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs index 882e8a6b9c..99b1ad21ae 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs @@ -9,50 +9,49 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class DateTimeFieldPropertiesDto : FieldPropertiesDto { - public sealed class DateTimeFieldPropertiesDto : FieldPropertiesDto + /// <summary> + /// The language specific default value for the field value. + /// </summary> + public LocalizedValue<Instant?>? DefaultValues { get; set; } + + /// <summary> + /// The default value for the field value. + /// </summary> + public Instant? DefaultValue { get; set; } + + /// <summary> + /// The maximum allowed value for the field value. + /// </summary> + public Instant? MaxValue { get; set; } + + /// <summary> + /// The minimum allowed value for the field value. + /// </summary> + public Instant? MinValue { get; set; } + + /// <summary> + /// The format pattern when displayed in the UI. + /// </summary> + public string? Format { get; set; } + + /// <summary> + /// The editor that is used to manage this field. + /// </summary> + public DateTimeFieldEditor Editor { get; set; } + + /// <summary> + /// The calculated default value for the field value. + /// </summary> + public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } + + public override FieldProperties ToProperties() { - /// <summary> - /// The language specific default value for the field value. - /// </summary> - public LocalizedValue<Instant?>? DefaultValues { get; set; } - - /// <summary> - /// The default value for the field value. - /// </summary> - public Instant? DefaultValue { get; set; } - - /// <summary> - /// The maximum allowed value for the field value. - /// </summary> - public Instant? MaxValue { get; set; } - - /// <summary> - /// The minimum allowed value for the field value. - /// </summary> - public Instant? MinValue { get; set; } - - /// <summary> - /// The format pattern when displayed in the UI. - /// </summary> - public string? Format { get; set; } - - /// <summary> - /// The editor that is used to manage this field. - /// </summary> - public DateTimeFieldEditor Editor { get; set; } - - /// <summary> - /// The calculated default value for the field value. - /// </summary> - public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } - - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new DateTimeFieldProperties()); - - return result; - } + var result = SimpleMapper.Map(this, new DateTimeFieldProperties()); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs index a49bfe7847..f013cbaa85 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs @@ -8,20 +8,19 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto { - public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto - { - /// <summary> - /// The editor that is used to manage this field. - /// </summary> - public GeolocationFieldEditor Editor { get; set; } + /// <summary> + /// The editor that is used to manage this field. + /// </summary> + public GeolocationFieldEditor Editor { get; set; } - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new GeolocationFieldProperties()); + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new GeolocationFieldProperties()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs index 35019de9a5..2433d5318f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs @@ -8,20 +8,19 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class JsonFieldPropertiesDto : FieldPropertiesDto { - public sealed class JsonFieldPropertiesDto : FieldPropertiesDto - { - /// <summary> - /// The GraphQL schema. - /// </summary> - public string? GraphQLSchema { get; set; } + /// <summary> + /// The GraphQL schema. + /// </summary> + public string? GraphQLSchema { get; set; } - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new JsonFieldProperties()); + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new JsonFieldProperties()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs index 15689dcfdc..35affe91a8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs @@ -9,55 +9,54 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class NumberFieldPropertiesDto : FieldPropertiesDto { - public sealed class NumberFieldPropertiesDto : FieldPropertiesDto + /// <summary> + /// The language specific default value for the field value. + /// </summary> + public LocalizedValue<double?>? DefaultValues { get; set; } + + /// <summary> + /// The default value for the field value. + /// </summary> + public double? DefaultValue { get; set; } + + /// <summary> + /// The maximum allowed value for the field value. + /// </summary> + public double? MaxValue { get; set; } + + /// <summary> + /// The minimum allowed value for the field value. + /// </summary> + public double? MinValue { get; set; } + + /// <summary> + /// The allowed values for the field value. + /// </summary> + public ReadonlyList<double>? AllowedValues { get; set; } + + /// <summary> + /// Indicates if the field value must be unique. Ignored for nested fields and localized fields. + /// </summary> + public bool IsUnique { get; set; } + + /// <summary> + /// Indicates that the inline editor is enabled for this field. + /// </summary> + public bool InlineEditable { get; set; } + + /// <summary> + /// The editor that is used to manage this field. + /// </summary> + public NumberFieldEditor Editor { get; set; } + + public override FieldProperties ToProperties() { - /// <summary> - /// The language specific default value for the field value. - /// </summary> - public LocalizedValue<double?>? DefaultValues { get; set; } - - /// <summary> - /// The default value for the field value. - /// </summary> - public double? DefaultValue { get; set; } - - /// <summary> - /// The maximum allowed value for the field value. - /// </summary> - public double? MaxValue { get; set; } - - /// <summary> - /// The minimum allowed value for the field value. - /// </summary> - public double? MinValue { get; set; } - - /// <summary> - /// The allowed values for the field value. - /// </summary> - public ReadonlyList<double>? AllowedValues { get; set; } - - /// <summary> - /// Indicates if the field value must be unique. Ignored for nested fields and localized fields. - /// </summary> - public bool IsUnique { get; set; } - - /// <summary> - /// Indicates that the inline editor is enabled for this field. - /// </summary> - public bool InlineEditable { get; set; } - - /// <summary> - /// The editor that is used to manage this field. - /// </summary> - public NumberFieldEditor Editor { get; set; } - - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new NumberFieldProperties()); - - return result; - } + var result = SimpleMapper.Map(this, new NumberFieldProperties()); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index 43c8bffff1..20148509e3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -10,60 +10,59 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class ReferencesFieldPropertiesDto : FieldPropertiesDto { - public sealed class ReferencesFieldPropertiesDto : FieldPropertiesDto - { - /// <summary> - /// The language specific default value as a list of content ids. - /// </summary> - public LocalizedValue<ReadonlyList<string>?>? DefaultValues { get; set; } + /// <summary> + /// The language specific default value as a list of content ids. + /// </summary> + public LocalizedValue<ReadonlyList<string>?>? DefaultValues { get; set; } - /// <summary> - /// The default value as a list of content ids. - /// </summary> - public ReadonlyList<string>? DefaultValue { get; set; } + /// <summary> + /// The default value as a list of content ids. + /// </summary> + public ReadonlyList<string>? DefaultValue { get; set; } - /// <summary> - /// The minimum allowed items for the field value. - /// </summary> - public int? MinItems { get; set; } + /// <summary> + /// The minimum allowed items for the field value. + /// </summary> + public int? MinItems { get; set; } - /// <summary> - /// The maximum allowed items for the field value. - /// </summary> - public int? MaxItems { get; set; } + /// <summary> + /// The maximum allowed items for the field value. + /// </summary> + public int? MaxItems { get; set; } - /// <summary> - /// True, if duplicate values are allowed. - /// </summary> - public bool AllowDuplicates { get; set; } + /// <summary> + /// True, if duplicate values are allowed. + /// </summary> + public bool AllowDuplicates { get; set; } - /// <summary> - /// True to resolve references in the content list. - /// </summary> - public bool ResolveReference { get; set; } + /// <summary> + /// True to resolve references in the content list. + /// </summary> + public bool ResolveReference { get; set; } - /// <summary> - /// True when all references must be published. - /// </summary> - public bool MustBePublished { get; set; } + /// <summary> + /// True when all references must be published. + /// </summary> + public bool MustBePublished { get; set; } - /// <summary> - /// The editor that is used to manage this field. - /// </summary> - public ReferencesFieldEditor Editor { get; set; } + /// <summary> + /// The editor that is used to manage this field. + /// </summary> + public ReferencesFieldEditor Editor { get; set; } - /// <summary> - /// The ID of the referenced schemas. - /// </summary> - public ReadonlyList<DomainId>? SchemaIds { get; set; } + /// <summary> + /// The ID of the referenced schemas. + /// </summary> + public ReadonlyList<DomainId>? SchemaIds { get; set; } - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new ReferencesFieldProperties()); + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new ReferencesFieldProperties()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs index f48cfa54a7..08d82972f3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -10,110 +10,109 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class StringFieldPropertiesDto : FieldPropertiesDto { - public sealed class StringFieldPropertiesDto : FieldPropertiesDto + /// <summary> + /// The language specific default value for the field value. + /// </summary> + public LocalizedValue<string?>? DefaultValues { get; set; } + + /// <summary> + /// The default value for the field value. + /// </summary> + public string? DefaultValue { get; set; } + + /// <summary> + /// The pattern to enforce a specific format for the field value. + /// </summary> + public string? Pattern { get; set; } + + /// <summary> + /// The validation message for the pattern. + /// </summary> + public string? PatternMessage { get; set; } + + /// <summary> + /// The initial id to the folder when the control supports file uploads. + /// </summary> + public string? FolderId { get; set; } + + /// <summary> + /// The minimum allowed length for the field value. + /// </summary> + public int? MinLength { get; set; } + + /// <summary> + /// The maximum allowed length for the field value. + /// </summary> + public int? MaxLength { get; set; } + + /// <summary> + /// The minimum allowed of normal characters for the field value. + /// </summary> + public int? MinCharacters { get; set; } + + /// <summary> + /// The maximum allowed of normal characters for the field value. + /// </summary> + public int? MaxCharacters { get; set; } + + /// <summary> + /// The minimum allowed number of words for the field value. + /// </summary> + public int? MinWords { get; set; } + + /// <summary> + /// The maximum allowed number of words for the field value. + /// </summary> + public int? MaxWords { get; set; } + + /// <summary> + /// The allowed values for the field value. + /// </summary> + public ReadonlyList<string>? AllowedValues { get; set; } + + /// <summary> + /// The allowed schema ids that can be embedded. + /// </summary> + public ReadonlyList<DomainId>? SchemaIds { get; init; } + + /// <summary> + /// Indicates if the field value must be unique. Ignored for nested fields and localized fields. + /// </summary> + public bool IsUnique { get; set; } + + /// <summary> + /// Indicates that other content items or references are embedded. + /// </summary> + public bool IsEmbeddable { get; set; } + + /// <summary> + /// Indicates that the inline editor is enabled for this field. + /// </summary> + public bool InlineEditable { get; set; } + + /// <summary> + /// Indicates whether GraphQL Enum should be created. + /// </summary> + public bool CreateEnum { get; init; } + + /// <summary> + /// How the string content should be interpreted. + /// </summary> + public StringContentType ContentType { get; set; } + + /// <summary> + /// The editor that is used to manage this field. + /// </summary> + public StringFieldEditor Editor { get; set; } + + public override FieldProperties ToProperties() { - /// <summary> - /// The language specific default value for the field value. - /// </summary> - public LocalizedValue<string?>? DefaultValues { get; set; } - - /// <summary> - /// The default value for the field value. - /// </summary> - public string? DefaultValue { get; set; } - - /// <summary> - /// The pattern to enforce a specific format for the field value. - /// </summary> - public string? Pattern { get; set; } - - /// <summary> - /// The validation message for the pattern. - /// </summary> - public string? PatternMessage { get; set; } - - /// <summary> - /// The initial id to the folder when the control supports file uploads. - /// </summary> - public string? FolderId { get; set; } - - /// <summary> - /// The minimum allowed length for the field value. - /// </summary> - public int? MinLength { get; set; } - - /// <summary> - /// The maximum allowed length for the field value. - /// </summary> - public int? MaxLength { get; set; } - - /// <summary> - /// The minimum allowed of normal characters for the field value. - /// </summary> - public int? MinCharacters { get; set; } - - /// <summary> - /// The maximum allowed of normal characters for the field value. - /// </summary> - public int? MaxCharacters { get; set; } - - /// <summary> - /// The minimum allowed number of words for the field value. - /// </summary> - public int? MinWords { get; set; } - - /// <summary> - /// The maximum allowed number of words for the field value. - /// </summary> - public int? MaxWords { get; set; } - - /// <summary> - /// The allowed values for the field value. - /// </summary> - public ReadonlyList<string>? AllowedValues { get; set; } - - /// <summary> - /// The allowed schema ids that can be embedded. - /// </summary> - public ReadonlyList<DomainId>? SchemaIds { get; init; } - - /// <summary> - /// Indicates if the field value must be unique. Ignored for nested fields and localized fields. - /// </summary> - public bool IsUnique { get; set; } - - /// <summary> - /// Indicates that other content items or references are embedded. - /// </summary> - public bool IsEmbeddable { get; set; } - - /// <summary> - /// Indicates that the inline editor is enabled for this field. - /// </summary> - public bool InlineEditable { get; set; } - - /// <summary> - /// Indicates whether GraphQL Enum should be created. - /// </summary> - public bool CreateEnum { get; init; } - - /// <summary> - /// How the string content should be interpreted. - /// </summary> - public StringContentType ContentType { get; set; } - - /// <summary> - /// The editor that is used to manage this field. - /// </summary> - public StringFieldEditor Editor { get; set; } - - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new StringFieldProperties()); - - return result; - } + var result = SimpleMapper.Map(this, new StringFieldProperties()); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs index 6001706d6f..21161c6dee 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs @@ -9,50 +9,49 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class TagsFieldPropertiesDto : FieldPropertiesDto { - public sealed class TagsFieldPropertiesDto : FieldPropertiesDto + /// <summary> + /// The language specific default value for the field value. + /// </summary> + public LocalizedValue<ReadonlyList<string>?>? DefaultValues { get; set; } + + /// <summary> + /// The default value. + /// </summary> + public ReadonlyList<string>? DefaultValue { get; set; } + + /// <summary> + /// The minimum allowed items for the field value. + /// </summary> + public int? MinItems { get; set; } + + /// <summary> + /// The maximum allowed items for the field value. + /// </summary> + public int? MaxItems { get; set; } + + /// <summary> + /// The allowed values for the field value. + /// </summary> + public ReadonlyList<string>? AllowedValues { get; set; } + + /// <summary> + /// Indicates whether GraphQL Enum should be created. + /// </summary> + public bool CreateEnum { get; init; } + + /// <summary> + /// The editor that is used to manage this field. + /// </summary> + public TagsFieldEditor Editor { get; set; } + + public override FieldProperties ToProperties() { - /// <summary> - /// The language specific default value for the field value. - /// </summary> - public LocalizedValue<ReadonlyList<string>?>? DefaultValues { get; set; } - - /// <summary> - /// The default value. - /// </summary> - public ReadonlyList<string>? DefaultValue { get; set; } - - /// <summary> - /// The minimum allowed items for the field value. - /// </summary> - public int? MinItems { get; set; } - - /// <summary> - /// The maximum allowed items for the field value. - /// </summary> - public int? MaxItems { get; set; } - - /// <summary> - /// The allowed values for the field value. - /// </summary> - public ReadonlyList<string>? AllowedValues { get; set; } - - /// <summary> - /// Indicates whether GraphQL Enum should be created. - /// </summary> - public bool CreateEnum { get; init; } - - /// <summary> - /// The editor that is used to manage this field. - /// </summary> - public TagsFieldEditor Editor { get; set; } - - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new TagsFieldProperties()); - - return result; - } + var result = SimpleMapper.Map(this, new TagsFieldProperties()); + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs index adc60d96a2..8fc904589d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs @@ -8,20 +8,19 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; + +public sealed class UIFieldPropertiesDto : FieldPropertiesDto { - public sealed class UIFieldPropertiesDto : FieldPropertiesDto - { - /// <summary> - /// The editor that is used to manage this field. - /// </summary> - public UIFieldEditor Editor { get; set; } + /// <summary> + /// The editor that is used to manage this field. + /// </summary> + public UIFieldEditor Editor { get; set; } - public override FieldProperties ToProperties() - { - var result = SimpleMapper.Map(this, new UIFieldProperties()); + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new UIFieldProperties()); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs index 7b62d2a5b9..0b5571b00d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/NestedFieldDto.cs @@ -8,85 +8,84 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class NestedFieldDto : Resource { - public sealed class NestedFieldDto : Resource - { - /// <summary> - /// The ID of the field. - /// </summary> - public long FieldId { get; set; } + /// <summary> + /// The ID of the field. + /// </summary> + public long FieldId { get; set; } - /// <summary> - /// The name of the field. Must be unique within the schema. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Name { get; set; } + /// <summary> + /// The name of the field. Must be unique within the schema. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Name { get; set; } - /// <summary> - /// Defines if the field is hidden. - /// </summary> - public bool IsHidden { get; set; } + /// <summary> + /// Defines if the field is hidden. + /// </summary> + public bool IsHidden { get; set; } - /// <summary> - /// Defines if the field is locked. - /// </summary> - public bool IsLocked { get; set; } + /// <summary> + /// Defines if the field is locked. + /// </summary> + public bool IsLocked { get; set; } - /// <summary> - /// Defines if the field is disabled. - /// </summary> - public bool IsDisabled { get; set; } + /// <summary> + /// Defines if the field is disabled. + /// </summary> + public bool IsDisabled { get; set; } - /// <summary> - /// The field properties. - /// </summary> - [LocalizedRequired] - public FieldPropertiesDto Properties { get; set; } + /// <summary> + /// The field properties. + /// </summary> + [LocalizedRequired] + public FieldPropertiesDto Properties { get; set; } - public void CreateLinks(Resources resources, string schema, long parentId, bool allowUpdate) - { - allowUpdate = allowUpdate && !IsLocked; - - if (allowUpdate) - { - var values = new { app = resources.App, schema, parentId, id = FieldId }; + public void CreateLinks(Resources resources, string schema, long parentId, bool allowUpdate) + { + allowUpdate = allowUpdate && !IsLocked; - AddPutLink("update", - resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedField), values)); + if (allowUpdate) + { + var values = new { app = resources.App, schema, parentId, id = FieldId }; - if (IsHidden) - { - AddPutLink("show", - resources.Url<SchemaFieldsController>(x => nameof(x.ShowNestedField), values)); - } - else - { - AddPutLink("hide", - resources.Url<SchemaFieldsController>(x => nameof(x.HideNestedField), values)); - } + AddPutLink("update", + resources.Url<SchemaFieldsController>(x => nameof(x.PutNestedField), values)); - if (IsDisabled) - { - AddPutLink("enable", - resources.Url<SchemaFieldsController>(x => nameof(x.EnableNestedField), values)); - } - else - { - AddPutLink("disable", - resources.Url<SchemaFieldsController>(x => nameof(x.DisableNestedField), values)); - } + if (IsHidden) + { + AddPutLink("show", + resources.Url<SchemaFieldsController>(x => nameof(x.ShowNestedField), values)); + } + else + { + AddPutLink("hide", + resources.Url<SchemaFieldsController>(x => nameof(x.HideNestedField), values)); + } - if (!IsLocked) - { - AddPutLink("lock", - resources.Url<SchemaFieldsController>(x => nameof(x.LockNestedField), values)); - } + if (IsDisabled) + { + AddPutLink("enable", + resources.Url<SchemaFieldsController>(x => nameof(x.EnableNestedField), values)); + } + else + { + AddPutLink("disable", + resources.Url<SchemaFieldsController>(x => nameof(x.DisableNestedField), values)); + } - AddDeleteLink("delete", - resources.Url<SchemaFieldsController>(x => nameof(x.DeleteNestedField), values)); + if (!IsLocked) + { + AddPutLink("lock", + resources.Url<SchemaFieldsController>(x => nameof(x.LockNestedField), values)); } + + AddDeleteLink("delete", + resources.Url<SchemaFieldsController>(x => nameof(x.DeleteNestedField), values)); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs index f8ec3079cd..5da1dbb566 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs @@ -8,19 +8,18 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class ReorderFieldsDto { - public sealed class ReorderFieldsDto - { - /// <summary> - /// The field ids in the target order. - /// </summary> - [LocalizedRequired] - public long[] FieldIds { get; set; } + /// <summary> + /// The field ids in the target order. + /// </summary> + [LocalizedRequired] + public long[] FieldIds { get; set; } - public ReorderFields ToCommand(long? parentId = null) - { - return new ReorderFields { ParentFieldId = parentId, FieldIds = FieldIds }; - } + public ReorderFields ToCommand(long? parentId = null) + { + return new ReorderFields { ParentFieldId = parentId, FieldIds = FieldIds }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs index 59a44d0cec..4787e3daf2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs @@ -15,220 +15,219 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public class SchemaDto : Resource { - public class SchemaDto : Resource + /// <summary> + /// The ID of the schema. + /// </summary> + public DomainId Id { get; set; } + + /// <summary> + /// The user that has created the schema. + /// </summary> + [LocalizedRequired] + public RefToken CreatedBy { get; set; } + + /// <summary> + /// The user that has updated the schema. + /// </summary> + [LocalizedRequired] + public RefToken LastModifiedBy { get; set; } + + /// <summary> + /// The name of the schema. Unique within the app. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + public string Name { get; set; } + + /// <summary> + /// The type of the schema. + /// </summary> + public SchemaType Type { get; set; } + + /// <summary> + /// The name of the category. + /// </summary> + public string? Category { get; set; } + + /// <summary> + /// The schema properties. + /// </summary> + [LocalizedRequired] + public SchemaPropertiesDto Properties { get; set; } = new SchemaPropertiesDto(); + + /// <summary> + /// Indicates if the schema is a singleton. + /// </summary> + [Obsolete("Use 'type' field now.")] + public bool IsSingleton { - /// <summary> - /// The ID of the schema. - /// </summary> - public DomainId Id { get; set; } - - /// <summary> - /// The user that has created the schema. - /// </summary> - [LocalizedRequired] - public RefToken CreatedBy { get; set; } - - /// <summary> - /// The user that has updated the schema. - /// </summary> - [LocalizedRequired] - public RefToken LastModifiedBy { get; set; } - - /// <summary> - /// The name of the schema. Unique within the app. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] - public string Name { get; set; } - - /// <summary> - /// The type of the schema. - /// </summary> - public SchemaType Type { get; set; } - - /// <summary> - /// The name of the category. - /// </summary> - public string? Category { get; set; } - - /// <summary> - /// The schema properties. - /// </summary> - [LocalizedRequired] - public SchemaPropertiesDto Properties { get; set; } = new SchemaPropertiesDto(); - - /// <summary> - /// Indicates if the schema is a singleton. - /// </summary> - [Obsolete("Use 'type' field now.")] - public bool IsSingleton + get => Type == SchemaType.Singleton; + } + + /// <summary> + /// Indicates if the schema is published. + /// </summary> + public bool IsPublished { get; set; } + + /// <summary> + /// The date and time when the schema has been created. + /// </summary> + public Instant Created { get; set; } + + /// <summary> + /// The date and time when the schema has been modified last. + /// </summary> + public Instant LastModified { get; set; } + + /// <summary> + /// The version of the schema. + /// </summary> + public long Version { get; set; } + + /// <summary> + /// The scripts. + /// </summary> + [LocalizedRequired] + public SchemaScriptsDto Scripts { get; set; } = new SchemaScriptsDto(); + + /// <summary> + /// The preview Urls. + /// </summary> + [LocalizedRequired] + public ReadonlyDictionary<string, string> PreviewUrls { get; set; } + + /// <summary> + /// The name of fields that are used in content lists. + /// </summary> + [LocalizedRequired] + public FieldNames FieldsInLists { get; set; } + + /// <summary> + /// The name of fields that are used in content references. + /// </summary> + [LocalizedRequired] + public FieldNames FieldsInReferences { get; set; } + + /// <summary> + /// The field rules. + /// </summary> + public List<FieldRuleDto> FieldRules { get; set; } = new List<FieldRuleDto>(); + + /// <summary> + /// The list of fields. + /// </summary> + [LocalizedRequired] + public List<FieldDto> Fields { get; set; } = new List<FieldDto>(); + + public static SchemaDto FromDomain(ISchemaEntity schema, Resources resources) + { + var result = new SchemaDto(); + + SimpleMapper.Map(schema, result); + SimpleMapper.Map(schema.SchemaDef, result); + SimpleMapper.Map(schema.SchemaDef.Scripts, result.Scripts); + SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties); + + foreach (var rule in schema.SchemaDef.FieldRules) { - get => Type == SchemaType.Singleton; + result.FieldRules.Add(FieldRuleDto.FromDomain(rule)); } - /// <summary> - /// Indicates if the schema is published. - /// </summary> - public bool IsPublished { get; set; } - - /// <summary> - /// The date and time when the schema has been created. - /// </summary> - public Instant Created { get; set; } - - /// <summary> - /// The date and time when the schema has been modified last. - /// </summary> - public Instant LastModified { get; set; } - - /// <summary> - /// The version of the schema. - /// </summary> - public long Version { get; set; } - - /// <summary> - /// The scripts. - /// </summary> - [LocalizedRequired] - public SchemaScriptsDto Scripts { get; set; } = new SchemaScriptsDto(); - - /// <summary> - /// The preview Urls. - /// </summary> - [LocalizedRequired] - public ReadonlyDictionary<string, string> PreviewUrls { get; set; } - - /// <summary> - /// The name of fields that are used in content lists. - /// </summary> - [LocalizedRequired] - public FieldNames FieldsInLists { get; set; } - - /// <summary> - /// The name of fields that are used in content references. - /// </summary> - [LocalizedRequired] - public FieldNames FieldsInReferences { get; set; } - - /// <summary> - /// The field rules. - /// </summary> - public List<FieldRuleDto> FieldRules { get; set; } = new List<FieldRuleDto>(); - - /// <summary> - /// The list of fields. - /// </summary> - [LocalizedRequired] - public List<FieldDto> Fields { get; set; } = new List<FieldDto>(); - - public static SchemaDto FromDomain(ISchemaEntity schema, Resources resources) + foreach (var field in schema.SchemaDef.Fields) { - var result = new SchemaDto(); + result.Fields.Add(FieldDto.FromDomain(field)); + } - SimpleMapper.Map(schema, result); - SimpleMapper.Map(schema.SchemaDef, result); - SimpleMapper.Map(schema.SchemaDef.Scripts, result.Scripts); - SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties); + result.CreateLinks(resources); - foreach (var rule in schema.SchemaDef.FieldRules) - { - result.FieldRules.Add(FieldRuleDto.FromDomain(rule)); - } + return result; + } - foreach (var field in schema.SchemaDef.Fields) - { - result.Fields.Add(FieldDto.FromDomain(field)); - } + protected virtual void CreateLinks(Resources resources) + { + var values = new { app = resources.App, schema = Name }; - result.CreateLinks(resources); + var allowUpdate = resources.CanUpdateSchema(Name); - return result; - } + AddSelfLink(resources.Url<SchemasController>(x => nameof(x.GetSchema), values)); - protected virtual void CreateLinks(Resources resources) + if (resources.CanReadContent(Name) && Type != SchemaType.Component) { - var values = new { app = resources.App, schema = Name }; - - var allowUpdate = resources.CanUpdateSchema(Name); + AddGetLink("contents", + resources.Url<ContentsController>(x => nameof(x.GetContents), values)); + } - AddSelfLink(resources.Url<SchemasController>(x => nameof(x.GetSchema), values)); + if (resources.CanCreateContent(Name) && Type == SchemaType.Default) + { + AddPostLink("contents/create", + resources.Url<ContentsController>(x => nameof(x.PostContent), values)); - if (resources.CanReadContent(Name) && Type != SchemaType.Component) - { - AddGetLink("contents", - resources.Url<ContentsController>(x => nameof(x.GetContents), values)); - } + AddPostLink("contents/create/publish", + resources.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true"); + } - if (resources.CanCreateContent(Name) && Type == SchemaType.Default) + if (resources.CanPublishSchema(Name)) + { + if (IsPublished) { - AddPostLink("contents/create", - resources.Url<ContentsController>(x => nameof(x.PostContent), values)); - - AddPostLink("contents/create/publish", - resources.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true"); + AddPutLink("unpublish", + resources.Url<SchemasController>(x => nameof(x.UnpublishSchema), values)); } - - if (resources.CanPublishSchema(Name)) + else { - if (IsPublished) - { - AddPutLink("unpublish", - resources.Url<SchemasController>(x => nameof(x.UnpublishSchema), values)); - } - else - { - AddPutLink("publish", - resources.Url<SchemasController>(x => nameof(x.PublishSchema), values)); - } + AddPutLink("publish", + resources.Url<SchemasController>(x => nameof(x.PublishSchema), values)); } + } - if (allowUpdate) - { - AddPostLink("fields/add", - resources.Url<SchemaFieldsController>(x => nameof(x.PostField), values)); + if (allowUpdate) + { + AddPostLink("fields/add", + resources.Url<SchemaFieldsController>(x => nameof(x.PostField), values)); - AddPutLink("fields/order", - resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaFieldOrdering), values)); + AddPutLink("fields/order", + resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaFieldOrdering), values)); - AddPutLink("fields/ui", - resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaUIFields), values)); + AddPutLink("fields/ui", + resources.Url<SchemaFieldsController>(x => nameof(x.PutSchemaUIFields), values)); - AddPutLink("update", - resources.Url<SchemasController>(x => nameof(x.PutSchema), values)); + AddPutLink("update", + resources.Url<SchemasController>(x => nameof(x.PutSchema), values)); - AddPutLink("update/category", - resources.Url<SchemasController>(x => nameof(x.PutCategory), values)); + AddPutLink("update/category", + resources.Url<SchemasController>(x => nameof(x.PutCategory), values)); - AddPutLink("update/rules", - resources.Url<SchemasController>(x => nameof(x.PutRules), values)); + AddPutLink("update/rules", + resources.Url<SchemasController>(x => nameof(x.PutRules), values)); - AddPutLink("update/sync", - resources.Url<SchemasController>(x => nameof(x.PutSchemaSync), values)); + AddPutLink("update/sync", + resources.Url<SchemasController>(x => nameof(x.PutSchemaSync), values)); - AddPutLink("update/urls", - resources.Url<SchemasController>(x => nameof(x.PutPreviewUrls), values)); - } + AddPutLink("update/urls", + resources.Url<SchemasController>(x => nameof(x.PutPreviewUrls), values)); + } - if (resources.CanUpdateSchemaScripts(Name)) - { - AddPutLink("update/scripts", - resources.Url<SchemasController>(x => nameof(x.PutScripts), values)); - } + if (resources.CanUpdateSchemaScripts(Name)) + { + AddPutLink("update/scripts", + resources.Url<SchemasController>(x => nameof(x.PutScripts), values)); + } - if (resources.CanDeleteSchema(Name)) - { - AddDeleteLink("delete", - resources.Url<SchemasController>(x => nameof(x.DeleteSchema), values)); - } + if (resources.CanDeleteSchema(Name)) + { + AddDeleteLink("delete", + resources.Url<SchemasController>(x => nameof(x.DeleteSchema), values)); + } - if (Fields != null) + if (Fields != null) + { + foreach (var nested in Fields) { - foreach (var nested in Fields) - { - nested.CreateLinks(resources, Name, allowUpdate); - } + nested.CreateLinks(resources, Name, allowUpdate); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs index cc03ea6eee..4d948d0576 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs @@ -8,45 +8,44 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class SchemaPropertiesDto { - public sealed class SchemaPropertiesDto - { - /// <summary> - /// Optional label for the editor. - /// </summary> - [LocalizedStringLength(100)] - public string? Label { get; set; } - - /// <summary> - /// Hints to describe the schema. - /// </summary> - [LocalizedStringLength(1000)] - public string? Hints { get; set; } - - /// <summary> - /// The url to a the sidebar plugin for content lists. - /// </summary> - public string? ContentsSidebarUrl { get; set; } - - /// <summary> - /// The url to a the sidebar plugin for content items. - /// </summary> - public string? ContentSidebarUrl { get; set; } - - /// <summary> - /// The url to the editor plugin. - /// </summary> - public string? ContentEditorUrl { get; set; } - - /// <summary> - /// True to validate the content items on publish. - /// </summary> - public bool ValidateOnPublish { get; set; } - - /// <summary> - /// Tags for automation processes. - /// </summary> - public ReadonlyList<string>? Tags { get; set; } - } + /// <summary> + /// Optional label for the editor. + /// </summary> + [LocalizedStringLength(100)] + public string? Label { get; set; } + + /// <summary> + /// Hints to describe the schema. + /// </summary> + [LocalizedStringLength(1000)] + public string? Hints { get; set; } + + /// <summary> + /// The url to a the sidebar plugin for content lists. + /// </summary> + public string? ContentsSidebarUrl { get; set; } + + /// <summary> + /// The url to a the sidebar plugin for content items. + /// </summary> + public string? ContentSidebarUrl { get; set; } + + /// <summary> + /// The url to the editor plugin. + /// </summary> + public string? ContentEditorUrl { get; set; } + + /// <summary> + /// True to validate the content items on publish. + /// </summary> + public bool ValidateOnPublish { get; set; } + + /// <summary> + /// Tags for automation processes. + /// </summary> + public ReadonlyList<string>? Tags { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs index f69122d112..c04d9d60be 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs @@ -9,45 +9,44 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class SchemaScriptsDto { - public sealed class SchemaScriptsDto + /// <summary> + /// The script that is executed for each content when querying contents. + /// </summary> + public string? Query { get; set; } + + /// <summary> + /// The script that is executed for all contents when querying contents. + /// </summary> + public string? QueryPre { get; set; } + + /// <summary> + /// The script that is executed when creating a content. + /// </summary> + public string? Create { get; set; } + + /// <summary> + /// The script that is executed when updating a content. + /// </summary> + public string? Update { get; set; } + + /// <summary> + /// The script that is executed when deleting a content. + /// </summary> + public string? Delete { get; set; } + + /// <summary> + /// The script that is executed when change a content status. + /// </summary> + public string? Change { get; set; } + + public ConfigureScripts ToCommand() { - /// <summary> - /// The script that is executed for each content when querying contents. - /// </summary> - public string? Query { get; set; } - - /// <summary> - /// The script that is executed for all contents when querying contents. - /// </summary> - public string? QueryPre { get; set; } - - /// <summary> - /// The script that is executed when creating a content. - /// </summary> - public string? Create { get; set; } - - /// <summary> - /// The script that is executed when updating a content. - /// </summary> - public string? Update { get; set; } - - /// <summary> - /// The script that is executed when deleting a content. - /// </summary> - public string? Delete { get; set; } - - /// <summary> - /// The script that is executed when change a content status. - /// </summary> - public string? Change { get; set; } - - public ConfigureScripts ToCommand() - { - var scripts = SimpleMapper.Map(this, new SchemaScripts()); - - return new ConfigureScripts { Scripts = scripts }; - } + var scripts = SimpleMapper.Map(this, new SchemaScripts()); + + return new ConfigureScripts { Scripts = scripts }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs index 0735cc723e..9b53589591 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs @@ -8,38 +8,37 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class SchemasDto : Resource { - public sealed class SchemasDto : Resource - { - /// <summary> - /// The schemas. - /// </summary> - public SchemaDto[] Items { get; set; } + /// <summary> + /// The schemas. + /// </summary> + public SchemaDto[] Items { get; set; } - public static SchemasDto FromDomain(IList<ISchemaEntity> schemas, Resources resources) + public static SchemasDto FromDomain(IList<ISchemaEntity> schemas, Resources resources) + { + var result = new SchemasDto { - var result = new SchemasDto - { - Items = schemas.Select(x => SchemaDto.FromDomain(x, resources)).ToArray() - }; - - return result.CreateLinks(resources); - } + Items = schemas.Select(x => SchemaDto.FromDomain(x, resources)).ToArray() + }; - private SchemasDto CreateLinks(Resources resources) - { - var values = new { app = resources.App }; + return result.CreateLinks(resources); + } - AddSelfLink(resources.Url<SchemasController>(x => nameof(x.GetSchemas), values)); + private SchemasDto CreateLinks(Resources resources) + { + var values = new { app = resources.App }; - if (resources.CanCreateSchema) - { - AddPostLink("create", - resources.Url<SchemasController>(x => nameof(x.PostSchema), values)); - } + AddSelfLink(resources.Url<SchemasController>(x => nameof(x.GetSchemas), values)); - return this; + if (resources.CanCreateSchema) + { + AddPostLink("create", + resources.Url<SchemasController>(x => nameof(x.PostSchema), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs index 42c5a33701..dccf64bf48 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs @@ -7,23 +7,22 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class SynchronizeSchemaDto : UpsertSchemaDto { - public sealed class SynchronizeSchemaDto : UpsertSchemaDto - { - /// <summary> - /// True, when fields should not be deleted. - /// </summary> - public bool NoFieldDeletion { get; set; } + /// <summary> + /// True, when fields should not be deleted. + /// </summary> + public bool NoFieldDeletion { get; set; } - /// <summary> - /// True, when fields with different types should not be recreated. - /// </summary> - public bool NoFieldRecreation { get; set; } + /// <summary> + /// True, when fields with different types should not be recreated. + /// </summary> + public bool NoFieldRecreation { get; set; } - public SynchronizeSchema ToCommand() - { - return ToCommand(this, new SynchronizeSchema()); - } + public SynchronizeSchema ToCommand() + { + return ToCommand(this, new SynchronizeSchema()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs index 5e97643815..5a4e8cb63d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs @@ -8,19 +8,18 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class UpdateFieldDto { - public sealed class UpdateFieldDto - { - /// <summary> - /// The field properties. - /// </summary> - [LocalizedRequired] - public FieldPropertiesDto Properties { get; set; } + /// <summary> + /// The field properties. + /// </summary> + [LocalizedRequired] + public FieldPropertiesDto Properties { get; set; } - public UpdateField ToCommand(long id, long? parentId = null) - { - return new UpdateField { ParentFieldId = parentId, FieldId = id, Properties = Properties?.ToProperties()! }; - } + public UpdateField ToCommand(long id, long? parentId = null) + { + return new UpdateField { ParentFieldId = parentId, FieldId = id, Properties = Properties?.ToProperties()! }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs index bb26119839..3eecd2bee0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs @@ -11,52 +11,51 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class UpdateSchemaDto { - public sealed class UpdateSchemaDto + /// <summary> + /// Optional label for the editor. + /// </summary> + [LocalizedStringLength(100)] + public string? Label { get; set; } + + /// <summary> + /// Hints to describe the schema. + /// </summary> + [LocalizedStringLength(1000)] + public string? Hints { get; set; } + + /// <summary> + /// The url to a the sidebar plugin for content lists. + /// </summary> + public string? ContentsSidebarUrl { get; set; } + + /// <summary> + /// The url to a the sidebar plugin for content items. + /// </summary> + public string? ContentSidebarUrl { get; set; } + + /// <summary> + /// The url to the editor plugin. + /// </summary> + public string? ContentEditorUrl { get; set; } + + /// <summary> + /// True to validate the content items on publish. + /// </summary> + public bool ValidateOnPublish { get; set; } + + /// <summary> + /// Tags for automation processes. + /// </summary> + public ReadonlyList<string>? Tags { get; set; } + + public UpdateSchema ToCommand() { - /// <summary> - /// Optional label for the editor. - /// </summary> - [LocalizedStringLength(100)] - public string? Label { get; set; } - - /// <summary> - /// Hints to describe the schema. - /// </summary> - [LocalizedStringLength(1000)] - public string? Hints { get; set; } - - /// <summary> - /// The url to a the sidebar plugin for content lists. - /// </summary> - public string? ContentsSidebarUrl { get; set; } - - /// <summary> - /// The url to a the sidebar plugin for content items. - /// </summary> - public string? ContentSidebarUrl { get; set; } - - /// <summary> - /// The url to the editor plugin. - /// </summary> - public string? ContentEditorUrl { get; set; } - - /// <summary> - /// True to validate the content items on publish. - /// </summary> - public bool ValidateOnPublish { get; set; } - - /// <summary> - /// Tags for automation processes. - /// </summary> - public ReadonlyList<string>? Tags { get; set; } - - public UpdateSchema ToCommand() - { - var properties = SimpleMapper.Map(this, new SchemaProperties()); - - return new UpdateSchema { Properties = properties }; - } + var properties = SimpleMapper.Map(this, new SchemaProperties()); + + return new UpdateSchema { Properties = properties }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs index c7a31761c7..ff8298ea28 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs @@ -10,126 +10,125 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public abstract class UpsertSchemaDto { - public abstract class UpsertSchemaDto + /// <summary> + /// The optional properties. + /// </summary> + public SchemaPropertiesDto? Properties { get; set; } + + /// <summary> + /// The optional scripts. + /// </summary> + public SchemaScriptsDto? Scripts { get; set; } + + /// <summary> + /// The names of the fields that should be used in references. + /// </summary> + public FieldNames? FieldsInReferences { get; set; } + + /// <summary> + /// The names of the fields that should be shown in lists, including meta fields. + /// </summary> + public FieldNames? FieldsInLists { get; set; } + + /// <summary> + /// Optional fields. + /// </summary> + public UpsertSchemaFieldDto[]? Fields { get; set; } + + /// <summary> + /// The optional preview urls. + /// </summary> + public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } + + /// <summary> + /// The optional field Rules. + /// </summary> + public List<FieldRuleDto>? FieldRules { get; set; } + + /// <summary> + /// The category. + /// </summary> + public string? Category { get; set; } + + /// <summary> + /// Set it to true to autopublish the schema. + /// </summary> + public bool IsPublished { get; set; } + + public static T ToCommand<T, TSoure>(TSoure dto, T command) where T : SchemaCommandBase, IUpsertCommand where TSoure : UpsertSchemaDto { - /// <summary> - /// The optional properties. - /// </summary> - public SchemaPropertiesDto? Properties { get; set; } - - /// <summary> - /// The optional scripts. - /// </summary> - public SchemaScriptsDto? Scripts { get; set; } - - /// <summary> - /// The names of the fields that should be used in references. - /// </summary> - public FieldNames? FieldsInReferences { get; set; } - - /// <summary> - /// The names of the fields that should be shown in lists, including meta fields. - /// </summary> - public FieldNames? FieldsInLists { get; set; } - - /// <summary> - /// Optional fields. - /// </summary> - public UpsertSchemaFieldDto[]? Fields { get; set; } - - /// <summary> - /// The optional preview urls. - /// </summary> - public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } - - /// <summary> - /// The optional field Rules. - /// </summary> - public List<FieldRuleDto>? FieldRules { get; set; } - - /// <summary> - /// The category. - /// </summary> - public string? Category { get; set; } - - /// <summary> - /// Set it to true to autopublish the schema. - /// </summary> - public bool IsPublished { get; set; } - - public static T ToCommand<T, TSoure>(TSoure dto, T command) where T : SchemaCommandBase, IUpsertCommand where TSoure : UpsertSchemaDto + SimpleMapper.Map(dto, command); + + if (dto.Properties != null) { - SimpleMapper.Map(dto, command); + command.Properties = new SchemaProperties(); - if (dto.Properties != null) - { - command.Properties = new SchemaProperties(); + SimpleMapper.Map(dto.Properties, command.Properties); + } - SimpleMapper.Map(dto.Properties, command.Properties); - } + if (dto.Scripts != null) + { + command.Scripts = new SchemaScripts(); - if (dto.Scripts != null) - { - command.Scripts = new SchemaScripts(); + SimpleMapper.Map(dto.Scripts, command.Scripts); + } - SimpleMapper.Map(dto.Scripts, command.Scripts); - } + if (dto.Fields?.Length > 0) + { + var fields = new List<UpsertSchemaField>(); - if (dto.Fields?.Length > 0) + foreach (var rootFieldDto in dto.Fields) { - var fields = new List<UpsertSchemaField>(); + var rootProperties = rootFieldDto?.Properties?.ToProperties(); + var rootField = new UpsertSchemaField { Properties = rootProperties! }; - foreach (var rootFieldDto in dto.Fields) + if (rootFieldDto != null) { - var rootProperties = rootFieldDto?.Properties?.ToProperties(); - var rootField = new UpsertSchemaField { Properties = rootProperties! }; + SimpleMapper.Map(rootFieldDto, rootField); - if (rootFieldDto != null) + if (rootFieldDto?.Nested?.Length > 0) { - SimpleMapper.Map(rootFieldDto, rootField); + var nestedFields = new List<UpsertSchemaNestedField>(); - if (rootFieldDto?.Nested?.Length > 0) + foreach (var nestedFieldDto in rootFieldDto.Nested) { - var nestedFields = new List<UpsertSchemaNestedField>(); + var nestedProperties = nestedFieldDto?.Properties?.ToProperties(); + var nestedField = new UpsertSchemaNestedField { Properties = nestedProperties! }; - foreach (var nestedFieldDto in rootFieldDto.Nested) + if (nestedFieldDto != null) { - var nestedProperties = nestedFieldDto?.Properties?.ToProperties(); - var nestedField = new UpsertSchemaNestedField { Properties = nestedProperties! }; - - if (nestedFieldDto != null) - { - SimpleMapper.Map(nestedFieldDto, nestedField); - } - - nestedFields.Add(nestedField); + SimpleMapper.Map(nestedFieldDto, nestedField); } - rootField.Nested = nestedFields.ToArray(); + nestedFields.Add(nestedField); } - } - fields.Add(rootField); + rootField.Nested = nestedFields.ToArray(); + } } - command.Fields = fields.ToArray(); + fields.Add(rootField); } - if (dto.FieldRules?.Count > 0) - { - var fieldRuleCommands = new List<FieldRuleCommand>(); + command.Fields = fields.ToArray(); + } - foreach (var fieldRule in dto.FieldRules) - { - fieldRuleCommands.Add(fieldRule.ToCommand()); - } + if (dto.FieldRules?.Count > 0) + { + var fieldRuleCommands = new List<FieldRuleCommand>(); - command.FieldRules = fieldRuleCommands.ToArray(); + foreach (var fieldRule in dto.FieldRules) + { + fieldRuleCommands.Add(fieldRule.ToCommand()); } - return command; + command.FieldRules = fieldRuleCommands.ToArray(); } + + return command; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs index 6faed457a4..eaec3434bd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs @@ -7,46 +7,45 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class UpsertSchemaFieldDto { - public sealed class UpsertSchemaFieldDto - { - /// <summary> - /// The name of the field. Must be unique within the schema. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$")] - public string Name { get; set; } - - /// <summary> - /// Defines if the field is hidden. - /// </summary> - public bool IsHidden { get; set; } - - /// <summary> - /// Defines if the field is locked. - /// </summary> - public bool IsLocked { get; set; } - - /// <summary> - /// Defines if the field is disabled. - /// </summary> - public bool IsDisabled { get; set; } - - /// <summary> - /// Determines the optional partitioning of the field. - /// </summary> - public string? Partitioning { get; set; } - - /// <summary> - /// The field properties. - /// </summary> - [LocalizedRequired] - public FieldPropertiesDto Properties { get; set; } - - /// <summary> - /// The nested fields. - /// </summary> - public UpsertSchemaNestedFieldDto[]? Nested { get; set; } - } + /// <summary> + /// The name of the field. Must be unique within the schema. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$")] + public string Name { get; set; } + + /// <summary> + /// Defines if the field is hidden. + /// </summary> + public bool IsHidden { get; set; } + + /// <summary> + /// Defines if the field is locked. + /// </summary> + public bool IsLocked { get; set; } + + /// <summary> + /// Defines if the field is disabled. + /// </summary> + public bool IsDisabled { get; set; } + + /// <summary> + /// Determines the optional partitioning of the field. + /// </summary> + public string? Partitioning { get; set; } + + /// <summary> + /// The field properties. + /// </summary> + [LocalizedRequired] + public FieldPropertiesDto Properties { get; set; } + + /// <summary> + /// The nested fields. + /// </summary> + public UpsertSchemaNestedFieldDto[]? Nested { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs index e83369ee3c..7095de2c1b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs @@ -7,36 +7,35 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class UpsertSchemaNestedFieldDto { - public sealed class UpsertSchemaNestedFieldDto - { - /// <summary> - /// The name of the field. Must be unique within the schema. - /// </summary> - [LocalizedRequired] - [LocalizedRegularExpression("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$")] - public string Name { get; set; } + /// <summary> + /// The name of the field. Must be unique within the schema. + /// </summary> + [LocalizedRequired] + [LocalizedRegularExpression("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$")] + public string Name { get; set; } - /// <summary> - /// Defines if the field is hidden. - /// </summary> - public bool IsHidden { get; set; } + /// <summary> + /// Defines if the field is hidden. + /// </summary> + public bool IsHidden { get; set; } - /// <summary> - /// Defines if the field is locked. - /// </summary> - public bool IsLocked { get; set; } + /// <summary> + /// Defines if the field is locked. + /// </summary> + public bool IsLocked { get; set; } - /// <summary> - /// Defines if the field is disabled. - /// </summary> - public bool IsDisabled { get; set; } + /// <summary> + /// Defines if the field is disabled. + /// </summary> + public bool IsDisabled { get; set; } - /// <summary> - /// The field properties. - /// </summary> - [LocalizedRequired] - public FieldPropertiesDto Properties { get; set; } - } + /// <summary> + /// The field properties. + /// </summary> + [LocalizedRequired] + public FieldPropertiesDto Properties { get; set; } } \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index cb5b604161..408d7fd38b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -13,545 +13,544 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Schemas +namespace Squidex.Areas.Api.Controllers.Schemas; + +/// <summary> +/// Update and query information about schemas. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Schemas))] +public sealed class SchemaFieldsController : ApiController { + public SchemaFieldsController(ICommandBus commandBus) + : base(commandBus) + { + } + + /// <summary> + /// Add a schema field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The field object that needs to be added to the schema.</param> + /// <returns> + /// 201 => Schema field created. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// 409 => Schema field name already in use. + /// </returns> + [HttpPost] + [Route("apps/{app}/schemas/{schema}/fields/")] + [ProducesResponseType(typeof(SchemaDto), 201)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PostField(string app, string schema, [FromBody] AddFieldDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(command); + + return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, schema }, response); + } + + /// <summary> + /// Add a nested field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="request">The field object that needs to be added to the schema.</param> + /// <returns> + /// 201 => Schema field created. + /// 400 => Schema request not valid. + /// 409 => Schema field name already in use. + /// 404 => Schema, field or app not found. + /// </returns> + [HttpPost] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/")] + [ProducesResponseType(typeof(SchemaDto), 201)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PostNestedField(string app, string schema, long parentId, [FromBody] AddFieldDto request) + { + var command = request.ToCommand(parentId); + + var response = await InvokeCommandAsync(command); + + return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, schema }, response); + } + /// <summary> - /// Update and query information about schemas. + /// Configure UI fields. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Schemas))] - public sealed class SchemaFieldsController : ApiController + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The request that contains the field names.</param> + /// <returns> + /// 200 => Schema UI fields defined. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/ui/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutSchemaUIFields(string app, string schema, [FromBody] ConfigureUIFieldsDto request) { - public SchemaFieldsController(ICommandBus commandBus) - : base(commandBus) - { - } - - /// <summary> - /// Add a schema field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The field object that needs to be added to the schema.</param> - /// <returns> - /// 201 => Schema field created. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// 409 => Schema field name already in use. - /// </returns> - [HttpPost] - [Route("apps/{app}/schemas/{schema}/fields/")] - [ProducesResponseType(typeof(SchemaDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PostField(string app, string schema, [FromBody] AddFieldDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(command); - - return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, schema }, response); - } - - /// <summary> - /// Add a nested field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="request">The field object that needs to be added to the schema.</param> - /// <returns> - /// 201 => Schema field created. - /// 400 => Schema request not valid. - /// 409 => Schema field name already in use. - /// 404 => Schema, field or app not found. - /// </returns> - [HttpPost] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/")] - [ProducesResponseType(typeof(SchemaDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PostNestedField(string app, string schema, long parentId, [FromBody] AddFieldDto request) - { - var command = request.ToCommand(parentId); - - var response = await InvokeCommandAsync(command); - - return CreatedAtAction(nameof(SchemasController.GetSchema), "Schemas", new { app, schema }, response); - } - - /// <summary> - /// Configure UI fields. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The request that contains the field names.</param> - /// <returns> - /// 200 => Schema UI fields defined. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/ui/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutSchemaUIFields(string app, string schema, [FromBody] ConfigureUIFieldsDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Reorder all fields. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The request that contains the field ids.</param> - /// <returns> - /// 200 => Schema fields reordered. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/ordering/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutSchemaFieldOrdering(string app, string schema, [FromBody] ReorderFieldsDto request) - { - var command = request.ToCommand(); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Reorder all nested fields. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="request">The request that contains the field ids.</param> - /// <returns> - /// 200 => Schema fields reordered. - /// 400 => Schema field request not valid. - /// 404 => Schema, field or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/ordering/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutNestedFieldOrdering(string app, string schema, long parentId, [FromBody] ReorderFieldsDto request) - { - var command = request.ToCommand(parentId); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Update a schema field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the field to update.</param> - /// <param name="request">The field object that needs to be added to the schema.</param> - /// <returns> - /// 200 => Schema field updated. - /// 400 => Schema field request not valid. - /// 404 => Schema, field or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{id:long}/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutField(string app, string schema, long id, [FromBody] UpdateFieldDto request) - { - var command = request.ToCommand(id); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Update a nested field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="id">The ID of the field to update.</param> - /// <param name="request">The field object that needs to be added to the schema.</param> - /// <returns> - /// 200 => Schema field updated. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutNestedField(string app, string schema, long parentId, long id, [FromBody] UpdateFieldDto request) - { - var command = request.ToCommand(id, parentId); - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Lock a schema field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the field to lock.</param> - /// <returns> - /// 200 => Schema field shown. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - /// <remarks> - /// A locked field cannot be updated or deleted. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{id:long}/lock/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> LockField(string app, string schema, long id) - { - var command = new LockField { FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Lock a nested field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="id">The ID of the field to lock.</param> - /// <returns> - /// 200 => Schema field hidden. - /// 400 => Schema field request not valid or field locked. - /// 404 => Field, schema, or app not found. - /// </returns> - /// <remarks> - /// A locked field cannot be edited or deleted. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/lock/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> LockNestedField(string app, string schema, long parentId, long id) - { - var command = new LockField { ParentFieldId = parentId, FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Hide a schema field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the field to hide.</param> - /// <returns> - /// 200 => Schema field hidden. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - /// <remarks> - /// A hidden field is not part of the API response, but can still be edited in the portal. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{id:long}/hide/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> HideField(string app, string schema, long id) - { - var command = new HideField { FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Hide a nested field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="id">The ID of the field to hide.</param> - /// <returns> - /// 200 => Schema field hidden. - /// 400 => Schema field request not valid or field locked. - /// 404 => Field, schema, or app not found. - /// </returns> - /// <remarks> - /// A hidden field is not part of the API response, but can still be edited in the portal. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/hide/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> HideNestedField(string app, string schema, long parentId, long id) - { - var command = new HideField { ParentFieldId = parentId, FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Show a schema field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the field to show.</param> - /// <returns> - /// 200 => Schema field shown. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - /// <remarks> - /// A hidden field is not part of the API response, but can still be edited in the portal. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{id:long}/show/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> ShowField(string app, string schema, long id) - { - var command = new ShowField { FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Show a nested field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="id">The ID of the field to show.</param> - /// <returns> - /// 200 => Schema field shown. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - /// <remarks> - /// A hidden field is not part of the API response, but can still be edited in the portal. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/show/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> ShowNestedField(string app, string schema, long parentId, long id) - { - var command = new ShowField { ParentFieldId = parentId, FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Enable a schema field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the field to enable.</param> - /// <returns> - /// 200 => Schema field enabled. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - /// <remarks> - /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{id:long}/enable/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> EnableField(string app, string schema, long id) - { - var command = new EnableField { FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Enable a nested field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="id">The ID of the field to enable.</param> - /// <returns> - /// 200 => Schema field enabled. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - /// <remarks> - /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/enable/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> EnableNestedField(string app, string schema, long parentId, long id) - { - var command = new EnableField { ParentFieldId = parentId, FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Disable a schema field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the field to disable.</param> - /// <returns> - /// 200 => Schema field disabled. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - /// <remarks> - /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{id:long}/disable/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> DisableField(string app, string schema, long id) - { - var command = new DisableField { FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Disable a nested field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="id">The ID of the field to disable.</param> - /// <returns> - /// 200 => Schema field disabled. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - /// <remarks> - /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. - /// </remarks> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/disable/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> DisableNestedField(string app, string schema, long parentId, long id) - { - var command = new DisableField { ParentFieldId = parentId, FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Delete a schema field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="id">The ID of the field to disable.</param> - /// <returns> - /// 200 => Schema field deleted. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/schemas/{schema}/fields/{id:long}/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteField(string app, string schema, long id) - { - var command = new DeleteField { FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - /// <summary> - /// Delete a nested field. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="parentId">The parent field id.</param> - /// <param name="id">The ID of the field to disable.</param> - /// <returns> - /// 200 => Schema field deleted. - /// 400 => Schema field request not valid or field locked. - /// 404 => Schema, field or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteNestedField(string app, string schema, long parentId, long id) - { - var command = new DeleteField { ParentFieldId = parentId, FieldId = id }; - - var response = await InvokeCommandAsync(command); - - return Ok(response); - } - - private async Task<SchemaDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - var result = context.Result<ISchemaEntity>(); - var response = SchemaDto.FromDomain(result, Resources); - - return response; - } + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Reorder all fields. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The request that contains the field ids.</param> + /// <returns> + /// 200 => Schema fields reordered. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/ordering/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutSchemaFieldOrdering(string app, string schema, [FromBody] ReorderFieldsDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Reorder all nested fields. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="request">The request that contains the field ids.</param> + /// <returns> + /// 200 => Schema fields reordered. + /// 400 => Schema field request not valid. + /// 404 => Schema, field or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/ordering/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutNestedFieldOrdering(string app, string schema, long parentId, [FromBody] ReorderFieldsDto request) + { + var command = request.ToCommand(parentId); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Update a schema field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the field to update.</param> + /// <param name="request">The field object that needs to be added to the schema.</param> + /// <returns> + /// 200 => Schema field updated. + /// 400 => Schema field request not valid. + /// 404 => Schema, field or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{id:long}/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutField(string app, string schema, long id, [FromBody] UpdateFieldDto request) + { + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Update a nested field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="id">The ID of the field to update.</param> + /// <param name="request">The field object that needs to be added to the schema.</param> + /// <returns> + /// 200 => Schema field updated. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutNestedField(string app, string schema, long parentId, long id, [FromBody] UpdateFieldDto request) + { + var command = request.ToCommand(id, parentId); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Lock a schema field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the field to lock.</param> + /// <returns> + /// 200 => Schema field shown. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + /// <remarks> + /// A locked field cannot be updated or deleted. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{id:long}/lock/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> LockField(string app, string schema, long id) + { + var command = new LockField { FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Lock a nested field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="id">The ID of the field to lock.</param> + /// <returns> + /// 200 => Schema field hidden. + /// 400 => Schema field request not valid or field locked. + /// 404 => Field, schema, or app not found. + /// </returns> + /// <remarks> + /// A locked field cannot be edited or deleted. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/lock/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> LockNestedField(string app, string schema, long parentId, long id) + { + var command = new LockField { ParentFieldId = parentId, FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Hide a schema field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the field to hide.</param> + /// <returns> + /// 200 => Schema field hidden. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + /// <remarks> + /// A hidden field is not part of the API response, but can still be edited in the portal. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{id:long}/hide/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> HideField(string app, string schema, long id) + { + var command = new HideField { FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Hide a nested field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="id">The ID of the field to hide.</param> + /// <returns> + /// 200 => Schema field hidden. + /// 400 => Schema field request not valid or field locked. + /// 404 => Field, schema, or app not found. + /// </returns> + /// <remarks> + /// A hidden field is not part of the API response, but can still be edited in the portal. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/hide/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> HideNestedField(string app, string schema, long parentId, long id) + { + var command = new HideField { ParentFieldId = parentId, FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Show a schema field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the field to show.</param> + /// <returns> + /// 200 => Schema field shown. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + /// <remarks> + /// A hidden field is not part of the API response, but can still be edited in the portal. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{id:long}/show/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> ShowField(string app, string schema, long id) + { + var command = new ShowField { FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Show a nested field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="id">The ID of the field to show.</param> + /// <returns> + /// 200 => Schema field shown. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + /// <remarks> + /// A hidden field is not part of the API response, but can still be edited in the portal. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/show/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> ShowNestedField(string app, string schema, long parentId, long id) + { + var command = new ShowField { ParentFieldId = parentId, FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Enable a schema field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the field to enable.</param> + /// <returns> + /// 200 => Schema field enabled. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + /// <remarks> + /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{id:long}/enable/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> EnableField(string app, string schema, long id) + { + var command = new EnableField { FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Enable a nested field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="id">The ID of the field to enable.</param> + /// <returns> + /// 200 => Schema field enabled. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + /// <remarks> + /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/enable/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> EnableNestedField(string app, string schema, long parentId, long id) + { + var command = new EnableField { ParentFieldId = parentId, FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Disable a schema field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the field to disable.</param> + /// <returns> + /// 200 => Schema field disabled. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + /// <remarks> + /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{id:long}/disable/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> DisableField(string app, string schema, long id) + { + var command = new DisableField { FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Disable a nested field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="id">The ID of the field to disable.</param> + /// <returns> + /// 200 => Schema field disabled. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + /// <remarks> + /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. + /// </remarks> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/disable/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> DisableNestedField(string app, string schema, long parentId, long id) + { + var command = new DisableField { ParentFieldId = parentId, FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Delete a schema field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="id">The ID of the field to disable.</param> + /// <returns> + /// 200 => Schema field deleted. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/schemas/{schema}/fields/{id:long}/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteField(string app, string schema, long id) + { + var command = new DeleteField { FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// <summary> + /// Delete a nested field. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="parentId">The parent field id.</param> + /// <param name="id">The ID of the field to disable.</param> + /// <returns> + /// 200 => Schema field deleted. + /// 400 => Schema field request not valid or field locked. + /// 404 => Schema, field or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteNestedField(string app, string schema, long parentId, long id) + { + var command = new DeleteField { ParentFieldId = parentId, FieldId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + private async Task<SchemaDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + + var result = context.Result<ISchemaEntity>(); + var response = SchemaDto.FromDomain(result, Resources); + + return response; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 440556373b..916b3ebf21 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -21,386 +21,385 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Schemas +namespace Squidex.Areas.Api.Controllers.Schemas; + +/// <summary> +/// Update and query information about schemas. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Schemas))] +public sealed class SchemasController : ApiController { + private readonly IAppProvider appProvider; + + public SchemasController(ICommandBus commandBus, IAppProvider appProvider) + : base(commandBus) + { + this.appProvider = appProvider; + } + /// <summary> - /// Update and query information about schemas. + /// Get schemas. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Schemas))] - public sealed class SchemasController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Schemas returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/schemas/")] + [ProducesResponseType(typeof(SchemasDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasRead)] + [ApiCosts(0)] + public async Task<IActionResult> GetSchemas(string app) { - private readonly IAppProvider appProvider; + var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); - public SchemasController(ICommandBus commandBus, IAppProvider appProvider) - : base(commandBus) + var response = Deferred.Response(() => { - this.appProvider = appProvider; - } + return SchemasDto.FromDomain(schemas, Resources); + }); - /// <summary> - /// Get schemas. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Schemas returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/schemas/")] - [ProducesResponseType(typeof(SchemasDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasRead)] - [ApiCosts(0)] - public async Task<IActionResult> GetSchemas(string app) - { - var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); + Response.Headers[HeaderNames.ETag] = schemas.ToEtag(); - var response = Deferred.Response(() => - { - return SchemasDto.FromDomain(schemas, Resources); - }); + return Ok(response); + } - Response.Headers[HeaderNames.ETag] = schemas.ToEtag(); + /// <summary> + /// Get a schema by name. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema to retrieve.</param> + /// <returns> + /// 200 => Schema found. + /// 404 => Schema or app not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/schemas/{schema}/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasRead)] + [ApiCosts(0)] + public IActionResult GetSchema(string app, string schema) + { + var response = Deferred.Response(() => + { + return SchemaDto.FromDomain(Schema, Resources); + }); - return Ok(response); - } + Response.Headers[HeaderNames.ETag] = Schema.ToEtag(); - /// <summary> - /// Get a schema by name. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema to retrieve.</param> - /// <returns> - /// 200 => Schema found. - /// 404 => Schema or app not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/schemas/{schema}/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasRead)] - [ApiCosts(0)] - public IActionResult GetSchema(string app, string schema) - { - var response = Deferred.Response(() => - { - return SchemaDto.FromDomain(Schema, Resources); - }); + return Ok(response); + } - Response.Headers[HeaderNames.ETag] = Schema.ToEtag(); + /// <summary> + /// Create a new schema. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="request">The schema object that needs to be added to the app.</param> + /// <returns> + /// 201 => Schema created. + /// 400 => Schema request not valid. + /// 409 => Schema name already in use. + /// </returns> + [HttpPost] + [Route("apps/{app}/schemas/")] + [ProducesResponseType(typeof(SchemaDto), 201)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasCreate)] + [ApiCosts(1)] + public async Task<IActionResult> PostSchema(string app, [FromBody] CreateSchemaDto request) + { + var command = request.ToCommand(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Create a new schema. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The schema object that needs to be added to the app.</param> - /// <returns> - /// 201 => Schema created. - /// 400 => Schema request not valid. - /// 409 => Schema name already in use. - /// </returns> - [HttpPost] - [Route("apps/{app}/schemas/")] - [ProducesResponseType(typeof(SchemaDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasCreate)] - [ApiCosts(1)] - public async Task<IActionResult> PostSchema(string app, [FromBody] CreateSchemaDto request) - { - var command = request.ToCommand(); + return CreatedAtAction(nameof(GetSchema), new { app, schema = request.Name }, response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Update a schema. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The schema object that needs to updated.</param> + /// <returns> + /// 200 => Schema updated. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutSchema(string app, string schema, [FromBody] UpdateSchemaDto request) + { + var command = request.ToCommand(); - return CreatedAtAction(nameof(GetSchema), new { app, schema = request.Name }, response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Update a schema. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The schema object that needs to updated.</param> - /// <returns> - /// 200 => Schema updated. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutSchema(string app, string schema, [FromBody] UpdateSchemaDto request) - { - var command = request.ToCommand(); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Synchronize a schema. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The schema object that needs to updated.</param> + /// <returns> + /// 200 => Schema updated. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/sync")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutSchemaSync(string app, string schema, [FromBody] SynchronizeSchemaDto request) + { + var command = request.ToCommand(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Synchronize a schema. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The schema object that needs to updated.</param> - /// <returns> - /// 200 => Schema updated. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/sync")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutSchemaSync(string app, string schema, [FromBody] SynchronizeSchemaDto request) - { - var command = request.ToCommand(); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Update a schema category. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The schema object that needs to updated.</param> + /// <returns> + /// 200 => Schema updated. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/category")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutCategory(string app, string schema, [FromBody] ChangeCategoryDto request) + { + var command = request.ToCommand(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Update a schema category. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The schema object that needs to updated.</param> - /// <returns> - /// 200 => Schema updated. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/category")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutCategory(string app, string schema, [FromBody] ChangeCategoryDto request) - { - var command = request.ToCommand(); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Update the preview urls. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The preview urls for the schema.</param> + /// <returns> + /// 200 => Schema updated. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/preview-urls")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutPreviewUrls(string app, string schema, [FromBody] ConfigurePreviewUrlsDto request) + { + var command = request.ToCommand(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Update the preview urls. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The preview urls for the schema.</param> - /// <returns> - /// 200 => Schema updated. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/preview-urls")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutPreviewUrls(string app, string schema, [FromBody] ConfigurePreviewUrlsDto request) - { - var command = request.ToCommand(); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Update the scripts. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The schema scripts object that needs to updated.</param> + /// <returns> + /// 200 => Schema updated. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/scripts/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasScripts)] + [ApiCosts(1)] + public async Task<IActionResult> PutScripts(string app, string schema, [FromBody] SchemaScriptsDto request) + { + var command = request.ToCommand(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Update the scripts. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The schema scripts object that needs to updated.</param> - /// <returns> - /// 200 => Schema updated. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/scripts/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasScripts)] - [ApiCosts(1)] - public async Task<IActionResult> PutScripts(string app, string schema, [FromBody] SchemaScriptsDto request) - { - var command = request.ToCommand(); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Update the rules. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema.</param> + /// <param name="request">The schema rules object that needs to updated.</param> + /// <returns> + /// 200 => Schema updated. + /// 400 => Schema request not valid. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/rules/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task<IActionResult> PutRules(string app, string schema, [FromBody] ConfigureFieldRulesDto request) + { + var command = request.ToCommand(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Update the rules. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema.</param> - /// <param name="request">The schema rules object that needs to updated.</param> - /// <returns> - /// 200 => Schema updated. - /// 400 => Schema request not valid. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/rules/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasUpdate)] - [ApiCosts(1)] - public async Task<IActionResult> PutRules(string app, string schema, [FromBody] ConfigureFieldRulesDto request) - { - var command = request.ToCommand(); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Publish a schema. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema to publish.</param> + /// <returns> + /// 200 => Schema published. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/publish/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasPublish)] + [ApiCosts(1)] + public async Task<IActionResult> PublishSchema(string app, string schema) + { + var command = new PublishSchema(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Publish a schema. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema to publish.</param> - /// <returns> - /// 200 => Schema published. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/publish/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasPublish)] - [ApiCosts(1)] - public async Task<IActionResult> PublishSchema(string app, string schema) - { - var command = new PublishSchema(); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Unpublish a schema. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema to unpublish.</param> + /// <returns> + /// 200 => Schema unpublished. + /// 404 => Schema or app not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/schemas/{schema}/unpublish/")] + [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasPublish)] + [ApiCosts(1)] + public async Task<IActionResult> UnpublishSchema(string app, string schema) + { + var command = new UnpublishSchema(); - return Ok(response); - } + var response = await InvokeCommandAsync(command); - /// <summary> - /// Unpublish a schema. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema to unpublish.</param> - /// <returns> - /// 200 => Schema unpublished. - /// 404 => Schema or app not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/schemas/{schema}/unpublish/")] - [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasPublish)] - [ApiCosts(1)] - public async Task<IActionResult> UnpublishSchema(string app, string schema) - { - var command = new UnpublishSchema(); + return Ok(response); + } - var response = await InvokeCommandAsync(command); + /// <summary> + /// Delete a schema. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="schema">The name of the schema to delete.</param> + /// <returns> + /// 204 => Schema deleted. + /// 404 => Schema or app not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/schemas/{schema}/")] + [ApiPermissionOrAnonymous(PermissionIds.AppSchemasDelete)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteSchema(string app, string schema) + { + var command = new DeleteSchema(); - return Ok(response); - } + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - /// <summary> - /// Delete a schema. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="schema">The name of the schema to delete.</param> - /// <returns> - /// 204 => Schema deleted. - /// 404 => Schema or app not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/schemas/{schema}/")] - [ApiPermissionOrAnonymous(PermissionIds.AppSchemasDelete)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteSchema(string app, string schema) - { - var command = new DeleteSchema(); + return NoContent(); + } - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + [HttpGet] + [Route("apps/{app}/schemas/{schema}/completion")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + [OpenApiIgnore] + public async Task<IActionResult> GetScriptCompletion(string app, string schema, + [FromServices] ScriptingCompleter completer) + { + var completion = completer.ContentScript(await BuildModel()); - return NoContent(); - } + return Ok(completion); + } - [HttpGet] - [Route("apps/{app}/schemas/{schema}/completion")] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - [OpenApiIgnore] - public async Task<IActionResult> GetScriptCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) - { - var completion = completer.ContentScript(await BuildModel()); + [HttpGet] + [Route("apps/{app}/schemas/{schema}/completion/triggers")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + [OpenApiIgnore] + public async Task<IActionResult> GetScriptTriggerCompletion(string app, string schema, + [FromServices] ScriptingCompleter completer) + { + var completion = completer.ContentTrigger(await BuildModel()); - return Ok(completion); - } + return Ok(completion); + } - [HttpGet] - [Route("apps/{app}/schemas/{schema}/completion/triggers")] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - [OpenApiIgnore] - public async Task<IActionResult> GetScriptTriggerCompletion(string app, string schema, - [FromServices] ScriptingCompleter completer) - { - var completion = completer.ContentTrigger(await BuildModel()); + [HttpGet] + [Route("apps/{app}/schemas/{schema}/filters")] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + [OpenApiIgnore] + public async Task<IActionResult> GetFilters(string app, string schema) + { + var components = await appProvider.GetComponentsAsync(Schema, HttpContext.RequestAborted); - return Ok(completion); - } + var filters = ContentQueryModel.Build(Schema.SchemaDef, App.PartitionResolver(), components).Flatten(); - [HttpGet] - [Route("apps/{app}/schemas/{schema}/filters")] - [ApiPermissionOrAnonymous] - [ApiCosts(1)] - [OpenApiIgnore] - public async Task<IActionResult> GetFilters(string app, string schema) - { - var components = await appProvider.GetComponentsAsync(Schema, HttpContext.RequestAborted); + return Ok(filters); + } - var filters = ContentQueryModel.Build(Schema.SchemaDef, App.PartitionResolver(), components).Flatten(); + private async Task<FilterSchema> BuildModel() + { + var components = await appProvider.GetComponentsAsync(Schema, HttpContext.RequestAborted); - return Ok(filters); - } + return Schema.SchemaDef.BuildDataSchema(App.PartitionResolver(), components); + } - private async Task<FilterSchema> BuildModel() + private Task<ISchemaEntity?> GetSchemaAsync(string schema) + { + if (Guid.TryParse(schema, out var guid)) { - var components = await appProvider.GetComponentsAsync(Schema, HttpContext.RequestAborted); + var schemaId = DomainId.Create(guid); - return Schema.SchemaDef.BuildDataSchema(App.PartitionResolver(), components); + return appProvider.GetSchemaAsync(AppId, schemaId, ct: HttpContext.RequestAborted); } - - private Task<ISchemaEntity?> GetSchemaAsync(string schema) + else { - if (Guid.TryParse(schema, out var guid)) - { - var schemaId = DomainId.Create(guid); - - return appProvider.GetSchemaAsync(AppId, schemaId, ct: HttpContext.RequestAborted); - } - else - { - return appProvider.GetSchemaAsync(AppId, schema, ct: HttpContext.RequestAborted); - } + return appProvider.GetSchemaAsync(AppId, schema, ct: HttpContext.RequestAborted); } + } - private async Task<SchemaDto> InvokeCommandAsync(ICommand command) - { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + private async Task<SchemaDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var result = context.Result<ISchemaEntity>(); - var response = SchemaDto.FromDomain(result, Resources); + var result = context.Result<ISchemaEntity>(); + var response = SchemaDto.FromDomain(result, Resources); - return response; - } + return response; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Search/Models/SearchResultDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Search/Models/SearchResultDto.cs index 50cd961f0a..408c283259 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Search/Models/SearchResultDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Search/Models/SearchResultDto.cs @@ -10,40 +10,39 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Search.Models +namespace Squidex.Areas.Api.Controllers.Search.Models; + +public class SearchResultDto : Resource { - public class SearchResultDto : Resource + /// <summary> + /// The name of the search result. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } + + /// <summary> + /// The type of the search result. + /// </summary> + [LocalizedRequired] + public SearchResultType Type { get; set; } + + /// <summary> + /// An optional label. + /// </summary> + public string? Label { get; set; } + + public static SearchResultDto FromDomain(SearchResult searchResult) + { + var result = SimpleMapper.Map(searchResult, new SearchResultDto()); + + return result.CreateLinks(searchResult); + } + + protected SearchResultDto CreateLinks(SearchResult searchResult) { - /// <summary> - /// The name of the search result. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } - - /// <summary> - /// The type of the search result. - /// </summary> - [LocalizedRequired] - public SearchResultType Type { get; set; } - - /// <summary> - /// An optional label. - /// </summary> - public string? Label { get; set; } - - public static SearchResultDto FromDomain(SearchResult searchResult) - { - var result = SimpleMapper.Map(searchResult, new SearchResultDto()); - - return result.CreateLinks(searchResult); - } - - protected SearchResultDto CreateLinks(SearchResult searchResult) - { - AddGetLink("url", - searchResult.Url); - - return this; - } + AddGetLink("url", + searchResult.Url); + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs index dbe6444f53..d6dc997cf0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs @@ -12,43 +12,42 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Search +namespace Squidex.Areas.Api.Controllers.Search; + +/// <summary> +/// Retrieves search results. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Search))] +public class SearchController : ApiController { + private readonly ISearchManager searchManager; + + public SearchController(ISearchManager searchManager, ICommandBus commandBus) + : base(commandBus) + { + this.searchManager = searchManager; + } + /// <summary> - /// Retrieves search results. + /// Get search results. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Search))] - public class SearchController : ApiController + /// <param name="app">The name of the app.</param> + /// <param name="query">The search query.</param> + /// <returns> + /// 200 => Search results returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/search/")] + [ProducesResponseType(typeof(SearchResultDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppSearch)] + [ApiCosts(0)] + public async Task<IActionResult> GetSearchResults(string app, [FromQuery] string? query = null) { - private readonly ISearchManager searchManager; - - public SearchController(ISearchManager searchManager, ICommandBus commandBus) - : base(commandBus) - { - this.searchManager = searchManager; - } - - /// <summary> - /// Get search results. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="query">The search query.</param> - /// <returns> - /// 200 => Search results returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/search/")] - [ProducesResponseType(typeof(SearchResultDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppSearch)] - [ApiCosts(0)] - public async Task<IActionResult> GetSearchResults(string app, [FromQuery] string? query = null) - { - var result = await searchManager.SearchAsync(query, Context, HttpContext.RequestAborted); + var result = await searchManager.SearchAsync(query, Context, HttpContext.RequestAborted); - var response = result.Select(SearchResultDto.FromDomain).ToArray(); + var response = result.Select(SearchResultDto.FromDomain).ToArray(); - return Ok(response); - } + return Ok(response); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs index d616a903a6..a9bbcc82d0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs @@ -9,70 +9,69 @@ using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models; + +public sealed class CallsUsageDtoDto { - public sealed class CallsUsageDtoDto - { - /// <summary> - /// The total number of API calls. - /// </summary> - public long TotalCalls { get; set; } + /// <summary> + /// The total number of API calls. + /// </summary> + public long TotalCalls { get; set; } - /// <summary> - /// The total number of bytes transferred. - /// </summary> - public long TotalBytes { get; set; } + /// <summary> + /// The total number of bytes transferred. + /// </summary> + public long TotalBytes { get; set; } - /// <summary> - /// The total number of API calls this month. - /// </summary> - public long MonthCalls { get; set; } + /// <summary> + /// The total number of API calls this month. + /// </summary> + public long MonthCalls { get; set; } - /// <summary> - /// The total number of bytes transferred this month. - /// </summary> - public long MonthBytes { get; set; } + /// <summary> + /// The total number of bytes transferred this month. + /// </summary> + public long MonthBytes { get; set; } - /// <summary> - /// The amount of calls that will block the app. - /// </summary> - public long BlockingApiCalls { get; set; } + /// <summary> + /// The amount of calls that will block the app. + /// </summary> + public long BlockingApiCalls { get; set; } - /// <summary> - /// The included API traffic. - /// </summary> - public long AllowedBytes { get; set; } + /// <summary> + /// The included API traffic. + /// </summary> + public long AllowedBytes { get; set; } - /// <summary> - /// The included API calls. - /// </summary> - public long AllowedCalls { get; set; } + /// <summary> + /// The included API calls. + /// </summary> + public long AllowedCalls { get; set; } - /// <summary> - /// The average duration in milliseconds. - /// </summary> - public double AverageElapsedMs { get; set; } + /// <summary> + /// The average duration in milliseconds. + /// </summary> + public double AverageElapsedMs { get; set; } - /// <summary> - /// The statistics by date and group. - /// </summary> - [LocalizedRequired] - public Dictionary<string, CallsUsagePerDateDto[]> Details { get; set; } + /// <summary> + /// The statistics by date and group. + /// </summary> + [LocalizedRequired] + public Dictionary<string, CallsUsagePerDateDto[]> Details { get; set; } - public static CallsUsageDtoDto FromDomain(Plan plan, ApiStatsSummary summary, Dictionary<string, List<ApiStats>> details) + public static CallsUsageDtoDto FromDomain(Plan plan, ApiStatsSummary summary, Dictionary<string, List<ApiStats>> details) + { + return new CallsUsageDtoDto { - return new CallsUsageDtoDto - { - AverageElapsedMs = summary.AverageElapsedMs, - BlockingApiCalls = plan.BlockingApiCalls, - AllowedBytes = plan.MaxApiBytes, - AllowedCalls = plan.MaxApiCalls, - TotalBytes = summary.TotalBytes, - TotalCalls = summary.TotalCalls, - MonthBytes = summary.MonthBytes, - MonthCalls = summary.MonthCalls, - Details = details.ToDictionary(x => x.Key, x => x.Value.Select(CallsUsagePerDateDto.FromDomain).ToArray()) - }; - } + AverageElapsedMs = summary.AverageElapsedMs, + BlockingApiCalls = plan.BlockingApiCalls, + AllowedBytes = plan.MaxApiBytes, + AllowedCalls = plan.MaxApiCalls, + TotalBytes = summary.TotalBytes, + TotalCalls = summary.TotalCalls, + MonthBytes = summary.MonthBytes, + MonthCalls = summary.MonthCalls, + Details = details.ToDictionary(x => x.Key, x => x.Value.Select(CallsUsagePerDateDto.FromDomain).ToArray()) + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsagePerDateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsagePerDateDto.cs index 2011e0e78f..cc59315d62 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsagePerDateDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsagePerDateDto.cs @@ -8,41 +8,40 @@ using NodaTime; using Squidex.Infrastructure.UsageTracking; -namespace Squidex.Areas.Api.Controllers.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models; + +public sealed class CallsUsagePerDateDto { - public sealed class CallsUsagePerDateDto + /// <summary> + /// The date when the usage was tracked. + /// </summary> + public LocalDate Date { get; set; } + + /// <summary> + /// The total number of API calls. + /// </summary> + public long TotalCalls { get; set; } + + /// <summary> + /// The total number of bytes transferred. + /// </summary> + public long TotalBytes { get; set; } + + /// <summary> + /// The average duration in milliseconds. + /// </summary> + public double AverageElapsedMs { get; set; } + + public static CallsUsagePerDateDto FromDomain(ApiStats stats) { - /// <summary> - /// The date when the usage was tracked. - /// </summary> - public LocalDate Date { get; set; } - - /// <summary> - /// The total number of API calls. - /// </summary> - public long TotalCalls { get; set; } - - /// <summary> - /// The total number of bytes transferred. - /// </summary> - public long TotalBytes { get; set; } - - /// <summary> - /// The average duration in milliseconds. - /// </summary> - public double AverageElapsedMs { get; set; } - - public static CallsUsagePerDateDto FromDomain(ApiStats stats) + var result = new CallsUsagePerDateDto { - var result = new CallsUsagePerDateDto - { - Date = LocalDate.FromDateTime(DateTime.SpecifyKind(stats.Date, DateTimeKind.Utc)), - TotalBytes = stats.TotalBytes, - TotalCalls = stats.TotalCalls, - AverageElapsedMs = stats.AverageElapsedMs - }; - - return result; - } + Date = LocalDate.FromDateTime(DateTime.SpecifyKind(stats.Date, DateTimeKind.Utc)), + TotalBytes = stats.TotalBytes, + TotalCalls = stats.TotalCalls, + AverageElapsedMs = stats.AverageElapsedMs + }; + + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs index 24ddefa730..861ccf0b21 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs @@ -5,18 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models; + +public sealed class CurrentStorageDto { - public sealed class CurrentStorageDto - { - /// <summary> - /// The size in bytes. - /// </summary> - public long Size { get; set; } + /// <summary> + /// The size in bytes. + /// </summary> + public long Size { get; set; } - /// <summary> - /// The maximum allowed asset size. - /// </summary> - public long MaxAllowed { get; set; } - } + /// <summary> + /// The maximum allowed asset size. + /// </summary> + public long MaxAllowed { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs index 72f50f65fd..0089b25134 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/LogDownloadDto.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models; + +public sealed class LogDownloadDto { - public sealed class LogDownloadDto - { - /// <summary> - /// The url to download the log. - /// </summary> - public string? DownloadUrl { get; set; } - } + /// <summary> + /// The url to download the log. + /// </summary> + public string? DownloadUrl { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsagePerDateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsagePerDateDto.cs index acf0a69f10..64b52c5300 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsagePerDateDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsagePerDateDto.cs @@ -8,35 +8,34 @@ using NodaTime; using Squidex.Domain.Apps.Entities.Assets; -namespace Squidex.Areas.Api.Controllers.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models; + +public sealed class StorageUsagePerDateDto { - public sealed class StorageUsagePerDateDto - { - /// <summary> - /// The date when the usage was tracked. - /// </summary> - public LocalDate Date { get; set; } + /// <summary> + /// The date when the usage was tracked. + /// </summary> + public LocalDate Date { get; set; } - /// <summary> - /// The number of assets. - /// </summary> - public long TotalCount { get; set; } + /// <summary> + /// The number of assets. + /// </summary> + public long TotalCount { get; set; } - /// <summary> - /// The size in bytes. - /// </summary> - public long TotalSize { get; set; } + /// <summary> + /// The size in bytes. + /// </summary> + public long TotalSize { get; set; } - public static StorageUsagePerDateDto FromDomain(AssetStats stats) + public static StorageUsagePerDateDto FromDomain(AssetStats stats) + { + var result = new StorageUsagePerDateDto { - var result = new StorageUsagePerDateDto - { - Date = LocalDate.FromDateTime(DateTime.SpecifyKind(stats.Date, DateTimeKind.Utc)), - TotalCount = stats.TotalCount, - TotalSize = stats.TotalSize - }; + Date = LocalDate.FromDateTime(DateTime.SpecifyKind(stats.Date, DateTimeKind.Utc)), + TotalCount = stats.TotalCount, + TotalSize = stats.TotalSize + }; - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 411118f625..d4877a38c1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -18,263 +18,262 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Statistics +namespace Squidex.Areas.Api.Controllers.Statistics; + +/// <summary> +/// Retrieves usage information for apps and teams. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Statistics))] +public sealed class UsagesController : ApiController { + private readonly IApiUsageTracker usageTracker; + private readonly IAppLogStore usageLog; + private readonly IUsageGate usageGate; + private readonly IAssetUsageTracker assetStatsRepository; + private readonly IDataProtector dataProtector; + private readonly IUrlGenerator urlGenerator; + + public UsagesController( + ICommandBus commandBus, + IDataProtectionProvider dataProtection, + IApiUsageTracker usageTracker, + IAppLogStore usageLog, + IUsageGate usageGate, + IAssetUsageTracker assetStatsRepository, + IUrlGenerator urlGenerator) + : base(commandBus) + { + this.usageLog = usageLog; + this.assetStatsRepository = assetStatsRepository; + this.urlGenerator = urlGenerator; + this.usageGate = usageGate; + this.usageTracker = usageTracker; + + dataProtector = dataProtection.CreateProtector("LogToken"); + } + /// <summary> - /// Retrieves usage information for apps and teams. + /// Get api calls as log file. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Statistics))] - public sealed class UsagesController : ApiController + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Usage tracking results returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/usages/log/")] + [ProducesResponseType(typeof(LogDownloadDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] + [ApiCosts(0)] + public IActionResult GetLog(string app) { - private readonly IApiUsageTracker usageTracker; - private readonly IAppLogStore usageLog; - private readonly IUsageGate usageGate; - private readonly IAssetUsageTracker assetStatsRepository; - private readonly IDataProtector dataProtector; - private readonly IUrlGenerator urlGenerator; - - public UsagesController( - ICommandBus commandBus, - IDataProtectionProvider dataProtection, - IApiUsageTracker usageTracker, - IAppLogStore usageLog, - IUsageGate usageGate, - IAssetUsageTracker assetStatsRepository, - IUrlGenerator urlGenerator) - : base(commandBus) - { - this.usageLog = usageLog; - this.assetStatsRepository = assetStatsRepository; - this.urlGenerator = urlGenerator; - this.usageGate = usageGate; - this.usageTracker = usageTracker; + var token = dataProtector.Protect(App.Id.ToString()); - dataProtector = dataProtection.CreateProtector("LogToken"); - } + // Generate an URL with a encrypted token to provide a normal GET link without authorization infos. + var url = urlGenerator.BuildUrl($"/api/apps/log/{token}/"); - /// <summary> - /// Get api calls as log file. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Usage tracking results returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/usages/log/")] - [ProducesResponseType(typeof(LogDownloadDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] - [ApiCosts(0)] - public IActionResult GetLog(string app) - { - var token = dataProtector.Protect(App.Id.ToString()); + var response = new LogDownloadDto { DownloadUrl = url }; - // Generate an URL with a encrypted token to provide a normal GET link without authorization infos. - var url = urlGenerator.BuildUrl($"/api/apps/log/{token}/"); + return Ok(response); + } - var response = new LogDownloadDto { DownloadUrl = url }; + [HttpGet] + [Route("apps/log/{token}/")] + [ApiExplorerSettings(IgnoreApi = true)] + public IActionResult GetLogFile(string token) + { + // Decrypt the token that has previously been generated. + var appId = DomainId.Create(dataProtector.Unprotect(token)); - return Ok(response); - } + var fileDate = DateTime.UtcNow.Date; + var fileName = $"Usage-{fileDate:yyy-MM-dd}.csv"; - [HttpGet] - [Route("apps/log/{token}/")] - [ApiExplorerSettings(IgnoreApi = true)] - public IActionResult GetLogFile(string token) + var callback = new FileCallback((body, range, ct) => { - // Decrypt the token that has previously been generated. - var appId = DomainId.Create(dataProtector.Unprotect(token)); - - var fileDate = DateTime.UtcNow.Date; - var fileName = $"Usage-{fileDate:yyy-MM-dd}.csv"; - - var callback = new FileCallback((body, range, ct) => - { - return usageLog.ReadLogAsync(appId, fileDate.AddDays(-30), fileDate, body, ct); - }); + return usageLog.ReadLogAsync(appId, fileDate.AddDays(-30), fileDate, body, ct); + }); - return new FileCallbackResult("text/csv", callback) - { - FileDownloadName = fileName - }; - } + return new FileCallbackResult("text/csv", callback) + { + FileDownloadName = fileName + }; + } - /// <summary> - /// Get api calls in date range. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="fromDate">The from date.</param> - /// <param name="toDate">The to date.</param> - /// <returns> - /// 200 => API call returned. - /// 404 => App not found. - /// 400 => Range between from date and to date is not valid or has more than 100 days. - /// </returns> - [HttpGet] - [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] - [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] - [ApiCosts(0)] - public async Task<IActionResult> GetUsages(string app, DateTime fromDate, DateTime toDate) + /// <summary> + /// Get api calls in date range. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="fromDate">The from date.</param> + /// <param name="toDate">The to date.</param> + /// <returns> + /// 200 => API call returned. + /// 404 => App not found. + /// 400 => Range between from date and to date is not valid or has more than 100 days. + /// </returns> + [HttpGet] + [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] + [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] + [ApiCosts(0)] + public async Task<IActionResult> GetUsages(string app, DateTime fromDate, DateTime toDate) + { + // We can only query 100 logs for up to 100 days. + if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) { - // We can only query 100 logs for up to 100 days. - if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) - { - return BadRequest(); - } + return BadRequest(); + } - var (summary, details) = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); + var (summary, details) = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); - // Use the current app plan to show the limits to the user. - var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + // Use the current app plan to show the limits to the user. + var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); - var response = CallsUsageDtoDto.FromDomain(plan, summary, details); + var response = CallsUsageDtoDto.FromDomain(plan, summary, details); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Get api calls in date range for team. - /// </summary> - /// <param name="team">The name of the team.</param> - /// <param name="fromDate">The from date.</param> - /// <param name="toDate">The to date.</param> - /// <returns> - /// 200 => API call returned. - /// 400 => Range between from date and to date is not valid or has more than 100 days. - /// 404 => Team not found. - /// </returns> - [HttpGet] - [Route("teams/{team}/usages/calls/{fromDate}/{toDate}/")] - [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] - [ApiCosts(0)] - public async Task<IActionResult> GetUsagesForTeam(string team, DateTime fromDate, DateTime toDate) + /// <summary> + /// Get api calls in date range for team. + /// </summary> + /// <param name="team">The name of the team.</param> + /// <param name="fromDate">The from date.</param> + /// <param name="toDate">The to date.</param> + /// <returns> + /// 200 => API call returned. + /// 400 => Range between from date and to date is not valid or has more than 100 days. + /// 404 => Team not found. + /// </returns> + [HttpGet] + [Route("teams/{team}/usages/calls/{fromDate}/{toDate}/")] + [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] + [ApiCosts(0)] + public async Task<IActionResult> GetUsagesForTeam(string team, DateTime fromDate, DateTime toDate) + { + // We can only query 100 logs for up to 100 days. + if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) { - // We can only query 100 logs for up to 100 days. - if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) - { - return BadRequest(); - } + return BadRequest(); + } - var (summary, details) = await usageTracker.QueryAsync(TeamId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); + var (summary, details) = await usageTracker.QueryAsync(TeamId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); - // Use the current team plan to show the limits to the user. - var (plan, _) = await usageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted); + // Use the current team plan to show the limits to the user. + var (plan, _) = await usageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted); - var response = CallsUsageDtoDto.FromDomain(plan, summary, details); + var response = CallsUsageDtoDto.FromDomain(plan, summary, details); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Get total asset size. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => Storage usage returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/usages/storage/today/")] - [ProducesResponseType(typeof(CurrentStorageDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] - [ApiCosts(0)] - public async Task<IActionResult> GetCurrentStorageSize(string app) - { - var size = await assetStatsRepository.GetTotalSizeByAppAsync(AppId, HttpContext.RequestAborted); + /// <summary> + /// Get total asset size. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => Storage usage returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/usages/storage/today/")] + [ProducesResponseType(typeof(CurrentStorageDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] + [ApiCosts(0)] + public async Task<IActionResult> GetCurrentStorageSize(string app) + { + var size = await assetStatsRepository.GetTotalSizeByAppAsync(AppId, HttpContext.RequestAborted); - // Use the current app plan to show the limits to the user. - var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + // Use the current app plan to show the limits to the user. + var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); - var response = new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize }; + var response = new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize }; - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Get total asset size by team. - /// </summary> - /// <param name="team">The ID of the team.</param> - /// <returns> - /// 200 => Storage usage returned. - /// 404 => Team not found. - /// </returns> - [HttpGet] - [Route("teams/{team}/usages/storage/today/")] - [ProducesResponseType(typeof(CurrentStorageDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] - [ApiCosts(0)] - public async Task<IActionResult> GetTeamCurrentStorageSizeForTeam(string team) - { - var size = await assetStatsRepository.GetTotalSizeByTeamAsync(TeamId, HttpContext.RequestAborted); + /// <summary> + /// Get total asset size by team. + /// </summary> + /// <param name="team">The ID of the team.</param> + /// <returns> + /// 200 => Storage usage returned. + /// 404 => Team not found. + /// </returns> + [HttpGet] + [Route("teams/{team}/usages/storage/today/")] + [ProducesResponseType(typeof(CurrentStorageDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] + [ApiCosts(0)] + public async Task<IActionResult> GetTeamCurrentStorageSizeForTeam(string team) + { + var size = await assetStatsRepository.GetTotalSizeByTeamAsync(TeamId, HttpContext.RequestAborted); - // Use the current team plan to show the limits to the user. - var (plan, _) = await usageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted); + // Use the current team plan to show the limits to the user. + var (plan, _) = await usageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted); - var response = new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize }; + var response = new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize }; - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Get asset usage by date. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="fromDate">The from date.</param> - /// <param name="toDate">The to date.</param> - /// <returns> - /// 200 => Storage usage returned. - /// 400 => Range between from date and to date is not valid or has more than 100 days. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")] - [ProducesResponseType(typeof(StorageUsagePerDateDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] - [ApiCosts(0)] - public async Task<IActionResult> GetStorageSizes(string app, DateTime fromDate, DateTime toDate) + /// <summary> + /// Get asset usage by date. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="fromDate">The from date.</param> + /// <param name="toDate">The to date.</param> + /// <returns> + /// 200 => Storage usage returned. + /// 400 => Range between from date and to date is not valid or has more than 100 days. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")] + [ProducesResponseType(typeof(StorageUsagePerDateDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppUsage)] + [ApiCosts(0)] + public async Task<IActionResult> GetStorageSizes(string app, DateTime fromDate, DateTime toDate) + { + if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) { - if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) - { - return BadRequest(); - } + return BadRequest(); + } - var usages = await assetStatsRepository.QueryByAppAsync(AppId, fromDate.Date, toDate.Date, HttpContext.RequestAborted); + var usages = await assetStatsRepository.QueryByAppAsync(AppId, fromDate.Date, toDate.Date, HttpContext.RequestAborted); - var models = usages.Select(StorageUsagePerDateDto.FromDomain).ToArray(); + var models = usages.Select(StorageUsagePerDateDto.FromDomain).ToArray(); - return Ok(models); - } + return Ok(models); + } - /// <summary> - /// Get asset usage by date for team. - /// </summary> - /// <param name="team">The ID of the team.</param> - /// <param name="fromDate">The from date.</param> - /// <param name="toDate">The to date.</param> - /// <returns> - /// 200 => Storage usage returned. - /// 400 => Range between from date and to date is not valid or has more than 100 days. - /// 404 => Team not found. - /// </returns> - [HttpGet] - [Route("teams/{team}/usages/storage/{fromDate}/{toDate}/")] - [ProducesResponseType(typeof(StorageUsagePerDateDto[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] - [ApiCosts(0)] - public async Task<IActionResult> GetStorageSizesForTeam(string team, DateTime fromDate, DateTime toDate) + /// <summary> + /// Get asset usage by date for team. + /// </summary> + /// <param name="team">The ID of the team.</param> + /// <param name="fromDate">The from date.</param> + /// <param name="toDate">The to date.</param> + /// <returns> + /// 200 => Storage usage returned. + /// 400 => Range between from date and to date is not valid or has more than 100 days. + /// 404 => Team not found. + /// </returns> + [HttpGet] + [Route("teams/{team}/usages/storage/{fromDate}/{toDate}/")] + [ProducesResponseType(typeof(StorageUsagePerDateDto[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamUsage)] + [ApiCosts(0)] + public async Task<IActionResult> GetStorageSizesForTeam(string team, DateTime fromDate, DateTime toDate) + { + if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) { - if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) - { - return BadRequest(); - } + return BadRequest(); + } - var usages = await assetStatsRepository.QueryByTeamAsync(TeamId, fromDate.Date, toDate.Date, HttpContext.RequestAborted); + var usages = await assetStatsRepository.QueryByTeamAsync(TeamId, fromDate.Date, toDate.Date, HttpContext.RequestAborted); - var models = usages.Select(StorageUsagePerDateDto.FromDomain).ToArray(); + var models = usages.Select(StorageUsagePerDateDto.FromDomain).ToArray(); - return Ok(models); - } + return Ok(models); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/CreateTeamDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/CreateTeamDto.cs index ea11fa2b03..fbe47314e6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/CreateTeamDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/CreateTeamDto.cs @@ -9,19 +9,18 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Teams.Models +namespace Squidex.Areas.Api.Controllers.Teams.Models; + +public sealed class CreateTeamDto { - public sealed class CreateTeamDto - { - /// <summary> - /// The name of the team. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The name of the team. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - public CreateTeam ToCommand() - { - return SimpleMapper.Map(this, new CreateTeam()); - } + public CreateTeam ToCommand() + { + return SimpleMapper.Map(this, new CreateTeam()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/TeamDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/TeamDto.cs index 54e51d9c24..f1b20f5037 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/TeamDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/TeamDto.cs @@ -16,84 +16,83 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Teams.Models +namespace Squidex.Areas.Api.Controllers.Teams.Models; + +public sealed class TeamDto : Resource { - public sealed class TeamDto : Resource + /// <summary> + /// The ID of the team. + /// </summary> + public DomainId Id { get; set; } + + /// <summary> + /// The name of the team. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } + + /// <summary> + /// The version of the team. + /// </summary> + public long Version { get; set; } + + /// <summary> + /// The timestamp when the team has been created. + /// </summary> + public Instant Created { get; set; } + + /// <summary> + /// The timestamp when the team has been modified last. + /// </summary> + public Instant LastModified { get; set; } + + /// <summary> + /// The role name of the user. + /// </summary> + public string? RoleName { get; set; } + + public static TeamDto FromDomain(ITeamEntity team, string userId, Resources resources) { - /// <summary> - /// The ID of the team. - /// </summary> - public DomainId Id { get; set; } - - /// <summary> - /// The name of the team. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } - - /// <summary> - /// The version of the team. - /// </summary> - public long Version { get; set; } - - /// <summary> - /// The timestamp when the team has been created. - /// </summary> - public Instant Created { get; set; } - - /// <summary> - /// The timestamp when the team has been modified last. - /// </summary> - public Instant LastModified { get; set; } - - /// <summary> - /// The role name of the user. - /// </summary> - public string? RoleName { get; set; } - - public static TeamDto FromDomain(ITeamEntity team, string userId, Resources resources) + var result = SimpleMapper.Map(team, new TeamDto()); + + var permissions = PermissionSet.Empty; + + if (team.TryGetContributorRole(userId, out _)) { - var result = SimpleMapper.Map(team, new TeamDto()); + permissions = new PermissionSet(PermissionIds.ForApp(PermissionIds.TeamAdmin, team: team.Id.ToString())); + } + + return result.CreateLinks(team, resources, permissions, true); + } + + private TeamDto CreateLinks(ITeamEntity team, Resources resources, PermissionSet permissions, bool isContributor) + { + var values = new { team = Id.ToString() }; - var permissions = PermissionSet.Empty; + if (isContributor) + { + AddDeleteLink("leave", + resources.Url<TeamContributorsController>(x => nameof(x.DeleteMyself), values)); + } - if (team.TryGetContributorRole(userId, out _)) - { - permissions = new PermissionSet(PermissionIds.ForApp(PermissionIds.TeamAdmin, team: team.Id.ToString())); - } + if (resources.IsAllowed(PermissionIds.TeamUpdate, team: values.team, additional: permissions)) + { + AddPutLink("update", + resources.Url<TeamsController>(x => nameof(x.PutTeam), values)); + } - return result.CreateLinks(team, resources, permissions, true); + if (resources.IsAllowed(PermissionIds.TeamContributorsRead, team: values.team, additional: permissions)) + { + AddGetLink("contributors", + resources.Url<TeamContributorsController>(x => nameof(x.GetContributors), values)); } - private TeamDto CreateLinks(ITeamEntity team, Resources resources, PermissionSet permissions, bool isContributor) + if (resources.IsAllowed(PermissionIds.TeamPlansRead, team: values.team, additional: permissions)) { - var values = new { team = Id.ToString() }; - - if (isContributor) - { - AddDeleteLink("leave", - resources.Url<TeamContributorsController>(x => nameof(x.DeleteMyself), values)); - } - - if (resources.IsAllowed(PermissionIds.TeamUpdate, team: values.team, additional: permissions)) - { - AddPutLink("update", - resources.Url<TeamsController>(x => nameof(x.PutTeam), values)); - } - - if (resources.IsAllowed(PermissionIds.TeamContributorsRead, team: values.team, additional: permissions)) - { - AddGetLink("contributors", - resources.Url<TeamContributorsController>(x => nameof(x.GetContributors), values)); - } - - if (resources.IsAllowed(PermissionIds.TeamPlansRead, team: values.team, additional: permissions)) - { - AddGetLink("plans", - resources.Url<TeamPlansController>(x => nameof(x.GetTeamPlans), values)); - } - - return this; + AddGetLink("plans", + resources.Url<TeamPlansController>(x => nameof(x.GetTeamPlans), values)); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/UpdateTeamDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/UpdateTeamDto.cs index 2c70e2409a..613744178d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/UpdateTeamDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/Models/UpdateTeamDto.cs @@ -9,19 +9,18 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Teams.Models +namespace Squidex.Areas.Api.Controllers.Teams.Models; + +public sealed class UpdateTeamDto { - public sealed class UpdateTeamDto - { - /// <summary> - /// The name of the team. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } + /// <summary> + /// The name of the team. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } - public UpdateTeam ToCommand() - { - return SimpleMapper.Map(this, new UpdateTeam()); - } + public UpdateTeam ToCommand() + { + return SimpleMapper.Map(this, new UpdateTeam()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs index 0f064dd469..b9b351f443 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs @@ -16,133 +16,132 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Teams +namespace Squidex.Areas.Api.Controllers.Teams; + +/// <summary> +/// Update and query teams. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Teams))] +public sealed class TeamContributorsController : ApiController { + private readonly IUserResolver userResolver; + + public TeamContributorsController(ICommandBus commandBus, IUserResolver userResolver) + : base(commandBus) + { + this.userResolver = userResolver; + } + /// <summary> - /// Update and query teams. + /// Get team contributors. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Teams))] - public sealed class TeamContributorsController : ApiController + /// <param name="team">The ID of the team.</param> + /// <returns> + /// 200 => Contributors returned. + /// 404 => Team not found. + /// </returns> + [HttpGet] + [Route("teams/{team}/contributors/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsRead)] + [ApiCosts(0)] + public IActionResult GetContributors(string team) { - private readonly IUserResolver userResolver; - - public TeamContributorsController(ICommandBus commandBus, IUserResolver userResolver) - : base(commandBus) + var response = Deferred.AsyncResponse(() => { - this.userResolver = userResolver; - } + return GetResponseAsync(Team, false); + }); - /// <summary> - /// Get team contributors. - /// </summary> - /// <param name="team">The ID of the team.</param> - /// <returns> - /// 200 => Contributors returned. - /// 404 => Team not found. - /// </returns> - [HttpGet] - [Route("teams/{team}/contributors/")] - [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsRead)] - [ApiCosts(0)] - public IActionResult GetContributors(string team) - { - var response = Deferred.AsyncResponse(() => - { - return GetResponseAsync(Team, false); - }); + Response.Headers[HeaderNames.ETag] = Team.ToEtag(); - Response.Headers[HeaderNames.ETag] = Team.ToEtag(); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Assign contributor to team. + /// </summary> + /// <param name="team">The ID of the team.</param> + /// <param name="request">Contributor object that needs to be added to the team.</param> + /// <returns> + /// 201 => Contributor assigned to team. + /// 400 => Contributor request not valid. + /// 404 => Team not found. + /// </returns> + [HttpPost] + [Route("teams/{team}/contributors/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)] + [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsAssign)] + [ApiCosts(1)] + public async Task<IActionResult> PostContributor(string team, [FromBody] AssignContributorDto request) + { + var command = SimpleMapper.Map(request, new AssignContributor()); - /// <summary> - /// Assign contributor to team. - /// </summary> - /// <param name="team">The ID of the team.</param> - /// <param name="request">Contributor object that needs to be added to the team.</param> - /// <returns> - /// 201 => Contributor assigned to team. - /// 400 => Contributor request not valid. - /// 404 => Team not found. - /// </returns> - [HttpPost] - [Route("teams/{team}/contributors/")] - [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)] - [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsAssign)] - [ApiCosts(1)] - public async Task<IActionResult> PostContributor(string team, [FromBody] AssignContributorDto request) - { - var command = SimpleMapper.Map(request, new AssignContributor()); + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return CreatedAtAction(nameof(GetContributors), new { team }, response); + } - return CreatedAtAction(nameof(GetContributors), new { team }, response); - } + /// <summary> + /// Remove yourself. + /// </summary> + /// <param name="team">The ID of the team.</param> + /// <returns> + /// 200 => Contributor removed. + /// 404 => Contributor or team not found. + /// </returns> + [HttpDelete] + [Route("teams/{team}/contributors/me/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(1)] + public async Task<IActionResult> DeleteMyself(string team) + { + var command = new RemoveContributor { ContributorId = UserId }; - /// <summary> - /// Remove yourself. - /// </summary> - /// <param name="team">The ID of the team.</param> - /// <returns> - /// 200 => Contributor removed. - /// 404 => Contributor or team not found. - /// </returns> - [HttpDelete] - [Route("teams/{team}/contributors/me/")] - [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(1)] - public async Task<IActionResult> DeleteMyself(string team) - { - var command = new RemoveContributor { ContributorId = UserId }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Remove contributor. + /// </summary> + /// <param name="team">The ID of the team.</param> + /// <param name="id">The ID of the contributor.</param> + /// <returns> + /// 200 => Contributor removed. + /// 404 => Contributor or team not found. + /// </returns> + [HttpDelete] + [Route("teams/{team}/contributors/{id}/")] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsRevoke)] + [ApiCosts(1)] + public async Task<IActionResult> DeleteContributor(string team, string id) + { + var command = new RemoveContributor { ContributorId = id }; - /// <summary> - /// Remove contributor. - /// </summary> - /// <param name="team">The ID of the team.</param> - /// <param name="id">The ID of the contributor.</param> - /// <returns> - /// 200 => Contributor removed. - /// 404 => Contributor or team not found. - /// </returns> - [HttpDelete] - [Route("teams/{team}/contributors/{id}/")] - [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamContributorsRevoke)] - [ApiCosts(1)] - public async Task<IActionResult> DeleteContributor(string team, string id) - { - var command = new RemoveContributor { ContributorId = id }; + var response = await InvokeCommandAsync(command); - var response = await InvokeCommandAsync(command); + return Ok(response); + } - return Ok(response); - } + private async Task<ContributorsDto> InvokeCommandAsync(ICommand command) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - private async Task<ContributorsDto> InvokeCommandAsync(ICommand command) + if (context.PlainResult is InvitedResult<ITeamEntity> invited) { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - if (context.PlainResult is InvitedResult<ITeamEntity> invited) - { - return await GetResponseAsync(invited.Entity, true); - } - else - { - return await GetResponseAsync(context.Result<ITeamEntity>(), false); - } + return await GetResponseAsync(invited.Entity, true); } - - private async Task<ContributorsDto> GetResponseAsync(ITeamEntity team, bool invited) + else { - return await ContributorsDto.FromDomainAsync(team, Resources, userResolver, invited); + return await GetResponseAsync(context.Result<ITeamEntity>(), false); } } + + private async Task<ContributorsDto> GetResponseAsync(ITeamEntity team, bool invited) + { + return await ContributorsDto.FromDomainAsync(team, Resources, userResolver, invited); + } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs index ffbeeca08a..ce32f1ed8c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs @@ -14,139 +14,138 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Teams +namespace Squidex.Areas.Api.Controllers.Teams; + +/// <summary> +/// Update and query teams. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Teams))] +public sealed class TeamsController : ApiController { + private readonly IAppProvider appProvider; + + public TeamsController(ICommandBus commandBus, IAppProvider appProvider) + : base(commandBus) + { + this.appProvider = appProvider; + } + /// <summary> - /// Update and query teams. + /// Get your teams. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Teams))] - public sealed class TeamsController : ApiController + /// <returns> + /// 200 => Teams returned. + /// </returns> + /// <remarks> + /// You can only retrieve the list of teams when you are authenticated as a user (OpenID implicit flow). + /// You will retrieve all teams, where you are assigned as a contributor. + /// </remarks> + [HttpGet] + [Route("teams/")] + [ProducesResponseType(typeof(TeamDto[]), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public async Task<IActionResult> GetTeams() { - private readonly IAppProvider appProvider; + var teams = await appProvider.GetUserTeamsAsync(UserOrClientId, HttpContext.RequestAborted); - public TeamsController(ICommandBus commandBus, IAppProvider appProvider) - : base(commandBus) - { - this.appProvider = appProvider; - } - - /// <summary> - /// Get your teams. - /// </summary> - /// <returns> - /// 200 => Teams returned. - /// </returns> - /// <remarks> - /// You can only retrieve the list of teams when you are authenticated as a user (OpenID implicit flow). - /// You will retrieve all teams, where you are assigned as a contributor. - /// </remarks> - [HttpGet] - [Route("teams/")] - [ProducesResponseType(typeof(TeamDto[]), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(0)] - public async Task<IActionResult> GetTeams() - { - var teams = await appProvider.GetUserTeamsAsync(UserOrClientId, HttpContext.RequestAborted); - - var response = Deferred.Response(() => - { - return teams.OrderBy(x => x.Name).Select(a => TeamDto.FromDomain(a, UserOrClientId, Resources)).ToArray(); - }); - - Response.Headers[HeaderNames.ETag] = teams.ToEtag(); - - return Ok(response); - } - - /// <summary> - /// Get an team by name. - /// </summary> - /// <param name="team">The name of the team.</param> - /// <returns> - /// 200 => Teams returned. - /// 404 => Team not found. - /// </returns> - [HttpGet] - [Route("teams/{team}")] - [ProducesResponseType(typeof(TeamDto), StatusCodes.Status200OK)] - [ApiPermission] - [ApiCosts(0)] - public IActionResult GetTeam(string team) - { - var response = Deferred.Response(() => - { - return TeamDto.FromDomain(Team, UserOrClientId, Resources); - }); - - Response.Headers[HeaderNames.ETag] = Team.ToEtag(); - - return Ok(response); - } - - /// <summary> - /// Create a new team. - /// </summary> - /// <param name="request">The team object that needs to be added to Squidex.</param> - /// <returns> - /// 201 => Team created. - /// 400 => Team request not valid. - /// 409 => Team name is already in use. - /// </returns> - /// <remarks> - /// You can only create an team when you are authenticated as a user (OpenID implicit flow). - /// You will be assigned as owner of the new team automatically. - /// </remarks> - [HttpPost] - [Route("teams/")] - [ProducesResponseType(typeof(TeamDto), 201)] - [ApiPermission] - [ApiCosts(0)] - public async Task<IActionResult> PostTeam([FromBody] CreateTeamDto request) - { - var response = await InvokeCommandAsync(request.ToCommand()); - - return CreatedAtAction(nameof(GetTeams), response); - } - - /// <summary> - /// Update the team. - /// </summary> - /// <param name="team">The name of the team to update.</param> - /// <param name="request">The values to update.</param> - /// <returns> - /// 200 => Team updated. - /// 400 => Team request not valid. - /// 404 => Team not found. - /// </returns> - [HttpPut] - [Route("teams/{team}/")] - [ProducesResponseType(typeof(TeamDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.TeamUpdate)] - [ApiCosts(0)] - public async Task<IActionResult> PutTeam(string team, [FromBody] UpdateTeamDto request) + var response = Deferred.Response(() => { - var response = await InvokeCommandAsync(request.ToCommand()); + return teams.OrderBy(x => x.Name).Select(a => TeamDto.FromDomain(a, UserOrClientId, Resources)).ToArray(); + }); - return Ok(response); - } + Response.Headers[HeaderNames.ETag] = teams.ToEtag(); - private Task<TeamDto> InvokeCommandAsync(ICommand command) + return Ok(response); + } + + /// <summary> + /// Get an team by name. + /// </summary> + /// <param name="team">The name of the team.</param> + /// <returns> + /// 200 => Teams returned. + /// 404 => Team not found. + /// </returns> + [HttpGet] + [Route("teams/{team}")] + [ProducesResponseType(typeof(TeamDto), StatusCodes.Status200OK)] + [ApiPermission] + [ApiCosts(0)] + public IActionResult GetTeam(string team) + { + var response = Deferred.Response(() => { - return InvokeCommandAsync(command, x => - { - return TeamDto.FromDomain(x, UserOrClientId, Resources); - }); - } + return TeamDto.FromDomain(Team, UserOrClientId, Resources); + }); + + Response.Headers[HeaderNames.ETag] = Team.ToEtag(); + + return Ok(response); + } - private async Task<T> InvokeCommandAsync<T>(ICommand command, Func<ITeamEntity, T> converter) + /// <summary> + /// Create a new team. + /// </summary> + /// <param name="request">The team object that needs to be added to Squidex.</param> + /// <returns> + /// 201 => Team created. + /// 400 => Team request not valid. + /// 409 => Team name is already in use. + /// </returns> + /// <remarks> + /// You can only create an team when you are authenticated as a user (OpenID implicit flow). + /// You will be assigned as owner of the new team automatically. + /// </remarks> + [HttpPost] + [Route("teams/")] + [ProducesResponseType(typeof(TeamDto), 201)] + [ApiPermission] + [ApiCosts(0)] + public async Task<IActionResult> PostTeam([FromBody] CreateTeamDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return CreatedAtAction(nameof(GetTeams), response); + } + + /// <summary> + /// Update the team. + /// </summary> + /// <param name="team">The name of the team to update.</param> + /// <param name="request">The values to update.</param> + /// <returns> + /// 200 => Team updated. + /// 400 => Team request not valid. + /// 404 => Team not found. + /// </returns> + [HttpPut] + [Route("teams/{team}/")] + [ProducesResponseType(typeof(TeamDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.TeamUpdate)] + [ApiCosts(0)] + public async Task<IActionResult> PutTeam(string team, [FromBody] UpdateTeamDto request) + { + var response = await InvokeCommandAsync(request.ToCommand()); + + return Ok(response); + } + + private Task<TeamDto> InvokeCommandAsync(ICommand command) + { + return InvokeCommandAsync(command, x => { - var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); + return TeamDto.FromDomain(x, UserOrClientId, Resources); + }); + } + + private async Task<T> InvokeCommandAsync<T>(ICommand command, Func<ITeamEntity, T> converter) + { + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - var result = context.Result<ITeamEntity>(); - var response = converter(result); + var result = context.Result<ITeamEntity>(); + var response = converter(result); - return response; - } + return response; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplateDetailsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplateDetailsDto.cs index 0c7215b859..1dc1ca10a0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplateDetailsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplateDetailsDto.cs @@ -8,33 +8,32 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Templates.Models +namespace Squidex.Areas.Api.Controllers.Templates.Models; + +public class TemplateDetailsDto : Resource { - public class TemplateDetailsDto : Resource - { - /// <summary> - /// The details of the template. - /// </summary> - [LocalizedRequired] - public string Details { get; set; } + /// <summary> + /// The details of the template. + /// </summary> + [LocalizedRequired] + public string Details { get; set; } - public static TemplateDetailsDto FromDomain(string name, string details, Resources resources) + public static TemplateDetailsDto FromDomain(string name, string details, Resources resources) + { + var result = new TemplateDetailsDto { - var result = new TemplateDetailsDto - { - Details = details - }; + Details = details + }; - return result.CreateLinks(name, resources); - } + return result.CreateLinks(name, resources); + } - private TemplateDetailsDto CreateLinks(string name, Resources resources) - { - var values = new { name }; + private TemplateDetailsDto CreateLinks(string name, Resources resources) + { + var values = new { name }; - AddSelfLink(resources.Url<TemplatesController>(c => nameof(c.GetTemplate), values)); + AddSelfLink(resources.Url<TemplatesController>(c => nameof(c.GetTemplate), values)); - return this; - } + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplateDto.cs index 28f226a73e..10657dfb36 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplateDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplateDto.cs @@ -10,47 +10,46 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Templates.Models +namespace Squidex.Areas.Api.Controllers.Templates.Models; + +public sealed class TemplateDto : Resource { - public sealed class TemplateDto : Resource + /// <summary> + /// The name of the template. + /// </summary> + [LocalizedRequired] + public string Name { get; set; } + + /// <summary> + /// The title of the template. + /// </summary> + [LocalizedRequired] + public string Title { get; set; } + + /// <summary> + /// The description of the template. + /// </summary> + [LocalizedRequired] + public string Description { get; set; } + + /// <summary> + /// True, if the template is a starter. + /// </summary> + public bool IsStarter { get; set; } + + public static TemplateDto FromDomain(Template template, Resources resources) { - /// <summary> - /// The name of the template. - /// </summary> - [LocalizedRequired] - public string Name { get; set; } - - /// <summary> - /// The title of the template. - /// </summary> - [LocalizedRequired] - public string Title { get; set; } - - /// <summary> - /// The description of the template. - /// </summary> - [LocalizedRequired] - public string Description { get; set; } - - /// <summary> - /// True, if the template is a starter. - /// </summary> - public bool IsStarter { get; set; } - - public static TemplateDto FromDomain(Template template, Resources resources) - { - var result = SimpleMapper.Map(template, new TemplateDto()); - - return result.CreateLinks(resources); - } - - private TemplateDto CreateLinks(Resources resources) - { - var values = new { name = Name }; - - AddSelfLink(resources.Url<TemplatesController>(c => nameof(c.GetTemplate), values)); - - return this; - } + var result = SimpleMapper.Map(template, new TemplateDto()); + + return result.CreateLinks(resources); + } + + private TemplateDto CreateLinks(Resources resources) + { + var values = new { name = Name }; + + AddSelfLink(resources.Url<TemplatesController>(c => nameof(c.GetTemplate), values)); + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplatesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplatesDto.cs index 5c12595632..dcba501b7f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplatesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Templates/Models/TemplatesDto.cs @@ -8,30 +8,29 @@ using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Templates.Models +namespace Squidex.Areas.Api.Controllers.Templates.Models; + +public sealed class TemplatesDto : Resource { - public sealed class TemplatesDto : Resource - { - /// <summary> - /// The event consumers. - /// </summary> - public TemplateDto[] Items { get; set; } + /// <summary> + /// The event consumers. + /// </summary> + public TemplateDto[] Items { get; set; } - public static TemplatesDto FromDomain(IEnumerable<Template> items, Resources resources) + public static TemplatesDto FromDomain(IEnumerable<Template> items, Resources resources) + { + var result = new TemplatesDto { - var result = new TemplatesDto - { - Items = items.Select(x => TemplateDto.FromDomain(x, resources)).ToArray() - }; + Items = items.Select(x => TemplateDto.FromDomain(x, resources)).ToArray() + }; - return result.CreateLinks(resources); - } + return result.CreateLinks(resources); + } - private TemplatesDto CreateLinks(Resources resources) - { - AddSelfLink(resources.Url<TemplatesController>(c => nameof(c.GetTemplates))); + private TemplatesDto CreateLinks(Resources resources) + { + AddSelfLink(resources.Url<TemplatesController>(c => nameof(c.GetTemplates))); - return this; - } + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Templates/TemplatesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Templates/TemplatesController.cs index 882a9fad71..cdea2882c2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Templates/TemplatesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Templates/TemplatesController.cs @@ -11,65 +11,64 @@ using Squidex.Infrastructure.Commands; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Templates +namespace Squidex.Areas.Api.Controllers.Templates; + +/// <summary> +/// Readonly API for news items. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Templates))] +public sealed class TemplatesController : ApiController { + private readonly TemplatesClient templatesClient; + + public TemplatesController(ICommandBus commandBus, TemplatesClient templatesClient) + : base(commandBus) + { + this.templatesClient = templatesClient; + } + /// <summary> - /// Readonly API for news items. + /// Get all templates. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Templates))] - public sealed class TemplatesController : ApiController + /// <returns> + /// 200 => Templates returned. + /// </returns> + [HttpGet] + [Route("templates/")] + [ProducesResponseType(typeof(TemplatesDto), StatusCodes.Status200OK)] + [ApiPermission] + public async Task<IActionResult> GetTemplates() { - private readonly TemplatesClient templatesClient; + var templates = await templatesClient.GetTemplatesAsync(HttpContext.RequestAborted); - public TemplatesController(ICommandBus commandBus, TemplatesClient templatesClient) - : base(commandBus) - { - this.templatesClient = templatesClient; - } - - /// <summary> - /// Get all templates. - /// </summary> - /// <returns> - /// 200 => Templates returned. - /// </returns> - [HttpGet] - [Route("templates/")] - [ProducesResponseType(typeof(TemplatesDto), StatusCodes.Status200OK)] - [ApiPermission] - public async Task<IActionResult> GetTemplates() - { - var templates = await templatesClient.GetTemplatesAsync(HttpContext.RequestAborted); + var response = TemplatesDto.FromDomain(templates, Resources); - var response = TemplatesDto.FromDomain(templates, Resources); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Get template details. + /// </summary> + /// <param name="name">The name of the template.</param> + /// <returns> + /// 200 => Template returned. + /// 404 => Template not found. + /// </returns> + [HttpGet] + [Route("templates/{name}")] + [ProducesResponseType(typeof(TemplateDetailsDto), StatusCodes.Status200OK)] + [ApiPermission] + public async Task<IActionResult> GetTemplate(string name) + { + var details = await templatesClient.GetDetailAsync(name, HttpContext.RequestAborted); - /// <summary> - /// Get template details. - /// </summary> - /// <param name="name">The name of the template.</param> - /// <returns> - /// 200 => Template returned. - /// 404 => Template not found. - /// </returns> - [HttpGet] - [Route("templates/{name}")] - [ProducesResponseType(typeof(TemplateDetailsDto), StatusCodes.Status200OK)] - [ApiPermission] - public async Task<IActionResult> GetTemplate(string name) + if (details == null) { - var details = await templatesClient.GetDetailAsync(name, HttpContext.RequestAborted); - - if (details == null) - { - return NotFound(); - } + return NotFound(); + } - var response = TemplateDetailsDto.FromDomain(name, details, Resources); + var response = TemplateDetailsDto.FromDomain(name, details, Resources); - return Ok(response); - } + return Ok(response); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs index d50575cac9..8a3fdd408a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs @@ -8,25 +8,24 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Translations.Models +namespace Squidex.Areas.Api.Controllers.Translations.Models; + +public sealed class TranslateDto { - public sealed class TranslateDto - { - /// <summary> - /// The text to translate. - /// </summary> - [LocalizedRequired] - public string Text { get; set; } + /// <summary> + /// The text to translate. + /// </summary> + [LocalizedRequired] + public string Text { get; set; } - /// <summary> - /// The target language. - /// </summary> - [LocalizedRequired] - public Language TargetLanguage { get; set; } + /// <summary> + /// The target language. + /// </summary> + [LocalizedRequired] + public Language TargetLanguage { get; set; } - /// <summary> - /// The optional source language. - /// </summary> - public Language SourceLanguage { get; set; } - } + /// <summary> + /// The optional source language. + /// </summary> + public Language SourceLanguage { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs index 4f8c7ee94c..3fc1e31ed6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs @@ -8,23 +8,22 @@ using Squidex.Infrastructure.Reflection; using Squidex.Text.Translations; -namespace Squidex.Areas.Api.Controllers.Translations.Models +namespace Squidex.Areas.Api.Controllers.Translations.Models; + +public sealed class TranslationDto { - public sealed class TranslationDto - { - /// <summary> - /// The result of the translation. - /// </summary> - public TranslationResultCode Result { get; set; } + /// <summary> + /// The result of the translation. + /// </summary> + public TranslationResultCode Result { get; set; } - /// <summary> - /// The translated text. - /// </summary> - public string? Text { get; set; } + /// <summary> + /// The translated text. + /// </summary> + public string? Text { get; set; } - public static TranslationDto FromDomain(TranslationResult translation) - { - return SimpleMapper.Map(translation, new TranslationDto { Result = translation.Code }); - } + public static TranslationDto FromDomain(TranslationResult translation) + { + return SimpleMapper.Map(translation, new TranslationDto { Result = translation.Code }); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index 54c851d9b7..60fbdfff08 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -12,41 +12,40 @@ using Squidex.Text.Translations; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Translations +namespace Squidex.Areas.Api.Controllers.Translations; + +/// <summary> +/// Manage translations. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Translations))] +public sealed class TranslationsController : ApiController { + private readonly ITranslator translator; + + public TranslationsController(ICommandBus commandBus, ITranslator translator) + : base(commandBus) + { + this.translator = translator; + } + /// <summary> - /// Manage translations. + /// Translate a text. /// </summary> - [ApiExplorerSettings(GroupName = nameof(Translations))] - public sealed class TranslationsController : ApiController + /// <param name="app">The name of the app.</param> + /// <param name="request">The translation request.</param> + /// <returns> + /// 200 => Text translated. + /// </returns> + [HttpPost] + [Route("apps/{app}/translations/")] + [ProducesResponseType(typeof(TranslationDto), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppTranslate)] + [ApiCosts(0)] + public async Task<IActionResult> PostTranslation(string app, [FromBody] TranslateDto request) { - private readonly ITranslator translator; - - public TranslationsController(ICommandBus commandBus, ITranslator translator) - : base(commandBus) - { - this.translator = translator; - } - - /// <summary> - /// Translate a text. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="request">The translation request.</param> - /// <returns> - /// 200 => Text translated. - /// </returns> - [HttpPost] - [Route("apps/{app}/translations/")] - [ProducesResponseType(typeof(TranslationDto), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous(PermissionIds.AppTranslate)] - [ApiCosts(0)] - public async Task<IActionResult> PostTranslation(string app, [FromBody] TranslateDto request) - { - var result = await translator.TranslateAsync(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted); - var response = TranslationDto.FromDomain(result); + var result = await translator.TranslateAsync(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted); + var response = TranslationDto.FromDomain(result); - return Ok(response); - } + return Ok(response); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs index 8ff567a103..3f77545eed 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs @@ -5,18 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Api.Controllers.UI.Models +namespace Squidex.Areas.Api.Controllers.UI.Models; + +public sealed class UISettingsDto { - public sealed class UISettingsDto - { - /// <summary> - /// True when the user can create apps. - /// </summary> - public bool CanCreateApps { get; set; } + /// <summary> + /// True when the user can create apps. + /// </summary> + public bool CanCreateApps { get; set; } - /// <summary> - /// True when the user can create teams. - /// </summary> - public bool CanCreateTeams { get; set; } - } + /// <summary> + /// True when the user can create teams. + /// </summary> + public bool CanCreateTeams { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs index 5932be9a6a..a765aa1cfd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/Models/UpdateSettingDto.cs @@ -7,13 +7,12 @@ using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Areas.Api.Controllers.UI.Models +namespace Squidex.Areas.Api.Controllers.UI.Models; + +public sealed class UpdateSettingDto { - public sealed class UpdateSettingDto - { - /// <summary> - /// The value for the setting. - /// </summary> - public JsonValue Value { get; set; } - } + /// <summary> + /// The value for the setting. + /// </summary> + public JsonValue Value { get; set; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs index dd60f36c46..e8975f054d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs @@ -7,62 +7,61 @@ using System.Text.Json.Serialization; -namespace Squidex.Areas.Api.Controllers.UI +namespace Squidex.Areas.Api.Controllers.UI; + +public sealed record MyUIOptions { - public sealed record MyUIOptions - { - [JsonExtensionData] - public Dictionary<string, object> More { get; set; } = new Dictionary<string, object>(); + [JsonExtensionData] + public Dictionary<string, object> More { get; set; } = new Dictionary<string, object>(); - [JsonPropertyName("regexSuggestions")] - public Dictionary<string, string> RegexSuggestions { get; set; } + [JsonPropertyName("regexSuggestions")] + public Dictionary<string, string> RegexSuggestions { get; set; } - [JsonPropertyName("map")] - public MapOptions Map { get; set; } + [JsonPropertyName("map")] + public MapOptions Map { get; set; } - [JsonPropertyName("referencesDropdownItemCount")] - public int ReferencesDropdownItemCount { get; set; } = 100; + [JsonPropertyName("referencesDropdownItemCount")] + public int ReferencesDropdownItemCount { get; set; } = 100; - [JsonPropertyName("showInfo")] - public bool ShowInfo { get; set; } + [JsonPropertyName("showInfo")] + public bool ShowInfo { get; set; } - [JsonPropertyName("hideNews")] - public bool HideNews { get; set; } + [JsonPropertyName("hideNews")] + public bool HideNews { get; set; } - [JsonPropertyName("hideOnboarding")] - public bool HideOnboarding { get; set; } + [JsonPropertyName("hideOnboarding")] + public bool HideOnboarding { get; set; } - [JsonPropertyName("hideDateButtons")] - public bool HideDateButtons { get; set; } + [JsonPropertyName("hideDateButtons")] + public bool HideDateButtons { get; set; } - [JsonPropertyName("hideDateTimeModeButton")] - public bool HideDateTimeModeButton { get; set; } + [JsonPropertyName("hideDateTimeModeButton")] + public bool HideDateTimeModeButton { get; set; } - [JsonPropertyName("disableScheduledChanges")] - public bool DisableScheduledChanges { get; set; } + [JsonPropertyName("disableScheduledChanges")] + public bool DisableScheduledChanges { get; set; } - [JsonPropertyName("redirectToLogin")] - public bool RedirectToLogin { get; set; } + [JsonPropertyName("redirectToLogin")] + public bool RedirectToLogin { get; set; } - [JsonPropertyName("onlyAdminsCanCreateApps")] - public bool OnlyAdminsCanCreateApps { get; set; } + [JsonPropertyName("onlyAdminsCanCreateApps")] + public bool OnlyAdminsCanCreateApps { get; set; } - [JsonPropertyName("onlyAdminsCanCreateTeams")] - public bool OnlyAdminsCanCreateTeams { get; set; } + [JsonPropertyName("onlyAdminsCanCreateTeams")] + public bool OnlyAdminsCanCreateTeams { get; set; } - public sealed class MapOptions - { - [JsonPropertyName("type")] - public string Type { get; set; } + public sealed class MapOptions + { + [JsonPropertyName("type")] + public string Type { get; set; } - [JsonPropertyName("googleMaps")] - public MapGoogleOptions GoogleMaps { get; set; } - } + [JsonPropertyName("googleMaps")] + public MapGoogleOptions GoogleMaps { get; set; } + } - public sealed class MapGoogleOptions - { - [JsonPropertyName("key")] - public string Key { get; set; } - } + public sealed class MapGoogleOptions + { + [JsonPropertyName("key")] + public string Key { get; set; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 6cce1f2558..997c565c44 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -14,158 +14,157 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.UI +namespace Squidex.Areas.Api.Controllers.UI; + +public sealed class UIController : ApiController { - public sealed class UIController : ApiController + private static readonly Permission CreateAppPermission = new Permission(PermissionIds.AdminAppCreate); + private static readonly Permission CreateTeamPermission = new Permission(PermissionIds.AdminTeamCreate); + private readonly MyUIOptions uiOptions; + private readonly IAppUISettings appUISettings; + + public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions, IAppUISettings appUISettings) + : base(commandBus) { - private static readonly Permission CreateAppPermission = new Permission(PermissionIds.AdminAppCreate); - private static readonly Permission CreateTeamPermission = new Permission(PermissionIds.AdminTeamCreate); - private readonly MyUIOptions uiOptions; - private readonly IAppUISettings appUISettings; + this.uiOptions = uiOptions.Value; - public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions, IAppUISettings appUISettings) - : base(commandBus) - { - this.uiOptions = uiOptions.Value; - - this.appUISettings = appUISettings; - } - - /// <summary> - /// Get ui settings. - /// </summary> - /// <returns> - /// 200 => UI settings returned. - /// </returns> - [HttpGet] - [Route("ui/settings/")] - [ProducesResponseType(typeof(UISettingsDto), StatusCodes.Status200OK)] - [ApiPermission] - public IActionResult GetSettings() - { - var result = new UISettingsDto - { - CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateAppPermission), - CanCreateTeams = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateTeamPermission), - }; - - return Ok(result); - } - - /// <summary> - /// Get ui settings. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => UI settings returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/ui/settings/")] - [ProducesResponseType(typeof(Dictionary<string, string>), StatusCodes.Status200OK)] - [ApiPermission] - public async Task<IActionResult> GetSettings(string app) - { - var result = await appUISettings.GetAsync(AppId, null, HttpContext.RequestAborted); - - return Ok(result); - } - - /// <summary> - /// Get my ui settings. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <returns> - /// 200 => UI settings returned. - /// 404 => App not found. - /// </returns> - [HttpGet] - [Route("apps/{app}/ui/settings/me")] - [ProducesResponseType(typeof(Dictionary<string, string>), StatusCodes.Status200OK)] - [ApiPermission] - public async Task<IActionResult> GetUserSettings(string app) - { - var result = await appUISettings.GetAsync(AppId, UserId, HttpContext.RequestAborted); - - return Ok(result); - } - - /// <summary> - /// Set ui settings. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="key">The name of the setting.</param> - /// <param name="request">The request with the value to update.</param> - /// <returns> - /// 200 => UI setting set. - /// 404 => App not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/ui/settings/{key}")] - [ApiPermission] - public async Task<IActionResult> PutSetting(string app, string key, [FromBody] UpdateSettingDto request) - { - await appUISettings.SetAsync(AppId, null, key, request.Value, HttpContext.RequestAborted); - - return NoContent(); - } - - /// <summary> - /// Set my ui settings. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="key">The name of the setting.</param> - /// <param name="request">The request with the value to update.</param> - /// <returns> - /// 200 => UI setting set. - /// 404 => App not found. - /// </returns> - [HttpPut] - [Route("apps/{app}/ui/settings/me/{key}")] - [ApiPermission] - public async Task<IActionResult> PutUserSetting(string app, string key, [FromBody] UpdateSettingDto request) - { - await appUISettings.SetAsync(AppId, UserId, key, request.Value, HttpContext.RequestAborted); - - return NoContent(); - } - - /// <summary> - /// Remove ui settings. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="key">The name of the setting.</param> - /// <returns> - /// 200 => UI setting removed. - /// 404 => App not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/ui/settings/{key}")] - [ApiPermission] - public async Task<IActionResult> DeleteSetting(string app, string key) - { - await appUISettings.RemoveAsync(AppId, null, key, HttpContext.RequestAborted); - - return NoContent(); - } - - /// <summary> - /// Remove my ui settings. - /// </summary> - /// <param name="app">The name of the app.</param> - /// <param name="key">The name of the setting.</param> - /// <returns> - /// 200 => UI setting removed. - /// 404 => App not found. - /// </returns> - [HttpDelete] - [Route("apps/{app}/ui/settings/me/{key}")] - [ApiPermission] - public async Task<IActionResult> DeleteUserSetting(string app, string key) + this.appUISettings = appUISettings; + } + + /// <summary> + /// Get ui settings. + /// </summary> + /// <returns> + /// 200 => UI settings returned. + /// </returns> + [HttpGet] + [Route("ui/settings/")] + [ProducesResponseType(typeof(UISettingsDto), StatusCodes.Status200OK)] + [ApiPermission] + public IActionResult GetSettings() + { + var result = new UISettingsDto { - await appUISettings.RemoveAsync(AppId, UserId, key, HttpContext.RequestAborted); + CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateAppPermission), + CanCreateTeams = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateTeamPermission), + }; + + return Ok(result); + } + + /// <summary> + /// Get ui settings. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => UI settings returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/ui/settings/")] + [ProducesResponseType(typeof(Dictionary<string, string>), StatusCodes.Status200OK)] + [ApiPermission] + public async Task<IActionResult> GetSettings(string app) + { + var result = await appUISettings.GetAsync(AppId, null, HttpContext.RequestAborted); + + return Ok(result); + } + + /// <summary> + /// Get my ui settings. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <returns> + /// 200 => UI settings returned. + /// 404 => App not found. + /// </returns> + [HttpGet] + [Route("apps/{app}/ui/settings/me")] + [ProducesResponseType(typeof(Dictionary<string, string>), StatusCodes.Status200OK)] + [ApiPermission] + public async Task<IActionResult> GetUserSettings(string app) + { + var result = await appUISettings.GetAsync(AppId, UserId, HttpContext.RequestAborted); + + return Ok(result); + } + + /// <summary> + /// Set ui settings. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="key">The name of the setting.</param> + /// <param name="request">The request with the value to update.</param> + /// <returns> + /// 200 => UI setting set. + /// 404 => App not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/ui/settings/{key}")] + [ApiPermission] + public async Task<IActionResult> PutSetting(string app, string key, [FromBody] UpdateSettingDto request) + { + await appUISettings.SetAsync(AppId, null, key, request.Value, HttpContext.RequestAborted); + + return NoContent(); + } + + /// <summary> + /// Set my ui settings. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="key">The name of the setting.</param> + /// <param name="request">The request with the value to update.</param> + /// <returns> + /// 200 => UI setting set. + /// 404 => App not found. + /// </returns> + [HttpPut] + [Route("apps/{app}/ui/settings/me/{key}")] + [ApiPermission] + public async Task<IActionResult> PutUserSetting(string app, string key, [FromBody] UpdateSettingDto request) + { + await appUISettings.SetAsync(AppId, UserId, key, request.Value, HttpContext.RequestAborted); + + return NoContent(); + } + + /// <summary> + /// Remove ui settings. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="key">The name of the setting.</param> + /// <returns> + /// 200 => UI setting removed. + /// 404 => App not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/ui/settings/{key}")] + [ApiPermission] + public async Task<IActionResult> DeleteSetting(string app, string key) + { + await appUISettings.RemoveAsync(AppId, null, key, HttpContext.RequestAborted); + + return NoContent(); + } + + /// <summary> + /// Remove my ui settings. + /// </summary> + /// <param name="app">The name of the app.</param> + /// <param name="key">The name of the setting.</param> + /// <returns> + /// 200 => UI setting removed. + /// 404 => App not found. + /// </returns> + [HttpDelete] + [Route("apps/{app}/ui/settings/me/{key}")] + [ApiPermission] + public async Task<IActionResult> DeleteUserSetting(string app, string key) + { + await appUISettings.RemoveAsync(AppId, UserId, key, HttpContext.RequestAborted); - return NoContent(); - } + return NoContent(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs index d1e3d5a646..321c48f65f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs @@ -9,44 +9,43 @@ using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models; + +public sealed class CreateUserDto { - public sealed class CreateUserDto - { - /// <summary> - /// The email of the user. Unique value. - /// </summary> - [LocalizedRequired] - [LocalizedEmailAddress] - public string Email { get; set; } + /// <summary> + /// The email of the user. Unique value. + /// </summary> + [LocalizedRequired] + [LocalizedEmailAddress] + public string Email { get; set; } - /// <summary> - /// The display name (usually first name and last name) of the user. - /// </summary> - [LocalizedRequired] - public string DisplayName { get; set; } + /// <summary> + /// The display name (usually first name and last name) of the user. + /// </summary> + [LocalizedRequired] + public string DisplayName { get; set; } - /// <summary> - /// The password of the user. - /// </summary> - [LocalizedRequired] - public string Password { get; set; } + /// <summary> + /// The password of the user. + /// </summary> + [LocalizedRequired] + public string Password { get; set; } - /// <summary> - /// Additional permissions for the user. - /// </summary> - [LocalizedRequired] - public string[] Permissions { get; set; } + /// <summary> + /// Additional permissions for the user. + /// </summary> + [LocalizedRequired] + public string[] Permissions { get; set; } - public UserValues ToValues() + public UserValues ToValues() + { + return new UserValues { - return new UserValues - { - Email = Email, - DisplayName = DisplayName, - Password = Password, - Permissions = new PermissionSet(Permissions) - }; - } + Email = Email, + DisplayName = DisplayName, + Password = Password, + Permissions = new PermissionSet(Permissions) + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs index 6a9e2ccfd9..9176892d5e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs @@ -11,39 +11,38 @@ using Squidex.Areas.Api.Controllers.Ping; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models; + +public sealed class ResourcesDto : Resource { - public sealed class ResourcesDto : Resource + public static ResourcesDto FromDomain(Resources resources) { - public static ResourcesDto FromDomain(Resources resources) - { - var result = new ResourcesDto(); + var result = new ResourcesDto(); - result.AddGetLink("ping", - resources.Url<PingController>(x => nameof(x.GetPing))); + result.AddGetLink("ping", + resources.Url<PingController>(x => nameof(x.GetPing))); - if (resources.CanReadEvents) - { - result.AddGetLink("admin/events", - resources.Url<EventConsumersController>(x => nameof(x.GetEventConsumers))); - } + if (resources.CanReadEvents) + { + result.AddGetLink("admin/events", + resources.Url<EventConsumersController>(x => nameof(x.GetEventConsumers))); + } - if (resources.CanRestoreBackup) - { - result.AddGetLink("admin/restore", - resources.Url<RestoreController>(x => nameof(x.GetRestoreJob))); - } + if (resources.CanRestoreBackup) + { + result.AddGetLink("admin/restore", + resources.Url<RestoreController>(x => nameof(x.GetRestoreJob))); + } - if (resources.CanReadUsers) - { - result.AddGetLink("admin/users", - resources.Url<UserManagementController>(x => nameof(x.GetUsers))); - } + if (resources.CanReadUsers) + { + result.AddGetLink("admin/users", + resources.Url<UserManagementController>(x => nameof(x.GetUsers))); + } - result.AddGetLink("languages", - resources.Url<LanguagesController>(x => nameof(x.GetLanguages))); + result.AddGetLink("languages", + resources.Url<LanguagesController>(x => nameof(x.GetLanguages))); - return result; - } + return result; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs index 73890b1d32..0a87004ef7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs @@ -9,43 +9,42 @@ using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.Api.Controllers.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models; + +public sealed class UpdateUserDto { - public sealed class UpdateUserDto - { - /// <summary> - /// The email of the user. Unique value. - /// </summary> - [LocalizedRequired] - [LocalizedEmailAddress] - public string Email { get; set; } + /// <summary> + /// The email of the user. Unique value. + /// </summary> + [LocalizedRequired] + [LocalizedEmailAddress] + public string Email { get; set; } - /// <summary> - /// The display name (usually first name and last name) of the user. - /// </summary> - [LocalizedRequired] - public string DisplayName { get; set; } + /// <summary> + /// The display name (usually first name and last name) of the user. + /// </summary> + [LocalizedRequired] + public string DisplayName { get; set; } - /// <summary> - /// The password of the user. - /// </summary> - public string? Password { get; set; } + /// <summary> + /// The password of the user. + /// </summary> + public string? Password { get; set; } - /// <summary> - /// Additional permissions for the user. - /// </summary> - [LocalizedRequired] - public string[] Permissions { get; set; } + /// <summary> + /// Additional permissions for the user. + /// </summary> + [LocalizedRequired] + public string[] Permissions { get; set; } - public UserValues ToValues() + public UserValues ToValues() + { + return new UserValues { - return new UserValues - { - Email = Email, - DisplayName = DisplayName, - Password = Password, - Permissions = new PermissionSet(Permissions) - }; - } + Email = Email, + DisplayName = DisplayName, + Password = Password, + Permissions = new PermissionSet(Permissions) + }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs index 34f6020253..f40b539a89 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -11,91 +11,90 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models; + +public sealed class UserDto : Resource { - public sealed class UserDto : Resource + /// <summary> + /// The ID of the user. + /// </summary> + [LocalizedRequired] + public string Id { get; set; } + + /// <summary> + /// The email of the user. Unique value. + /// </summary> + [LocalizedRequired] + public string Email { get; set; } + + /// <summary> + /// The display name (usually first name and last name) of the user. + /// </summary> + [LocalizedRequired] + public string DisplayName { get; set; } + + /// <summary> + /// Determines if the user is locked. + /// </summary> + [LocalizedRequired] + public bool IsLocked { get; set; } + + /// <summary> + /// Additional permissions for the user. + /// </summary> + [LocalizedRequired] + public IEnumerable<string> Permissions { get; set; } + + public static UserDto FromDomain(IUser user, Resources resources) { - /// <summary> - /// The ID of the user. - /// </summary> - [LocalizedRequired] - public string Id { get; set; } - - /// <summary> - /// The email of the user. Unique value. - /// </summary> - [LocalizedRequired] - public string Email { get; set; } - - /// <summary> - /// The display name (usually first name and last name) of the user. - /// </summary> - [LocalizedRequired] - public string DisplayName { get; set; } - - /// <summary> - /// Determines if the user is locked. - /// </summary> - [LocalizedRequired] - public bool IsLocked { get; set; } - - /// <summary> - /// Additional permissions for the user. - /// </summary> - [LocalizedRequired] - public IEnumerable<string> Permissions { get; set; } - - public static UserDto FromDomain(IUser user, Resources resources) - { - var result = SimpleMapper.Map(user, new UserDto()); + var result = SimpleMapper.Map(user, new UserDto()); - result.DisplayName = user.Claims.DisplayName()!; - result.Permissions = user.Claims.Permissions().ToIds(); + result.DisplayName = user.Claims.DisplayName()!; + result.Permissions = user.Claims.Permissions().ToIds(); - return result.CreateLinks(resources); - } + return result.CreateLinks(resources); + } - private UserDto CreateLinks(Resources resources) - { - var values = new { id = Id }; + private UserDto CreateLinks(Resources resources) + { + var values = new { id = Id }; - if (resources.Controller is UserManagementController) - { - AddSelfLink(resources.Url<UserManagementController>(c => nameof(c.GetUser), values)); - } - else - { - AddSelfLink(resources.Url<UsersController>(c => nameof(c.GetUser), values)); - } + if (resources.Controller is UserManagementController) + { + AddSelfLink(resources.Url<UserManagementController>(c => nameof(c.GetUser), values)); + } + else + { + AddSelfLink(resources.Url<UsersController>(c => nameof(c.GetUser), values)); + } - if (!resources.Controller.IsUser(Id)) + if (!resources.Controller.IsUser(Id)) + { + if (resources.CanLockUser && !IsLocked) { - if (resources.CanLockUser && !IsLocked) - { - AddPutLink("lock", - resources.Url<UserManagementController>(c => nameof(c.LockUser), values)); - } - - if (resources.CanUnlockUser && IsLocked) - { - AddPutLink("unlock", - resources.Url<UserManagementController>(c => nameof(c.UnlockUser), values)); - } - - AddDeleteLink("delete", - resources.Url<UserManagementController>(x => nameof(x.DeleteUser), values)); + AddPutLink("lock", + resources.Url<UserManagementController>(c => nameof(c.LockUser), values)); } - if (resources.CanUpdateUser) + if (resources.CanUnlockUser && IsLocked) { - AddPutLink("update", - resources.Url<UserManagementController>(c => nameof(c.PutUser), values)); + AddPutLink("unlock", + resources.Url<UserManagementController>(c => nameof(c.UnlockUser), values)); } - AddGetLink("picture", - resources.Url<UsersController>(c => nameof(c.GetUserPicture), values)); + AddDeleteLink("delete", + resources.Url<UserManagementController>(x => nameof(x.DeleteUser), values)); + } - return this; + if (resources.CanUpdateUser) + { + AddPutLink("update", + resources.Url<UserManagementController>(c => nameof(c.PutUser), values)); } + + AddGetLink("picture", + resources.Url<UsersController>(c => nameof(c.GetUserPicture), values)); + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs index 1c345287dc..e7e6b5ffe6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs @@ -9,43 +9,42 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models; + +public sealed class UsersDto : Resource { - public sealed class UsersDto : Resource + /// <summary> + /// The total number of users. + /// </summary> + public long Total { get; set; } + + /// <summary> + /// The users. + /// </summary> + [LocalizedRequired] + public UserDto[] Items { get; set; } + + public static UsersDto FromDomain(IEnumerable<IUser> items, long total, Resources resources) { - /// <summary> - /// The total number of users. - /// </summary> - public long Total { get; set; } - - /// <summary> - /// The users. - /// </summary> - [LocalizedRequired] - public UserDto[] Items { get; set; } - - public static UsersDto FromDomain(IEnumerable<IUser> items, long total, Resources resources) + var result = new UsersDto { - var result = new UsersDto - { - Total = total, - Items = items.Select(x => UserDto.FromDomain(x, resources)).ToArray() - }; - - return result.CreateLinks(resources); - } + Total = total, + Items = items.Select(x => UserDto.FromDomain(x, resources)).ToArray() + }; - private UsersDto CreateLinks(Resources context) - { - AddSelfLink(context.Url<UserManagementController>(c => nameof(c.GetUsers))); + return result.CreateLinks(resources); + } - if (context.CanCreateUser) - { - AddPostLink("create", - context.Url<UserManagementController>(c => nameof(c.PostUser))); - } + private UsersDto CreateLinks(Resources context) + { + AddSelfLink(context.Url<UserManagementController>(c => nameof(c.GetUsers))); - return this; + if (context.CanCreateUser) + { + AddPostLink("create", + context.Url<UserManagementController>(c => nameof(c.PostUser))); } + + return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index dff46d148f..aac01013bd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -14,192 +14,191 @@ using Squidex.Shared; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Users +namespace Squidex.Areas.Api.Controllers.Users; + +/// <summary> +/// Retrieve and manage users. +/// </summary> +[ApiModelValidation(true)] +[ApiExplorerSettings(GroupName = "UserManagement")] +public sealed class UserManagementController : ApiController { + private readonly IUserService userService; + + public UserManagementController(ICommandBus commandBus, IUserService userService) + : base(commandBus) + { + this.userService = userService; + } + /// <summary> - /// Retrieve and manage users. + /// Get users by query. /// </summary> - [ApiModelValidation(true)] - [ApiExplorerSettings(GroupName = "UserManagement")] - public sealed class UserManagementController : ApiController + /// <param name="query">Optional query to search by email address or username.</param> + /// <param name="skip">The number of users to skip.</param> + /// <param name="take">The number of users to return.</param> + /// <returns> + /// 200 => Users returned. + /// </returns> + [HttpGet] + [Route("user-management/")] + [ProducesResponseType(typeof(UsersDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminUsersRead)] + public async Task<IActionResult> GetUsers([FromQuery] string? query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) { - private readonly IUserService userService; - - public UserManagementController(ICommandBus commandBus, IUserService userService) - : base(commandBus) - { - this.userService = userService; - } + var users = await userService.QueryAsync(query, take, skip, HttpContext.RequestAborted); - /// <summary> - /// Get users by query. - /// </summary> - /// <param name="query">Optional query to search by email address or username.</param> - /// <param name="skip">The number of users to skip.</param> - /// <param name="take">The number of users to return.</param> - /// <returns> - /// 200 => Users returned. - /// </returns> - [HttpGet] - [Route("user-management/")] - [ProducesResponseType(typeof(UsersDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminUsersRead)] - public async Task<IActionResult> GetUsers([FromQuery] string? query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) - { - var users = await userService.QueryAsync(query, take, skip, HttpContext.RequestAborted); + var response = UsersDto.FromDomain(users, users.Total, Resources); - var response = UsersDto.FromDomain(users, users.Total, Resources); + return Ok(response); + } - return Ok(response); - } + /// <summary> + /// Get a user by ID. + /// </summary> + /// <param name="id">The ID of the user.</param> + /// <returns> + /// 200 => User returned. + /// 404 => User not found. + /// </returns> + [HttpGet] + [Route("user-management/{id}/")] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminUsersRead)] + public async Task<IActionResult> GetUser(string id) + { + var user = await userService.FindByIdAsync(id, HttpContext.RequestAborted); - /// <summary> - /// Get a user by ID. - /// </summary> - /// <param name="id">The ID of the user.</param> - /// <returns> - /// 200 => User returned. - /// 404 => User not found. - /// </returns> - [HttpGet] - [Route("user-management/{id}/")] - [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminUsersRead)] - public async Task<IActionResult> GetUser(string id) + if (user == null) { - var user = await userService.FindByIdAsync(id, HttpContext.RequestAborted); - - if (user == null) - { - return NotFound(); - } + return NotFound(); + } - var response = UserDto.FromDomain(user, Resources); + var response = UserDto.FromDomain(user, Resources); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Create a new user. - /// </summary> - /// <param name="request">The user object that needs to be added.</param> - /// <returns> - /// 201 => User created. - /// 400 => User request not valid. - /// </returns> - [HttpPost] - [Route("user-management/")] - [ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)] - [ApiPermission(PermissionIds.AdminUsersCreate)] - public async Task<IActionResult> PostUser([FromBody] CreateUserDto request) - { - var user = await userService.CreateAsync(request.Email, request.ToValues(), ct: HttpContext.RequestAborted); + /// <summary> + /// Create a new user. + /// </summary> + /// <param name="request">The user object that needs to be added.</param> + /// <returns> + /// 201 => User created. + /// 400 => User request not valid. + /// </returns> + [HttpPost] + [Route("user-management/")] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)] + [ApiPermission(PermissionIds.AdminUsersCreate)] + public async Task<IActionResult> PostUser([FromBody] CreateUserDto request) + { + var user = await userService.CreateAsync(request.Email, request.ToValues(), ct: HttpContext.RequestAborted); - var response = UserDto.FromDomain(user, Resources); + var response = UserDto.FromDomain(user, Resources); - return CreatedAtAction(nameof(GetUser), new { id = user.Id }, response); - } + return CreatedAtAction(nameof(GetUser), new { id = user.Id }, response); + } - /// <summary> - /// Update a user. - /// </summary> - /// <param name="id">The ID of the user.</param> - /// <param name="request">The user object that needs to be updated.</param> - /// <returns> - /// 200 => User created. - /// 400 => User request not valid. - /// 404 => User not found. - /// </returns> - [HttpPut] - [Route("user-management/{id}/")] - [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminUsersUpdate)] - public async Task<IActionResult> PutUser(string id, [FromBody] UpdateUserDto request) - { - var user = await userService.UpdateAsync(id, request.ToValues(), ct: HttpContext.RequestAborted); + /// <summary> + /// Update a user. + /// </summary> + /// <param name="id">The ID of the user.</param> + /// <param name="request">The user object that needs to be updated.</param> + /// <returns> + /// 200 => User created. + /// 400 => User request not valid. + /// 404 => User not found. + /// </returns> + [HttpPut] + [Route("user-management/{id}/")] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminUsersUpdate)] + public async Task<IActionResult> PutUser(string id, [FromBody] UpdateUserDto request) + { + var user = await userService.UpdateAsync(id, request.ToValues(), ct: HttpContext.RequestAborted); - var response = UserDto.FromDomain(user, Resources); + var response = UserDto.FromDomain(user, Resources); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Lock a user. - /// </summary> - /// <param name="id">The ID of the user to lock.</param> - /// <returns> - /// 200 => User locked. - /// 403 => User is the current user. - /// 404 => User not found. - /// </returns> - [HttpPut] - [Route("user-management/{id}/lock/")] - [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminUsersLock)] - public async Task<IActionResult> LockUser(string id) + /// <summary> + /// Lock a user. + /// </summary> + /// <param name="id">The ID of the user to lock.</param> + /// <returns> + /// 200 => User locked. + /// 403 => User is the current user. + /// 404 => User not found. + /// </returns> + [HttpPut] + [Route("user-management/{id}/lock/")] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminUsersLock)] + public async Task<IActionResult> LockUser(string id) + { + if (this.IsUser(id)) { - if (this.IsUser(id)) - { - throw new DomainForbiddenException(T.Get("users.lockYourselfError")); - } + throw new DomainForbiddenException(T.Get("users.lockYourselfError")); + } - var user = await userService.LockAsync(id, HttpContext.RequestAborted); + var user = await userService.LockAsync(id, HttpContext.RequestAborted); - var response = UserDto.FromDomain(user, Resources); + var response = UserDto.FromDomain(user, Resources); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Unlock a user. - /// </summary> - /// <param name="id">The ID of the user to unlock.</param> - /// <returns> - /// 200 => User unlocked. - /// 403 => User is the current user. - /// 404 => User not found. - /// </returns> - [HttpPut] - [Route("user-management/{id}/unlock/")] - [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] - [ApiPermission(PermissionIds.AdminUsersUnlock)] - public async Task<IActionResult> UnlockUser(string id) + /// <summary> + /// Unlock a user. + /// </summary> + /// <param name="id">The ID of the user to unlock.</param> + /// <returns> + /// 200 => User unlocked. + /// 403 => User is the current user. + /// 404 => User not found. + /// </returns> + [HttpPut] + [Route("user-management/{id}/unlock/")] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] + [ApiPermission(PermissionIds.AdminUsersUnlock)] + public async Task<IActionResult> UnlockUser(string id) + { + if (this.IsUser(id)) { - if (this.IsUser(id)) - { - throw new DomainForbiddenException(T.Get("users.unlockYourselfError")); - } + throw new DomainForbiddenException(T.Get("users.unlockYourselfError")); + } - var user = await userService.UnlockAsync(id, HttpContext.RequestAborted); + var user = await userService.UnlockAsync(id, HttpContext.RequestAborted); - var response = UserDto.FromDomain(user, Resources); + var response = UserDto.FromDomain(user, Resources); - return Ok(response); - } + return Ok(response); + } - /// <summary> - /// Delete a User. - /// </summary> - /// <param name="id">The ID of the user to delete.</param> - /// <returns> - /// 204 => User deleted. - /// 403 => User is the current user. - /// 404 => User not found. - /// </returns> - [HttpDelete] - [Route("user-management/{id}/")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ApiPermission(PermissionIds.AdminUsersUnlock)] - public async Task<IActionResult> DeleteUser(string id) + /// <summary> + /// Delete a User. + /// </summary> + /// <param name="id">The ID of the user to delete.</param> + /// <returns> + /// 204 => User deleted. + /// 403 => User is the current user. + /// 404 => User not found. + /// </returns> + [HttpDelete] + [Route("user-management/{id}/")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ApiPermission(PermissionIds.AdminUsersUnlock)] + public async Task<IActionResult> DeleteUser(string id) + { + if (this.IsUser(id)) { - if (this.IsUser(id)) - { - throw new DomainForbiddenException(T.Get("users.deleteYourselfError")); - } + throw new DomainForbiddenException(T.Get("users.deleteYourselfError")); + } - await userService.DeleteAsync(id, HttpContext.RequestAborted); + await userService.DeleteAsync(id, HttpContext.RequestAborted); - return NoContent(); - } + return NoContent(); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 2478bb6e59..bfaef887a6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -14,200 +14,199 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.Api.Controllers.Users +namespace Squidex.Areas.Api.Controllers.Users; + +/// <summary> +/// Update and query users. +/// </summary> +[ApiExplorerSettings(GroupName = nameof(Users))] +public sealed class UsersController : ApiController { - /// <summary> - /// Update and query users. - /// </summary> - [ApiExplorerSettings(GroupName = nameof(Users))] - public sealed class UsersController : ApiController + private static readonly byte[] AvatarBytes; + private readonly IHttpClientFactory httpClientFactory; + private readonly IUserPictureStore userPictureStore; + private readonly IUserResolver userResolver; + private readonly ILogger<UsersController> log; + + static UsersController() { - private static readonly byte[] AvatarBytes; - private readonly IHttpClientFactory httpClientFactory; - private readonly IUserPictureStore userPictureStore; - private readonly IUserResolver userResolver; - private readonly ILogger<UsersController> log; + var assembly = typeof(UsersController).Assembly; - static UsersController() + using (var resourceStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) { - var assembly = typeof(UsersController).Assembly; - - using (var resourceStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) - { - AvatarBytes = new byte[resourceStream!.Length]; + AvatarBytes = new byte[resourceStream!.Length]; - _ = resourceStream.Read(AvatarBytes, 0, AvatarBytes.Length); - } + _ = resourceStream.Read(AvatarBytes, 0, AvatarBytes.Length); } + } - public UsersController( - ICommandBus commandBus, - IHttpClientFactory httpClientFactory, - IUserPictureStore userPictureStore, - IUserResolver userResolver, - ILogger<UsersController> log) - : base(commandBus) - { - this.httpClientFactory = httpClientFactory; - this.userPictureStore = userPictureStore; - this.userResolver = userResolver; + public UsersController( + ICommandBus commandBus, + IHttpClientFactory httpClientFactory, + IUserPictureStore userPictureStore, + IUserResolver userResolver, + ILogger<UsersController> log) + : base(commandBus) + { + this.httpClientFactory = httpClientFactory; + this.userPictureStore = userPictureStore; + this.userResolver = userResolver; - this.log = log; - } + this.log = log; + } - /// <summary> - /// Get the user resources. - /// </summary> - /// <returns> - /// 200 => User resources returned. - /// </returns> - [HttpGet] - [Route("")] - [ProducesResponseType(typeof(ResourcesDto), StatusCodes.Status200OK)] - [ApiPermission] - public IActionResult GetUserResources() + /// <summary> + /// Get the user resources. + /// </summary> + /// <returns> + /// 200 => User resources returned. + /// </returns> + [HttpGet] + [Route("")] + [ProducesResponseType(typeof(ResourcesDto), StatusCodes.Status200OK)] + [ApiPermission] + public IActionResult GetUserResources() + { + var response = ResourcesDto.FromDomain(Resources); + + return Ok(response); + } + + /// <summary> + /// Get users by query. + /// </summary> + /// <param name="query">The query to search the user by email address. Case invariant.</param> + /// <remarks> + /// Search the user by query that contains the email address or the part of the email address. + /// </remarks> + /// <returns> + /// 200 => Users returned. + /// </returns> + [HttpGet] + [Route("users/")] + [ProducesResponseType(typeof(UserDto[]), StatusCodes.Status200OK)] + [ApiPermission] + public async Task<IActionResult> GetUsers(string query) + { + try { - var response = ResourcesDto.FromDomain(Resources); + var users = await userResolver.QueryByEmailAsync(query, HttpContext.RequestAborted); + + var response = users.Select(x => UserDto.FromDomain(x, Resources)).ToArray(); return Ok(response); } + catch (Exception ex) + { + log.LogError(ex, "Failed to return users, returning empty results."); + } - /// <summary> - /// Get users by query. - /// </summary> - /// <param name="query">The query to search the user by email address. Case invariant.</param> - /// <remarks> - /// Search the user by query that contains the email address or the part of the email address. - /// </remarks> - /// <returns> - /// 200 => Users returned. - /// </returns> - [HttpGet] - [Route("users/")] - [ProducesResponseType(typeof(UserDto[]), StatusCodes.Status200OK)] - [ApiPermission] - public async Task<IActionResult> GetUsers(string query) + return Ok(Array.Empty<UserDto>()); + } + + /// <summary> + /// Get user by id. + /// </summary> + /// <param name="id">The ID of the user (GUID).</param> + /// <returns> + /// 200 => User found. + /// 404 => User not found. + /// </returns> + [HttpGet] + [Route("users/{id}/")] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] + [ApiPermission] + public async Task<IActionResult> GetUser(string id) + { + try { - try - { - var users = await userResolver.QueryByEmailAsync(query, HttpContext.RequestAborted); + var entity = await userResolver.FindByIdAsync(id, HttpContext.RequestAborted); - var response = users.Select(x => UserDto.FromDomain(x, Resources)).ToArray(); + if (entity != null) + { + var response = UserDto.FromDomain(entity, Resources); return Ok(response); } - catch (Exception ex) - { - log.LogError(ex, "Failed to return users, returning empty results."); - } - - return Ok(Array.Empty<UserDto>()); } - - /// <summary> - /// Get user by id. - /// </summary> - /// <param name="id">The ID of the user (GUID).</param> - /// <returns> - /// 200 => User found. - /// 404 => User not found. - /// </returns> - [HttpGet] - [Route("users/{id}/")] - [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] - [ApiPermission] - public async Task<IActionResult> GetUser(string id) + catch (Exception ex) { - try - { - var entity = await userResolver.FindByIdAsync(id, HttpContext.RequestAborted); - - if (entity != null) - { - var response = UserDto.FromDomain(entity, Resources); - - return Ok(response); - } - } - catch (Exception ex) - { - log.LogError(ex, "Failed to return user, returning empty results."); - } - - return NotFound(); + log.LogError(ex, "Failed to return user, returning empty results."); } - /// <summary> - /// Get user picture by id. - /// </summary> - /// <param name="id">The ID of the user (GUID).</param> - /// <returns> - /// 200 => User found and image or fallback returned. - /// 404 => User not found. - /// </returns> - [HttpGet] - [Route("users/{id}/picture/")] - [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] - [ResponseCache(Duration = 300)] - public async Task<IActionResult> GetUserPicture(string id) + return NotFound(); + } + + /// <summary> + /// Get user picture by id. + /// </summary> + /// <param name="id">The ID of the user (GUID).</param> + /// <returns> + /// 200 => User found and image or fallback returned. + /// 404 => User not found. + /// </returns> + [HttpGet] + [Route("users/{id}/picture/")] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [ResponseCache(Duration = 300)] + public async Task<IActionResult> GetUserPicture(string id) + { + try { - try - { - var entity = await userResolver.FindByIdAsync(id, HttpContext.RequestAborted); + var entity = await userResolver.FindByIdAsync(id, HttpContext.RequestAborted); - if (entity != null) + if (entity != null) + { + if (entity.Claims.IsPictureUrlStored()) { - if (entity.Claims.IsPictureUrlStored()) + var callback = new FileCallback(async (body, range, ct) => { - var callback = new FileCallback(async (body, range, ct) => + try { - try - { - await userPictureStore.DownloadAsync(entity.Id, body, ct); - } - catch - { - await body.WriteAsync(AvatarBytes, ct); - } - }); + await userPictureStore.DownloadAsync(entity.Id, body, ct); + } + catch + { + await body.WriteAsync(AvatarBytes, ct); + } + }); - return new FileCallbackResult("image/png", callback); - } + return new FileCallbackResult("image/png", callback); + } - using (var client = httpClientFactory.CreateClient()) + using (var client = httpClientFactory.CreateClient()) + { + var url = entity.Claims.PictureNormalizedUrl(); + + if (!string.IsNullOrWhiteSpace(url)) { - var url = entity.Claims.PictureNormalizedUrl(); + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted); - if (!string.IsNullOrWhiteSpace(url)) + if (response.IsSuccessStatusCode) { - var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted); - - if (response.IsSuccessStatusCode) - { - var contentType = response.Content.Headers.ContentType?.ToString()!; - var contentStream = await response.Content.ReadAsStreamAsync(HttpContext.RequestAborted); + var contentType = response.Content.Headers.ContentType?.ToString()!; + var contentStream = await response.Content.ReadAsStreamAsync(HttpContext.RequestAborted); - var etag = response.Headers.ETag; + var etag = response.Headers.ETag; - var result = new FileStreamResult(contentStream, contentType); + var result = new FileStreamResult(contentStream, contentType); - if (!string.IsNullOrWhiteSpace(etag?.Tag)) - { - result.EntityTag = new EntityTagHeaderValue(etag.Tag, etag.IsWeak); - } - - return result; + if (!string.IsNullOrWhiteSpace(etag?.Tag)) + { + result.EntityTag = new EntityTagHeaderValue(etag.Tag, etag.IsWeak); } + + return result; } } } } - catch (Exception ex) - { - log.LogError(ex, "Failed to return user picture, returning fallback image."); - } - - return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png"); } + catch (Exception ex) + { + log.LogError(ex, "Failed to return user picture, returning fallback image."); + } + + return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png"); } } diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/EmbedMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/EmbedMiddleware.cs index 06d8ccc697..8737dbeb88 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/EmbedMiddleware.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/EmbedMiddleware.cs @@ -5,33 +5,32 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Frontend.Middlewares +namespace Squidex.Areas.Frontend.Middlewares; + +public sealed class EmbedMiddleware { - public sealed class EmbedMiddleware + private readonly RequestDelegate next; + + public EmbedMiddleware(RequestDelegate next) { - private readonly RequestDelegate next; + this.next = next; + } - public EmbedMiddleware(RequestDelegate next) - { - this.next = next; - } + public Task InvokeAsync(HttpContext context) + { + var request = context.Request; - public Task InvokeAsync(HttpContext context) + if (request.Path.StartsWithSegments("/embed", StringComparison.Ordinal, out var remaining)) { - var request = context.Request; - - if (request.Path.StartsWithSegments("/embed", StringComparison.Ordinal, out var remaining)) - { - request.Path = remaining; + request.Path = remaining; - var uiOptions = new OptionsFeature(); - uiOptions.Options["embedded"] = true; - uiOptions.Options["embedPath"] = "/embed"; + var uiOptions = new OptionsFeature(); + uiOptions.Options["embedded"] = true; + uiOptions.Options["embedPath"] = "/embed"; - context.Features.Set(uiOptions); - } - - return next(context); + context.Features.Set(uiOptions); } + + return next(context); } } diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs index 5a9619fe46..e4acf17f2f 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs @@ -13,97 +13,96 @@ using Squidex.Domain.Apps.Entities.History; using Squidex.Web; -namespace Squidex.Areas.Frontend.Middlewares +namespace Squidex.Areas.Frontend.Middlewares; + +public static class IndexExtensions { - public static class IndexExtensions + private static readonly ConcurrentDictionary<string, string> Texts = new ConcurrentDictionary<string, string>(); + + public static string AddOptions(this string html, HttpContext httpContext) { - private static readonly ConcurrentDictionary<string, string> Texts = new ConcurrentDictionary<string, string>(); + const string Placeholder = "/* INJECT OPTIONS */"; - public static string AddOptions(this string html, HttpContext httpContext) + if (!html.Contains(Placeholder, StringComparison.Ordinal)) { - const string Placeholder = "/* INJECT OPTIONS */"; - - if (!html.Contains(Placeholder, StringComparison.Ordinal)) - { - return html; - } + return html; + } - var scripts = new List<string> - { - $"var texts = {GetText(CultureInfo.CurrentUICulture.Name)};" - }; + var scripts = new List<string> + { + $"var texts = {GetText(CultureInfo.CurrentUICulture.Name)};" + }; - var uiOptions = httpContext.RequestServices.GetService<IOptions<MyUIOptions>>()?.Value; + var uiOptions = httpContext.RequestServices.GetService<IOptions<MyUIOptions>>()?.Value; - if (uiOptions != null) + if (uiOptions != null) + { + var clonedOptions = uiOptions with { - var clonedOptions = uiOptions with + More = new Dictionary<string, object> { - More = new Dictionary<string, object> - { - ["culture"] = CultureInfo.CurrentUICulture.Name - } - }; + ["culture"] = CultureInfo.CurrentUICulture.Name + } + }; - var jsonOptions = httpContext.RequestServices.GetRequiredService<JsonSerializerOptions>(); + var jsonOptions = httpContext.RequestServices.GetRequiredService<JsonSerializerOptions>(); - using var jsonDocument = JsonSerializer.SerializeToDocument(uiOptions, jsonOptions); + using var jsonDocument = JsonSerializer.SerializeToDocument(uiOptions, jsonOptions); - if (httpContext.RequestServices.GetService<ExposedValues>() is ExposedValues values) - { - clonedOptions.More["info"] = values.ToString(); - } + if (httpContext.RequestServices.GetService<ExposedValues>() is ExposedValues values) + { + clonedOptions.More["info"] = values.ToString(); + } - var notifo = httpContext.RequestServices!.GetService<IOptions<NotifoOptions>>()?.Value; + var notifo = httpContext.RequestServices!.GetService<IOptions<NotifoOptions>>()?.Value; - if (notifo?.IsConfigured() == true) - { - clonedOptions.More["notifoApi"] = notifo.ApiUrl; - } + if (notifo?.IsConfigured() == true) + { + clonedOptions.More["notifoApi"] = notifo.ApiUrl; + } - var options = httpContext.Features.Get<OptionsFeature>(); + var options = httpContext.Features.Get<OptionsFeature>(); - if (options != null) + if (options != null) + { + foreach (var (key, value) in options.Options) { - foreach (var (key, value) in options.Options) - { - clonedOptions.More[key] = value; - } + clonedOptions.More[key] = value; } - - scripts.Add($"var options = {JsonSerializer.Serialize(clonedOptions)};"); } - html = html.Replace(Placeholder, string.Join(Environment.NewLine, scripts), StringComparison.OrdinalIgnoreCase); - - return html; + scripts.Add($"var options = {JsonSerializer.Serialize(clonedOptions)};"); } - private static string GetText(string culture) + html = html.Replace(Placeholder, string.Join(Environment.NewLine, scripts), StringComparison.OrdinalIgnoreCase); + + return html; + } + + private static string GetText(string culture) + { + if (!Texts.TryGetValue(culture, out var result)) { - if (!Texts.TryGetValue(culture, out var result)) - { - var assembly = typeof(IndexExtensions).Assembly; + var assembly = typeof(IndexExtensions).Assembly; - var resourceName = $"Squidex.Areas.Frontend.Resources.frontend_{culture}.json"; - var resourceStream = assembly.GetManifestResourceStream(resourceName); + var resourceName = $"Squidex.Areas.Frontend.Resources.frontend_{culture}.json"; + var resourceStream = assembly.GetManifestResourceStream(resourceName); - if (resourceStream != null) + if (resourceStream != null) + { + using (var reader = new StreamReader(resourceStream)) { - using (var reader = new StreamReader(resourceStream)) - { - result = reader.ReadToEnd(); + result = reader.ReadToEnd(); - Texts[culture] = result; - } - } - else - { - return GetText("en"); + Texts[culture] = result; } } - - return result; + else + { + return GetText("en"); + } } + + return result; } } diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs index a220a74f60..2ceddeca3f 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs @@ -9,51 +9,50 @@ using Microsoft.Net.Http.Headers; using Squidex.Domain.Apps.Entities.History; -namespace Squidex.Areas.Frontend.Middlewares +namespace Squidex.Areas.Frontend.Middlewares; + +public sealed class NotifoMiddleware { - public sealed class NotifoMiddleware + private readonly RequestDelegate next; + private readonly string? workerUrl; + + public NotifoMiddleware(RequestDelegate next, IOptions<NotifoOptions> options) { - private readonly RequestDelegate next; - private readonly string? workerUrl; + this.next = next; + + workerUrl = GetUrl(options.Value); + } - public NotifoMiddleware(RequestDelegate next, IOptions<NotifoOptions> options) + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.Equals("/notifo-sw.js", StringComparison.Ordinal) && workerUrl != null) { - this.next = next; + context.Response.Headers[HeaderNames.ContentType] = "text/javascript"; + + var script = $"importScripts('{workerUrl}')"; - workerUrl = GetUrl(options.Value); + await context.Response.WriteAsync(script, context.RequestAborted); } + else + { + await next(context); + } + } - public async Task InvokeAsync(HttpContext context) + private static string? GetUrl(NotifoOptions options) + { + if (!options.IsConfigured()) { - if (context.Request.Path.Equals("/notifo-sw.js", StringComparison.Ordinal) && workerUrl != null) - { - context.Response.Headers[HeaderNames.ContentType] = "text/javascript"; - - var script = $"importScripts('{workerUrl}')"; - - await context.Response.WriteAsync(script, context.RequestAborted); - } - else - { - await next(context); - } + return null; } - private static string? GetUrl(NotifoOptions options) + if (options.ApiUrl.Contains("localhost:5002", StringComparison.Ordinal)) + { + return "https://localhost:3002/notifo-sdk-worker.js"; + } + else { - if (!options.IsConfigured()) - { - return null; - } - - if (options.ApiUrl.Contains("localhost:5002", StringComparison.Ordinal)) - { - return "https://localhost:3002/notifo-sdk-worker.js"; - } - else - { - return $"{options.ApiUrl}/build/notifo-sdk-worker.js"; - } + return $"{options.ApiUrl}/build/notifo-sdk-worker.js"; } } } diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/OptionsFeature.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/OptionsFeature.cs index f4ba677ba9..d44c74da45 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/OptionsFeature.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/OptionsFeature.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.Frontend.Middlewares +namespace Squidex.Areas.Frontend.Middlewares; + +public sealed class OptionsFeature { - public sealed class OptionsFeature - { - public Dictionary<string, object> Options { get; } = new Dictionary<string, object>(); - } + public Dictionary<string, object> Options { get; } = new Dictionary<string, object>(); } diff --git a/backend/src/Squidex/Areas/Frontend/Startup.cs b/backend/src/Squidex/Areas/Frontend/Startup.cs index 9f2baee104..864331ddb2 100644 --- a/backend/src/Squidex/Areas/Frontend/Startup.cs +++ b/backend/src/Squidex/Areas/Frontend/Startup.cs @@ -12,100 +12,99 @@ using Squidex.Pipeline.Squid; using Squidex.Web.Pipeline; -namespace Squidex.Areas.Frontend +namespace Squidex.Areas.Frontend; + +public static class Startup { - public static class Startup + public static void UseFrontend(this IApplicationBuilder app) { - public static void UseFrontend(this IApplicationBuilder app) - { - var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>(); + var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>(); - var fileProvider = environment.WebRootFileProvider; + var fileProvider = environment.WebRootFileProvider; - app.UseMiddleware<EmbedMiddleware>(); + app.UseMiddleware<EmbedMiddleware>(); - if (!environment.IsDevelopment()) - { - fileProvider = new CompositeFileProvider(fileProvider, - new PhysicalFileProvider(Path.Combine(environment.WebRootPath, "build"))); - } + if (!environment.IsDevelopment()) + { + fileProvider = new CompositeFileProvider(fileProvider, + new PhysicalFileProvider(Path.Combine(environment.WebRootPath, "build"))); + } - app.Map("/squid.svg", builder => - { - builder.UseMiddleware<SquidMiddleware>(); - }); + app.Map("/squid.svg", builder => + { + builder.UseMiddleware<SquidMiddleware>(); + }); - app.UseMiddleware<NotifoMiddleware>(); + app.UseMiddleware<NotifoMiddleware>(); - app.UseWhen(c => c.IsSpaFile(), builder => - { - builder.UseMiddleware<SetupMiddleware>(); - }); + app.UseWhen(c => c.IsSpaFile(), builder => + { + builder.UseMiddleware<SetupMiddleware>(); + }); - app.UseWhen(c => c.IsSpaFile() || c.IsHtmlPath(), builder => + app.UseWhen(c => c.IsSpaFile() || c.IsHtmlPath(), builder => + { + // Adjust the base for all potential html files. + builder.UseHtmlTransform(new HtmlTransformOptions { - // Adjust the base for all potential html files. - builder.UseHtmlTransform(new HtmlTransformOptions + Transform = (html, context) => { - Transform = (html, context) => - { - return new ValueTask<string>(html.AddOptions(context)); - } - }); + return new ValueTask<string>(html.AddOptions(context)); + } }); + }); - app.Use((context, next) => - { - return next(); - }); + app.Use((context, next) => + { + return next(); + }); - app.UseSquidexStaticFiles(fileProvider); + app.UseSquidexStaticFiles(fileProvider); - if (!environment.IsDevelopment()) - { - // Try static files again to serve index.html. - app.UsePathOverride("/index.html"); - app.UseSquidexStaticFiles(fileProvider); - } - else - { - // Forward requests to SPA development server. - app.UseSpa(builder => - { - builder.UseProxyToSpaDevelopmentServer("https://localhost:3000"); - }); - } + if (!environment.IsDevelopment()) + { + // Try static files again to serve index.html. + app.UsePathOverride("/index.html"); + app.UseSquidexStaticFiles(fileProvider); } - - private static void UseSquidexStaticFiles(this IApplicationBuilder app, IFileProvider fileProvider) + else { - app.UseStaticFiles(new StaticFileOptions + // Forward requests to SPA development server. + app.UseSpa(builder => { - OnPrepareResponse = context => - { - var response = context.Context.Response; - - if (!string.IsNullOrWhiteSpace(context.Context.Request.QueryString.ToString())) - { - response.Headers[HeaderNames.CacheControl] = "max-age=5184000"; - } - else if (string.Equals(response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase)) - { - response.Headers[HeaderNames.CacheControl] = "no-cache"; - } - }, - FileProvider = fileProvider + builder.UseProxyToSpaDevelopmentServer("https://localhost:3000"); }); } + } - private static bool IsSpaFile(this HttpContext context) + private static void UseSquidexStaticFiles(this IApplicationBuilder app, IFileProvider fileProvider) + { + app.UseStaticFiles(new StaticFileOptions { - return (context.IsIndex() || !Path.HasExtension(context.Request.Path)) && !context.IsDevServer(); - } + OnPrepareResponse = context => + { + var response = context.Context.Response; - private static bool IsDevServer(this HttpContext context) - { - return context.Request.Path.StartsWithSegments("/ws", StringComparison.OrdinalIgnoreCase); - } + if (!string.IsNullOrWhiteSpace(context.Context.Request.QueryString.ToString())) + { + response.Headers[HeaderNames.CacheControl] = "max-age=5184000"; + } + else if (string.Equals(response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase)) + { + response.Headers[HeaderNames.CacheControl] = "no-cache"; + } + }, + FileProvider = fileProvider + }); + } + + private static bool IsSpaFile(this HttpContext context) + { + return (context.IsIndex() || !Path.HasExtension(context.Request.Path)) && !context.IsDevServer(); + } + + private static bool IsDevServer(this HttpContext context) + { + return context.Request.Path.StartsWithSegments("/ws", StringComparison.OrdinalIgnoreCase); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/AlwaysAddTokenHandler.cs b/backend/src/Squidex/Areas/IdentityServer/Config/AlwaysAddTokenHandler.cs index 7d77e873ec..9be2bea3e6 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/AlwaysAddTokenHandler.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/AlwaysAddTokenHandler.cs @@ -10,25 +10,24 @@ using OpenIddict.Server; using static OpenIddict.Server.OpenIddictServerEvents; -namespace Squidex.Areas.IdentityServer.Config +namespace Squidex.Areas.IdentityServer.Config; + +public sealed class AlwaysAddTokenHandler : IOpenIddictServerHandler<ProcessSignInContext> { - public sealed class AlwaysAddTokenHandler : IOpenIddictServerHandler<ProcessSignInContext> + public ValueTask HandleAsync(ProcessSignInContext context) { - public ValueTask HandleAsync(ProcessSignInContext context) + if (context == null) { - if (context == null) - { - return default; - } - - if (!string.IsNullOrWhiteSpace(context.Response.AccessToken)) - { - var scopes = context.AccessTokenPrincipal?.GetScopes() ?? ImmutableArray<string>.Empty; + return default; + } - context.Response.Scope = string.Join(" ", scopes); - } + if (!string.IsNullOrWhiteSpace(context.Response.AccessToken)) + { + var scopes = context.AccessTokenPrincipal?.GetScopes() ?? ImmutableArray<string>.Empty; - return default; + context.Response.Scope = string.Join(" ", scopes); } + + return default; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationExtensions.cs b/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationExtensions.cs index 95d6c2ed6e..f6694eda5d 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationExtensions.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationExtensions.cs @@ -13,49 +13,48 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Areas.IdentityServer.Config +namespace Squidex.Areas.IdentityServer.Config; + +public static class ApplicationExtensions { - public static class ApplicationExtensions + public static OpenIddictApplicationDescriptor SetAdmin(this OpenIddictApplicationDescriptor application) { - public static OpenIddictApplicationDescriptor SetAdmin(this OpenIddictApplicationDescriptor application) - { - application.Properties[SquidexClaimTypes.Permissions] = CreateParameter(Enumerable.Repeat(PermissionIds.All, 1)); + application.Properties[SquidexClaimTypes.Permissions] = CreateParameter(Enumerable.Repeat(PermissionIds.All, 1)); - return application; - } + return application; + } - public static OpenIddictApplicationDescriptor CopyClaims(this OpenIddictApplicationDescriptor application, IUser claims) + public static OpenIddictApplicationDescriptor CopyClaims(this OpenIddictApplicationDescriptor application, IUser claims) + { + foreach (var group in claims.Claims.GroupBy(x => x.Type)) { - foreach (var group in claims.Claims.GroupBy(x => x.Type)) - { - application.Properties[group.Key] = CreateParameter(group.Select(x => x.Value)); - } - - return application; + application.Properties[group.Key] = CreateParameter(group.Select(x => x.Value)); } - private static JsonElement CreateParameter(IEnumerable<string> values) - { - return (JsonElement)new OpenIddictParameter(values.ToArray()); - } + return application; + } - public static IEnumerable<Claim> Claims(this IReadOnlyDictionary<string, JsonElement> properties) + private static JsonElement CreateParameter(IEnumerable<string> values) + { + return (JsonElement)new OpenIddictParameter(values.ToArray()); + } + + public static IEnumerable<Claim> Claims(this IReadOnlyDictionary<string, JsonElement> properties) + { + foreach (var (key, value) in properties) { - foreach (var (key, value) in properties) - { - var values = (string[]?)new OpenIddictParameter(value); + var values = (string[]?)new OpenIddictParameter(value); - if (values != null) + if (values != null) + { + foreach (var claimValue in values) { - foreach (var claimValue in values) + if (key == SquidexClaimTypes.DisplayName) { - if (key == SquidexClaimTypes.DisplayName) - { - yield return new Claim(OpenIdClaims.Name, claimValue); - } - - yield return new Claim(key, claimValue); + yield return new Claim(OpenIdClaims.Name, claimValue); } + + yield return new Claim(key, claimValue); } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs b/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs index a5f1d258db..6344ce4c94 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs @@ -9,23 +9,22 @@ using OpenIddict.Abstractions; using OpenIddict.Core; -namespace Squidex.Areas.IdentityServer.Config +namespace Squidex.Areas.IdentityServer.Config; + +public sealed class ApplicationManager<T> : OpenIddictApplicationManager<T> where T : class { - public sealed class ApplicationManager<T> : OpenIddictApplicationManager<T> where T : class + public ApplicationManager( + IOptionsMonitor<OpenIddictCoreOptions> options, + IOpenIddictApplicationCache<T> cache, + IOpenIddictApplicationStoreResolver resolver, + ILogger<OpenIddictApplicationManager<T>> logger) + : base(cache, logger, options, resolver) { - public ApplicationManager( - IOptionsMonitor<OpenIddictCoreOptions> options, - IOpenIddictApplicationCache<T> cache, - IOpenIddictApplicationStoreResolver resolver, - ILogger<OpenIddictApplicationManager<T>> logger) - : base(cache, logger, options, resolver) - { - } + } - protected override ValueTask<bool> ValidateClientSecretAsync(string secret, string comparand, - CancellationToken cancellationToken = default) - { - return new ValueTask<bool>(string.Equals(secret, comparand, StringComparison.Ordinal)); - } + protected override ValueTask<bool> ValidateClientSecretAsync(string secret, string comparand, + CancellationToken cancellationToken = default) + { + return new ValueTask<bool>(string.Equals(secret, comparand, StringComparison.Ordinal)); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs b/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs index fc42f9cf57..fa7d3dd588 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs @@ -15,101 +15,100 @@ using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Areas.IdentityServer.Config +namespace Squidex.Areas.IdentityServer.Config; + +public sealed class CreateAdminInitializer : IInitializable { - public sealed class CreateAdminInitializer : IInitializable - { - private readonly IServiceProvider serviceProvider; - private readonly MyIdentityOptions identityOptions; + private readonly IServiceProvider serviceProvider; + private readonly MyIdentityOptions identityOptions; - public int Order => int.MaxValue; + public int Order => int.MaxValue; - public CreateAdminInitializer(IServiceProvider serviceProvider, IOptions<MyIdentityOptions> identityOptions) - { - this.serviceProvider = serviceProvider; + public CreateAdminInitializer(IServiceProvider serviceProvider, IOptions<MyIdentityOptions> identityOptions) + { + this.serviceProvider = serviceProvider; - this.identityOptions = identityOptions.Value; - } + this.identityOptions = identityOptions.Value; + } - public async Task InitializeAsync( - CancellationToken ct) - { - IdentityModelEventSource.ShowPII = identityOptions.ShowPII; + public async Task InitializeAsync( + CancellationToken ct) + { + IdentityModelEventSource.ShowPII = identityOptions.ShowPII; - if (identityOptions.IsAdminConfigured()) + if (identityOptions.IsAdminConfigured()) + { + await using (var scope = serviceProvider.CreateAsyncScope()) { - await using (var scope = serviceProvider.CreateAsyncScope()) - { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - var adminEmail = identityOptions.AdminEmail; - var adminPass = identityOptions.AdminPassword; + var adminEmail = identityOptions.AdminEmail; + var adminPass = identityOptions.AdminPassword; - var isEmpty = await IsEmptyAsync(userService); + var isEmpty = await IsEmptyAsync(userService); - if (isEmpty || identityOptions.AdminRecreate) + if (isEmpty || identityOptions.AdminRecreate) + { + try { - try - { - var user = await userService.FindByEmailAsync(adminEmail, ct); + var user = await userService.FindByEmailAsync(adminEmail, ct); - if (user != null) - { - if (identityOptions.AdminRecreate) - { - var permissions = CreatePermissions(user.Claims.Permissions()); - - var values = new UserValues - { - Password = adminPass, - Permissions = permissions - }; - - await userService.UpdateAsync(user.Id, values, ct: ct); - } - } - else + if (user != null) + { + if (identityOptions.AdminRecreate) { - var permissions = CreatePermissions(PermissionSet.Empty); + var permissions = CreatePermissions(user.Claims.Permissions()); var values = new UserValues { Password = adminPass, - Permissions = permissions, - DisplayName = adminEmail + Permissions = permissions }; - await userService.CreateAsync(adminEmail, values, ct: ct); + await userService.UpdateAsync(user.Id, values, ct: ct); } } - catch (Exception ex) + else { - var log = serviceProvider.GetRequiredService<ILogger<CreateAdminInitializer>>(); + var permissions = CreatePermissions(PermissionSet.Empty); - log.LogError(ex, "Failed to create administrator."); + var values = new UserValues + { + Password = adminPass, + Permissions = permissions, + DisplayName = adminEmail + }; + + await userService.CreateAsync(adminEmail, values, ct: ct); } } + catch (Exception ex) + { + var log = serviceProvider.GetRequiredService<ILogger<CreateAdminInitializer>>(); + + log.LogError(ex, "Failed to create administrator."); + } } } } + } - private PermissionSet CreatePermissions(PermissionSet permissions) - { - permissions = permissions.Add(PermissionIds.Admin); - - foreach (var app in identityOptions.AdminApps.OrEmpty()) - { - permissions = permissions.Add(PermissionIds.ForApp(PermissionIds.AppAdmin, app)); - } + private PermissionSet CreatePermissions(PermissionSet permissions) + { + permissions = permissions.Add(PermissionIds.Admin); - return permissions; + foreach (var app in identityOptions.AdminApps.OrEmpty()) + { + permissions = permissions.Add(PermissionIds.ForApp(PermissionIds.AppAdmin, app)); } - private static async Task<bool> IsEmptyAsync(IUserService userService) - { - var users = await userService.QueryAsync(take: 1); + return permissions; + } - return users.Total == 0; - } + private static async Task<bool> IsEmptyAsync(IUserService userService) + { + var users = await userService.QueryAsync(take: 1); + + return users.Total == 0; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs b/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs index 3ae1b57ff8..10f0b79e2e 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs @@ -18,219 +18,218 @@ using Squidex.Web; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Squidex.Areas.IdentityServer.Config +namespace Squidex.Areas.IdentityServer.Config; + +public class DynamicApplicationStore : InMemoryApplicationStore { - public class DynamicApplicationStore : InMemoryApplicationStore + private readonly IServiceProvider serviceProvider; + + public DynamicApplicationStore(IServiceProvider serviceProvider) + : base(CreateStaticClients(serviceProvider)) + { + this.serviceProvider = serviceProvider; + } + + public override async ValueTask<ImmutableApplication?> FindByIdAsync(string identifier, + CancellationToken cancellationToken) { - private readonly IServiceProvider serviceProvider; + var application = await base.FindByIdAsync(identifier, cancellationToken); - public DynamicApplicationStore(IServiceProvider serviceProvider) - : base(CreateStaticClients(serviceProvider)) + if (application == null) { - this.serviceProvider = serviceProvider; + application = await GetDynamicAsync(identifier); } - public override async ValueTask<ImmutableApplication?> FindByIdAsync(string identifier, - CancellationToken cancellationToken) - { - var application = await base.FindByIdAsync(identifier, cancellationToken); + return application; + } - if (application == null) - { - application = await GetDynamicAsync(identifier); - } + public override async ValueTask<ImmutableApplication?> FindByClientIdAsync(string identifier, + CancellationToken cancellationToken) + { + var application = await base.FindByClientIdAsync(identifier, cancellationToken); - return application; + if (application == null) + { + application = await GetDynamicAsync(identifier); } - public override async ValueTask<ImmutableApplication?> FindByClientIdAsync(string identifier, - CancellationToken cancellationToken) - { - var application = await base.FindByClientIdAsync(identifier, cancellationToken); + return application; + } - if (application == null) - { - application = await GetDynamicAsync(identifier); - } + private async Task<ImmutableApplication?> GetDynamicAsync(string clientId) + { + var (appName, appClientId) = clientId.GetClientParts(); - return application; - } + var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); - private async Task<ImmutableApplication?> GetDynamicAsync(string clientId) + if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId)) { - var (appName, appClientId) = clientId.GetClientParts(); + var app = await appProvider.GetAppAsync(appName, true); - var appProvider = serviceProvider.GetRequiredService<IAppProvider>(); + var appClient = app?.Clients.GetValueOrDefault(appClientId); - if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId)) + if (appClient != null) { - var app = await appProvider.GetAppAsync(appName, true); - - var appClient = app?.Clients.GetValueOrDefault(appClientId); - - if (appClient != null) - { - return CreateClientFromApp(clientId, appClient); - } + return CreateClientFromApp(clientId, appClient); } + } - await using (var scope = serviceProvider.CreateAsyncScope()) - { - var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); + await using (var scope = serviceProvider.CreateAsyncScope()) + { + var userService = scope.ServiceProvider.GetRequiredService<IUserService>(); - var user = await userService.FindByIdAsync(clientId); + var user = await userService.FindByIdAsync(clientId); - if (user == null) - { - return null; - } + if (user == null) + { + return null; + } - var secret = user.Claims.ClientSecret(); + var secret = user.Claims.ClientSecret(); - if (!string.IsNullOrWhiteSpace(secret)) - { - return CreateClientFromUser(user, secret); - } + if (!string.IsNullOrWhiteSpace(secret)) + { + return CreateClientFromUser(user, secret); } - - return null; } - private static ImmutableApplication CreateClientFromUser(IUser user, string secret) + return null; + } + + private static ImmutableApplication CreateClientFromUser(IUser user, string secret) + { + return new ImmutableApplication(user.Id, new OpenIddictApplicationDescriptor { - return new ImmutableApplication(user.Id, new OpenIddictApplicationDescriptor + DisplayName = $"{user.Email} Client", + ClientId = user.Id, + ClientSecret = secret, + Permissions = { - DisplayName = $"{user.Email} Client", - ClientId = user.Id, - ClientSecret = secret, - Permissions = - { - Permissions.Endpoints.Token, - Permissions.GrantTypes.ClientCredentials, - Permissions.ResponseTypes.Token, - Permissions.Scopes.Email, - Permissions.Scopes.Profile, - Permissions.Scopes.Roles, - Permissions.Prefixes.Scope + Constants.ScopeApi, - Permissions.Prefixes.Scope + Constants.ScopePermissions - } - }.CopyClaims(user)); - } + Permissions.Endpoints.Token, + Permissions.GrantTypes.ClientCredentials, + Permissions.ResponseTypes.Token, + Permissions.Scopes.Email, + Permissions.Scopes.Profile, + Permissions.Scopes.Roles, + Permissions.Prefixes.Scope + Constants.ScopeApi, + Permissions.Prefixes.Scope + Constants.ScopePermissions + } + }.CopyClaims(user)); + } - private static ImmutableApplication CreateClientFromApp(string id, AppClient appClient) + private static ImmutableApplication CreateClientFromApp(string id, AppClient appClient) + { + return new ImmutableApplication(id, new OpenIddictApplicationDescriptor { - return new ImmutableApplication(id, new OpenIddictApplicationDescriptor + DisplayName = id, + ClientId = id, + ClientSecret = appClient.Secret, + Permissions = { - DisplayName = id, - ClientId = id, - ClientSecret = appClient.Secret, - Permissions = - { - Permissions.Endpoints.Token, - Permissions.GrantTypes.ClientCredentials, - Permissions.ResponseTypes.Token, - Permissions.Scopes.Email, - Permissions.Scopes.Profile, - Permissions.Scopes.Roles, - Permissions.Prefixes.Scope + Constants.ScopeApi, - Permissions.Prefixes.Scope + Constants.ScopePermissions - } - }); - } + Permissions.Endpoints.Token, + Permissions.GrantTypes.ClientCredentials, + Permissions.ResponseTypes.Token, + Permissions.Scopes.Email, + Permissions.Scopes.Profile, + Permissions.Scopes.Roles, + Permissions.Prefixes.Scope + Constants.ScopeApi, + Permissions.Prefixes.Scope + Constants.ScopePermissions + } + }); + } - private static IEnumerable<(string, OpenIddictApplicationDescriptor)> CreateStaticClients(IServiceProvider serviceProvider) - { - var identityOptions = serviceProvider.GetRequiredService<IOptions<MyIdentityOptions>>().Value; + private static IEnumerable<(string, OpenIddictApplicationDescriptor)> CreateStaticClients(IServiceProvider serviceProvider) + { + var identityOptions = serviceProvider.GetRequiredService<IOptions<MyIdentityOptions>>().Value; - var urlGenerator = serviceProvider.GetRequiredService<IUrlGenerator>(); + var urlGenerator = serviceProvider.GetRequiredService<IUrlGenerator>(); - var frontendId = Constants.ClientFrontendId; + var frontendId = Constants.ClientFrontendId; - yield return (frontendId, new OpenIddictApplicationDescriptor + yield return (frontendId, new OpenIddictApplicationDescriptor + { + DisplayName = "Frontend Client", + ClientId = frontendId, + ClientSecret = null, + RedirectUris = { - DisplayName = "Frontend Client", - ClientId = frontendId, - ClientSecret = null, - RedirectUris = - { - new Uri(urlGenerator.BuildUrl("login;")), - new Uri(urlGenerator.BuildUrl("client-callback-silent.html", false)), - new Uri(urlGenerator.BuildUrl("client-callback-popup.html", false)) - }, - PostLogoutRedirectUris = - { - new Uri(urlGenerator.BuildUrl("logout", false)) - }, - Permissions = - { - Permissions.Endpoints.Authorization, - Permissions.Endpoints.Logout, - Permissions.Endpoints.Token, - Permissions.GrantTypes.AuthorizationCode, - Permissions.GrantTypes.RefreshToken, - Permissions.ResponseTypes.Code, - Permissions.Scopes.Email, - Permissions.Scopes.Profile, - Permissions.Scopes.Roles, - Permissions.Prefixes.Scope + Constants.ScopeApi, - Permissions.Prefixes.Scope + Constants.ScopePermissions - }, - Type = ClientTypes.Public - }); - - var internalClientId = Constants.ClientInternalId; - - yield return (internalClientId, new OpenIddictApplicationDescriptor + new Uri(urlGenerator.BuildUrl("login;")), + new Uri(urlGenerator.BuildUrl("client-callback-silent.html", false)), + new Uri(urlGenerator.BuildUrl("client-callback-popup.html", false)) + }, + PostLogoutRedirectUris = { - DisplayName = "Internal Client", - ClientId = internalClientId, - ClientSecret = Constants.ClientInternalSecret, - RedirectUris = - { - new Uri(urlGenerator.BuildUrl("/signin-internal", false)) - }, - Permissions = - { - Permissions.Endpoints.Authorization, - Permissions.Endpoints.Logout, - Permissions.Endpoints.Token, - Permissions.GrantTypes.Implicit, - Permissions.ResponseTypes.IdToken, - Permissions.ResponseTypes.IdTokenToken, - Permissions.ResponseTypes.Token, - Permissions.Scopes.Email, - Permissions.Scopes.Profile, - Permissions.Scopes.Roles, - Permissions.Prefixes.Scope + Constants.ScopeApi, - Permissions.Prefixes.Scope + Constants.ScopePermissions - }, - Type = ClientTypes.Public - }); - - if (!identityOptions.IsAdminClientConfigured()) + new Uri(urlGenerator.BuildUrl("logout", false)) + }, + Permissions = { - yield break; - } + Permissions.Endpoints.Authorization, + Permissions.Endpoints.Logout, + Permissions.Endpoints.Token, + Permissions.GrantTypes.AuthorizationCode, + Permissions.GrantTypes.RefreshToken, + Permissions.ResponseTypes.Code, + Permissions.Scopes.Email, + Permissions.Scopes.Profile, + Permissions.Scopes.Roles, + Permissions.Prefixes.Scope + Constants.ScopeApi, + Permissions.Prefixes.Scope + Constants.ScopePermissions + }, + Type = ClientTypes.Public + }); + + var internalClientId = Constants.ClientInternalId; + + yield return (internalClientId, new OpenIddictApplicationDescriptor + { + DisplayName = "Internal Client", + ClientId = internalClientId, + ClientSecret = Constants.ClientInternalSecret, + RedirectUris = + { + new Uri(urlGenerator.BuildUrl("/signin-internal", false)) + }, + Permissions = + { + Permissions.Endpoints.Authorization, + Permissions.Endpoints.Logout, + Permissions.Endpoints.Token, + Permissions.GrantTypes.Implicit, + Permissions.ResponseTypes.IdToken, + Permissions.ResponseTypes.IdTokenToken, + Permissions.ResponseTypes.Token, + Permissions.Scopes.Email, + Permissions.Scopes.Profile, + Permissions.Scopes.Roles, + Permissions.Prefixes.Scope + Constants.ScopeApi, + Permissions.Prefixes.Scope + Constants.ScopePermissions + }, + Type = ClientTypes.Public + }); + + if (!identityOptions.IsAdminClientConfigured()) + { + yield break; + } - var adminClientId = identityOptions.AdminClientId; + var adminClientId = identityOptions.AdminClientId; - yield return (adminClientId, new OpenIddictApplicationDescriptor + yield return (adminClientId, new OpenIddictApplicationDescriptor + { + DisplayName = "Admin Client", + ClientId = adminClientId, + ClientSecret = identityOptions.AdminClientSecret, + Permissions = { - DisplayName = "Admin Client", - ClientId = adminClientId, - ClientSecret = identityOptions.AdminClientSecret, - Permissions = - { - Permissions.Endpoints.Token, - Permissions.GrantTypes.ClientCredentials, - Permissions.ResponseTypes.Token, - Permissions.Scopes.Email, - Permissions.Scopes.Profile, - Permissions.Scopes.Roles, - Permissions.Prefixes.Scope + Constants.ScopeApi, - Permissions.Prefixes.Scope + Constants.ScopePermissions - } - }.SetAdmin()); - } + Permissions.Endpoints.Token, + Permissions.GrantTypes.ClientCredentials, + Permissions.ResponseTypes.Token, + Permissions.Scopes.Email, + Permissions.Scopes.Profile, + Permissions.Scopes.Roles, + Permissions.Prefixes.Scope + Constants.ScopeApi, + Permissions.Prefixes.Scope + Constants.ScopePermissions + } + }.SetAdmin()); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerConfiguration.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerConfiguration.cs index 1d8dca5587..12e2fdec82 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerConfiguration.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerConfiguration.cs @@ -10,28 +10,27 @@ using Squidex.Web; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Squidex.Areas.IdentityServer.Config +namespace Squidex.Areas.IdentityServer.Config; + +public static class IdentityServerConfiguration { - public static class IdentityServerConfiguration + public sealed class Scopes : InMemoryScopeStore { - public sealed class Scopes : InMemoryScopeStore + public Scopes() + : base(BuildScopes()) { - public Scopes() - : base(BuildScopes()) - { - } + } - private static IEnumerable<(string, OpenIddictScopeDescriptor)> BuildScopes() + private static IEnumerable<(string, OpenIddictScopeDescriptor)> BuildScopes() + { + yield return (Constants.ScopeApi, new OpenIddictScopeDescriptor { - yield return (Constants.ScopeApi, new OpenIddictScopeDescriptor + Name = Constants.ScopeApi, + Resources = { - Name = Constants.ScopeApi, - Resources = - { - Permissions.Prefixes.Scope + Constants.ScopeApi - } - }); - } + Permissions.Prefixes.Scope + Constants.ScopeApi + } + }); } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index 2a73f00e72..e66ebcd06b 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -24,154 +24,153 @@ using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; -namespace Squidex.Areas.IdentityServer.Config +namespace Squidex.Areas.IdentityServer.Config; + +public static class IdentityServerServices { - public static class IdentityServerServices + public static void AddSquidexIdentityServer(this IServiceCollection services) { - public static void AddSquidexIdentityServer(this IServiceCollection services) + services.Configure<KeyManagementOptions>((c, options) => { - services.Configure<KeyManagementOptions>((c, options) => - { - options.XmlRepository = c.GetRequiredService<IXmlRepository>(); - }); + options.XmlRepository = c.GetRequiredService<IXmlRepository>(); + }); - services.AddDataProtection() - .SetApplicationName("Squidex"); + services.AddDataProtection() + .SetApplicationName("Squidex"); - services.AddIdentity<IdentityUser, IdentityRole>() - .AddDefaultTokenProviders(); + services.AddIdentity<IdentityUser, IdentityRole>() + .AddDefaultTokenProviders(); - services.AddSingletonAs<DefaultXmlRepository>() - .As<IXmlRepository>(); + services.AddSingletonAs<DefaultXmlRepository>() + .As<IXmlRepository>(); - services.AddScopedAs<DefaultUserService>() - .As<IUserService>(); + services.AddScopedAs<DefaultUserService>() + .As<IUserService>(); - services.AddScopedAs<UserClaimsPrincipalFactoryWithEmail>() - .As<IUserClaimsPrincipalFactory<IdentityUser>>(); + services.AddScopedAs<UserClaimsPrincipalFactoryWithEmail>() + .As<IUserClaimsPrincipalFactory<IdentityUser>>(); - services.AddSingletonAs<ApiPermissionUnifier>() - .As<IClaimsTransformation>(); + services.AddSingletonAs<ApiPermissionUnifier>() + .As<IClaimsTransformation>(); - services.AddSingletonAs<TokenStoreInitializer>() - .AsSelf(); + services.AddSingletonAs<TokenStoreInitializer>() + .AsSelf(); - services.AddSingletonAs<CreateAdminInitializer>() - .AsSelf(); + services.AddSingletonAs<CreateAdminInitializer>() + .AsSelf(); - services.ConfigureOptions<DefaultKeyStore>(); + services.ConfigureOptions<DefaultKeyStore>(); - services.Configure<IdentityOptions>(options => - { - options.ClaimsIdentity.UserIdClaimType = Claims.Subject; - options.ClaimsIdentity.UserNameClaimType = Claims.Name; - options.ClaimsIdentity.RoleClaimType = Claims.Role; - }); + services.Configure<IdentityOptions>(options => + { + options.ClaimsIdentity.UserIdClaimType = Claims.Subject; + options.ClaimsIdentity.UserNameClaimType = Claims.Name; + options.ClaimsIdentity.RoleClaimType = Claims.Role; + }); - services.AddOpenIddict() - .AddCore(builder => - { - builder.Services.AddSingletonAs<IdentityServerConfiguration.Scopes>() - .As<IOpenIddictScopeStore<ImmutableScope>>(); + services.AddOpenIddict() + .AddCore(builder => + { + builder.Services.AddSingletonAs<IdentityServerConfiguration.Scopes>() + .As<IOpenIddictScopeStore<ImmutableScope>>(); - builder.Services.AddSingletonAs<DynamicApplicationStore>() - .As<IOpenIddictApplicationStore<ImmutableApplication>>(); + builder.Services.AddSingletonAs<DynamicApplicationStore>() + .As<IOpenIddictApplicationStore<ImmutableApplication>>(); - builder.ReplaceApplicationManager(typeof(ApplicationManager<>)); - }) - .AddServer(builder => - { - builder.AddEventHandler<ProcessSignInContext>(builder => - { - builder.UseSingletonHandler<AlwaysAddTokenHandler>() - .SetOrder(AttachTokenParameters.Descriptor.Order + 1); - }); - - builder.SetAccessTokenLifetime(TimeSpan.FromDays(30)); - - builder.DisableAccessTokenEncryption(); - - builder.RegisterScopes( - Scopes.Email, - Scopes.Profile, - Scopes.Roles, - Constants.ScopeApi, - Constants.ScopePermissions); - - builder.AllowClientCredentialsFlow(); - builder.AllowImplicitFlow(); - builder.AllowAuthorizationCodeFlow(); - - builder.UseAspNetCore() - .DisableTransportSecurityRequirement() - .EnableAuthorizationEndpointPassthrough() - .EnableLogoutEndpointPassthrough() - .EnableStatusCodePagesIntegration() - .EnableTokenEndpointPassthrough() - .EnableUserinfoEndpointPassthrough(); - }) - .AddValidation(options => + builder.ReplaceApplicationManager(typeof(ApplicationManager<>)); + }) + .AddServer(builder => + { + builder.AddEventHandler<ProcessSignInContext>(builder => { - options.UseLocalServer(); - options.UseAspNetCore(); + builder.UseSingletonHandler<AlwaysAddTokenHandler>() + .SetOrder(AttachTokenParameters.Descriptor.Order + 1); }); - services.Configure<AntiforgeryOptions>((c, options) => + builder.SetAccessTokenLifetime(TimeSpan.FromDays(30)); + + builder.DisableAccessTokenEncryption(); + + builder.RegisterScopes( + Scopes.Email, + Scopes.Profile, + Scopes.Roles, + Constants.ScopeApi, + Constants.ScopePermissions); + + builder.AllowClientCredentialsFlow(); + builder.AllowImplicitFlow(); + builder.AllowAuthorizationCodeFlow(); + + builder.UseAspNetCore() + .DisableTransportSecurityRequirement() + .EnableAuthorizationEndpointPassthrough() + .EnableLogoutEndpointPassthrough() + .EnableStatusCodePagesIntegration() + .EnableTokenEndpointPassthrough() + .EnableUserinfoEndpointPassthrough(); + }) + .AddValidation(options => { - var identityOptions = c.GetRequiredService<IOptions<MyIdentityOptions>>().Value; - - options.SuppressXFrameOptionsHeader = identityOptions.SuppressXFrameOptionsHeader; + options.UseLocalServer(); + options.UseAspNetCore(); }); - services.Configure<OpenIddictServerOptions>((c, options) => - { - var urlGenerator = c.GetRequiredService<IUrlGenerator>(); + services.Configure<AntiforgeryOptions>((c, options) => + { + var identityOptions = c.GetRequiredService<IOptions<MyIdentityOptions>>().Value; - var identityPrefix = Constants.PrefixIdentityServer; - var identityOptions = c.GetRequiredService<IOptions<MyIdentityOptions>>().Value; + options.SuppressXFrameOptionsHeader = identityOptions.SuppressXFrameOptionsHeader; + }); - Func<string, Uri> buildUrl; + services.Configure<OpenIddictServerOptions>((c, options) => + { + var urlGenerator = c.GetRequiredService<IUrlGenerator>(); - if (identityOptions.MultipleDomains) - { - buildUrl = url => new Uri($"{identityPrefix}{url}", UriKind.Relative); + var identityPrefix = Constants.PrefixIdentityServer; + var identityOptions = c.GetRequiredService<IOptions<MyIdentityOptions>>().Value; - options.Issuer = new Uri(urlGenerator.BuildUrl()); - } - else - { - buildUrl = url => new Uri(urlGenerator.BuildUrl($"{identityPrefix}{url}", false)); + Func<string, Uri> buildUrl; + + if (identityOptions.MultipleDomains) + { + buildUrl = url => new Uri($"{identityPrefix}{url}", UriKind.Relative); - options.Issuer = new Uri(urlGenerator.BuildUrl(identityPrefix, false)); - } + options.Issuer = new Uri(urlGenerator.BuildUrl()); + } + else + { + buildUrl = url => new Uri(urlGenerator.BuildUrl($"{identityPrefix}{url}", false)); - options.AuthorizationEndpointUris.SetEndpoint( - buildUrl("/connect/authorize")); + options.Issuer = new Uri(urlGenerator.BuildUrl(identityPrefix, false)); + } - options.IntrospectionEndpointUris.SetEndpoint( - buildUrl("/connect/introspect")); + options.AuthorizationEndpointUris.SetEndpoint( + buildUrl("/connect/authorize")); - options.LogoutEndpointUris.SetEndpoint( - buildUrl("/connect/logout")); + options.IntrospectionEndpointUris.SetEndpoint( + buildUrl("/connect/introspect")); - options.TokenEndpointUris.SetEndpoint( - buildUrl("/connect/token")); + options.LogoutEndpointUris.SetEndpoint( + buildUrl("/connect/logout")); - options.UserinfoEndpointUris.SetEndpoint( - buildUrl("/connect/userinfo")); + options.TokenEndpointUris.SetEndpoint( + buildUrl("/connect/token")); - options.CryptographyEndpointUris.SetEndpoint( - buildUrl("/.well-known/jwks")); + options.UserinfoEndpointUris.SetEndpoint( + buildUrl("/connect/userinfo")); - options.ConfigurationEndpointUris.SetEndpoint( - buildUrl("/.well-known/openid-configuration")); - }); - } + options.CryptographyEndpointUris.SetEndpoint( + buildUrl("/.well-known/jwks")); - private static void SetEndpoint(this List<Uri> endpointUris, Uri uri) - { - endpointUris.Clear(); - endpointUris.Add(uri); - } + options.ConfigurationEndpointUris.SetEndpoint( + buildUrl("/.well-known/openid-configuration")); + }); + } + + private static void SetEndpoint(this List<Uri> endpointUris, Uri uri) + { + endpointUris.Clear(); + endpointUris.Add(uri); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs b/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs index 6b0d4fb589..b5ebb2d562 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs @@ -13,66 +13,65 @@ using Squidex.Hosting; using Squidex.Infrastructure.Timers; -namespace Squidex.Areas.IdentityServer.Config +namespace Squidex.Areas.IdentityServer.Config; + +public sealed class TokenStoreInitializer : IInitializable, IBackgroundProcess { - public sealed class TokenStoreInitializer : IInitializable, IBackgroundProcess - { - private readonly OpenIddictMongoDbOptions options; - private readonly IServiceProvider serviceProvider; - private CompletionTimer timer; + private readonly OpenIddictMongoDbOptions options; + private readonly IServiceProvider serviceProvider; + private CompletionTimer timer; - public TokenStoreInitializer(IOptions<OpenIddictMongoDbOptions> options, IServiceProvider serviceProvider) - { - this.options = options.Value; - this.serviceProvider = serviceProvider; - } + public TokenStoreInitializer(IOptions<OpenIddictMongoDbOptions> options, IServiceProvider serviceProvider) + { + this.options = options.Value; + this.serviceProvider = serviceProvider; + } - public async Task InitializeAsync( - CancellationToken ct) - { - await SetupIndexAsync(ct); - } + public async Task InitializeAsync( + CancellationToken ct) + { + await SetupIndexAsync(ct); + } - public async Task StartAsync( - CancellationToken ct) - { - timer = new CompletionTimer((int)TimeSpan.FromHours(6).TotalMilliseconds, PruneAsync); + public async Task StartAsync( + CancellationToken ct) + { + timer = new CompletionTimer((int)TimeSpan.FromHours(6).TotalMilliseconds, PruneAsync); - await PruneAsync(ct); - } + await PruneAsync(ct); + } - public Task StopAsync( - CancellationToken ct) - { - return timer?.StopAsync() ?? Task.CompletedTask; - } + public Task StopAsync( + CancellationToken ct) + { + return timer?.StopAsync() ?? Task.CompletedTask; + } - private async Task PruneAsync( - CancellationToken ct) + private async Task PruneAsync( + CancellationToken ct) + { + await using (var scope = serviceProvider.CreateAsyncScope()) { - await using (var scope = serviceProvider.CreateAsyncScope()) - { - var tokenManager = scope.ServiceProvider.GetRequiredService<IOpenIddictTokenManager>(); + var tokenManager = scope.ServiceProvider.GetRequiredService<IOpenIddictTokenManager>(); - await tokenManager.PruneAsync(DateTimeOffset.UtcNow.AddDays(-40), ct); - } + await tokenManager.PruneAsync(DateTimeOffset.UtcNow.AddDays(-40), ct); } + } - private async Task SetupIndexAsync( - CancellationToken ct) + private async Task SetupIndexAsync( + CancellationToken ct) + { + await using (var scope = serviceProvider.CreateAsyncScope()) { - await using (var scope = serviceProvider.CreateAsyncScope()) - { - var database = await scope.ServiceProvider.GetRequiredService<IOpenIddictMongoDbContext>().GetDatabaseAsync(ct); + var database = await scope.ServiceProvider.GetRequiredService<IOpenIddictMongoDbContext>().GetDatabaseAsync(ct); - var collection = database.GetCollection<OpenIddictMongoDbToken<string>>(options.TokensCollectionName); + var collection = database.GetCollection<OpenIddictMongoDbToken<string>>(options.TokensCollectionName); - await collection.Indexes.CreateOneAsync( - new CreateIndexModel<OpenIddictMongoDbToken<string>>( - Builders<OpenIddictMongoDbToken<string>>.IndexKeys - .Ascending(x => x.ReferenceId)), - cancellationToken: ct); - } + await collection.Indexes.CreateOneAsync( + new CreateIndexModel<OpenIddictMongoDbToken<string>>( + Builders<OpenIddictMongoDbToken<string>>.IndexKeys + .Ascending(x => x.ReferenceId)), + cancellationToken: ct); } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index 42c2c96d6d..aca33f5e2d 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -17,306 +17,305 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.IdentityServer.Controllers.Account +namespace Squidex.Areas.IdentityServer.Controllers.Account; + +public sealed class AccountController : IdentityServerController { - public sealed class AccountController : IdentityServerController + private readonly IUserService userService; + private readonly MyIdentityOptions identityOptions; + + public AccountController( + IUserService userService, + IOptions<MyIdentityOptions> identityOptions) { - private readonly IUserService userService; - private readonly MyIdentityOptions identityOptions; + this.identityOptions = identityOptions.Value; + this.userService = userService; + } - public AccountController( - IUserService userService, - IOptions<MyIdentityOptions> identityOptions) - { - this.identityOptions = identityOptions.Value; - this.userService = userService; - } + [HttpGet] + [Route("account/error/")] + public IActionResult LoginError() + { + throw new InvalidOperationException(); + } - [HttpGet] - [Route("account/error/")] - public IActionResult LoginError() - { - throw new InvalidOperationException(); - } + [HttpGet] + [Route("account/forbidden/")] + public IActionResult Forbidden() + { + throw new DomainForbiddenException(T.Get("users.userLocked")); + } - [HttpGet] - [Route("account/forbidden/")] - public IActionResult Forbidden() - { - throw new DomainForbiddenException(T.Get("users.userLocked")); - } + [HttpGet] + [Route("account/lockedout/")] + public IActionResult LockedOut() + { + return View(); + } + + [HttpGet] + [Route("account/accessdenied/")] + public IActionResult AccessDenied() + { + return View(); + } + + [HttpGet] + [Route("account/logout-completed/")] + public IActionResult LogoutCompleted() + { + return View(); + } + + [HttpGet] + [Route("account/consent/")] + public IActionResult Consent(string? returnUrl = null) + { + return View(new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }); + } - [HttpGet] - [Route("account/lockedout/")] - public IActionResult LockedOut() + [HttpPost] + [Route("account/consent/")] + public async Task<IActionResult> Consent(ConsentModel model, string? returnUrl = null) + { + // We ask new users to agree to the cookie and privacy agreements and show and error if they do not agree. + if (!model.ConsentToCookies) { - return View(); + ModelState.AddModelError(nameof(model.ConsentToCookies), T.Get("users.consent.needed")); } - [HttpGet] - [Route("account/accessdenied/")] - public IActionResult AccessDenied() + if (!model.ConsentToPersonalInformation) { - return View(); + ModelState.AddModelError(nameof(model.ConsentToPersonalInformation), T.Get("users.consent.needed")); } - [HttpGet] - [Route("account/logout-completed/")] - public IActionResult LogoutCompleted() + if (!ModelState.IsValid) { - return View(); + var vm = new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }; + + return View(vm); } - [HttpGet] - [Route("account/consent/")] - public IActionResult Consent(string? returnUrl = null) + var user = await userService.GetAsync(User, HttpContext.RequestAborted); + + // There is almost no case where this could have happened. + if (user == null) { - return View(new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }); + throw new DomainException(T.Get("users.userNotFound")); } - [HttpPost] - [Route("account/consent/")] - public async Task<IActionResult> Consent(ConsentModel model, string? returnUrl = null) + var update = new UserValues { - // We ask new users to agree to the cookie and privacy agreements and show and error if they do not agree. - if (!model.ConsentToCookies) - { - ModelState.AddModelError(nameof(model.ConsentToCookies), T.Get("users.consent.needed")); - } + Consent = true, + ConsentForEmails = model.ConsentToAutomatedEmails + }; - if (!model.ConsentToPersonalInformation) - { - ModelState.AddModelError(nameof(model.ConsentToPersonalInformation), T.Get("users.consent.needed")); - } + await userService.UpdateAsync(user.Id, update, ct: HttpContext.RequestAborted); - if (!ModelState.IsValid) - { - var vm = new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }; + return RedirectToReturnUrl(returnUrl); + } - return View(vm); - } + [HttpGet] + [Route("account/logout/")] + public async Task<IActionResult> Logout(string logoutId) + { + await SignInManager.SignOutAsync(); - var user = await userService.GetAsync(User, HttpContext.RequestAborted); + return Redirect("~/../"); + } - // There is almost no case where this could have happened. - if (user == null) - { - throw new DomainException(T.Get("users.userNotFound")); - } + [HttpGet] + [Route("account/logout-redirect/")] + public async Task<IActionResult> LogoutRedirect() + { + await SignInManager.SignOutAsync(); - var update = new UserValues - { - Consent = true, - ConsentForEmails = model.ConsentToAutomatedEmails - }; + return RedirectToAction(nameof(LogoutCompleted)); + } - await userService.UpdateAsync(user.Id, update, ct: HttpContext.RequestAborted); + [HttpGet] + [Route("account/signup/")] + public Task<IActionResult> Signup(string? returnUrl = null) + { + return LoginViewAsync(returnUrl, false, false); + } - return RedirectToReturnUrl(returnUrl); - } + [HttpGet] + [Route("account/login/")] + [ClearCookies] + public Task<IActionResult> Login(string? returnUrl = null) + { + return LoginViewAsync(returnUrl, true, false); + } - [HttpGet] - [Route("account/logout/")] - public async Task<IActionResult> Logout(string logoutId) + [HttpPost] + [Route("account/login/")] + public async Task<IActionResult> Login(LoginModel model, string? returnUrl = null) + { + if (!ModelState.IsValid) { - await SignInManager.SignOutAsync(); - - return Redirect("~/../"); + return await LoginViewAsync(returnUrl, true, true); } - [HttpGet] - [Route("account/logout-redirect/")] - public async Task<IActionResult> LogoutRedirect() - { - await SignInManager.SignOutAsync(); + var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, true, true); - return RedirectToAction(nameof(LogoutCompleted)); + if (!result.Succeeded && result.IsLockedOut) + { + return View(nameof(LockedOut)); } - - [HttpGet] - [Route("account/signup/")] - public Task<IActionResult> Signup(string? returnUrl = null) + else if (!result.Succeeded) { - return LoginViewAsync(returnUrl, false, false); + return await LoginViewAsync(returnUrl, true, true); } - - [HttpGet] - [Route("account/login/")] - [ClearCookies] - public Task<IActionResult> Login(string? returnUrl = null) + else { - return LoginViewAsync(returnUrl, true, false); + return RedirectToReturnUrl(returnUrl); } + } - [HttpPost] - [Route("account/login/")] - public async Task<IActionResult> Login(LoginModel model, string? returnUrl = null) + private async Task<IActionResult> LoginViewAsync(string? returnUrl, bool isLogin, bool isFailed) + { + // If password authentication is enabled we always show the page. + var allowPasswordAuth = identityOptions.AllowPasswordAuth; + + var externalProviders = await SignInManager.GetExternalProvidersAsync(); + + // If there is only one external authentication provider, we can redirect just directly. + if (externalProviders.Count == 1 && !allowPasswordAuth) { - if (!ModelState.IsValid) - { - return await LoginViewAsync(returnUrl, true, true); - } + var provider = externalProviders[0].AuthenticationScheme; - var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, true, true); + var challengeRedirectUrl = Url.Action(nameof(ExternalCallback)); + var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl); - if (!result.Succeeded && result.IsLockedOut) - { - return View(nameof(LockedOut)); - } - else if (!result.Succeeded) - { - return await LoginViewAsync(returnUrl, true, true); - } - else - { - return RedirectToReturnUrl(returnUrl); - } + // Redirect to the external authentication provider. + return Challenge(challengeProperties, provider); } - private async Task<IActionResult> LoginViewAsync(string? returnUrl, bool isLogin, bool isFailed) + var vm = new LoginVM { - // If password authentication is enabled we always show the page. - var allowPasswordAuth = identityOptions.AllowPasswordAuth; - - var externalProviders = await SignInManager.GetExternalProvidersAsync(); + ExternalProviders = externalProviders, + IsFailed = isFailed, + IsLogin = isLogin, + HasPasswordAuth = allowPasswordAuth, + ReturnUrl = returnUrl + }; + + return View(nameof(Login), vm); + } - // If there is only one external authentication provider, we can redirect just directly. - if (externalProviders.Count == 1 && !allowPasswordAuth) - { - var provider = externalProviders[0].AuthenticationScheme; + [HttpPost] + [Route("account/external/")] + public IActionResult External(string provider, string? returnUrl = null) + { + var challengeRedirectUrl = Url.Action(nameof(ExternalCallback), new { returnUrl }); + var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl); - var challengeRedirectUrl = Url.Action(nameof(ExternalCallback)); - var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl); + return Challenge(challengeProperties, provider); + } - // Redirect to the external authentication provider. - return Challenge(challengeProperties, provider); - } + [HttpGet] + [Route("account/external-callback/")] + public async Task<IActionResult> ExternalCallback(string? returnUrl = null) + { + var login = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(); - var vm = new LoginVM - { - ExternalProviders = externalProviders, - IsFailed = isFailed, - IsLogin = isLogin, - HasPasswordAuth = allowPasswordAuth, - ReturnUrl = returnUrl - }; - - return View(nameof(Login), vm); + if (login == null) + { + return RedirectToAction(nameof(Login)); } - [HttpPost] - [Route("account/external/")] - public IActionResult External(string provider, string? returnUrl = null) - { - var challengeRedirectUrl = Url.Action(nameof(ExternalCallback), new { returnUrl }); - var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl); + var result = await SignInManager.ExternalLoginSignInAsync(login.LoginProvider, login.ProviderKey, true); - return Challenge(challengeProperties, provider); + if (!result.Succeeded && result.IsLockedOut) + { + return View(nameof(LockedOut)); } - [HttpGet] - [Route("account/external-callback/")] - public async Task<IActionResult> ExternalCallback(string? returnUrl = null) - { - var login = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(); + var isLoggedIn = result.Succeeded; + var isLocked = false; - if (login == null) - { - return RedirectToAction(nameof(Login)); - } + IUser? user = null; - var result = await SignInManager.ExternalLoginSignInAsync(login.LoginProvider, login.ProviderKey, true); + if (isLoggedIn) + { + user = await userService.FindByLoginAsync(login.LoginProvider, login.ProviderKey, HttpContext.RequestAborted); + } + else + { + var email = login.Principal.GetEmail(); - if (!result.Succeeded && result.IsLockedOut) + if (string.IsNullOrWhiteSpace(email)) { - return View(nameof(LockedOut)); + throw new DomainException(T.Get("users.noEmailAddress")); } - var isLoggedIn = result.Succeeded; - var isLocked = false; + user = await userService.FindByEmailAsync(email!, HttpContext.RequestAborted); - IUser? user = null; - - if (isLoggedIn) + // User might not have a login or password if the user got invited. + if (user != null && await HasLoginAsync(user)) { - user = await userService.FindByLoginAsync(login.LoginProvider, login.ProviderKey, HttpContext.RequestAborted); + // If we have a login, we reject this user, otherwise you can login to an account you do not own. + user = null; } - else - { - var email = login.Principal.GetEmail(); - - if (string.IsNullOrWhiteSpace(email)) - { - throw new DomainException(T.Get("users.noEmailAddress")); - } - user = await userService.FindByEmailAsync(email!, HttpContext.RequestAborted); - - // User might not have a login or password if the user got invited. - if (user != null && await HasLoginAsync(user)) - { - // If we have a login, we reject this user, otherwise you can login to an account you do not own. - user = null; - } - - if (user == null) + if (user == null) + { + var values = new UserValues { - var values = new UserValues - { - CustomClaims = login.Principal.Claims.GetSquidexClaims().ToList() - }; - - var locked = identityOptions.LockAutomatically; + CustomClaims = login.Principal.Claims.GetSquidexClaims().ToList() + }; - // Try to create a user. If the user exists an exception message is shown to the user. - user = await userService.CreateAsync(email!, values, locked, HttpContext.RequestAborted); - } - - if (user != null) - { - await userService.AddLoginAsync(user.Id, login, HttpContext.RequestAborted); + var locked = identityOptions.LockAutomatically; - // Login might fail if the user is locked out. - (isLoggedIn, isLocked) = await LoginAsync(login); - } + // Try to create a user. If the user exists an exception message is shown to the user. + user = await userService.CreateAsync(email!, values, locked, HttpContext.RequestAborted); } - if (isLocked) - { - return View(nameof(LockedOut)); - } - else if (!isLoggedIn) - { - return RedirectToAction(nameof(Login)); - } - else if (user != null && !user.Claims.HasConsent() && !identityOptions.NoConsent) + if (user != null) { - // This should actually never happen, because user should not be null, when logged in. - return RedirectToAction(nameof(Consent), new { returnUrl }); - } - else - { - return RedirectToReturnUrl(returnUrl); + await userService.AddLoginAsync(user.Id, login, HttpContext.RequestAborted); + + // Login might fail if the user is locked out. + (isLoggedIn, isLocked) = await LoginAsync(login); } } - private async Task<bool> HasLoginAsync(IUser user) + if (isLocked) { - if (await userService.HasPasswordAsync(user, HttpContext.RequestAborted)) - { - return true; - } - - var logins = await userService.GetLoginsAsync(user, HttpContext.RequestAborted); - - return logins.Count > 0; + return View(nameof(LockedOut)); } - - private async Task<(bool Success, bool Locked)> LoginAsync(UserLoginInfo externalLogin) + else if (!isLoggedIn) { - var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); + return RedirectToAction(nameof(Login)); + } + else if (user != null && !user.Claims.HasConsent() && !identityOptions.NoConsent) + { + // This should actually never happen, because user should not be null, when logged in. + return RedirectToAction(nameof(Consent), new { returnUrl }); + } + else + { + return RedirectToReturnUrl(returnUrl); + } + } - return (result.Succeeded, result.IsLockedOut); + private async Task<bool> HasLoginAsync(IUser user) + { + if (await userService.HasPasswordAsync(user, HttpContext.RequestAborted)) + { + return true; } + + var logins = await userService.GetLoginsAsync(user, HttpContext.RequestAborted); + + return logins.Count > 0; + } + + private async Task<(bool Success, bool Locked)> LoginAsync(UserLoginInfo externalLogin) + { + var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); + + return (result.Succeeded, result.IsLockedOut); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs index bcc77c7e57..3c738af6fe 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.IdentityServer.Controllers.Account +namespace Squidex.Areas.IdentityServer.Controllers.Account; + +public sealed class ConsentModel { - public sealed class ConsentModel - { - public bool ConsentToPersonalInformation { get; set; } + public bool ConsentToPersonalInformation { get; set; } - public bool ConsentToAutomatedEmails { get; set; } + public bool ConsentToAutomatedEmails { get; set; } - public bool ConsentToCookies { get; set; } - } + public bool ConsentToCookies { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs index 92aba14ae3..b65ecbbdfa 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.IdentityServer.Controllers.Account +namespace Squidex.Areas.IdentityServer.Controllers.Account; + +public sealed class ConsentVM { - public sealed class ConsentVM - { - public string? ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } - public string? PrivacyUrl { get; set; } - } + public string? PrivacyUrl { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs index 8e0634cc7f..0cf5296d6c 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.IdentityServer.Controllers.Account +namespace Squidex.Areas.IdentityServer.Controllers.Account; + +public sealed class LoginModel { - public sealed class LoginModel - { - [LocalizedRequired] - public string Email { get; set; } + [LocalizedRequired] + public string Email { get; set; } - [LocalizedRequired] - public string Password { get; set; } - } + [LocalizedRequired] + public string Password { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs index c3c63217fa..52e24a89f7 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs @@ -5,20 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.IdentityServer.Controllers.Account +namespace Squidex.Areas.IdentityServer.Controllers.Account; + +public class LoginVM { - public class LoginVM - { - public string? ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } - public bool IsLogin { get; set; } + public bool IsLogin { get; set; } - public bool IsFailed { get; set; } + public bool IsFailed { get; set; } - public bool HasPasswordAuth { get; set; } + public bool HasPasswordAuth { get; set; } - public bool HasExternalLogin => ExternalProviders.Any(); + public bool HasExternalLogin => ExternalProviders.Any(); - public IReadOnlyList<ExternalProvider> ExternalProviders { get; set; } - } + public IReadOnlyList<ExternalProvider> ExternalProviders { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/AuthorizationController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/AuthorizationController.cs index 4b3160ede4..79652bc7a3 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/AuthorizationController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/AuthorizationController.cs @@ -22,258 +22,257 @@ using Squidex.Web; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Notifo.Areas.Account.Controllers.Connect +namespace Notifo.Areas.Account.Controllers.Connect; + +public class AuthorizationController : IdentityServerController { - public class AuthorizationController : IdentityServerController + private readonly IOpenIddictScopeManager scopeManager; + private readonly IOpenIddictApplicationManager applicationManager; + private readonly IUserService userService; + + public AuthorizationController( + IOpenIddictScopeManager scopeManager, + IOpenIddictApplicationManager applicationManager, + IUserService userService) + { + this.scopeManager = scopeManager; + this.applicationManager = applicationManager; + this.userService = userService; + } + + [HttpPost("connect/token")] + [Produces("application/json")] + public async Task<IActionResult> Exchange() { - private readonly IOpenIddictScopeManager scopeManager; - private readonly IOpenIddictApplicationManager applicationManager; - private readonly IUserService userService; - - public AuthorizationController( - IOpenIddictScopeManager scopeManager, - IOpenIddictApplicationManager applicationManager, - IUserService userService) + var request = HttpContext.GetOpenIddictServerRequest(); + if (request == null) { - this.scopeManager = scopeManager; - this.applicationManager = applicationManager; - this.userService = userService; + ThrowHelper.InvalidOperationException("The OpenID Connect request cannot be retrieved."); + return default!; } - [HttpPost("connect/token")] - [Produces("application/json")] - public async Task<IActionResult> Exchange() + if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType() || request.IsImplicitFlow()) { - var request = HttpContext.GetOpenIddictServerRequest(); - if (request == null) + var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; + if (principal == null) { - ThrowHelper.InvalidOperationException("The OpenID Connect request cannot be retrieved."); + ThrowHelper.InvalidOperationException("The user details cannot be retrieved."); return default!; } - if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType() || request.IsImplicitFlow()) + var user = await userService.GetAsync(principal, HttpContext.RequestAborted); + if (user == null) { - var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; - if (principal == null) - { - ThrowHelper.InvalidOperationException("The user details cannot be retrieved."); - return default!; - } - - var user = await userService.GetAsync(principal, HttpContext.RequestAborted); - if (user == null) - { - return Forbid( - new AuthenticationProperties(new Dictionary<string, string?> - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." - }), - OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - - if (!await SignInManager.CanSignInAsync((IdentityUser)user.Identity)) - { - return Forbid( - new AuthenticationProperties(new Dictionary<string, string?> - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." - }), - OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - - foreach (var claim in principal.Claims) - { - claim.SetDestinations(GetDestinations(claim, principal, false)); - } - - return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + return Forbid( + new AuthenticationProperties(new Dictionary<string, string?> + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." + }), + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } - if (request.IsClientCredentialsGrantType()) + if (!await SignInManager.CanSignInAsync((IdentityUser)user.Identity)) { - if (request.ClientId == null) - { - ThrowHelper.InvalidOperationException("The OpenID Connect request cannot be retrieved."); - return default!; - } - - var application = await applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted); - if (application == null) - { - ThrowHelper.InvalidOperationException("The application details cannot be found in the database."); - return default!; - } - - var principal = await CreateApplicationPrincipalAsync(request, application); + return Forbid( + new AuthenticationProperties(new Dictionary<string, string?> + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." + }), + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } - return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + foreach (var claim in principal.Claims) + { + claim.SetDestinations(GetDestinations(claim, principal, false)); } - ThrowHelper.InvalidOperationException("The specified grant type is not supported."); - return default!; + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } - [HttpGet("connect/authorize")] - public async Task<IActionResult> Authorize() + if (request.IsClientCredentialsGrantType()) { - var request = HttpContext.GetOpenIddictServerRequest(); - if (request == null) + if (request.ClientId == null) { ThrowHelper.InvalidOperationException("The OpenID Connect request cannot be retrieved."); return default!; } - if (User.Identity?.IsAuthenticated != true) + var application = await applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted); + if (application == null) { - if (request.HasPrompt(Prompts.None)) - { - var properties = new AuthenticationProperties(new Dictionary<string, string?> - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." - }); - - return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } + ThrowHelper.InvalidOperationException("The application details cannot be found in the database."); + return default!; + } - var query = QueryString.Create( - Request.HasFormContentType ? - Request.Form.ToList() : - Request.Query.ToList()); + var principal = await CreateApplicationPrincipalAsync(request, application); - var redirectUri = Request.PathBase + Request.Path + query; + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } - return Challenge( - new AuthenticationProperties - { - RedirectUri = redirectUri - }); - } + ThrowHelper.InvalidOperationException("The specified grant type is not supported."); + return default!; + } - var user = await userService.GetAsync(User, HttpContext.RequestAborted); + [HttpGet("connect/authorize")] + public async Task<IActionResult> Authorize() + { + var request = HttpContext.GetOpenIddictServerRequest(); + if (request == null) + { + ThrowHelper.InvalidOperationException("The OpenID Connect request cannot be retrieved."); + return default!; + } - if (user == null) + if (User.Identity?.IsAuthenticated != true) + { + if (request.HasPrompt(Prompts.None)) { - ThrowHelper.InvalidOperationException("The user details cannot be retrieved."); - return default!; + var properties = new AuthenticationProperties(new Dictionary<string, string?> + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." + }); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } - var principal = await CreatePrincipalAsync(request, user); + var query = QueryString.Create( + Request.HasFormContentType ? + Request.Form.ToList() : + Request.Query.ToList()); - return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + var redirectUri = Request.PathBase + Request.Path + query; + + return Challenge( + new AuthenticationProperties + { + RedirectUri = redirectUri + }); } - [HttpGet("connect/logout")] - public async Task<IActionResult> Logout() - { - await SignInManager.SignOutAsync(); + var user = await userService.GetAsync(User, HttpContext.RequestAborted); - return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + if (user == null) + { + ThrowHelper.InvalidOperationException("The user details cannot be retrieved."); + return default!; } - private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, IUser user) - { - var principal = await SignInManager.CreateUserPrincipalAsync((IdentityUser)user.Identity); + var principal = await CreatePrincipalAsync(request, user); - return await EnrichPrincipalAsync(principal, request, false); - } + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } - private async Task<ClaimsPrincipal> CreateApplicationPrincipalAsync(OpenIddictRequest request, object application) - { - var identity = new ClaimsIdentity( - TokenValidationParameters.DefaultAuthenticationType, - Claims.Name, - Claims.Role); + [HttpGet("connect/logout")] + public async Task<IActionResult> Logout() + { + await SignInManager.SignOutAsync(); - var principal = new ClaimsPrincipal(identity); + return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } - if (request.ClientId != null) - { - identity.AddClaim(Claims.Subject, request.ClientId, - Destinations.AccessToken, - Destinations.IdentityToken); - } + private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, IUser user) + { + var principal = await SignInManager.CreateUserPrincipalAsync((IdentityUser)user.Identity); - var properties = await applicationManager.GetPropertiesAsync(application, HttpContext.RequestAborted); + return await EnrichPrincipalAsync(principal, request, false); + } - foreach (var claim in properties.Claims()) - { - identity.AddClaim(claim); - } + private async Task<ClaimsPrincipal> CreateApplicationPrincipalAsync(OpenIddictRequest request, object application) + { + var identity = new ClaimsIdentity( + TokenValidationParameters.DefaultAuthenticationType, + Claims.Name, + Claims.Role); + + var principal = new ClaimsPrincipal(identity); - return await EnrichPrincipalAsync(principal, request, true); + if (request.ClientId != null) + { + identity.AddClaim(Claims.Subject, request.ClientId, + Destinations.AccessToken, + Destinations.IdentityToken); } - private async Task<ClaimsPrincipal> EnrichPrincipalAsync(ClaimsPrincipal principal, OpenIddictRequest request, bool alwaysDeliverPermissions) + var properties = await applicationManager.GetPropertiesAsync(application, HttpContext.RequestAborted); + + foreach (var claim in properties.Claims()) { - var scopes = request.GetScopes(); + identity.AddClaim(claim); + } - var resources = await scopeManager.ListResourcesAsync(scopes, HttpContext.RequestAborted).ToListAsync(HttpContext.RequestAborted); + return await EnrichPrincipalAsync(principal, request, true); + } - principal.SetScopes(scopes); - principal.SetResources(resources); + private async Task<ClaimsPrincipal> EnrichPrincipalAsync(ClaimsPrincipal principal, OpenIddictRequest request, bool alwaysDeliverPermissions) + { + var scopes = request.GetScopes(); - foreach (var claim in principal.Claims) - { - claim.SetDestinations(GetDestinations(claim, principal, alwaysDeliverPermissions)); - } + var resources = await scopeManager.ListResourcesAsync(scopes, HttpContext.RequestAborted).ToListAsync(HttpContext.RequestAborted); - return principal; + principal.SetScopes(scopes); + principal.SetResources(resources); + + foreach (var claim in principal.Claims) + { + claim.SetDestinations(GetDestinations(claim, principal, alwaysDeliverPermissions)); } - private static IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal, bool alwaysDeliverPermissions) + return principal; + } + + private static IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal, bool alwaysDeliverPermissions) + { + switch (claim.Type) { - switch (claim.Type) - { - case SquidexClaimTypes.DisplayName: - yield return Destinations.IdentityToken; - yield break; + case SquidexClaimTypes.DisplayName: + yield return Destinations.IdentityToken; + yield break; - case SquidexClaimTypes.PictureUrl when principal.HasScope(Scopes.Profile): - yield return Destinations.IdentityToken; - yield break; + case SquidexClaimTypes.PictureUrl when principal.HasScope(Scopes.Profile): + yield return Destinations.IdentityToken; + yield break; - case SquidexClaimTypes.NotifoKey when principal.HasScope(Scopes.Profile): - yield return Destinations.IdentityToken; - yield break; + case SquidexClaimTypes.NotifoKey when principal.HasScope(Scopes.Profile): + yield return Destinations.IdentityToken; + yield break; - case SquidexClaimTypes.Permissions when principal.HasScope(Constants.ScopePermissions) || alwaysDeliverPermissions: - yield return Destinations.AccessToken; - yield return Destinations.IdentityToken; - yield break; + case SquidexClaimTypes.Permissions when principal.HasScope(Constants.ScopePermissions) || alwaysDeliverPermissions: + yield return Destinations.AccessToken; + yield return Destinations.IdentityToken; + yield break; - case Claims.Name: - yield return Destinations.AccessToken; + case Claims.Name: + yield return Destinations.AccessToken; - if (principal.HasScope(Scopes.Profile)) - { - yield return Destinations.IdentityToken; - } + if (principal.HasScope(Scopes.Profile)) + { + yield return Destinations.IdentityToken; + } - yield break; + yield break; - case Claims.Email: - yield return Destinations.AccessToken; + case Claims.Email: + yield return Destinations.AccessToken; - if (principal.HasScope(Scopes.Email)) - { - yield return Destinations.IdentityToken; - } + if (principal.HasScope(Scopes.Email)) + { + yield return Destinations.IdentityToken; + } - yield break; + yield break; - case Claims.Role: - yield return Destinations.AccessToken; + case Claims.Role: + yield return Destinations.AccessToken; - if (principal.HasScope(Scopes.Roles)) - { - yield return Destinations.IdentityToken; - } + if (principal.HasScope(Scopes.Roles)) + { + yield return Destinations.IdentityToken; + } - yield break; - } + yield break; } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs index 5549d9e603..25d36d41c7 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs @@ -10,37 +10,36 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Infrastructure; -namespace Squidex.Areas.IdentityServer.Controllers.Error +namespace Squidex.Areas.IdentityServer.Controllers.Error; + +public sealed class ErrorController : IdentityServerController { - public sealed class ErrorController : IdentityServerController + [Route("error/")] + public async Task<IActionResult> Error(string? errorId = null) { - [Route("error/")] - public async Task<IActionResult> Error(string? errorId = null) - { - await SignInManager.SignOutAsync(); + await SignInManager.SignOutAsync(); - var vm = new ErrorVM(); + var vm = new ErrorVM(); - var response = HttpContext.GetOpenIddictServerResponse(); + var response = HttpContext.GetOpenIddictServerResponse(); - vm.ErrorMessage = response?.ErrorDescription; - vm.ErrorCode = response?.Error; + vm.ErrorMessage = response?.ErrorDescription; + vm.ErrorCode = response?.Error; + + if (string.IsNullOrWhiteSpace(vm.ErrorMessage)) + { + var exception = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error; - if (string.IsNullOrWhiteSpace(vm.ErrorMessage)) + if (exception is DomainException domainException1) { - var exception = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error; - - if (exception is DomainException domainException1) - { - vm.ErrorMessage = domainException1.Message; - } - else if (exception?.InnerException is DomainException domainException2) - { - vm.ErrorMessage = domainException2.Message; - } + vm.ErrorMessage = domainException1.Message; + } + else if (exception?.InnerException is DomainException domainException2) + { + vm.ErrorMessage = domainException2.Message; } - - return View("Error", vm); } + + return View("Error", vm); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs index 9985a0ccfb..8cb232362f 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.IdentityServer.Controllers.Error +namespace Squidex.Areas.IdentityServer.Controllers.Error; + +public class ErrorVM { - public class ErrorVM - { - public string? ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } - public string? ErrorCode { get; set; } = "400"; - } + public string? ErrorCode { get; set; } = "400"; } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs index c354b4cfb6..7457e07901 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs @@ -11,44 +11,43 @@ using Squidex.Infrastructure.Security; using Squidex.Web; -namespace Squidex.Areas.IdentityServer.Controllers +namespace Squidex.Areas.IdentityServer.Controllers; + +public static class Extensions { - public static class Extensions + public static async Task<ExternalLoginInfo> GetExternalLoginInfoWithDisplayNameAsync(this SignInManager<IdentityUser> signInManager, string? expectedXsrf = null) { - public static async Task<ExternalLoginInfo> GetExternalLoginInfoWithDisplayNameAsync(this SignInManager<IdentityUser> signInManager, string? expectedXsrf = null) - { - var login = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); + var login = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); - if (login == null) - { - ThrowHelper.InvalidOperationException("Request from external provider cannot be handled."); - return default!; - } + if (login == null) + { + ThrowHelper.InvalidOperationException("Request from external provider cannot be handled."); + return default!; + } - var email = login.Principal.GetEmail(); + var email = login.Principal.GetEmail(); - if (string.IsNullOrWhiteSpace(email)) - { - ThrowHelper.InvalidOperationException("External provider does not provide email claim."); - return default!; - } + if (string.IsNullOrWhiteSpace(email)) + { + ThrowHelper.InvalidOperationException("External provider does not provide email claim."); + return default!; + } - login.ProviderDisplayName = email; + login.ProviderDisplayName = email; - return login; - } + return login; + } - public static async Task<List<ExternalProvider>> GetExternalProvidersAsync(this SignInManager<IdentityUser> signInManager) - { - var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); + public static async Task<List<ExternalProvider>> GetExternalProvidersAsync(this SignInManager<IdentityUser> signInManager) + { + var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); - var externalProviders = externalSchemes - .Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme) - .Where(x => x.Name != Constants.ApiSecurityScheme) - .Select(x => new ExternalProvider(x.Name, x.DisplayName ?? x.Name)) - .ToList(); + var externalProviders = externalSchemes + .Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme) + .Where(x => x.Name != Constants.ApiSecurityScheme) + .Select(x => new ExternalProvider(x.Name, x.DisplayName ?? x.Name)) + .ToList(); - return externalProviders; - } + return externalProviders; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs index 5deb021e2b..14da3b58dd 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs @@ -5,19 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.IdentityServer.Controllers +namespace Squidex.Areas.IdentityServer.Controllers; + +public class ExternalProvider { - public class ExternalProvider - { - public string DisplayName { get; } + public string DisplayName { get; } - public string AuthenticationScheme { get; } + public string AuthenticationScheme { get; } - public ExternalProvider(string authenticationSchema, string displayName) - { - AuthenticationScheme = authenticationSchema; + public ExternalProvider(string authenticationSchema, string displayName) + { + AuthenticationScheme = authenticationSchema; - DisplayName = displayName; - } + DisplayName = displayName; } } \ No newline at end of file diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs index 595a212446..20e45e6aa0 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs @@ -10,32 +10,31 @@ using Squidex.Hosting; using Squidex.Web; -namespace Squidex.Areas.IdentityServer.Controllers +namespace Squidex.Areas.IdentityServer.Controllers; + +[Area("IdentityServer")] +[Route(Constants.PrefixIdentityServer)] +public abstract class IdentityServerController : Controller { - [Area("IdentityServer")] - [Route(Constants.PrefixIdentityServer)] - public abstract class IdentityServerController : Controller + public SignInManager<IdentityUser> SignInManager { - public SignInManager<IdentityUser> SignInManager - { - get => HttpContext.RequestServices.GetRequiredService<SignInManager<IdentityUser>>(); - } + get => HttpContext.RequestServices.GetRequiredService<SignInManager<IdentityUser>>(); + } - protected IActionResult RedirectToReturnUrl(string? returnUrl) + protected IActionResult RedirectToReturnUrl(string? returnUrl) + { + if (string.IsNullOrWhiteSpace(returnUrl)) { - if (string.IsNullOrWhiteSpace(returnUrl)) - { - return Redirect("~/../"); - } - - var urlGenerator = HttpContext.RequestServices.GetRequiredService<IUrlGenerator>(); + return Redirect("~/../"); + } - if (urlGenerator.IsAllowedHost(returnUrl)) - { - return Redirect(returnUrl); - } + var urlGenerator = HttpContext.RequestServices.GetRequiredService<IUrlGenerator>(); - return Redirect("~/../"); + if (urlGenerator.IsAllowedHost(returnUrl)) + { + return Redirect(returnUrl); } + + return Redirect("~/../"); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Info/InfoController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Info/InfoController.cs index 3bd2ad230d..2b5de75e84 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Info/InfoController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Info/InfoController.cs @@ -8,17 +8,16 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Shared.Identity; -namespace Squidex.Areas.IdentityServer.Controllers.Info +namespace Squidex.Areas.IdentityServer.Controllers.Info; + +public sealed class InfoController : IdentityServerController { - public sealed class InfoController : IdentityServerController + [Route("info")] + [HttpGet] + public IActionResult Info() { - [Route("info")] - [HttpGet] - public IActionResult Info() - { - var displayName = User.Claims.DisplayName(); + var displayName = User.Claims.DisplayName(); - return Ok(new { displayName }); - } + return Ok(new { displayName }); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs index 9c8e649511..0afa7c6ca7 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.IdentityServer.Controllers.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile; + +public class ChangePasswordModel { - public class ChangePasswordModel - { - [LocalizedRequired] - public string OldPassword { get; set; } + [LocalizedRequired] + public string OldPassword { get; set; } - [LocalizedRequired] - public string Password { get; set; } + [LocalizedRequired] + public string Password { get; set; } - [LocalizedCompare(nameof(Password))] - public string PasswordConfirm { get; set; } - } + [LocalizedCompare(nameof(Password))] + public string PasswordConfirm { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs index a72b5d6245..2830cb09ae 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs @@ -8,21 +8,20 @@ using Squidex.Domain.Users; using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.IdentityServer.Controllers.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile; + +public class ChangeProfileModel { - public class ChangeProfileModel - { - [LocalizedRequired] - public string Email { get; set; } + [LocalizedRequired] + public string Email { get; set; } - [LocalizedRequired] - public string DisplayName { get; set; } + [LocalizedRequired] + public string DisplayName { get; set; } - public bool IsHidden { get; set; } + public bool IsHidden { get; set; } - public UserValues ToValues() - { - return new UserValues { Email = Email, DisplayName = DisplayName, Hidden = IsHidden }; - } + public UserValues ToValues() + { + return new UserValues { Email = Email, DisplayName = DisplayName, Hidden = IsHidden }; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePropertiesModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePropertiesModel.cs index 1355733335..2d11c95afe 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePropertiesModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePropertiesModel.cs @@ -7,17 +7,16 @@ using Squidex.Domain.Users; -namespace Squidex.Areas.IdentityServer.Controllers.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile; + +public class ChangePropertiesModel { - public class ChangePropertiesModel - { - public List<UserProperty> Properties { get; set; } + public List<UserProperty> Properties { get; set; } - public UserValues ToValues() - { - var properties = Properties?.Select(x => x.ToTuple()).ToList() ?? new List<(string Name, string Value)>(); + public UserValues ToValues() + { + var properties = Properties?.Select(x => x.ToTuple()).ToList() ?? new List<(string Name, string Value)>(); - return new UserValues { Properties = properties }; - } + return new UserValues { Properties = properties }; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index 8bcb52de2a..45c7c2e80a 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -22,251 +22,250 @@ using Squidex.Shared.Users; using Squidex.Web; -namespace Squidex.Areas.IdentityServer.Controllers.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile; + +[Authorize] +public sealed class ProfileController : IdentityServerController { - [Authorize] - public sealed class ProfileController : IdentityServerController + private readonly IUserPictureStore userPictureStore; + private readonly IUserService userService; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly MyIdentityOptions identityOptions; + + public ProfileController( + IOptions<MyIdentityOptions> identityOptions, + IUserPictureStore userPictureStore, + IUserService userService, + IAssetThumbnailGenerator assetThumbnailGenerator) { - private readonly IUserPictureStore userPictureStore; - private readonly IUserService userService; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly MyIdentityOptions identityOptions; - - public ProfileController( - IOptions<MyIdentityOptions> identityOptions, - IUserPictureStore userPictureStore, - IUserService userService, - IAssetThumbnailGenerator assetThumbnailGenerator) - { - this.identityOptions = identityOptions.Value; - this.userPictureStore = userPictureStore; - this.userService = userService; - this.assetThumbnailGenerator = assetThumbnailGenerator; - } + this.identityOptions = identityOptions.Value; + this.userPictureStore = userPictureStore; + this.userService = userService; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } - [HttpGet] - [Route("account/profile/")] - public async Task<IActionResult> Profile(string? successMessage = null) - { - var user = await userService.GetAsync(User, HttpContext.RequestAborted); + [HttpGet] + [Route("account/profile/")] + public async Task<IActionResult> Profile(string? successMessage = null) + { + var user = await userService.GetAsync(User, HttpContext.RequestAborted); - return View(await GetVM<None>(user, successMessage: successMessage)); - } + return View(await GetVM<None>(user, successMessage: successMessage)); + } - [HttpPost] - [Route("account/profile/login-add/")] - public async Task<IActionResult> AddLogin(string provider) - { - await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + [HttpPost] + [Route("account/profile/login-add/")] + public async Task<IActionResult> AddLogin(string provider) + { + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - var userId = userService.GetUserId(User, HttpContext.RequestAborted); + var userId = userService.GetUserId(User, HttpContext.RequestAborted); - var challengeRedirectUrl = Url.Action(nameof(AddLoginCallback)); - var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl, userId); + var challengeRedirectUrl = Url.Action(nameof(AddLoginCallback)); + var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl, userId); - return Challenge(challengeProperties, provider); - } + return Challenge(challengeProperties, provider); + } - [HttpGet] - [Route("account/profile/login-add-callback/")] - public Task<IActionResult> AddLoginCallback() - { - return MakeChangeAsync((id, ct) => AddLoginAsync(id, ct), - T.Get("users.profile.addLoginDone"), None.Value); - } + [HttpGet] + [Route("account/profile/login-add-callback/")] + public Task<IActionResult> AddLoginCallback() + { + return MakeChangeAsync((id, ct) => AddLoginAsync(id, ct), + T.Get("users.profile.addLoginDone"), None.Value); + } - [HttpPost] - [Route("account/profile/update/")] - public Task<IActionResult> UpdateProfile(ChangeProfileModel model) - { - return MakeChangeAsync((id, ct) => userService.UpdateAsync(id, model.ToValues(), ct: ct), - T.Get("users.profile.updateProfileDone"), model); - } + [HttpPost] + [Route("account/profile/update/")] + public Task<IActionResult> UpdateProfile(ChangeProfileModel model) + { + return MakeChangeAsync((id, ct) => userService.UpdateAsync(id, model.ToValues(), ct: ct), + T.Get("users.profile.updateProfileDone"), model); + } - [HttpPost] - [Route("account/profile/properties/")] - public Task<IActionResult> UpdateProperties(ChangePropertiesModel model) - { - return MakeChangeAsync((id, ct) => userService.UpdateAsync(id, model.ToValues(), ct: ct), - T.Get("users.profile.updatePropertiesDone"), model); - } + [HttpPost] + [Route("account/profile/properties/")] + public Task<IActionResult> UpdateProperties(ChangePropertiesModel model) + { + return MakeChangeAsync((id, ct) => userService.UpdateAsync(id, model.ToValues(), ct: ct), + T.Get("users.profile.updatePropertiesDone"), model); + } - [HttpPost] - [Route("account/profile/login-remove/")] - public Task<IActionResult> RemoveLogin(RemoveLoginModel model) - { - return MakeChangeAsync((id, ct) => userService.RemoveLoginAsync(id, model.LoginProvider, model.ProviderKey, ct), - T.Get("users.profile.removeLoginDone"), model); - } + [HttpPost] + [Route("account/profile/login-remove/")] + public Task<IActionResult> RemoveLogin(RemoveLoginModel model) + { + return MakeChangeAsync((id, ct) => userService.RemoveLoginAsync(id, model.LoginProvider, model.ProviderKey, ct), + T.Get("users.profile.removeLoginDone"), model); + } - [HttpPost] - [Route("account/profile/password-set/")] - public Task<IActionResult> SetPassword(SetPasswordModel model) - { - return MakeChangeAsync((id, ct) => userService.SetPasswordAsync(id, model.Password, ct: ct), - T.Get("users.profile.setPasswordDone"), model); - } + [HttpPost] + [Route("account/profile/password-set/")] + public Task<IActionResult> SetPassword(SetPasswordModel model) + { + return MakeChangeAsync((id, ct) => userService.SetPasswordAsync(id, model.Password, ct: ct), + T.Get("users.profile.setPasswordDone"), model); + } - [HttpPost] - [Route("account/profile/password-change/")] - public Task<IActionResult> ChangePassword(ChangePasswordModel model) - { - return MakeChangeAsync((id, ct) => userService.SetPasswordAsync(id, model.Password, model.OldPassword, ct), - T.Get("users.profile.changePasswordDone"), model); - } + [HttpPost] + [Route("account/profile/password-change/")] + public Task<IActionResult> ChangePassword(ChangePasswordModel model) + { + return MakeChangeAsync((id, ct) => userService.SetPasswordAsync(id, model.Password, model.OldPassword, ct), + T.Get("users.profile.changePasswordDone"), model); + } - [HttpPost] - [Route("account/profile/generate-client-secret/")] - public Task<IActionResult> GenerateClientSecret() - { - return MakeChangeAsync((id, ct) => GenerateClientSecretAsync(id, ct), - T.Get("users.profile.generateClientDone"), None.Value); - } + [HttpPost] + [Route("account/profile/generate-client-secret/")] + public Task<IActionResult> GenerateClientSecret() + { + return MakeChangeAsync((id, ct) => GenerateClientSecretAsync(id, ct), + T.Get("users.profile.generateClientDone"), None.Value); + } - [HttpPost] - [Route("account/profile/upload-picture/")] - public Task<IActionResult> UploadPicture(List<IFormFile> file) - { - return MakeChangeAsync((id, ct) => UpdatePictureAsync(file, id, ct), - T.Get("users.profile.uploadPictureDone"), None.Value); - } + [HttpPost] + [Route("account/profile/upload-picture/")] + public Task<IActionResult> UploadPicture(List<IFormFile> file) + { + return MakeChangeAsync((id, ct) => UpdatePictureAsync(file, id, ct), + T.Get("users.profile.uploadPictureDone"), None.Value); + } - private async Task GenerateClientSecretAsync(string id, - CancellationToken ct) - { - var update = new UserValues { ClientSecret = RandomHash.New() }; + private async Task GenerateClientSecretAsync(string id, + CancellationToken ct) + { + var update = new UserValues { ClientSecret = RandomHash.New() }; - await userService.UpdateAsync(id, update, ct: ct); - } + await userService.UpdateAsync(id, update, ct: ct); + } - private async Task AddLoginAsync(string id, - CancellationToken ct) - { - var login = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(id); + private async Task AddLoginAsync(string id, + CancellationToken ct) + { + var login = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(id); - await userService.AddLoginAsync(id, login, ct); - } + await userService.AddLoginAsync(id, login, ct); + } - private async Task UpdatePictureAsync(List<IFormFile> files, string id, - CancellationToken ct) + private async Task UpdatePictureAsync(List<IFormFile> files, string id, + CancellationToken ct) + { + if (files.Count != 1) { - if (files.Count != 1) - { - throw new ValidationException(T.Get("validation.onlyOneFile")); - } + throw new ValidationException(T.Get("validation.onlyOneFile")); + } - await UploadResizedAsync(files[0], id, ct); + await UploadResizedAsync(files[0], id, ct); - var update = new UserValues - { - PictureUrl = SquidexClaimTypes.PictureUrlStore - }; + var update = new UserValues + { + PictureUrl = SquidexClaimTypes.PictureUrlStore + }; - await userService.UpdateAsync(id, update, ct: ct); - } + await userService.UpdateAsync(id, update, ct: ct); + } - private async Task UploadResizedAsync(IFormFile file, string id, - CancellationToken ct) - { - await using var assetResized = TempAssetFile.Create(file.ToAssetFile()); + private async Task UploadResizedAsync(IFormFile file, string id, + CancellationToken ct) + { + await using var assetResized = TempAssetFile.Create(file.ToAssetFile()); - var resizeOptions = new ResizeOptions - { - TargetWidth = 128, - TargetHeight = 128 - }; + var resizeOptions = new ResizeOptions + { + TargetWidth = 128, + TargetHeight = 128 + }; - try + try + { + await using (var originalStream = file.OpenReadStream()) { - await using (var originalStream = file.OpenReadStream()) + await using (var resizeStream = assetResized.OpenWrite()) { - await using (var resizeStream = assetResized.OpenWrite()) - { - await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, file.ContentType, resizeStream, resizeOptions, ct); - } + await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, file.ContentType, resizeStream, resizeOptions, ct); } } - catch - { - throw new ValidationException(T.Get("validation.notAnImage")); - } - - await using (var resizeStream = assetResized.OpenWrite()) - { - await userPictureStore.UploadAsync(id, resizeStream, ct); - } + } + catch + { + throw new ValidationException(T.Get("validation.notAnImage")); } - private async Task<IActionResult> MakeChangeAsync<TModel>(Func<string, CancellationToken, Task> action, string successMessage, TModel? model = null) where TModel : class + await using (var resizeStream = assetResized.OpenWrite()) { - var user = await userService.GetAsync(User, HttpContext.RequestAborted); + await userPictureStore.UploadAsync(id, resizeStream, ct); + } + } - if (user == null) - { - return NotFound(); - } + private async Task<IActionResult> MakeChangeAsync<TModel>(Func<string, CancellationToken, Task> action, string successMessage, TModel? model = null) where TModel : class + { + var user = await userService.GetAsync(User, HttpContext.RequestAborted); - if (!ModelState.IsValid) - { - return View(nameof(Profile), await GetVM(user, model)); - } + if (user == null) + { + return NotFound(); + } - string errorMessage; - try - { - await action(user.Id, HttpContext.RequestAborted); + if (!ModelState.IsValid) + { + return View(nameof(Profile), await GetVM(user, model)); + } - await SignInManager.SignInAsync((IdentityUser)user.Identity, true); + string errorMessage; + try + { + await action(user.Id, HttpContext.RequestAborted); - return RedirectToAction(nameof(Profile), new { successMessage }); - } - catch (ValidationException ex) - { - errorMessage = ex.Message; - } - catch (Exception) - { - errorMessage = T.Get("users.errorHappened"); - } + await SignInManager.SignInAsync((IdentityUser)user.Identity, true); - return View(nameof(Profile), await GetVM(user, model, errorMessage)); + return RedirectToAction(nameof(Profile), new { successMessage }); } - - private async Task<ProfileVM> GetVM<TModel>(IUser? user, TModel? model = null, string? errorMessage = null, string? successMessage = null) where TModel : class + catch (ValidationException ex) { - if (user == null) - { - throw new DomainException(T.Get("users.userNotFound")); - } + errorMessage = ex.Message; + } + catch (Exception) + { + errorMessage = T.Get("users.errorHappened"); + } - var (providers, hasPassword, logins) = await AsyncHelper.WhenAll( - SignInManager.GetExternalProvidersAsync(), - userService.HasPasswordAsync(user, HttpContext.RequestAborted), - userService.GetLoginsAsync(user, HttpContext.RequestAborted)); + return View(nameof(Profile), await GetVM(user, model, errorMessage)); + } - var vm = new ProfileVM - { - Id = user.Id, - ClientSecret = user.Claims.ClientSecret()!, - Email = user.Email, - ErrorMessage = errorMessage, - ExternalLogins = logins, - ExternalProviders = providers, - DisplayName = user.Claims.DisplayName()!, - HasPassword = hasPassword, - HasPasswordAuth = identityOptions.AllowPasswordAuth, - IsHidden = user.Claims.IsHidden(), - SuccessMessage = successMessage - }; - - if (model != null) - { - SimpleMapper.Map(model, vm); - } + private async Task<ProfileVM> GetVM<TModel>(IUser? user, TModel? model = null, string? errorMessage = null, string? successMessage = null) where TModel : class + { + if (user == null) + { + throw new DomainException(T.Get("users.userNotFound")); + } - vm.Properties ??= user.Claims.GetCustomProperties().Select(UserProperty.FromTuple).ToList(); + var (providers, hasPassword, logins) = await AsyncHelper.WhenAll( + SignInManager.GetExternalProvidersAsync(), + userService.HasPasswordAsync(user, HttpContext.RequestAborted), + userService.GetLoginsAsync(user, HttpContext.RequestAborted)); - return vm; + var vm = new ProfileVM + { + Id = user.Id, + ClientSecret = user.Claims.ClientSecret()!, + Email = user.Email, + ErrorMessage = errorMessage, + ExternalLogins = logins, + ExternalProviders = providers, + DisplayName = user.Claims.DisplayName()!, + HasPassword = hasPassword, + HasPasswordAuth = identityOptions.AllowPasswordAuth, + IsHidden = user.Claims.IsHidden(), + SuccessMessage = successMessage + }; + + if (model != null) + { + SimpleMapper.Map(model, vm); } + + vm.Properties ??= user.Claims.GetCustomProperties().Select(UserProperty.FromTuple).ToList(); + + return vm; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs index c0c1e3ca83..70b80749c9 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs @@ -7,32 +7,31 @@ using Microsoft.AspNetCore.Identity; -namespace Squidex.Areas.IdentityServer.Controllers.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile; + +public sealed class ProfileVM { - public sealed class ProfileVM - { - public string Id { get; set; } + public string Id { get; set; } - public string Email { get; set; } + public string Email { get; set; } - public string DisplayName { get; set; } + public string DisplayName { get; set; } - public string? ClientSecret { get; set; } + public string? ClientSecret { get; set; } - public string? ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } - public string? SuccessMessage { get; set; } + public string? SuccessMessage { get; set; } - public bool IsHidden { get; set; } + public bool IsHidden { get; set; } - public bool HasPassword { get; set; } + public bool HasPassword { get; set; } - public bool HasPasswordAuth { get; set; } + public bool HasPasswordAuth { get; set; } - public List<UserProperty> Properties { get; set; } + public List<UserProperty> Properties { get; set; } - public IList<UserLoginInfo> ExternalLogins { get; set; } + public IList<UserLoginInfo> ExternalLogins { get; set; } - public IList<ExternalProvider> ExternalProviders { get; set; } - } + public IList<ExternalProvider> ExternalProviders { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs index e29b6e88a3..5aa6eff491 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.IdentityServer.Controllers.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile; + +public class RemoveLoginModel { - public class RemoveLoginModel - { - [LocalizedRequired] - public string LoginProvider { get; set; } + [LocalizedRequired] + public string LoginProvider { get; set; } - [LocalizedRequired] - public string ProviderKey { get; set; } - } + [LocalizedRequired] + public string ProviderKey { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs index 15a5650bec..c7ce385bc6 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs @@ -7,14 +7,13 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.IdentityServer.Controllers.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile; + +public class SetPasswordModel { - public class SetPasswordModel - { - [LocalizedRequired] - public string Password { get; set; } + [LocalizedRequired] + public string Password { get; set; } - [LocalizedRequired] - public string PasswordConfirm { get; set; } - } + [LocalizedRequired] + public string PasswordConfirm { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/UserProperty.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/UserProperty.cs index 62493f72fb..f560106aea 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/UserProperty.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/UserProperty.cs @@ -7,24 +7,23 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.IdentityServer.Controllers.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile; + +public sealed class UserProperty { - public sealed class UserProperty - { - [LocalizedRequired] - public string Name { get; set; } + [LocalizedRequired] + public string Name { get; set; } - [LocalizedRequired] - public string Value { get; set; } + [LocalizedRequired] + public string Value { get; set; } - public (string Name, string Value) ToTuple() - { - return (Name, Value); - } + public (string Name, string Value) ToTuple() + { + return (Name, Value); + } - public static UserProperty FromTuple((string Name, string Value) value) - { - return new UserProperty { Name = value.Name, Value = value.Value }; - } + public static UserProperty FromTuple((string Name, string Value) value) + { + return new UserProperty { Name = value.Name, Value = value.Value }; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/CreateUserModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/CreateUserModel.cs index 857b525ac7..2db457c0be 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/CreateUserModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/CreateUserModel.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure.Validation; -namespace Squidex.Areas.IdentityServer.Controllers.Setup +namespace Squidex.Areas.IdentityServer.Controllers.Setup; + +public sealed class CreateUserModel { - public sealed class CreateUserModel - { - [LocalizedRequired] - public string Email { get; set; } + [LocalizedRequired] + public string Email { get; set; } - [LocalizedRequired] - public string Password { get; set; } + [LocalizedRequired] + public string Password { get; set; } - [LocalizedRequired] - public string PasswordConfirm { get; set; } - } + [LocalizedRequired] + public string PasswordConfirm { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs index c823fddff5..5287099ddd 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs @@ -19,118 +19,117 @@ using Squidex.Infrastructure.Validation; using Squidex.Web; -namespace Squidex.Areas.IdentityServer.Controllers.Setup +namespace Squidex.Areas.IdentityServer.Controllers.Setup; + +public class SetupController : IdentityServerController { - public class SetupController : IdentityServerController + private readonly IAssetStore assetStore; + private readonly IUrlGenerator urlGenerator; + private readonly IUserService userService; + private readonly MyUIOptions uiOptions; + private readonly MyIdentityOptions identityOptions; + + public SetupController( + IAssetStore assetStore, + IOptions<MyUIOptions> uiOptions, + IOptions<MyIdentityOptions> identityOptions, + IUrlGenerator urlGenerator, + IUserService userService) { - private readonly IAssetStore assetStore; - private readonly IUrlGenerator urlGenerator; - private readonly IUserService userService; - private readonly MyUIOptions uiOptions; - private readonly MyIdentityOptions identityOptions; - - public SetupController( - IAssetStore assetStore, - IOptions<MyUIOptions> uiOptions, - IOptions<MyIdentityOptions> identityOptions, - IUrlGenerator urlGenerator, - IUserService userService) + this.assetStore = assetStore; + this.identityOptions = identityOptions.Value; + this.uiOptions = uiOptions.Value; + this.urlGenerator = urlGenerator; + this.userService = userService; + } + + [HttpGet] + [Route("setup/")] + public async Task<IActionResult> Setup() + { + if (!await userService.IsEmptyAsync(HttpContext.RequestAborted)) { - this.assetStore = assetStore; - this.identityOptions = identityOptions.Value; - this.uiOptions = uiOptions.Value; - this.urlGenerator = urlGenerator; - this.userService = userService; + return RedirectToReturnUrl(null); } - [HttpGet] - [Route("setup/")] - public async Task<IActionResult> Setup() - { - if (!await userService.IsEmptyAsync(HttpContext.RequestAborted)) - { - return RedirectToReturnUrl(null); - } + return View(nameof(Setup), await GetVM(None.Value)); + } - return View(nameof(Setup), await GetVM(None.Value)); + [HttpPost] + [Route("setup/")] + public async Task<IActionResult> Setup(CreateUserModel model) + { + if (!await userService.IsEmptyAsync(HttpContext.RequestAborted)) + { + return RedirectToReturnUrl(null); } - [HttpPost] - [Route("setup/")] - public async Task<IActionResult> Setup(CreateUserModel model) + if (!ModelState.IsValid) { - if (!await userService.IsEmptyAsync(HttpContext.RequestAborted)) - { - return RedirectToReturnUrl(null); - } - - if (!ModelState.IsValid) - { - return View(nameof(Profile), await GetVM(model)); - } + return View(nameof(Profile), await GetVM(model)); + } - string errorMessage; - try + string errorMessage; + try + { + var user = await userService.CreateAsync(model.Email, new UserValues { - var user = await userService.CreateAsync(model.Email, new UserValues - { - Password = model.Password - }, ct: HttpContext.RequestAborted); - - await SignInManager.SignInAsync((IdentityUser)user.Identity, true); + Password = model.Password + }, ct: HttpContext.RequestAborted); - return RedirectToReturnUrl(null); - } - catch (ValidationException ex) - { - errorMessage = ex.Message; - } - catch (Exception) - { - errorMessage = T.Get("users.errorHappened"); - } + await SignInManager.SignInAsync((IdentityUser)user.Identity, true); - return View(nameof(Setup), await GetVM(model, errorMessage)); + return RedirectToReturnUrl(null); } - - private async Task<SetupVM> GetVM<TModel>(TModel? model = null, string? errorMessage = null) where TModel : class + catch (ValidationException ex) + { + errorMessage = ex.Message; + } + catch (Exception) { - var externalProviders = await SignInManager.GetExternalProvidersAsync(); + errorMessage = T.Get("users.errorHappened"); + } - var result = new SetupVM - { - BaseUrlConfigured = urlGenerator.BuildUrl(string.Empty, false), - BaseUrlCurrent = GetCurrentUrl(), - ErrorMessage = errorMessage, - EverybodyCanCreateApps = !uiOptions.OnlyAdminsCanCreateApps, - EverybodyCanCreateTeams = !uiOptions.OnlyAdminsCanCreateTeams, - IsValidHttps = HttpContext.Request.IsHttps, - IsAssetStoreFile = assetStore is FolderAssetStore, - IsAssetStoreFtp = assetStore is FTPAssetStore, - HasExternalLogin = externalProviders.Any(), - HasPasswordAuth = identityOptions.AllowPasswordAuth - }; - - if (model != null) - { - SimpleMapper.Map(model, result); - } + return View(nameof(Setup), await GetVM(model, errorMessage)); + } - return result; - } + private async Task<SetupVM> GetVM<TModel>(TModel? model = null, string? errorMessage = null) where TModel : class + { + var externalProviders = await SignInManager.GetExternalProvidersAsync(); - private string GetCurrentUrl() + var result = new SetupVM { - var request = HttpContext.Request; + BaseUrlConfigured = urlGenerator.BuildUrl(string.Empty, false), + BaseUrlCurrent = GetCurrentUrl(), + ErrorMessage = errorMessage, + EverybodyCanCreateApps = !uiOptions.OnlyAdminsCanCreateApps, + EverybodyCanCreateTeams = !uiOptions.OnlyAdminsCanCreateTeams, + IsValidHttps = HttpContext.Request.IsHttps, + IsAssetStoreFile = assetStore is FolderAssetStore, + IsAssetStoreFtp = assetStore is FTPAssetStore, + HasExternalLogin = externalProviders.Any(), + HasPasswordAuth = identityOptions.AllowPasswordAuth + }; + + if (model != null) + { + SimpleMapper.Map(model, result); + } - var url = $"{request.Scheme}://{request.Host}{request.PathBase}"; + return result; + } - if (url.EndsWith(Constants.PrefixIdentityServer, StringComparison.Ordinal)) - { - url = url[..^Constants.PrefixIdentityServer.Length]; - } + private string GetCurrentUrl() + { + var request = HttpContext.Request; + + var url = $"{request.Scheme}://{request.Host}{request.PathBase}"; - return url.TrimEnd('/'); + if (url.EndsWith(Constants.PrefixIdentityServer, StringComparison.Ordinal)) + { + url = url[..^Constants.PrefixIdentityServer.Length]; } + + return url.TrimEnd('/'); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs index 0ec56fe39a..a896a4a32a 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs @@ -5,30 +5,29 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Areas.IdentityServer.Controllers.Setup +namespace Squidex.Areas.IdentityServer.Controllers.Setup; + +public sealed class SetupVM { - public sealed class SetupVM - { - public string Email { get; set; } + public string Email { get; set; } - public string BaseUrlCurrent { get; set; } + public string BaseUrlCurrent { get; set; } - public string BaseUrlConfigured { get; set; } + public string BaseUrlConfigured { get; set; } - public string? ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } - public bool IsValidHttps { get; set; } + public bool IsValidHttps { get; set; } - public bool IsAssetStoreFtp { get; set; } + public bool IsAssetStoreFtp { get; set; } - public bool IsAssetStoreFile { get; set; } + public bool IsAssetStoreFile { get; set; } - public bool EverybodyCanCreateApps { get; set; } + public bool EverybodyCanCreateApps { get; set; } - public bool EverybodyCanCreateTeams { get; set; } + public bool EverybodyCanCreateTeams { get; set; } - public bool HasExternalLogin { get; set; } + public bool HasExternalLogin { get; set; } - public bool HasPasswordAuth { get; set; } - } + public bool HasPasswordAuth { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/UserInfo/UserInfoController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/UserInfo/UserInfoController.cs index 62f015124a..c71e18e741 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/UserInfo/UserInfoController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/UserInfo/UserInfoController.cs @@ -13,54 +13,53 @@ using Squidex.Domain.Users; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Squidex.Areas.IdentityServer.Controllers.UserInfo +namespace Squidex.Areas.IdentityServer.Controllers.UserInfo; + +public class UserInfoController : IdentityServerController { - public class UserInfoController : IdentityServerController + private readonly IUserService userService; + + public UserInfoController(IUserService userService) + { + this.userService = userService; + } + + [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] + [HttpGet] + [HttpPost] + [Route("connect/userinfo")] + [Produces("application/json")] + public async Task<IActionResult> UserInfo() { - private readonly IUserService userService; + var user = await userService.GetAsync(User, HttpContext.RequestAborted); - public UserInfoController(IUserService userService) + if (user == null) { - this.userService = userService; + return Challenge( + new AuthenticationProperties(new Dictionary<string, string?> + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified access token is bound to an account that no longer exists." + }), + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } - [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] - [HttpGet] - [HttpPost] - [Route("connect/userinfo")] - [Produces("application/json")] - public async Task<IActionResult> UserInfo() + var claims = new Dictionary<string, object>(StringComparer.Ordinal) { - var user = await userService.GetAsync(User, HttpContext.RequestAborted); - - if (user == null) - { - return Challenge( - new AuthenticationProperties(new Dictionary<string, string?> - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified access token is bound to an account that no longer exists." - }), - OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } + [Claims.Subject] = user.Id + }; - var claims = new Dictionary<string, object>(StringComparer.Ordinal) - { - [Claims.Subject] = user.Id - }; - - if (User.HasScope(Scopes.Email)) - { - claims[Claims.Email] = user.Email; - claims[Claims.EmailVerified] = true; - } - - if (User.HasScope(Scopes.Roles)) - { - claims[Claims.Role] = Array.Empty<string>(); - } + if (User.HasScope(Scopes.Email)) + { + claims[Claims.Email] = user.Email; + claims[Claims.EmailVerified] = true; + } - return Ok(claims); + if (User.HasScope(Scopes.Roles)) + { + claims[Claims.Role] = Array.Empty<string>(); } + + return Ok(claims); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml index acc02cf783..59d181cc6c 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml @@ -5,10 +5,10 @@ } @functions { - public string ErrorClass(string error) - { - return ViewData.ModelState[error]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid ? "border-danger" : ""; - } +public string ErrorClass(string error) +{ + return ViewData.ModelState[error]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid ? "border-danger" : ""; +} } <form asp-controller="Account" asp-action="Consent" asp-route-returnurl="@Model!.ReturnUrl" method="post"> diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml index 9bb4e6b3aa..ad145e38cf 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml @@ -3,7 +3,7 @@ @{ var action = Model!.IsLogin ? T.Get("common.login") : T.Get("common.signup"); - ViewBag.Title = action; +ViewBag.Title = action; } <div class="login-container"> @@ -11,55 +11,55 @@ <div class="row text-center"> <div class="btn-group profile-headline"> @if (Model!.IsLogin) - { - <a class="btn btn-toggle btn-primary" asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model!.ReturnUrl">@T.Get("common.login")</a> - } - else - { - <a class="btn btn-toggle" asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model!.ReturnUrl">@T.Get("common.login")</a> - } + { + <a class="btn btn-toggle btn-primary" asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model!.ReturnUrl">@T.Get("common.login")</a> + } + else + { + <a class="btn btn-toggle" asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model!.ReturnUrl">@T.Get("common.login")</a> + } @if (!Model!.IsLogin) - { - <a class="btn btn-toggle btn-primary" asp-controller="Account" asp-action="Signup" asp-route-returnurl="@Model!.ReturnUrl">@T.Get("common.signup")</a> - } - else - { - <a class="btn btn-toggle" asp-controller="Account" asp-action="Signup" asp-route-returnurl="@Model!.ReturnUrl">@T.Get("common.signup")</a> - } + { + <a class="btn btn-toggle btn-primary" asp-controller="Account" asp-action="Signup" asp-route-returnurl="@Model!.ReturnUrl">@T.Get("common.signup")</a> + } + else + { + <a class="btn btn-toggle" asp-controller="Account" asp-action="Signup" asp-route-returnurl="@Model!.ReturnUrl">@T.Get("common.signup")</a> + } </div> </div> </div> <form asp-controller="Account" asp-action="External" asp-route-returnurl="@Model!.ReturnUrl" method="post"> @foreach (var provider in Model!.ExternalProviders) - { - var schema = provider.AuthenticationScheme.ToLowerInvariant(); + { + var schema = provider.AuthenticationScheme.ToLowerInvariant(); - <div class="form-group"> + <div class="form-group"> <button class="btn external-button btn-block btn btn-@schema" type="submit" name="provider" value="@provider.AuthenticationScheme"> <i class="icon-@schema external-icon"></i> @Html.Raw(T.Get("users.login.loginWith", new { action, provider = provider.DisplayName })) </button> </div> - } + } </form> @if (Model!.HasExternalLogin && Model!.HasPasswordAuth) - { - <div class="profile-separator"> +{ + <div class="profile-separator"> <div class="profile-separator-text">@T.Get("users.login.separator")</div> </div> - } +} @if (Model!.HasPasswordAuth) +{ + if (Model!.IsLogin) { - if (Model!.IsLogin) + if (Model!.IsFailed) { - if (Model!.IsFailed) - { - <div class="form-alert form-alert-error">@T.Get("users.login.error")</div> - } + <div class="form-alert form-alert-error">@T.Get("users.login.error")</div> + } - <form asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model!.ReturnUrl" method="post"> + <form asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model!.ReturnUrl" method="post"> <div class="form-group"> <input type="email" class="form-control" name="email" id="email" placeholder="@T.Get("users.login.emailPlaceholder")" /> </div> @@ -70,31 +70,31 @@ <button type="submit" class="btn btn-block btn-primary">@action</button> </form> - } - else - { - <div class="profile-password-signup text-center">@T.Get("users.login.askAdmin")</div> - } } + else + { + <div class="profile-password-signup text-center">@T.Get("users.login.askAdmin")</div> + } +} @if (Model!.IsLogin) - { - <p class="profile-footer"> +{ + <p class="profile-footer"> @T.Get("users.login.noAccountSignupQuestion") <a asp-controller="Account" asp-action="Signup" asp-route-returnurl="@Model!.ReturnUrl"> @T.Get("users.login.noAccountSignupAction") </a> </p> - } - else - { - <p class="profile-footer"> +} +else +{ + <p class="profile-footer"> @T.Get("users.login.noAccountLoginQuestion") <a asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model!.ReturnUrl"> @T.Get("users.login.noAccountLoginAction") </a> </p> - } +} </div> \ No newline at end of file diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml index 60b248f2c3..131624e5ba 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml @@ -10,11 +10,11 @@ <p class="splash-text"> @if (Model!.ErrorMessage != null) - { - @Model!.ErrorMessage - } - else - { - <span>@T.Get("users.error.text")</span> - } +{ + @Model!.ErrorMessage +} +else +{ + <span>@T.Get("users.error.text")</span> +} </p> \ No newline at end of file diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs b/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs index 87a68d686b..b3631e63db 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Extensions.cs @@ -7,37 +7,36 @@ using Microsoft.AspNetCore.Mvc; -namespace Squidex.Areas.IdentityServer.Views +namespace Squidex.Areas.IdentityServer.Views; + +public static class Extensions { - public static class Extensions + public static string? RootContentUrl(this IUrlHelper urlHelper, string contentPath) { - public static string? RootContentUrl(this IUrlHelper urlHelper, string contentPath) + if (string.IsNullOrEmpty(contentPath)) { - if (string.IsNullOrEmpty(contentPath)) - { - return null; - } + return null; + } - if (contentPath[0] == '~') - { - var segment = new PathString(contentPath[1..]); + if (contentPath[0] == '~') + { + var segment = new PathString(contentPath[1..]); - var applicationPath = urlHelper.ActionContext.HttpContext.Request.PathBase; + var applicationPath = urlHelper.ActionContext.HttpContext.Request.PathBase; - if (applicationPath.Value != null) - { - var indexOfLastPart = applicationPath.Value.LastIndexOf('/'); + if (applicationPath.Value != null) + { + var indexOfLastPart = applicationPath.Value.LastIndexOf('/'); - if (indexOfLastPart >= 0) - { - applicationPath = applicationPath.Value[..indexOfLastPart]; - } + if (indexOfLastPart >= 0) + { + applicationPath = applicationPath.Value[..indexOfLastPart]; } - - return applicationPath.Add(segment).Value; } - return contentPath; + return applicationPath.Add(segment).Value; } + + return contentPath; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml index dae4db68e9..c500534fe8 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml @@ -3,16 +3,16 @@ @{ ViewBag.Title = T.Get("users.profile.title"); - void RenderValidation(string field) +void RenderValidation(string field) +{ + @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) { - @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) - { - <div class="errors-container"> + <div class="errors-container"> <span class="errors">@Html.ValidationMessage(field)</span> </div> - } } } +} <h1>@T.Get("users.profile.headline")</h1> @@ -89,8 +89,8 @@ <col style="width: 100px;" /> </colgroup> @foreach (var login in Model!.ExternalLogins) - { - <tr> + { + <tr> <td> <span>@login.LoginProvider</span> </td> @@ -99,8 +99,8 @@ </td> <td class="text-right"> @if (Model!.ExternalLogins.Count > 1 || Model!.HasPassword) - { - <form asp-controller="Profile" asp-action="RemoveLogin" method="post"> + { + <form asp-controller="Profile" asp-action="RemoveLogin" method="post"> <input type="hidden" value="@login.LoginProvider" name="LoginProvider" /> <input type="hidden" value="@login.ProviderKey" name="ProviderKey" /> @@ -108,21 +108,21 @@ @T.Get("common.remove") </button> </form> - } + } </td> </tr> - } + } </table> <form asp-controller="Profile" asp-action="AddLogin" method="post"> @foreach (var provider in Model!.ExternalProviders.Where(x => Model!.ExternalLogins.All(y => x.AuthenticationScheme != y.LoginProvider))) - { - var schema = provider.AuthenticationScheme.ToLowerInvariant(); + { + var schema = provider.AuthenticationScheme.ToLowerInvariant(); - <button class="btn external-button-small btn-@schema" type="submit" name="provider" value="@provider.AuthenticationScheme"> + <button class="btn external-button-small btn-@schema" type="submit" name="provider" value="@provider.AuthenticationScheme"> <i class="icon-@schema external-icon"></i> </button> - } + } </form> </div> } @@ -135,8 +135,8 @@ <h2>@T.Get("users.profile.passwordTitle")</h2> @if (Model!.HasPassword) - { - <form class="profile-form" asp-controller="Profile" asp-action="ChangePassword" method="post"> + { + <form class="profile-form" asp-controller="Profile" asp-action="ChangePassword" method="post"> <div class="form-group"> <label for="oldPassword">@T.Get("common.oldPassword")</label> @@ -165,10 +165,10 @@ <button type="submit" class="btn btn-primary">@T.Get("users.profile.changePassword")</button> </div> </form> - } - else - { - <form class="profile-form" asp-controller="Profile" asp-action="SetPassword" method="post"> + } + else + { + <form class="profile-form" asp-controller="Profile" asp-action="SetPassword" method="post"> <div class="form-group"> <label for="password">@T.Get("common.password")</label> @@ -189,7 +189,7 @@ <button type="submit" class="btn btn-primary">@T.Get("users.profile.setPassword")</button> </div> </form> - } + } </div> } @@ -233,8 +233,8 @@ <form class="profile-form" asp-controller="Profile" asp-action="UpdateProperties" method="post"> <div class="mb-2" id="properties"> @for (var i = 0; i < Model!.Properties.Count; i++) - { - <div class="row g-2 form-group"> + { + <div class="row g-2 form-group"> <div class="col-5 pr-2"> @{ RenderValidation($"Properties[{i}].Name"); } @@ -253,7 +253,7 @@ </button> </div> </div> - } + } </div> <div class="form-group"> diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml index f037db1a1b..700f16460b 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml @@ -3,19 +3,19 @@ @{ ViewBag.Title = T.Get("setup.title"); - void RenderValidation(string field) +void RenderValidation(string field) +{ + @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) { - @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) - { - <div class="errors-container"> + <div class="errors-container"> <span class="errors">@Html.ValidationMessage(field)</span> </div> - } } +} - void RenderRuleAsSuccess(string message) - { - <div class="row mt-4"> +void RenderRuleAsSuccess(string message) +{ + <div class="row mt-4"> <div class="col-auto"> <div class="status-icon status-icon-success mt-2"> <i class="icon-checkmark"></i> @@ -28,11 +28,11 @@ </div> </div> </div> - } +} - void RenderRuleAsCritical(string message) - { - <div class="row mt-4"> +void RenderRuleAsCritical(string message) +{ + <div class="row mt-4"> <div class="col-auto"> <div class="status-icon status-icon-failed mt-2"> <i class="icon-exclamation"></i> @@ -45,11 +45,11 @@ </div> </div> </div> - } +} - void RenderRuleAsWarning(string message) - { - <div class="row mt-4"> +void RenderRuleAsWarning(string message) +{ + <div class="row mt-4"> <div class="col-auto"> <div class="status-icon status-icon-warning mt-2"> <i class="icon-exclamation"></i> @@ -62,7 +62,7 @@ </div> </div> </div> - } +} } <h1>@T.Get("setup.headline")</h1> @@ -77,50 +77,50 @@ <h2>@T.Get("setup.rules.headline")</h2> @if (Model!.IsValidHttps) - { - RenderRuleAsSuccess(T.Get("setup.ruleHttps.success")); - } - else - { - RenderRuleAsCritical(T.Get("setup.ruleHttps.failure")); - } +{ + RenderRuleAsSuccess(T.Get("setup.ruleHttps.success")); +} +else +{ + RenderRuleAsCritical(T.Get("setup.ruleHttps.failure")); +} @if (Model!.BaseUrlConfigured == Model!.BaseUrlCurrent) - { - RenderRuleAsSuccess(T.Get("setup.ruleUrl.success")); - } - else - { - RenderRuleAsCritical(T.Get("setup.ruleUrl.failure", new { actual = Model!.BaseUrlCurrent, configured = Model!.BaseUrlConfigured })); - } +{ + RenderRuleAsSuccess(T.Get("setup.ruleUrl.success")); +} +else +{ + RenderRuleAsCritical(T.Get("setup.ruleUrl.failure", new { actual = Model!.BaseUrlCurrent, configured = Model!.BaseUrlConfigured })); +} @if (Model!.EverybodyCanCreateApps) - { - RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAdmins")); - } - else - { - RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAll")); - } +{ + RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAdmins")); +} +else +{ + RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAll")); +} @if (Model!.EverybodyCanCreateTeams) - { - RenderRuleAsWarning(T.Get("setup.ruleTeamCreation.warningAdmins")); - } - else - { - RenderRuleAsWarning(T.Get("setup.ruleTeamCreation.warningAll")); - } +{ + RenderRuleAsWarning(T.Get("setup.ruleTeamCreation.warningAdmins")); +} +else +{ + RenderRuleAsWarning(T.Get("setup.ruleTeamCreation.warningAll")); +} @if (Model!.IsAssetStoreFtp) - { - RenderRuleAsWarning(T.Get("setup.ruleFtp.warning")); - } +{ + RenderRuleAsWarning(T.Get("setup.ruleFtp.warning")); +} @if (Model!.IsAssetStoreFile) - { - RenderRuleAsWarning(T.Get("setup.ruleFolder.warning")); - } +{ + RenderRuleAsWarning(T.Get("setup.ruleFolder.warning")); +} </div> <hr /> @@ -129,8 +129,8 @@ <h2 class="mb-3">@T.Get("setup.createUser.headline")</h2> @if (Model!.HasExternalLogin) - { - <div> +{ + <div> <small class="form-text text-muted mt-2 mb-2">@T.Get("setup.createUser.loginHint")</small> <div class="mt-3"> @@ -139,27 +139,27 @@ </a> </div> </div> - } +} @if (Model!.HasExternalLogin && Model!.HasPasswordAuth) - { - <div class="profile-separator"> +{ + <div class="profile-separator"> <div class="profile-separator-text">@T.Get("setup.createUser.separator")</div> </div> - } +} @if (Model!.HasPasswordAuth) - { - <h3>@T.Get("setup.createUser.headlineCreate")</h3> +{ + <h3>@T.Get("setup.createUser.headlineCreate")</h3> - @if (!string.IsNullOrWhiteSpace(Model!.ErrorMessage)) - { - <div class="form-alert form-alert-error"> + @if (!string.IsNullOrWhiteSpace(Model!.ErrorMessage)) + { + <div class="form-alert form-alert-error"> @Model!.ErrorMessage </div> - } + } - <form class="profile-form" asp-controller="Setup" asp-action="Setup" method="post"> + <form class="profile-form" asp-controller="Setup" asp-action="Setup" method="post"> <div class="form-group"> <label for="email">@T.Get("common.email")</label> @@ -188,12 +188,12 @@ <button type="submit" class="btn btn-success">@T.Get("setup.createUser.button")</button> </div> </form> - } +} @if (!Model!.HasExternalLogin && !Model!.HasPasswordAuth) - { - <div> +{ + <div> @T.Get("setup.createUser.failure") </div> - } +} </div> \ No newline at end of file diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml index 132fd98148..8d4fe3ebf9 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml @@ -11,9 +11,9 @@ <link rel="stylesheet" asp-append-version="true" href="styles.css" /> @if (IsSectionDefined("header")) - { - @await RenderSectionAsync("header") - } +{ + @await RenderSectionAsync("header") +} <environment include="Development"> <script type="text/javascript" src="runtime.js"></script> diff --git a/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs index e59a682a65..6f0250b9e0 100644 --- a/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs +++ b/backend/src/Squidex/Config/Authentication/AuthenticationServices.cs @@ -8,42 +8,41 @@ using Microsoft.AspNetCore.Authentication; using Squidex.Hosting.Web; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public static class AuthenticationServices { - public static class AuthenticationServices + public static void AddSquidexAuthentication(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexAuthentication(this IServiceCollection services, IConfiguration config) - { - var identityOptions = config.GetSection("identity").Get<MyIdentityOptions>() ?? new (); - - services.AddAuthentication() - .AddSquidexCookies(config) - .AddSquidexExternalGithubAuthentication(identityOptions) - .AddSquidexExternalGoogleAuthentication(identityOptions) - .AddSquidexExternalMicrosoftAuthentication(identityOptions) - .AddSquidexExternalOdic(identityOptions) - .AddSquidexIdentityServerAuthentication(identityOptions, config); - } - - public static AuthenticationBuilder AddSquidexCookies(this AuthenticationBuilder builder, IConfiguration config) - { - var urlsOptions = config.GetSection("urls").Get<UrlOptions>() ?? new (); + var identityOptions = config.GetSection("identity").Get<MyIdentityOptions>() ?? new (); + + services.AddAuthentication() + .AddSquidexCookies(config) + .AddSquidexExternalGithubAuthentication(identityOptions) + .AddSquidexExternalGoogleAuthentication(identityOptions) + .AddSquidexExternalMicrosoftAuthentication(identityOptions) + .AddSquidexExternalOdic(identityOptions) + .AddSquidexIdentityServerAuthentication(identityOptions, config); + } - builder.Services.ConfigureApplicationCookie(options => - { - options.AccessDeniedPath = "/identity-server/account/access-denied"; - options.LoginPath = "/identity-server/account/login"; - options.LogoutPath = "/identity-server/account/login"; + public static AuthenticationBuilder AddSquidexCookies(this AuthenticationBuilder builder, IConfiguration config) + { + var urlsOptions = config.GetSection("urls").Get<UrlOptions>() ?? new (); + + builder.Services.ConfigureApplicationCookie(options => + { + options.AccessDeniedPath = "/identity-server/account/access-denied"; + options.LoginPath = "/identity-server/account/login"; + options.LogoutPath = "/identity-server/account/login"; - options.Cookie.Name = ".sq.auth2"; + options.Cookie.Name = ".sq.auth2"; - if (urlsOptions.BaseUrl?.StartsWith("https://", StringComparison.OrdinalIgnoreCase) == true) - { - options.Cookie.SameSite = SameSiteMode.None; - } - }); + if (urlsOptions.BaseUrl?.StartsWith("https://", StringComparison.OrdinalIgnoreCase) == true) + { + options.Cookie.SameSite = SameSiteMode.None; + } + }); - return builder.AddCookie(); - } + return builder.AddCookie(); } } diff --git a/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs index d76f7150cb..5ddc5614e6 100644 --- a/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs +++ b/backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs @@ -7,24 +7,23 @@ using Microsoft.AspNetCore.Authentication; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public static class GithubAuthenticationServices { - public static class GithubAuthenticationServices + public static AuthenticationBuilder AddSquidexExternalGithubAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) { - public static AuthenticationBuilder AddSquidexExternalGithubAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + if (identityOptions.IsGithubAuthConfigured()) { - if (identityOptions.IsGithubAuthConfigured()) + authBuilder.AddGitHub(options => { - authBuilder.AddGitHub(options => - { - options.ClientId = identityOptions.GithubClient; - options.ClientSecret = identityOptions.GithubSecret; - options.Events = new GithubHandler(); - options.Scope.Add("user:email"); - }); - } - - return authBuilder; + options.ClientId = identityOptions.GithubClient; + options.ClientSecret = identityOptions.GithubSecret; + options.Events = new GithubHandler(); + options.Scope.Add("user:email"); + }); } + + return authBuilder; } } diff --git a/backend/src/Squidex/Config/Authentication/GithubHandler.cs b/backend/src/Squidex/Config/Authentication/GithubHandler.cs index dff4900f41..d23b5ed48d 100644 --- a/backend/src/Squidex/Config/Authentication/GithubHandler.cs +++ b/backend/src/Squidex/Config/Authentication/GithubHandler.cs @@ -11,25 +11,24 @@ using Squidex.Infrastructure.Translations; using Squidex.Shared.Identity; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public sealed class GithubHandler : OAuthEvents { - public sealed class GithubHandler : OAuthEvents + public override Task CreatingTicket(OAuthCreatingTicketContext context) { - public override Task CreatingTicket(OAuthCreatingTicketContext context) - { - var nameClaim = context.Identity?.FindFirst(ClaimTypes.Name)?.Value; + var nameClaim = context.Identity?.FindFirst(ClaimTypes.Name)?.Value; - if (!string.IsNullOrWhiteSpace(nameClaim)) - { - context.Identity?.AddClaim(new Claim(SquidexClaimTypes.DisplayName, nameClaim)); - } - - if (string.IsNullOrWhiteSpace(context.Identity?.FindFirst(ClaimTypes.Email)?.Value)) - { - throw new DomainException(T.Get("login.githubPrivateEmail")); - } + if (!string.IsNullOrWhiteSpace(nameClaim)) + { + context.Identity?.AddClaim(new Claim(SquidexClaimTypes.DisplayName, nameClaim)); + } - return base.CreatingTicket(context); + if (string.IsNullOrWhiteSpace(context.Identity?.FindFirst(ClaimTypes.Email)?.Value)) + { + throw new DomainException(T.Get("login.githubPrivateEmail")); } + + return base.CreatingTicket(context); } } diff --git a/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs index e70a6cb7a8..d197972ae2 100644 --- a/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs +++ b/backend/src/Squidex/Config/Authentication/GoogleAuthenticationServices.cs @@ -7,23 +7,22 @@ using Microsoft.AspNetCore.Authentication; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public static class GoogleAuthenticationServices { - public static class GoogleAuthenticationServices + public static AuthenticationBuilder AddSquidexExternalGoogleAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) { - public static AuthenticationBuilder AddSquidexExternalGoogleAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + if (identityOptions.IsGoogleAuthConfigured()) { - if (identityOptions.IsGoogleAuthConfigured()) + authBuilder.AddGoogle(options => { - authBuilder.AddGoogle(options => - { - options.ClientId = identityOptions.GoogleClient; - options.ClientSecret = identityOptions.GoogleSecret; - options.Events = new GoogleHandler(); - }); - } - - return authBuilder; + options.ClientId = identityOptions.GoogleClient; + options.ClientSecret = identityOptions.GoogleSecret; + options.Events = new GoogleHandler(); + }); } + + return authBuilder; } } diff --git a/backend/src/Squidex/Config/Authentication/GoogleHandler.cs b/backend/src/Squidex/Config/Authentication/GoogleHandler.cs index 4fb9ce0e51..7c74460597 100644 --- a/backend/src/Squidex/Config/Authentication/GoogleHandler.cs +++ b/backend/src/Squidex/Config/Authentication/GoogleHandler.cs @@ -11,52 +11,51 @@ using Microsoft.AspNetCore.Authentication.OAuth; using Squidex.Shared.Identity; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public sealed class GoogleHandler : OAuthEvents { - public sealed class GoogleHandler : OAuthEvents + public override Task RedirectToAuthorizationEndpoint(RedirectContext<OAuthOptions> context) { - public override Task RedirectToAuthorizationEndpoint(RedirectContext<OAuthOptions> context) - { - context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); + context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public override Task CreatingTicket(OAuthCreatingTicketContext context) - { - var nameClaim = context.Identity?.FindFirst(ClaimTypes.Name)?.Value; + public override Task CreatingTicket(OAuthCreatingTicketContext context) + { + var nameClaim = context.Identity?.FindFirst(ClaimTypes.Name)?.Value; - if (!string.IsNullOrWhiteSpace(nameClaim)) - { - context.Identity?.AddClaim(new Claim(SquidexClaimTypes.DisplayName, nameClaim)); - } + if (!string.IsNullOrWhiteSpace(nameClaim)) + { + context.Identity?.AddClaim(new Claim(SquidexClaimTypes.DisplayName, nameClaim)); + } - string? pictureUrl = null; + string? pictureUrl = null; - if (context.User.TryGetProperty("picture", out var picture) && picture.ValueKind == JsonValueKind.String) - { - pictureUrl = picture.GetString(); - } + if (context.User.TryGetProperty("picture", out var picture) && picture.ValueKind == JsonValueKind.String) + { + pictureUrl = picture.GetString(); + } - if (string.IsNullOrWhiteSpace(pictureUrl)) + if (string.IsNullOrWhiteSpace(pictureUrl)) + { + if (context.User.TryGetProperty("image", out var image) && image.TryGetProperty("url", out var url) && url.ValueKind == JsonValueKind.String) { - if (context.User.TryGetProperty("image", out var image) && image.TryGetProperty("url", out var url) && url.ValueKind == JsonValueKind.String) - { - pictureUrl = url.GetString(); - } - - if (pictureUrl != null && pictureUrl.EndsWith("?sz=50", StringComparison.Ordinal)) - { - pictureUrl = pictureUrl[..^6]; - } + pictureUrl = url.GetString(); } - if (!string.IsNullOrWhiteSpace(pictureUrl)) + if (pictureUrl != null && pictureUrl.EndsWith("?sz=50", StringComparison.Ordinal)) { - context.Identity?.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); + pictureUrl = pictureUrl[..^6]; } + } - return base.CreatingTicket(context); + if (!string.IsNullOrWhiteSpace(pictureUrl)) + { + context.Identity?.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); } + + return base.CreatingTicket(context); } } diff --git a/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs b/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs index c8a0e1ec19..adee79922c 100644 --- a/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs +++ b/backend/src/Squidex/Config/Authentication/IdentityServerServices.cs @@ -13,66 +13,65 @@ using Squidex.Web; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public static class IdentityServerServices { - public static class IdentityServerServices + public static AuthenticationBuilder AddSquidexIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config) { - public static AuthenticationBuilder AddSquidexIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config) + var useCustomAuthorityUrl = !string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl); + + if (useCustomAuthorityUrl) { - var useCustomAuthorityUrl = !string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl); + const string ExternalIdentityServerSchema = nameof(ExternalIdentityServerSchema); - if (useCustomAuthorityUrl) + authBuilder.AddOpenIdConnect(ExternalIdentityServerSchema, options => { - const string ExternalIdentityServerSchema = nameof(ExternalIdentityServerSchema); - - authBuilder.AddOpenIdConnect(ExternalIdentityServerSchema, options => - { - options.Authority = identityOptions.AuthorityUrl; - options.Scope.Add(Scopes.Email); - options.Scope.Add(Scopes.Profile); - options.Scope.Add(Constants.ScopePermissions); - options.Scope.Add(Constants.ScopeApi); - }); + options.Authority = identityOptions.AuthorityUrl; + options.Scope.Add(Scopes.Email); + options.Scope.Add(Scopes.Profile); + options.Scope.Add(Constants.ScopePermissions); + options.Scope.Add(Constants.ScopeApi); + }); - authBuilder.AddPolicyScheme(Constants.ApiSecurityScheme, Constants.ApiSecurityScheme, options => - { - options.ForwardDefaultSelector = context => ExternalIdentityServerSchema; - }); - } - else + authBuilder.AddPolicyScheme(Constants.ApiSecurityScheme, Constants.ApiSecurityScheme, options => { - authBuilder.AddPolicyScheme(Constants.ApiSecurityScheme, Constants.ApiSecurityScheme, options => - { - options.ForwardDefaultSelector = _ => OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; - }); - } + options.ForwardDefaultSelector = context => ExternalIdentityServerSchema; + }); + } + else + { + authBuilder.AddPolicyScheme(Constants.ApiSecurityScheme, Constants.ApiSecurityScheme, options => + { + options.ForwardDefaultSelector = _ => OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + }); + } - authBuilder.AddOpenIdConnect(); + authBuilder.AddOpenIdConnect(); - authBuilder.Services.AddOptions<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme) - .Configure<IUrlGenerator>((options, urlGenerator) => + authBuilder.Services.AddOptions<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme) + .Configure<IUrlGenerator>((options, urlGenerator) => + { + if (!string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl)) { - if (!string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl)) - { - options.Authority = identityOptions.AuthorityUrl; - } - else - { - options.Authority = urlGenerator.BuildUrl(Constants.PrefixIdentityServer, false); - } + options.Authority = identityOptions.AuthorityUrl; + } + else + { + options.Authority = urlGenerator.BuildUrl(Constants.PrefixIdentityServer, false); + } - options.ClientId = Constants.ClientInternalId; - options.ClientSecret = Constants.ClientInternalSecret; - options.CallbackPath = "/signin-internal"; - options.RequireHttpsMetadata = identityOptions.RequiresHttps; - options.SaveTokens = true; - options.Scope.Add(Scopes.Email); - options.Scope.Add(Scopes.Profile); - options.Scope.Add(Constants.ScopePermissions); - options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }); + options.ClientId = Constants.ClientInternalId; + options.ClientSecret = Constants.ClientInternalSecret; + options.CallbackPath = "/signin-internal"; + options.RequireHttpsMetadata = identityOptions.RequiresHttps; + options.SaveTokens = true; + options.Scope.Add(Scopes.Email); + options.Scope.Add(Scopes.Profile); + options.Scope.Add(Constants.ScopePermissions); + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }); - return authBuilder; - } + return authBuilder; } } diff --git a/backend/src/Squidex/Config/Authentication/IdentityServices.cs b/backend/src/Squidex/Config/Authentication/IdentityServices.cs index 03856215f6..eb92a0ca9f 100644 --- a/backend/src/Squidex/Config/Authentication/IdentityServices.cs +++ b/backend/src/Squidex/Config/Authentication/IdentityServices.cs @@ -8,20 +8,19 @@ using Squidex.Domain.Users; using Squidex.Shared.Users; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public static class IdentityServices { - public static class IdentityServices + public static void AddSquidexIdentity(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexIdentity(this IServiceCollection services, IConfiguration config) - { - services.Configure<MyIdentityOptions>(config, - "identity"); + services.Configure<MyIdentityOptions>(config, + "identity"); - services.AddSingletonAs<DefaultUserResolver>() - .AsOptional<IUserResolver>(); + services.AddSingletonAs<DefaultUserResolver>() + .AsOptional<IUserResolver>(); - services.AddSingletonAs<DefaultUserPictureStore>() - .AsOptional<IUserPictureStore>(); - } + services.AddSingletonAs<DefaultUserPictureStore>() + .AsOptional<IUserPictureStore>(); } } diff --git a/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs b/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs index 440a581722..f9ecda1880 100644 --- a/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs +++ b/backend/src/Squidex/Config/Authentication/MicrosoftAuthenticationServices.cs @@ -7,33 +7,32 @@ using Microsoft.AspNetCore.Authentication; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public static class MicrosoftAuthenticationServices { - public static class MicrosoftAuthenticationServices + public static AuthenticationBuilder AddSquidexExternalMicrosoftAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) { - public static AuthenticationBuilder AddSquidexExternalMicrosoftAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + if (identityOptions.IsMicrosoftAuthConfigured()) { - if (identityOptions.IsMicrosoftAuthConfigured()) + authBuilder.AddMicrosoftAccount(options => { - authBuilder.AddMicrosoftAccount(options => - { - options.ClientId = identityOptions.MicrosoftClient; - options.ClientSecret = identityOptions.MicrosoftSecret; - options.Events = new MicrosoftHandler(); - - var tenantId = identityOptions.MicrosoftTenant; + options.ClientId = identityOptions.MicrosoftClient; + options.ClientSecret = identityOptions.MicrosoftSecret; + options.Events = new MicrosoftHandler(); - if (!string.IsNullOrEmpty(tenantId)) - { - var resource = "https://graph.microsoft.com"; + var tenantId = identityOptions.MicrosoftTenant; - options.AuthorizationEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/authorize?resource={resource}"; - options.TokenEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/token?resource={resource}"; - } - }); - } + if (!string.IsNullOrEmpty(tenantId)) + { + var resource = "https://graph.microsoft.com"; - return authBuilder; + options.AuthorizationEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/authorize?resource={resource}"; + options.TokenEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/token?resource={resource}"; + } + }); } + + return authBuilder; } } diff --git a/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs b/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs index c950d8697b..30cc80f28e 100644 --- a/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs +++ b/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs @@ -10,39 +10,38 @@ using Microsoft.AspNetCore.Authentication.OAuth; using Squidex.Shared.Identity; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public sealed class MicrosoftHandler : OAuthEvents { - public sealed class MicrosoftHandler : OAuthEvents + public override Task CreatingTicket(OAuthCreatingTicketContext context) { - public override Task CreatingTicket(OAuthCreatingTicketContext context) - { - string? displayName = null; - - if (context.User.TryGetProperty("displayName", out var element1) && element1.ValueKind == JsonValueKind.String) - { - displayName = element1.GetString(); - } + string? displayName = null; - if (!string.IsNullOrEmpty(displayName)) - { - context.Identity?.AddClaim(new Claim(SquidexClaimTypes.DisplayName, displayName)); - } + if (context.User.TryGetProperty("displayName", out var element1) && element1.ValueKind == JsonValueKind.String) + { + displayName = element1.GetString(); + } - string? id = null; + if (!string.IsNullOrEmpty(displayName)) + { + context.Identity?.AddClaim(new Claim(SquidexClaimTypes.DisplayName, displayName)); + } - if (context.User.TryGetProperty("id", out var element2) && element2.ValueKind == JsonValueKind.String) - { - id = element2.GetString(); - } + string? id = null; - if (!string.IsNullOrEmpty(id)) - { - var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture"; + if (context.User.TryGetProperty("id", out var element2) && element2.ValueKind == JsonValueKind.String) + { + id = element2.GetString(); + } - context.Identity?.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); - } + if (!string.IsNullOrEmpty(id)) + { + var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture"; - return base.CreatingTicket(context); + context.Identity?.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); } + + return base.CreatingTicket(context); } } diff --git a/backend/src/Squidex/Config/Authentication/OidcHandler.cs b/backend/src/Squidex/Config/Authentication/OidcHandler.cs index 64d95bf0ce..8f956f9db0 100644 --- a/backend/src/Squidex/Config/Authentication/OidcHandler.cs +++ b/backend/src/Squidex/Config/Authentication/OidcHandler.cs @@ -9,51 +9,50 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Squidex.Shared.Identity; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public sealed class OidcHandler : OpenIdConnectEvents { - public sealed class OidcHandler : OpenIdConnectEvents + private readonly MyIdentityOptions options; + + public OidcHandler(MyIdentityOptions options) { - private readonly MyIdentityOptions options; + this.options = options; + } - public OidcHandler(MyIdentityOptions options) - { - this.options = options; - } + public override Task TokenValidated(TokenValidatedContext context) + { + var identity = (ClaimsIdentity)context.Principal!.Identity!; - public override Task TokenValidated(TokenValidatedContext context) + if (!string.IsNullOrWhiteSpace(options.OidcRoleClaimType) && options.OidcRoleMapping?.Count >= 0) { - var identity = (ClaimsIdentity)context.Principal!.Identity!; + var permissions = options.OidcRoleMapping + .Where(r => identity.HasClaim(options.OidcRoleClaimType, r.Key)) + .Select(r => r.Value) + .SelectMany(r => r) + .Distinct(); - if (!string.IsNullOrWhiteSpace(options.OidcRoleClaimType) && options.OidcRoleMapping?.Count >= 0) + foreach (var permission in permissions) { - var permissions = options.OidcRoleMapping - .Where(r => identity.HasClaim(options.OidcRoleClaimType, r.Key)) - .Select(r => r.Value) - .SelectMany(r => r) - .Distinct(); - - foreach (var permission in permissions) - { - identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); - } + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); } - - return base.TokenValidated(context); } - public override Task RedirectToIdentityProviderForSignOut(RedirectContext context) - { - if (!string.IsNullOrEmpty(options.OidcOnSignoutRedirectUrl)) - { - var logoutUri = options.OidcOnSignoutRedirectUrl; + return base.TokenValidated(context); + } - context.Response.Redirect(logoutUri); - context.HandleResponse(); + public override Task RedirectToIdentityProviderForSignOut(RedirectContext context) + { + if (!string.IsNullOrEmpty(options.OidcOnSignoutRedirectUrl)) + { + var logoutUri = options.OidcOnSignoutRedirectUrl; - return Task.CompletedTask; - } + context.Response.Redirect(logoutUri); + context.HandleResponse(); - return base.RedirectToIdentityProviderForSignOut(context); + return Task.CompletedTask; } + + return base.RedirectToIdentityProviderForSignOut(context); } } diff --git a/backend/src/Squidex/Config/Authentication/OidcServices.cs b/backend/src/Squidex/Config/Authentication/OidcServices.cs index c7ad268e64..fd2d993a63 100644 --- a/backend/src/Squidex/Config/Authentication/OidcServices.cs +++ b/backend/src/Squidex/Config/Authentication/OidcServices.cs @@ -8,47 +8,46 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; -namespace Squidex.Config.Authentication +namespace Squidex.Config.Authentication; + +public static class OidcServices { - public static class OidcServices + public static AuthenticationBuilder AddSquidexExternalOdic(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) { - public static AuthenticationBuilder AddSquidexExternalOdic(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions) + if (identityOptions.IsOidcConfigured()) { - if (identityOptions.IsOidcConfigured()) + var displayName = !string.IsNullOrWhiteSpace(identityOptions.OidcName) ? identityOptions.OidcName : OpenIdConnectDefaults.DisplayName; + + authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options => { - var displayName = !string.IsNullOrWhiteSpace(identityOptions.OidcName) ? identityOptions.OidcName : OpenIdConnectDefaults.DisplayName; + options.Authority = identityOptions.OidcAuthority; + options.ClientId = identityOptions.OidcClient; + options.ClientSecret = identityOptions.OidcSecret; + options.RequireHttpsMetadata = identityOptions.RequiresHttps; + options.Events = new OidcHandler(identityOptions); - authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options => + if (!string.IsNullOrEmpty(identityOptions.OidcMetadataAddress)) { - options.Authority = identityOptions.OidcAuthority; - options.ClientId = identityOptions.OidcClient; - options.ClientSecret = identityOptions.OidcSecret; - options.RequireHttpsMetadata = identityOptions.RequiresHttps; - options.Events = new OidcHandler(identityOptions); - - if (!string.IsNullOrEmpty(identityOptions.OidcMetadataAddress)) - { - options.MetadataAddress = identityOptions.OidcMetadataAddress; - } + options.MetadataAddress = identityOptions.OidcMetadataAddress; + } - if (!string.IsNullOrEmpty(identityOptions.OidcResponseType)) - { - options.ResponseType = identityOptions.OidcResponseType; - } + if (!string.IsNullOrEmpty(identityOptions.OidcResponseType)) + { + options.ResponseType = identityOptions.OidcResponseType; + } - options.GetClaimsFromUserInfoEndpoint = identityOptions.OidcGetClaimsFromUserInfoEndpoint; + options.GetClaimsFromUserInfoEndpoint = identityOptions.OidcGetClaimsFromUserInfoEndpoint; - if (identityOptions.OidcScopes != null) + if (identityOptions.OidcScopes != null) + { + foreach (var scope in identityOptions.OidcScopes) { - foreach (var scope in identityOptions.OidcScopes) - { - options.Scope.Add(scope); - } + options.Scope.Add(scope); } - }); - } - - return authBuilder; + } + }); } + + return authBuilder; } } diff --git a/backend/src/Squidex/Config/Domain/AppsServices.cs b/backend/src/Squidex/Config/Domain/AppsServices.cs index 1cced7b158..046dce7b8d 100644 --- a/backend/src/Squidex/Config/Domain/AppsServices.cs +++ b/backend/src/Squidex/Config/Domain/AppsServices.cs @@ -15,71 +15,70 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class AppsServices { - public static class AppsServices + public static void AddSquidexApps(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexApps(this IServiceCollection services, IConfiguration config) + if (config.GetValue<bool>("apps:deletePermanent")) { - if (config.GetValue<bool>("apps:deletePermanent")) - { - services.AddSingletonAs<AppPermanentDeleter>() - .As<IEventConsumer>(); - } + services.AddSingletonAs<AppPermanentDeleter>() + .As<IEventConsumer>(); + } - services.AddSingletonAs<RolePermissionsProvider>() - .AsSelf(); + services.AddSingletonAs<RolePermissionsProvider>() + .AsSelf(); - services.AddSingletonAs<AppEventDeleter>() - .As<IDeleter>(); + services.AddSingletonAs<AppEventDeleter>() + .As<IDeleter>(); - services.AddSingletonAs<AppUsageDeleter>() - .As<IDeleter>(); + services.AddSingletonAs<AppUsageDeleter>() + .As<IDeleter>(); - services.AddSingletonAs<DefaultAppLogStore>() - .As<IAppLogStore>().As<IDeleter>(); + services.AddSingletonAs<DefaultAppLogStore>() + .As<IAppLogStore>().As<IDeleter>(); - services.AddSingletonAs<AppHistoryEventsCreator>() - .As<IHistoryEventsCreator>(); + services.AddSingletonAs<AppHistoryEventsCreator>() + .As<IHistoryEventsCreator>(); - services.AddSingletonAs<DefaultAppImageStore>() - .As<IAppImageStore>(); + services.AddSingletonAs<DefaultAppImageStore>() + .As<IAppImageStore>(); - services.AddSingletonAs<AppProvider>() - .As<IAppProvider>(); + services.AddSingletonAs<AppProvider>() + .As<IAppProvider>(); - services.AddSingletonAs<AppUISettings>() - .As<IAppUISettings>().As<IDeleter>(); + services.AddSingletonAs<AppUISettings>() + .As<IAppUISettings>().As<IDeleter>(); - services.AddSingletonAs<AppSettingsSearchSource>() - .As<ISearchSource>(); + services.AddSingletonAs<AppSettingsSearchSource>() + .As<ISearchSource>(); - services.AddSingleton(c => - { - var uiOptions = c.GetRequiredService<IOptions<MyUIOptions>>().Value; + services.AddSingleton(c => + { + var uiOptions = c.GetRequiredService<IOptions<MyUIOptions>>().Value; - var patterns = new List<Pattern>(); + var patterns = new List<Pattern>(); - if (uiOptions.RegexSuggestions != null) + if (uiOptions.RegexSuggestions != null) + { + foreach (var (key, value) in uiOptions.RegexSuggestions) { - foreach (var (key, value) in uiOptions.RegexSuggestions) + if (!string.IsNullOrWhiteSpace(key) && + !string.IsNullOrWhiteSpace(value)) { - if (!string.IsNullOrWhiteSpace(key) && - !string.IsNullOrWhiteSpace(value)) - { - patterns.Add(new Pattern(key, value)); - } + patterns.Add(new Pattern(key, value)); } } + } - return new InitialSettings + return new InitialSettings + { + Settings = new AppSettings { - Settings = new AppSettings - { - Patterns = patterns.ToReadonlyList() - } - }; - }); - } + Patterns = patterns.ToReadonlyList() + } + }; + }); } } diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index f8afda87be..1ce0918db0 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -17,172 +17,171 @@ using Squidex.Infrastructure.EventSourcing; using tusdotnet.Interfaces; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class AssetServices { - public static class AssetServices + public static void AddSquidexAssets(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexAssets(this IServiceCollection services, IConfiguration config) - { - services.Configure<AssetOptions>(config, - "assets"); + services.Configure<AssetOptions>(config, + "assets"); - if (config.GetValue<bool>("assets:deleteRecursive")) - { - services.AddTransientAs<RecursiveDeleter>() - .As<IEventConsumer>(); - } + if (config.GetValue<bool>("assets:deleteRecursive")) + { + services.AddTransientAs<RecursiveDeleter>() + .As<IEventConsumer>(); + } - if (config.GetValue<bool>("assets:deletePermanent")) - { - services.AddTransientAs<AssetPermanentDeleter>() - .As<IEventConsumer>(); - } + if (config.GetValue<bool>("assets:deletePermanent")) + { + services.AddTransientAs<AssetPermanentDeleter>() + .As<IEventConsumer>(); + } - services.AddSingletonAs<AssetQueryParser>() - .AsSelf(); + services.AddSingletonAs<AssetQueryParser>() + .AsSelf(); - services.AddTransientAs<AssetTagsDeleter>() - .As<IDeleter>(); + services.AddTransientAs<AssetTagsDeleter>() + .As<IDeleter>(); - services.AddTransientAs<AssetCache>() - .As<IAssetCache>(); + services.AddTransientAs<AssetCache>() + .As<IAssetCache>(); - services.AddSingletonAs<AssetTusRunner>() - .AsSelf(); + services.AddSingletonAs<AssetTusRunner>() + .AsSelf(); - services.AddSingletonAs<AssetTusStore>() - .As<ITusStore>().As<ITusExpirationStore>(); + services.AddSingletonAs<AssetTusStore>() + .As<ITusStore>().As<ITusExpirationStore>(); - services.AddSingletonAs<RebuildFiles>() - .AsSelf(); + services.AddSingletonAs<RebuildFiles>() + .AsSelf(); - services.AddTransientAs<AssetHistoryEventsCreator>() - .As<IHistoryEventsCreator>(); + services.AddTransientAs<AssetHistoryEventsCreator>() + .As<IHistoryEventsCreator>(); - services.AddSingletonAs<AssetsSearchSource>() - .As<ISearchSource>(); + services.AddSingletonAs<AssetsSearchSource>() + .As<ISearchSource>(); - services.AddSingletonAs<DefaultAssetFileStore>() - .As<IAssetFileStore>().As<IDeleter>(); + services.AddSingletonAs<DefaultAssetFileStore>() + .As<IAssetFileStore>().As<IDeleter>(); - services.AddSingletonAs<AssetEnricher>() - .As<IAssetEnricher>(); + services.AddSingletonAs<AssetEnricher>() + .As<IAssetEnricher>(); - services.AddSingletonAs<AssetQueryService>() - .As<IAssetQueryService>(); + services.AddSingletonAs<AssetQueryService>() + .As<IAssetQueryService>(); - services.AddSingletonAs<AssetLoader>() - .As<IAssetLoader>(); + services.AddSingletonAs<AssetLoader>() + .As<IAssetLoader>(); - services.AddSingletonAs<AssetUsageTracker>() - .As<IEventConsumer>().As<IDeleter>(); + services.AddSingletonAs<AssetUsageTracker>() + .As<IEventConsumer>().As<IDeleter>(); - services.AddSingletonAs<FileTypeAssetMetadataSource>() - .As<IAssetMetadataSource>(); + services.AddSingletonAs<FileTypeAssetMetadataSource>() + .As<IAssetMetadataSource>(); - services.AddSingletonAs<FileTagAssetMetadataSource>() - .As<IAssetMetadataSource>(); + services.AddSingletonAs<FileTagAssetMetadataSource>() + .As<IAssetMetadataSource>(); - services.AddSingletonAs<ImageAssetMetadataSource>() - .As<IAssetMetadataSource>(); + services.AddSingletonAs<ImageAssetMetadataSource>() + .As<IAssetMetadataSource>(); - services.AddSingletonAs<SvgAssetMetadataSource>() - .As<IAssetMetadataSource>(); - } + services.AddSingletonAs<SvgAssetMetadataSource>() + .As<IAssetMetadataSource>(); + } - public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) + public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) + { + config.ConfigureByOption("assetStore:type", new Alternatives { - config.ConfigureByOption("assetStore:type", new Alternatives + ["Default"] = () => { - ["Default"] = () => - { - services.AddSingletonAs<NoopAssetStore>() - .AsOptional<IAssetStore>(); - }, - ["Folder"] = () => - { - var path = config.GetRequiredValue("assetStore:folder:path"); + services.AddSingletonAs<NoopAssetStore>() + .AsOptional<IAssetStore>(); + }, + ["Folder"] = () => + { + var path = config.GetRequiredValue("assetStore:folder:path"); - services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService<ILogger<FolderAssetStore>>())) - .As<IAssetStore>(); - }, - ["GoogleCloud"] = () => + services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService<ILogger<FolderAssetStore>>())) + .As<IAssetStore>(); + }, + ["GoogleCloud"] = () => + { + var options = new GoogleCloudAssetOptions { - var options = new GoogleCloudAssetOptions - { - BucketName = config.GetRequiredValue("assetStore:googleCloud:bucket") - }; + BucketName = config.GetRequiredValue("assetStore:googleCloud:bucket") + }; - services.AddSingletonAs(c => new GoogleCloudAssetStore(options)) - .As<IAssetStore>(); - }, - ["AzureBlob"] = () => - { - var options = new AzureBlobAssetOptions - { - ConnectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"), - ContainerName = config.GetRequiredValue("assetStore:azureBlob:containerName") - }; - - services.AddSingletonAs(c => new AzureBlobAssetStore(options)) - .As<IAssetStore>(); - }, - ["AmazonS3"] = () => + services.AddSingletonAs(c => new GoogleCloudAssetStore(options)) + .As<IAssetStore>(); + }, + ["AzureBlob"] = () => + { + var options = new AzureBlobAssetOptions { - var amazonS3Options = config.GetSection("assetStore:amazonS3").Get<AmazonS3AssetOptions>() ?? new (); + ConnectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"), + ContainerName = config.GetRequiredValue("assetStore:azureBlob:containerName") + }; + + services.AddSingletonAs(c => new AzureBlobAssetStore(options)) + .As<IAssetStore>(); + }, + ["AmazonS3"] = () => + { + var amazonS3Options = config.GetSection("assetStore:amazonS3").Get<AmazonS3AssetOptions>() ?? new (); - services.AddSingletonAs(c => new AmazonS3AssetStore(amazonS3Options)) - .As<IAssetStore>(); - }, - ["MongoDb"] = () => - { - var mongoConfiguration = config.GetRequiredValue("assetStore:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database"); - var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket"); + services.AddSingletonAs(c => new AmazonS3AssetStore(amazonS3Options)) + .As<IAssetStore>(); + }, + ["MongoDb"] = () => + { + var mongoConfiguration = config.GetRequiredValue("assetStore:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database"); + var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket"); + + services.AddSingletonAs(c => + { + var mongoClient = StoreServices.GetMongoClient(mongoConfiguration); + var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); - services.AddSingletonAs(c => + var gridFsbucket = new GridFSBucket<string>(mongoDatabase, new GridFSBucketOptions { - var mongoClient = StoreServices.GetMongoClient(mongoConfiguration); - var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); - - var gridFsbucket = new GridFSBucket<string>(mongoDatabase, new GridFSBucketOptions - { - BucketName = mongoGridFsBucketName - }); - - return new MongoGridFsAssetStore(gridFsbucket); - }) - .As<IAssetStore>(); - }, - ["Ftp"] = () => - { - var serverHost = config.GetRequiredValue("assetStore:ftp:serverHost"); - var serverPort = config.GetOptionalValue<int>("assetStore:ftp:serverPort", 21); + BucketName = mongoGridFsBucketName + }); + + return new MongoGridFsAssetStore(gridFsbucket); + }) + .As<IAssetStore>(); + }, + ["Ftp"] = () => + { + var serverHost = config.GetRequiredValue("assetStore:ftp:serverHost"); + var serverPort = config.GetOptionalValue<int>("assetStore:ftp:serverPort", 21); - var username = config.GetRequiredValue("assetStore:ftp:username"); - var password = config.GetRequiredValue("assetStore:ftp:password"); + var username = config.GetRequiredValue("assetStore:ftp:username"); + var password = config.GetRequiredValue("assetStore:ftp:password"); - var options = new FTPAssetOptions - { - Path = config.GetOptionalValue("assetStore:ftp:path", "/") - }; + var options = new FTPAssetOptions + { + Path = config.GetOptionalValue("assetStore:ftp:path", "/") + }; - services.AddSingletonAs(c => - { - var factory = new Func<FtpClient>(() => new FtpClient(serverHost, serverPort, username, password)); + services.AddSingletonAs(c => + { + var factory = new Func<FtpClient>(() => new FtpClient(serverHost, serverPort, username, password)); - return new FTPAssetStore(factory, options, c.GetRequiredService<ILogger<FTPAssetStore>>()); - }) - .As<IAssetStore>(); - } - }); + return new FTPAssetStore(factory, options, c.GetRequiredService<ILogger<FTPAssetStore>>()); + }) + .As<IAssetStore>(); + } + }); - services.AddSingletonAs<IInitializable>(c => - { - var service = c.GetRequiredService<IAssetStore>(); + services.AddSingletonAs<IInitializable>(c => + { + var service = c.GetRequiredService<IAssetStore>(); - return new DelegateInitializer(service.GetType().Name, service.InitializeAsync); - }); - } + return new DelegateInitializer(service.GetType().Name, service.InitializeAsync); + }); } } diff --git a/backend/src/Squidex/Config/Domain/BackupsServices.cs b/backend/src/Squidex/Config/Domain/BackupsServices.cs index 2b214af00a..ffe8971b89 100644 --- a/backend/src/Squidex/Config/Domain/BackupsServices.cs +++ b/backend/src/Squidex/Config/Domain/BackupsServices.cs @@ -13,44 +13,43 @@ using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Schemas; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class BackupsServices { - public static class BackupsServices + public static void AddSquidexBackups(this IServiceCollection services) { - public static void AddSquidexBackups(this IServiceCollection services) - { - services.AddSingletonAs<TempFolderBackupArchiveLocation>() - .As<IBackupArchiveLocation>(); + services.AddSingletonAs<TempFolderBackupArchiveLocation>() + .As<IBackupArchiveLocation>(); - services.AddSingletonAs<DefaultBackupHandlerFactory>() - .As<IBackupHandlerFactory>(); + services.AddSingletonAs<DefaultBackupHandlerFactory>() + .As<IBackupHandlerFactory>(); - services.AddSingletonAs<DefaultBackupArchiveStore>() - .As<IBackupArchiveStore>(); + services.AddSingletonAs<DefaultBackupArchiveStore>() + .As<IBackupArchiveStore>(); - services.AddTransientAs<BackupService>() - .As<IBackupService>().As<IDeleter>(); + services.AddTransientAs<BackupService>() + .As<IBackupService>().As<IDeleter>(); - services.AddTransientAs<BackupApps>() - .As<IBackupHandler>(); + services.AddTransientAs<BackupApps>() + .As<IBackupHandler>(); - services.AddTransientAs<BackupAssets>() - .As<IBackupHandler>(); + services.AddTransientAs<BackupAssets>() + .As<IBackupHandler>(); - services.AddTransientAs<BackupContents>() - .As<IBackupHandler>(); + services.AddTransientAs<BackupContents>() + .As<IBackupHandler>(); - services.AddTransientAs<BackupRules>() - .As<IBackupHandler>(); + services.AddTransientAs<BackupRules>() + .As<IBackupHandler>(); - services.AddTransientAs<BackupSchemas>() - .As<IBackupHandler>(); + services.AddTransientAs<BackupSchemas>() + .As<IBackupHandler>(); - services.AddTransientAs<DefaultBackupHandlerFactory>() - .As<IBackupHandlerFactory>(); + services.AddTransientAs<DefaultBackupHandlerFactory>() + .As<IBackupHandlerFactory>(); - services.AddTransientAs<RestoreProcessor>() - .AsSelf(); - } + services.AddTransientAs<RestoreProcessor>() + .AsSelf(); } } diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index 43b02b001d..fadc941deb 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -28,116 +28,115 @@ using Squidex.Infrastructure.Commands; using Squidex.Web.CommandMiddlewares; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class CommandsServices { - public static class CommandsServices + public static void AddSquidexCommands(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexCommands(this IServiceCollection services, IConfiguration config) - { - services.Configure<ReadonlyOptions>(config, - "mode"); + services.Configure<ReadonlyOptions>(config, + "mode"); - services.Configure<RestrictAppsOptions>(config, - "usage"); + services.Configure<RestrictAppsOptions>(config, + "usage"); - services.Configure<DomainObjectCacheOptions>(config, - "caching:domainObjects"); + services.Configure<DomainObjectCacheOptions>(config, + "caching:domainObjects"); - services.AddSingletonAs<InMemoryCommandBus>() - .As<ICommandBus>(); + services.AddSingletonAs<InMemoryCommandBus>() + .As<ICommandBus>(); - services.AddSingletonAs<ReadonlyCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<ReadonlyCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<ETagCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<ETagCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<EnrichWithTimestampCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<EnrichWithTimestampCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<EnrichWithActorCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<EnrichWithActorCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<EnrichWithAppIdCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<EnrichWithAppIdCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<EnrichWithTeamIdCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<EnrichWithTeamIdCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<EnrichWithSchemaIdCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<EnrichWithSchemaIdCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<EnrichWithContentIdCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<EnrichWithContentIdCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<CustomCommandMiddlewareRunner>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<CustomCommandMiddlewareRunner>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<TemplateCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<TemplateCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<AlwaysCreateClientCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<AlwaysCreateClientCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<RestrictAppsCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<RestrictAppsCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<InviteUserCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<InviteUserCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<AppsIndex>() - .As<ICommandMiddleware>().As<IAppsIndex>(); + services.AddSingletonAs<AppsIndex>() + .As<ICommandMiddleware>().As<IAppsIndex>(); - services.AddSingletonAs<RulesIndex>() - .As<IRulesIndex>(); + services.AddSingletonAs<RulesIndex>() + .As<IRulesIndex>(); - services.AddSingletonAs<SchemasIndex>() - .As<ICommandMiddleware>().As<ISchemasIndex>(); + services.AddSingletonAs<SchemasIndex>() + .As<ICommandMiddleware>().As<ISchemasIndex>(); - services.AddSingletonAs<TeamsIndex>() - .As<ITeamsIndex>(); + services.AddSingletonAs<TeamsIndex>() + .As<ITeamsIndex>(); - services.AddSingletonAs<AppCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<AppCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<AssetsBulkUpdateCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<AssetsBulkUpdateCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<AssetCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<AssetCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<CommentsCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<CommentsCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<ContentsBulkUpdateCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<ContentsBulkUpdateCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<ContentCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<ContentCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<RuleCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<RuleCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<AggregateCommandMiddleware<AssetFolderCommandBase, AssetFolderDomainObject>>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<AggregateCommandMiddleware<AssetFolderCommandBase, AssetFolderDomainObject>>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<AggregateCommandMiddleware<SchemaCommandBase, SchemaDomainObject>>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<AggregateCommandMiddleware<SchemaCommandBase, SchemaDomainObject>>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<AggregateCommandMiddleware<TeamCommandBase, TeamDomainObject>>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<AggregateCommandMiddleware<TeamCommandBase, TeamDomainObject>>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<SingletonCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<SingletonCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<UsageTrackerCommandMiddleware>() - .As<ICommandMiddleware>(); + services.AddSingletonAs<UsageTrackerCommandMiddleware>() + .As<ICommandMiddleware>(); - services.AddSingletonAs<DefaultDomainObjectFactory>() - .As<IDomainObjectFactory>(); + services.AddSingletonAs<DefaultDomainObjectFactory>() + .As<IDomainObjectFactory>(); - services.AddSingletonAs<DefaultDomainObjectCache>() - .As<IDomainObjectCache>(); - } + services.AddSingletonAs<DefaultDomainObjectCache>() + .As<IDomainObjectCache>(); } } diff --git a/backend/src/Squidex/Config/Domain/CommentsServices.cs b/backend/src/Squidex/Config/Domain/CommentsServices.cs index 73f7a84ce9..ffe4d0532e 100644 --- a/backend/src/Squidex/Config/Domain/CommentsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommentsServices.cs @@ -7,17 +7,16 @@ using Squidex.Domain.Apps.Entities.Comments; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class CommentsServices { - public static class CommentsServices + public static void AddSquidexComments(this IServiceCollection services) { - public static void AddSquidexComments(this IServiceCollection services) - { - services.AddSingletonAs<CommentsLoader>() - .As<ICommentsLoader>(); + services.AddSingletonAs<CommentsLoader>() + .As<ICommentsLoader>(); - services.AddSingletonAs<WatchingService>() - .As<IWatchingService>(); - } + services.AddSingletonAs<WatchingService>() + .As<IWatchingService>(); } } diff --git a/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs b/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs index 7e485002b1..dee89e7317 100644 --- a/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs +++ b/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs @@ -5,13 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class ConfigurationExtensions { - public static class ConfigurationExtensions + public static void ConfigureForSquidex(this IConfigurationBuilder builder) { - public static void ConfigureForSquidex(this IConfigurationBuilder builder) - { - builder.AddJsonFile("appsettings.Custom.json", true); - } + builder.AddJsonFile("appsettings.Custom.json", true); } } diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index 6ffd6342eb..4e1203c5b8 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -18,86 +18,85 @@ using Squidex.Domain.Apps.Entities.Search; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class ContentsServices { - public static class ContentsServices + public static void AddSquidexContents(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexContents(this IServiceCollection services, IConfiguration config) - { - services.Configure<ContentOptions>(config, - "contents"); + services.Configure<ContentOptions>(config, + "contents"); - services.Configure<TemplatesOptions>(config, - "templates"); + services.Configure<TemplatesOptions>(config, + "templates"); - services.AddSingletonAs(c => new Lazy<IContentQueryService>(c.GetRequiredService<IContentQueryService>)) - .AsSelf(); + services.AddSingletonAs(c => new Lazy<IContentQueryService>(c.GetRequiredService<IContentQueryService>)) + .AsSelf(); - services.AddSingletonAs<ContentQueryParser>() - .AsSelf(); + services.AddSingletonAs<ContentQueryParser>() + .AsSelf(); - services.AddTransientAs<CounterService>() - .As<ICounterService>().As<IDeleter>(); + services.AddTransientAs<CounterService>() + .As<ICounterService>().As<IDeleter>(); - services.AddTransientAs<ContentCache>() - .As<IContentCache>(); + services.AddTransientAs<ContentCache>() + .As<IContentCache>(); - services.AddSingletonAs<DefaultValidatorsFactory>() - .As<IValidatorsFactory>(); + services.AddSingletonAs<DefaultValidatorsFactory>() + .As<IValidatorsFactory>(); - services.AddSingletonAs<DependencyValidatorsFactory>() - .As<IValidatorsFactory>(); + services.AddSingletonAs<DependencyValidatorsFactory>() + .As<IValidatorsFactory>(); - services.AddSingletonAs<ContentHistoryEventsCreator>() - .As<IHistoryEventsCreator>(); + services.AddSingletonAs<ContentHistoryEventsCreator>() + .As<IHistoryEventsCreator>(); - services.AddSingletonAs<ContentQueryService>() - .As<IContentQueryService>(); + services.AddSingletonAs<ContentQueryService>() + .As<IContentQueryService>(); - services.AddSingletonAs<ConvertData>() - .As<IContentEnricherStep>(); + services.AddSingletonAs<ConvertData>() + .As<IContentEnricherStep>(); - services.AddSingletonAs<CalculateTokens>() - .As<IContentEnricherStep>(); + services.AddSingletonAs<CalculateTokens>() + .As<IContentEnricherStep>(); - services.AddSingletonAs<EnrichForCaching>() - .As<IContentEnricherStep>(); + services.AddSingletonAs<EnrichForCaching>() + .As<IContentEnricherStep>(); - services.AddSingletonAs<EnrichWithSchema>() - .As<IContentEnricherStep>(); + services.AddSingletonAs<EnrichWithSchema>() + .As<IContentEnricherStep>(); - services.AddSingletonAs<EnrichWithWorkflows>() - .As<IContentEnricherStep>(); + services.AddSingletonAs<EnrichWithWorkflows>() + .As<IContentEnricherStep>(); - services.AddSingletonAs<ResolveAssets>() - .As<IContentEnricherStep>(); + services.AddSingletonAs<ResolveAssets>() + .As<IContentEnricherStep>(); - services.AddSingletonAs<ResolveReferences>() - .As<IContentEnricherStep>(); + services.AddSingletonAs<ResolveReferences>() + .As<IContentEnricherStep>(); - services.AddSingletonAs<ScriptContent>() - .As<IContentEnricherStep>(); + services.AddSingletonAs<ScriptContent>() + .As<IContentEnricherStep>(); - services.AddSingletonAs<ContentEnricher>() - .As<IContentEnricher>(); + services.AddSingletonAs<ContentEnricher>() + .As<IContentEnricher>(); - services.AddSingletonAs<ContentLoader>() - .As<IContentLoader>(); + services.AddSingletonAs<ContentLoader>() + .As<IContentLoader>(); - services.AddSingletonAs<DynamicContentWorkflow>() - .AsOptional<IContentWorkflow>(); + services.AddSingletonAs<DynamicContentWorkflow>() + .AsOptional<IContentWorkflow>(); - services.AddSingletonAs<DefaultWorkflowsValidator>() - .AsOptional<IWorkflowsValidator>(); + services.AddSingletonAs<DefaultWorkflowsValidator>() + .AsOptional<IWorkflowsValidator>(); - services.AddSingletonAs<TextIndexingProcess>() - .As<IEventConsumer>(); + services.AddSingletonAs<TextIndexingProcess>() + .As<IEventConsumer>(); - services.AddSingletonAs<ContentsSearchSource>() - .As<ISearchSource>(); + services.AddSingletonAs<ContentsSearchSource>() + .As<ISearchSource>(); - services.AddSingletonAs<TemplatesClient>() - .AsSelf(); - } + services.AddSingletonAs<TemplatesClient>() + .AsSelf(); } } diff --git a/backend/src/Squidex/Config/Domain/EventPublishersServices.cs b/backend/src/Squidex/Config/Domain/EventPublishersServices.cs index b7095d87e4..b33589c193 100644 --- a/backend/src/Squidex/Config/Domain/EventPublishersServices.cs +++ b/backend/src/Squidex/Config/Domain/EventPublishersServices.cs @@ -10,64 +10,63 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class EventPublishersServices { - public static class EventPublishersServices + public static void AddSquidexEventPublisher(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexEventPublisher(this IServiceCollection services, IConfiguration config) + var eventPublishers = config.GetSection("eventPublishers"); + + foreach (var child in eventPublishers.GetChildren()) { - var eventPublishers = config.GetSection("eventPublishers"); + var eventPublisherType = child.GetValue<string>("type"); - foreach (var child in eventPublishers.GetChildren()) + if (string.IsNullOrWhiteSpace(eventPublisherType)) { - var eventPublisherType = child.GetValue<string>("type"); + var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:type"); - if (string.IsNullOrWhiteSpace(eventPublisherType)) - { - var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:type"); + throw new ConfigurationException(error); + } - throw new ConfigurationException(error); - } + var eventsFilter = child.GetValue<string>("eventsFilter"); - var eventsFilter = child.GetValue<string>("eventsFilter"); + var enabled = child.GetValue<bool>("enabled"); - var enabled = child.GetValue<bool>("enabled"); + if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase)) + { + var publisherConfig = child.GetValue<string>("configuration"); - if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(publisherConfig)) { - var publisherConfig = child.GetValue<string>("configuration"); - - if (string.IsNullOrWhiteSpace(publisherConfig)) - { - var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:configuration"); + var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:configuration"); - throw new ConfigurationException(error); - } + throw new ConfigurationException(error); + } - var exchange = child.GetValue<string>("exchange"); + var exchange = child.GetValue<string>("exchange"); - if (string.IsNullOrWhiteSpace(exchange)) - { - var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:exchange"); + if (string.IsNullOrWhiteSpace(exchange)) + { + var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:exchange"); - throw new ConfigurationException(error); - } + throw new ConfigurationException(error); + } - var name = $"EventPublishers_{child.Key}"; + var name = $"EventPublishers_{child.Key}"; - if (enabled) - { - services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService<IJsonSerializer>(), name, publisherConfig, exchange, eventsFilter)) - .As<IEventConsumer>(); - } - } - else + if (enabled) { - var error = new ConfigurationError($"Unsupported value '{child.Key}", "eventPublishers:{child.Key}:type."); - - throw new ConfigurationException(error); + services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService<IJsonSerializer>(), name, publisherConfig, exchange, eventsFilter)) + .As<IEventConsumer>(); } } + else + { + var error = new ConfigurationError($"Unsupported value '{child.Key}", "eventPublishers:{child.Key}:type."); + + throw new ConfigurationException(error); + } } } } diff --git a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs index 7d4c007aa2..4a01d5898c 100644 --- a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs +++ b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs @@ -12,60 +12,59 @@ using Squidex.Infrastructure.EventSourcing.Consume; using Squidex.Infrastructure.States; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class EventSourcingServices { - public static class EventSourcingServices + public static void AddSquidexEventSourcing(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexEventSourcing(this IServiceCollection services, IConfiguration config) + config.ConfigureByOption("eventStore:type", new Alternatives { - config.ConfigureByOption("eventStore:type", new Alternatives + ["MongoDb"] = () => { - ["MongoDb"] = () => - { - var mongoConfiguration = config.GetRequiredValue("eventStore:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("eventStore:mongoDb:database"); + var mongoConfiguration = config.GetRequiredValue("eventStore:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("eventStore:mongoDb:database"); - services.AddSingletonAs(c => - { - var mongoClient = StoreServices.GetMongoClient(mongoConfiguration); - var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); + services.AddSingletonAs(c => + { + var mongoClient = StoreServices.GetMongoClient(mongoConfiguration); + var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); - return new MongoEventStore(mongoDatabase, c.GetRequiredService<IEventNotifier>()); - }) - .As<IEventStore>(); - }, - ["GetEventStore"] = () => - { - var configuration = config.GetRequiredValue("eventStore:getEventStore:configuration"); + return new MongoEventStore(mongoDatabase, c.GetRequiredService<IEventNotifier>()); + }) + .As<IEventStore>(); + }, + ["GetEventStore"] = () => + { + var configuration = config.GetRequiredValue("eventStore:getEventStore:configuration"); - services.AddSingletonAs(_ => EventStoreClientSettings.Create(configuration)) - .AsSelf(); + services.AddSingletonAs(_ => EventStoreClientSettings.Create(configuration)) + .AsSelf(); - services.AddSingletonAs<GetEventStore>() - .As<IEventStore>(); + services.AddSingletonAs<GetEventStore>() + .As<IEventStore>(); - services.AddHealthChecks() - .AddCheck<GetEventStoreHealthCheck>("EventStore", tags: new[] { "node" }); - } - }); + services.AddHealthChecks() + .AddCheck<GetEventStoreHealthCheck>("EventStore", tags: new[] { "node" }); + } + }); - services.AddTransientAs<Rebuilder>() - .AsSelf(); + services.AddTransientAs<Rebuilder>() + .AsSelf(); - services.AddSingletonAs<EventConsumerManager>() - .As<IEventConsumerManager>(); + services.AddSingletonAs<EventConsumerManager>() + .As<IEventConsumerManager>(); - services.AddSingletonAs<DefaultEventStreamNames>() - .As<IEventStreamNames>(); + services.AddSingletonAs<DefaultEventStreamNames>() + .As<IEventStreamNames>(); - services.AddSingletonAs<DefaultEventFormatter>() - .As<IEventFormatter>(); + services.AddSingletonAs<DefaultEventFormatter>() + .As<IEventFormatter>(); - services.AddSingletonAs<NoopEventNotifier>() - .As<IEventNotifier>(); + services.AddSingletonAs<NoopEventNotifier>() + .As<IEventNotifier>(); - services.AddSingleton<Func<IEventConsumer, EventConsumerProcessor>>( - sb => c => ActivatorUtilities.CreateInstance<EventConsumerProcessor>(sb, c)); - } + services.AddSingleton<Func<IEventConsumer, EventConsumerProcessor>>( + sb => c => ActivatorUtilities.CreateInstance<EventConsumerProcessor>(sb, c)); } } diff --git a/backend/src/Squidex/Config/Domain/HealthCheckServices.cs b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs index a0d8ad9598..df7b9765b7 100644 --- a/backend/src/Squidex/Config/Domain/HealthCheckServices.cs +++ b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs @@ -7,17 +7,16 @@ using Squidex.Infrastructure.Diagnostics; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class HealthCheckServices { - public static class HealthCheckServices + public static void AddSquidexHealthChecks(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexHealthChecks(this IServiceCollection services, IConfiguration config) - { - services.Configure<GCHealthCheckOptions>(config, - "diagnostics:gc"); + services.Configure<GCHealthCheckOptions>(config, + "diagnostics:gc"); - services.AddHealthChecks() - .AddCheck<GCHealthCheck>("GC", tags: new[] { "node" }); - } + services.AddHealthChecks() + .AddCheck<GCHealthCheck>("GC", tags: new[] { "node" }); } } diff --git a/backend/src/Squidex/Config/Domain/HistoryServices.cs b/backend/src/Squidex/Config/Domain/HistoryServices.cs index d4adc76a79..d73078d6cc 100644 --- a/backend/src/Squidex/Config/Domain/HistoryServices.cs +++ b/backend/src/Squidex/Config/Domain/HistoryServices.cs @@ -9,20 +9,19 @@ using Squidex.Domain.Users; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class HistoryServices { - public static class HistoryServices + public static void AddSquidexHistory(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexHistory(this IServiceCollection services, IConfiguration config) - { - services.Configure<NotifoOptions>( - config.GetSection("notifo")); + services.Configure<NotifoOptions>( + config.GetSection("notifo")); - services.AddSingletonAs<NotifoService>() - .AsSelf().As<IUserEvents>(); + services.AddSingletonAs<NotifoService>() + .AsSelf().As<IUserEvents>(); - services.AddSingletonAs<HistoryService>() - .As<IEventConsumer>().As<IHistoryService>(); - } + services.AddSingletonAs<HistoryService>() + .As<IEventConsumer>().As<IHistoryService>(); } } \ No newline at end of file diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index d155983adf..da11eb7959 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -32,150 +32,149 @@ using Squidex.Web; using Squidex.Web.Pipeline; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class InfrastructureServices { - public static class InfrastructureServices + public static void AddSquidexInfrastructure(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexInfrastructure(this IServiceCollection services, IConfiguration config) - { - services.Configure<ExposedConfiguration>(config, - "exposedConfiguration"); + services.Configure<ExposedConfiguration>(config, + "exposedConfiguration"); - services.Configure<ReplicatedCacheOptions>(config, - "caching:replicated"); + services.Configure<ReplicatedCacheOptions>(config, + "caching:replicated"); - services.Configure<JintScriptOptions>(config, - "scripting"); + services.Configure<JintScriptOptions>(config, + "scripting"); - services.Configure<DiagnoserOptions>(config, - "diagnostics"); + services.Configure<DiagnoserOptions>(config, + "diagnostics"); - services.AddReplicatedCache(); - services.AddAsyncLocalCache(); - services.AddBackgroundCache(); + services.AddReplicatedCache(); + services.AddAsyncLocalCache(); + services.AddBackgroundCache(); - services.AddSingletonAs(_ => SystemClock.Instance) - .As<IClock>(); + services.AddSingletonAs(_ => SystemClock.Instance) + .As<IClock>(); - services.AddSingletonAs<BackgroundRequestLogStore>() - .AsOptional<IRequestLogStore>(); + services.AddSingletonAs<BackgroundRequestLogStore>() + .AsOptional<IRequestLogStore>(); - services.AddSingletonAs<Diagnoser>() - .AsSelf(); + services.AddSingletonAs<Diagnoser>() + .AsSelf(); - services.AddSingletonAs<ScriptingCompleter>() - .AsSelf(); + services.AddSingletonAs<ScriptingCompleter>() + .AsSelf(); - services.AddSingletonAs<JintScriptEngine>() - .As<IScriptEngine>().As<IScriptDescriptor>(); + services.AddSingletonAs<JintScriptEngine>() + .As<IScriptEngine>().As<IScriptDescriptor>(); - services.AddSingletonAs<TagService>() - .As<ITagService>(); + services.AddSingletonAs<TagService>() + .As<ITagService>(); - services.AddSingletonAs<CounterJintExtension>() - .As<IJintExtension>().As<IScriptDescriptor>(); + services.AddSingletonAs<CounterJintExtension>() + .As<IJintExtension>().As<IScriptDescriptor>(); - services.AddSingletonAs<DateTimeJintExtension>() - .As<IJintExtension>().As<IScriptDescriptor>(); + services.AddSingletonAs<DateTimeJintExtension>() + .As<IJintExtension>().As<IScriptDescriptor>(); - services.AddSingletonAs<StringJintExtension>() - .As<IJintExtension>().As<IScriptDescriptor>(); + services.AddSingletonAs<StringJintExtension>() + .As<IJintExtension>().As<IScriptDescriptor>(); - services.AddSingletonAs<StringWordsJintExtension>() - .As<IJintExtension>().As<IScriptDescriptor>(); + services.AddSingletonAs<StringWordsJintExtension>() + .As<IJintExtension>().As<IScriptDescriptor>(); - services.AddSingletonAs<HttpJintExtension>() - .As<IJintExtension>().As<IScriptDescriptor>(); + services.AddSingletonAs<HttpJintExtension>() + .As<IJintExtension>().As<IScriptDescriptor>(); - services.AddSingletonAs<FluidTemplateEngine>() - .AsOptional<ITemplateEngine>(); + services.AddSingletonAs<FluidTemplateEngine>() + .AsOptional<ITemplateEngine>(); - services.AddSingletonAs<ContentFluidExtension>() - .As<IFluidExtension>(); + services.AddSingletonAs<ContentFluidExtension>() + .As<IFluidExtension>(); - services.AddSingletonAs<DateTimeFluidExtension>() - .As<IFluidExtension>(); + services.AddSingletonAs<DateTimeFluidExtension>() + .As<IFluidExtension>(); - services.AddSingletonAs<StringFluidExtension>() - .As<IFluidExtension>(); + services.AddSingletonAs<StringFluidExtension>() + .As<IFluidExtension>(); - services.AddSingletonAs<StringWordsFluidExtension>() - .As<IFluidExtension>(); + services.AddSingletonAs<StringWordsFluidExtension>() + .As<IFluidExtension>(); - services.AddSingletonAs<UserFluidExtension>() - .As<IFluidExtension>(); - } + services.AddSingletonAs<UserFluidExtension>() + .As<IFluidExtension>(); + } - public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config) - { - services.Configure<UsageOptions>(config, - "usage"); + public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config) + { + services.Configure<UsageOptions>(config, + "usage"); - services.AddSingletonAs(c => new CachingUsageTracker( - c.GetRequiredService<BackgroundUsageTracker>(), - c.GetRequiredService<IMemoryCache>())) - .As<IUsageTracker>(); + services.AddSingletonAs(c => new CachingUsageTracker( + c.GetRequiredService<BackgroundUsageTracker>(), + c.GetRequiredService<IMemoryCache>())) + .As<IUsageTracker>(); - services.AddSingletonAs<ApiUsageTracker>() - .As<IApiUsageTracker>(); + services.AddSingletonAs<ApiUsageTracker>() + .As<IApiUsageTracker>(); - services.AddSingletonAs<BackgroundUsageTracker>() - .AsSelf(); - } + services.AddSingletonAs<BackgroundUsageTracker>() + .AsSelf(); + } - public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config) - { - services.Configure<GoogleCloudTranslationOptions>(config, - "translations:googleCloud"); + public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config) + { + services.Configure<GoogleCloudTranslationOptions>(config, + "translations:googleCloud"); - services.Configure<DeepLOptions>(config, - "translations:deepL"); + services.Configure<DeepLOptions>(config, + "translations:deepL"); - services.Configure<LanguagesOptions>(config, - "languages"); + services.Configure<LanguagesOptions>(config, + "languages"); - services.AddSingletonAs<LanguagesInitializer>() - .AsSelf(); + services.AddSingletonAs<LanguagesInitializer>() + .AsSelf(); - services.AddSingletonAs(c => new DeepLTranslationService(c.GetRequiredService<IOptions<DeepLOptions>>().Value)) - .As<ITranslationService>(); + services.AddSingletonAs(c => new DeepLTranslationService(c.GetRequiredService<IOptions<DeepLOptions>>().Value)) + .As<ITranslationService>(); - services.AddSingletonAs(c => new GoogleCloudTranslationService(c.GetRequiredService<IOptions<GoogleCloudTranslationOptions>>().Value)) - .As<ITranslationService>(); + services.AddSingletonAs(c => new GoogleCloudTranslationService(c.GetRequiredService<IOptions<GoogleCloudTranslationOptions>>().Value)) + .As<ITranslationService>(); - services.AddSingletonAs<Translator>() - .As<ITranslator>(); - } + services.AddSingletonAs<Translator>() + .As<ITranslator>(); + } - public static void AddSquidexLocalization(this IServiceCollection services) - { - var translator = new ResourcesLocalizer(Texts.ResourceManager); + public static void AddSquidexLocalization(this IServiceCollection services) + { + var translator = new ResourcesLocalizer(Texts.ResourceManager); - T.Setup(translator); + T.Setup(translator); - services.AddSingletonAs(c => translator) - .As<ILocalizer>(); - } + services.AddSingletonAs(c => translator) + .As<ILocalizer>(); + } - public static void AddSquidexControllerServices(this IServiceCollection services, IConfiguration config) - { - services.Configure<RobotsTxtOptions>(config, - "robots"); + public static void AddSquidexControllerServices(this IServiceCollection services, IConfiguration config) + { + services.Configure<RobotsTxtOptions>(config, + "robots"); - services.Configure<CachingOptions>(config, - "caching"); + services.Configure<CachingOptions>(config, + "caching"); - services.Configure<MyUIOptions>(config, - "ui"); + services.Configure<MyUIOptions>(config, + "ui"); - services.Configure<MyNewsOptions>(config, - "news"); + services.Configure<MyNewsOptions>(config, + "news"); - services.AddSingletonAs<FeaturesService>() - .AsSelf(); + services.AddSingletonAs<FeaturesService>() + .AsSelf(); - services.AddSingletonAs<SchemasOpenApiGenerator>() - .AsSelf(); - } + services.AddSingletonAs<SchemasOpenApiGenerator>() + .AsSelf(); } } diff --git a/backend/src/Squidex/Config/Domain/LoggingServices.cs b/backend/src/Squidex/Config/Domain/LoggingServices.cs index 16e0e261c9..3c14715a8f 100644 --- a/backend/src/Squidex/Config/Domain/LoggingServices.cs +++ b/backend/src/Squidex/Config/Domain/LoggingServices.cs @@ -11,75 +11,74 @@ using Squidex.Log; using Squidex.Web.Pipeline; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class LoggingServices { - public static class LoggingServices + public static void ConfigureForSquidex(this ILoggingBuilder builder, IConfiguration config) { - public static void ConfigureForSquidex(this ILoggingBuilder builder, IConfiguration config) - { - builder.ClearProviders(); + builder.ClearProviders(); - // Also adds semantic logging. - builder.ConfigureSemanticLog(config); + // Also adds semantic logging. + builder.ConfigureSemanticLog(config); - builder.AddConfiguration(config.GetSection("logging")); - builder.AddFilters(); + builder.AddConfiguration(config.GetSection("logging")); + builder.AddFilters(); - builder.Services.AddServices(config); - } + builder.Services.AddServices(config); + } - private static void AddServices(this IServiceCollection services, IConfiguration config) - { - services.Configure<RequestLogOptions>(config, - "logging"); + private static void AddServices(this IServiceCollection services, IConfiguration config) + { + services.Configure<RequestLogOptions>(config, + "logging"); - services.Configure<RequestLogStoreOptions>(config, - "logging"); + services.Configure<RequestLogStoreOptions>(config, + "logging"); - services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(LoggingServices).Assembly, Guid.NewGuid())) - .As<ILogAppender>(); + services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(LoggingServices).Assembly, Guid.NewGuid())) + .As<ILogAppender>(); - services.AddSingletonAs<ActionContextLogAppender>() - .As<ILogAppender>(); - } + services.AddSingletonAs<ActionContextLogAppender>() + .As<ILogAppender>(); + } - private static void AddFilters(this ILoggingBuilder builder) + private static void AddFilters(this ILoggingBuilder builder) + { + builder.AddFilter((category, level) => { - builder.AddFilter((category, level) => - { #if LOG_ALL_IDENTITY_SERVER - if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase)) - { - return true; - } -#endif - if (level < LogLevel.Information) - { - return false; - } - - if (category.StartsWith("OpenIddict", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } - - if (category.StartsWith("Runtime.", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } - - if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } + if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase)) + { return true; - }); - } + } +#endif + if (level < LogLevel.Information) + { + return false; + } + + if (category.StartsWith("OpenIddict", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + if (category.StartsWith("Runtime.", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + if (category.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)) + { + return level >= LogLevel.Warning; + } + + return true; + }); } } diff --git a/backend/src/Squidex/Config/Domain/MigrationServices.cs b/backend/src/Squidex/Config/Domain/MigrationServices.cs index cd4bd1ef50..ce6e2f8d63 100644 --- a/backend/src/Squidex/Config/Domain/MigrationServices.cs +++ b/backend/src/Squidex/Config/Domain/MigrationServices.cs @@ -9,59 +9,58 @@ using Migrations.Migrations; using Squidex.Infrastructure.Migrations; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class MigrationServices { - public static class MigrationServices + public static void AddSquidexMigration(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexMigration(this IServiceCollection services, IConfiguration config) - { - services.Configure<RebuildOptions>(config, - "rebuild"); + services.Configure<RebuildOptions>(config, + "rebuild"); - services.AddSingletonAs<Migrator>() - .AsSelf(); + services.AddSingletonAs<Migrator>() + .AsSelf(); - services.AddTransientAs<RebuildRunner>() - .AsSelf(); + services.AddTransientAs<RebuildRunner>() + .AsSelf(); - services.AddTransientAs<MigrationPath>() - .As<IMigrationPath>(); + services.AddTransientAs<MigrationPath>() + .As<IMigrationPath>(); - services.AddTransientAs<ConvertEventStore>() - .As<IMigration>(); + services.AddTransientAs<ConvertEventStore>() + .As<IMigration>(); - services.AddTransientAs<ConvertEventStoreAppId>() - .As<IMigration>(); + services.AddTransientAs<ConvertEventStoreAppId>() + .As<IMigration>(); - services.AddTransientAs<ClearRules>() - .As<IMigration>(); + services.AddTransientAs<ClearRules>() + .As<IMigration>(); - services.AddTransientAs<ClearSchemas>() - .As<IMigration>(); + services.AddTransientAs<ClearSchemas>() + .As<IMigration>(); - services.AddTransientAs<CreateAssetSlugs>() - .As<IMigration>(); + services.AddTransientAs<CreateAssetSlugs>() + .As<IMigration>(); - services.AddTransientAs<RebuildContents>() - .As<IMigration>(); + services.AddTransientAs<RebuildContents>() + .As<IMigration>(); - services.AddTransientAs<RebuildSnapshots>() - .As<IMigration>(); + services.AddTransientAs<RebuildSnapshots>() + .As<IMigration>(); - services.AddTransientAs<RebuildApps>() - .As<IMigration>(); + services.AddTransientAs<RebuildApps>() + .As<IMigration>(); - services.AddTransientAs<RebuildSchemas>() - .As<IMigration>(); + services.AddTransientAs<RebuildSchemas>() + .As<IMigration>(); - services.AddTransientAs<RebuildRules>() - .As<IMigration>(); + services.AddTransientAs<RebuildRules>() + .As<IMigration>(); - services.AddTransientAs<RebuildAssets>() - .As<IMigration>(); + services.AddTransientAs<RebuildAssets>() + .As<IMigration>(); - services.AddTransientAs<RebuildAssetFolders>() - .As<IMigration>(); - } + services.AddTransientAs<RebuildAssetFolders>() + .As<IMigration>(); } } diff --git a/backend/src/Squidex/Config/Domain/NotificationsServices.cs b/backend/src/Squidex/Config/Domain/NotificationsServices.cs index d6d58cbd36..d94f733b40 100644 --- a/backend/src/Squidex/Config/Domain/NotificationsServices.cs +++ b/backend/src/Squidex/Config/Domain/NotificationsServices.cs @@ -11,35 +11,34 @@ using Squidex.Infrastructure.Email; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class NotificationsServices { - public static class NotificationsServices + public static void AddSquidexNotifications(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexNotifications(this IServiceCollection services, IConfiguration config) - { - var emailOptions = config.GetSection("email:smtp").Get<SmtpOptions>() ?? new (); + var emailOptions = config.GetSection("email:smtp").Get<SmtpOptions>() ?? new (); - if (emailOptions.IsConfigured()) - { - services.AddSingleton(Options.Create(emailOptions)); - - services.Configure<EmailUserNotificationOptions>(config, - "email:notifications"); + if (emailOptions.IsConfigured()) + { + services.AddSingleton(Options.Create(emailOptions)); - services.AddSingletonAs<SmtpEmailSender>() - .As<IEmailSender>(); + services.Configure<EmailUserNotificationOptions>(config, + "email:notifications"); - services.AddSingletonAs<EmailUserNotifications>() - .AsOptional<IUserNotifications>(); - } - else - { - services.AddSingletonAs<NoopUserNotifications>() - .AsOptional<IUserNotifications>(); - } + services.AddSingletonAs<SmtpEmailSender>() + .As<IEmailSender>(); - services.AddSingletonAs<InvitationEventConsumer>() - .As<IEventConsumer>(); + services.AddSingletonAs<EmailUserNotifications>() + .AsOptional<IUserNotifications>(); } + else + { + services.AddSingletonAs<NoopUserNotifications>() + .AsOptional<IUserNotifications>(); + } + + services.AddSingletonAs<InvitationEventConsumer>() + .As<IEventConsumer>(); } } diff --git a/backend/src/Squidex/Config/Domain/QueryServices.cs b/backend/src/Squidex/Config/Domain/QueryServices.cs index aa132bb2ca..918960e4d4 100644 --- a/backend/src/Squidex/Config/Domain/QueryServices.cs +++ b/backend/src/Squidex/Config/Domain/QueryServices.cs @@ -11,29 +11,28 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Web.Services; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class QueryServices { - public static class QueryServices + public static void AddSquidexQueries(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexQueries(this IServiceCollection services, IConfiguration config) - { - services.Configure<GraphQLOptions>(config, - "graphql"); + services.Configure<GraphQLOptions>(config, + "graphql"); - services.AddSingletonAs<StringReferenceExtractor>() - .AsSelf(); + services.AddSingletonAs<StringReferenceExtractor>() + .AsSelf(); - services.AddSingletonAs<UrlGenerator>() - .As<IUrlGenerator>(); + services.AddSingletonAs<UrlGenerator>() + .As<IUrlGenerator>(); - services.AddSingletonAs<InstantGraphType>() - .AsSelf(); + services.AddSingletonAs<InstantGraphType>() + .AsSelf(); - services.AddSingletonAs<JsonGraphType>() - .AsSelf(); + services.AddSingletonAs<JsonGraphType>() + .AsSelf(); - services.AddSingletonAs<JsonNoopGraphType>() - .AsSelf(); - } + services.AddSingletonAs<JsonNoopGraphType>() + .AsSelf(); } } diff --git a/backend/src/Squidex/Config/Domain/ResizeServices.cs b/backend/src/Squidex/Config/Domain/ResizeServices.cs index 55b81ad19b..1304d18378 100644 --- a/backend/src/Squidex/Config/Domain/ResizeServices.cs +++ b/backend/src/Squidex/Config/Domain/ResizeServices.cs @@ -8,36 +8,35 @@ using Squidex.Assets; using Squidex.Assets.Remote; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class ResizeServices { - public static class ResizeServices + public static void AddSquidexImageResizing(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexImageResizing(this IServiceCollection services, IConfiguration config) - { - var thumbnailGenerator = new CompositeThumbnailGenerator( - new IAssetThumbnailGenerator[] - { - new ImageSharpThumbnailGenerator(), - new ImageMagickThumbnailGenerator() - }); + var thumbnailGenerator = new CompositeThumbnailGenerator( + new IAssetThumbnailGenerator[] + { + new ImageSharpThumbnailGenerator(), + new ImageMagickThumbnailGenerator() + }); - var resizerUrl = config.GetValue<string>("assets:resizerUrl"); + var resizerUrl = config.GetValue<string>("assets:resizerUrl"); - if (!string.IsNullOrWhiteSpace(resizerUrl)) + if (!string.IsNullOrWhiteSpace(resizerUrl)) + { + services.AddHttpClient("Resize", options => { - services.AddHttpClient("Resize", options => - { - options.BaseAddress = new Uri(resizerUrl); - }); + options.BaseAddress = new Uri(resizerUrl); + }); - services.AddSingletonAs(c => new RemoteThumbnailGenerator(c.GetRequiredService<IHttpClientFactory>(), thumbnailGenerator)) - .As<IAssetThumbnailGenerator>(); - } - else - { - services.AddSingletonAs(c => thumbnailGenerator) - .As<IAssetThumbnailGenerator>(); - } + services.AddSingletonAs(c => new RemoteThumbnailGenerator(c.GetRequiredService<IHttpClientFactory>(), thumbnailGenerator)) + .As<IAssetThumbnailGenerator>(); + } + else + { + services.AddSingletonAs(c => thumbnailGenerator) + .As<IAssetThumbnailGenerator>(); } } } diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index b8557c43e9..c91021ade6 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -22,85 +22,84 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class RuleServices { - public static class RuleServices + public static void AddSquidexRules(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexRules(this IServiceCollection services, IConfiguration config) - { - services.Configure<RuleOptions>(config, - "rules"); + services.Configure<RuleOptions>(config, + "rules"); - services.AddSingletonAs<EventEnricher>() - .As<IEventEnricher>(); + services.AddSingletonAs<EventEnricher>() + .As<IEventEnricher>(); - services.AddSingletonAs<AssetChangedTriggerHandler>() - .As<IRuleTriggerHandler>().As<ISubscriptionEventCreator>(); + services.AddSingletonAs<AssetChangedTriggerHandler>() + .As<IRuleTriggerHandler>().As<ISubscriptionEventCreator>(); - services.AddSingletonAs<CommentTriggerHandler>() - .As<IRuleTriggerHandler>(); + services.AddSingletonAs<CommentTriggerHandler>() + .As<IRuleTriggerHandler>(); - services.AddSingletonAs<ContentChangedTriggerHandler>() - .As<IRuleTriggerHandler>().As<ISubscriptionEventCreator>(); + services.AddSingletonAs<ContentChangedTriggerHandler>() + .As<IRuleTriggerHandler>().As<ISubscriptionEventCreator>(); - services.AddSingletonAs<AssetsFluidExtension>() - .As<IFluidExtension>(); + services.AddSingletonAs<AssetsFluidExtension>() + .As<IFluidExtension>(); - services.AddSingletonAs<AssetsJintExtension>() - .As<IJintExtension>().As<IScriptDescriptor>(); + services.AddSingletonAs<AssetsJintExtension>() + .As<IJintExtension>().As<IScriptDescriptor>(); - services.AddSingletonAs<ReferencesFluidExtension>() - .As<IFluidExtension>(); + services.AddSingletonAs<ReferencesFluidExtension>() + .As<IFluidExtension>(); - services.AddSingletonAs<ReferencesJintExtension>() - .As<IJintExtension>().As<IScriptDescriptor>(); + services.AddSingletonAs<ReferencesJintExtension>() + .As<IJintExtension>().As<IScriptDescriptor>(); - services.AddSingletonAs<ManualTriggerHandler>() - .As<IRuleTriggerHandler>(); + services.AddSingletonAs<ManualTriggerHandler>() + .As<IRuleTriggerHandler>(); - services.AddSingletonAs<SchemaChangedTriggerHandler>() - .As<IRuleTriggerHandler>(); + services.AddSingletonAs<SchemaChangedTriggerHandler>() + .As<IRuleTriggerHandler>(); - services.AddSingletonAs<UsageTriggerHandler>() - .As<IRuleTriggerHandler>(); + services.AddSingletonAs<UsageTriggerHandler>() + .As<IRuleTriggerHandler>(); - services.AddSingletonAs<RuleQueryService>() - .As<IRuleQueryService>(); + services.AddSingletonAs<RuleQueryService>() + .As<IRuleQueryService>(); - services.AddSingletonAs<DefaultRuleRunnerService>() - .As<IRuleRunnerService>(); + services.AddSingletonAs<DefaultRuleRunnerService>() + .As<IRuleRunnerService>(); - services.AddSingletonAs<RuleEnricher>() - .As<IRuleEnricher>(); + services.AddSingletonAs<RuleEnricher>() + .As<IRuleEnricher>(); - services.AddSingletonAs<RuleEnqueuer>() - .As<IRuleEnqueuer>().As<IEventConsumer>(); + services.AddSingletonAs<RuleEnqueuer>() + .As<IRuleEnqueuer>().As<IEventConsumer>(); - services.AddSingletonAs<EventJsonSchemaGenerator>() - .AsSelf(); + services.AddSingletonAs<EventJsonSchemaGenerator>() + .AsSelf(); - services.AddSingletonAs<RuleTypeProvider>() - .As<ITypeProvider>().AsSelf(); + services.AddSingletonAs<RuleTypeProvider>() + .As<ITypeProvider>().AsSelf(); - services.AddSingletonAs<EventJintExtension>() - .As<IJintExtension>().As<IScriptDescriptor>(); + services.AddSingletonAs<EventJintExtension>() + .As<IJintExtension>().As<IScriptDescriptor>(); - services.AddSingletonAs<EventFluidExtensions>() - .As<IFluidExtension>(); + services.AddSingletonAs<EventFluidExtensions>() + .As<IFluidExtension>(); - services.AddSingletonAs<PredefinedPatternsFormatter>() - .As<IRuleEventFormatter>(); + services.AddSingletonAs<PredefinedPatternsFormatter>() + .As<IRuleEventFormatter>(); - services.AddSingletonAs<RuleService>() - .As<IRuleService>(); + services.AddSingletonAs<RuleService>() + .As<IRuleService>(); - services.AddSingletonAs<RuleEventFormatter>() - .AsSelf(); + services.AddSingletonAs<RuleEventFormatter>() + .AsSelf(); - services.AddInitializer<RuleTypeProvider>("Serializer (Rules)", registry => - { - RuleActionConverter.Mapping = registry.Actions.ToDictionary(x => x.Key, x => x.Value.Type); - }, -1); - } + services.AddInitializer<RuleTypeProvider>("Serializer (Rules)", registry => + { + RuleActionConverter.Mapping = registry.Actions.ToDictionary(x => x.Key, x => x.Value.Type); + }, -1); } } diff --git a/backend/src/Squidex/Config/Domain/SchemasServices.cs b/backend/src/Squidex/Config/Domain/SchemasServices.cs index 0ad86e2f53..4f00654612 100644 --- a/backend/src/Squidex/Config/Domain/SchemasServices.cs +++ b/backend/src/Squidex/Config/Domain/SchemasServices.cs @@ -9,17 +9,16 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Search; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class SchemasServices { - public static class SchemasServices + public static void AddSquidexSchemas(this IServiceCollection services) { - public static void AddSquidexSchemas(this IServiceCollection services) - { - services.AddTransientAs<SchemasSearchSource>() - .As<ISearchSource>(); + services.AddTransientAs<SchemasSearchSource>() + .As<ISearchSource>(); - services.AddSingletonAs<SchemaHistoryEventsCreator>() - .As<IHistoryEventsCreator>(); - } + services.AddSingletonAs<SchemaHistoryEventsCreator>() + .As<IHistoryEventsCreator>(); } } diff --git a/backend/src/Squidex/Config/Domain/SearchServices.cs b/backend/src/Squidex/Config/Domain/SearchServices.cs index 8b1bb37060..8d1ccd88e0 100644 --- a/backend/src/Squidex/Config/Domain/SearchServices.cs +++ b/backend/src/Squidex/Config/Domain/SearchServices.cs @@ -7,14 +7,13 @@ using Squidex.Domain.Apps.Entities.Search; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class SearchServices { - public static class SearchServices + public static void AddSquidexSearch(this IServiceCollection services) { - public static void AddSquidexSearch(this IServiceCollection services) - { - services.AddSingletonAs<SearchManager>() - .As<ISearchManager>(); - } + services.AddSingletonAs<SearchManager>() + .As<ISearchManager>(); } } diff --git a/backend/src/Squidex/Config/Domain/SerializationServices.cs b/backend/src/Squidex/Config/Domain/SerializationServices.cs index a2688d3eff..cff287daf5 100644 --- a/backend/src/Squidex/Config/Domain/SerializationServices.cs +++ b/backend/src/Squidex/Config/Domain/SerializationServices.cs @@ -32,98 +32,97 @@ using Squidex.Infrastructure.Queries.Json; using Squidex.Infrastructure.Reflection; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class SerializationServices { - public static class SerializationServices + private static JsonSerializerOptions ConfigureJson(TypeNameRegistry typeNameRegistry, JsonSerializerOptions? options = null) { - private static JsonSerializerOptions ConfigureJson(TypeNameRegistry typeNameRegistry, JsonSerializerOptions? options = null) - { - options ??= new JsonSerializerOptions(JsonSerializerDefaults.Web); - - options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. - options.Converters.Add(new StringConverter<PropertyPath>(x => x)); - options.Converters.Add(new GeoJsonConverterFactory()); - options.Converters.Add(new InheritanceConverter<IEvent>(typeNameRegistry)); - options.Converters.Add(new InheritanceConverter<FieldProperties>(typeNameRegistry)); - options.Converters.Add(new InheritanceConverter<RuleAction>(typeNameRegistry)); - options.Converters.Add(new InheritanceConverter<RuleTrigger>(typeNameRegistry)); - options.Converters.Add(new JsonValueConverter()); - options.Converters.Add(new ReadonlyDictionaryConverterFactory()); - options.Converters.Add(new ReadonlyListConverterFactory()); - options.Converters.Add(new SurrogateJsonConverter<ClaimsPrincipal, ClaimsPrincipalSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<FilterNode<JsonValue>, JsonFilterSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<LanguageConfig, LanguageConfigSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<LanguagesConfig, LanguagesConfigSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<Roles, RolesSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<Rule, RuleSorrgate>()); - options.Converters.Add(new SurrogateJsonConverter<Schema, SchemaSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<WorkflowStep, WorkflowStepSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<WorkflowTransition, WorkflowTransitionSurrogate>()); - options.Converters.Add(new StringConverter<CompareOperator>()); - options.Converters.Add(new StringConverter<DomainId>()); - options.Converters.Add(new StringConverter<NamedId<DomainId>>()); - options.Converters.Add(new StringConverter<NamedId<Guid>>()); - options.Converters.Add(new StringConverter<NamedId<long>>()); - options.Converters.Add(new StringConverter<NamedId<string>>()); - options.Converters.Add(new StringConverter<Language>()); - options.Converters.Add(new StringConverter<PropertyPath>(x => x)); - options.Converters.Add(new StringConverter<RefToken>()); - options.Converters.Add(new StringConverter<Status>()); - options.Converters.Add(new JsonStringEnumConverter()); - options.IncludeFields = true; - - return options; - } - - public static IServiceCollection AddSquidexSerializers(this IServiceCollection services) - { - services.AddSingletonAs<AutoAssembyTypeProvider<SquidexCoreModel>>() - .As<ITypeProvider>(); + options ??= new JsonSerializerOptions(JsonSerializerDefaults.Web); + + options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. + options.Converters.Add(new StringConverter<PropertyPath>(x => x)); + options.Converters.Add(new GeoJsonConverterFactory()); + options.Converters.Add(new InheritanceConverter<IEvent>(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter<FieldProperties>(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter<RuleAction>(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter<RuleTrigger>(typeNameRegistry)); + options.Converters.Add(new JsonValueConverter()); + options.Converters.Add(new ReadonlyDictionaryConverterFactory()); + options.Converters.Add(new ReadonlyListConverterFactory()); + options.Converters.Add(new SurrogateJsonConverter<ClaimsPrincipal, ClaimsPrincipalSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<FilterNode<JsonValue>, JsonFilterSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<LanguageConfig, LanguageConfigSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<LanguagesConfig, LanguagesConfigSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<Roles, RolesSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<Rule, RuleSorrgate>()); + options.Converters.Add(new SurrogateJsonConverter<Schema, SchemaSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<WorkflowStep, WorkflowStepSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<WorkflowTransition, WorkflowTransitionSurrogate>()); + options.Converters.Add(new StringConverter<CompareOperator>()); + options.Converters.Add(new StringConverter<DomainId>()); + options.Converters.Add(new StringConverter<NamedId<DomainId>>()); + options.Converters.Add(new StringConverter<NamedId<Guid>>()); + options.Converters.Add(new StringConverter<NamedId<long>>()); + options.Converters.Add(new StringConverter<NamedId<string>>()); + options.Converters.Add(new StringConverter<Language>()); + options.Converters.Add(new StringConverter<PropertyPath>(x => x)); + options.Converters.Add(new StringConverter<RefToken>()); + options.Converters.Add(new StringConverter<Status>()); + options.Converters.Add(new JsonStringEnumConverter()); + options.IncludeFields = true; + + return options; + } - services.AddSingletonAs<AutoAssembyTypeProvider<SquidexEvents>>() - .As<ITypeProvider>(); + public static IServiceCollection AddSquidexSerializers(this IServiceCollection services) + { + services.AddSingletonAs<AutoAssembyTypeProvider<SquidexCoreModel>>() + .As<ITypeProvider>(); - services.AddSingletonAs<AutoAssembyTypeProvider<SquidexInfrastructure>>() - .As<ITypeProvider>(); + services.AddSingletonAs<AutoAssembyTypeProvider<SquidexEvents>>() + .As<ITypeProvider>(); - services.AddSingletonAs<AutoAssembyTypeProvider<SquidexMigrations>>() - .As<ITypeProvider>(); + services.AddSingletonAs<AutoAssembyTypeProvider<SquidexInfrastructure>>() + .As<ITypeProvider>(); - services.AddSingletonAs<FieldTypeProvider>() - .As<ITypeProvider>(); + services.AddSingletonAs<AutoAssembyTypeProvider<SquidexMigrations>>() + .As<ITypeProvider>(); - services.AddSingletonAs<SystemJsonSerializer>() - .As<IJsonSerializer>(); + services.AddSingletonAs<FieldTypeProvider>() + .As<ITypeProvider>(); - services.AddSingletonAs<TypeNameRegistry>() - .AsSelf(); + services.AddSingletonAs<SystemJsonSerializer>() + .As<IJsonSerializer>(); - services.AddSingletonAs(c => ConfigureJson(c.GetRequiredService<TypeNameRegistry>())) - .As<JsonSerializerOptions>(); + services.AddSingletonAs<TypeNameRegistry>() + .AsSelf(); - services.Configure<JsonSerializerOptions>((c, options) => - { - ConfigureJson(c.GetRequiredService<TypeNameRegistry>(), options); - }); + services.AddSingletonAs(c => ConfigureJson(c.GetRequiredService<TypeNameRegistry>())) + .As<JsonSerializerOptions>(); - return services; - } + services.Configure<JsonSerializerOptions>((c, options) => + { + ConfigureJson(c.GetRequiredService<TypeNameRegistry>(), options); + }); + + return services; + } - public static IMvcBuilder AddSquidexSerializers(this IMvcBuilder builder) + public static IMvcBuilder AddSquidexSerializers(this IMvcBuilder builder) + { + builder.Services.Configure<JsonOptions>((c, options) => { - builder.Services.Configure<JsonOptions>((c, options) => - { - ConfigureJson(c.GetRequiredService<TypeNameRegistry>(), options.JsonSerializerOptions); + ConfigureJson(c.GetRequiredService<TypeNameRegistry>(), options.JsonSerializerOptions); - // Do not write null values. - options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + // Do not write null values. + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - // Expose the json exceptions to the model state. - options.AllowInputFormatterExceptionMessages = false; - }); + // Expose the json exceptions to the model state. + options.AllowInputFormatterExceptionMessages = false; + }); - return builder; - } + return builder; } } diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index 963f3972fa..be7ad511d0 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -52,176 +52,175 @@ using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class StoreServices { - public static class StoreServices + public static void AddSquidexStoreServices(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexStoreServices(this IServiceCollection services, IConfiguration config) + config.ConfigureByOption("store:type", new Alternatives { - config.ConfigureByOption("store:type", new Alternatives + ["MongoDB"] = () => { - ["MongoDB"] = () => - { - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName); + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName); - services.AddSingleton(typeof(ISnapshotStore<>), typeof(MongoSnapshotStore<>)); + services.AddSingleton(typeof(ISnapshotStore<>), typeof(MongoSnapshotStore<>)); - services.AddSingletonAs(c => GetMongoClient(mongoConfiguration)) - .As<IMongoClient>(); + services.AddSingletonAs(c => GetMongoClient(mongoConfiguration)) + .As<IMongoClient>(); - services.AddSingletonAs(c => GetDatabase(c, mongoDatabaseName)) - .As<IMongoDatabase>(); + services.AddSingletonAs(c => GetDatabase(c, mongoDatabaseName)) + .As<IMongoDatabase>(); - services.AddSingletonAs<MongoMigrationStatus>() - .As<IMigrationStatus>(); + services.AddSingletonAs<MongoMigrationStatus>() + .As<IMigrationStatus>(); - services.AddTransientAs<ConvertOldSnapshotStores>() - .As<IMigration>(); + services.AddTransientAs<ConvertOldSnapshotStores>() + .As<IMigration>(); - services.AddTransientAs(c => new DeleteContentCollections(GetDatabase(c, mongoContentDatabaseName))) - .As<IMigration>(); + services.AddTransientAs(c => new DeleteContentCollections(GetDatabase(c, mongoContentDatabaseName))) + .As<IMigration>(); - services.AddTransientAs(c => new RestructureContentCollection(GetDatabase(c, mongoContentDatabaseName))) - .As<IMigration>(); + services.AddTransientAs(c => new RestructureContentCollection(GetDatabase(c, mongoContentDatabaseName))) + .As<IMigration>(); - services.AddTransientAs(c => new ConvertDocumentIds(GetDatabase(c, mongoDatabaseName), GetDatabase(c, mongoContentDatabaseName))) - .As<IMigration>(); + services.AddTransientAs(c => new ConvertDocumentIds(GetDatabase(c, mongoDatabaseName), GetDatabase(c, mongoContentDatabaseName))) + .As<IMigration>(); - services.AddSingletonAs(c => ActivatorUtilities.CreateInstance<MongoContentRepository>(c, GetDatabase(c, mongoContentDatabaseName))) - .As<IContentRepository>().As<ISnapshotStore<ContentDomainObject.State>>().As<IDeleter>(); + services.AddSingletonAs(c => ActivatorUtilities.CreateInstance<MongoContentRepository>(c, GetDatabase(c, mongoContentDatabaseName))) + .As<IContentRepository>().As<ISnapshotStore<ContentDomainObject.State>>().As<IDeleter>(); - services.AddTransientAs<ConvertRuleEventsJson>() - .As<IMigration>(); + services.AddTransientAs<ConvertRuleEventsJson>() + .As<IMigration>(); - services.AddTransientAs<RenameAssetSlugField>() - .As<IMigration>(); + services.AddTransientAs<RenameAssetSlugField>() + .As<IMigration>(); - services.AddTransientAs<RenameAssetMetadata>() - .As<IMigration>(); + services.AddTransientAs<RenameAssetMetadata>() + .As<IMigration>(); - services.AddTransientAs<AddAppIdToEventStream>() - .As<IMigration>(); + services.AddTransientAs<AddAppIdToEventStream>() + .As<IMigration>(); - services.AddSingletonAs<MongoDistributedCache>() - .As<IDistributedCache>(); + services.AddSingletonAs<MongoDistributedCache>() + .As<IDistributedCache>(); - services.AddHealthChecks() - .AddCheck<MongoHealthCheck>("MongoDB", tags: new[] { "node" }); + services.AddHealthChecks() + .AddCheck<MongoHealthCheck>("MongoDB", tags: new[] { "node" }); - services.AddSingletonAs<MongoAssetKeyValueStore<TusMetadata>>() - .As<IAssetKeyValueStore<TusMetadata>>(); + services.AddSingletonAs<MongoAssetKeyValueStore<TusMetadata>>() + .As<IAssetKeyValueStore<TusMetadata>>(); - services.AddSingletonAs<MongoRequestLogRepository>() - .As<IRequestLogRepository>(); + services.AddSingletonAs<MongoRequestLogRepository>() + .As<IRequestLogRepository>(); - services.AddSingletonAs<MongoUsageRepository>() - .As<IUsageRepository>(); + services.AddSingletonAs<MongoUsageRepository>() + .As<IUsageRepository>(); - services.AddSingletonAs<MongoRuleEventRepository>() - .As<IRuleEventRepository>().As<IDeleter>(); + services.AddSingletonAs<MongoRuleEventRepository>() + .As<IRuleEventRepository>().As<IDeleter>(); - services.AddSingletonAs<MongoHistoryEventRepository>() - .As<IHistoryEventRepository>().As<IDeleter>(); + services.AddSingletonAs<MongoHistoryEventRepository>() + .As<IHistoryEventRepository>().As<IDeleter>(); - services.AddSingletonAs<MongoRoleStore>() - .As<IRoleStore<IdentityRole>>(); + services.AddSingletonAs<MongoRoleStore>() + .As<IRoleStore<IdentityRole>>(); - services.AddSingletonAs<MongoUserStore>() - .As<IUserStore<IdentityUser>>().As<IUserFactory>(); + services.AddSingletonAs<MongoUserStore>() + .As<IUserStore<IdentityUser>>().As<IUserFactory>(); - services.AddSingletonAs<MongoAssetRepository>() - .As<IAssetRepository>().As<ISnapshotStore<AssetDomainObject.State>>().As<IDeleter>(); + services.AddSingletonAs<MongoAssetRepository>() + .As<IAssetRepository>().As<ISnapshotStore<AssetDomainObject.State>>().As<IDeleter>(); - services.AddSingletonAs<MongoAssetFolderRepository>() - .As<IAssetFolderRepository>().As<ISnapshotStore<AssetFolderDomainObject.State>>().As<IDeleter>(); + services.AddSingletonAs<MongoAssetFolderRepository>() + .As<IAssetFolderRepository>().As<ISnapshotStore<AssetFolderDomainObject.State>>().As<IDeleter>(); - services.AddSingletonAs<MongoAppRepository>() - .As<IAppRepository>().As<ISnapshotStore<AppDomainObject.State>>().As<IDeleter>(); + services.AddSingletonAs<MongoAppRepository>() + .As<IAppRepository>().As<ISnapshotStore<AppDomainObject.State>>().As<IDeleter>(); - services.AddSingletonAs<MongoTeamRepository>() - .As<ITeamRepository>().As<ISnapshotStore<TeamDomainObject.State>>(); + services.AddSingletonAs<MongoTeamRepository>() + .As<ITeamRepository>().As<ISnapshotStore<TeamDomainObject.State>>(); - services.AddSingletonAs<MongoRuleRepository>() - .As<IRuleRepository>().As<ISnapshotStore<RuleDomainObject.State>>().As<IDeleter>(); + services.AddSingletonAs<MongoRuleRepository>() + .As<IRuleRepository>().As<ISnapshotStore<RuleDomainObject.State>>().As<IDeleter>(); - services.AddSingletonAs<MongoSchemaRepository>() - .As<ISchemaRepository>().As<ISnapshotStore<SchemaDomainObject.State>>().As<IDeleter>(); + services.AddSingletonAs<MongoSchemaRepository>() + .As<ISchemaRepository>().As<ISnapshotStore<SchemaDomainObject.State>>().As<IDeleter>(); - services.AddSingletonAs<MongoSchemasHash>() - .AsOptional<ISchemasHash>().As<IEventConsumer>().As<IDeleter>(); + services.AddSingletonAs<MongoSchemasHash>() + .AsOptional<ISchemasHash>().As<IEventConsumer>().As<IDeleter>(); - services.AddSingletonAs<MongoTextIndexerState>() - .As<ITextIndexerState>().As<IDeleter>(); + services.AddSingletonAs<MongoTextIndexerState>() + .As<ITextIndexerState>().As<IDeleter>(); - services.AddOpenIddict() - .AddCore(builder => - { - builder.UseMongoDb<string>() - .SetScopesCollectionName("Identity_Scopes") - .SetTokensCollectionName("Identity_Tokens"); + services.AddOpenIddict() + .AddCore(builder => + { + builder.UseMongoDb<string>() + .SetScopesCollectionName("Identity_Scopes") + .SetTokensCollectionName("Identity_Tokens"); - builder.SetDefaultScopeEntity<ImmutableScope>(); - builder.SetDefaultApplicationEntity<ImmutableApplication>(); - }); + builder.SetDefaultScopeEntity<ImmutableScope>(); + builder.SetDefaultApplicationEntity<ImmutableApplication>(); + }); - var atlasOptions = config.GetSection("store:mongoDb:atlas").Get<AtlasOptions>() ?? new (); + var atlasOptions = config.GetSection("store:mongoDb:atlas").Get<AtlasOptions>() ?? new (); - if (atlasOptions.IsConfigured() && atlasOptions.FullTextEnabled) - { - services.Configure<AtlasOptions>(config.GetSection("store:mongoDb:atlas")); + if (atlasOptions.IsConfigured() && atlasOptions.FullTextEnabled) + { + services.Configure<AtlasOptions>(config.GetSection("store:mongoDb:atlas")); - services.AddSingletonAs<AtlasTextIndex>() - .AsOptional<ITextIndex>().As<IDeleter>(); - } - else - { - services.AddSingletonAs<MongoTextIndex>() - .AsOptional<ITextIndex>().As<IDeleter>(); - } + services.AddSingletonAs<AtlasTextIndex>() + .AsOptional<ITextIndex>().As<IDeleter>(); + } + else + { + services.AddSingletonAs<MongoTextIndex>() + .AsOptional<ITextIndex>().As<IDeleter>(); + } - services.AddInitializer<JsonSerializerOptions>("Serializer (BSON)", jsonSerializerOptions => - { - var representation = config.GetValue<BsonType>("store:mongoDB:valueRepresentation"); + services.AddInitializer<JsonSerializerOptions>("Serializer (BSON)", jsonSerializerOptions => + { + var representation = config.GetValue<BsonType>("store:mongoDB:valueRepresentation"); - BsonJsonConvention.Register(jsonSerializerOptions, representation); - }, int.MinValue); - } - }); + BsonJsonConvention.Register(jsonSerializerOptions, representation); + }, int.MinValue); + } + }); - services.AddSingleton(typeof(IStore<>), - typeof(Store<>)); + services.AddSingleton(typeof(IStore<>), + typeof(Store<>)); - services.AddSingleton(typeof(IPersistenceFactory<>), - typeof(Store<>)); + services.AddSingleton(typeof(IPersistenceFactory<>), + typeof(Store<>)); - services.AddSingletonAs<IInitializable>(c => - { - var service = c.GetRequiredService<IAssetKeyValueStore<TusMetadata>>(); + services.AddSingletonAs<IInitializable>(c => + { + var service = c.GetRequiredService<IAssetKeyValueStore<TusMetadata>>(); - return new DelegateInitializer(service.GetType().Name, service.InitializeAsync); - }); - } + return new DelegateInitializer(service.GetType().Name, service.InitializeAsync); + }); + } - public static IMongoClient GetMongoClient(string configuration) + public static IMongoClient GetMongoClient(string configuration) + { + return Singletons<IMongoClient>.GetOrAdd(configuration, connectionString => { - return Singletons<IMongoClient>.GetOrAdd(configuration, connectionString => - { - var clientSettings = MongoClientSettings.FromConnectionString(connectionString); + var clientSettings = MongoClientSettings.FromConnectionString(connectionString); - clientSettings.ClusterConfigurator = builder => - { - builder.Subscribe(new DiagnosticsActivityEventSubscriber()); - }; + clientSettings.ClusterConfigurator = builder => + { + builder.Subscribe(new DiagnosticsActivityEventSubscriber()); + }; - return new MongoClient(clientSettings); - }); - } + return new MongoClient(clientSettings); + }); + } - private static IMongoDatabase GetDatabase(IServiceProvider serviceProvider, string name) - { - return serviceProvider.GetRequiredService<IMongoClient>().GetDatabase(name); - } + private static IMongoDatabase GetDatabase(IServiceProvider serviceProvider, string name) + { + return serviceProvider.GetRequiredService<IMongoClient>().GetDatabase(name); } } diff --git a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs index 8a1590ca94..cb76f6e013 100644 --- a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs +++ b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -11,22 +11,21 @@ using Squidex.Infrastructure; using Squidex.Web; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class SubscriptionServices { - public static class SubscriptionServices + public static void AddSquidexSubscriptions(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexSubscriptions(this IServiceCollection services, IConfiguration config) - { - services.AddSingletonAs(c => c.GetRequiredService<IOptions<UsageOptions>>()?.Value?.Plans.OrEmpty()!); + services.AddSingletonAs(c => c.GetRequiredService<IOptions<UsageOptions>>()?.Value?.Plans.OrEmpty()!); - services.AddSingletonAs<ConfigPlansProvider>() - .AsOptional<IBillingPlans>(); + services.AddSingletonAs<ConfigPlansProvider>() + .AsOptional<IBillingPlans>(); - services.AddSingletonAs<NoopBillingManager>() - .AsOptional<IBillingManager>(); + services.AddSingletonAs<NoopBillingManager>() + .AsOptional<IBillingManager>(); - services.AddSingletonAs<UsageGate>() - .AsOptional<IUsageGate>().As<IAssetUsageTracker>(); - } + services.AddSingletonAs<UsageGate>() + .AsOptional<IUsageGate>().As<IAssetUsageTracker>(); } } diff --git a/backend/src/Squidex/Config/Domain/TeamServices.cs b/backend/src/Squidex/Config/Domain/TeamServices.cs index 3f30c03396..3e290c01cc 100644 --- a/backend/src/Squidex/Config/Domain/TeamServices.cs +++ b/backend/src/Squidex/Config/Domain/TeamServices.cs @@ -8,14 +8,13 @@ using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Teams.Entities.Teams; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class TeamServices { - public static class TeamServices + public static void AddSquidexTeams(this IServiceCollection services) { - public static void AddSquidexTeams(this IServiceCollection services) - { - services.AddSingletonAs<TeamHistoryEventsCreator>() - .As<IHistoryEventsCreator>(); - } + services.AddSingletonAs<TeamHistoryEventsCreator>() + .As<IHistoryEventsCreator>(); } } diff --git a/backend/src/Squidex/Config/Domain/TelemetryServices.cs b/backend/src/Squidex/Config/Domain/TelemetryServices.cs index 24f3c3c122..c6c373d9cb 100644 --- a/backend/src/Squidex/Config/Domain/TelemetryServices.cs +++ b/backend/src/Squidex/Config/Domain/TelemetryServices.cs @@ -10,47 +10,46 @@ using OpenTelemetry.Trace; using Squidex.Infrastructure; -namespace Squidex.Config.Domain +namespace Squidex.Config.Domain; + +public static class TelemetryServices { - public static class TelemetryServices + public static void AddSquidexTelemetry(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexTelemetry(this IServiceCollection services, IConfiguration config) - { - services.AddOpenTelemetryTracing(); + services.AddOpenTelemetryTracing(); - services.AddSingleton(serviceProvider => - { - var builder = Sdk.CreateTracerProviderBuilder(); + services.AddSingleton(serviceProvider => + { + var builder = Sdk.CreateTracerProviderBuilder(); - var serviceName = config.GetValue<string>("logging:name") ?? "Squidex"; + var serviceName = config.GetValue<string>("logging:name") ?? "Squidex"; - builder.SetResourceBuilder( - ResourceBuilder.CreateDefault() - .AddService(serviceName, "Squidex", - typeof(TelemetryServices).Assembly.GetName().Version!.ToString())); + builder.SetResourceBuilder( + ResourceBuilder.CreateDefault() + .AddService(serviceName, "Squidex", + typeof(TelemetryServices).Assembly.GetName().Version!.ToString())); - builder.AddSource("Squidex"); + builder.AddSource("Squidex"); - builder.AddAspNetCoreInstrumentation(); - builder.AddHttpClientInstrumentation(); - builder.AddMongoDBInstrumentation(); + builder.AddAspNetCoreInstrumentation(); + builder.AddHttpClientInstrumentation(); + builder.AddMongoDBInstrumentation(); - var sampling = config.GetValue<double>("logging:otlp:sampling"); + var sampling = config.GetValue<double>("logging:otlp:sampling"); - if (sampling > 0 && sampling < 1) - { - builder.SetSampler( - new ParentBasedSampler( - new TraceIdRatioBasedSampler(sampling))); - } + if (sampling > 0 && sampling < 1) + { + builder.SetSampler( + new ParentBasedSampler( + new TraceIdRatioBasedSampler(sampling))); + } - foreach (var configurator in serviceProvider.GetRequiredService<IEnumerable<ITelemetryConfigurator>>()) - { - configurator.Configure(builder); - } + foreach (var configurator in serviceProvider.GetRequiredService<IEnumerable<ITelemetryConfigurator>>()) + { + configurator.Configure(builder); + } - return builder.Build(); - }); - } + return builder.Build(); + }); } } diff --git a/backend/src/Squidex/Config/Messaging/MessagingServices.cs b/backend/src/Squidex/Config/Messaging/MessagingServices.cs index 891ca883fb..16d21c0b80 100644 --- a/backend/src/Squidex/Config/Messaging/MessagingServices.cs +++ b/backend/src/Squidex/Config/Messaging/MessagingServices.cs @@ -22,99 +22,98 @@ using Squidex.Messaging.Implementation.Scheduler; using Squidex.Messaging.Subscriptions; -namespace Squidex.Config.Messaging +namespace Squidex.Config.Messaging; + +public static class MessagingServices { - public static class MessagingServices + public static void AddSquidexMessaging(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexMessaging(this IServiceCollection services, IConfiguration config) + var channelBackupRestore = new ChannelName("backup.restore"); + var channelBackupStart = new ChannelName("backup.start"); + var channelFallback = new ChannelName("default"); + var channelRules = new ChannelName("rules.run"); + var isCaching = config.GetValue<bool>("caching:replicated:enable"); + var isWorker = config.GetValue<bool>("clustering:worker"); + + if (isWorker) { - var channelBackupRestore = new ChannelName("backup.restore"); - var channelBackupStart = new ChannelName("backup.start"); - var channelFallback = new ChannelName("default"); - var channelRules = new ChannelName("rules.run"); - var isCaching = config.GetValue<bool>("caching:replicated:enable"); - var isWorker = config.GetValue<bool>("clustering:worker"); - - if (isWorker) - { - services.AddSingletonAs<AssetCleanupProcess>() - .AsSelf(); - - services.AddSingletonAs<ContentSchedulerProcess>() - .AsSelf(); - - services.AddSingletonAs<RuleDequeuerWorker>() - .AsSelf(); - - services.AddSingletonAs<EventConsumerWorker>() - .AsSelf().As<IMessageHandler>(); - - services.AddSingletonAs<RuleRunnerWorker>() - .AsSelf().As<IMessageHandler>(); - - services.AddSingletonAs<BackupWorker>() - .AsSelf().As<IMessageHandler>(); - - services.AddSingletonAs<UsageNotifierWorker>() - .AsSelf().As<IMessageHandler>(); - - services.AddSingletonAs<UsageTrackerWorker>() - .AsSelf().As<IMessageHandler>(); - } - - services.AddSingleton<IMessagingSerializer>(c => - new SystemTextJsonMessagingSerializer(c.GetRequiredService<JsonSerializerOptions>())); - - services.AddSingletonAs<SubscriptionPublisher>() - .As<IEventConsumer>(); - - services.AddSingletonAs<EventMessageEvaluator>() - .As<IMessageEvaluator>(); - - services.AddReplicatedCacheMessaging(isCaching, options => - { - options.TransportSelector = (transport, _) => transport.First(x => x is NullTransport != isCaching); - }); - - services.Configure<SubscriptionOptions>(options => - { - options.SendMessagesToSelf = false; - }); - - services.AddMessagingSubscriptions(); - services.AddMessagingTransport(config); - services.AddMessaging(options => - { - options.Routing.Add(m => m is RuleRunnerRun, channelRules); - options.Routing.Add(m => m is BackupStart, channelBackupStart); - options.Routing.Add(m => m is BackupRestore, channelBackupRestore); - options.Routing.AddFallback(channelFallback); - }); - - services.AddMessaging(channelBackupStart, isWorker, options => - { - options.Timeout = TimeSpan.FromHours(4); - options.Scheduler = new ParallelScheduler(4); - options.LogMessage = x => true; - }); - - services.AddMessaging(channelBackupRestore, isWorker, options => - { - options.Timeout = TimeSpan.FromHours(24); - options.Scheduler = InlineScheduler.Instance; - options.LogMessage = x => true; - }); - - services.AddMessaging(channelRules, isWorker, options => - { - options.Scheduler = new ParallelScheduler(4); - options.LogMessage = x => true; - }); - - services.AddMessaging(channelFallback, isWorker, options => - { - options.Scheduler = InlineScheduler.Instance; - }); + services.AddSingletonAs<AssetCleanupProcess>() + .AsSelf(); + + services.AddSingletonAs<ContentSchedulerProcess>() + .AsSelf(); + + services.AddSingletonAs<RuleDequeuerWorker>() + .AsSelf(); + + services.AddSingletonAs<EventConsumerWorker>() + .AsSelf().As<IMessageHandler>(); + + services.AddSingletonAs<RuleRunnerWorker>() + .AsSelf().As<IMessageHandler>(); + + services.AddSingletonAs<BackupWorker>() + .AsSelf().As<IMessageHandler>(); + + services.AddSingletonAs<UsageNotifierWorker>() + .AsSelf().As<IMessageHandler>(); + + services.AddSingletonAs<UsageTrackerWorker>() + .AsSelf().As<IMessageHandler>(); } + + services.AddSingleton<IMessagingSerializer>(c => + new SystemTextJsonMessagingSerializer(c.GetRequiredService<JsonSerializerOptions>())); + + services.AddSingletonAs<SubscriptionPublisher>() + .As<IEventConsumer>(); + + services.AddSingletonAs<EventMessageEvaluator>() + .As<IMessageEvaluator>(); + + services.AddReplicatedCacheMessaging(isCaching, options => + { + options.TransportSelector = (transport, _) => transport.First(x => x is NullTransport != isCaching); + }); + + services.Configure<SubscriptionOptions>(options => + { + options.SendMessagesToSelf = false; + }); + + services.AddMessagingSubscriptions(); + services.AddMessagingTransport(config); + services.AddMessaging(options => + { + options.Routing.Add(m => m is RuleRunnerRun, channelRules); + options.Routing.Add(m => m is BackupStart, channelBackupStart); + options.Routing.Add(m => m is BackupRestore, channelBackupRestore); + options.Routing.AddFallback(channelFallback); + }); + + services.AddMessaging(channelBackupStart, isWorker, options => + { + options.Timeout = TimeSpan.FromHours(4); + options.Scheduler = new ParallelScheduler(4); + options.LogMessage = x => true; + }); + + services.AddMessaging(channelBackupRestore, isWorker, options => + { + options.Timeout = TimeSpan.FromHours(24); + options.Scheduler = InlineScheduler.Instance; + options.LogMessage = x => true; + }); + + services.AddMessaging(channelRules, isWorker, options => + { + options.Scheduler = new ParallelScheduler(4); + options.LogMessage = x => true; + }); + + services.AddMessaging(channelFallback, isWorker, options => + { + options.Scheduler = InlineScheduler.Instance; + }); } } diff --git a/backend/src/Squidex/Config/MyIdentityOptions.cs b/backend/src/Squidex/Config/MyIdentityOptions.cs index 57b79f1e6f..c91ac29de3 100644 --- a/backend/src/Squidex/Config/MyIdentityOptions.cs +++ b/backend/src/Squidex/Config/MyIdentityOptions.cs @@ -5,104 +5,103 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Config +namespace Squidex.Config; + +public sealed class MyIdentityOptions { - public sealed class MyIdentityOptions - { - public string PrivacyUrl { get; set; } + public string PrivacyUrl { get; set; } - public string AuthorityUrl { get; set; } + public string AuthorityUrl { get; set; } - public string AdminEmail { get; set; } + public string AdminEmail { get; set; } - public string AdminPassword { get; set; } + public string AdminPassword { get; set; } - public string[] AdminApps { get; set; } + public string[] AdminApps { get; set; } - public string AdminClientId { get; set; } + public string AdminClientId { get; set; } - public string AdminClientSecret { get; set; } + public string AdminClientSecret { get; set; } - public string GithubClient { get; set; } + public string GithubClient { get; set; } - public string GithubSecret { get; set; } + public string GithubSecret { get; set; } - public string GoogleClient { get; set; } + public string GoogleClient { get; set; } - public string GoogleSecret { get; set; } + public string GoogleSecret { get; set; } - public string MicrosoftClient { get; set; } + public string MicrosoftClient { get; set; } - public string MicrosoftSecret { get; set; } + public string MicrosoftSecret { get; set; } - public string MicrosoftTenant { get; set; } + public string MicrosoftTenant { get; set; } - public Dictionary<string, string[]> OidcRoleMapping { get; set; } + public Dictionary<string, string[]> OidcRoleMapping { get; set; } - public string OidcName { get; set; } + public string OidcName { get; set; } - public string OidcClient { get; set; } + public string OidcClient { get; set; } - public string OidcSecret { get; set; } + public string OidcSecret { get; set; } - public string OidcAuthority { get; set; } + public string OidcAuthority { get; set; } - public string OidcMetadataAddress { get; set; } + public string OidcMetadataAddress { get; set; } - public string OidcRoleClaimType { get; set; } + public string OidcRoleClaimType { get; set; } - public string OidcResponseType { get; set; } + public string OidcResponseType { get; set; } - public string OidcOnSignoutRedirectUrl { get; set; } + public string OidcOnSignoutRedirectUrl { get; set; } - public string[] OidcScopes { get; set; } + public string[] OidcScopes { get; set; } - public bool OidcGetClaimsFromUserInfoEndpoint { get; set; } + public bool OidcGetClaimsFromUserInfoEndpoint { get; set; } - public bool AdminRecreate { get; set; } + public bool AdminRecreate { get; set; } - public bool AllowPasswordAuth { get; set; } + public bool AllowPasswordAuth { get; set; } - public bool LockAutomatically { get; set; } + public bool LockAutomatically { get; set; } - public bool MultipleDomains { get; set; } + public bool MultipleDomains { get; set; } - public bool NoConsent { get; set; } + public bool NoConsent { get; set; } - public bool RequiresHttps { get; set; } + public bool RequiresHttps { get; set; } - public bool ShowPII { get; set; } + public bool ShowPII { get; set; } - public bool SuppressXFrameOptionsHeader { get; set; } + public bool SuppressXFrameOptionsHeader { get; set; } - public bool IsAdminConfigured() - { - return !string.IsNullOrWhiteSpace(AdminEmail) && !string.IsNullOrWhiteSpace(AdminPassword); - } + public bool IsAdminConfigured() + { + return !string.IsNullOrWhiteSpace(AdminEmail) && !string.IsNullOrWhiteSpace(AdminPassword); + } - public bool IsAdminClientConfigured() - { - return !string.IsNullOrWhiteSpace(AdminClientId) && !string.IsNullOrWhiteSpace(AdminClientSecret); - } + public bool IsAdminClientConfigured() + { + return !string.IsNullOrWhiteSpace(AdminClientId) && !string.IsNullOrWhiteSpace(AdminClientSecret); + } - public bool IsOidcConfigured() - { - return !string.IsNullOrWhiteSpace(OidcAuthority) && !string.IsNullOrWhiteSpace(OidcClient); - } + public bool IsOidcConfigured() + { + return !string.IsNullOrWhiteSpace(OidcAuthority) && !string.IsNullOrWhiteSpace(OidcClient); + } - public bool IsGithubAuthConfigured() - { - return !string.IsNullOrWhiteSpace(GithubClient) && !string.IsNullOrWhiteSpace(GithubSecret); - } + public bool IsGithubAuthConfigured() + { + return !string.IsNullOrWhiteSpace(GithubClient) && !string.IsNullOrWhiteSpace(GithubSecret); + } - public bool IsGoogleAuthConfigured() - { - return !string.IsNullOrWhiteSpace(GoogleClient) && !string.IsNullOrWhiteSpace(GoogleSecret); - } + public bool IsGoogleAuthConfigured() + { + return !string.IsNullOrWhiteSpace(GoogleClient) && !string.IsNullOrWhiteSpace(GoogleSecret); + } - public bool IsMicrosoftAuthConfigured() - { - return !string.IsNullOrWhiteSpace(MicrosoftClient) && !string.IsNullOrWhiteSpace(MicrosoftSecret); - } + public bool IsMicrosoftAuthConfigured() + { + return !string.IsNullOrWhiteSpace(MicrosoftClient) && !string.IsNullOrWhiteSpace(MicrosoftSecret); } } diff --git a/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs b/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs index c225319833..e2b51186c7 100644 --- a/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs +++ b/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs @@ -7,47 +7,46 @@ using Squidex.Log; -namespace Squidex.Config.Startup +namespace Squidex.Config.Startup; + +public sealed class LogConfigurationHost : IHostedService { - public sealed class LogConfigurationHost : IHostedService + private readonly IConfiguration configuration; + private readonly ISemanticLog log; + + public LogConfigurationHost(IConfiguration configuration, ISemanticLog log) { - private readonly IConfiguration configuration; - private readonly ISemanticLog log; - - public LogConfigurationHost(IConfiguration configuration, ISemanticLog log) - { - this.configuration = configuration; - - this.log = log; - } - - public Task StartAsync( - CancellationToken cancellationToken) - { - log.LogInformation(w => w - .WriteProperty("message", "Application started") - .WriteObject("environment", c => - { - var logged = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + this.configuration = configuration; + + this.log = log; + } - var orderedConfigs = configuration.AsEnumerable().Where(kvp => kvp.Value != null).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase); + public Task StartAsync( + CancellationToken cancellationToken) + { + log.LogInformation(w => w + .WriteProperty("message", "Application started") + .WriteObject("environment", c => + { + var logged = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + + var orderedConfigs = configuration.AsEnumerable().Where(kvp => kvp.Value != null).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase); - foreach (var (key, val) in orderedConfigs) + foreach (var (key, val) in orderedConfigs) + { + if (logged.Add(key)) { - if (logged.Add(key)) - { - c.WriteProperty(key.ToLowerInvariant(), val); - } + c.WriteProperty(key.ToLowerInvariant(), val); } - })); + } + })); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task StopAsync( - CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public Task StopAsync( + CancellationToken cancellationToken) + { + return Task.CompletedTask; } } diff --git a/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs b/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs index 441028cda6..3220012430 100644 --- a/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs +++ b/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs @@ -7,27 +7,26 @@ using Migrations; -namespace Squidex.Config.Startup +namespace Squidex.Config.Startup; + +public sealed class MigrationRebuilderHost : IHostedService { - public sealed class MigrationRebuilderHost : IHostedService - { - private readonly RebuildRunner rebuildRunner; + private readonly RebuildRunner rebuildRunner; - public MigrationRebuilderHost(RebuildRunner rebuildRunner) - { - this.rebuildRunner = rebuildRunner; - } + public MigrationRebuilderHost(RebuildRunner rebuildRunner) + { + this.rebuildRunner = rebuildRunner; + } - public Task StartAsync( - CancellationToken cancellationToken) - { - return rebuildRunner.RunAsync(cancellationToken); - } + public Task StartAsync( + CancellationToken cancellationToken) + { + return rebuildRunner.RunAsync(cancellationToken); + } - public Task StopAsync( - CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public Task StopAsync( + CancellationToken cancellationToken) + { + return Task.CompletedTask; } } diff --git a/backend/src/Squidex/Config/Startup/MigratorHost.cs b/backend/src/Squidex/Config/Startup/MigratorHost.cs index 571f2d552c..c42ed711e9 100644 --- a/backend/src/Squidex/Config/Startup/MigratorHost.cs +++ b/backend/src/Squidex/Config/Startup/MigratorHost.cs @@ -7,27 +7,26 @@ using Squidex.Infrastructure.Migrations; -namespace Squidex.Config.Startup +namespace Squidex.Config.Startup; + +public sealed class MigratorHost : IHostedService { - public sealed class MigratorHost : IHostedService - { - private readonly Migrator migrator; + private readonly Migrator migrator; - public MigratorHost(Migrator migrator) - { - this.migrator = migrator; - } + public MigratorHost(Migrator migrator) + { + this.migrator = migrator; + } - public Task StartAsync( - CancellationToken cancellationToken) - { - return migrator.MigrateAsync(cancellationToken); - } + public Task StartAsync( + CancellationToken cancellationToken) + { + return migrator.MigrateAsync(cancellationToken); + } - public Task StopAsync( - CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public Task StopAsync( + CancellationToken cancellationToken) + { + return Task.CompletedTask; } } diff --git a/backend/src/Squidex/Config/Web/WebExtensions.cs b/backend/src/Squidex/Config/Web/WebExtensions.cs index b99475a98c..e603cb327c 100644 --- a/backend/src/Squidex/Config/Web/WebExtensions.cs +++ b/backend/src/Squidex/Config/Web/WebExtensions.cs @@ -12,131 +12,130 @@ using Squidex.Pipeline.Robots; using Squidex.Web.Pipeline; -namespace Squidex.Config.Web +namespace Squidex.Config.Web; + +public static class WebExtensions { - public static class WebExtensions + public static IApplicationBuilder UseSquidexCacheKeys(this IApplicationBuilder app) { - public static IApplicationBuilder UseSquidexCacheKeys(this IApplicationBuilder app) - { - app.UseMiddleware<CachingKeysMiddleware>(); + app.UseMiddleware<CachingKeysMiddleware>(); - return app; - } + return app; + } - public static IApplicationBuilder UseSquidexLocalCache(this IApplicationBuilder app) - { - app.UseMiddleware<LocalCacheMiddleware>(); + public static IApplicationBuilder UseSquidexLocalCache(this IApplicationBuilder app) + { + app.UseMiddleware<LocalCacheMiddleware>(); - return app; - } + return app; + } - public static IApplicationBuilder UseSquidexLocalization(this IApplicationBuilder app) - { - var supportedCultures = new[] { "en", "nl", "it", "zh" }; + public static IApplicationBuilder UseSquidexLocalization(this IApplicationBuilder app) + { + var supportedCultures = new[] { "en", "nl", "it", "zh" }; - var localizationOptions = new RequestLocalizationOptions() - .SetDefaultCulture(supportedCultures[0]) - .AddSupportedCultures(supportedCultures) - .AddSupportedUICultures(supportedCultures); + var localizationOptions = new RequestLocalizationOptions() + .SetDefaultCulture(supportedCultures[0]) + .AddSupportedCultures(supportedCultures) + .AddSupportedUICultures(supportedCultures); - app.UseRequestLocalization(localizationOptions); + app.UseRequestLocalization(localizationOptions); - return app; - } + return app; + } - public static IApplicationBuilder UseSquidexLogging(this IApplicationBuilder app) - { - app.UseMiddleware<RequestLogPerformanceMiddleware>(); + public static IApplicationBuilder UseSquidexLogging(this IApplicationBuilder app) + { + app.UseMiddleware<RequestLogPerformanceMiddleware>(); - return app; - } + return app; + } - public static IApplicationBuilder UseSquidexUsage(this IApplicationBuilder app) - { - app.UseMiddleware<UsageMiddleware>(); + public static IApplicationBuilder UseSquidexUsage(this IApplicationBuilder app) + { + app.UseMiddleware<UsageMiddleware>(); - return app; - } + return app; + } - public static IApplicationBuilder UseSquidexExceptionHandling(this IApplicationBuilder app) - { - app.UseMiddleware<RequestExceptionMiddleware>(); + public static IApplicationBuilder UseSquidexExceptionHandling(this IApplicationBuilder app) + { + app.UseMiddleware<RequestExceptionMiddleware>(); - return app; - } + return app; + } - public static IApplicationBuilder UseSquidexHealthCheck(this IApplicationBuilder app) - { - var serializer = app.ApplicationServices.GetRequiredService<IJsonSerializer>(); + public static IApplicationBuilder UseSquidexHealthCheck(this IApplicationBuilder app) + { + var serializer = app.ApplicationServices.GetRequiredService<IJsonSerializer>(); - var writer = new Func<HttpContext, HealthReport, Task>((httpContext, report) => + var writer = new Func<HttpContext, HealthReport, Task>((httpContext, report) => + { + var response = new { - var response = new + Entries = report.Entries.ToDictionary(x => x.Key, x => { - Entries = report.Entries.ToDictionary(x => x.Key, x => - { - var value = x.Value; + var value = x.Value; - return new - { - Data = value.Data.Count > 0 ? new Dictionary<string, object>(value.Data) : null, - value.Description, - value.Duration, - value.Status - }; - }), - report.Status, - report.TotalDuration - }; + return new + { + Data = value.Data.Count > 0 ? new Dictionary<string, object>(value.Data) : null, + value.Description, + value.Duration, + value.Status + }; + }), + report.Status, + report.TotalDuration + }; - var json = serializer.Serialize(response); + var json = serializer.Serialize(response); - httpContext.Response.Headers[HeaderNames.ContentType] = "text/json"; + httpContext.Response.Headers[HeaderNames.ContentType] = "text/json"; - return httpContext.Response.WriteAsync(json); - }); + return httpContext.Response.WriteAsync(json); + }); - app.UseHealthChecks("/readiness", new HealthCheckOptions - { - Predicate = check => !check.Tags.Contains("background"), - ResponseWriter = writer - }); + app.UseHealthChecks("/readiness", new HealthCheckOptions + { + Predicate = check => !check.Tags.Contains("background"), + ResponseWriter = writer + }); - app.UseHealthChecks("/healthz", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("node"), - ResponseWriter = writer - }); + app.UseHealthChecks("/healthz", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("node"), + ResponseWriter = writer + }); - app.UseHealthChecks("/cluster-healthz", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("cluster"), - ResponseWriter = writer - }); + app.UseHealthChecks("/cluster-healthz", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("cluster"), + ResponseWriter = writer + }); - app.UseHealthChecks("/background-healthz", new HealthCheckOptions - { - Predicate = check => check.Tags.Contains("background"), - ResponseWriter = writer - }); + app.UseHealthChecks("/background-healthz", new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("background"), + ResponseWriter = writer + }); - return app; - } + return app; + } - public static IApplicationBuilder UseSquidexRobotsTxt(this IApplicationBuilder app) - { - app.Map("/robots.txt", builder => builder.UseMiddleware<RobotsTxtMiddleware>()); + public static IApplicationBuilder UseSquidexRobotsTxt(this IApplicationBuilder app) + { + app.Map("/robots.txt", builder => builder.UseMiddleware<RobotsTxtMiddleware>()); - return app; - } + return app; + } - public static void UseSquidexCors(this IApplicationBuilder app) - { - app.UseCors(builder => builder - .SetIsOriginAllowed(x => true) - .AllowCredentials() - .AllowAnyMethod() - .AllowAnyHeader()); - } + public static void UseSquidexCors(this IApplicationBuilder app) + { + app.UseCors(builder => builder + .SetIsOriginAllowed(x => true) + .AllowCredentials() + .AllowAnyMethod() + .AllowAnyHeader()); } } diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 49ba99139c..d5b5909936 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -25,101 +25,100 @@ using Squidex.Web.Pipeline; using Squidex.Web.Services; -namespace Squidex.Config.Web +namespace Squidex.Config.Web; + +public static class WebServices { - public static class WebServices + public static void AddSquidexMvcWithPlugins(this IServiceCollection services, IConfiguration config) { - public static void AddSquidexMvcWithPlugins(this IServiceCollection services, IConfiguration config) + services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService<IOptions<ExposedConfiguration>>().Value, config, typeof(WebServices).Assembly)) + .AsSelf(); + + services.AddSingletonAs<FileCallbackResultExecutor>() + .AsSelf(); + + services.AddSingletonAs<ApiCostsFilter>() + .AsSelf(); + + services.AddSingletonAs<AppResolver>() + .AsSelf(); + + services.AddSingletonAs<SchemaResolver>() + .AsSelf(); + + services.AddSingletonAs<TeamResolver>() + .AsSelf(); + + services.AddSingletonAs<UsageMiddleware>() + .AsSelf(); + + services.AddSingletonAs<StringLocalizer>() + .As<IStringLocalizer>().As<IStringLocalizerFactory>(); + + services.AddSingletonAs<CachingManager>() + .As<IRequestCache>(); + + services.AddSingletonAs<ContextProvider>() + .As<IContextProvider>(); + + services.AddSingletonAs<HttpContextAccessor>() + .As<IHttpContextAccessor>(); + + services.AddSingletonAs<ActionContextAccessor>() + .As<IActionContextAccessor>(); + + services.Configure<ApiBehaviorOptions>(options => { - services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService<IOptions<ExposedConfiguration>>().Value, config, typeof(WebServices).Assembly)) - .AsSelf(); - - services.AddSingletonAs<FileCallbackResultExecutor>() - .AsSelf(); - - services.AddSingletonAs<ApiCostsFilter>() - .AsSelf(); - - services.AddSingletonAs<AppResolver>() - .AsSelf(); - - services.AddSingletonAs<SchemaResolver>() - .AsSelf(); - - services.AddSingletonAs<TeamResolver>() - .AsSelf(); - - services.AddSingletonAs<UsageMiddleware>() - .AsSelf(); - - services.AddSingletonAs<StringLocalizer>() - .As<IStringLocalizer>().As<IStringLocalizerFactory>(); - - services.AddSingletonAs<CachingManager>() - .As<IRequestCache>(); - - services.AddSingletonAs<ContextProvider>() - .As<IContextProvider>(); - - services.AddSingletonAs<HttpContextAccessor>() - .As<IHttpContextAccessor>(); - - services.AddSingletonAs<ActionContextAccessor>() - .As<IActionContextAccessor>(); - - services.Configure<ApiBehaviorOptions>(options => - { - options.SuppressInferBindingSourcesForParameters = true; - options.SuppressModelStateInvalidFilter = true; - }); - - services.AddLocalization(); - - services.AddMvc(options => - { - // Never change this order here. - options.Filters.Add<CachingFilter>(); - options.Filters.Add<DeferredActionFilter>(); - options.Filters.Add<ContextFilter>(); - options.Filters.Add<AppResolver>(); - options.Filters.Add<TeamResolver>(); - options.Filters.Add<SchemaResolver>(); - options.Filters.Add<MeasureResultFilter>(); - - // Ingore all values that could have JsonValue somewhere. - options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(ContentData))); - options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(ContentFieldData))); - options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(JsonArray))); - options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(JsonObject))); - options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(JsonValue))); - }) - .AddDataAnnotationsLocalization() - .AddRazorRuntimeCompilation() - .AddSquidexPlugins(config) - .AddSquidexSerializers(); - } - - public static void AddSquidexGraphQL(this IServiceCollection services) + options.SuppressInferBindingSourcesForParameters = true; + options.SuppressModelStateInvalidFilter = true; + }); + + services.AddLocalization(); + + services.AddMvc(options => { - services.AddGraphQL(builder => - { - builder.UseApolloTracing(); - builder.AddSchema<DummySchema>(); - builder.AddSystemTextJson(); - builder.AddDataLoader(); - }); - - services.AddSingletonAs<DummySchema>() - .AsSelf(); - - services.AddSingletonAs<DynamicUserContextBuilder>() - .As<IUserContextBuilder>(); - - services.AddSingletonAs<CachingGraphQLResolver>() - .As<IConfigureExecution>(); - - services.AddSingletonAs<GraphQLRunner>() - .AsSelf(); - } + // Never change this order here. + options.Filters.Add<CachingFilter>(); + options.Filters.Add<DeferredActionFilter>(); + options.Filters.Add<ContextFilter>(); + options.Filters.Add<AppResolver>(); + options.Filters.Add<TeamResolver>(); + options.Filters.Add<SchemaResolver>(); + options.Filters.Add<MeasureResultFilter>(); + + // Ingore all values that could have JsonValue somewhere. + options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(ContentData))); + options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(ContentFieldData))); + options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(JsonArray))); + options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(JsonObject))); + options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(JsonValue))); + }) + .AddDataAnnotationsLocalization() + .AddRazorRuntimeCompilation() + .AddSquidexPlugins(config) + .AddSquidexSerializers(); + } + + public static void AddSquidexGraphQL(this IServiceCollection services) + { + services.AddGraphQL(builder => + { + builder.UseApolloTracing(); + builder.AddSchema<DummySchema>(); + builder.AddSystemTextJson(); + builder.AddDataLoader(); + }); + + services.AddSingletonAs<DummySchema>() + .AsSelf(); + + services.AddSingletonAs<DynamicUserContextBuilder>() + .As<IUserContextBuilder>(); + + services.AddSingletonAs<CachingGraphQLResolver>() + .As<IConfigureExecution>(); + + services.AddSingletonAs<GraphQLRunner>() + .AsSelf(); } } diff --git a/backend/src/Squidex/Pipeline/Plugins/MvcParts.cs b/backend/src/Squidex/Pipeline/Plugins/MvcParts.cs index 4c67fcb4bc..e0cd15c3e2 100644 --- a/backend/src/Squidex/Pipeline/Plugins/MvcParts.cs +++ b/backend/src/Squidex/Pipeline/Plugins/MvcParts.cs @@ -8,40 +8,39 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationParts; -namespace Squidex.Pipeline.Plugins +namespace Squidex.Pipeline.Plugins; + +public static class MvcParts { - public static class MvcParts + public static void AddParts(this Assembly assembly, IMvcBuilder mvcBuilder) { - public static void AddParts(this Assembly assembly, IMvcBuilder mvcBuilder) + mvcBuilder.ConfigureApplicationPartManager(manager => { - mvcBuilder.ConfigureApplicationPartManager(manager => - { - var parts = manager.ApplicationParts; + var parts = manager.ApplicationParts; - AddParts(parts, assembly); + AddParts(parts, assembly); - foreach (var reference in RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, false)) - { - AddParts(parts, reference); - } - }); - } + foreach (var reference in RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, false)) + { + AddParts(parts, reference); + } + }); + } - private static void AddParts(IList<ApplicationPart> applicationParts, Assembly assembly) + private static void AddParts(IList<ApplicationPart> applicationParts, Assembly assembly) + { + var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); + + foreach (var part in partFactory.GetApplicationParts(assembly)) { - var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); + var existings = applicationParts.Where(x => x.Name == part.Name).ToList(); - foreach (var part in partFactory.GetApplicationParts(assembly)) + foreach (var existing in existings) { - var existings = applicationParts.Where(x => x.Name == part.Name).ToList(); - - foreach (var existing in existings) - { - applicationParts.Remove(existing); - } - - applicationParts.Add(part); + applicationParts.Remove(existing); } + + applicationParts.Add(part); } } } diff --git a/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs b/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs index 5c61215c26..dc37f429c8 100644 --- a/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs +++ b/backend/src/Squidex/Pipeline/Plugins/PluginExtensions.cs @@ -14,52 +14,51 @@ using Squidex.Log; using Squidex.Web; -namespace Squidex.Pipeline.Plugins +namespace Squidex.Pipeline.Plugins; + +public static class PluginExtensions { - public static class PluginExtensions + private static readonly AssemblyName[] SharedAssemblies = new[] { - private static readonly AssemblyName[] SharedAssemblies = new[] - { - typeof(IPlugin), - typeof(SquidexCoreModel), - typeof(SquidexCoreOperations), - typeof(SquidexEntities), - typeof(SquidexEvents), - typeof(SquidexInfrastructure), - typeof(SquidexWeb) - }.Select(x => x.Assembly.GetName()).ToArray(); + typeof(IPlugin), + typeof(SquidexCoreModel), + typeof(SquidexCoreOperations), + typeof(SquidexEntities), + typeof(SquidexEvents), + typeof(SquidexInfrastructure), + typeof(SquidexWeb) + }.Select(x => x.Assembly.GetName()).ToArray(); - public static IMvcBuilder AddSquidexPlugins(this IMvcBuilder mvcBuilder, IConfiguration config) - { - var pluginManager = new PluginManager(); + public static IMvcBuilder AddSquidexPlugins(this IMvcBuilder mvcBuilder, IConfiguration config) + { + var pluginManager = new PluginManager(); - var options = config.Get<PluginOptions>(); + var options = config.Get<PluginOptions>(); - if (options.Plugins != null) + if (options.Plugins != null) + { + foreach (var path in options.Plugins) { - foreach (var path in options.Plugins) - { - var pluginAssembly = pluginManager.Load(path, SharedAssemblies); + var pluginAssembly = pluginManager.Load(path, SharedAssemblies); - if (pluginAssembly != null) - { - pluginAssembly.AddParts(mvcBuilder); - } + if (pluginAssembly != null) + { + pluginAssembly.AddParts(mvcBuilder); } } + } - pluginManager.ConfigureServices(mvcBuilder.Services, config); + pluginManager.ConfigureServices(mvcBuilder.Services, config); - mvcBuilder.Services.AddSingleton(pluginManager); + mvcBuilder.Services.AddSingleton(pluginManager); - return mvcBuilder; - } + return mvcBuilder; + } - public static void UsePlugins(this IApplicationBuilder app) - { - var pluginManager = app.ApplicationServices.GetRequiredService<PluginManager>(); + public static void UsePlugins(this IApplicationBuilder app) + { + var pluginManager = app.ApplicationServices.GetRequiredService<PluginManager>(); - pluginManager.Log(app.ApplicationServices.GetRequiredService<ISemanticLog>()); - } + pluginManager.Log(app.ApplicationServices.GetRequiredService<ISemanticLog>()); } } diff --git a/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs index 316f12374c..2576639197 100644 --- a/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs +++ b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs @@ -7,37 +7,36 @@ using Microsoft.Extensions.Options; -namespace Squidex.Pipeline.Robots +namespace Squidex.Pipeline.Robots; + +public sealed class RobotsTxtMiddleware { - public sealed class RobotsTxtMiddleware + private readonly RequestDelegate next; + + public RobotsTxtMiddleware(RequestDelegate next) { - private readonly RequestDelegate next; + this.next = next; + } - public RobotsTxtMiddleware(RequestDelegate next) - { - this.next = next; - } + public async Task InvokeAsync(HttpContext context, IOptions<RobotsTxtOptions> robotsTxtOptions) + { + var text = robotsTxtOptions.Value.Text; - public async Task InvokeAsync(HttpContext context, IOptions<RobotsTxtOptions> robotsTxtOptions) + if (CanServeRequest(context.Request) && !string.IsNullOrWhiteSpace(text)) { - var text = robotsTxtOptions.Value.Text; - - if (CanServeRequest(context.Request) && !string.IsNullOrWhiteSpace(text)) - { - context.Response.ContentType = "text/plain"; - context.Response.StatusCode = 200; - - await context.Response.WriteAsync(text, context.RequestAborted); - } - else - { - await next(context); - } - } + context.Response.ContentType = "text/plain"; + context.Response.StatusCode = 200; - private static bool CanServeRequest(HttpRequest request) + await context.Response.WriteAsync(text, context.RequestAborted); + } + else { - return HttpMethods.IsGet(request.Method) && string.IsNullOrEmpty(request.Path); + await next(context); } } + + private static bool CanServeRequest(HttpRequest request) + { + return HttpMethods.IsGet(request.Method) && string.IsNullOrEmpty(request.Path); + } } diff --git a/backend/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs b/backend/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs index 3d1972f79e..d8284d6e14 100644 --- a/backend/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs +++ b/backend/src/Squidex/Pipeline/Robots/RobotsTxtOptions.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Pipeline.Robots +namespace Squidex.Pipeline.Robots; + +public sealed class RobotsTxtOptions { - public sealed class RobotsTxtOptions - { - public string Text { get; set; } - } + public string Text { get; set; } } diff --git a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs index adab239e43..ab183323f9 100644 --- a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -8,120 +8,119 @@ using Squidex.Infrastructure; using System.Text; -namespace Squidex.Pipeline.Squid +namespace Squidex.Pipeline.Squid; + +public sealed class SquidMiddleware { - public sealed class SquidMiddleware + private readonly string squidHappyLG = LoadSvg("happy"); + private readonly string squidHappySM = LoadSvg("happy-sm"); + private readonly string squidSadLG = LoadSvg("sad"); + private readonly string squidSadSM = LoadSvg("sad-sm"); + + public SquidMiddleware(RequestDelegate next) { - private readonly string squidHappyLG = LoadSvg("happy"); - private readonly string squidHappySM = LoadSvg("happy-sm"); - private readonly string squidSadLG = LoadSvg("sad"); - private readonly string squidSadSM = LoadSvg("sad-sm"); + } - public SquidMiddleware(RequestDelegate next) - { - } + public async Task InvokeAsync(HttpContext context) + { + var request = context.Request; - public async Task InvokeAsync(HttpContext context) + var face = "sad"; + + if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy")) { - var request = context.Request; + face = faceValue; + } - var face = "sad"; + var isSad = face == "sad"; - if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy")) - { - face = faceValue; - } + var title = isSad ? "OH DAMN!" : "OH YEAH!"; - var isSad = face == "sad"; + if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue)) + { + title = titleValue; + } - var title = isSad ? "OH DAMN!" : "OH YEAH!"; + var text = "text"; - if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue)) - { - title = titleValue; - } + if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) + { + text = textValue; + } - var text = "text"; + var background = isSad ? "#F5F5F9" : "#4CC159"; - if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) - { - text = textValue; - } + if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) + { + background = backgroundValue; + } - var background = isSad ? "#F5F5F9" : "#4CC159"; + var isSmall = request.Query.TryGetValue("small", out _); - if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) - { - background = backgroundValue; - } + string svg; - var isSmall = request.Query.TryGetValue("small", out _); + if (isSmall) + { + svg = isSad ? squidSadSM : squidHappySM; + } + else + { + svg = isSad ? squidSadLG : squidHappyLG; + } - string svg; + var (l1, l2, l3) = SplitText(text); - if (isSmall) - { - svg = isSad ? squidSadSM : squidHappySM; - } - else - { - svg = isSad ? squidSadLG : squidHappyLG; - } + svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant(), StringComparison.Ordinal); + svg = svg.Replace("{{TEXT1}}", l1, StringComparison.Ordinal); + svg = svg.Replace("{{TEXT2}}", l2, StringComparison.Ordinal); + svg = svg.Replace("{{TEXT3}}", l3, StringComparison.Ordinal); + svg = svg.Replace("[COLOR]", background, StringComparison.Ordinal); - var (l1, l2, l3) = SplitText(text); + context.Response.StatusCode = 200; + context.Response.ContentType = "image/svg+xml"; + context.Response.Headers["Cache-Control"] = "public, max-age=604800"; - svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant(), StringComparison.Ordinal); - svg = svg.Replace("{{TEXT1}}", l1, StringComparison.Ordinal); - svg = svg.Replace("{{TEXT2}}", l2, StringComparison.Ordinal); - svg = svg.Replace("{{TEXT3}}", l3, StringComparison.Ordinal); - svg = svg.Replace("[COLOR]", background, StringComparison.Ordinal); + await context.Response.WriteAsync(svg, context.RequestAborted); + } - context.Response.StatusCode = 200; - context.Response.ContentType = "image/svg+xml"; - context.Response.Headers["Cache-Control"] = "public, max-age=604800"; + private static (string, string, string) SplitText(string text) + { + var result = new List<string>(); - await context.Response.WriteAsync(svg, context.RequestAborted); - } + var line = new StringBuilder(); - private static (string, string, string) SplitText(string text) + foreach (var word in text.Split(' ')) { - var result = new List<string>(); - - var line = new StringBuilder(); - - foreach (var word in text.Split(' ')) + if (line.Length + word.Length > 16 && line.Length > 0) { - if (line.Length + word.Length > 16 && line.Length > 0) - { - result.Add(line.ToString()); - - line.Clear(); - } + result.Add(line.ToString()); - line.AppendIfNotEmpty(' '); - line.Append(word); + line.Clear(); } - result.Add(line.ToString()); + line.AppendIfNotEmpty(' '); + line.Append(word); + } - while (result.Count < 3) - { - result.Add(string.Empty); - } + result.Add(line.ToString()); - return (result[0], result[1], result[2]); + while (result.Count < 3) + { + result.Add(string.Empty); } - private static string LoadSvg(string name) - { - var assembly = typeof(SquidMiddleware).Assembly; + return (result[0], result[1], result[2]); + } - using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Pipeline.Squid.icon-{name}.svg")) + private static string LoadSvg(string name) + { + var assembly = typeof(SquidMiddleware).Assembly; + + using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Pipeline.Squid.icon-{name}.svg")) + { + using (var streamReader = new StreamReader(resourceStream!)) { - using (var streamReader = new StreamReader(resourceStream!)) - { - return streamReader.ReadToEnd(); - } + return streamReader.ReadToEnd(); } } } diff --git a/backend/src/Squidex/Program.cs b/backend/src/Squidex/Program.cs index a65f912205..86be4de232 100644 --- a/backend/src/Squidex/Program.cs +++ b/backend/src/Squidex/Program.cs @@ -8,57 +8,56 @@ using Squidex.Config.Domain; using Squidex.Config.Startup; -namespace Squidex +namespace Squidex; + +public static class Program { - public static class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureLogging((context, builder) => - { - builder.ConfigureForSquidex(context.Configuration); - }) - .ConfigureAppConfiguration((hostContext, builder) => - { - builder.ConfigureForSquidex(); - }) - .ConfigureServices((context, services) => - { - // Step 0: Log all configuration. - services.AddHostedService<LogConfigurationHost>(); - - // Step 1: Initialize all services. - services.AddInitializer(); - - // Step 2: Run migration. - services.AddHostedService<MigratorHost>(); - - // Step 3: Run rebuild processes. - services.AddHostedService<MigrationRebuilderHost>(); + CreateHostBuilder(args).Build().Run(); + } - // Step 4: Start background processes. - services.AddBackgroundProcesses(); - }) - .ConfigureWebHostDefaults(builder => + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureLogging((context, builder) => + { + builder.ConfigureForSquidex(context.Configuration); + }) + .ConfigureAppConfiguration((hostContext, builder) => + { + builder.ConfigureForSquidex(); + }) + .ConfigureServices((context, services) => + { + // Step 0: Log all configuration. + services.AddHostedService<LogConfigurationHost>(); + + // Step 1: Initialize all services. + services.AddInitializer(); + + // Step 2: Run migration. + services.AddHostedService<MigratorHost>(); + + // Step 3: Run rebuild processes. + services.AddHostedService<MigrationRebuilderHost>(); + + // Step 4: Start background processes. + services.AddBackgroundProcesses(); + }) + .ConfigureWebHostDefaults(builder => + { + builder.ConfigureKestrel((context, serverOptions) => { - builder.ConfigureKestrel((context, serverOptions) => + if (context.HostingEnvironment.IsDevelopment() || context.Configuration.GetValue<bool>("devMode:enable")) { - if (context.HostingEnvironment.IsDevelopment() || context.Configuration.GetValue<bool>("devMode:enable")) - { - serverOptions.ListenAnyIP( - 5001, - listenOptions => listenOptions.UseHttps("../../../dev/squidex-dev.pfx", "password")); + serverOptions.ListenAnyIP( + 5001, + listenOptions => listenOptions.UseHttps("../../../dev/squidex-dev.pfx", "password")); - serverOptions.ListenAnyIP(5000); - } - }); - - builder.UseStartup<Startup>(); + serverOptions.ListenAnyIP(5000); + } }); - } + + builder.UseStartup<Startup>(); + }); } diff --git a/backend/src/Squidex/Properties/Resources.Designer.cs b/backend/src/Squidex/Properties/Resources.Designer.cs index e03950fc3c..0ca84648d4 100644 --- a/backend/src/Squidex/Properties/Resources.Designer.cs +++ b/backend/src/Squidex/Properties/Resources.Designer.cs @@ -8,125 +8,124 @@ // </auto-generated> //------------------------------------------------------------------------------ -namespace Squidex.Properties { - using System; +namespace Squidex.Properties; +using System; + + +/// <summary> +/// A strongly-typed resource class, for looking up localized strings, etc. +/// </summary> +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } /// <summary> - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Returns the cached ResourceManager instance used by this class. /// </summary> - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// <summary> - /// Returns the cached ResourceManager instance used by this class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// <summary> - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// <summary> - /// Looks up a localized string similar to # Introduction - /// - ///The API provides two specifications. The Content API usually returns one object per schema field where the keys are the languages (or `iv` for non-localized) fields and the values are the actual field values. - /// - ///You can use the `X-Flatten` header to return a flat structure when you query content items. This is more performant and easier for code generation. Unfortunantely it cannot be modelled with OpenAPI. Therefore we provide two different documents for your API. - /// - ///Read more about this [rest of string was truncated]";. - /// </summary> - internal static string OpenApiContentDescription { - get { - return ResourceManager.GetString("OpenApiContentDescription", resourceCulture); - } + set { + resourceCulture = value; } - - /// <summary> - /// Looks up a localized string similar to The data of the content. - /// - ///Please note that each field is an object with one entry per language. - ///If the field is not localizable you must use `iv` (invariant language) as a key. - /// - ///Read more about it at: https://docs.squidex.io/04-guides/02-api.html. - /// </summary> - internal static string OpenApiSchemaBody { - get { - return ResourceManager.GetString("OpenApiSchemaBody", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to # Introduction + /// + ///The API provides two specifications. The Content API usually returns one object per schema field where the keys are the languages (or `iv` for non-localized) fields and the values are the actual field values. + /// + ///You can use the `X-Flatten` header to return a flat structure when you query content items. This is more performant and easier for code generation. Unfortunantely it cannot be modelled with OpenAPI. Therefore we provide two different documents for your API. + /// + ///Read more about this [rest of string was truncated]";. + /// </summary> + internal static string OpenApiContentDescription { + get { + return ResourceManager.GetString("OpenApiContentDescription", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to How to make queries? - /// - ///Read more about it at: https://docs.squidex.io/04-guides/02-api.html - /// - ///The query endpoints support three options: - /// - ///### Query with OData - /// - ///Squidex supports a subset of the OData (https://www.odata.org/) syntax with with the following query options: - /// - ///* **$top**: The $top query option requests the number of items in the queried collection to be included in the result. The default value is 20 and the maximum allowed value is 200. You can change the maximum in the app settings, when [rest of string was truncated]";. - /// </summary> - internal static string OpenApiSchemaQuery { - get { - return ResourceManager.GetString("OpenApiSchemaQuery", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to The data of the content. + /// + ///Please note that each field is an object with one entry per language. + ///If the field is not localizable you must use `iv` (invariant language) as a key. + /// + ///Read more about it at: https://docs.squidex.io/04-guides/02-api.html. + /// </summary> + internal static string OpenApiSchemaBody { + get { + return ResourceManager.GetString("OpenApiSchemaBody", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Squidex uses oauth2 client authentication. Read more about it at: https://oauth.net/2/ and https://tools.ietf.org/html/rfc6750. - /// - ///To retrieve an access token, the client id must make a request to the token url. For example: - /// - /// $ curl - /// -X POST '<TOKEN_URL>' - /// -H 'Content-Type: application/x-www-form-urlencoded' - /// -d 'grant_type=client_credentials& - /// client_id=[CLIENT_ID]& - /// client_secret=[CLIENT_SECRET]& - /// scope=squidex-api' - /// - ///You must send this token in [rest of string was truncated]";. - /// </summary> - internal static string OpenApiSecurity { - get { - return ResourceManager.GetString("OpenApiSecurity", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to How to make queries? + /// + ///Read more about it at: https://docs.squidex.io/04-guides/02-api.html + /// + ///The query endpoints support three options: + /// + ///### Query with OData + /// + ///Squidex supports a subset of the OData (https://www.odata.org/) syntax with with the following query options: + /// + ///* **$top**: The $top query option requests the number of items in the queried collection to be included in the result. The default value is 20 and the maximum allowed value is 200. You can change the maximum in the app settings, when [rest of string was truncated]";. + /// </summary> + internal static string OpenApiSchemaQuery { + get { + return ResourceManager.GetString("OpenApiSchemaQuery", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Squidex uses oauth2 client authentication. Read more about it at: https://oauth.net/2/ and https://tools.ietf.org/html/rfc6750. + /// + ///To retrieve an access token, the client id must make a request to the token url. For example: + /// + /// $ curl + /// -X POST '<TOKEN_URL>' + /// -H 'Content-Type: application/x-www-form-urlencoded' + /// -d 'grant_type=client_credentials& + /// client_id=[CLIENT_ID]& + /// client_secret=[CLIENT_SECRET]& + /// scope=squidex-api' + /// + ///You must send this token in [rest of string was truncated]";. + /// </summary> + internal static string OpenApiSecurity { + get { + return ResourceManager.GetString("OpenApiSecurity", resourceCulture); } } } diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index cd9b67ffe9..0caaaa53df 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -16,116 +16,115 @@ using Squidex.Web; using Squidex.Web.Pipeline; -namespace Squidex +namespace Squidex; + +public sealed class Startup { - public sealed class Startup + private readonly IConfiguration config; + + public Startup(IConfiguration config) + { + this.config = config; + } + + public void ConfigureServices(IServiceCollection services) { - private readonly IConfiguration config; + services.AddHttpClient(); + services.AddMemoryCache(); + services.AddHealthChecks(); + services.AddDefaultWebServices(config); + services.AddDefaultForwardRules(); + + // They must be called in this order. + services.AddSquidexMvcWithPlugins(config); + services.AddSquidexIdentity(config); + services.AddSquidexIdentityServer(); + services.AddSquidexAuthentication(config); + + services.AddSquidexApps(config); + services.AddSquidexAssetInfrastructure(config); + services.AddSquidexAssets(config); + services.AddSquidexBackups(); + services.AddSquidexCommands(config); + services.AddSquidexComments(); + services.AddSquidexContents(config); + services.AddSquidexControllerServices(config); + services.AddSquidexEventPublisher(config); + services.AddSquidexEventSourcing(config); + services.AddSquidexGraphQL(); + services.AddSquidexHealthChecks(config); + services.AddSquidexHistory(config); + services.AddSquidexImageResizing(config); + services.AddSquidexInfrastructure(config); + services.AddSquidexLocalization(); + services.AddSquidexMessaging(config); + services.AddSquidexMigration(config); + services.AddSquidexNotifications(config); + services.AddSquidexOpenApiSettings(); + services.AddSquidexQueries(config); + services.AddSquidexRules(config); + services.AddSquidexSchemas(); + services.AddSquidexSearch(); + services.AddSquidexSerializers(); + services.AddSquidexStoreServices(config); + services.AddSquidexSubscriptions(config); + services.AddSquidexTeams(); + services.AddSquidexTelemetry(config); + services.AddSquidexTranslation(config); + services.AddSquidexUsageTracking(config); + } + + public void Configure(IApplicationBuilder app) + { + app.UseWebSockets(); + + app.UseCookiePolicy(); + + app.UseDefaultPathBase(); + app.UseDefaultForwardRules(); - public Startup(IConfiguration config) + app.UseSquidexHealthCheck(); + app.UseSquidexRobotsTxt(); + app.UseSquidexLogging(); + app.UseSquidexLocalization(); + app.UseSquidexLocalCache(); + app.UseSquidexCors(); + app.UseOpenApi(options => { - this.config = config; - } + options.Path = "/api/swagger/v1/swagger.json"; + }); - public void ConfigureServices(IServiceCollection services) + app.UseWhen(c => c.Request.Path.StartsWithSegments(Constants.PrefixIdentityServer, StringComparison.OrdinalIgnoreCase), builder => { - services.AddHttpClient(); - services.AddMemoryCache(); - services.AddHealthChecks(); - services.AddDefaultWebServices(config); - services.AddDefaultForwardRules(); - - // They must be called in this order. - services.AddSquidexMvcWithPlugins(config); - services.AddSquidexIdentity(config); - services.AddSquidexIdentityServer(); - services.AddSquidexAuthentication(config); - - services.AddSquidexApps(config); - services.AddSquidexAssetInfrastructure(config); - services.AddSquidexAssets(config); - services.AddSquidexBackups(); - services.AddSquidexCommands(config); - services.AddSquidexComments(); - services.AddSquidexContents(config); - services.AddSquidexControllerServices(config); - services.AddSquidexEventPublisher(config); - services.AddSquidexEventSourcing(config); - services.AddSquidexGraphQL(); - services.AddSquidexHealthChecks(config); - services.AddSquidexHistory(config); - services.AddSquidexImageResizing(config); - services.AddSquidexInfrastructure(config); - services.AddSquidexLocalization(); - services.AddSquidexMessaging(config); - services.AddSquidexMigration(config); - services.AddSquidexNotifications(config); - services.AddSquidexOpenApiSettings(); - services.AddSquidexQueries(config); - services.AddSquidexRules(config); - services.AddSquidexSchemas(); - services.AddSquidexSearch(); - services.AddSquidexSerializers(); - services.AddSquidexStoreServices(config); - services.AddSquidexSubscriptions(config); - services.AddSquidexTeams(); - services.AddSquidexTelemetry(config); - services.AddSquidexTranslation(config); - services.AddSquidexUsageTracking(config); - } - - public void Configure(IApplicationBuilder app) + builder.UseExceptionHandler("/identity-server/error"); + }); + + app.UseWhen(c => c.Request.Path.StartsWithSegments(Constants.PrefixApi, StringComparison.OrdinalIgnoreCase), builder => + { + builder.UseSquidexCacheKeys(); + builder.UseSquidexExceptionHandling(); + builder.UseSquidexUsage(); + builder.UseAccessTokenQueryString(); + }); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { - app.UseWebSockets(); - - app.UseCookiePolicy(); - - app.UseDefaultPathBase(); - app.UseDefaultForwardRules(); - - app.UseSquidexHealthCheck(); - app.UseSquidexRobotsTxt(); - app.UseSquidexLogging(); - app.UseSquidexLocalization(); - app.UseSquidexLocalCache(); - app.UseSquidexCors(); - app.UseOpenApi(options => - { - options.Path = "/api/swagger/v1/swagger.json"; - }); - - app.UseWhen(c => c.Request.Path.StartsWithSegments(Constants.PrefixIdentityServer, StringComparison.OrdinalIgnoreCase), builder => - { - builder.UseExceptionHandler("/identity-server/error"); - }); - - app.UseWhen(c => c.Request.Path.StartsWithSegments(Constants.PrefixApi, StringComparison.OrdinalIgnoreCase), builder => - { - builder.UseSquidexCacheKeys(); - builder.UseSquidexExceptionHandling(); - builder.UseSquidexUsage(); - builder.UseAccessTokenQueryString(); - }); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - - // Return a 404 for all unresolved api requests. - app.Map(Constants.PrefixApi, builder => - { - builder.Use404(); - }); - - app.UseFrontend(); - - app.UsePlugins(); - } + endpoints.MapControllers(); + }); + + // Return a 404 for all unresolved api requests. + app.Map(Constants.PrefixApi, builder => + { + builder.Use404(); + }); + + app.UseFrontend(); + + app.UsePlugins(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs index d0657ba278..7794976a95 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs @@ -9,28 +9,27 @@ using Squidex.Domain.Apps.Core.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class AppClientJsonTests { - public class AppClientJsonTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var clients = AppClients.Empty; + var clients = AppClients.Empty; - clients = clients.Add("1", "my-secret"); - clients = clients.Add("2", "my-secret"); - clients = clients.Add("3", "my-secret"); - clients = clients.Add("4", "my-secret"); - clients = clients.Update("3", role: Role.Editor); - clients = clients.Update("2", name: "My Client 2"); - clients = clients.Update("3", name: "My Client 3"); - clients = clients.Update("1", allowAnonymous: true, apiCallsLimit: 3); - clients = clients.Revoke("4"); + clients = clients.Add("1", "my-secret"); + clients = clients.Add("2", "my-secret"); + clients = clients.Add("3", "my-secret"); + clients = clients.Add("4", "my-secret"); + clients = clients.Update("3", role: Role.Editor); + clients = clients.Update("2", name: "My Client 2"); + clients = clients.Update("3", name: "My Client 3"); + clients = clients.Update("1", allowAnonymous: true, apiCallsLimit: 3); + clients = clients.Revoke("4"); - var serialized = clients.SerializeAndDeserialize(); + var serialized = clients.SerializeAndDeserialize(); - Assert.Equal(clients, serialized); - } + Assert.Equal(clients, serialized); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 17e81f8ba7..8fc6660ab0 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs @@ -10,100 +10,99 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class AppClientsTests { - public class AppClientsTests - { - private readonly AppClients clients_0 = AppClients.Empty.Add("1", "my-secret"); + private readonly AppClients clients_0 = AppClients.Empty.Add("1", "my-secret"); - [Fact] - public void Should_assign_client() - { - var clients_1 = clients_0.Add("2", "my-secret"); + [Fact] + public void Should_assign_client() + { + var clients_1 = clients_0.Add("2", "my-secret"); - Assert.Equal(new AppClient("2", "my-secret"), clients_1["2"]); - } + Assert.Equal(new AppClient("2", "my-secret"), clients_1["2"]); + } - [Fact] - public void Should_assign_clients_with_permission() - { - var clients_1 = clients_0.Add("2", "my-secret", Role.Reader); + [Fact] + public void Should_assign_clients_with_permission() + { + var clients_1 = clients_0.Add("2", "my-secret", Role.Reader); - Assert.Equal(new AppClient("2", "my-secret") with { Role = Role.Reader }, clients_1["2"]); - } + Assert.Equal(new AppClient("2", "my-secret") with { Role = Role.Reader }, clients_1["2"]); + } - [Fact] - public void Should_do_nothing_if_assigning_client_with_same_id() - { - var clients_1 = clients_0.Add("1", "my-secret"); + [Fact] + public void Should_do_nothing_if_assigning_client_with_same_id() + { + var clients_1 = clients_0.Add("1", "my-secret"); - Assert.Same(clients_0, clients_1); - } + Assert.Same(clients_0, clients_1); + } - [Fact] - public void Should_update_client_with_role() - { - var client_1 = clients_0.Update("1", role: Role.Reader); + [Fact] + public void Should_update_client_with_role() + { + var client_1 = clients_0.Update("1", role: Role.Reader); - Assert.Equal(new AppClient("1", "my-secret") with { Role = Role.Reader }, client_1["1"]); - } + Assert.Equal(new AppClient("1", "my-secret") with { Role = Role.Reader }, client_1["1"]); + } - [Fact] - public void Should_update_client_with_name() - { - var client_1 = clients_0.Update("1", name: "New-Name"); + [Fact] + public void Should_update_client_with_name() + { + var client_1 = clients_0.Update("1", name: "New-Name"); - Assert.Equal(new AppClient("New-Name", "my-secret"), client_1["1"]); - } + Assert.Equal(new AppClient("New-Name", "my-secret"), client_1["1"]); + } - [Fact] - public void Should_update_client_with_allow_anonymous() - { - var client_1 = clients_0.Update("1", allowAnonymous: true); + [Fact] + public void Should_update_client_with_allow_anonymous() + { + var client_1 = clients_0.Update("1", allowAnonymous: true); - Assert.Equal(new AppClient("1", "my-secret") with { AllowAnonymous = true }, client_1["1"]); - } + Assert.Equal(new AppClient("1", "my-secret") with { AllowAnonymous = true }, client_1["1"]); + } - [Fact] - public void Should_update_client_with_allow_api_calls_limit() - { - var client_1 = clients_0.Update("1", apiCallsLimit: 1000); + [Fact] + public void Should_update_client_with_allow_api_calls_limit() + { + var client_1 = clients_0.Update("1", apiCallsLimit: 1000); - Assert.Equal(new AppClient("1", "my-secret") with { ApiCallsLimit = 1000 }, client_1["1"]); - } + Assert.Equal(new AppClient("1", "my-secret") with { ApiCallsLimit = 1000 }, client_1["1"]); + } - [Fact] - public void Should_update_client_with_allow_api_traffic_limit() - { - var client_1 = clients_0.Update("1", apiTrafficLimit: 1000); + [Fact] + public void Should_update_client_with_allow_api_traffic_limit() + { + var client_1 = clients_0.Update("1", apiTrafficLimit: 1000); - Assert.Equal(new AppClient("1", "my-secret") with { ApiTrafficLimit = 1000 }, client_1["1"]); - } + Assert.Equal(new AppClient("1", "my-secret") with { ApiTrafficLimit = 1000 }, client_1["1"]); + } - [Fact] - public void Should_return_same_clients_if_client_to_update_not_found() - { - var clients_1 = clients_0.Update("2", role: Role.Reader); + [Fact] + public void Should_return_same_clients_if_client_to_update_not_found() + { + var clients_1 = clients_0.Update("2", role: Role.Reader); - Assert.Same(clients_0, clients_1); - } + Assert.Same(clients_0, clients_1); + } - [Fact] - public void Should_revoke_client() - { - var clients_1 = clients_0.Add("2", "secret2"); - var clients_2 = clients_1.Add("3", "secret3"); - var clients_3 = clients_2.Revoke("2"); + [Fact] + public void Should_revoke_client() + { + var clients_1 = clients_0.Add("2", "secret2"); + var clients_2 = clients_1.Add("3", "secret3"); + var clients_3 = clients_2.Revoke("2"); - Assert.Equal(new[] { "1", "3" }, clients_3.Keys); - } + Assert.Equal(new[] { "1", "3" }, clients_3.Keys); + } - [Fact] - public void Should_do_nothing_if_client_to_revoke_not_found() - { - var clients_1 = clients_0.Revoke("2"); + [Fact] + public void Should_do_nothing_if_client_to_revoke_not_found() + { + var clients_1 = clients_0.Revoke("2"); - Assert.Same(clients_0, clients_1); - } + Assert.Same(clients_0, clients_1); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs index 8044b309d2..792f0c3380 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsJsonTests.cs @@ -9,22 +9,21 @@ using Squidex.Domain.Apps.Core.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class AppContributorsJsonTests { - public class AppContributorsJsonTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var contributors = Contributors.Empty; + var contributors = Contributors.Empty; - contributors = contributors.Assign("1", Role.Developer); - contributors = contributors.Assign("2", Role.Editor); - contributors = contributors.Assign("3", Role.Owner); + contributors = contributors.Assign("1", Role.Developer); + contributors = contributors.Assign("2", Role.Editor); + contributors = contributors.Assign("3", Role.Owner); - var serialized = contributors.SerializeAndDeserialize(); + var serialized = contributors.SerializeAndDeserialize(); - Assert.Equal(contributors, serialized); - } + Assert.Equal(contributors, serialized); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs index c3e2ba209b..467d3909a1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs @@ -10,57 +10,56 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class AppContributorsTests { - public class AppContributorsTests - { - private readonly Contributors contributors_0 = Contributors.Empty; + private readonly Contributors contributors_0 = Contributors.Empty; - [Fact] - public void Should_assign_new_contributor() - { - var contributors_1 = contributors_0.Assign("1", Role.Developer); - var contributors_2 = contributors_1.Assign("2", Role.Editor); + [Fact] + public void Should_assign_new_contributor() + { + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("2", Role.Editor); - Assert.Equal(Role.Developer, contributors_2["1"]); - Assert.Equal(Role.Editor, contributors_2["2"]); - } + Assert.Equal(Role.Developer, contributors_2["1"]); + Assert.Equal(Role.Editor, contributors_2["2"]); + } - [Fact] - public void Should_replace_contributor_if_already_exists() - { - var contributors_1 = contributors_0.Assign("1", Role.Developer); - var contributors_2 = contributors_1.Assign("1", Role.Owner); + [Fact] + public void Should_replace_contributor_if_already_exists() + { + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("1", Role.Owner); - Assert.Equal(Role.Owner, contributors_2["1"]); - } + Assert.Equal(Role.Owner, contributors_2["1"]); + } - [Fact] - public void Should_return_same_contributors_if_contributor_is_updated_with_same_role() - { - var contributors_1 = contributors_0.Assign("1", Role.Developer); - var contributors_2 = contributors_1.Assign("1", Role.Developer); + [Fact] + public void Should_return_same_contributors_if_contributor_is_updated_with_same_role() + { + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("1", Role.Developer); - Assert.Same(contributors_1, contributors_2); - } + Assert.Same(contributors_1, contributors_2); + } - [Fact] - public void Should_remove_contributor() - { - var contributors_1 = contributors_0.Assign("1", Role.Developer); - var contributors_2 = contributors_1.Assign("2", Role.Developer); - var contributors_3 = contributors_2.Assign("3", Role.Developer); - var contributors_4 = contributors_3.Remove("2"); + [Fact] + public void Should_remove_contributor() + { + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("2", Role.Developer); + var contributors_3 = contributors_2.Assign("3", Role.Developer); + var contributors_4 = contributors_3.Remove("2"); - Assert.Equal(new[] { "1", "3" }, contributors_4.Keys); - } + Assert.Equal(new[] { "1", "3" }, contributors_4.Keys); + } - [Fact] - public void Should_do_nothing_if_contributor_to_remove_not_found() - { - var contributors_1 = contributors_0.Remove("2"); + [Fact] + public void Should_do_nothing_if_contributor_to_remove_not_found() + { + var contributors_1 = contributors_0.Remove("2"); - Assert.Same(contributors_0, contributors_1); - } + Assert.Same(contributors_0, contributors_1); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppImageTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppImageTests.cs index 1c475d5dcc..f157dba632 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppImageTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppImageTests.cs @@ -9,38 +9,37 @@ using Squidex.Domain.Apps.Core.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class AppImageTests { - public class AppImageTests + [Fact] + public void Should_create_app_image() { - [Fact] - public void Should_create_app_image() - { - var imageEtag = "etag"; - var imageMime = "image/png"; + var imageEtag = "etag"; + var imageMime = "image/png"; - var appImage = new AppImage(imageMime, imageEtag); + var appImage = new AppImage(imageMime, imageEtag); - Assert.Equal(imageEtag, appImage.Etag); - Assert.Equal(imageMime, appImage.MimeType); - } + Assert.Equal(imageEtag, appImage.Etag); + Assert.Equal(imageMime, appImage.MimeType); + } - [Fact] - public void Should_create_app_image_with_autogenerated_etag() - { - var appImage = new AppImage("image/png"); + [Fact] + public void Should_create_app_image_with_autogenerated_etag() + { + var appImage = new AppImage("image/png"); - Assert.True(appImage.Etag.Length > 10); - } + Assert.True(appImage.Etag.Length > 10); + } - [Fact] - public void Should_serialize_and_deserialize() - { - var appImage = new AppImage("image/png"); + [Fact] + public void Should_serialize_and_deserialize() + { + var appImage = new AppImage("image/png"); - var serialized = appImage.SerializeAndDeserialize(); + var serialized = appImage.SerializeAndDeserialize(); - Assert.Equal(appImage, serialized); - } + Assert.Equal(appImage, serialized); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs index e9ef1b387f..65b2aa0ea0 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs @@ -9,18 +9,17 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class AppPlanTests { - public class AppPlanTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var plan = new AssignedPlan(RefToken.Client("Me"), "free"); + var plan = new AssignedPlan(RefToken.Client("Me"), "free"); - var serialized = plan.SerializeAndDeserialize(); + var serialized = plan.SerializeAndDeserialize(); - Assert.Equal(plan, serialized); - } + Assert.Equal(plan, serialized); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs index 89184c6014..1748e74387 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs @@ -11,23 +11,22 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class LanguagesConfigJsonTests { - public class LanguagesConfigJsonTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var languages = - LanguagesConfig.English - .Set(Language.FR) - .Set(Language.IT) - .Set(Language.DE, true, Language.IT) - .MakeMaster(Language.FR); + var languages = + LanguagesConfig.English + .Set(Language.FR) + .Set(Language.IT) + .Set(Language.DE, true, Language.IT) + .MakeMaster(Language.FR); - var serialized = languages.SerializeAndDeserialize(); + var serialized = languages.SerializeAndDeserialize(); - serialized.Should().BeEquivalentTo(languages); - } + serialized.Should().BeEquivalentTo(languages); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs index a2c73711f1..429dc21280 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs @@ -14,267 +14,266 @@ #pragma warning disable SA1310 // Field names must not contain underscore #pragma warning disable CA1806 // Do not ignore method results -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class LanguagesConfigTests { - public class LanguagesConfigTests + private readonly LanguagesConfig config_0 = LanguagesConfig.English; + + [Fact] + public void Should_make_contains_test() + { + Assert.True(config_0.Contains(Language.EN)); + + Assert.False(config_0.Contains(Language.FR)); + Assert.False(config_0.Contains(null!)); + } + + [Fact] + public void Should_provide_name() + { + Assert.Equal("English", config_0.GetName(Language.EN)); + + Assert.Null(config_0.GetName(Language.FR)); + Assert.Null(config_0.GetName(null!)); + } + + [Fact] + public void Should_make_master_test() + { + var config = + LanguagesConfig.English + .Set(Language.DE) + .Set(Language.ES, true); + + Assert.True(config.IsMaster(Language.EN)); + + Assert.False(config.IsMaster(Language.DE)); + Assert.False(config.IsMaster(Language.ES)); + Assert.False(config.IsMaster(Language.FR)); + Assert.False(config.IsMaster(null!)); + } + + [Fact] + public void Should_make_optional_test() + { + var config = + LanguagesConfig.English + .Set(Language.DE) + .Set(Language.ES, true); + + Assert.True(config.IsOptional(Language.ES)); + + Assert.False(config.IsOptional(Language.EN)); + Assert.False(config.IsOptional(Language.DE)); + Assert.False(config.IsOptional(Language.FR)); + Assert.False(config.IsOptional(null!)); + } + + [Fact] + public void Should_provide_priorities() + { + var config = + LanguagesConfig.English + .Set(Language.DE) + .Set(Language.ES, true, Language.DE); + + Assert.Empty(config.GetPriorities(Language.FR)); + Assert.Empty(config.GetPriorities(null!)); + + Assert.Equal(new string[] { Language.ES, Language.DE, Language.EN }, config.GetPriorities(Language.ES)); + Assert.Equal(new string[] { Language.DE, Language.EN }, config.GetPriorities(Language.DE)); + Assert.Equal(new string[] { Language.EN }, config.GetPriorities(Language.EN)); + } + + [Fact] + public void Should_create_initial_config() + { + config_0.Languages.Should().BeEquivalentTo( + new Dictionary<string, LanguageConfig> + { + [Language.EN] = new LanguageConfig() + }); + + Assert.Equal(Language.EN, config_0.Master); + } + + [Fact] + public void Should_create_initial_config_0_with_multiple_languages() + { + var config = + LanguagesConfig.English + .Set(Language.DE) + .Set(Language.ES, true) + .Set(Language.IT, true, Language.ES) + .MakeMaster(Language.DE); + + config.Languages.Should().BeEquivalentTo( + new Dictionary<string, LanguageConfig> + { + [Language.EN] = new LanguageConfig(), + [Language.DE] = new LanguageConfig(), + [Language.ES] = new LanguageConfig(true), + [Language.IT] = new LanguageConfig(true, ReadonlyList.Create(Language.ES)) + }); + + Assert.Equal(Language.DE, config.Master); + } + + [Fact] + public void Should_not_throw_exception_if_language_to_add_already_exists() + { + config_0.Set(Language.EN); + } + + [Fact] + public void Should_return_same_language_if_already_added() + { + var config_1 = config_0.Set(Language.EN); + + Assert.Same(config_1, config_0); + } + + [Fact] + public void Should_make_master_language() + { + var config = + LanguagesConfig.English + .Set(Language.DE) + .Set(Language.IT, true, Language.ES) + .MakeMaster(Language.IT); + + config.Languages.Should().BeEquivalentTo( + new Dictionary<string, LanguageConfig> + { + [Language.EN] = new LanguageConfig(), + [Language.DE] = new LanguageConfig(), + [Language.IT] = new LanguageConfig() + }); + + Assert.Equal(Language.IT, config.Master); + } + + [Fact] + public void Should_return_same_languages_if_master_language_is_already_master() + { + var config_1 = config_0.Set(Language.DE); + var config_2 = config_1.Set(Language.IT); + var config_3 = config_2.MakeMaster(Language.IT); + var config_4 = config_3.MakeMaster(Language.IT); + + Assert.Same(config_3, config_4); + } + + [Fact] + public void Should_keep_master_language_if_language_to_make_master_is_not_found() + { + var config_1 = config_0.Set(Language.DE); + var config_2 = config_1.Set(Language.IT); + var config_3 = config_2.MakeMaster(Language.IT); + var config_4 = config_3.MakeMaster(Language.FR); + + Assert.Same(config_3, config_4); + Assert.Equal(Language.IT, config_4.Master); + } + + [Fact] + public void Should_remove_language() + { + var config_1 = config_0.Set(Language.DE); + var config_2 = config_1.Set(Language.IT); + var config_3 = config_2.Remove(Language.DE); + + config_3.Languages.Should().BeEquivalentTo( + new Dictionary<string, LanguageConfig> + { + [Language.EN] = new LanguageConfig(), + [Language.IT] = new LanguageConfig() + }); + + Assert.Equal(Language.EN, config_3.Master); + } + + [Fact] + public void Should_remove_fallbacks_if_removing_language() + { + var config_1 = config_0.Set(Language.DE); + var config_2 = config_1.Set(Language.IT, true, Language.UK); + var config_3 = config_2.Remove(Language.DE); + + config_3.Languages.Should().BeEquivalentTo( + new Dictionary<string, LanguageConfig> + { + [Language.EN] = new LanguageConfig(), + [Language.IT] = new LanguageConfig(true) + }); + + Assert.Equal(Language.EN, config_3.Master); + } + + [Fact] + public void Should_same_languages_if_removing_single_language() + { + var config_1 = config_0.Remove(Language.EN); + + Assert.Same(config_0, config_1); + } + + [Fact] + public void Should_update_master_language_if_removed() + { + var config_1 = config_0.Set(Language.DE); + var config_2 = config_1.Set(Language.IT); + var config_3 = config_2.Remove(Language.EN); + + config_3.Languages.Should().BeEquivalentTo( + new Dictionary<string, LanguageConfig> + { + [Language.DE] = new LanguageConfig(), + [Language.IT] = new LanguageConfig() + }); + + Assert.Equal(Language.DE, config_3.Master); + } + + [Fact] + public void Should_return_same_languages_if_language_to_remove_is_not_found() + { + var config_1 = config_0.Remove(Language.IT); + + Assert.Equal(config_0, config_1); + } + + [Fact] + public void Should_update_language() + { + var config_1 = config_0.Set(Language.IT); + var config_2 = config_1.Set(Language.IT, true, Language.EN); + + config_2.Languages.Should().BeEquivalentTo( + new Dictionary<string, LanguageConfig> + { + [Language.EN] = new LanguageConfig(), + [Language.IT] = new LanguageConfig(true, ReadonlyList.Create(Language.EN)) + }); + + Assert.Equal(Language.EN, config_2.Master); + } + + [Fact] + public void Should_eliminate_invalid_fallbacks_and_self() { - private readonly LanguagesConfig config_0 = LanguagesConfig.English; - - [Fact] - public void Should_make_contains_test() - { - Assert.True(config_0.Contains(Language.EN)); - - Assert.False(config_0.Contains(Language.FR)); - Assert.False(config_0.Contains(null!)); - } - - [Fact] - public void Should_provide_name() - { - Assert.Equal("English", config_0.GetName(Language.EN)); - - Assert.Null(config_0.GetName(Language.FR)); - Assert.Null(config_0.GetName(null!)); - } - - [Fact] - public void Should_make_master_test() - { - var config = - LanguagesConfig.English - .Set(Language.DE) - .Set(Language.ES, true); - - Assert.True(config.IsMaster(Language.EN)); - - Assert.False(config.IsMaster(Language.DE)); - Assert.False(config.IsMaster(Language.ES)); - Assert.False(config.IsMaster(Language.FR)); - Assert.False(config.IsMaster(null!)); - } - - [Fact] - public void Should_make_optional_test() - { - var config = - LanguagesConfig.English - .Set(Language.DE) - .Set(Language.ES, true); - - Assert.True(config.IsOptional(Language.ES)); - - Assert.False(config.IsOptional(Language.EN)); - Assert.False(config.IsOptional(Language.DE)); - Assert.False(config.IsOptional(Language.FR)); - Assert.False(config.IsOptional(null!)); - } - - [Fact] - public void Should_provide_priorities() - { - var config = - LanguagesConfig.English - .Set(Language.DE) - .Set(Language.ES, true, Language.DE); - - Assert.Empty(config.GetPriorities(Language.FR)); - Assert.Empty(config.GetPriorities(null!)); - - Assert.Equal(new string[] { Language.ES, Language.DE, Language.EN }, config.GetPriorities(Language.ES)); - Assert.Equal(new string[] { Language.DE, Language.EN }, config.GetPriorities(Language.DE)); - Assert.Equal(new string[] { Language.EN }, config.GetPriorities(Language.EN)); - } - - [Fact] - public void Should_create_initial_config() - { - config_0.Languages.Should().BeEquivalentTo( - new Dictionary<string, LanguageConfig> - { - [Language.EN] = new LanguageConfig() - }); - - Assert.Equal(Language.EN, config_0.Master); - } - - [Fact] - public void Should_create_initial_config_0_with_multiple_languages() - { - var config = - LanguagesConfig.English - .Set(Language.DE) - .Set(Language.ES, true) - .Set(Language.IT, true, Language.ES) - .MakeMaster(Language.DE); - - config.Languages.Should().BeEquivalentTo( - new Dictionary<string, LanguageConfig> - { - [Language.EN] = new LanguageConfig(), - [Language.DE] = new LanguageConfig(), - [Language.ES] = new LanguageConfig(true), - [Language.IT] = new LanguageConfig(true, ReadonlyList.Create(Language.ES)) - }); - - Assert.Equal(Language.DE, config.Master); - } - - [Fact] - public void Should_not_throw_exception_if_language_to_add_already_exists() - { - config_0.Set(Language.EN); - } - - [Fact] - public void Should_return_same_language_if_already_added() - { - var config_1 = config_0.Set(Language.EN); - - Assert.Same(config_1, config_0); - } - - [Fact] - public void Should_make_master_language() - { - var config = - LanguagesConfig.English - .Set(Language.DE) - .Set(Language.IT, true, Language.ES) - .MakeMaster(Language.IT); - - config.Languages.Should().BeEquivalentTo( - new Dictionary<string, LanguageConfig> - { - [Language.EN] = new LanguageConfig(), - [Language.DE] = new LanguageConfig(), - [Language.IT] = new LanguageConfig() - }); - - Assert.Equal(Language.IT, config.Master); - } - - [Fact] - public void Should_return_same_languages_if_master_language_is_already_master() - { - var config_1 = config_0.Set(Language.DE); - var config_2 = config_1.Set(Language.IT); - var config_3 = config_2.MakeMaster(Language.IT); - var config_4 = config_3.MakeMaster(Language.IT); - - Assert.Same(config_3, config_4); - } - - [Fact] - public void Should_keep_master_language_if_language_to_make_master_is_not_found() - { - var config_1 = config_0.Set(Language.DE); - var config_2 = config_1.Set(Language.IT); - var config_3 = config_2.MakeMaster(Language.IT); - var config_4 = config_3.MakeMaster(Language.FR); - - Assert.Same(config_3, config_4); - Assert.Equal(Language.IT, config_4.Master); - } - - [Fact] - public void Should_remove_language() - { - var config_1 = config_0.Set(Language.DE); - var config_2 = config_1.Set(Language.IT); - var config_3 = config_2.Remove(Language.DE); - - config_3.Languages.Should().BeEquivalentTo( - new Dictionary<string, LanguageConfig> - { - [Language.EN] = new LanguageConfig(), - [Language.IT] = new LanguageConfig() - }); - - Assert.Equal(Language.EN, config_3.Master); - } - - [Fact] - public void Should_remove_fallbacks_if_removing_language() - { - var config_1 = config_0.Set(Language.DE); - var config_2 = config_1.Set(Language.IT, true, Language.UK); - var config_3 = config_2.Remove(Language.DE); - - config_3.Languages.Should().BeEquivalentTo( - new Dictionary<string, LanguageConfig> - { - [Language.EN] = new LanguageConfig(), - [Language.IT] = new LanguageConfig(true) - }); - - Assert.Equal(Language.EN, config_3.Master); - } - - [Fact] - public void Should_same_languages_if_removing_single_language() - { - var config_1 = config_0.Remove(Language.EN); - - Assert.Same(config_0, config_1); - } - - [Fact] - public void Should_update_master_language_if_removed() - { - var config_1 = config_0.Set(Language.DE); - var config_2 = config_1.Set(Language.IT); - var config_3 = config_2.Remove(Language.EN); - - config_3.Languages.Should().BeEquivalentTo( - new Dictionary<string, LanguageConfig> - { - [Language.DE] = new LanguageConfig(), - [Language.IT] = new LanguageConfig() - }); - - Assert.Equal(Language.DE, config_3.Master); - } - - [Fact] - public void Should_return_same_languages_if_language_to_remove_is_not_found() - { - var config_1 = config_0.Remove(Language.IT); - - Assert.Equal(config_0, config_1); - } - - [Fact] - public void Should_update_language() - { - var config_1 = config_0.Set(Language.IT); - var config_2 = config_1.Set(Language.IT, true, Language.EN); - - config_2.Languages.Should().BeEquivalentTo( - new Dictionary<string, LanguageConfig> - { - [Language.EN] = new LanguageConfig(), - [Language.IT] = new LanguageConfig(true, ReadonlyList.Create(Language.EN)) - }); - - Assert.Equal(Language.EN, config_2.Master); - } - - [Fact] - public void Should_eliminate_invalid_fallbacks_and_self() - { - var config_1 = config_0.Set(Language.IT); - var config_2 = config_1.Set(Language.IT); - var config_3 = config_2.Set(Language.IT, true, Language.EN, Language.IT, Language.DE); - - config_3.Languages.Should().BeEquivalentTo( - new Dictionary<string, LanguageConfig> - { - [Language.EN] = new LanguageConfig(), - [Language.IT] = new LanguageConfig(true, ReadonlyList.Create(Language.EN)) - }); - - Assert.Equal(Language.EN, config_2.Master); - } + var config_1 = config_0.Set(Language.IT); + var config_2 = config_1.Set(Language.IT); + var config_3 = config_2.Set(Language.IT, true, Language.EN, Language.IT, Language.DE); + + config_3.Languages.Should().BeEquivalentTo( + new Dictionary<string, LanguageConfig> + { + [Language.EN] = new LanguageConfig(), + [Language.IT] = new LanguageConfig(true, ReadonlyList.Create(Language.EN)) + }); + + Assert.Equal(Language.EN, config_2.Master); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs index a014663d46..37e518b75e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs @@ -8,71 +8,70 @@ using Squidex.Domain.Apps.Core.Apps; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class RoleTests { - public class RoleTests + [Fact] + public void Should_be_default_role() { - [Fact] - public void Should_be_default_role() - { - var role = new Role("Owner"); + var role = new Role("Owner"); - Assert.True(role.IsDefault); - } + Assert.True(role.IsDefault); + } - [Fact] - public void Should_not_be_default_role() - { - var role = new Role("Custom"); + [Fact] + public void Should_not_be_default_role() + { + var role = new Role("Custom"); - Assert.False(role.IsDefault); - } + Assert.False(role.IsDefault); + } - [Fact] - public void Should_not_add_common_permission() - { - var role = new Role("Name"); + [Fact] + public void Should_not_add_common_permission() + { + var role = new Role("Name"); - var actual = role.ForApp("my-app").Permissions.ToIds(); + var actual = role.ForApp("my-app").Permissions.ToIds(); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public void Should_not_have_duplicate_permission() - { - var role = Role.WithPermissions("Name", "common", "common", "common"); + [Fact] + public void Should_not_have_duplicate_permission() + { + var role = Role.WithPermissions("Name", "common", "common", "common"); - var actual = role.ForApp("my-app").Permissions.ToIds(); + var actual = role.ForApp("my-app").Permissions.ToIds(); - Assert.Single(actual); - } + Assert.Single(actual); + } - [Fact] - public void Should_append_app_prefix_to_permission() - { - var role = Role.WithPermissions("Name", "clients.read"); + [Fact] + public void Should_append_app_prefix_to_permission() + { + var role = Role.WithPermissions("Name", "clients.read"); - var actual = role.ForApp("my-app").Permissions.ToIds(); + var actual = role.ForApp("my-app").Permissions.ToIds(); - Assert.Equal("squidex.apps.my-app.clients.read", actual.ElementAt(0)); - } + Assert.Equal("squidex.apps.my-app.clients.read", actual.ElementAt(0)); + } - [Fact] - public void Should_check_for_name() - { - var role = Role.WithPermissions("Custom"); + [Fact] + public void Should_check_for_name() + { + var role = Role.WithPermissions("Custom"); - Assert.True(role.Equals("Custom")); - } + Assert.True(role.Equals("Custom")); + } - [Fact] - public void Should_check_for_null_name() - { - var role = Role.WithPermissions("Custom"); + [Fact] + public void Should_check_for_null_name() + { + var role = Role.WithPermissions("Custom"); - Assert.False(role.Equals((string)null!)); - Assert.False(role.Equals("Other")); - } + Assert.False(role.Equals((string)null!)); + Assert.False(role.Equals("Other")); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs index 5c07b55722..061cd0564f 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs @@ -12,62 +12,61 @@ using Squidex.Infrastructure.Security; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps; + +public class RolesJsonTests { - public class RolesJsonTests + [Fact] + public void Should_deserialize_from_old_role_format() { - [Fact] - public void Should_deserialize_from_old_role_format() + var source = new Dictionary<string, string[]> { - var source = new Dictionary<string, string[]> + ["Custom"] = new[] { - ["Custom"] = new[] - { - "Permission1", - "Permission2" - } - }; + "Permission1", + "Permission2" + } + }; - var expected = - Roles.Empty - .Add("Custom") - .Update("Custom", - new PermissionSet( - "Permission1", - "Permission2")); + var expected = + Roles.Empty + .Add("Custom") + .Update("Custom", + new PermissionSet( + "Permission1", + "Permission2")); - var roles = source.SerializeAndDeserialize<Roles, Dictionary<string, string[]>>(); + var roles = source.SerializeAndDeserialize<Roles, Dictionary<string, string[]>>(); - roles.Should().BeEquivalentTo(expected); - } + roles.Should().BeEquivalentTo(expected); + } - [Fact] - public void Should_serialize_and_deserialize() - { - var sut = - Roles.Empty - .Add("Custom") - .Update("Custom", - new PermissionSet( - "Permission1", - "Permission2"), - new JsonObject() - .Add("Property1", true) - .Add("Property2", true)); + [Fact] + public void Should_serialize_and_deserialize() + { + var sut = + Roles.Empty + .Add("Custom") + .Update("Custom", + new PermissionSet( + "Permission1", + "Permission2"), + new JsonObject() + .Add("Property1", true) + .Add("Property2", true)); - var roles = sut.SerializeAndDeserialize(); + var roles = sut.SerializeAndDeserialize(); - roles.Should().BeEquivalentTo(sut); - } + roles.Should().BeEquivalentTo(sut); + } - [Fact] - public void Should_serialize_and_deserialize_empty() - { - var sut = Roles.Empty; + [Fact] + public void Should_serialize_and_deserialize_empty() + { + var sut = Roles.Empty; - var roles = sut.SerializeAndDeserialize(); + var roles = sut.SerializeAndDeserialize(); - Assert.Same(Roles.Empty, roles); - } + Assert.Same(Roles.Empty, roles); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index 103875f7eb..1ae8dc23f4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -13,184 +13,183 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Core.Model.Apps -{ - public class RolesTests - { - private readonly Roles roles_0; - private readonly string firstRole = "Role1"; - private readonly string role = "Role2"; +namespace Squidex.Domain.Apps.Core.Model.Apps; - public RolesTests() - { - roles_0 = Roles.Empty.Add(firstRole); - } +public class RolesTests +{ + private readonly Roles roles_0; + private readonly string firstRole = "Role1"; + private readonly string role = "Role2"; - [Fact] - public void Should_create_roles_without_defaults() - { - var roles = new Roles(new Dictionary<string, Role>(Roles.Defaults)); + public RolesTests() + { + roles_0 = Roles.Empty.Add(firstRole); + } - Assert.Equal(0, roles.CustomCount); - } + [Fact] + public void Should_create_roles_without_defaults() + { + var roles = new Roles(new Dictionary<string, Role>(Roles.Defaults)); - [Fact] - public void Should_add_role() - { - var roles_1 = roles_0.Add(role); + Assert.Equal(0, roles.CustomCount); + } - Assert.Equal(new Role(role, null, new JsonObject()), roles_1[role]); - } + [Fact] + public void Should_add_role() + { + var roles_1 = roles_0.Add(role); - [Fact] - public void Should_return_same_instance_if_adding_role_with_existing_name() - { - var roles_1 = roles_0.Add(role); - var roles_2 = roles_1.Add(role); + Assert.Equal(new Role(role, null, new JsonObject()), roles_1[role]); + } - Assert.Same(roles_1, roles_2); - } + [Fact] + public void Should_return_same_instance_if_adding_role_with_existing_name() + { + var roles_1 = roles_0.Add(role); + var roles_2 = roles_1.Add(role); - [Fact] - public void Should_do_nothing_if_role_to_add_is_default() - { - var roles_1 = roles_0.Add(Role.Developer); + Assert.Same(roles_1, roles_2); + } - Assert.True(roles_1.CustomCount > 0); - } + [Fact] + public void Should_do_nothing_if_role_to_add_is_default() + { + var roles_1 = roles_0.Add(Role.Developer); - [Fact] - public void Should_update_role_permissions() - { - var roles_1 = roles_0.Update(firstRole, permissions: new PermissionSet("P1", "P2")); + Assert.True(roles_1.CustomCount > 0); + } - roles_1[firstRole].Should().BeEquivalentTo(Role.WithPermissions(firstRole, "P1", "P2")); - } + [Fact] + public void Should_update_role_permissions() + { + var roles_1 = roles_0.Update(firstRole, permissions: new PermissionSet("P1", "P2")); - [Fact] - public void Should_update_role_properties() - { - var roles_1 = roles_0.Update(firstRole, properties: new JsonObject().Add("P1", true)); + roles_1[firstRole].Should().BeEquivalentTo(Role.WithPermissions(firstRole, "P1", "P2")); + } - roles_1[firstRole].Should().BeEquivalentTo(Role.WithProperties(firstRole, new JsonObject().Add("P1", true))); - } + [Fact] + public void Should_update_role_properties() + { + var roles_1 = roles_0.Update(firstRole, properties: new JsonObject().Add("P1", true)); - [Fact] - public void Should_return_same_roles_if_role_is_updated_with_same_permissions() - { - var roles_1 = roles_0.Update(firstRole); + roles_1[firstRole].Should().BeEquivalentTo(Role.WithProperties(firstRole, new JsonObject().Add("P1", true))); + } - Assert.Same(roles_0, roles_1); - } + [Fact] + public void Should_return_same_roles_if_role_is_updated_with_same_permissions() + { + var roles_1 = roles_0.Update(firstRole); - [Fact] - public void Should_return_same_roles_if_role_not_found() - { - var roles_1 = roles_0.Update(role, permissions: new PermissionSet("P1", "P2")); + Assert.Same(roles_0, roles_1); + } - Assert.Same(roles_0, roles_1); - } + [Fact] + public void Should_return_same_roles_if_role_not_found() + { + var roles_1 = roles_0.Update(role, permissions: new PermissionSet("P1", "P2")); - [Fact] - public void Should_remove_role() - { - var roles_1 = roles_0.Add("role1"); - var roles_2 = roles_1.Add("role2"); - var roles_3 = roles_2.Remove(firstRole); + Assert.Same(roles_0, roles_1); + } - Assert.Equal(new[] { "role1", "role2" }, roles_3.Custom.Select(x => x.Name)); - } + [Fact] + public void Should_remove_role() + { + var roles_1 = roles_0.Add("role1"); + var roles_2 = roles_1.Add("role2"); + var roles_3 = roles_2.Remove(firstRole); - [Fact] - public void Should_do_nothing_if_remove_role_not_found() - { - var roles_1 = roles_0.Remove(role); + Assert.Equal(new[] { "role1", "role2" }, roles_3.Custom.Select(x => x.Name)); + } - Assert.Same(roles_0, roles_1); - } + [Fact] + public void Should_do_nothing_if_remove_role_not_found() + { + var roles_1 = roles_0.Remove(role); - [Fact] - public void Should_get_custom_roles() - { - var names = roles_0.Custom.Select(x => x.Name).ToArray(); + Assert.Same(roles_0, roles_1); + } - Assert.Equal(new[] { firstRole }, names); - } + [Fact] + public void Should_get_custom_roles() + { + var names = roles_0.Custom.Select(x => x.Name).ToArray(); - [Fact] - public void Should_get_all_roles() - { - var names = roles_0.All.Select(x => x.Name).ToArray(); + Assert.Equal(new[] { firstRole }, names); + } - Assert.Equal(new[] { firstRole, "Owner", "Reader", "Editor", "Developer" }, names); - } + [Fact] + public void Should_get_all_roles() + { + var names = roles_0.All.Select(x => x.Name).ToArray(); - [Fact] - public void Should_check_for_custom_role() - { - Assert.True(roles_0.ContainsCustom(firstRole)); - } + Assert.Equal(new[] { firstRole, "Owner", "Reader", "Editor", "Developer" }, names); + } - [Fact] - public void Should_check_for_non_custom_role() - { - Assert.False(roles_0.ContainsCustom(Role.Owner)); - } + [Fact] + public void Should_check_for_custom_role() + { + Assert.True(roles_0.ContainsCustom(firstRole)); + } - [Fact] - public void Should_check_for_default_role() - { - Assert.True(Roles.IsDefault(Role.Owner)); - } + [Fact] + public void Should_check_for_non_custom_role() + { + Assert.False(roles_0.ContainsCustom(Role.Owner)); + } - [Fact] - public void Should_check_for_non_default_role() - { - Assert.False(Roles.IsDefault(firstRole)); - } + [Fact] + public void Should_check_for_default_role() + { + Assert.True(Roles.IsDefault(Role.Owner)); + } - [InlineData("Developer", 6)] - [InlineData("Editor", 4)] - [InlineData("Reader", 2)] - [InlineData("Owner", 1)] - [Theory] - public void Should_get_default_roles(string name, int permissionCount) - { - var found = roles_0.TryGet("app", name, false, out var actual); + [Fact] + public void Should_check_for_non_default_role() + { + Assert.False(Roles.IsDefault(firstRole)); + } - Assert.True(found); - Assert.True(actual!.IsDefault); - Assert.True(roles_0.Contains(name)); + [InlineData("Developer", 6)] + [InlineData("Editor", 4)] + [InlineData("Reader", 2)] + [InlineData("Owner", 1)] + [Theory] + public void Should_get_default_roles(string name, int permissionCount) + { + var found = roles_0.TryGet("app", name, false, out var actual); - foreach (var permission in actual.Permissions) - { - Assert.StartsWith("squidex.apps.app.", permission.Id, StringComparison.Ordinal); - Assert.DoesNotContain("{app}", permission.Id, StringComparison.Ordinal); - } + Assert.True(found); + Assert.True(actual!.IsDefault); + Assert.True(roles_0.Contains(name)); - Assert.Equal(permissionCount, actual!.Permissions.Count); + foreach (var permission in actual.Permissions) + { + Assert.StartsWith("squidex.apps.app.", permission.Id, StringComparison.Ordinal); + Assert.DoesNotContain("{app}", permission.Id, StringComparison.Ordinal); } - [InlineData("Developer", 15)] - [InlineData("Editor", 13)] - [InlineData("Reader", 12)] - [InlineData("Owner", 1)] - [Theory] - public void Should_add_extra_permissions_for_frontend_client(string name, int permissionCount) - { - var found = roles_0.TryGet("app", name, true, out var actual); + Assert.Equal(permissionCount, actual!.Permissions.Count); + } - Assert.True(found); - Assert.Equal(permissionCount, actual!.Permissions.Count); - } + [InlineData("Developer", 15)] + [InlineData("Editor", 13)] + [InlineData("Reader", 12)] + [InlineData("Owner", 1)] + [Theory] + public void Should_add_extra_permissions_for_frontend_client(string name, int permissionCount) + { + var found = roles_0.TryGet("app", name, true, out var actual); - [Fact] - public void Should_return_null_if_role_not_found() - { - var found = roles_0.TryGet("app", "custom", false, out var actual); + Assert.True(found); + Assert.Equal(permissionCount, actual!.Permissions.Count); + } - Assert.False(found); - Assert.Null(actual); - } + [Fact] + public void Should_return_null_if_role_not_found() + { + var found = roles_0.TryGet("app", "custom", false, out var actual); + + Assert.False(found); + Assert.Null(actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs index f73ced573b..b17d8602e5 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Assets/AssetMetadataTests.cs @@ -9,186 +9,185 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Assets +namespace Squidex.Domain.Apps.Core.Model.Assets; + +public class AssetMetadataTests { - public class AssetMetadataTests + [Fact] + public void Should_return_focus_infos_if_found() { - [Fact] - public void Should_return_focus_infos_if_found() - { - var sut = - new AssetMetadata() - .SetFocusX(.5f) - .SetFocusY(.2f); + var sut = + new AssetMetadata() + .SetFocusX(.5f) + .SetFocusY(.2f); - Assert.Equal(.5f, sut.GetFocusX()); - Assert.Equal(.2f, sut.GetFocusY()); - } + Assert.Equal(.5f, sut.GetFocusX()); + Assert.Equal(.2f, sut.GetFocusY()); + } - [Fact] - public void Should_return_null_if_focus_infos_not_found() - { - var sut = new AssetMetadata(); + [Fact] + public void Should_return_null_if_focus_infos_not_found() + { + var sut = new AssetMetadata(); - Assert.Null(sut.GetFocusX()); - Assert.Null(sut.GetFocusY()); - } + Assert.Null(sut.GetFocusX()); + Assert.Null(sut.GetFocusY()); + } - [Fact] - public void Should_return_pixel_infos_if_found() - { - var sut = - new AssetMetadata() - .SetPixelWidth(800) - .SetPixelHeight(600); + [Fact] + public void Should_return_pixel_infos_if_found() + { + var sut = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600); - Assert.Equal(800, sut.GetPixelWidth()); - Assert.Equal(600, sut.GetPixelHeight()); - } + Assert.Equal(800, sut.GetPixelWidth()); + Assert.Equal(600, sut.GetPixelHeight()); + } - [Fact] - public void Should_return_null_if_pixel_infos_not_found() - { - var sut = new AssetMetadata(); + [Fact] + public void Should_return_null_if_pixel_infos_not_found() + { + var sut = new AssetMetadata(); - Assert.Null(sut.GetPixelWidth()); - Assert.Null(sut.GetPixelHeight()); - } + Assert.Null(sut.GetPixelWidth()); + Assert.Null(sut.GetPixelHeight()); + } - [Fact] - public void Should_return_self_if_path_is_empty() - { - var sut = new AssetMetadata(); + [Fact] + public void Should_return_self_if_path_is_empty() + { + var sut = new AssetMetadata(); - var found = sut.TryGetByPath(string.Empty, out var actual); + var found = sut.TryGetByPath(string.Empty, out var actual); - Assert.False(found); - Assert.Same(sut, actual); - } + Assert.False(found); + Assert.Same(sut, actual); + } - [Fact] - public void Should_return_plain_value_if_found() + [Fact] + public void Should_return_plain_value_if_found() + { + var sut = new AssetMetadata { - var sut = new AssetMetadata - { - ["someValue"] = JsonValue.Create(800) - }; + ["someValue"] = JsonValue.Create(800) + }; - var found = sut.TryGetByPath("someValue", out var actual); + var found = sut.TryGetByPath("someValue", out var actual); - Assert.True(found); - Assert.Equal(JsonValue.Create(800), actual); - } + Assert.True(found); + Assert.Equal(JsonValue.Create(800), actual); + } - [Fact] - public void Should_return_null_if_not_found() - { - var sut = new AssetMetadata(); + [Fact] + public void Should_return_null_if_not_found() + { + var sut = new AssetMetadata(); - var found = sut.TryGetByPath("notFound", out var actual); + var found = sut.TryGetByPath("notFound", out var actual); - Assert.False(found); - Assert.Null(actual); - } + Assert.False(found); + Assert.Null(actual); + } - [Fact] - public void Should_return_nested_value_if_found() - { - var sut = new AssetMetadata - { - ["meta"] = - new JsonObject() - .Add("nested1", - new JsonObject() - .Add("nested2", 12)) - }; - - var found = sut.TryGetByPath("meta.nested1.nested2", out var actual); - - Assert.True(found); - Assert.Equal(JsonValue.Create(12), actual); - } - - [Fact] - public void Should_get_string_if_found() + [Fact] + public void Should_return_nested_value_if_found() + { + var sut = new AssetMetadata { - var value = "Hello"; + ["meta"] = + new JsonObject() + .Add("nested1", + new JsonObject() + .Add("nested2", 12)) + }; - var sut = new AssetMetadata - { - ["string"] = JsonValue.Create(value) - }; + var found = sut.TryGetByPath("meta.nested1.nested2", out var actual); - var found = sut.TryGetString("string", out var actual); + Assert.True(found); + Assert.Equal(JsonValue.Create(12), actual); + } - Assert.True(found); - Assert.Equal(value, actual); - } + [Fact] + public void Should_get_string_if_found() + { + var value = "Hello"; - [Fact] - public void Should_get_null_if_property_is_not_string() + var sut = new AssetMetadata { - var sut = new AssetMetadata - { - ["string"] = JsonValue.Create(12) - }; + ["string"] = JsonValue.Create(value) + }; - var found = sut.TryGetString("string", out var actual); + var found = sut.TryGetString("string", out var actual); - Assert.False(found); - Assert.Null(actual); - } + Assert.True(found); + Assert.Equal(value, actual); + } - [Fact] - public void Should_get_null_if_string_property_not_found() + [Fact] + public void Should_get_null_if_property_is_not_string() + { + var sut = new AssetMetadata { - var sut = new AssetMetadata(); + ["string"] = JsonValue.Create(12) + }; - var found = sut.TryGetString("other", out var actual); + var found = sut.TryGetString("string", out var actual); - Assert.False(found); - Assert.Null(actual); - } + Assert.False(found); + Assert.Null(actual); + } - [Fact] - public void Should_get_number_if_found() - { - var value = 12.5; + [Fact] + public void Should_get_null_if_string_property_not_found() + { + var sut = new AssetMetadata(); - var sut = new AssetMetadata - { - ["number"] = JsonValue.Create(value) - }; + var found = sut.TryGetString("other", out var actual); - var found = sut.TryGetNumber("number", out var actual); + Assert.False(found); + Assert.Null(actual); + } - Assert.True(found); - Assert.Equal(value, actual); - } + [Fact] + public void Should_get_number_if_found() + { + var value = 12.5; - [Fact] - public void Should_get_null_if_property_is_not_number() + var sut = new AssetMetadata { - var sut = new AssetMetadata - { - ["number"] = JsonValue.Create(true) - }; + ["number"] = JsonValue.Create(value) + }; - var found = sut.TryGetNumber("number", out var actual); + var found = sut.TryGetNumber("number", out var actual); - Assert.False(found); - Assert.Equal(0, actual); - } + Assert.True(found); + Assert.Equal(value, actual); + } - [Fact] - public void Should_get_null_if_number_property_not_found() + [Fact] + public void Should_get_null_if_property_is_not_number() + { + var sut = new AssetMetadata { - var sut = new AssetMetadata(); + ["number"] = JsonValue.Create(true) + }; + + var found = sut.TryGetNumber("number", out var actual); + + Assert.False(found); + Assert.Equal(0, actual); + } + + [Fact] + public void Should_get_null_if_number_property_not_found() + { + var sut = new AssetMetadata(); - var found = sut.TryGetNumber("other", out var actual); + var found = sut.TryGetNumber("other", out var actual); - Assert.False(found); - Assert.Equal(0, actual); - } + Assert.False(found); + Assert.Equal(0, actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs index 663491eecd..9a3ad5dc10 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs @@ -8,206 +8,205 @@ using Squidex.Domain.Apps.Core.Contents; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents; + +public class ContentDataTests { - public class ContentDataTests + [Fact] + public void Should_return_same_content_if_merging_same_references() { - [Fact] - public void Should_return_same_content_if_merging_same_references() - { - var source = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(1)) - .AddField("field2", - new ContentFieldData() - .AddLocalized("de", 2)); - - var actual = source.MergeInto(source); - - Assert.Same(source, actual); - } + var source = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field2", + new ContentFieldData() + .AddLocalized("de", 2)); + + var actual = source.MergeInto(source); + + Assert.Same(source, actual); + } - [Fact] - public void Should_merge_two_name_models() - { - var lhs = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(1)) - .AddField("field2", - new ContentFieldData() - .AddLocalized("de", 2) - .AddLocalized("it", 2)); - - var rhs = - new ContentData() - .AddField("field2", - new ContentFieldData() - .AddLocalized("it", 3) - .AddLocalized("en", 3)) - .AddField("field3", - new ContentFieldData() - .AddInvariant(4)); - - var expected = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(1)) - .AddField("field2", - new ContentFieldData() - .AddLocalized("it", 2) - .AddLocalized("de", 2) - .AddLocalized("en", 3)) - .AddField("field3", - new ContentFieldData() - .AddInvariant(4)); - - var actual = lhs.MergeInto(rhs); - - Assert.Equal(expected, actual); - Assert.NotSame(expected, rhs); - Assert.NotSame(expected, lhs); - } + [Fact] + public void Should_merge_two_name_models() + { + var lhs = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field2", + new ContentFieldData() + .AddLocalized("de", 2) + .AddLocalized("it", 2)); + + var rhs = + new ContentData() + .AddField("field2", + new ContentFieldData() + .AddLocalized("it", 3) + .AddLocalized("en", 3)) + .AddField("field3", + new ContentFieldData() + .AddInvariant(4)); + + var expected = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field2", + new ContentFieldData() + .AddLocalized("it", 2) + .AddLocalized("de", 2) + .AddLocalized("en", 3)) + .AddField("field3", + new ContentFieldData() + .AddInvariant(4)); + + var actual = lhs.MergeInto(rhs); + + Assert.Equal(expected, actual); + Assert.NotSame(expected, rhs); + Assert.NotSame(expected, lhs); + } - [Fact] - public void Should_be_equal_if_data_have_same_structure() - { - var lhs = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(2)) - .AddField("field2", - new ContentFieldData() - .AddInvariant(2)); - - var rhs = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(2)) - .AddField("field2", - new ContentFieldData() - .AddInvariant(2)); - - Assert.True(lhs.Equals(rhs)); - Assert.True(lhs.Equals((object)rhs)); - Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); - } + [Fact] + public void Should_be_equal_if_data_have_same_structure() + { + var lhs = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(2)) + .AddField("field2", + new ContentFieldData() + .AddInvariant(2)); + + var rhs = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(2)) + .AddField("field2", + new ContentFieldData() + .AddInvariant(2)); + + Assert.True(lhs.Equals(rhs)); + Assert.True(lhs.Equals((object)rhs)); + Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); + } - [Fact] - public void Should_not_be_equal_if_data_have_not_same_structure() - { - var lhs = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(2)) - .AddField("field2", - new ContentFieldData() - .AddInvariant(2)); - - var rhs = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddLocalized("en", 2)) - .AddField("field3", - new ContentFieldData() - .AddInvariant(2)); - - Assert.False(lhs.Equals(rhs)); - Assert.False(lhs.Equals((object)rhs)); - Assert.NotEqual(lhs.GetHashCode(), rhs.GetHashCode()); - } + [Fact] + public void Should_not_be_equal_if_data_have_not_same_structure() + { + var lhs = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(2)) + .AddField("field2", + new ContentFieldData() + .AddInvariant(2)); + + var rhs = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddLocalized("en", 2)) + .AddField("field3", + new ContentFieldData() + .AddInvariant(2)); + + Assert.False(lhs.Equals(rhs)); + Assert.False(lhs.Equals((object)rhs)); + Assert.NotEqual(lhs.GetHashCode(), rhs.GetHashCode()); + } - [Fact] - public void Should_be_equal_fields_if_they_have_same_value() - { - var lhs = - new ContentFieldData() - .AddInvariant(2); + [Fact] + public void Should_be_equal_fields_if_they_have_same_value() + { + var lhs = + new ContentFieldData() + .AddInvariant(2); - var rhs = - new ContentFieldData() - .AddInvariant(2); + var rhs = + new ContentFieldData() + .AddInvariant(2); - Assert.True(lhs.Equals(rhs)); - Assert.True(lhs.Equals((object)rhs)); - Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); - } + Assert.True(lhs.Equals(rhs)); + Assert.True(lhs.Equals((object)rhs)); + Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); + } - [Fact] - public void Should_clone_named_value_and_also_children() + [Fact] + public void Should_clone_named_value_and_also_children() + { + var source = new ContentData { - var source = new ContentData - { - ["field1"] = new ContentFieldData(), - ["field2"] = new ContentFieldData() - }; + ["field1"] = new ContentFieldData(), + ["field2"] = new ContentFieldData() + }; - var clone = source.Clone(); + var clone = source.Clone(); - Assert.NotSame(source, clone); - - foreach (var (key, value) in clone) - { - Assert.NotSame(value, source[key]); - } - } + Assert.NotSame(source, clone); - [Fact] - public void Should_copy_fields_from_other_data_if_they_are_equal() + foreach (var (key, value) in clone) { - var oldData = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(1)) - .AddField("field2", - new ContentFieldData() - .AddInvariant(2)); - - var newData = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(1)) - .AddField("field2", - new ContentFieldData() - .AddInvariant(3)); - - newData.UseSameFields(oldData); - - Assert.Same(newData["field1"], oldData["field1"]); - Assert.NotSame(newData["field2"], oldData["field2"]); + Assert.NotSame(value, source[key]); } + } - [Fact] - public void Should_copy_field_values_from_other_data_if_they_are_equal() - { - var oldData = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddLocalized("en", 1) - .AddLocalized("de", 2)); - var newData = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddLocalized("en", 1) - .AddLocalized("de", 3)); - - newData.UseSameFields(oldData); - - Assert.Same(newData["field1"]!["en"].Value, oldData["field1"]!["en"].Value); - Assert.NotSame(newData["field1"]!["de"].Value, oldData["field1"]!["de"].Value); - Assert.NotSame(newData["field1"], oldData["field1"]); - } + [Fact] + public void Should_copy_fields_from_other_data_if_they_are_equal() + { + var oldData = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field2", + new ContentFieldData() + .AddInvariant(2)); + + var newData = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field2", + new ContentFieldData() + .AddInvariant(3)); + + newData.UseSameFields(oldData); + + Assert.Same(newData["field1"], oldData["field1"]); + Assert.NotSame(newData["field2"], oldData["field2"]); + } + + [Fact] + public void Should_copy_field_values_from_other_data_if_they_are_equal() + { + var oldData = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddLocalized("en", 1) + .AddLocalized("de", 2)); + var newData = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddLocalized("en", 1) + .AddLocalized("de", 3)); + + newData.UseSameFields(oldData); + + Assert.Same(newData["field1"]!["en"].Value, oldData["field1"]!["en"].Value); + Assert.NotSame(newData["field1"]!["de"].Value, oldData["field1"]!["de"].Value); + Assert.NotSame(newData["field1"], oldData["field1"]); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs index a300b9f55a..a268bb7eac 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs @@ -10,75 +10,74 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents; + +public class ContentFieldDataTests { - public class ContentFieldDataTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var fieldData = - new ContentFieldData() - .AddInvariant(12); + var fieldData = + new ContentFieldData() + .AddInvariant(12); - var serialized = fieldData.SerializeAndDeserialize(); + var serialized = fieldData.SerializeAndDeserialize(); - Assert.Equal(fieldData, serialized); - } + Assert.Equal(fieldData, serialized); + } - [Fact] - public void Should_intern_invariant_key() - { - var fieldData = - new ContentFieldData() - .AddInvariant(12); + [Fact] + public void Should_intern_invariant_key() + { + var fieldData = + new ContentFieldData() + .AddInvariant(12); - var serialized = fieldData.SerializeAndDeserialize(); + var serialized = fieldData.SerializeAndDeserialize(); - Assert.NotNull(string.IsInterned(serialized.Keys.First())); - } + Assert.NotNull(string.IsInterned(serialized.Keys.First())); + } - [Fact] - public void Should_intern_known_language() - { - var fieldData = - new ContentFieldData() - .AddLocalized("en", 12); + [Fact] + public void Should_intern_known_language() + { + var fieldData = + new ContentFieldData() + .AddLocalized("en", 12); - var serialized = fieldData.SerializeAndDeserialize(); + var serialized = fieldData.SerializeAndDeserialize(); - Assert.NotNull(string.IsInterned(serialized.Keys.First())); - } + Assert.NotNull(string.IsInterned(serialized.Keys.First())); + } - [Fact] - public void Should_not_intern_unknown_key() - { - var fieldData = - new ContentFieldData() - .AddLocalized(Guid.NewGuid().ToString(), 12); + [Fact] + public void Should_not_intern_unknown_key() + { + var fieldData = + new ContentFieldData() + .AddLocalized(Guid.NewGuid().ToString(), 12); - var serialized = fieldData.SerializeAndDeserialize(); + var serialized = fieldData.SerializeAndDeserialize(); - Assert.Null(string.IsInterned(serialized.Keys.First())); - } + Assert.Null(string.IsInterned(serialized.Keys.First())); + } - [Fact] - public void Should_clone_value_and_also_children() + [Fact] + public void Should_clone_value_and_also_children() + { + var source = new ContentFieldData { - var source = new ContentFieldData - { - ["en"] = new JsonArray(), - ["de"] = new JsonArray() - }; + ["en"] = new JsonArray(), + ["de"] = new JsonArray() + }; - var clone = source.Clone(); + var clone = source.Clone(); - Assert.NotSame(source, clone); + Assert.NotSame(source, clone); - foreach (var (key, value) in clone) - { - Assert.NotSame(value.Value, source[key].Value); - } + foreach (var (key, value) in clone) + { + Assert.NotSame(value.Value, source[key].Value); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs index 73c6f51539..a18c2dd20b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs @@ -10,117 +10,116 @@ using Squidex.Domain.Apps.Core.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents; + +public class StatusTests { - public class StatusTests - { - private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(Status)); + private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(Status)); - [Fact] - public void Should_convert_from_string() - { - var actual = typeConverter.ConvertFromString("Draft"); + [Fact] + public void Should_convert_from_string() + { + var actual = typeConverter.ConvertFromString("Draft"); - Assert.Equal(Status.Draft, actual); - } + Assert.Equal(Status.Draft, actual); + } - [Fact] - public void Should_convert_to_string() - { - var actual = typeConverter.ConvertToString(Status.Draft); + [Fact] + public void Should_convert_to_string() + { + var actual = typeConverter.ConvertToString(Status.Draft); - Assert.Equal("Draft", actual); - } + Assert.Equal("Draft", actual); + } - [Fact] - public void Should_initialize_default() - { - Status status = default; + [Fact] + public void Should_initialize_default() + { + Status status = default; - Assert.Equal("Unknown", status.Name); - Assert.Equal("Unknown", status.ToString()); - } + Assert.Equal("Unknown", status.Name); + Assert.Equal("Unknown", status.ToString()); + } - [Fact] - public void Should_initialize_status_from_string() - { - var status = new Status("Custom"); + [Fact] + public void Should_initialize_status_from_string() + { + var status = new Status("Custom"); - Assert.Equal("Custom", status.Name); - Assert.Equal("Custom", status.ToString()); - } + Assert.Equal("Custom", status.Name); + Assert.Equal("Custom", status.ToString()); + } - [Fact] - public void Should_provide_draft_status() - { - var status = Status.Draft; + [Fact] + public void Should_provide_draft_status() + { + var status = Status.Draft; - Assert.Equal("Draft", status.Name); - Assert.Equal("Draft", status.ToString()); - } + Assert.Equal("Draft", status.Name); + Assert.Equal("Draft", status.ToString()); + } - [Fact] - public void Should_provide_archived_status() - { - var status = Status.Archived; + [Fact] + public void Should_provide_archived_status() + { + var status = Status.Archived; - Assert.Equal("Archived", status.Name); - Assert.Equal("Archived", status.ToString()); - } + Assert.Equal("Archived", status.Name); + Assert.Equal("Archived", status.ToString()); + } - [Fact] - public void Should_provide_published_status() - { - var status = Status.Published; + [Fact] + public void Should_provide_published_status() + { + var status = Status.Published; - Assert.Equal("Published", status.Name); - Assert.Equal("Published", status.ToString()); - } + Assert.Equal("Published", status.Name); + Assert.Equal("Published", status.ToString()); + } - [Fact] - public void Should_make_correct_equal_comparisons() - { - var status_1_a = Status.Draft; - var status_1_b = Status.Draft; + [Fact] + public void Should_make_correct_equal_comparisons() + { + var status_1_a = Status.Draft; + var status_1_b = Status.Draft; - var status2_a = Status.Published; + var status2_a = Status.Published; - Assert.Equal(status_1_a, status_1_b); - Assert.Equal(status_1_a.GetHashCode(), status_1_b.GetHashCode()); - Assert.True(status_1_a.Equals((object)status_1_b)); + Assert.Equal(status_1_a, status_1_b); + Assert.Equal(status_1_a.GetHashCode(), status_1_b.GetHashCode()); + Assert.True(status_1_a.Equals((object)status_1_b)); - Assert.NotEqual(status_1_a, status2_a); - Assert.NotEqual(status_1_a.GetHashCode(), status2_a.GetHashCode()); - Assert.False(status_1_a.Equals((object)status2_a)); + Assert.NotEqual(status_1_a, status2_a); + Assert.NotEqual(status_1_a.GetHashCode(), status2_a.GetHashCode()); + Assert.False(status_1_a.Equals((object)status2_a)); - Assert.True(status_1_a == status_1_b); - Assert.True(status_1_a != status2_a); + Assert.True(status_1_a == status_1_b); + Assert.True(status_1_a != status2_a); - Assert.False(status_1_a != status_1_b); - Assert.False(status_1_a == status2_a); - } + Assert.False(status_1_a != status_1_b); + Assert.False(status_1_a == status2_a); + } - [Fact] - public void Should_serialize_and_deserialize() - { - var status = Status.Draft; + [Fact] + public void Should_serialize_and_deserialize() + { + var status = Status.Draft; - var serialized = status.SerializeAndDeserialize(); + var serialized = status.SerializeAndDeserialize(); - Assert.Equal(status, serialized); - } + Assert.Equal(status, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_as_dictionary_key() + [Fact] + public void Should_serialize_and_deserialize_as_dictionary_key() + { + var dictionary = new Dictionary<Status, int> { - var dictionary = new Dictionary<Status, int> - { - [Status.Draft] = 123 - }; + [Status.Draft] = 123 + }; - var serialized = dictionary.SerializeAndDeserialize(); + var serialized = dictionary.SerializeAndDeserialize(); - Assert.Equal(123, serialized[Status.Draft]); - } + Assert.Equal(123, serialized[Status.Draft]); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs index 20b0c21fd3..468c1795b4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs @@ -11,93 +11,92 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents; + +public class TranslationStatusTests { - public class TranslationStatusTests + private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE).Set(Language.IT); + + [Fact] + public void Should_create_info_for_empty_schema() { - private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE).Set(Language.IT); + var schema = new Schema("my-schema"); - [Fact] - public void Should_create_info_for_empty_schema() + var actual = TranslationStatus.Create(new ContentData(), schema, languages); + + Assert.Equal(new TranslationStatus { - var schema = new Schema("my-schema"); + [Language.EN] = 100, + [Language.DE] = 100, + [Language.IT] = 100 + }, actual); + } - var actual = TranslationStatus.Create(new ContentData(), schema, languages); + [Fact] + public void Should_create_info_for_schema_without_localized_field() + { + var schema = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Invariant); - Assert.Equal(new TranslationStatus - { - [Language.EN] = 100, - [Language.DE] = 100, - [Language.IT] = 100 - }, actual); - } + var actual = TranslationStatus.Create(new ContentData(), schema, languages); - [Fact] - public void Should_create_info_for_schema_without_localized_field() + Assert.Equal(new TranslationStatus { - var schema = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Invariant); - - var actual = TranslationStatus.Create(new ContentData(), schema, languages); - - Assert.Equal(new TranslationStatus - { - [Language.EN] = 100, - [Language.DE] = 100, - [Language.IT] = 100 - }, actual); - } - - [Fact] - public void Should_create_info_for_schema_with_localized_field() + [Language.EN] = 100, + [Language.DE] = 100, + [Language.IT] = 100 + }, actual); + } + + [Fact] + public void Should_create_info_for_schema_with_localized_field() + { + var schema = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Language); + + var actual = TranslationStatus.Create(new ContentData(), schema, languages); + + Assert.Equal(new TranslationStatus { - var schema = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Language); - - var actual = TranslationStatus.Create(new ContentData(), schema, languages); - - Assert.Equal(new TranslationStatus - { - [Language.EN] = 0, - [Language.DE] = 0, - [Language.IT] = 0 - }, actual); - } - - [Fact] - public void Should_create_translation_info() + [Language.EN] = 0, + [Language.DE] = 0, + [Language.IT] = 0 + }, actual); + } + + [Fact] + public void Should_create_translation_info() + { + var schema = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Language) + .AddString(2, "field2", Partitioning.Language) + .AddString(3, "field3", Partitioning.Language) + .AddString(4, "field4", Partitioning.Invariant); + + var data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddLocalized(Language.EN, "en") + .AddLocalized(Language.DE, "de")) + .AddField("field2", + new ContentFieldData() + .AddLocalized(Language.EN, "en") + .AddLocalized(Language.DE, "de")) + .AddField("field3", + new ContentFieldData() + .AddLocalized(Language.EN, "en")); + + var actual = TranslationStatus.Create(data, schema, languages); + + Assert.Equal(new TranslationStatus { - var schema = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Language) - .AddString(2, "field2", Partitioning.Language) - .AddString(3, "field3", Partitioning.Language) - .AddString(4, "field4", Partitioning.Invariant); - - var data = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddLocalized(Language.EN, "en") - .AddLocalized(Language.DE, "de")) - .AddField("field2", - new ContentFieldData() - .AddLocalized(Language.EN, "en") - .AddLocalized(Language.DE, "de")) - .AddField("field3", - new ContentFieldData() - .AddLocalized(Language.EN, "en")); - - var actual = TranslationStatus.Create(data, schema, languages); - - Assert.Equal(new TranslationStatus - { - [Language.EN] = 100, - [Language.DE] = 67, - [Language.IT] = 0 - }, actual); - } + [Language.EN] = 100, + [Language.DE] = 67, + [Language.IT] = 0 + }, actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs index 836de411d3..fab8b80538 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs @@ -12,73 +12,72 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents; + +public class WorkflowJsonTests { - public class WorkflowJsonTests + [Fact] + public void Should_serialize_and_deserialize() + { + var workflow = new Workflow( + Status.Draft, + new Dictionary<Status, WorkflowStep> + { + [Status.Draft] = new WorkflowStep( + new Dictionary<Status, WorkflowTransition> + { + [Status.Published] = WorkflowTransition.When("Expression", "Role1", "Role2") + }.ToReadonlyDictionary(), + "#00ff00", + NoUpdate.When("Expression", "Role1", "Role2"), + true) + }.ToReadonlyDictionary(), + ReadonlyList.Create(DomainId.NewGuid()), "MyName"); + + var serialized = workflow.SerializeAndDeserialize(); + + Assert.Equal(workflow, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_default() + { + var workflow = Workflow.Default; + + var serialized = workflow.SerializeAndDeserialize(); + + Assert.Equal(workflow, serialized); + } + + [Fact] + public void Should_deserialize_old_noUpdate_condition() + { + var jsonStep = new { noUpdate = true }; + + var serialized = jsonStep.SerializeAndDeserialize<WorkflowStep, object>(); + + Assert.Equal(new WorkflowStep(null, null, NoUpdate.Always), serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_no_update_condition() { - [Fact] - public void Should_serialize_and_deserialize() - { - var workflow = new Workflow( - Status.Draft, - new Dictionary<Status, WorkflowStep> - { - [Status.Draft] = new WorkflowStep( - new Dictionary<Status, WorkflowTransition> - { - [Status.Published] = WorkflowTransition.When("Expression", "Role1", "Role2") - }.ToReadonlyDictionary(), - "#00ff00", - NoUpdate.When("Expression", "Role1", "Role2"), - true) - }.ToReadonlyDictionary(), - ReadonlyList.Create(DomainId.NewGuid()), "MyName"); - - var serialized = workflow.SerializeAndDeserialize(); - - Assert.Equal(workflow, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_default() - { - var workflow = Workflow.Default; - - var serialized = workflow.SerializeAndDeserialize(); - - Assert.Equal(workflow, serialized); - } - - [Fact] - public void Should_deserialize_old_noUpdate_condition() - { - var jsonStep = new { noUpdate = true }; - - var serialized = jsonStep.SerializeAndDeserialize<WorkflowStep, object>(); - - Assert.Equal(new WorkflowStep(null, null, NoUpdate.Always), serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_no_update_condition() - { - var step = new WorkflowStep(NoUpdate: NoUpdate.When("Expression", "Role1", "Role2")); - - var serialized = step.SerializeAndDeserialize(); - - Assert.Equal(step, serialized); - } - - [Fact] - public void Should_verify_roles_mapping_in_workflow_transition() - { - var source = new WorkflowTransitionSurrogate { Expression = "expression_1", Role = "role_1" }; - - var serialized = source.SerializeAndDeserialize(); - - var actual = serialized.ToSource(); - - Assert.Equal(source.Role, actual?.Roles?.Single()); - } + var step = new WorkflowStep(NoUpdate: NoUpdate.When("Expression", "Role1", "Role2")); + + var serialized = step.SerializeAndDeserialize(); + + Assert.Equal(step, serialized); + } + + [Fact] + public void Should_verify_roles_mapping_in_workflow_transition() + { + var source = new WorkflowTransitionSurrogate { Expression = "expression_1", Role = "role_1" }; + + var serialized = source.SerializeAndDeserialize(); + + var actual = serialized.ToSource(); + + Assert.Equal(source.Role, actual?.Roles?.Single()); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs index 7c62fba50b..1c6eb4d74d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs @@ -9,137 +9,136 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents; + +public class WorkflowTests { - public class WorkflowTests - { - private readonly Workflow workflow = new Workflow( - Status.Draft, - new Dictionary<Status, WorkflowStep> - { - [Status.Draft] = - new WorkflowStep( - new Dictionary<Status, WorkflowTransition> - { - [Status.Archived] = WorkflowTransition.When("ToArchivedExpr", "ToArchivedRole"), - [Status.Published] = WorkflowTransition.When("ToPublishedExpr", "ToPublishedRole") - }.ToReadonlyDictionary(), - StatusColors.Draft), - [Status.Archived] = - new WorkflowStep(), - [Status.Published] = - new WorkflowStep() - }.ToReadonlyDictionary()); - - [Fact] - public void Should_provide_default_workflow_if_none_found() + private readonly Workflow workflow = new Workflow( + Status.Draft, + new Dictionary<Status, WorkflowStep> { - var actual = Workflows.Empty.GetFirst(); + [Status.Draft] = + new WorkflowStep( + new Dictionary<Status, WorkflowTransition> + { + [Status.Archived] = WorkflowTransition.When("ToArchivedExpr", "ToArchivedRole"), + [Status.Published] = WorkflowTransition.When("ToPublishedExpr", "ToPublishedRole") + }.ToReadonlyDictionary(), + StatusColors.Draft), + [Status.Archived] = + new WorkflowStep(), + [Status.Published] = + new WorkflowStep() + }.ToReadonlyDictionary()); + + [Fact] + public void Should_provide_default_workflow_if_none_found() + { + var actual = Workflows.Empty.GetFirst(); - Assert.Same(Workflow.Default, actual); - } + Assert.Same(Workflow.Default, actual); + } - [Fact] - public void Should_provide_initial_state() - { - var (status, step) = workflow.GetInitialStep(); + [Fact] + public void Should_provide_initial_state() + { + var (status, step) = workflow.GetInitialStep(); - Assert.Equal(Status.Draft, status); - Assert.Equal(StatusColors.Draft, step.Color); - Assert.Same(workflow.Steps[Status.Draft], step); - } + Assert.Equal(Status.Draft, status); + Assert.Equal(StatusColors.Draft, step.Color); + Assert.Same(workflow.Steps[Status.Draft], step); + } - [Fact] - public void Should_provide_step() - { - var found = workflow.TryGetStep(Status.Draft, out var step); + [Fact] + public void Should_provide_step() + { + var found = workflow.TryGetStep(Status.Draft, out var step); - Assert.True(found); - Assert.Same(workflow.Steps[Status.Draft], step); - } + Assert.True(found); + Assert.Same(workflow.Steps[Status.Draft], step); + } - [Fact] - public void Should_not_provide_unknown_step() - { - var found = workflow.TryGetStep(default, out var step); + [Fact] + public void Should_not_provide_unknown_step() + { + var found = workflow.TryGetStep(default, out var step); - Assert.False(found); - Assert.Null(step); - } + Assert.False(found); + Assert.Null(step); + } - [Fact] - public void Should_provide_transition() - { - var found = workflow.TryGetTransition(Status.Draft, Status.Archived, out var transition); + [Fact] + public void Should_provide_transition() + { + var found = workflow.TryGetTransition(Status.Draft, Status.Archived, out var transition); - Assert.True(found); - Assert.Equal("ToArchivedExpr", transition?.Expression); - Assert.Equal("ToArchivedRole", transition?.Roles?.Single()); - } + Assert.True(found); + Assert.Equal("ToArchivedExpr", transition?.Expression); + Assert.Equal("ToArchivedRole", transition?.Roles?.Single()); + } - [Fact] - public void Should_provide_transition_to_initial_if_step_not_found() - { - var found = workflow.TryGetTransition(new Status("Other"), Status.Draft, out var transition); + [Fact] + public void Should_provide_transition_to_initial_if_step_not_found() + { + var found = workflow.TryGetTransition(new Status("Other"), Status.Draft, out var transition); - Assert.True(found); - Assert.Null(transition?.Expression); - Assert.Null(transition?.Roles); - } + Assert.True(found); + Assert.Null(transition?.Expression); + Assert.Null(transition?.Roles); + } - [Fact] - public void Should_not_provide_transition_from_unknown_step() - { - var found = workflow.TryGetTransition(new Status("Other"), Status.Archived, out var transition); + [Fact] + public void Should_not_provide_transition_from_unknown_step() + { + var found = workflow.TryGetTransition(new Status("Other"), Status.Archived, out var transition); - Assert.False(found); - Assert.Null(transition); - } + Assert.False(found); + Assert.Null(transition); + } - [Fact] - public void Should_not_provide_transition_to_unknown_step() - { - var found = workflow.TryGetTransition(Status.Draft, default, out var transition); + [Fact] + public void Should_not_provide_transition_to_unknown_step() + { + var found = workflow.TryGetTransition(Status.Draft, default, out var transition); - Assert.False(found); - Assert.Null(transition); - } + Assert.False(found); + Assert.Null(transition); + } - [Fact] - public void Should_provide_transitions() - { - var transitions = workflow.GetTransitions(Status.Draft).ToArray(); + [Fact] + public void Should_provide_transitions() + { + var transitions = workflow.GetTransitions(Status.Draft).ToArray(); - Assert.Equal(2, transitions.Length); + Assert.Equal(2, transitions.Length); - var (status1, step1, transition1) = transitions[0]; + var (status1, step1, transition1) = transitions[0]; - Assert.Equal(Status.Archived, status1); - Assert.Equal("ToArchivedExpr", transition1?.Expression); - Assert.Equal("ToArchivedRole", transition1?.Roles?.Single()); - Assert.Same(workflow.Steps[status1], step1); + Assert.Equal(Status.Archived, status1); + Assert.Equal("ToArchivedExpr", transition1?.Expression); + Assert.Equal("ToArchivedRole", transition1?.Roles?.Single()); + Assert.Same(workflow.Steps[status1], step1); - var (status2, step2, transition2) = transitions[1]; + var (status2, step2, transition2) = transitions[1]; - Assert.Equal(Status.Published, status2); - Assert.Equal("ToPublishedExpr", transition2?.Expression); - Assert.Equal("ToPublishedRole", transition2?.Roles?.Single()); - Assert.Same(workflow.Steps[status2], step2); - } + Assert.Equal(Status.Published, status2); + Assert.Equal("ToPublishedExpr", transition2?.Expression); + Assert.Equal("ToPublishedRole", transition2?.Roles?.Single()); + Assert.Same(workflow.Steps[status2], step2); + } - [Fact] - public void Should_provide_transitions_to_initial_step_if_status_not_found() - { - var transitions = workflow.GetTransitions(new Status("Other")).ToArray(); + [Fact] + public void Should_provide_transitions_to_initial_step_if_status_not_found() + { + var transitions = workflow.GetTransitions(new Status("Other")).ToArray(); - Assert.Single(transitions); + Assert.Single(transitions); - var (status1, step1, transition1) = transitions[0]; + var (status1, step1, transition1) = transitions[0]; - Assert.Equal(Status.Draft, status1); - Assert.Null(transition1?.Expression); - Assert.Null(transition1?.Roles); - Assert.Same(workflow.Steps[status1], step1); - } + Assert.Equal(Status.Draft, status1); + Assert.Null(transition1?.Expression); + Assert.Null(transition1?.Roles); + Assert.Same(workflow.Steps[status1], step1); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs index 832480f24b..4986a30d59 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs @@ -10,18 +10,17 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents; + +public class WorkflowsJsonTests { - public class WorkflowsJsonTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var workflows = Workflows.Empty.Add(DomainId.NewGuid(), "my-workflow"); + var workflows = Workflows.Empty.Add(DomainId.NewGuid(), "my-workflow"); - var serialized = workflows.SerializeAndDeserialize(); + var serialized = workflows.SerializeAndDeserialize(); - Assert.Equal(workflows, serialized); - } + Assert.Equal(workflows, serialized); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs index b353af7da3..7c4c4b81c1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs @@ -11,83 +11,82 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Core.Model.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents; + +public class WorkflowsTests { - public class WorkflowsTests - { - private readonly Workflows workflows_0 = Workflows.Empty; + private readonly Workflows workflows_0 = Workflows.Empty; - [Fact] - public void Should_provide_default_workflow_if_none_found() - { - var workflow = workflows_0.GetFirst(); + [Fact] + public void Should_provide_default_workflow_if_none_found() + { + var workflow = workflows_0.GetFirst(); - Assert.Same(Workflow.Default, workflow); - } + Assert.Same(Workflow.Default, workflow); + } - [Fact] - public void Should_set_workflow_with_empty_guid() - { - var workflows_1 = workflows_0.Set(Workflow.Default); + [Fact] + public void Should_set_workflow_with_empty_guid() + { + var workflows_1 = workflows_0.Set(Workflow.Default); - Assert.Single(workflows_1); - Assert.Same(Workflow.Default, workflows_1[DomainId.Empty]); - } + Assert.Single(workflows_1); + Assert.Same(Workflow.Default, workflows_1[DomainId.Empty]); + } - [Fact] - public void Should_add_new_workflow_with_default_states() - { - var id = DomainId.NewGuid(); + [Fact] + public void Should_add_new_workflow_with_default_states() + { + var id = DomainId.NewGuid(); - var workflows_1 = workflows_0.Add(id, "1"); + var workflows_1 = workflows_0.Add(id, "1"); - Assert.Equal(workflows_1[id].Steps.Keys, new[] { Status.Archived, Status.Draft, Status.Published }); - } + Assert.Equal(workflows_1[id].Steps.Keys, new[] { Status.Archived, Status.Draft, Status.Published }); + } - [Fact] - public void Should_update_workflow() - { - var id = DomainId.NewGuid(); + [Fact] + public void Should_update_workflow() + { + var id = DomainId.NewGuid(); - var workflows_1 = workflows_0.Add(id, "1"); - var workflows_2 = workflows_1.Update(id, Workflow.Empty); + var workflows_1 = workflows_0.Add(id, "1"); + var workflows_2 = workflows_1.Update(id, Workflow.Empty); - Assert.Empty(workflows_2.GetFirst().Steps.Keys); - } + Assert.Empty(workflows_2.GetFirst().Steps.Keys); + } - [Fact] - public void Should_update_workflow_with_default_guid() - { - var workflows_1 = workflows_0.Update(default, Workflow.Empty); + [Fact] + public void Should_update_workflow_with_default_guid() + { + var workflows_1 = workflows_0.Update(default, Workflow.Empty); - Assert.NotEmpty(workflows_1); - } + Assert.NotEmpty(workflows_1); + } - [Fact] - public void Should_do_nothing_if_workflow_to_update_not_found() - { - var workflows_1 = workflows_0.Update(DomainId.NewGuid(), Workflow.Empty); + [Fact] + public void Should_do_nothing_if_workflow_to_update_not_found() + { + var workflows_1 = workflows_0.Update(DomainId.NewGuid(), Workflow.Empty); - Assert.Same(workflows_0, workflows_1); - } + Assert.Same(workflows_0, workflows_1); + } - [Fact] - public void Should_remove_workflow() - { - var id = DomainId.NewGuid(); + [Fact] + public void Should_remove_workflow() + { + var id = DomainId.NewGuid(); - var workflows_1 = workflows_0.Add(id, "1"); - var workflows_2 = workflows_1.Remove(id); + var workflows_1 = workflows_0.Add(id, "1"); + var workflows_2 = workflows_1.Remove(id); - Assert.Empty(workflows_2); - } + Assert.Empty(workflows_2); + } - [Fact] - public void Should_do_nothing_if_workflow_to_remove_not_found() - { - var workflows_1 = workflows_0.Remove(DomainId.NewGuid()); + [Fact] + public void Should_do_nothing_if_workflow_to_remove_not_found() + { + var workflows_1 = workflows_0.Remove(DomainId.NewGuid()); - Assert.Empty(workflows_1); - } + Assert.Empty(workflows_1); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs index 62e9d0a25e..4ab105f406 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs @@ -7,47 +7,46 @@ using Xunit; -namespace Squidex.Domain.Apps.Core.Model +namespace Squidex.Domain.Apps.Core.Model; + +public class InvariantPartitionTests { - public class InvariantPartitionTests + [Fact] + public void Should_provide_name() { - [Fact] - public void Should_provide_name() - { - var sut = InvariantPartitioning.Instance; + var sut = InvariantPartitioning.Instance; - Assert.Equal("invariant value", sut.ToString()); - } + Assert.Equal("invariant value", sut.ToString()); + } - [Fact] - public void Should_provide_master() - { - var sut = InvariantPartitioning.Instance; + [Fact] + public void Should_provide_master() + { + var sut = InvariantPartitioning.Instance; - Assert.Equal("iv", sut.Master); - Assert.Equal("Invariant", sut.GetName("iv")); + Assert.Equal("iv", sut.Master); + Assert.Equal("Invariant", sut.GetName("iv")); - Assert.Equal(new[] { "iv" }, sut.AllKeys.ToArray()); - Assert.Equal(new[] { "iv" }, sut.GetPriorities("iv").ToArray()); + Assert.Equal(new[] { "iv" }, sut.AllKeys.ToArray()); + Assert.Equal(new[] { "iv" }, sut.GetPriorities("iv").ToArray()); - Assert.True(sut.IsMaster("iv")); - Assert.True(sut.Contains("iv")); + Assert.True(sut.IsMaster("iv")); + Assert.True(sut.Contains("iv")); - Assert.False(sut.IsOptional("iv")); - } + Assert.False(sut.IsOptional("iv")); + } - [Fact] - public void Should_handle_unsupported_key() - { - var sut = InvariantPartitioning.Instance; + [Fact] + public void Should_handle_unsupported_key() + { + var sut = InvariantPartitioning.Instance; - Assert.Null(sut.GetName("invalid")); + Assert.Null(sut.GetName("invalid")); - Assert.Empty(sut.GetPriorities("invalid")); + Assert.Empty(sut.GetPriorities("invalid")); - Assert.False(sut.IsMaster("invalid")); - Assert.False(sut.IsOptional("invalid")); - Assert.False(sut.Contains("invalid")); - } + Assert.False(sut.IsMaster("invalid")); + Assert.False(sut.IsOptional("invalid")); + Assert.False(sut.Contains("invalid")); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs index 6bbc8c4f7c..07c43783e3 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs @@ -7,79 +7,78 @@ using Xunit; -namespace Squidex.Domain.Apps.Core.Model +namespace Squidex.Domain.Apps.Core.Model; + +public class PartitioningTests { - public class PartitioningTests + [Fact] + public void Should_consider_null_as_valid_partitioning() + { + string? partitioning = null; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_consider_invariant_as_valid_partitioning() + { + var partitioning = "invariant"; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_consider_language_as_valid_partitioning() + { + var partitioning = "language"; + + Assert.True(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_not_consider_empty_as_valid_partitioning() + { + var partitioning = string.Empty; + + Assert.False(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_not_consider_other_string_as_valid_partitioning() + { + var partitioning = "invalid"; + + Assert.False(partitioning.IsValidPartitioning()); + } + + [Fact] + public void Should_provide_invariant_instance() { - [Fact] - public void Should_consider_null_as_valid_partitioning() - { - string? partitioning = null; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_consider_invariant_as_valid_partitioning() - { - var partitioning = "invariant"; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_consider_language_as_valid_partitioning() - { - var partitioning = "language"; - - Assert.True(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_not_consider_empty_as_valid_partitioning() - { - var partitioning = string.Empty; - - Assert.False(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_not_consider_other_string_as_valid_partitioning() - { - var partitioning = "invalid"; - - Assert.False(partitioning.IsValidPartitioning()); - } - - [Fact] - public void Should_provide_invariant_instance() - { - Assert.Equal("invariant", Partitioning.Invariant.Key); - Assert.Equal("invariant", Partitioning.Invariant.ToString()); - } - - [Fact] - public void Should_provide_language_instance() - { - Assert.Equal("language", Partitioning.Language.Key); - Assert.Equal("language", Partitioning.Language.ToString()); - } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var partitioning1_a = new Partitioning("partitioning1"); - var partitioning1_b = new Partitioning("partitioning1"); - - var partitioning2 = new Partitioning("partitioning2"); - - Assert.Equal(partitioning1_a, partitioning1_b); - Assert.Equal(partitioning1_a.GetHashCode(), partitioning1_b.GetHashCode()); - Assert.True(partitioning1_a.Equals((object)partitioning1_b)); - - Assert.NotEqual(partitioning1_a, partitioning2); - Assert.NotEqual(partitioning1_a.GetHashCode(), partitioning2.GetHashCode()); - Assert.False(partitioning1_a.Equals((object)partitioning2)); - } + Assert.Equal("invariant", Partitioning.Invariant.Key); + Assert.Equal("invariant", Partitioning.Invariant.ToString()); + } + + [Fact] + public void Should_provide_language_instance() + { + Assert.Equal("language", Partitioning.Language.Key); + Assert.Equal("language", Partitioning.Language.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var partitioning1_a = new Partitioning("partitioning1"); + var partitioning1_b = new Partitioning("partitioning1"); + + var partitioning2 = new Partitioning("partitioning2"); + + Assert.Equal(partitioning1_a, partitioning1_b); + Assert.Equal(partitioning1_a.GetHashCode(), partitioning1_b.GetHashCode()); + Assert.True(partitioning1_a.Equals((object)partitioning1_b)); + + Assert.NotEqual(partitioning1_a, partitioning2); + Assert.NotEqual(partitioning1_a.GetHashCode(), partitioning2.GetHashCode()); + Assert.False(partitioning1_a.Equals((object)partitioning2)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs index 34b5bfaaae..c1a77cc3c9 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs @@ -15,165 +15,164 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Core.Model.Rules +namespace Squidex.Domain.Apps.Core.Model.Rules; + +public class RuleTests { - public class RuleTests - { - public static readonly List<object[]> Triggers = - typeof(Rule).Assembly.GetTypes() - .Where(x => x.BaseType == typeof(RuleTrigger)) - .Select(Activator.CreateInstance) - .Select(x => new[] { x }) - .ToList()!; + public static readonly List<object[]> Triggers = + typeof(Rule).Assembly.GetTypes() + .Where(x => x.BaseType == typeof(RuleTrigger)) + .Select(Activator.CreateInstance) + .Select(x => new[] { x }) + .ToList()!; - private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction1()); + private readonly Rule rule_0 = new Rule(new ContentChangedTriggerV2(), new TestAction1()); - public sealed record OtherTrigger : RuleTrigger + public sealed record OtherTrigger : RuleTrigger + { + public override T Accept<T>(IRuleTriggerVisitor<T> visitor) { - public override T Accept<T>(IRuleTriggerVisitor<T> visitor) - { - throw new NotSupportedException(); - } + throw new NotSupportedException(); } + } - public sealed record MigratedTrigger : RuleTrigger, IMigrated<RuleTrigger> + public sealed record MigratedTrigger : RuleTrigger, IMigrated<RuleTrigger> + { + public override T Accept<T>(IRuleTriggerVisitor<T> visitor) { - public override T Accept<T>(IRuleTriggerVisitor<T> visitor) - { - throw new NotSupportedException(); - } - - public RuleTrigger Migrate() - { - return new OtherTrigger(); - } + throw new NotSupportedException(); } - [TypeName(nameof(TestAction1))] - public sealed record TestAction1 : RuleAction + public RuleTrigger Migrate() { - public string Property { get; set; } + return new OtherTrigger(); } + } - [TypeName(nameof(TestAction2))] - public sealed record TestAction2 : RuleAction - { - public string Property { get; set; } - } + [TypeName(nameof(TestAction1))] + public sealed record TestAction1 : RuleAction + { + public string Property { get; set; } + } - [Fact] - public void Should_create_with_trigger_and_action() - { - var ruleTrigger = new ContentChangedTriggerV2(); - var ruleAction = new TestAction1(); + [TypeName(nameof(TestAction2))] + public sealed record TestAction2 : RuleAction + { + public string Property { get; set; } + } - var newRule = new Rule(ruleTrigger, ruleAction); + [Fact] + public void Should_create_with_trigger_and_action() + { + var ruleTrigger = new ContentChangedTriggerV2(); + var ruleAction = new TestAction1(); - Assert.Equal(ruleTrigger, newRule.Trigger); - Assert.Equal(ruleAction, newRule.Action); - Assert.True(newRule.IsEnabled); - } + var newRule = new Rule(ruleTrigger, ruleAction); - [Fact] - public void Should_set_enabled_to_true_if_enabling() - { - var rule_1 = rule_0.Disable(); - var rule_2 = rule_1.Enable(); - var rule_3 = rule_2.Enable(); - - Assert.NotSame(rule_1, rule_2); - Assert.False(rule_1.IsEnabled); - Assert.True(rule_2.IsEnabled); - Assert.True(rule_3.IsEnabled); - Assert.Same(rule_2, rule_3); - } + Assert.Equal(ruleTrigger, newRule.Trigger); + Assert.Equal(ruleAction, newRule.Action); + Assert.True(newRule.IsEnabled); + } - [Fact] - public void Should_set_enabled_to_false_if_disabling() - { - var rule_1 = rule_0.Disable(); - var rule_2 = rule_1.Disable(); + [Fact] + public void Should_set_enabled_to_true_if_enabling() + { + var rule_1 = rule_0.Disable(); + var rule_2 = rule_1.Enable(); + var rule_3 = rule_2.Enable(); + + Assert.NotSame(rule_1, rule_2); + Assert.False(rule_1.IsEnabled); + Assert.True(rule_2.IsEnabled); + Assert.True(rule_3.IsEnabled); + Assert.Same(rule_2, rule_3); + } - Assert.NotSame(rule_0, rule_1); - Assert.False(rule_1.IsEnabled); - Assert.False(rule_2.IsEnabled); - Assert.Same(rule_1, rule_2); - } + [Fact] + public void Should_set_enabled_to_false_if_disabling() + { + var rule_1 = rule_0.Disable(); + var rule_2 = rule_1.Disable(); - [Fact] - public void Should_replace_name_if_renaming() - { - var rule_1 = rule_0.Rename("MyName"); - var rule_2 = rule_1.Rename("MyName"); + Assert.NotSame(rule_0, rule_1); + Assert.False(rule_1.IsEnabled); + Assert.False(rule_2.IsEnabled); + Assert.Same(rule_1, rule_2); + } - Assert.NotSame(rule_0, rule_1); - Assert.Equal("MyName", rule_1.Name); - Assert.Equal("MyName", rule_2.Name); - Assert.Same(rule_1, rule_2); - } + [Fact] + public void Should_replace_name_if_renaming() + { + var rule_1 = rule_0.Rename("MyName"); + var rule_2 = rule_1.Rename("MyName"); - [Fact] - public void Should_replace_trigger_if_updating() - { - var newTrigger1 = new ContentChangedTriggerV2 { HandleAll = true }; - var newTrigger2 = new ContentChangedTriggerV2 { HandleAll = true }; + Assert.NotSame(rule_0, rule_1); + Assert.Equal("MyName", rule_1.Name); + Assert.Equal("MyName", rule_2.Name); + Assert.Same(rule_1, rule_2); + } - var rule_1 = rule_0.Update(newTrigger1); - var rule_2 = rule_1.Update(newTrigger2); + [Fact] + public void Should_replace_trigger_if_updating() + { + var newTrigger1 = new ContentChangedTriggerV2 { HandleAll = true }; + var newTrigger2 = new ContentChangedTriggerV2 { HandleAll = true }; - Assert.NotSame(rule_0.Action, newTrigger1); - Assert.NotSame(rule_0, rule_1); - Assert.Same(newTrigger1, rule_1.Trigger); - Assert.Same(newTrigger1, rule_2.Trigger); - Assert.Same(rule_1, rule_2); - } + var rule_1 = rule_0.Update(newTrigger1); + var rule_2 = rule_1.Update(newTrigger2); - [Fact] - public void Should_throw_exception_if_new_trigger_has_other_type() - { - Assert.Throws<ArgumentException>(() => rule_0.Update(new OtherTrigger())); - } + Assert.NotSame(rule_0.Action, newTrigger1); + Assert.NotSame(rule_0, rule_1); + Assert.Same(newTrigger1, rule_1.Trigger); + Assert.Same(newTrigger1, rule_2.Trigger); + Assert.Same(rule_1, rule_2); + } - [Fact] - public void Should_replace_action_if_updating() - { - var newAction1 = new TestAction1 { Property = "NewValue" }; - var newAction2 = new TestAction1 { Property = "NewValue" }; + [Fact] + public void Should_throw_exception_if_new_trigger_has_other_type() + { + Assert.Throws<ArgumentException>(() => rule_0.Update(new OtherTrigger())); + } - var rule_1 = rule_0.Update(newAction1); - var rule_2 = rule_1.Update(newAction2); + [Fact] + public void Should_replace_action_if_updating() + { + var newAction1 = new TestAction1 { Property = "NewValue" }; + var newAction2 = new TestAction1 { Property = "NewValue" }; - Assert.NotSame(rule_0.Action, newAction1); - Assert.NotSame(rule_0, rule_1); - Assert.Same(newAction1, rule_1.Action); - Assert.Same(newAction1, rule_2.Action); - Assert.Same(rule_1, rule_2); - } + var rule_1 = rule_0.Update(newAction1); + var rule_2 = rule_1.Update(newAction2); - [Fact] - public void Should_throw_exception_if_new_action_has_other_type() - { - Assert.Throws<ArgumentException>(() => rule_0.Update(new TestAction2())); - } + Assert.NotSame(rule_0.Action, newAction1); + Assert.NotSame(rule_0, rule_1); + Assert.Same(newAction1, rule_1.Action); + Assert.Same(newAction1, rule_2.Action); + Assert.Same(rule_1, rule_2); + } - [Fact] - public void Should_serialize_and_deserialize() - { - var rule_1 = rule_0.Disable().Rename("MyName"); + [Fact] + public void Should_throw_exception_if_new_action_has_other_type() + { + Assert.Throws<ArgumentException>(() => rule_0.Update(new TestAction2())); + } - var serialized = rule_1.SerializeAndDeserialize(); + [Fact] + public void Should_serialize_and_deserialize() + { + var rule_1 = rule_0.Disable().Rename("MyName"); - serialized.Should().BeEquivalentTo(rule_1); - } + var serialized = rule_1.SerializeAndDeserialize(); - [Fact] - public void Should_serialize_and_deserialize_and_migrate_trigger() - { - var rule_X = new Rule(new MigratedTrigger(), new TestAction1()); + serialized.Should().BeEquivalentTo(rule_1); + } - var serialized = rule_X.SerializeAndDeserialize(); + [Fact] + public void Should_serialize_and_deserialize_and_migrate_trigger() + { + var rule_X = new Rule(new MigratedTrigger(), new TestAction1()); - Assert.IsType<OtherTrigger>(serialized.Trigger); - } + var serialized = rule_X.SerializeAndDeserialize(); + + Assert.IsType<OtherTrigger>(serialized.Trigger); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs index 0af5c5962b..a764e92837 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs @@ -10,232 +10,231 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Core.Model.Schemas +namespace Squidex.Domain.Apps.Core.Model.Schemas; + +public class ArrayfieldTests { - public class ArrayfieldTests - { - private readonly ArrayField parent_0 = Fields.Array(100, "root", Partitioning.Invariant); + private readonly ArrayField parent_0 = Fields.Array(100, "root", Partitioning.Invariant); - [Fact] - public void Should_add_field() - { - var field = CreateField(1); + [Fact] + public void Should_add_field() + { + var field = CreateField(1); - var parent_1 = parent_0.AddField(field); + var parent_1 = parent_0.AddField(field); - Assert.Empty(parent_0.Fields); - Assert.Equal(field, parent_1.FieldsById[1]); - } + Assert.Empty(parent_0.Fields); + Assert.Equal(field, parent_1.FieldsById[1]); + } - [Fact] - public void Should_throw_exception_if_adding_field_with_name_that_already_exists() - { - var parent_1 = parent_0.AddField(CreateField(1)); + [Fact] + public void Should_throw_exception_if_adding_field_with_name_that_already_exists() + { + var parent_1 = parent_0.AddField(CreateField(1)); - Assert.Throws<ArgumentException>(() => parent_1.AddNumber(2, "myField1")); - } + Assert.Throws<ArgumentException>(() => parent_1.AddNumber(2, "myField1")); + } - [Fact] - public void Should_throw_exception_if_adding_field_with_id_that_already_exists() - { - var parent_1 = parent_0.AddField(CreateField(1)); + [Fact] + public void Should_throw_exception_if_adding_field_with_id_that_already_exists() + { + var parent_1 = parent_0.AddField(CreateField(1)); - Assert.Throws<ArgumentException>(() => parent_1.AddNumber(1, "myField2")); - } + Assert.Throws<ArgumentException>(() => parent_1.AddNumber(1, "myField2")); + } - [Fact] - public void Should_hide_field() - { - var parent_1 = parent_0.AddField(CreateField(1)); + [Fact] + public void Should_hide_field() + { + var parent_1 = parent_0.AddField(CreateField(1)); - var parent_2 = parent_1.UpdateField(1, f => f.Hide()); - var parent_3 = parent_2.UpdateField(1, f => f.Hide()); + var parent_2 = parent_1.UpdateField(1, f => f.Hide()); + var parent_3 = parent_2.UpdateField(1, f => f.Hide()); - Assert.False(parent_1.FieldsById[1].IsHidden); - Assert.True(parent_2.FieldsById[1].IsHidden); - Assert.True(parent_3.FieldsById[1].IsHidden); - Assert.Same(parent_2, parent_3); - } + Assert.False(parent_1.FieldsById[1].IsHidden); + Assert.True(parent_2.FieldsById[1].IsHidden); + Assert.True(parent_3.FieldsById[1].IsHidden); + Assert.Same(parent_2, parent_3); + } - [Fact] - public void Should_return_same_parent_if_field_to_hide_does_not_exist() - { - var parent_1 = parent_0.UpdateField(1, f => f.Hide()); + [Fact] + public void Should_return_same_parent_if_field_to_hide_does_not_exist() + { + var parent_1 = parent_0.UpdateField(1, f => f.Hide()); - Assert.Same(parent_0, parent_1); - } + Assert.Same(parent_0, parent_1); + } - [Fact] - public void Should_show_field() - { - var parent_1 = parent_0.AddField(CreateField(1)); + [Fact] + public void Should_show_field() + { + var parent_1 = parent_0.AddField(CreateField(1)); - var parent_2 = parent_1.UpdateField(1, f => f.Hide()); - var parent_3 = parent_2.UpdateField(1, f => f.Show()); - var parent_4 = parent_3.UpdateField(1, f => f.Show()); + var parent_2 = parent_1.UpdateField(1, f => f.Hide()); + var parent_3 = parent_2.UpdateField(1, f => f.Show()); + var parent_4 = parent_3.UpdateField(1, f => f.Show()); - Assert.True(parent_2.FieldsById[1].IsHidden); - Assert.False(parent_3.FieldsById[1].IsHidden); - Assert.False(parent_4.FieldsById[1].IsHidden); - Assert.Same(parent_3, parent_4); - } + Assert.True(parent_2.FieldsById[1].IsHidden); + Assert.False(parent_3.FieldsById[1].IsHidden); + Assert.False(parent_4.FieldsById[1].IsHidden); + Assert.Same(parent_3, parent_4); + } - [Fact] - public void Should_return_same_parent_if_field_to_show_does_not_exist() - { - var parent_1 = parent_0.UpdateField(1, f => f.Show()); + [Fact] + public void Should_return_same_parent_if_field_to_show_does_not_exist() + { + var parent_1 = parent_0.UpdateField(1, f => f.Show()); - Assert.Same(parent_0, parent_1); - } + Assert.Same(parent_0, parent_1); + } - [Fact] - public void Should_disable_field() - { - var parent_1 = parent_0.AddField(CreateField(1)); + [Fact] + public void Should_disable_field() + { + var parent_1 = parent_0.AddField(CreateField(1)); - var parent_2 = parent_1.UpdateField(1, f => f.Disable()); - var parent_3 = parent_2.UpdateField(1, f => f.Disable()); + var parent_2 = parent_1.UpdateField(1, f => f.Disable()); + var parent_3 = parent_2.UpdateField(1, f => f.Disable()); - Assert.False(parent_1.FieldsById[1].IsDisabled); - Assert.True(parent_2.FieldsById[1].IsDisabled); - Assert.True(parent_3.FieldsById[1].IsDisabled); - Assert.Same(parent_2, parent_3); - } + Assert.False(parent_1.FieldsById[1].IsDisabled); + Assert.True(parent_2.FieldsById[1].IsDisabled); + Assert.True(parent_3.FieldsById[1].IsDisabled); + Assert.Same(parent_2, parent_3); + } - [Fact] - public void Should_return_same_parent_if_field_to_disable_does_not_exist() - { - var parent_1 = parent_0.UpdateField(1, f => f.Disable()); + [Fact] + public void Should_return_same_parent_if_field_to_disable_does_not_exist() + { + var parent_1 = parent_0.UpdateField(1, f => f.Disable()); - Assert.Same(parent_0, parent_1); - } + Assert.Same(parent_0, parent_1); + } - [Fact] - public void Should_enable_field() - { - var parent_1 = parent_0.AddField(CreateField(1)); + [Fact] + public void Should_enable_field() + { + var parent_1 = parent_0.AddField(CreateField(1)); - var parent_2 = parent_1.UpdateField(1, f => f.Disable()); - var parent_3 = parent_2.UpdateField(1, f => f.Enable()); - var parent_4 = parent_3.UpdateField(1, f => f.Enable()); + var parent_2 = parent_1.UpdateField(1, f => f.Disable()); + var parent_3 = parent_2.UpdateField(1, f => f.Enable()); + var parent_4 = parent_3.UpdateField(1, f => f.Enable()); - Assert.True(parent_2.FieldsById[1].IsDisabled); - Assert.False(parent_3.FieldsById[1].IsDisabled); - Assert.False(parent_4.FieldsById[1].IsDisabled); - Assert.Same(parent_3, parent_4); - } + Assert.True(parent_2.FieldsById[1].IsDisabled); + Assert.False(parent_3.FieldsById[1].IsDisabled); + Assert.False(parent_4.FieldsById[1].IsDisabled); + Assert.Same(parent_3, parent_4); + } - [Fact] - public void Should_return_same_parent_if_field_to_enable_does_not_exist() - { - var parent_1 = parent_0.UpdateField(1, f => f.Enable()); + [Fact] + public void Should_return_same_parent_if_field_to_enable_does_not_exist() + { + var parent_1 = parent_0.UpdateField(1, f => f.Enable()); - Assert.Same(parent_0, parent_1); - } + Assert.Same(parent_0, parent_1); + } - [Fact] - public void Should_update_field() + [Fact] + public void Should_update_field() + { + var properties1 = new NumberFieldProperties { - var properties1 = new NumberFieldProperties - { - MinValue = 10 - }; - var properties2 = new NumberFieldProperties - { - MinValue = 10 - }; - - var parent_1 = parent_0.AddField(CreateField(1)); - var parent_2 = parent_1.UpdateField(1, f => f.Update(properties1)); - var parent_3 = parent_2.UpdateField(1, f => f.Update(properties2)); - - Assert.NotSame(properties1, parent_1.FieldsById[1].RawProperties); - Assert.Same(properties1, parent_2.FieldsById[1].RawProperties); - Assert.Same(properties1, parent_3.FieldsById[1].RawProperties); - Assert.Same(parent_2, parent_3); - } - - [Fact] - public void Should_throw_exception_if_updating_with_invalid_properties_type() + MinValue = 10 + }; + var properties2 = new NumberFieldProperties { - var parent_1 = parent_0.AddField(CreateField(1)); + MinValue = 10 + }; - Assert.Throws<ArgumentException>(() => parent_1.UpdateField(1, f => f.Update(new StringFieldProperties()))); - } + var parent_1 = parent_0.AddField(CreateField(1)); + var parent_2 = parent_1.UpdateField(1, f => f.Update(properties1)); + var parent_3 = parent_2.UpdateField(1, f => f.Update(properties2)); - [Fact] - public void Should_return_same_parent_if_field_to_update_does_not_exist() - { - var parent_1 = parent_0.UpdateField(1, f => f.Update(new StringFieldProperties())); + Assert.NotSame(properties1, parent_1.FieldsById[1].RawProperties); + Assert.Same(properties1, parent_2.FieldsById[1].RawProperties); + Assert.Same(properties1, parent_3.FieldsById[1].RawProperties); + Assert.Same(parent_2, parent_3); + } - Assert.Same(parent_0, parent_1); - } + [Fact] + public void Should_throw_exception_if_updating_with_invalid_properties_type() + { + var parent_1 = parent_0.AddField(CreateField(1)); - [Fact] - public void Should_delete_field() - { - var parent_1 = parent_0.AddField(CreateField(1)); - var parent_2 = parent_1.DeleteField(1); - var parent_3 = parent_2.DeleteField(1); + Assert.Throws<ArgumentException>(() => parent_1.UpdateField(1, f => f.Update(new StringFieldProperties()))); + } - Assert.Empty(parent_2.FieldsById); - Assert.Empty(parent_3.FieldsById); - Assert.Same(parent_2, parent_3); - } + [Fact] + public void Should_return_same_parent_if_field_to_update_does_not_exist() + { + var parent_1 = parent_0.UpdateField(1, f => f.Update(new StringFieldProperties())); - [Fact] - public void Should_return_same_parent_if_field_to_delete_does_not_exist() - { - var parent_1 = parent_0.DeleteField(1); + Assert.Same(parent_0, parent_1); + } - Assert.Same(parent_0, parent_1); - } + [Fact] + public void Should_delete_field() + { + var parent_1 = parent_0.AddField(CreateField(1)); + var parent_2 = parent_1.DeleteField(1); + var parent_3 = parent_2.DeleteField(1); - [Fact] - public void Should_reorder_fields() - { - var field1 = CreateField(1); - var field2 = CreateField(2); - var field3 = CreateField(3); - - var parent_1 = parent_0.AddField(field1); - var parent_2 = parent_1.AddField(field2); - var parent_3 = parent_2.AddField(field3); - var parent_4 = parent_3.ReorderFields(new List<long> { 3, 2, 1 }); - var parent_5 = parent_4.ReorderFields(new List<long> { 3, 2, 1 }); - - Assert.Equal(new List<NestedField> { field3, field2, field1 }, parent_4.Fields.ToList()); - Assert.Equal(new List<NestedField> { field3, field2, field1 }, parent_5.Fields.ToList()); - Assert.Same(parent_4, parent_5); - } - - [Fact] - public void Should_throw_exception_if_not_all_fields_are_covered_for_reordering() - { - var field1 = CreateField(1); - var field2 = CreateField(2); + Assert.Empty(parent_2.FieldsById); + Assert.Empty(parent_3.FieldsById); + Assert.Same(parent_2, parent_3); + } - var parent_1 = parent_0.AddField(field1); - var parent_2 = parent_1.AddField(field2); + [Fact] + public void Should_return_same_parent_if_field_to_delete_does_not_exist() + { + var parent_1 = parent_0.DeleteField(1); - Assert.Throws<ArgumentException>(() => parent_2.ReorderFields(new List<long> { 1 })); - } + Assert.Same(parent_0, parent_1); + } - [Fact] - public void Should_throw_exception_if_field_to_reorder_does_not_exist() - { - var field1 = CreateField(1); - var field2 = CreateField(2); + [Fact] + public void Should_reorder_fields() + { + var field1 = CreateField(1); + var field2 = CreateField(2); + var field3 = CreateField(3); + + var parent_1 = parent_0.AddField(field1); + var parent_2 = parent_1.AddField(field2); + var parent_3 = parent_2.AddField(field3); + var parent_4 = parent_3.ReorderFields(new List<long> { 3, 2, 1 }); + var parent_5 = parent_4.ReorderFields(new List<long> { 3, 2, 1 }); + + Assert.Equal(new List<NestedField> { field3, field2, field1 }, parent_4.Fields.ToList()); + Assert.Equal(new List<NestedField> { field3, field2, field1 }, parent_5.Fields.ToList()); + Assert.Same(parent_4, parent_5); + } - var parent_1 = parent_0.AddField(field1); - var parent_2 = parent_1.AddField(field2); + [Fact] + public void Should_throw_exception_if_not_all_fields_are_covered_for_reordering() + { + var field1 = CreateField(1); + var field2 = CreateField(2); - Assert.Throws<ArgumentException>(() => parent_2.ReorderFields(new List<long> { 1, 4 })); - } + var parent_1 = parent_0.AddField(field1); + var parent_2 = parent_1.AddField(field2); - private static NestedField<NumberFieldProperties> CreateField(int id) - { - return Fields.Number(id, $"myField{id}"); - } + Assert.Throws<ArgumentException>(() => parent_2.ReorderFields(new List<long> { 1 })); + } + + [Fact] + public void Should_throw_exception_if_field_to_reorder_does_not_exist() + { + var field1 = CreateField(1); + var field2 = CreateField(2); + + var parent_1 = parent_0.AddField(field1); + var parent_2 = parent_1.AddField(field2); + + Assert.Throws<ArgumentException>(() => parent_2.ReorderFields(new List<long> { 1, 4 })); + } + + private static NestedField<NumberFieldProperties> CreateField(int id) + { + return Fields.Number(id, $"myField{id}"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldCompareTests.cs index aae51dce3b..1b7e04d7aa 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldCompareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldCompareTests.cs @@ -9,52 +9,51 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Domain.Apps.Core.Model.Schemas +namespace Squidex.Domain.Apps.Core.Model.Schemas; + +public class FieldCompareTests { - public class FieldCompareTests + [Fact] + public void Should_compare_two_string_fields_as_equal() { - [Fact] - public void Should_compare_two_string_fields_as_equal() + var lhs = new StringFieldProperties { - var lhs = new StringFieldProperties + DefaultValues = new LocalizedValue<string?>(new Dictionary<string, string?> { - DefaultValues = new LocalizedValue<string?>(new Dictionary<string, string?> - { - ["iv"] = "ABC" - }) - }; + ["iv"] = "ABC" + }) + }; - var rhs = new StringFieldProperties + var rhs = new StringFieldProperties + { + DefaultValues = new LocalizedValue<string?>(new Dictionary<string, string?> { - DefaultValues = new LocalizedValue<string?>(new Dictionary<string, string?> - { - ["iv"] = "ABC" - }) - }; + ["iv"] = "ABC" + }) + }; - Assert.Equal(lhs, rhs); - } + Assert.Equal(lhs, rhs); + } - [Fact] - public void Should_compare_two_tags_fields_as_equal() + [Fact] + public void Should_compare_two_tags_fields_as_equal() + { + var lhs = new TagsFieldProperties { - var lhs = new TagsFieldProperties + DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> { - DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> - { - ["iv"] = ReadonlyList.Create("A", "B", "C") - }) - }; + ["iv"] = ReadonlyList.Create("A", "B", "C") + }) + }; - var rhs = new TagsFieldProperties + var rhs = new TagsFieldProperties + { + DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> { - DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> - { - ["iv"] = ReadonlyList.Create("A", "B", "C") - }) - }; - - Assert.Equal(lhs, rhs); - } + ["iv"] = ReadonlyList.Create("A", "B", "C") + }) + }; + + Assert.Equal(lhs, rhs); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs index 7612e4a236..07a247ae9f 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs @@ -10,95 +10,94 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Core.Model.Schemas +namespace Squidex.Domain.Apps.Core.Model.Schemas; + +public class SchemaFieldTests { - public class SchemaFieldTests + public static readonly List<object[]> FieldProperties = + typeof(Schema).Assembly.GetTypes() + .Where(x => x.BaseType == typeof(FieldProperties)) + .Select(Activator.CreateInstance) + .Select(x => new[] { x }) + .ToList()!; + + private readonly RootField<NumberFieldProperties> field_0 = Fields.Number(1, "myField", Partitioning.Invariant); + + [Fact] + public void Should_instantiate_field() + { + Assert.Equal("myField", field_0.Name); + } + + [Fact] + public void Should_throw_exception_if_creating_field_with_invalid_name() + { + Assert.Throws<ArgumentException>(() => Fields.Number(1, string.Empty, Partitioning.Invariant)); + } + + [Fact] + public void Should_hide_field() + { + var field_1 = field_0.Hide(); + var field_2 = field_1.Hide(); + + Assert.False(field_0.IsHidden); + Assert.True(field_2.IsHidden); + } + + [Fact] + public void Should_show_field() + { + var field_1 = field_0.Hide(); + var field_2 = field_1.Show(); + var field_3 = field_2.Show(); + + Assert.True(field_1.IsHidden); + Assert.False(field_3.IsHidden); + } + + [Fact] + public void Should_disable_field() + { + var field_1 = field_0.Disable(); + var field_2 = field_1.Disable(); + + Assert.False(field_0.IsDisabled); + Assert.True(field_2.IsDisabled); + } + + [Fact] + public void Should_enable_field() + { + var field_1 = field_0.Disable(); + var field_2 = field_1.Enable(); + var field_3 = field_2.Enable(); + + Assert.True(field_1.IsDisabled); + Assert.False(field_3.IsDisabled); + } + + [Fact] + public void Should_lock_field() + { + var field_1 = field_0.Lock(); + + Assert.False(field_0.IsLocked); + Assert.True(field_1.IsLocked); + } + + [Fact] + public void Should_update_field() + { + var field_1 = field_0.Update(new NumberFieldProperties { Hints = "my-hints" }); + + Assert.Null(field_0.RawProperties.Hints); + Assert.Equal("my-hints", field_1.RawProperties.Hints); + } + + [Fact] + public void Should_throw_exception_if_updating_with_invalid_properties_type() { - public static readonly List<object[]> FieldProperties = - typeof(Schema).Assembly.GetTypes() - .Where(x => x.BaseType == typeof(FieldProperties)) - .Select(Activator.CreateInstance) - .Select(x => new[] { x }) - .ToList()!; - - private readonly RootField<NumberFieldProperties> field_0 = Fields.Number(1, "myField", Partitioning.Invariant); - - [Fact] - public void Should_instantiate_field() - { - Assert.Equal("myField", field_0.Name); - } - - [Fact] - public void Should_throw_exception_if_creating_field_with_invalid_name() - { - Assert.Throws<ArgumentException>(() => Fields.Number(1, string.Empty, Partitioning.Invariant)); - } - - [Fact] - public void Should_hide_field() - { - var field_1 = field_0.Hide(); - var field_2 = field_1.Hide(); - - Assert.False(field_0.IsHidden); - Assert.True(field_2.IsHidden); - } - - [Fact] - public void Should_show_field() - { - var field_1 = field_0.Hide(); - var field_2 = field_1.Show(); - var field_3 = field_2.Show(); - - Assert.True(field_1.IsHidden); - Assert.False(field_3.IsHidden); - } - - [Fact] - public void Should_disable_field() - { - var field_1 = field_0.Disable(); - var field_2 = field_1.Disable(); - - Assert.False(field_0.IsDisabled); - Assert.True(field_2.IsDisabled); - } - - [Fact] - public void Should_enable_field() - { - var field_1 = field_0.Disable(); - var field_2 = field_1.Enable(); - var field_3 = field_2.Enable(); - - Assert.True(field_1.IsDisabled); - Assert.False(field_3.IsDisabled); - } - - [Fact] - public void Should_lock_field() - { - var field_1 = field_0.Lock(); - - Assert.False(field_0.IsLocked); - Assert.True(field_1.IsLocked); - } - - [Fact] - public void Should_update_field() - { - var field_1 = field_0.Update(new NumberFieldProperties { Hints = "my-hints" }); - - Assert.Null(field_0.RawProperties.Hints); - Assert.Equal("my-hints", field_1.RawProperties.Hints); - } - - [Fact] - public void Should_throw_exception_if_updating_with_invalid_properties_type() - { - Assert.Throws<ArgumentException>(() => field_0.Update(new StringFieldProperties())); - } + Assert.Throws<ArgumentException>(() => field_0.Update(new StringFieldProperties())); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs index 7752652176..5251c190d8 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -13,488 +13,487 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Core.Model.Schemas +namespace Squidex.Domain.Apps.Core.Model.Schemas; + +public class SchemaTests { - public class SchemaTests - { - private readonly Schema schema_0 = new Schema("my-schema"); + private readonly Schema schema_0 = new Schema("my-schema"); - public static IEnumerable<object[]> FieldProperyTypes() - { - return typeof(Schema).Assembly.GetTypes().Where(x => x.BaseType == typeof(FieldProperties)).Select(x => new object[] { x }); - } + public static IEnumerable<object[]> FieldProperyTypes() + { + return typeof(Schema).Assembly.GetTypes().Where(x => x.BaseType == typeof(FieldProperties)).Select(x => new object[] { x }); + } - [Theory] - [MemberData(nameof(FieldProperyTypes))] - public void Should_make_deep_equal_test(Type type) - { - var lhs = (FieldProperties)Activator.CreateInstance(type)!; - var rhs = (FieldProperties)Activator.CreateInstance(type)!; + [Theory] + [MemberData(nameof(FieldProperyTypes))] + public void Should_make_deep_equal_test(Type type) + { + var lhs = (FieldProperties)Activator.CreateInstance(type)!; + var rhs = (FieldProperties)Activator.CreateInstance(type)!; - Assert.True(lhs.Equals(rhs)); - } + Assert.True(lhs.Equals(rhs)); + } - [Fact] - public void Should_instantiate_schema() - { - Assert.Equal("my-schema", schema_0.Name); - } + [Fact] + public void Should_instantiate_schema() + { + Assert.Equal("my-schema", schema_0.Name); + } - [Fact] - public void Should_throw_exception_if_creating_schema_with_invalid_name() - { - Assert.Throws<ArgumentException>(() => new Schema(string.Empty)); - } + [Fact] + public void Should_throw_exception_if_creating_schema_with_invalid_name() + { + Assert.Throws<ArgumentException>(() => new Schema(string.Empty)); + } - [Fact] - public void Should_update_schema() - { - var properties1 = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; - var properties2 = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; + [Fact] + public void Should_update_schema() + { + var properties1 = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; + var properties2 = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; - var schema_1 = schema_0.Update(properties1); - var schema_2 = schema_1.Update(properties2); + var schema_1 = schema_0.Update(properties1); + var schema_2 = schema_1.Update(properties2); - Assert.NotSame(properties1, schema_0.Properties); - Assert.Same(properties1, schema_1.Properties); - Assert.Same(properties1, schema_2.Properties); - Assert.Same(schema_1, schema_2); - } + Assert.NotSame(properties1, schema_0.Properties); + Assert.Same(properties1, schema_1.Properties); + Assert.Same(properties1, schema_2.Properties); + Assert.Same(schema_1, schema_2); + } - [Fact] - public void Should_add_field() - { - var field = CreateField(1); + [Fact] + public void Should_add_field() + { + var field = CreateField(1); - var schema_1 = schema_0.AddField(field); + var schema_1 = schema_0.AddField(field); - Assert.Empty(schema_0.Fields); - Assert.Equal(field, schema_1.FieldsById[1]); - } + Assert.Empty(schema_0.Fields); + Assert.Equal(field, schema_1.FieldsById[1]); + } - [Fact] - public void Should_throw_exception_if_adding_field_with_name_that_already_exists() - { - var schema_1 = schema_0.AddField(CreateField(1)); + [Fact] + public void Should_throw_exception_if_adding_field_with_name_that_already_exists() + { + var schema_1 = schema_0.AddField(CreateField(1)); - Assert.Throws<ArgumentException>(() => schema_1.AddNumber(2, "myField1", Partitioning.Invariant)); - } + Assert.Throws<ArgumentException>(() => schema_1.AddNumber(2, "myField1", Partitioning.Invariant)); + } - [Fact] - public void Should_throw_exception_if_adding_field_with_id_that_already_exists() - { - var schema_1 = schema_0.AddField(CreateField(1)); + [Fact] + public void Should_throw_exception_if_adding_field_with_id_that_already_exists() + { + var schema_1 = schema_0.AddField(CreateField(1)); - Assert.Throws<ArgumentException>(() => schema_1.AddNumber(1, "myField2", Partitioning.Invariant)); - } + Assert.Throws<ArgumentException>(() => schema_1.AddNumber(1, "myField2", Partitioning.Invariant)); + } - [Fact] - public void Should_hide_field() - { - var schema_1 = schema_0.AddField(CreateField(1)); + [Fact] + public void Should_hide_field() + { + var schema_1 = schema_0.AddField(CreateField(1)); - var schema_2 = schema_1.UpdateField(1, f => f.Hide()); - var schema_3 = schema_2.UpdateField(1, f => f.Hide()); + var schema_2 = schema_1.UpdateField(1, f => f.Hide()); + var schema_3 = schema_2.UpdateField(1, f => f.Hide()); - Assert.False(schema_1.FieldsById[1].IsHidden); - Assert.True(schema_2.FieldsById[1].IsHidden); - Assert.True(schema_3.FieldsById[1].IsHidden); - Assert.Same(schema_2, schema_3); - } + Assert.False(schema_1.FieldsById[1].IsHidden); + Assert.True(schema_2.FieldsById[1].IsHidden); + Assert.True(schema_3.FieldsById[1].IsHidden); + Assert.Same(schema_2, schema_3); + } - [Fact] - public void Should_return_same_schema_if_field_to_hide_does_not_exist() - { - var schema_1 = schema_0.UpdateField(1, f => f.Hide()); + [Fact] + public void Should_return_same_schema_if_field_to_hide_does_not_exist() + { + var schema_1 = schema_0.UpdateField(1, f => f.Hide()); - Assert.Same(schema_0, schema_1); - } + Assert.Same(schema_0, schema_1); + } - [Fact] - public void Should_show_field() - { - var schema_1 = schema_0.AddField(CreateField(1)); + [Fact] + public void Should_show_field() + { + var schema_1 = schema_0.AddField(CreateField(1)); - var schema_2 = schema_1.UpdateField(1, f => f.Hide()); - var schema_3 = schema_2.UpdateField(1, f => f.Show()); - var schema_4 = schema_3.UpdateField(1, f => f.Show()); + var schema_2 = schema_1.UpdateField(1, f => f.Hide()); + var schema_3 = schema_2.UpdateField(1, f => f.Show()); + var schema_4 = schema_3.UpdateField(1, f => f.Show()); - Assert.True(schema_2.FieldsById[1].IsHidden); - Assert.False(schema_3.FieldsById[1].IsHidden); - Assert.False(schema_4.FieldsById[1].IsHidden); - Assert.Same(schema_3, schema_4); - } + Assert.True(schema_2.FieldsById[1].IsHidden); + Assert.False(schema_3.FieldsById[1].IsHidden); + Assert.False(schema_4.FieldsById[1].IsHidden); + Assert.Same(schema_3, schema_4); + } - [Fact] - public void Should_return_same_schema_if_field_to_show_does_not_exist() - { - var schema_1 = schema_0.UpdateField(1, f => f.Show()); + [Fact] + public void Should_return_same_schema_if_field_to_show_does_not_exist() + { + var schema_1 = schema_0.UpdateField(1, f => f.Show()); - Assert.Same(schema_0, schema_1); - } + Assert.Same(schema_0, schema_1); + } - [Fact] - public void Should_disable_field() - { - var schema_1 = schema_0.AddField(CreateField(1)); + [Fact] + public void Should_disable_field() + { + var schema_1 = schema_0.AddField(CreateField(1)); - var schema_2 = schema_1.UpdateField(1, f => f.Disable()); - var schema_3 = schema_2.UpdateField(1, f => f.Disable()); + var schema_2 = schema_1.UpdateField(1, f => f.Disable()); + var schema_3 = schema_2.UpdateField(1, f => f.Disable()); - Assert.False(schema_1.FieldsById[1].IsDisabled); - Assert.True(schema_2.FieldsById[1].IsDisabled); - Assert.True(schema_3.FieldsById[1].IsDisabled); - Assert.Same(schema_2, schema_3); - } + Assert.False(schema_1.FieldsById[1].IsDisabled); + Assert.True(schema_2.FieldsById[1].IsDisabled); + Assert.True(schema_3.FieldsById[1].IsDisabled); + Assert.Same(schema_2, schema_3); + } - [Fact] - public void Should_return_same_schema_if_field_to_disable_does_not_exist() - { - var schema_1 = schema_0.UpdateField(1, f => f.Disable()); + [Fact] + public void Should_return_same_schema_if_field_to_disable_does_not_exist() + { + var schema_1 = schema_0.UpdateField(1, f => f.Disable()); - Assert.Same(schema_0, schema_1); - } + Assert.Same(schema_0, schema_1); + } - [Fact] - public void Should_enable_field() - { - var schema_1 = schema_0.AddField(CreateField(1)); + [Fact] + public void Should_enable_field() + { + var schema_1 = schema_0.AddField(CreateField(1)); - var schema_2 = schema_1.UpdateField(1, f => f.Disable()); - var schema_3 = schema_2.UpdateField(1, f => f.Enable()); - var schema_4 = schema_3.UpdateField(1, f => f.Enable()); + var schema_2 = schema_1.UpdateField(1, f => f.Disable()); + var schema_3 = schema_2.UpdateField(1, f => f.Enable()); + var schema_4 = schema_3.UpdateField(1, f => f.Enable()); - Assert.True(schema_2.FieldsById[1].IsDisabled); - Assert.False(schema_3.FieldsById[1].IsDisabled); - Assert.False(schema_4.FieldsById[1].IsDisabled); - Assert.Same(schema_3, schema_4); - } + Assert.True(schema_2.FieldsById[1].IsDisabled); + Assert.False(schema_3.FieldsById[1].IsDisabled); + Assert.False(schema_4.FieldsById[1].IsDisabled); + Assert.Same(schema_3, schema_4); + } - [Fact] - public void Should_return_same_schema_if_field_to_enable_does_not_exist() - { - var schema_1 = schema_0.UpdateField(1, f => f.Enable()); + [Fact] + public void Should_return_same_schema_if_field_to_enable_does_not_exist() + { + var schema_1 = schema_0.UpdateField(1, f => f.Enable()); - Assert.Same(schema_0, schema_1); - } + Assert.Same(schema_0, schema_1); + } - [Fact] - public void Should_lock_field() - { - var schema_1 = schema_0.AddField(CreateField(1)); + [Fact] + public void Should_lock_field() + { + var schema_1 = schema_0.AddField(CreateField(1)); - var schema_2 = schema_1.UpdateField(1, f => f.Lock()); - var schema_3 = schema_2.UpdateField(1, f => f.Lock()); + var schema_2 = schema_1.UpdateField(1, f => f.Lock()); + var schema_3 = schema_2.UpdateField(1, f => f.Lock()); - Assert.False(schema_1.FieldsById[1].IsLocked); - Assert.True(schema_2.FieldsById[1].IsLocked); - Assert.True(schema_3.FieldsById[1].IsLocked); - Assert.Same(schema_2, schema_3); - } + Assert.False(schema_1.FieldsById[1].IsLocked); + Assert.True(schema_2.FieldsById[1].IsLocked); + Assert.True(schema_3.FieldsById[1].IsLocked); + Assert.Same(schema_2, schema_3); + } - [Fact] - public void Should_return_same_schema_if_field_to_lock_does_not_exist() - { - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + [Fact] + public void Should_return_same_schema_if_field_to_lock_does_not_exist() + { + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - Assert.Same(schema_0, schema_1); - } + Assert.Same(schema_0, schema_1); + } - [Fact] - public void Should_update_field() + [Fact] + public void Should_update_field() + { + var properties1 = new NumberFieldProperties { - var properties1 = new NumberFieldProperties - { - MinValue = 10 - }; - var properties2 = new NumberFieldProperties - { - MinValue = 10 - }; - - var schema_1 = schema_0.AddField(CreateField(1)); - var schema_2 = schema_1.UpdateField(1, f => f.Update(properties1)); - var schema_3 = schema_2.UpdateField(1, f => f.Update(properties2)); - - Assert.NotSame(properties1, schema_1.FieldsById[1].RawProperties); - Assert.Same(properties1, schema_2.FieldsById[1].RawProperties); - Assert.Same(properties1, schema_3.FieldsById[1].RawProperties); - Assert.Same(schema_2, schema_3); - } - - [Fact] - public void Should_throw_exception_if_updating_with_invalid_properties_type() + MinValue = 10 + }; + var properties2 = new NumberFieldProperties { - var schema_1 = schema_0.AddField(CreateField(1)); + MinValue = 10 + }; - Assert.Throws<ArgumentException>(() => schema_1.UpdateField(1, f => f.Update(new StringFieldProperties()))); - } + var schema_1 = schema_0.AddField(CreateField(1)); + var schema_2 = schema_1.UpdateField(1, f => f.Update(properties1)); + var schema_3 = schema_2.UpdateField(1, f => f.Update(properties2)); - [Fact] - public void Should_return_same_schema_if_field_to_update_does_not_exist() - { - var schema_1 = schema_0.UpdateField(1, f => f.Update(new StringFieldProperties())); + Assert.NotSame(properties1, schema_1.FieldsById[1].RawProperties); + Assert.Same(properties1, schema_2.FieldsById[1].RawProperties); + Assert.Same(properties1, schema_3.FieldsById[1].RawProperties); + Assert.Same(schema_2, schema_3); + } - Assert.Same(schema_0, schema_1); - } + [Fact] + public void Should_throw_exception_if_updating_with_invalid_properties_type() + { + var schema_1 = schema_0.AddField(CreateField(1)); - [Fact] - public void Should_delete_field() - { - var schema_1 = schema_0.AddField(CreateField(1)); - var schema_2 = schema_1.DeleteField(1); - var schema_3 = schema_2.DeleteField(1); + Assert.Throws<ArgumentException>(() => schema_1.UpdateField(1, f => f.Update(new StringFieldProperties()))); + } - Assert.Empty(schema_2.FieldsById); - Assert.Empty(schema_3.FieldsById); - Assert.Same(schema_2, schema_3); - } + [Fact] + public void Should_return_same_schema_if_field_to_update_does_not_exist() + { + var schema_1 = schema_0.UpdateField(1, f => f.Update(new StringFieldProperties())); - [Fact] - public void Should_also_remove_deleted_fields_from_lists() - { - var field = CreateField(1); + Assert.Same(schema_0, schema_1); + } - var schema_1 = schema_0 - .AddField(field) - .SetFieldsInLists(field.Name) - .SetFieldsInReferences(field.Name); - var schema_2 = schema_1.DeleteField(1); + [Fact] + public void Should_delete_field() + { + var schema_1 = schema_0.AddField(CreateField(1)); + var schema_2 = schema_1.DeleteField(1); + var schema_3 = schema_2.DeleteField(1); - Assert.Empty(schema_2.FieldsById); - Assert.Empty(schema_2.FieldsInLists); - Assert.Empty(schema_2.FieldsInReferences); - } + Assert.Empty(schema_2.FieldsById); + Assert.Empty(schema_3.FieldsById); + Assert.Same(schema_2, schema_3); + } - [Fact] - public void Should_return_same_schema_if_field_to_delete_does_not_exist() - { - var schema_1 = schema_0.DeleteField(1); + [Fact] + public void Should_also_remove_deleted_fields_from_lists() + { + var field = CreateField(1); - Assert.Same(schema_0, schema_1); - } + var schema_1 = schema_0 + .AddField(field) + .SetFieldsInLists(field.Name) + .SetFieldsInReferences(field.Name); + var schema_2 = schema_1.DeleteField(1); - [Fact] - public void Should_publish_schema() - { - var schema_1 = schema_0.Publish(); - var schema_2 = schema_1.Publish(); + Assert.Empty(schema_2.FieldsById); + Assert.Empty(schema_2.FieldsInLists); + Assert.Empty(schema_2.FieldsInReferences); + } - Assert.False(schema_0.IsPublished); - Assert.True(schema_1.IsPublished); - Assert.True(schema_2.IsPublished); - Assert.Same(schema_1, schema_2); - } + [Fact] + public void Should_return_same_schema_if_field_to_delete_does_not_exist() + { + var schema_1 = schema_0.DeleteField(1); - [Fact] - public void Should_unpublish_schema() - { - var schema_1 = schema_0.Publish(); - var schema_2 = schema_1.Unpublish(); - var schema_3 = schema_2.Unpublish(); - - Assert.True(schema_1.IsPublished); - Assert.False(schema_2.IsPublished); - Assert.False(schema_3.IsPublished); - Assert.Same(schema_2, schema_3); - } - - [Fact] - public void Should_reorder_fields() - { - var field1 = CreateField(1); - var field2 = CreateField(2); - var field3 = CreateField(3); - - var schema_1 = schema_0.AddField(field1); - var schema_2 = schema_1.AddField(field2); - var schema_3 = schema_2.AddField(field3); - var schema_4 = schema_3.ReorderFields(new List<long> { 3, 2, 1 }); - var schema_5 = schema_4.ReorderFields(new List<long> { 3, 2, 1 }); - - Assert.Equal(new List<RootField> { field3, field2, field1 }, schema_4.Fields.ToList()); - Assert.Equal(new List<RootField> { field3, field2, field1 }, schema_5.Fields.ToList()); - Assert.Same(schema_4, schema_5); - } - - [Fact] - public void Should_throw_exception_if_not_all_fields_are_covered_for_reordering() - { - var field1 = CreateField(1); - var field2 = CreateField(2); + Assert.Same(schema_0, schema_1); + } - var schema_1 = schema_0.AddField(field1); - var schema_2 = schema_1.AddField(field2); + [Fact] + public void Should_publish_schema() + { + var schema_1 = schema_0.Publish(); + var schema_2 = schema_1.Publish(); - Assert.Throws<ArgumentException>(() => schema_2.ReorderFields(new List<long> { 1 })); - } + Assert.False(schema_0.IsPublished); + Assert.True(schema_1.IsPublished); + Assert.True(schema_2.IsPublished); + Assert.Same(schema_1, schema_2); + } - [Fact] - public void Should_throw_exception_if_field_to_reorder_does_not_exist() - { - var field1 = CreateField(1); - var field2 = CreateField(2); + [Fact] + public void Should_unpublish_schema() + { + var schema_1 = schema_0.Publish(); + var schema_2 = schema_1.Unpublish(); + var schema_3 = schema_2.Unpublish(); + + Assert.True(schema_1.IsPublished); + Assert.False(schema_2.IsPublished); + Assert.False(schema_3.IsPublished); + Assert.Same(schema_2, schema_3); + } - var schema_1 = schema_0.AddField(field1); - var schema_2 = schema_1.AddField(field2); + [Fact] + public void Should_reorder_fields() + { + var field1 = CreateField(1); + var field2 = CreateField(2); + var field3 = CreateField(3); + + var schema_1 = schema_0.AddField(field1); + var schema_2 = schema_1.AddField(field2); + var schema_3 = schema_2.AddField(field3); + var schema_4 = schema_3.ReorderFields(new List<long> { 3, 2, 1 }); + var schema_5 = schema_4.ReorderFields(new List<long> { 3, 2, 1 }); + + Assert.Equal(new List<RootField> { field3, field2, field1 }, schema_4.Fields.ToList()); + Assert.Equal(new List<RootField> { field3, field2, field1 }, schema_5.Fields.ToList()); + Assert.Same(schema_4, schema_5); + } - Assert.Throws<ArgumentException>(() => schema_2.ReorderFields(new List<long> { 1, 4 })); - } + [Fact] + public void Should_throw_exception_if_not_all_fields_are_covered_for_reordering() + { + var field1 = CreateField(1); + var field2 = CreateField(2); - [Fact] - public void Should_change_category() - { - var schema_1 = schema_0.ChangeCategory("Category"); - var schema_2 = schema_1.ChangeCategory("Category"); + var schema_1 = schema_0.AddField(field1); + var schema_2 = schema_1.AddField(field2); - Assert.Equal("Category", schema_1.Category); - Assert.Equal("Category", schema_2.Category); - Assert.Same(schema_1, schema_2); - } + Assert.Throws<ArgumentException>(() => schema_2.ReorderFields(new List<long> { 1 })); + } - [Fact] - public void Should_set_list_fields() - { - var schema_1 = schema_0.SetFieldsInLists("2"); - var schema_2 = schema_1.SetFieldsInLists("2"); + [Fact] + public void Should_throw_exception_if_field_to_reorder_does_not_exist() + { + var field1 = CreateField(1); + var field2 = CreateField(2); - Assert.Equal(new[] { "2" }, schema_1.FieldsInLists); - Assert.Equal(new[] { "2" }, schema_2.FieldsInLists); - Assert.Same(schema_1, schema_2); - } + var schema_1 = schema_0.AddField(field1); + var schema_2 = schema_1.AddField(field2); - [Fact] - public void Should_also_set_list_fields_if_reordered() - { - var schema_1 = schema_0.SetFieldsInLists("2", "1"); - var schema_2 = schema_1.SetFieldsInLists("1", "2"); + Assert.Throws<ArgumentException>(() => schema_2.ReorderFields(new List<long> { 1, 4 })); + } - Assert.Equal(new[] { "2", "1" }, schema_1.FieldsInLists); - Assert.Equal(new[] { "1", "2" }, schema_2.FieldsInLists); - Assert.NotSame(schema_1, schema_2); - } + [Fact] + public void Should_change_category() + { + var schema_1 = schema_0.ChangeCategory("Category"); + var schema_2 = schema_1.ChangeCategory("Category"); - [Fact] - public void Should_set_reference_fields() - { - var schema_1 = schema_0.SetFieldsInReferences("2"); - var schema_2 = schema_1.SetFieldsInReferences("2"); + Assert.Equal("Category", schema_1.Category); + Assert.Equal("Category", schema_2.Category); + Assert.Same(schema_1, schema_2); + } - Assert.Equal(new[] { "2" }, schema_1.FieldsInReferences); - Assert.Equal(new[] { "2" }, schema_2.FieldsInReferences); - Assert.Same(schema_1, schema_2); - } + [Fact] + public void Should_set_list_fields() + { + var schema_1 = schema_0.SetFieldsInLists("2"); + var schema_2 = schema_1.SetFieldsInLists("2"); - [Fact] - public void Should_also_set_reference_fields_if_reordered() - { - var schema_1 = schema_0.SetFieldsInReferences("2", "1"); - var schema_2 = schema_1.SetFieldsInReferences("1", "2"); + Assert.Equal(new[] { "2" }, schema_1.FieldsInLists); + Assert.Equal(new[] { "2" }, schema_2.FieldsInLists); + Assert.Same(schema_1, schema_2); + } - Assert.Equal(new[] { "2", "1" }, schema_1.FieldsInReferences); - Assert.Equal(new[] { "1", "2" }, schema_2.FieldsInReferences); - Assert.NotSame(schema_1, schema_2); - } + [Fact] + public void Should_also_set_list_fields_if_reordered() + { + var schema_1 = schema_0.SetFieldsInLists("2", "1"); + var schema_2 = schema_1.SetFieldsInLists("1", "2"); - [Fact] - public void Should_set_field_rules() - { - var schema_1 = schema_0.SetFieldRules(FieldRule.Hide("2")); - var schema_2 = schema_1.SetFieldRules(FieldRule.Hide("2")); + Assert.Equal(new[] { "2", "1" }, schema_1.FieldsInLists); + Assert.Equal(new[] { "1", "2" }, schema_2.FieldsInLists); + Assert.NotSame(schema_1, schema_2); + } + + [Fact] + public void Should_set_reference_fields() + { + var schema_1 = schema_0.SetFieldsInReferences("2"); + var schema_2 = schema_1.SetFieldsInReferences("2"); + + Assert.Equal(new[] { "2" }, schema_1.FieldsInReferences); + Assert.Equal(new[] { "2" }, schema_2.FieldsInReferences); + Assert.Same(schema_1, schema_2); + } + + [Fact] + public void Should_also_set_reference_fields_if_reordered() + { + var schema_1 = schema_0.SetFieldsInReferences("2", "1"); + var schema_2 = schema_1.SetFieldsInReferences("1", "2"); + + Assert.Equal(new[] { "2", "1" }, schema_1.FieldsInReferences); + Assert.Equal(new[] { "1", "2" }, schema_2.FieldsInReferences); + Assert.NotSame(schema_1, schema_2); + } + + [Fact] + public void Should_set_field_rules() + { + var schema_1 = schema_0.SetFieldRules(FieldRule.Hide("2")); + var schema_2 = schema_1.SetFieldRules(FieldRule.Hide("2")); - Assert.NotEmpty(schema_1.FieldRules); - Assert.NotEmpty(schema_2.FieldRules); - Assert.Same(schema_1, schema_2); - } + Assert.NotEmpty(schema_1.FieldRules); + Assert.NotEmpty(schema_2.FieldRules); + Assert.Same(schema_1, schema_2); + } - [Fact] - public void Should_set_scripts() + [Fact] + public void Should_set_scripts() + { + var scripts1 = new SchemaScripts { - var scripts1 = new SchemaScripts - { - Query = "<query-script>" - }; - var scripts2 = new SchemaScripts - { - Query = "<query-script>" - }; - - var schema_1 = schema_0.SetScripts(scripts1); - var schema_2 = schema_1.SetScripts(scripts2); - - Assert.Equal("<query-script>", schema_1.Scripts.Query); - - Assert.Equal(scripts1, schema_1.Scripts); - Assert.Equal(scripts1, schema_2.Scripts); - Assert.Same(schema_1, schema_2); - } - - [Fact] - public void Should_set_preview_urls() + Query = "<query-script>" + }; + var scripts2 = new SchemaScripts { - var urls1 = new Dictionary<string, string> - { - ["web"] = "Url" - }.ToReadonlyDictionary(); - var urls2 = new Dictionary<string, string> - { - ["web"] = "Url" - }.ToReadonlyDictionary(); - - var schema_1 = schema_0.SetPreviewUrls(urls1); - var schema_2 = schema_1.SetPreviewUrls(urls2); - - Assert.Equal("Url", schema_1.PreviewUrls["web"]); - Assert.Equal(urls1, schema_1.PreviewUrls); - Assert.Equal(urls1, schema_2.PreviewUrls); - Assert.Same(schema_1, schema_2); - } - - [Fact] - public void Should_serialize_and_deserialize_schema() + Query = "<query-script>" + }; + + var schema_1 = schema_0.SetScripts(scripts1); + var schema_2 = schema_1.SetScripts(scripts2); + + Assert.Equal("<query-script>", schema_1.Scripts.Query); + + Assert.Equal(scripts1, schema_1.Scripts); + Assert.Equal(scripts1, schema_2.Scripts); + Assert.Same(schema_1, schema_2); + } + + [Fact] + public void Should_set_preview_urls() + { + var urls1 = new Dictionary<string, string> { - var schemaSource = - TestSchema.MixedSchema(SchemaType.Singleton).Schema - .ChangeCategory("Category") - .SetFieldRules(FieldRule.Hide("2")) - .SetFieldsInLists("field2") - .SetFieldsInReferences("field1") - .SetScripts(new SchemaScripts - { - Create = "<create-script>" - }) - .SetPreviewUrls(new Dictionary<string, string> - { - ["web"] = "Url" - }.ToReadonlyDictionary()); - - var schemaTarget = schemaSource.SerializeAndDeserialize(); - - schemaTarget.Should().BeEquivalentTo(schemaSource); - } - - [Fact] - public void Should_deserialize_obsolete_isSingleton_property() + ["web"] = "Url" + }.ToReadonlyDictionary(); + var urls2 = new Dictionary<string, string> { - var schemaSource = new - { - name = "my-schema", - isPublished = true, - isSingleton = true - }; + ["web"] = "Url" + }.ToReadonlyDictionary(); - var expected = - new Schema("my-schema", type: SchemaType.Singleton) - .Publish(); + var schema_1 = schema_0.SetPreviewUrls(urls1); + var schema_2 = schema_1.SetPreviewUrls(urls2); - var schemaTarget = schemaSource.SerializeAndDeserialize<Schema, object>(); + Assert.Equal("Url", schema_1.PreviewUrls["web"]); + Assert.Equal(urls1, schema_1.PreviewUrls); + Assert.Equal(urls1, schema_2.PreviewUrls); + Assert.Same(schema_1, schema_2); + } - schemaTarget.Should().BeEquivalentTo(expected); - } + [Fact] + public void Should_serialize_and_deserialize_schema() + { + var schemaSource = + TestSchema.MixedSchema(SchemaType.Singleton).Schema + .ChangeCategory("Category") + .SetFieldRules(FieldRule.Hide("2")) + .SetFieldsInLists("field2") + .SetFieldsInReferences("field1") + .SetScripts(new SchemaScripts + { + Create = "<create-script>" + }) + .SetPreviewUrls(new Dictionary<string, string> + { + ["web"] = "Url" + }.ToReadonlyDictionary()); + + var schemaTarget = schemaSource.SerializeAndDeserialize(); + + schemaTarget.Should().BeEquivalentTo(schemaSource); + } - private static RootField<NumberFieldProperties> CreateField(int id) + [Fact] + public void Should_deserialize_obsolete_isSingleton_property() + { + var schemaSource = new { - return Fields.Number(id, $"myField{id}", Partitioning.Invariant); - } + name = "my-schema", + isPublished = true, + isSingleton = true + }; + + var expected = + new Schema("my-schema", type: SchemaType.Singleton) + .Publish(); + + var schemaTarget = schemaSource.SerializeAndDeserialize<Schema, object>(); + + schemaTarget.Should().BeEquivalentTo(expected); + } + + private static RootField<NumberFieldProperties> CreateField(int id) + { + return Fields.Number(id, $"myField{id}", Partitioning.Invariant); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs index 6a7f8b7f1e..854f66a1f9 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs @@ -11,69 +11,68 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ConvertContent +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent; + +public class ContentConversionFlatTests { - public class ContentConversionFlatTests + private readonly ContentData source = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddLocalized("de", 1) + .AddLocalized("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddLocalized("de", JsonValue.Null) + .AddLocalized("it", 4)) + .AddField("field3", + new ContentFieldData() + .AddLocalized("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddLocalized("it", 7)) + .AddField("field5", + new ContentFieldData()); + + [Fact] + public void Should_return_flatten_value() { - private readonly ContentData source = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddLocalized("de", 1) - .AddLocalized("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddLocalized("de", JsonValue.Null) - .AddLocalized("it", 4)) - .AddField("field3", - new ContentFieldData() - .AddLocalized("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddLocalized("it", 7)) - .AddField("field5", - new ContentFieldData()); + var output = source.ToFlatten(); - [Fact] - public void Should_return_flatten_value() + var expected = new Dictionary<string, object?> { - var output = source.ToFlatten(); - - var expected = new Dictionary<string, object?> { - { - "field1", - new ContentFieldData() - .AddLocalized("de", 1) - .AddLocalized("en", 2) - }, - { - "field2", - new ContentFieldData() - .AddLocalized("de", JsonValue.Null) - .AddLocalized("it", 4) - }, - { "field3", JsonValue.Create(6) }, - { "field4", JsonValue.Create(7) } - }; + "field1", + new ContentFieldData() + .AddLocalized("de", 1) + .AddLocalized("en", 2) + }, + { + "field2", + new ContentFieldData() + .AddLocalized("de", JsonValue.Null) + .AddLocalized("it", 4) + }, + { "field3", JsonValue.Create(6) }, + { "field4", JsonValue.Create(7) } + }; - Assert.True(expected.EqualsDictionary(output)); - } + Assert.True(expected.EqualsDictionary(output)); + } - [Fact] - public void Should_return_flatten_value_and_always_with_first() - { - var output = source.ToFlatten("it"); + [Fact] + public void Should_return_flatten_value_and_always_with_first() + { + var output = source.ToFlatten("it"); - var expected = new FlatContentData - { - { "field1", JsonValue.Create(1) }, - { "field2", JsonValue.Create(4) }, - { "field3", JsonValue.Create(6) }, - { "field4", JsonValue.Create(7) } - }; + var expected = new FlatContentData + { + { "field1", JsonValue.Create(1) }, + { "field2", JsonValue.Create(4) }, + { "field3", JsonValue.Create(6) }, + { "field4", JsonValue.Create(7) } + }; - Assert.True(expected.EqualsDictionary(output)); - } + Assert.True(expected.EqualsDictionary(output)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs index f74d57db1c..0dae97627e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs @@ -12,51 +12,65 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ConvertContent +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent; + +public class ContentConversionTests { - public class ContentConversionTests - { - private readonly Schema schema; - private readonly ResolvedComponents components; + private readonly Schema schema; + private readonly ResolvedComponents components; - public ContentConversionTests() + public ContentConversionTests() + { + schema = + new Schema("my-schema") + .AddComponent(1, "component", Partitioning.Invariant) + .AddComponents(2, "components", Partitioning.Invariant) + .AddAssets(3, "assets1", Partitioning.Invariant) + .AddAssets(4, "assets2", Partitioning.Invariant) + .AddReferences(5, "references", Partitioning.Invariant) + .AddArray(6, "array", Partitioning.Invariant, a => a + .AddAssets(31, "nested")); + + components = new ResolvedComponents(new Dictionary<DomainId, Schema> { - schema = - new Schema("my-schema") - .AddComponent(1, "component", Partitioning.Invariant) - .AddComponents(2, "components", Partitioning.Invariant) - .AddAssets(3, "assets1", Partitioning.Invariant) - .AddAssets(4, "assets2", Partitioning.Invariant) - .AddReferences(5, "references", Partitioning.Invariant) - .AddArray(6, "array", Partitioning.Invariant, a => a - .AddAssets(31, "nested")); - - components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [DomainId.Empty] = schema - }); - } + [DomainId.Empty] = schema + }); + } - [Fact] - public void Should_apply_value_conversion_on_all_levels() - { - var source = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1, 2))) - .AddField("assets1", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1))) - .AddField("array", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("nested", JsonValue.Array(1, 2))))) - .AddField("component", - new ContentFieldData() - .AddInvariant( + [Fact] + public void Should_apply_value_conversion_on_all_levels() + { + var source = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1, 2))) + .AddField("assets1", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1))) + .AddField("array", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject() + .Add("nested", JsonValue.Array(1, 2))))) + .AddField("component", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("references", + JsonValue.Array(1, 2)) + .Add("assets1", + JsonValue.Array(1)) + .Add("array", + JsonValue.Array( + new JsonObject() + .Add("nested", JsonValue.Array(1, 2)))) + .Add(Component.Discriminator, DomainId.Empty))) + .AddField("components", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( new JsonObject() .Add("references", JsonValue.Array(1, 2)) @@ -66,84 +80,84 @@ public void Should_apply_value_conversion_on_all_levels() JsonValue.Array( new JsonObject() .Add("nested", JsonValue.Array(1, 2)))) - .Add(Component.Discriminator, DomainId.Empty))) - .AddField("components", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("references", - JsonValue.Array(1, 2)) - .Add("assets1", - JsonValue.Array(1)) - .Add("array", - JsonValue.Array( - new JsonObject() - .Add("nested", JsonValue.Array(1, 2)))) - .Add(Component.Discriminator, DomainId.Empty)))); - - var actual = - new ContentConverter(components, schema) - .Add(new ValueConverter()) - .Convert(source); - - var expected = - new ContentData() - .AddField("references", - new ContentFieldData()) - .AddField("assets1", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1))) - .AddField("array", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject()))) - .AddField("component", - new ContentFieldData() - .AddInvariant( + .Add(Component.Discriminator, DomainId.Empty)))); + + var actual = + new ContentConverter(components, schema) + .Add(new ValueConverter()) + .Convert(source); + + var expected = + new ContentData() + .AddField("references", + new ContentFieldData()) + .AddField("assets1", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1))) + .AddField("array", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject()))) + .AddField("component", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("assets1", + JsonValue.Array(1)) + .Add("array", + JsonValue.Array( + new JsonObject())) + .Add(Component.Discriminator, DomainId.Empty))) + .AddField("components", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( new JsonObject() .Add("assets1", JsonValue.Array(1)) .Add("array", JsonValue.Array( new JsonObject())) - .Add(Component.Discriminator, DomainId.Empty))) - .AddField("components", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("assets1", - JsonValue.Array(1)) - .Add("array", - JsonValue.Array( - new JsonObject())) - .Add(Component.Discriminator, DomainId.Empty)))); - - Assert.Equal(expected, actual); - } + .Add(Component.Discriminator, DomainId.Empty)))); - [Fact] - public void Should_apply_item_conversion_on_all_levels() - { - var source = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1, 2))) - .AddField("assets1", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1))) - .AddField("array", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("nested", JsonValue.Array(1, 2))))) - .AddField("component", - new ContentFieldData() - .AddInvariant( + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_apply_item_conversion_on_all_levels() + { + var source = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1, 2))) + .AddField("assets1", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1))) + .AddField("array", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject() + .Add("nested", JsonValue.Array(1, 2))))) + .AddField("component", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("references", + JsonValue.Array(1, 2)) + .Add("assets1", + JsonValue.Array(1)) + .Add("array", + JsonValue.Array( + new JsonObject() + .Add("nested", JsonValue.Array(1, 2)))) + .Add(Component.Discriminator, DomainId.Empty))) + .AddField("components", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( new JsonObject() .Add("references", JsonValue.Array(1, 2)) @@ -153,45 +167,47 @@ public void Should_apply_item_conversion_on_all_levels() JsonValue.Array( new JsonObject() .Add("nested", JsonValue.Array(1, 2)))) - .Add(Component.Discriminator, DomainId.Empty))) - .AddField("components", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("references", - JsonValue.Array(1, 2)) - .Add("assets1", - JsonValue.Array(1)) - .Add("array", - JsonValue.Array( - new JsonObject() - .Add("nested", JsonValue.Array(1, 2)))) - .Add(Component.Discriminator, DomainId.Empty)))); - - var actual = - new ContentConverter(components, schema) - .Add(new ItemConverter()) - .Convert(source); - - var expected = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1, 2))) - .AddField("assets1", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1))) - .AddField("array", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("extraField", 42) - .Add("nested", JsonValue.Array(1, 2))))) - .AddField("component", - new ContentFieldData() - .AddInvariant( + .Add(Component.Discriminator, DomainId.Empty)))); + + var actual = + new ContentConverter(components, schema) + .Add(new ItemConverter()) + .Convert(source); + + var expected = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1, 2))) + .AddField("assets1", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1))) + .AddField("array", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject() + .Add("extraField", 42) + .Add("nested", JsonValue.Array(1, 2))))) + .AddField("component", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("extraField", 42) + .Add("references", + JsonValue.Array(1, 2)) + .Add("assets1", + JsonValue.Array(1)) + .Add("array", + JsonValue.Array( + new JsonObject() + .Add("extraField", 42) + .Add("nested", JsonValue.Array(1, 2)))) + .Add(Component.Discriminator, DomainId.Empty))) + .AddField("components", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( new JsonObject() .Add("extraField", 42) .Add("references", @@ -203,45 +219,28 @@ public void Should_apply_item_conversion_on_all_levels() new JsonObject() .Add("extraField", 42) .Add("nested", JsonValue.Array(1, 2)))) - .Add(Component.Discriminator, DomainId.Empty))) - .AddField("components", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("extraField", 42) - .Add("references", - JsonValue.Array(1, 2)) - .Add("assets1", - JsonValue.Array(1)) - .Add("array", - JsonValue.Array( - new JsonObject() - .Add("extraField", 42) - .Add("nested", JsonValue.Array(1, 2)))) - .Add(Component.Discriminator, DomainId.Empty)))); - - Assert.Equal(expected, actual); - } + .Add(Component.Discriminator, DomainId.Empty)))); - private sealed class ItemConverter : IContentItemConverter + Assert.Equal(expected, actual); + } + + private sealed class ItemConverter : IContentItemConverter + { + public JsonObject ConvertItem(IField field, JsonObject source) { - public JsonObject ConvertItem(IField field, JsonObject source) - { - source["extraField"] = 42; + source["extraField"] = 42; - return source; - } + return source; } + } - private sealed class ValueConverter : IContentValueConverter + private sealed class ValueConverter : IContentValueConverter + { + public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) { - public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) - { - var remove = field.Name != "assets1"; + var remove = field.Name != "assets1"; - return (remove, source); - } + return (remove, source); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs index 2308c89139..225a87d1a5 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs @@ -14,626 +14,625 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ConvertContent +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent; + +public class FieldConvertersTests { - public class FieldConvertersTests + private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); + + private static IEnumerable<object?[]> InvalidValues() { - private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); + yield return new object?[] { JsonValue.Null }; + yield return new object?[] { JsonValue.Create(false) }; // Undefined + } - private static IEnumerable<object?[]> InvalidValues() - { - yield return new object?[] { JsonValue.Null }; - yield return new object?[] { JsonValue.Create(false) }; // Undefined - } + [Fact] + public void Should_not_change_data_if_all_field_values_have_correct_type() + { + var field1 = Fields.Number(1, "number1", Partitioning.Language); + var field2 = Fields.Number(2, "number2", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1) + .AddField(field2); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)) + .AddField(field2.Name, + new ContentFieldData() + .AddLocalized("en", JsonValue.Null) + .AddLocalized("de", 1)); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) + .Convert(source); + + var expected = source; + + Assert.Equal(expected, actual); + } - [Fact] - public void Should_not_change_data_if_all_field_values_have_correct_type() - { - var field1 = Fields.Number(1, "number1", Partitioning.Language); - var field2 = Fields.Number(2, "number2", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1) - .AddField(field2); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", 1)) - .AddField(field2.Name, - new ContentFieldData() - .AddLocalized("en", JsonValue.Null) - .AddLocalized("de", 1)); - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) - .Convert(source); - - var expected = source; - - Assert.Equal(expected, actual); - } + [Fact] + public void Should_remove_fields_with_invalid_data() + { + var field1 = Fields.Number(1, "number1", Partitioning.Language); + var field2 = Fields.Number(2, "number2", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1) + .AddField(field2); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)) + .AddField(field2.Name, + new ContentFieldData() + .AddLocalized("en", "2") + .AddLocalized("de", 2)); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)); + + Assert.Equal(expected, actual); + } - [Fact] - public void Should_remove_fields_with_invalid_data() - { - var field1 = Fields.Number(1, "number1", Partitioning.Language); - var field2 = Fields.Number(2, "number2", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1) - .AddField(field2); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", 1)) - .AddField(field2.Name, - new ContentFieldData() - .AddLocalized("en", "2") - .AddLocalized("de", 2)); - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", 1)); - - Assert.Equal(expected, actual); - } + [Fact] + public void Should_remove_hidden_fields() + { + var field1 = Fields.Number(1, "number1", Partitioning.Language); + var field2 = Fields.Number(2, "number2", Partitioning.Language).Hide(); + + var schema = + new Schema("my-schema") + .AddField(field1) + .AddField(field2); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)) + .AddField(field2.Name, + new ContentFieldData() + .AddLocalized("en", JsonValue.Null) + .AddLocalized("de", 1)); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(ExcludeHidden.Instance) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)); + + Assert.Equal(expected, actual); + } - [Fact] - public void Should_remove_hidden_fields() - { - var field1 = Fields.Number(1, "number1", Partitioning.Language); - var field2 = Fields.Number(2, "number2", Partitioning.Language).Hide(); - - var schema = - new Schema("my-schema") - .AddField(field1) - .AddField(field2); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", 1)) - .AddField(field2.Name, - new ContentFieldData() - .AddLocalized("en", JsonValue.Null) - .AddLocalized("de", 1)); - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(ExcludeHidden.Instance) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", 1)); - - Assert.Equal(expected, actual); - } + [Fact] + public void Should_not_remove_hidden_fields() + { + var field1 = Fields.Number(1, "number1", Partitioning.Language); + var field2 = Fields.Number(2, "number2", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1) + .AddField(field2) + .HideField(2); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)) + .AddField(field2.Name, + new ContentFieldData() + .AddLocalized("en", "2")); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)); + + Assert.Equal(expected, actual); + } - [Fact] - public void Should_not_remove_hidden_fields() - { - var field1 = Fields.Number(1, "number1", Partitioning.Language); - var field2 = Fields.Number(2, "number2", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1) - .AddField(field2) - .HideField(2); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", 1)) - .AddField(field2.Name, - new ContentFieldData() - .AddLocalized("en", "2")); - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ExcludeChangedTypes(TestUtils.DefaultSerializer)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", 1)); - - Assert.Equal(expected, actual); - } + [Fact] + public void Should_remove_old_languages() + { + var field1 = Fields.String(1, "string1", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "EN") + .AddLocalized("de", "DE") + .AddLocalized("it", "IT")); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "EN") + .AddLocalized("de", "DE")); + + Assert.Equal(expected, actual); + } - [Fact] - public void Should_remove_old_languages() - { - var field1 = Fields.String(1, "string1", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "EN") - .AddLocalized("de", "DE") - .AddLocalized("it", "IT")); - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "EN") - .AddLocalized("de", "DE")); - - Assert.Equal(expected, actual); - } + [Fact] + public void Should_remove_unwanted_languages() + { + var field1 = Fields.String(1, "string1", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "EN") + .AddLocalized("de", "DE") + .AddLocalized("it", "IT")); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages, true, new[] { Language.DE })) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("de", "DE")); + + Assert.Equal(expected, actual); + } - [Fact] - public void Should_remove_unwanted_languages() - { - var field1 = Fields.String(1, "string1", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "EN") - .AddLocalized("de", "DE") - .AddLocalized("it", "IT")); - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages, true, new[] { Language.DE })) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("de", "DE")); - - Assert.Equal(expected, actual); - } + [Theory] + [MemberData(nameof(InvalidValues))] + public void Should_resolve_master_language_from_invariant(JsonValue value) + { + var field1 = Fields.String(1, "string", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1); - [Theory] - [MemberData(nameof(InvalidValues))] - public void Should_resolve_master_language_from_invariant(JsonValue value) + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("iv", "A") + .AddLocalized("it", "B")); + + if (value != false) { - var field1 = Fields.String(1, "string", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("iv", "A") - .AddLocalized("it", "B")); - - if (value != false) - { - source[field1.Name]!["en"] = value!; - } - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages, false)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "A")); - - Assert.Equal(expected, actual); + source[field1.Name]!["en"] = value!; } - [Theory] - [MemberData(nameof(InvalidValues))] - public void Should_remove_unwanted_languages_and_invariant(JsonValue value) + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages, false)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "A")); + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(InvalidValues))] + public void Should_remove_unwanted_languages_and_invariant(JsonValue value) + { + var field1 = Fields.String(1, "string", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("iv", "A") + .AddLocalized("it", "B")); + + if (value != false) { - var field1 = Fields.String(1, "string", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("iv", "A") - .AddLocalized("it", "B")); - - if (value != false) - { - source[field1.Name]!["en"] = value!; - } - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages, false, Language.DE)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData()); - - Assert.Equal(expected, actual); + source[field1.Name]!["en"] = value!; } - [Theory] - [MemberData(nameof(InvalidValues))] - public void Should_not_resolve_master_language_if_not_found(JsonValue value) + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages, false, Language.DE)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData()); + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(InvalidValues))] + public void Should_not_resolve_master_language_if_not_found(JsonValue value) + { + var field1 = Fields.String(1, "string", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("es", "A") + .AddLocalized("it", "B")); + + if (value != false) { - var field1 = Fields.String(1, "string", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("es", "A") - .AddLocalized("it", "B")); - - if (value != false) - { - source[field1.Name]!["en"] = value; - } - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData()); - - if (value != false) - { - expected[field1.Name]!["en"] = value; - } - - Assert.Equal(expected, actual); + source[field1.Name]!["en"] = value; } - [Fact] - public void Should_resolve_language_from_master_and_filter() + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData()); + + if (value != false) { - var field1 = Fields.String(1, "string", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "A")); - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages, true, Language.DE)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("de", "A")); - - Assert.Equal(expected, actual); + expected[field1.Name]!["en"] = value; } - [Fact] - public void Should_keep_invariant() - { - var field1 = Fields.String(1, "string", Partitioning.Invariant); + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_resolve_language_from_master_and_filter() + { + var field1 = Fields.String(1, "string", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "A")); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages, true, Language.DE)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("de", "A")); + + Assert.Equal(expected, actual); + } - var schema = - new Schema("my-schema") - .AddField(field1); + [Fact] + public void Should_keep_invariant() + { + var field1 = Fields.String(1, "string", Partitioning.Invariant); - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddInvariant("A")); + var schema = + new Schema("my-schema") + .AddField(field1); - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages)) - .Convert(source); + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddInvariant("A")); - var expected = source; + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages)) + .Convert(source); - Assert.Equal(expected, actual); - } + var expected = source; - [Theory] - [MemberData(nameof(InvalidValues))] - public void Should_resolve_invariant_from_master_language(JsonValue value) - { - var field1 = Fields.String(1, "string", Partitioning.Invariant); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("de", "DE") - .AddLocalized("en", "EN")); - - if (value != false) - { - source[field1.Name]![InvariantPartitioning.Key] = value; - } - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveInvariant(languages)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddInvariant("EN")); - - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [MemberData(nameof(InvalidValues))] - public void Should_resolve_invariant_from_first_language(JsonValue value) + [Theory] + [MemberData(nameof(InvalidValues))] + public void Should_resolve_invariant_from_master_language(JsonValue value) + { + var field1 = Fields.String(1, "string", Partitioning.Invariant); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("de", "DE") + .AddLocalized("en", "EN")); + + if (value != false) { - var field1 = Fields.String(1, "string", Partitioning.Invariant); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("de", "DE") - .AddLocalized("it", "IT")); - - if (value != false) - { - source[field1.Name]![InvariantPartitioning.Key] = value; - } - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveInvariant(languages)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddInvariant("DE")); - - Assert.Equal(expected, actual); + source[field1.Name]![InvariantPartitioning.Key] = value; } - [Fact] - public void Should_not_resolve_invariant_if_not_found() - { - var field1 = Fields.String(1, "string", Partitioning.Invariant); + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveInvariant(languages)) + .Convert(source); - var schema = - new Schema("my-schema") - .AddField(field1); + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddInvariant("EN")); - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData()); + Assert.Equal(expected, actual); + } - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages)) - .Convert(source); + [Theory] + [MemberData(nameof(InvalidValues))] + public void Should_resolve_invariant_from_first_language(JsonValue value) + { + var field1 = Fields.String(1, "string", Partitioning.Invariant); - var expected = source; + var schema = + new Schema("my-schema") + .AddField(field1); - Assert.Equal(expected, actual); - } + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("de", "DE") + .AddLocalized("it", "IT")); - [Theory] - [MemberData(nameof(InvalidValues))] - public void Should_resolve_from_fallback_language_if_found(JsonValue value) + if (value != false) { - var field1 = Fields.String(1, "string", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var config = - LanguagesConfig.English - .Set(Language.DE) - .Set(Language.IT) - .Set(Language.ES, false, Language.IT); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "EN") - .AddLocalized("it", "IT")); - - if (value != false) - { - source[field1.Name]!["de"] = value!; - } - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(config)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "EN") - .AddLocalized("it", "IT") - .AddLocalized("es", "IT") - .AddLocalized("de", "EN")); - - Assert.Equal(expected, actual); + source[field1.Name]![InvariantPartitioning.Key] = value; } - [Fact] - public void Should_return_master_language_if_languages_to_filter_are_invalid() + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveInvariant(languages)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddInvariant("DE")); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_not_resolve_invariant_if_not_found() + { + var field1 = Fields.String(1, "string", Partitioning.Invariant); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData()); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages)) + .Convert(source); + + var expected = source; + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(InvalidValues))] + public void Should_resolve_from_fallback_language_if_found(JsonValue value) + { + var field1 = Fields.String(1, "string", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var config = + LanguagesConfig.English + .Set(Language.DE) + .Set(Language.IT) + .Set(Language.ES, false, Language.IT); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "EN") + .AddLocalized("it", "IT")); + + if (value != false) { - var field1 = Fields.String(1, "string", Partitioning.Language); - - var schema = - new Schema("my-schema") - .AddField(field1); - - var source = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "EN") - .AddLocalized("de", "DE")); - - var actual = - new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new ResolveLanguages(languages, true, Language.IT)) - .Convert(source); - - var expected = - new ContentData() - .AddField(field1.Name, - new ContentFieldData() - .AddLocalized("en", "EN")); - - Assert.Equal(expected, actual); + source[field1.Name]!["de"] = value!; } - [Fact] - public void Should_return_same_values_if_resolving_fallback_languages_from_invariant_field() - { - var field = Fields.String(1, "string", Partitioning.Invariant); + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(config)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "EN") + .AddLocalized("it", "IT") + .AddLocalized("es", "IT") + .AddLocalized("de", "EN")); + + Assert.Equal(expected, actual); + } - var source = new ContentFieldData(); + [Fact] + public void Should_return_master_language_if_languages_to_filter_are_invalid() + { + var field1 = Fields.String(1, "string", Partitioning.Language); + + var schema = + new Schema("my-schema") + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "EN") + .AddLocalized("de", "DE")); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ResolveLanguages(languages, true, Language.IT)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", "EN")); + + Assert.Equal(expected, actual); + } - var actual = - new ResolveLanguages(languages) - .ConvertFieldAfter(field, source); + [Fact] + public void Should_return_same_values_if_resolving_fallback_languages_from_invariant_field() + { + var field = Fields.String(1, "string", Partitioning.Invariant); - Assert.Same(source, actual); - } + var source = new ContentFieldData(); - [Fact] - public void Should_return_same_values_if_filtered_languages_is_empty() - { - var field = Fields.String(1, "string", Partitioning.Language); + var actual = + new ResolveLanguages(languages) + .ConvertFieldAfter(field, source); + + Assert.Same(source, actual); + } - var source = new ContentFieldData(); + [Fact] + public void Should_return_same_values_if_filtered_languages_is_empty() + { + var field = Fields.String(1, "string", Partitioning.Language); - var actual = - new ResolveLanguages(languages, true, Array.Empty<Language>()) - .ConvertFieldAfter(field, source); + var source = new ContentFieldData(); - Assert.Same(source, actual); - } + var actual = + new ResolveLanguages(languages, true, Array.Empty<Language>()) + .ConvertFieldAfter(field, source); - [Fact] - public void Should_return_same_values_if_filtering_languages_from_invariant_field() - { - var field = Fields.String(1, "string", Partitioning.Invariant); + Assert.Same(source, actual); + } - var source = new ContentFieldData(); + [Fact] + public void Should_return_same_values_if_filtering_languages_from_invariant_field() + { + var field = Fields.String(1, "string", Partitioning.Invariant); - var actual = - new ResolveLanguages(languages) - .ConvertFieldAfter(field, source); + var source = new ContentFieldData(); - Assert.Same(source, actual); - } + var actual = + new ResolveLanguages(languages) + .ConvertFieldAfter(field, source); - /* - [Fact] - public void Should_add_schema_name_to_component() - { - var field = Fields.Component(1, "component", Partitioning.Invariant); - - var componentId = DomainId.NewGuid(); - var component = new Schema("my-component"); - var components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [componentId] = component - }); - - var source = - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add(Component.Discriminator, componentId)); - - var expected = - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add(Component.Discriminator, componentId) - .Add("schemaName", component.Name)); - - var actual = FieldConverters.AddSchemaName(components)(data, field); - - var expected = new ContentFieldData(); - - Assert.Equal(expected, actual); - }*/ + Assert.Same(source, actual); } + + /* + [Fact] + public void Should_add_schema_name_to_component() + { + var field = Fields.Component(1, "component", Partitioning.Invariant); + + var componentId = DomainId.NewGuid(); + var component = new Schema("my-component"); + var components = new ResolvedComponents(new Dictionary<DomainId, Schema> + { + [componentId] = component + }); + + var source = + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add(Component.Discriminator, componentId)); + + var expected = + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add(Component.Discriminator, componentId) + .Add("schemaName", component.Name)); + + var actual = FieldConverters.AddSchemaName(components)(data, field); + + var expected = new ContentFieldData(); + + Assert.Equal(expected, actual); + }*/ } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/StringFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/StringFormatterTests.cs index 2df9b5932d..fc7a9d0753 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/StringFormatterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/StringFormatterTests.cs @@ -10,376 +10,375 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ConvertContent +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent; + +public sealed class StringFormatterTests { - public sealed class StringFormatterTests + [Fact] + public void Should_format_null_value() { - [Fact] - public void Should_format_null_value() - { - var field = Fields.Array(1, "field", Partitioning.Invariant); + var field = Fields.Array(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, default); + var formatted = StringFormatter.Format(field, default); - Assert.Empty(formatted); - } + Assert.Empty(formatted); + } - [Fact] - public void Should_format_null_json_value() - { - var field = Fields.Array(1, "field", Partitioning.Invariant); + [Fact] + public void Should_format_null_json_value() + { + var field = Fields.Array(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, JsonValue.Null); + var formatted = StringFormatter.Format(field, JsonValue.Null); - Assert.Empty(formatted); - } + Assert.Empty(formatted); + } - [Fact] - public void Should_format_array_field_without_items() - { - var value = new JsonArray(); + [Fact] + public void Should_format_array_field_without_items() + { + var value = new JsonArray(); - var field = Fields.Array(1, "field", Partitioning.Invariant); + var field = Fields.Array(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("0 Items", formatted); - } + Assert.Equal("0 Items", formatted); + } - [Fact] - public void Should_format_array_field_with_single_item() - { - var value = JsonValue.Array(new JsonObject()); + [Fact] + public void Should_format_array_field_with_single_item() + { + var value = JsonValue.Array(new JsonObject()); - var field = Fields.Array(1, "field", Partitioning.Invariant); + var field = Fields.Array(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("1 Item", formatted); - } + Assert.Equal("1 Item", formatted); + } - [Fact] - public void Should_format_array_field_with_multiple_items() - { - var value = JsonValue.Array(new JsonObject(), new JsonObject()); + [Fact] + public void Should_format_array_field_with_multiple_items() + { + var value = JsonValue.Array(new JsonObject(), new JsonObject()); - var field = Fields.Array(1, "field", Partitioning.Invariant); + var field = Fields.Array(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("2 Items", formatted); - } + Assert.Equal("2 Items", formatted); + } - [Fact] - public void Should_format_array_field_with_wrong_type() - { - var value = JsonValue.True; + [Fact] + public void Should_format_array_field_with_wrong_type() + { + var value = JsonValue.True; - var field = Fields.Array(1, "field", Partitioning.Invariant); + var field = Fields.Array(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("0 Items", formatted); - } + Assert.Equal("0 Items", formatted); + } - [Fact] - public void Should_format_assets_field_without_items() - { - var value = new JsonArray(); + [Fact] + public void Should_format_assets_field_without_items() + { + var value = new JsonArray(); - var field = Fields.Assets(1, "field", Partitioning.Invariant); + var field = Fields.Assets(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("0 Assets", formatted); - } + Assert.Equal("0 Assets", formatted); + } - [Fact] - public void Should_format_assets_field_with_single_item() - { - var value = JsonValue.Array(new JsonObject()); + [Fact] + public void Should_format_assets_field_with_single_item() + { + var value = JsonValue.Array(new JsonObject()); - var field = Fields.Assets(1, "field", Partitioning.Invariant); + var field = Fields.Assets(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("1 Asset", formatted); - } + Assert.Equal("1 Asset", formatted); + } - [Fact] - public void Should_format_assets_field_with_multiple_items() - { - var value = JsonValue.Array(new JsonObject(), new JsonObject()); + [Fact] + public void Should_format_assets_field_with_multiple_items() + { + var value = JsonValue.Array(new JsonObject(), new JsonObject()); - var field = Fields.Assets(1, "field", Partitioning.Invariant); + var field = Fields.Assets(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("2 Assets", formatted); - } + Assert.Equal("2 Assets", formatted); + } - [Fact] - public void Should_format_assets_field_with_wrong_type() - { - var value = JsonValue.True; + [Fact] + public void Should_format_assets_field_with_wrong_type() + { + var value = JsonValue.True; - var field = Fields.Assets(1, "field", Partitioning.Invariant); + var field = Fields.Assets(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("0 Assets", formatted); - } + Assert.Equal("0 Assets", formatted); + } - [Fact] - public void Should_format_boolean_field_with_true() - { - var value = JsonValue.True; + [Fact] + public void Should_format_boolean_field_with_true() + { + var value = JsonValue.True; - var field = Fields.Boolean(1, "field", Partitioning.Invariant); + var field = Fields.Boolean(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("Yes", formatted); - } + Assert.Equal("Yes", formatted); + } - [Fact] - public void Should_format_boolean_field_with_false() - { - var value = JsonValue.False; + [Fact] + public void Should_format_boolean_field_with_false() + { + var value = JsonValue.False; - var field = Fields.Boolean(1, "field", Partitioning.Invariant); + var field = Fields.Boolean(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("No", formatted); - } + Assert.Equal("No", formatted); + } - [Fact] - public void Should_format_boolean_field_with_wrong_type() - { - var value = JsonValue.Zero; + [Fact] + public void Should_format_boolean_field_with_wrong_type() + { + var value = JsonValue.Zero; - var field = Fields.Boolean(1, "field", Partitioning.Invariant); + var field = Fields.Boolean(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("No", formatted); - } + Assert.Equal("No", formatted); + } - [Fact] - public void Should_format_component_field() - { - var value = new JsonObject(); + [Fact] + public void Should_format_component_field() + { + var value = new JsonObject(); - var field = Fields.Component(1, "field", Partitioning.Invariant); + var field = Fields.Component(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("{ Component }", formatted); - } + Assert.Equal("{ Component }", formatted); + } - [Fact] - public void Should_format_components_field_without_items() - { - var value = new JsonArray(); + [Fact] + public void Should_format_components_field_without_items() + { + var value = new JsonArray(); - var field = Fields.Components(1, "field", Partitioning.Invariant); + var field = Fields.Components(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("0 Components", formatted); - } + Assert.Equal("0 Components", formatted); + } - [Fact] - public void Should_format_components_field_with_single_item() - { - var value = JsonValue.Array(new JsonObject()); + [Fact] + public void Should_format_components_field_with_single_item() + { + var value = JsonValue.Array(new JsonObject()); - var field = Fields.Components(1, "field", Partitioning.Invariant); + var field = Fields.Components(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("1 Component", formatted); - } + Assert.Equal("1 Component", formatted); + } - [Fact] - public void Should_format_components_field_with_multiple_items() - { - var value = JsonValue.Array(new JsonObject(), new JsonObject()); + [Fact] + public void Should_format_components_field_with_multiple_items() + { + var value = JsonValue.Array(new JsonObject(), new JsonObject()); - var field = Fields.Components(1, "field", Partitioning.Invariant); + var field = Fields.Components(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("2 Components", formatted); - } + Assert.Equal("2 Components", formatted); + } - [Fact] - public void Should_format_datetime_field() - { - var value = JsonValue.Create("2019-01-19T12:00:00Z"); + [Fact] + public void Should_format_datetime_field() + { + var value = JsonValue.Create("2019-01-19T12:00:00Z"); - var field = Fields.DateTime(1, "field", Partitioning.Invariant); + var field = Fields.DateTime(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("2019-01-19T12:00:00Z", formatted); - } + Assert.Equal("2019-01-19T12:00:00Z", formatted); + } - [Fact] - public void Should_format_geolocation_field_with_correct_data() - { - var value = new JsonObject().Add("latitude", 18.9).Add("longitude", 10.9); + [Fact] + public void Should_format_geolocation_field_with_correct_data() + { + var value = new JsonObject().Add("latitude", 18.9).Add("longitude", 10.9); - var field = Fields.Geolocation(1, "field", Partitioning.Invariant); + var field = Fields.Geolocation(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("18.9, 10.9", formatted); - } + Assert.Equal("18.9, 10.9", formatted); + } - [Fact] - public void Should_format_geolocation_field_with_missing_property() - { - var value = new JsonObject().Add("latitude", 18.9); + [Fact] + public void Should_format_geolocation_field_with_missing_property() + { + var value = new JsonObject().Add("latitude", 18.9); - var field = Fields.Geolocation(1, "field", Partitioning.Invariant); + var field = Fields.Geolocation(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Empty(formatted); - } + Assert.Empty(formatted); + } - [Fact] - public void Should_format_geolocation_field_with_invalid_type() - { - var value = JsonValue.Zero; + [Fact] + public void Should_format_geolocation_field_with_invalid_type() + { + var value = JsonValue.Zero; - var field = Fields.Geolocation(1, "field", Partitioning.Invariant); + var field = Fields.Geolocation(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Empty(formatted); - } + Assert.Empty(formatted); + } - [Fact] - public void Should_format_json_field() - { - var value = new JsonObject(); + [Fact] + public void Should_format_json_field() + { + var value = new JsonObject(); - var field = Fields.Json(1, "field", Partitioning.Invariant); + var field = Fields.Json(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("<Json />", formatted); - } + Assert.Equal("<Json />", formatted); + } - [Fact] - public void Should_format_number_field() - { - var value = JsonValue.Create(123); + [Fact] + public void Should_format_number_field() + { + var value = JsonValue.Create(123); - var field = Fields.Number(1, "field", Partitioning.Invariant); + var field = Fields.Number(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("123", formatted); - } + Assert.Equal("123", formatted); + } - [Fact] - public void Should_format_references_field_without_items() - { - var value = new JsonArray(); + [Fact] + public void Should_format_references_field_without_items() + { + var value = new JsonArray(); - var field = Fields.References(1, "field", Partitioning.Invariant); + var field = Fields.References(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("0 References", formatted); - } + Assert.Equal("0 References", formatted); + } - [Fact] - public void Should_format_references_field_with_single_item() - { - var value = JsonValue.Array(new JsonObject()); + [Fact] + public void Should_format_references_field_with_single_item() + { + var value = JsonValue.Array(new JsonObject()); - var field = Fields.References(1, "field", Partitioning.Invariant); + var field = Fields.References(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("1 Reference", formatted); - } + Assert.Equal("1 Reference", formatted); + } - [Fact] - public void Should_format_references_field_with_multiple_items() - { - var value = JsonValue.Array(new JsonObject(), new JsonObject()); + [Fact] + public void Should_format_references_field_with_multiple_items() + { + var value = JsonValue.Array(new JsonObject(), new JsonObject()); - var field = Fields.References(1, "field", Partitioning.Invariant); + var field = Fields.References(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("2 References", formatted); - } + Assert.Equal("2 References", formatted); + } - [Fact] - public void Should_format_references_field_with_wrong_type() - { - var value = JsonValue.True; + [Fact] + public void Should_format_references_field_with_wrong_type() + { + var value = JsonValue.True; - var field = Fields.References(1, "field", Partitioning.Invariant); + var field = Fields.References(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("0 References", formatted); - } + Assert.Equal("0 References", formatted); + } - [Fact] - public void Should_format_string_field() - { - var value = JsonValue.Create("hello"); + [Fact] + public void Should_format_string_field() + { + var value = JsonValue.Create("hello"); - var field = Fields.String(1, "field", Partitioning.Invariant); + var field = Fields.String(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("hello", formatted); - } + Assert.Equal("hello", formatted); + } - [Fact] - public void Should_format_string_field_with_photo_editor() - { - var value = JsonValue.Create("hello"); + [Fact] + public void Should_format_string_field_with_photo_editor() + { + var value = JsonValue.Create("hello"); - var field = Fields.String(1, "field", Partitioning.Invariant, new StringFieldProperties { Editor = StringFieldEditor.StockPhoto }); + var field = Fields.String(1, "field", Partitioning.Invariant, new StringFieldProperties { Editor = StringFieldEditor.StockPhoto }); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("[Photo]", formatted); - } + Assert.Equal("[Photo]", formatted); + } - [Fact] - public void Should_format_tags_field() - { - var value = JsonValue.Array("hello", "squidex", "and", "team"); + [Fact] + public void Should_format_tags_field() + { + var value = JsonValue.Array("hello", "squidex", "and", "team"); - var field = Fields.Tags(1, "field", Partitioning.Invariant); + var field = Fields.Tags(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Equal("hello, squidex, and, team", formatted); - } + Assert.Equal("hello, squidex, and, team", formatted); + } - [Fact] - public void Should_format_tags_field_with_invalid_type() - { - var value = JsonValue.Zero; + [Fact] + public void Should_format_tags_field_with_invalid_type() + { + var value = JsonValue.Zero; - var field = Fields.Tags(1, "field", Partitioning.Invariant); + var field = Fields.Tags(1, "field", Partitioning.Invariant); - var formatted = StringFormatter.Format(field, value); + var formatted = StringFormatter.Format(field, value); - Assert.Empty(formatted); - } + Assert.Empty(formatted); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs index 8b3c76dbba..a8dcf82418 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs @@ -14,264 +14,263 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ConvertContent +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent; + +public class ValueConvertersTests { - public class ValueConvertersTests - { - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly DomainId id1 = DomainId.NewGuid(); - private readonly DomainId id2 = DomainId.NewGuid(); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly DomainId id1 = DomainId.NewGuid(); + private readonly DomainId id2 = DomainId.NewGuid(); - private readonly RootField<StringFieldProperties> stringField - = Fields.String(1, "1", Partitioning.Invariant); + private readonly RootField<StringFieldProperties> stringField + = Fields.String(1, "1", Partitioning.Invariant); - private readonly RootField<NumberFieldProperties> numberField - = Fields.Number(1, "1", Partitioning.Invariant); + private readonly RootField<NumberFieldProperties> numberField + = Fields.Number(1, "1", Partitioning.Invariant); - public ValueConvertersTests() - { - A.CallTo(() => urlGenerator.AssetContent(appId, A<string>._)) - .ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<string>(1)}"); - } + public ValueConvertersTests() + { + A.CallTo(() => urlGenerator.AssetContent(appId, A<string>._)) + .ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<string>(1)}"); + } - [Fact] - public void Should_return_true_if_field_hidden() - { - var source = JsonValue.Create(123); + [Fact] + public void Should_return_true_if_field_hidden() + { + var source = JsonValue.Create(123); - var (remove, _) = - ExcludeHidden.Instance - .ConvertValue(stringField.Hide(), source, null); + var (remove, _) = + ExcludeHidden.Instance + .ConvertValue(stringField.Hide(), source, null); - Assert.True(remove); - } + Assert.True(remove); + } - [Fact] - public void Should_return_true_if_field_has_wrong_type() - { - var source = JsonValue.Create("invalid"); + [Fact] + public void Should_return_true_if_field_has_wrong_type() + { + var source = JsonValue.Create("invalid"); - var (remove, _) = - new ExcludeChangedTypes(TestUtils.DefaultSerializer) - .ConvertValue(numberField, source, numberField); + var (remove, _) = + new ExcludeChangedTypes(TestUtils.DefaultSerializer) + .ConvertValue(numberField, source, numberField); - Assert.True(remove); - } + Assert.True(remove); + } - [Theory] - [InlineData("assets")] - [InlineData("*")] - public void Should_convert_asset_ids_to_urls(string path) - { - var field = Fields.Assets(1, "assets", Partitioning.Invariant); + [Theory] + [InlineData("assets")] + [InlineData("*")] + public void Should_convert_asset_ids_to_urls(string path) + { + var field = Fields.Assets(1, "assets", Partitioning.Invariant); - var source = - JsonValue.Array( - id1, - id2); + var source = + JsonValue.Array( + id1, + id2); - var (_, actual) = - new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) - .ConvertValue(field, source, null); + var (_, actual) = + new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) + .ConvertValue(field, source, null); - var expected = - JsonValue.Array( - $"url/to/{id1}", - $"url/to/{id2}"); + var expected = + JsonValue.Array( + $"url/to/{id1}", + $"url/to/{id2}"); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("other")] - [InlineData("**")] - public void Should_not_convert_asset_ids_if_field_name_does_not_match(string path) - { - var field = Fields.Assets(1, "assets", Partitioning.Invariant); + [Theory] + [InlineData("other")] + [InlineData("**")] + public void Should_not_convert_asset_ids_if_field_name_does_not_match(string path) + { + var field = Fields.Assets(1, "assets", Partitioning.Invariant); - var source = - JsonValue.Array( - id1, - id2); + var source = + JsonValue.Array( + id1, + id2); - var (_, actual) = - new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) - .ConvertValue(field, source, null); + var (_, actual) = + new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) + .ConvertValue(field, source, null); - var expected = source; + var expected = source; - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("parent.assets")] - [InlineData("*")] - public void Should_convert_nested_asset_ids_to_urls(string path) - { - var field = Fields.Array(1, "parent", Partitioning.Invariant, null, null, Fields.Assets(11, "assets")); - - var source = - JsonValue.Array( - id1, - id2); - - var (_, actual) = - new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) - .ConvertValue(field.FieldsByName["assets"], source, field); - - var expected = - JsonValue.Array( - $"url/to/{id1}", - $"url/to/{id2}"); - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("assets")] - [InlineData("parent")] - [InlineData("parent.other")] - [InlineData("other.assets")] - public void Should_not_convert_nested_asset_ids_if_field_name_does_not_match(string path) - { - var field = Fields.Array(1, "parent", Partitioning.Invariant, null, null, Fields.Assets(11, "assets")); + [Theory] + [InlineData("parent.assets")] + [InlineData("*")] + public void Should_convert_nested_asset_ids_to_urls(string path) + { + var field = Fields.Array(1, "parent", Partitioning.Invariant, null, null, Fields.Assets(11, "assets")); - var source = - JsonValue.Array( - id1, - id2); + var source = + JsonValue.Array( + id1, + id2); - var (_, actual) = - new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) - .ConvertValue(field, source, null); + var (_, actual) = + new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) + .ConvertValue(field.FieldsByName["assets"], source, field); - var expected = source; + var expected = + JsonValue.Array( + $"url/to/{id1}", + $"url/to/{id2}"); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_add_schema_name_if_component() - { - var field = Fields.Component(1, "component", Partitioning.Invariant); + [Theory] + [InlineData("assets")] + [InlineData("parent")] + [InlineData("parent.other")] + [InlineData("other.assets")] + public void Should_not_convert_nested_asset_ids_if_field_name_does_not_match(string path) + { + var field = Fields.Array(1, "parent", Partitioning.Invariant, null, null, Fields.Assets(11, "assets")); - var componentId = DomainId.NewGuid(); - var component = new Schema("my-component"); - var components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [componentId] = component - }); + var source = + JsonValue.Array( + id1, + id2); - var source = - new JsonObject() - .Add(Component.Discriminator, componentId); + var (_, actual) = + new ResolveAssetUrls(appId, urlGenerator, HashSet.Of(path)) + .ConvertValue(field, source, null); - var actual = - new AddSchemaNames(components) - .ConvertItem(field, source); + var expected = source; - var expected = - new JsonObject() - .Add(Component.Discriminator, componentId) - .Add("schemaName", component.Name); + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Fact] + public void Should_add_schema_name_if_component() + { + var field = Fields.Component(1, "component", Partitioning.Invariant); - [Fact] - public void Should_not_add_schema_name_if_field_already_exists() + var componentId = DomainId.NewGuid(); + var component = new Schema("my-component"); + var components = new ResolvedComponents(new Dictionary<DomainId, Schema> { - var field = Fields.Component(1, "component", Partitioning.Invariant); + [componentId] = component + }); - var componentId = DomainId.NewGuid(); - var component = new Schema("my-component"); - var components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [componentId] = component - }); + var source = + new JsonObject() + .Add(Component.Discriminator, componentId); - var source = - new JsonObject() - .Add(Component.Discriminator, componentId) - .Add("schemaName", "existing"); + var actual = + new AddSchemaNames(components) + .ConvertItem(field, source); - var actual = - new AddSchemaNames(components) - .ConvertItem(field, source); + var expected = + new JsonObject() + .Add(Component.Discriminator, componentId) + .Add("schemaName", component.Name); - var expected = source; + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Fact] + public void Should_not_add_schema_name_if_field_already_exists() + { + var field = Fields.Component(1, "component", Partitioning.Invariant); - [Fact] - public void Should_not_add_schema_name_if_array_field() + var componentId = DomainId.NewGuid(); + var component = new Schema("my-component"); + var components = new ResolvedComponents(new Dictionary<DomainId, Schema> { - var field = Fields.Array(1, "component", Partitioning.Invariant); + [componentId] = component + }); - var componentId = DomainId.NewGuid(); - var component = new Schema("my-component"); - var components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [componentId] = component - }); + var source = + new JsonObject() + .Add(Component.Discriminator, componentId) + .Add("schemaName", "existing"); - var source = - new JsonObject() - .Add(Component.Discriminator, componentId); + var actual = + new AddSchemaNames(components) + .ConvertItem(field, source); - var actual = - new AddSchemaNames(components) - .ConvertItem(field, source); + var expected = source; - var expected = source; + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Fact] + public void Should_not_add_schema_name_if_array_field() + { + var field = Fields.Array(1, "component", Partitioning.Invariant); - [Fact] - public void Should_not_add_schema_name_if_not_a_component() + var componentId = DomainId.NewGuid(); + var component = new Schema("my-component"); + var components = new ResolvedComponents(new Dictionary<DomainId, Schema> { - var field = Fields.Component(1, "component", Partitioning.Invariant); + [componentId] = component + }); - var componentId = DomainId.NewGuid(); - var component = new Schema("my-component"); - var components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [componentId] = component - }); + var source = + new JsonObject() + .Add(Component.Discriminator, componentId); - var source = - new JsonObject(); + var actual = + new AddSchemaNames(components) + .ConvertItem(field, source); - var actual = - new AddSchemaNames(components) - .ConvertItem(field, source); + var expected = source; - var expected = source; + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Fact] + public void Should_not_add_schema_name_if_not_a_component() + { + var field = Fields.Component(1, "component", Partitioning.Invariant); - [Fact] - public void Should_not_add_schema_name_if_component_not_found() + var componentId = DomainId.NewGuid(); + var component = new Schema("my-component"); + var components = new ResolvedComponents(new Dictionary<DomainId, Schema> { - var field = Fields.Component(1, "component", Partitioning.Invariant); + [componentId] = component + }); + + var source = + new JsonObject(); + + var actual = + new AddSchemaNames(components) + .ConvertItem(field, source); + + var expected = source; + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_not_add_schema_name_if_component_not_found() + { + var field = Fields.Component(1, "component", Partitioning.Invariant); - var componentId = DomainId.NewGuid(); + var componentId = DomainId.NewGuid(); - var source = - new JsonObject() - .Add(Component.Discriminator, componentId); + var source = + new JsonObject() + .Add(Component.Discriminator, componentId); - var actual = - new AddSchemaNames(ResolvedComponents.Empty) - .ConvertItem(field, source); + var actual = + new AddSchemaNames(ResolvedComponents.Empty) + .ConvertItem(field, source); - var expected = source; + var expected = source; - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs index daa4876526..fba91be4f4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs @@ -15,323 +15,322 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.DefaultValues +namespace Squidex.Domain.Apps.Core.Operations.DefaultValues; + +public class DefaultValuesTests { - public class DefaultValuesTests + private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10); + private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); + private readonly Language language = Language.DE; + private readonly Schema schema; + + public DefaultValuesTests() { - private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10); - private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); - private readonly Language language = Language.DE; - private readonly Schema schema; - - public DefaultValuesTests() - { - schema = - new Schema("my-schema") - .AddString(1, "myString", Partitioning.Language, - new StringFieldProperties { DefaultValue = "en-string" }) - .AddNumber(2, "myNumber", Partitioning.Invariant, - new NumberFieldProperties()) - .AddDateTime(3, "myDatetime", Partitioning.Invariant, - new DateTimeFieldProperties { DefaultValue = now }) - .AddBoolean(4, "myBoolean", Partitioning.Invariant, - new BooleanFieldProperties { DefaultValue = true }); - } - - [Fact] - public void Should_enrich_with_default_values() - { - var data = - new ContentData() - .AddField("myString", - new ContentFieldData() - .AddLocalized("de", "de-string")) - .AddField("myNumber", - new ContentFieldData() - .AddInvariant(456)); - - data.GenerateDefaultValues(schema, languages.ToResolver()); - - Assert.Equal(456, data["myNumber"]!["iv"].AsNumber); - - Assert.Equal("de-string", data["myString"]!["de"].AsString); - Assert.Equal("en-string", data["myString"]!["en"].AsString); - - Assert.Equal(now.ToString(), data["myDatetime"]!["iv"].AsString); - - Assert.True(data["myBoolean"]!["iv"].AsBoolean); - } - - [Fact] - public void Should_not_enrich_with_default_values_if_string_is_empty() - { - var data = - new ContentData() - .AddField("myString", - new ContentFieldData() - .AddLocalized("de", string.Empty)) - .AddField("myNumber", - new ContentFieldData() - .AddInvariant(456)); - - data.GenerateDefaultValues(schema, languages.ToResolver()); - - Assert.Equal(string.Empty, data["myString"]!["de"].AsString); - Assert.Equal("en-string", data["myString"]!["en"].AsString); - } - - [Fact] - public void Should_get_default_value_from_assets_field() - { - var field = - Fields.Assets(1, "1", Partitioning.Invariant, - new AssetsFieldProperties()); - - Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_assets_field_if_set() - { - var field = - Fields.Assets(1, "1", Partitioning.Invariant, - new AssetsFieldProperties { DefaultValue = ReadonlyList.Create("1", "2") }); - - Assert.Equal(JsonValue.Array("1", "2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_assets_field_if_localized() - { - var field = - Fields.Assets(1, "1", Partitioning.Invariant, - new AssetsFieldProperties - { - DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> - { - [language.Iso2Code] = null - }), - DefaultValue = ReadonlyList.Create("1", "2") - }); - - Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_boolean_field() - { - var field = - Fields.Boolean(1, "1", Partitioning.Invariant, + schema = + new Schema("my-schema") + .AddString(1, "myString", Partitioning.Language, + new StringFieldProperties { DefaultValue = "en-string" }) + .AddNumber(2, "myNumber", Partitioning.Invariant, + new NumberFieldProperties()) + .AddDateTime(3, "myDatetime", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = now }) + .AddBoolean(4, "myBoolean", Partitioning.Invariant, new BooleanFieldProperties { DefaultValue = true }); + } + + [Fact] + public void Should_enrich_with_default_values() + { + var data = + new ContentData() + .AddField("myString", + new ContentFieldData() + .AddLocalized("de", "de-string")) + .AddField("myNumber", + new ContentFieldData() + .AddInvariant(456)); + + data.GenerateDefaultValues(schema, languages.ToResolver()); + + Assert.Equal(456, data["myNumber"]!["iv"].AsNumber); + + Assert.Equal("de-string", data["myString"]!["de"].AsString); + Assert.Equal("en-string", data["myString"]!["en"].AsString); + + Assert.Equal(now.ToString(), data["myDatetime"]!["iv"].AsString); + + Assert.True(data["myBoolean"]!["iv"].AsBoolean); + } + + [Fact] + public void Should_not_enrich_with_default_values_if_string_is_empty() + { + var data = + new ContentData() + .AddField("myString", + new ContentFieldData() + .AddLocalized("de", string.Empty)) + .AddField("myNumber", + new ContentFieldData() + .AddInvariant(456)); + + data.GenerateDefaultValues(schema, languages.ToResolver()); + + Assert.Equal(string.Empty, data["myString"]!["de"].AsString); + Assert.Equal("en-string", data["myString"]!["en"].AsString); + } + + [Fact] + public void Should_get_default_value_from_assets_field() + { + var field = + Fields.Assets(1, "1", Partitioning.Invariant, + new AssetsFieldProperties()); + + Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_assets_field_if_set() + { + var field = + Fields.Assets(1, "1", Partitioning.Invariant, + new AssetsFieldProperties { DefaultValue = ReadonlyList.Create("1", "2") }); + + Assert.Equal(JsonValue.Array("1", "2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_assets_field_if_localized() + { + var field = + Fields.Assets(1, "1", Partitioning.Invariant, + new AssetsFieldProperties + { + DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> + { + [language.Iso2Code] = null + }), + DefaultValue = ReadonlyList.Create("1", "2") + }); + + Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_boolean_field() + { + var field = + Fields.Boolean(1, "1", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true }); - Assert.Equal(JsonValue.True, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } + Assert.Equal(JsonValue.True, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } - [Fact] - public void Should_get_default_value_from_boolean_field_if_localized() - { - var field = - Fields.Boolean(1, "1", Partitioning.Invariant, - new BooleanFieldProperties + [Fact] + public void Should_get_default_value_from_boolean_field_if_localized() + { + var field = + Fields.Boolean(1, "1", Partitioning.Invariant, + new BooleanFieldProperties + { + DefaultValues = new LocalizedValue<bool?>(new Dictionary<string, bool?> { - DefaultValues = new LocalizedValue<bool?>(new Dictionary<string, bool?> - { - [language.Iso2Code] = null - }), - DefaultValue = true - }); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); - - Assert.Equal(JsonValue.Create(FutureDays(15)), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field_if_set_to_today() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); - - Assert.Equal(JsonValue.Create("2017-10-12T00:00:00Z"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field_if_set_to_now() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); - - Assert.Equal(JsonValue.Create("2017-10-12T16:30:10Z"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_datetime_field_if_localized() - { - var field = - Fields.DateTime(1, "1", Partitioning.Invariant, - new DateTimeFieldProperties + [language.Iso2Code] = null + }), + DefaultValue = true + }); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); + + Assert.Equal(JsonValue.Create(FutureDays(15)), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_if_set_to_today() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); + + Assert.Equal(JsonValue.Create("2017-10-12T00:00:00Z"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_if_set_to_now() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); + + Assert.Equal(JsonValue.Create("2017-10-12T16:30:10Z"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_if_localized() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties + { + DefaultValues = new LocalizedValue<Instant?>(new Dictionary<string, Instant?> { - DefaultValues = new LocalizedValue<Instant?>(new Dictionary<string, Instant?> - { - [language.Iso2Code] = null - }), - DefaultValue = FutureDays(15) - }); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_json_field() - { - var field = - Fields.Json(1, "1", Partitioning.Invariant, - new JsonFieldProperties()); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_geolocation_field() - { - var field = - Fields.Geolocation(1, "1", Partitioning.Invariant, - new GeolocationFieldProperties()); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_number_field() - { - var field = - Fields.Number(1, "1", Partitioning.Invariant, - new NumberFieldProperties { DefaultValue = 12 }); - - Assert.Equal(JsonValue.Create(12), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_number_field_if_localized() - { - var field = - Fields.Number(1, "1", Partitioning.Invariant, - new NumberFieldProperties + [language.Iso2Code] = null + }), + DefaultValue = FutureDays(15) + }); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_json_field() + { + var field = + Fields.Json(1, "1", Partitioning.Invariant, + new JsonFieldProperties()); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_geolocation_field() + { + var field = + Fields.Geolocation(1, "1", Partitioning.Invariant, + new GeolocationFieldProperties()); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_number_field() + { + var field = + Fields.Number(1, "1", Partitioning.Invariant, + new NumberFieldProperties { DefaultValue = 12 }); + + Assert.Equal(JsonValue.Create(12), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_number_field_if_localized() + { + var field = + Fields.Number(1, "1", Partitioning.Invariant, + new NumberFieldProperties + { + DefaultValues = new LocalizedValue<double?>(new Dictionary<string, double?> { - DefaultValues = new LocalizedValue<double?>(new Dictionary<string, double?> - { - [language.Iso2Code] = null - }), - DefaultValue = 12 - }); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_references_field() - { - var field = - Fields.References(1, "1", Partitioning.Invariant, - new ReferencesFieldProperties()); - - Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_references_field_if_set() - { - var field = - Fields.References(1, "1", Partitioning.Invariant, - new ReferencesFieldProperties { DefaultValue = ReadonlyList.Create("1", "2") }); - - Assert.Equal(JsonValue.Array("1", "2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_references_field_if_localized() - { - var field = - Fields.References(1, "1", Partitioning.Invariant, - new ReferencesFieldProperties + [language.Iso2Code] = null + }), + DefaultValue = 12 + }); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_references_field() + { + var field = + Fields.References(1, "1", Partitioning.Invariant, + new ReferencesFieldProperties()); + + Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_references_field_if_set() + { + var field = + Fields.References(1, "1", Partitioning.Invariant, + new ReferencesFieldProperties { DefaultValue = ReadonlyList.Create("1", "2") }); + + Assert.Equal(JsonValue.Array("1", "2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_references_field_if_localized() + { + var field = + Fields.References(1, "1", Partitioning.Invariant, + new ReferencesFieldProperties + { + DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> { - DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> - { - [language.Iso2Code] = null - }), - DefaultValue = ReadonlyList.Create("1", "2") - }); - - Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_string_field() - { - var field = - Fields.String(1, "1", Partitioning.Invariant, - new StringFieldProperties { DefaultValue = "default" }); - - Assert.Equal(JsonValue.Create("default"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_string_field_if_localized() - { - var field = - Fields.String(1, "1", Partitioning.Invariant, - new StringFieldProperties + [language.Iso2Code] = null + }), + DefaultValue = ReadonlyList.Create("1", "2") + }); + + Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_string_field() + { + var field = + Fields.String(1, "1", Partitioning.Invariant, + new StringFieldProperties { DefaultValue = "default" }); + + Assert.Equal(JsonValue.Create("default"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_string_field_if_localized() + { + var field = + Fields.String(1, "1", Partitioning.Invariant, + new StringFieldProperties + { + DefaultValues = new LocalizedValue<string?>(new Dictionary<string, string?> { - DefaultValues = new LocalizedValue<string?>(new Dictionary<string, string?> - { - [language.Iso2Code] = null - }), - DefaultValue = "default" - }); - - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_tags_field() - { - var field = - Fields.Tags(1, "1", Partitioning.Invariant, - new TagsFieldProperties { DefaultValue = ReadonlyList.Create("tag1", "tag2") }); - - Assert.Equal(JsonValue.Array("tag1", "tag2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - [Fact] - public void Should_get_default_value_from_tags_field_if_localized() - { - var field = - Fields.Tags(1, "1", Partitioning.Invariant, - new TagsFieldProperties + [language.Iso2Code] = null + }), + DefaultValue = "default" + }); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_tags_field() + { + var field = + Fields.Tags(1, "1", Partitioning.Invariant, + new TagsFieldProperties { DefaultValue = ReadonlyList.Create("tag1", "tag2") }); + + Assert.Equal(JsonValue.Array("tag1", "tag2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_tags_field_if_localized() + { + var field = + Fields.Tags(1, "1", Partitioning.Invariant, + new TagsFieldProperties + { + DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> { - DefaultValues = new LocalizedValue<ReadonlyList<string>?>(new Dictionary<string, ReadonlyList<string>?> - { - [language.Iso2Code] = null - }), - DefaultValue = ReadonlyList.Create("tag1", "tag2") - }); - - Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); - } - - private Instant FutureDays(int days) - { - return now.WithoutMs().Plus(Duration.FromDays(days)); - } + [language.Iso2Code] = null + }), + DefaultValue = ReadonlyList.Create("tag1", "tag2") + }); + + Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + private Instant FutureDays(int days) + { + return now.WithoutMs().Plus(Duration.FromDays(days)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs index 4bdd082e94..4d69d2f768 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs @@ -8,33 +8,32 @@ using FluentAssertions; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization +namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization; + +public static class AssertHelper { - public static class AssertHelper + public static void ShouldHaveSameEvents(this IEnumerable<IEvent> events, params IEvent[] others) { - public static void ShouldHaveSameEvents(this IEnumerable<IEvent> events, params IEvent[] others) - { - var source = events.ToArray(); + var source = events.ToArray(); - source.Should().HaveSameCount(others); + source.Should().HaveSameCount(others); - for (var i = 0; i < source.Length; i++) - { - var lhs = source[i]; - var rhs = others[i]; + for (var i = 0; i < source.Length; i++) + { + var lhs = source[i]; + var rhs = others[i]; - lhs.ShouldBeSameEvent(rhs); - } + lhs.ShouldBeSameEvent(rhs); } + } - public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) - { - lhs.Should().BeOfType(rhs.GetType()); + public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) + { + lhs.Should().BeOfType(rhs.GetType()); - ((object)lhs).Should().BeEquivalentTo(rhs, o => o - .WithStrictOrdering() - .IncludingNestedObjects() - .IncludingAllRuntimeProperties()); - } + ((object)lhs).Should().BeEquivalentTo(rhs, o => o + .WithStrictOrdering() + .IncludingNestedObjects() + .IncludingAllRuntimeProperties()); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs index 33d57c9b0e..afac508fb5 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs @@ -12,656 +12,655 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization +namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization; + +public class SchemaSynchronizerTests { - public class SchemaSynchronizerTests + private readonly Func<long> idGenerator; + private readonly NamedId<long> stringId = NamedId.Of(13L, "myValue"); + private readonly NamedId<long> nestedId = NamedId.Of(141L, "myValue"); + private readonly NamedId<long> arrayId = NamedId.Of(14L, "11Array"); + private int fields = 50; + + public SchemaSynchronizerTests() { - private readonly Func<long> idGenerator; - private readonly NamedId<long> stringId = NamedId.Of(13L, "myValue"); - private readonly NamedId<long> nestedId = NamedId.Of(141L, "myValue"); - private readonly NamedId<long> arrayId = NamedId.Of(14L, "11Array"); - private int fields = 50; + idGenerator = () => fields++; + } - public SchemaSynchronizerTests() - { - idGenerator = () => fields++; - } + [Fact] + public void Should_create_events_if_schema_deleted() + { + var sourceSchema = + new Schema("source"); - [Fact] - public void Should_create_events_if_schema_deleted() - { - var sourceSchema = - new Schema("source"); + var targetSchema = + (Schema?)null; - var targetSchema = - (Schema?)null; + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new SchemaDeleted() + ); + } - events.ShouldHaveSameEvents( - new SchemaDeleted() - ); - } + [Fact] + public void Should_create_events_if_category_changed() + { + var sourceSchema = + new Schema("source"); + + var targetSchema = + new Schema("target") + .ChangeCategory("Category"); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - [Fact] - public void Should_create_events_if_category_changed() + events.ShouldHaveSameEvents( + new SchemaCategoryChanged { Name = "Category" } + ); + } + + [Fact] + public void Should_create_events_if_scripts_configured() + { + var scripts = new SchemaScripts { - var sourceSchema = - new Schema("source"); + Create = "<create-script>" + }; - var targetSchema = - new Schema("target") - .ChangeCategory("Category"); + var sourceSchema = + new Schema("source"); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var targetSchema = + new Schema("target").SetScripts(scripts); - events.ShouldHaveSameEvents( - new SchemaCategoryChanged { Name = "Category" } - ); - } + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - [Fact] - public void Should_create_events_if_scripts_configured() + events.ShouldHaveSameEvents( + new SchemaScriptsConfigured { Scripts = scripts } + ); + } + + [Fact] + public void Should_create_events_if_preview_urls_configured() + { + var previewUrls = new Dictionary<string, string> { - var scripts = new SchemaScripts - { - Create = "<create-script>" - }; + ["web"] = "Url" + }.ToReadonlyDictionary(); - var sourceSchema = - new Schema("source"); + var sourceSchema = + new Schema("source"); - var targetSchema = - new Schema("target").SetScripts(scripts); + var targetSchema = + new Schema("target") + .SetPreviewUrls(previewUrls); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - events.ShouldHaveSameEvents( - new SchemaScriptsConfigured { Scripts = scripts } - ); - } + events.ShouldHaveSameEvents( + new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls } + ); + } - [Fact] - public void Should_create_events_if_preview_urls_configured() - { - var previewUrls = new Dictionary<string, string> - { - ["web"] = "Url" - }.ToReadonlyDictionary(); + [Fact] + public void Should_create_events_if_schema_published() + { + var sourceSchema = + new Schema("source"); - var sourceSchema = - new Schema("source"); + var targetSchema = + new Schema("target") + .Publish(); - var targetSchema = - new Schema("target") - .SetPreviewUrls(previewUrls); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new SchemaPublished() + ); + } - events.ShouldHaveSameEvents( - new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls } - ); - } + [Fact] + public void Should_create_events_if_schema_unpublished() + { + var sourceSchema = + new Schema("source") + .Publish(); - [Fact] - public void Should_create_events_if_schema_published() - { - var sourceSchema = - new Schema("source"); + var targetSchema = + new Schema("target"); - var targetSchema = - new Schema("target") - .Publish(); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new SchemaUnpublished() + ); + } - events.ShouldHaveSameEvents( - new SchemaPublished() - ); - } + [Fact] + public void Should_create_events_if_list_fields_changed() + { + var sourceSchema = + new Schema("source") + .SetFieldsInLists("1", "2"); - [Fact] - public void Should_create_events_if_schema_unpublished() - { - var sourceSchema = - new Schema("source") - .Publish(); + var targetSchema = + new Schema("target") + .SetFieldsInLists("2", "1"); - var targetSchema = - new Schema("target"); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new SchemaUIFieldsConfigured { FieldsInLists = FieldNames.Create("2", "1") } + ); + } - events.ShouldHaveSameEvents( - new SchemaUnpublished() - ); - } + [Fact] + public void Should_create_events_if_reference_fields_changed() + { + var sourceSchema = + new Schema("source") + .SetFieldsInReferences("1", "2"); - [Fact] - public void Should_create_events_if_list_fields_changed() - { - var sourceSchema = - new Schema("source") - .SetFieldsInLists("1", "2"); + var targetSchema = + new Schema("target") + .SetFieldsInReferences("2", "1"); - var targetSchema = - new Schema("target") - .SetFieldsInLists("2", "1"); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new SchemaUIFieldsConfigured { FieldsInReferences = FieldNames.Create("2", "1") } + ); + } - events.ShouldHaveSameEvents( - new SchemaUIFieldsConfigured { FieldsInLists = FieldNames.Create("2", "1") } - ); - } + [Fact] + public void Should_create_events_if_field_rules_changed_changed() + { + var sourceSchema = + new Schema("source") + .SetFieldRules(FieldRule.Hide("2")); - [Fact] - public void Should_create_events_if_reference_fields_changed() - { - var sourceSchema = - new Schema("source") - .SetFieldsInReferences("1", "2"); + var targetSchema = + new Schema("target") + .SetFieldRules(FieldRule.Hide("1")); - var targetSchema = - new Schema("target") - .SetFieldsInReferences("2", "1"); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new SchemaFieldRulesConfigured { FieldRules = FieldRules.Create(FieldRule.Hide("1")) } + ); + } - events.ShouldHaveSameEvents( - new SchemaUIFieldsConfigured { FieldsInReferences = FieldNames.Create("2", "1") } - ); - } + [Fact] + public void Should_create_events_if_nested_field_deleted() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); - [Fact] - public void Should_create_events_if_field_rules_changed_changed() - { - var sourceSchema = - new Schema("source") - .SetFieldRules(FieldRule.Hide("2")); + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant); - var targetSchema = - new Schema("target") - .SetFieldRules(FieldRule.Hide("1")); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId } + ); + } - events.ShouldHaveSameEvents( - new SchemaFieldRulesConfigured { FieldRules = FieldRules.Create(FieldRule.Hide("1")) } - ); - } + [Fact] + public void Should_create_events_if_field_deleted() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - [Fact] - public void Should_create_events_if_nested_field_deleted() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); + var targetSchema = + new Schema("target"); - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId } + ); + } - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId } - ); - } + [Fact] + public void Should_create_events_if_nested_field_updated() + { + var properties = new StringFieldProperties { IsRequired = true }; - [Fact] - public void Should_create_events_if_field_deleted() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); - var targetSchema = - new Schema("target"); + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name, properties)); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId } - ); - } + events.ShouldHaveSameEvents( + new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId } + ); + } - [Fact] - public void Should_create_events_if_nested_field_updated() - { - var properties = new StringFieldProperties { IsRequired = true }; + [Fact] + public void Should_create_events_if_field_updated() + { + var properties = new StringFieldProperties { Pattern = "a-z" }; - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name, properties)); + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - events.ShouldHaveSameEvents( - new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId } - ); - } + events.ShouldHaveSameEvents( + new FieldUpdated { Properties = properties, FieldId = stringId } + ); + } - [Fact] - public void Should_create_events_if_field_updated() - { - var properties = new StringFieldProperties { Pattern = "a-z" }; + [Fact] + public void Should_create_events_if_nested_field_locked() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .LockField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId } + ); + } - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + [Fact] + public void Should_create_events_if_field_locked() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties); + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .LockField(stringId.Id); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - events.ShouldHaveSameEvents( - new FieldUpdated { Properties = properties, FieldId = stringId } - ); - } + events.ShouldHaveSameEvents( + new FieldLocked { FieldId = stringId } + ); + } - [Fact] - public void Should_create_events_if_nested_field_locked() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .LockField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_locked() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + [Fact] + public void Should_create_events_if_nested_field_hidden() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId } + ); + } - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .LockField(stringId.Id); + [Fact] + public void Should_create_events_if_field_hidden() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); - events.ShouldHaveSameEvents( - new FieldLocked { FieldId = stringId } - ); - } + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - [Fact] - public void Should_create_events_if_nested_field_hidden() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_hidden() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + events.ShouldHaveSameEvents( + new FieldHidden { FieldId = stringId } + ); + } - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); + [Fact] + public void Should_create_events_if_nested_field_shown() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new FieldShown { FieldId = nestedId, ParentFieldId = arrayId } + ); + } - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + [Fact] + public void Should_create_events_if_field_shown() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); - events.ShouldHaveSameEvents( - new FieldHidden { FieldId = stringId } - ); - } + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - [Fact] - public void Should_create_events_if_nested_field_shown() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new FieldShown { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_shown() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + events.ShouldHaveSameEvents( + new FieldShown { FieldId = stringId } + ); + } + + [Fact] + public void Should_create_events_if_nested_field_disabled() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .DisableField(nestedId.Id, arrayId.Id); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId } + ); + } - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + [Fact] + public void Should_create_events_if_field_disabled() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - events.ShouldHaveSameEvents( - new FieldShown { FieldId = stringId } - ); - } + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .DisableField(stringId.Id); - [Fact] - public void Should_create_events_if_nested_field_disabled() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .DisableField(nestedId.Id, arrayId.Id); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_disabled() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .DisableField(stringId.Id); + events.ShouldHaveSameEvents( + new FieldDisabled { FieldId = stringId } + ); + } - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + [Fact] + public void Should_create_events_if_nested_field_enabled() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .DisableField(nestedId.Id, arrayId.Id); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId } + ); + } - events.ShouldHaveSameEvents( - new FieldDisabled { FieldId = stringId } - ); - } + [Fact] + public void Should_create_events_if_field_enabled() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .DisableField(stringId.Id); - [Fact] - public void Should_create_events_if_nested_field_enabled() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .DisableField(nestedId.Id, arrayId.Id); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_field_enabled() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .DisableField(stringId.Id); + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + events.ShouldHaveSameEvents( + new FieldEnabled { FieldId = stringId } + ); + } - events.ShouldHaveSameEvents( - new FieldEnabled { FieldId = stringId } - ); - } + [Fact] + public void Should_create_events_if_field_created() + { + var sourceSchema = + new Schema("source"); - [Fact] - public void Should_create_events_if_field_created() - { - var sourceSchema = - new Schema("source"); + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) + .HideField(stringId.Id); - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant) - .HideField(stringId.Id); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var createdId = NamedId.Of(50L, stringId.Name); - var createdId = NamedId.Of(50L, stringId.Name); + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new FieldHidden { FieldId = createdId } + ); + } - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new FieldHidden { FieldId = createdId } - ); - } + [Fact] + public void Should_create_events_if_field_type_has_changed() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - [Fact] - public void Should_create_events_if_field_type_has_changed() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + var targetSchema = + new Schema("target") + .AddTags(stringId.Id, stringId.Name, Partitioning.Invariant); - var targetSchema = - new Schema("target") - .AddTags(stringId.Id, stringId.Name, Partitioning.Invariant); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var createdId = NamedId.Of(50L, stringId.Name); - var createdId = NamedId.Of(50L, stringId.Name); + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId }, + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new TagsFieldProperties() } + ); + } - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId }, - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new TagsFieldProperties() } - ); - } + [Fact] + public void Should_create_events_if_field_partitioning_has_changed() + { + var sourceSchema = + new Schema("source") + .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); - [Fact] - public void Should_create_events_if_field_partitioning_has_changed() - { - var sourceSchema = - new Schema("source") - .AddString(stringId.Id, stringId.Name, Partitioning.Invariant); + var targetSchema = + new Schema("target") + .AddString(stringId.Id, stringId.Name, Partitioning.Language); - var targetSchema = - new Schema("target") - .AddString(stringId.Id, stringId.Name, Partitioning.Language); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var createdId = NamedId.Of(50L, stringId.Name); - var createdId = NamedId.Of(50L, stringId.Name); + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = stringId }, + new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Language.Key, Properties = new StringFieldProperties() } + ); + } - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = stringId }, - new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Language.Key, Properties = new StringFieldProperties() } - ); - } + [Fact] + public void Should_create_events_if_nested_field_created() + { + var sourceSchema = + new Schema("source"); - [Fact] - public void Should_create_events_if_nested_field_created() - { - var sourceSchema = - new Schema("source"); + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(nestedId.Id, nestedId.Name)) + .HideField(nestedId.Id, arrayId.Id); - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(nestedId.Id, nestedId.Name)) - .HideField(nestedId.Id, arrayId.Id); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var id1 = NamedId.Of(50L, arrayId.Name); + var id2 = NamedId.Of(51L, stringId.Name); - var id1 = NamedId.Of(50L, arrayId.Name); - var id2 = NamedId.Of(51L, stringId.Name); + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = id1, Name = arrayId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new ArrayFieldProperties() }, + new FieldAdded { FieldId = id2, Name = stringId.Name, ParentFieldId = id1, Properties = new StringFieldProperties() }, + new FieldHidden { FieldId = id2, ParentFieldId = id1 } + ); + } - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = id1, Name = arrayId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new ArrayFieldProperties() }, - new FieldAdded { FieldId = id2, Name = stringId.Name, ParentFieldId = id1, Properties = new StringFieldProperties() }, - new FieldHidden { FieldId = id2, ParentFieldId = id1 } - ); - } + [Fact] + public void Should_create_events_if_nested_fields_reordered() + { + var sourceSchema = + new Schema("source") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(10, "f1") + .AddString(11, "f2")); + + var targetSchema = + new Schema("target") + .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f + .AddString(1, "f2") + .AddString(2, "f1")); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaFieldsReordered { FieldIds = new[] { 11L, 10L }, ParentFieldId = arrayId } + ); + } - [Fact] - public void Should_create_events_if_nested_fields_reordered() - { - var sourceSchema = - new Schema("source") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(10, "f1") - .AddString(11, "f2")); - - var targetSchema = - new Schema("target") - .AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f - .AddString(1, "f2") - .AddString(2, "f1")); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new SchemaFieldsReordered { FieldIds = new[] { 11L, 10L }, ParentFieldId = arrayId } - ); - } - - [Fact] - public void Should_create_events_if_fields_reordered() - { - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); + [Fact] + public void Should_create_events_if_fields_reordered() + { + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); - var targetSchema = - new Schema("target") - .AddString(1, "f2", Partitioning.Invariant) - .AddString(2, "f1", Partitioning.Invariant); + var targetSchema = + new Schema("target") + .AddString(1, "f2", Partitioning.Invariant) + .AddString(2, "f1", Partitioning.Invariant); - var events = sourceSchema.Synchronize(targetSchema, idGenerator); + var events = sourceSchema.Synchronize(targetSchema, idGenerator); - events.ShouldHaveSameEvents( - new SchemaFieldsReordered { FieldIds = new[] { 11L, 10L } } - ); - } + events.ShouldHaveSameEvents( + new SchemaFieldsReordered { FieldIds = new[] { 11L, 10L } } + ); + } - [Fact] - public void Should_create_events_if_fields_reordered_after_sync() - { - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f3", Partitioning.Invariant) - .AddString(2, "f1", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, - new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new[] { 50L, 10L } } - ); - } - - [Fact] - public void Should_create_events_if_fields_reordered_after_sync2() - { - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f1", Partitioning.Invariant) - .AddString(2, "f3", Partitioning.Invariant) - .AddString(3, "f2", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new[] { 10L, 50L, 11L } } - ); - } - - [Fact] - public void Should_create_events_if_field_renamed() - { - var sourceSchema = - new Schema("source") - .AddString(10, "f1", Partitioning.Invariant) - .AddString(11, "f2", Partitioning.Invariant); - - var targetSchema = - new Schema("target") - .AddString(1, "f3", Partitioning.Invariant) - .AddString(2, "f2", Partitioning.Invariant); - - var events = sourceSchema.Synchronize(targetSchema, idGenerator); - - events.ShouldHaveSameEvents( - new FieldDeleted { FieldId = NamedId.Of(10L, "f1") }, - new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new[] { 50L, 11L } } - ); - } + [Fact] + public void Should_create_events_if_fields_reordered_after_sync() + { + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f3", Partitioning.Invariant) + .AddString(2, "f1", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, + new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new SchemaFieldsReordered { FieldIds = new[] { 50L, 10L } } + ); + } + + [Fact] + public void Should_create_events_if_fields_reordered_after_sync2() + { + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f1", Partitioning.Invariant) + .AddString(2, "f3", Partitioning.Invariant) + .AddString(3, "f2", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new SchemaFieldsReordered { FieldIds = new[] { 10L, 50L, 11L } } + ); + } + + [Fact] + public void Should_create_events_if_field_renamed() + { + var sourceSchema = + new Schema("source") + .AddString(10, "f1", Partitioning.Invariant) + .AddString(11, "f2", Partitioning.Invariant); + + var targetSchema = + new Schema("target") + .AddString(1, "f3", Partitioning.Invariant) + .AddString(2, "f2", Partitioning.Invariant); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new FieldDeleted { FieldId = NamedId.Of(10L, "f1") }, + new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, + new SchemaFieldsReordered { FieldIds = new[] { 50L, 11L } } + ); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs index e4ec87e38a..577abda2cb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs @@ -13,105 +13,119 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds +namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds; + +public class ReferenceExtractionTests { - public class ReferenceExtractionTests - { - private readonly Schema schema; - private readonly ResolvedComponents components; + private readonly Schema schema; + private readonly ResolvedComponents components; - public ReferenceExtractionTests() - { - schema = - new Schema("my-schema") - .AddComponent(1, "component", Partitioning.Invariant) - .AddComponents(2, "components", Partitioning.Invariant) - .AddAssets(3, "assets1", Partitioning.Invariant) - .AddAssets(4, "assets2", Partitioning.Invariant) - .AddReferences(5, "references", Partitioning.Invariant) - .AddArray(6, "array", Partitioning.Invariant, a => a - .AddAssets(31, "nestedAssets") - .AddComponent(32, "nestedComponent") - .AddComponents(33, "nestedComponents")); - - components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [DomainId.Empty] = schema - }); - } - - [Fact] - public void Should_get_ids_from_name_data() + public ReferenceExtractionTests() + { + schema = + new Schema("my-schema") + .AddComponent(1, "component", Partitioning.Invariant) + .AddComponents(2, "components", Partitioning.Invariant) + .AddAssets(3, "assets1", Partitioning.Invariant) + .AddAssets(4, "assets2", Partitioning.Invariant) + .AddReferences(5, "references", Partitioning.Invariant) + .AddArray(6, "array", Partitioning.Invariant, a => a + .AddAssets(31, "nestedAssets") + .AddComponent(32, "nestedComponent") + .AddComponents(33, "nestedComponents")); + + components = new ResolvedComponents(new Dictionary<DomainId, Schema> { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + [DomainId.Empty] = schema + }); + } - var input = - new ContentData() - .AddField("assets1", - new ContentFieldData() - .AddInvariant(JsonValue.Array(id1.ToString(), id2.ToString()))); + [Fact] + public void Should_get_ids_from_name_data() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - var ids = new HashSet<DomainId>(); + var input = + new ContentData() + .AddField("assets1", + new ContentFieldData() + .AddInvariant(JsonValue.Array(id1.ToString(), id2.ToString()))); - input.AddReferencedIds(schema, ids, components); + var ids = new HashSet<DomainId>(); - Assert.Equal(new[] { id1, id2 }, ids); - } + input.AddReferencedIds(schema, ids, components); - [Fact] - public void Should_get_limited_ids_from_name_data() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + Assert.Equal(new[] { id1, id2 }, ids); + } - var input = - new ContentData() - .AddField("assets1", - new ContentFieldData() - .AddInvariant(JsonValue.Array(id1.ToString(), id2.ToString()))); + [Fact] + public void Should_get_limited_ids_from_name_data() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - var ids = new HashSet<DomainId>(); + var input = + new ContentData() + .AddField("assets1", + new ContentFieldData() + .AddInvariant(JsonValue.Array(id1.ToString(), id2.ToString()))); - input.AddReferencedIds(schema, ids, components, 1); + var ids = new HashSet<DomainId>(); - Assert.Equal(new[] { id1 }, ids); - } + input.AddReferencedIds(schema, ids, components, 1); - [Fact] - public void Should_cleanup_deleted_ids() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); - - var source = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(id1, id2))) - .AddField("assets1", - new ContentFieldData() - .AddInvariant(JsonValue.Array(id1))) - .AddField("array", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("nestedAssets", JsonValue.Array(id1, id2)) - .Add("nestedComponent", + Assert.Equal(new[] { id1 }, ids); + } + + [Fact] + public void Should_cleanup_deleted_ids() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); + + var source = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(id1, id2))) + .AddField("assets1", + new ContentFieldData() + .AddInvariant(JsonValue.Array(id1))) + .AddField("array", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject() + .Add("nestedAssets", JsonValue.Array(id1, id2)) + .Add("nestedComponent", + new JsonObject() + .Add("references", + JsonValue.Array(id1, id2)) + .Add(Component.Discriminator, DomainId.Empty)) + .Add("nestedComponents", + JsonValue.Array( new JsonObject() .Add("references", JsonValue.Array(id1, id2)) - .Add(Component.Discriminator, DomainId.Empty)) - .Add("nestedComponents", - JsonValue.Array( - new JsonObject() - .Add("references", - JsonValue.Array(id1, id2)) - .Add(Component.Discriminator, DomainId.Empty)))))) - .AddField("component", - new ContentFieldData() - .AddInvariant( + .Add(Component.Discriminator, DomainId.Empty)))))) + .AddField("component", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("references", + JsonValue.Array(id1, id2)) + .Add("assets1", + JsonValue.Array(id1)) + .Add("array", + JsonValue.Array( + new JsonObject() + .Add("nestedAssets", JsonValue.Array(id1, id2)))) + .Add(Component.Discriminator, DomainId.Empty))) + .AddField("components", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( new JsonObject() .Add("references", JsonValue.Array(id1, id2)) @@ -121,50 +135,50 @@ public void Should_cleanup_deleted_ids() JsonValue.Array( new JsonObject() .Add("nestedAssets", JsonValue.Array(id1, id2)))) - .Add(Component.Discriminator, DomainId.Empty))) - .AddField("components", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("references", - JsonValue.Array(id1, id2)) - .Add("assets1", - JsonValue.Array(id1)) - .Add("array", - JsonValue.Array( - new JsonObject() - .Add("nestedAssets", JsonValue.Array(id1, id2)))) - .Add(Component.Discriminator, DomainId.Empty)))); - - var expected = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(id2))) - .AddField("assets1", - new ContentFieldData() - .AddInvariant(new JsonArray())) - .AddField("array", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("nestedAssets", JsonValue.Array(id2)) - .Add("nestedComponent", + .Add(Component.Discriminator, DomainId.Empty)))); + + var expected = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(id2))) + .AddField("assets1", + new ContentFieldData() + .AddInvariant(new JsonArray())) + .AddField("array", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject() + .Add("nestedAssets", JsonValue.Array(id2)) + .Add("nestedComponent", + new JsonObject() + .Add("references", + JsonValue.Array(id2)) + .Add(Component.Discriminator, DomainId.Empty)) + .Add("nestedComponents", + JsonValue.Array( new JsonObject() .Add("references", JsonValue.Array(id2)) - .Add(Component.Discriminator, DomainId.Empty)) - .Add("nestedComponents", - JsonValue.Array( - new JsonObject() - .Add("references", - JsonValue.Array(id2)) - .Add(Component.Discriminator, DomainId.Empty)))))) - .AddField("component", - new ContentFieldData() - .AddInvariant( + .Add(Component.Discriminator, DomainId.Empty)))))) + .AddField("component", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("references", + JsonValue.Array(id2)) + .Add("assets1", + new JsonArray()) + .Add("array", + JsonValue.Array( + new JsonObject() + .Add("nestedAssets", JsonValue.Array(id2)))) + .Add(Component.Discriminator, DomainId.Empty))) + .AddField("components", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( new JsonObject() .Add("references", JsonValue.Array(id2)) @@ -174,152 +188,137 @@ public void Should_cleanup_deleted_ids() JsonValue.Array( new JsonObject() .Add("nestedAssets", JsonValue.Array(id2)))) - .Add(Component.Discriminator, DomainId.Empty))) - .AddField("components", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("references", - JsonValue.Array(id2)) - .Add("assets1", - new JsonArray()) - .Add("array", - JsonValue.Array( - new JsonObject() - .Add("nestedAssets", JsonValue.Array(id2)))) - .Add(Component.Discriminator, DomainId.Empty)))); - - var actual = - new ContentConverter(components, schema) - .Add(new ValueReferencesConverter(new HashSet<DomainId> { id2 })) - .Convert(source); - - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_return_empty_list_from_non_references_field() - { - var sut = Fields.String(1, "myString", Partitioning.Invariant); + .Add(Component.Discriminator, DomainId.Empty)))); - var actual = sut.GetReferencedIds(JsonValue.Create("invalid"), components).ToArray(); + var actual = + new ContentConverter(components, schema) + .Add(new ValueReferencesConverter(new HashSet<DomainId> { id2 })) + .Convert(source); - Assert.Empty(actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [MemberData(nameof(ReferencingNestedFields))] - public void Should_return_ids_from_nested_field(NestedField field) - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + [Fact] + public void Should_return_empty_list_from_non_references_field() + { + var sut = Fields.String(1, "myString", Partitioning.Invariant); - var arrayField = Fields.Array(1, "myArray", Partitioning.Invariant, null, null, field); + var actual = sut.GetReferencedIds(JsonValue.Create("invalid"), components).ToArray(); - var value = - JsonValue.Array( - new JsonObject() - .Add(field.Name, CreateValue(id1, id2))); + Assert.Empty(actual); + } - var actual = arrayField.GetReferencedIds(value, components).ToArray(); + [Theory] + [MemberData(nameof(ReferencingNestedFields))] + public void Should_return_ids_from_nested_field(NestedField field) + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - Assert.Equal(new[] { id1, id2 }, actual); - } + var arrayField = Fields.Array(1, "myArray", Partitioning.Invariant, null, null, field); - [Theory] - [MemberData(nameof(ReferencingFields))] - public void Should_return_empty_list_from_field_if_value_item_is_invalid(IField field) - { - var actual = field.GetReferencedIds(JsonValue.Array(1), components).ToArray(); + var value = + JsonValue.Array( + new JsonObject() + .Add(field.Name, CreateValue(id1, id2))); - Assert.Empty(actual); - } + var actual = arrayField.GetReferencedIds(value, components).ToArray(); - [Theory] - [MemberData(nameof(ReferencingFields))] - public void Should_return_empty_list_from_field_if_value_is_empty(IField field) - { - var actual = field.GetReferencedIds(new JsonArray(), components).ToArray(); + Assert.Equal(new[] { id1, id2 }, actual); + } - Assert.Empty(actual); - } + [Theory] + [MemberData(nameof(ReferencingFields))] + public void Should_return_empty_list_from_field_if_value_item_is_invalid(IField field) + { + var actual = field.GetReferencedIds(JsonValue.Array(1), components).ToArray(); - [Theory] - [MemberData(nameof(ReferencingFields))] - public void Should_return_empty_list_from_field_if_value_is_json_null(IField field) - { - var actual = field.GetReferencedIds(default, components).ToArray(); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Theory] + [MemberData(nameof(ReferencingFields))] + public void Should_return_empty_list_from_field_if_value_is_empty(IField field) + { + var actual = field.GetReferencedIds(new JsonArray(), components).ToArray(); - [Theory] - [MemberData(nameof(ReferencingFields))] - public void Should_return_empty_list_from_field_if_value_is_null(IField field) - { - var actual = field.GetReferencedIds(default, components).ToArray(); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Theory] + [MemberData(nameof(ReferencingFields))] + public void Should_return_empty_list_from_field_if_value_is_json_null(IField field) + { + var actual = field.GetReferencedIds(default, components).ToArray(); - [Theory] - [MemberData(nameof(ReferencingFields))] - public void Should_return_ids_from_field(IField field) - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + Assert.Empty(actual); + } - var value = CreateValue(id1, id2); + [Theory] + [MemberData(nameof(ReferencingFields))] + public void Should_return_empty_list_from_field_if_value_is_null(IField field) + { + var actual = field.GetReferencedIds(default, components).ToArray(); - var actual = field.GetReferencedIds(value, components); + Assert.Empty(actual); + } - Assert.Equal(new HashSet<DomainId> { id1, id2 }, actual); - } + [Theory] + [MemberData(nameof(ReferencingFields))] + public void Should_return_ids_from_field(IField field) + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - [Theory] - [MemberData(nameof(ReferencingFields))] - public void Should_return_same_value_from_field_if_value_is_json_null(IField field) - { - var (_, actual) = new ValueReferencesConverter(RandomIds()).ConvertValue(field, JsonValue.Null, null); + var value = CreateValue(id1, id2); - Assert.Equal(JsonValue.Null, actual); - } + var actual = field.GetReferencedIds(value, components); - [Theory] - [MemberData(nameof(ReferencingFields))] - public void Should_remove_deleted_ids_from_field(IField field) - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + Assert.Equal(new HashSet<DomainId> { id1, id2 }, actual); + } - var value = CreateValue(id1, id2); + [Theory] + [MemberData(nameof(ReferencingFields))] + public void Should_return_same_value_from_field_if_value_is_json_null(IField field) + { + var (_, actual) = new ValueReferencesConverter(RandomIds()).ConvertValue(field, JsonValue.Null, null); - var (_, actual) = new ValueReferencesConverter(HashSet.Of(id1)).ConvertValue(field, value, null); + Assert.Equal(JsonValue.Null, actual); + } - Assert.Equal(CreateValue(id1), actual); - } + [Theory] + [MemberData(nameof(ReferencingFields))] + public void Should_remove_deleted_ids_from_field(IField field) + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - public static IEnumerable<object[]> ReferencingNestedFields() - { - yield return new object[] { Fields.References(1, "myRefs") }; - yield return new object[] { Fields.Assets(1, "myAssets") }; - } + var value = CreateValue(id1, id2); - public static IEnumerable<object[]> ReferencingFields() - { - yield return new object[] { Fields.References(1, "myRefs", Partitioning.Invariant) }; - yield return new object[] { Fields.Assets(1, "myAssets", Partitioning.Invariant) }; - } + var (_, actual) = new ValueReferencesConverter(HashSet.Of(id1)).ConvertValue(field, value, null); - private static HashSet<DomainId> RandomIds() - { - return HashSet.Of(DomainId.NewGuid()); - } + Assert.Equal(CreateValue(id1), actual); + } - private static JsonValue CreateValue(params object[] ids) - { - return JsonValue.Array(ids); - } + public static IEnumerable<object[]> ReferencingNestedFields() + { + yield return new object[] { Fields.References(1, "myRefs") }; + yield return new object[] { Fields.Assets(1, "myAssets") }; + } + + public static IEnumerable<object[]> ReferencingFields() + { + yield return new object[] { Fields.References(1, "myRefs", Partitioning.Invariant) }; + yield return new object[] { Fields.Assets(1, "myAssets", Partitioning.Invariant) }; + } + + private static HashSet<DomainId> RandomIds() + { + return HashSet.Of(DomainId.NewGuid()); + } + + private static JsonValue CreateValue(params object[] ids) + { + return JsonValue.Array(ids); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs index 3591c73f3b..63da258d51 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceFormattingTests.cs @@ -13,91 +13,90 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds +namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds; + +public class ReferenceFormattingTests { - public class ReferenceFormattingTests + private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); + + [Fact] + public void Should_format_data_with_reference_fields() + { + var data = CreateData(); + + var schema = + new Schema("my-schema") + .AddString(1, "ref1", Partitioning.Invariant, + new StringFieldProperties()) + .AddString(2, "ref2", Partitioning.Invariant, + new StringFieldProperties()) + .AddString(3, "non-ref", Partitioning.Invariant) + .SetFieldsInReferences("ref1", "ref2"); + + var formatted = data.FormatReferences(schema, languages); + + var expected = + new JsonObject() + .Add("en", "EN, 12") + .Add("de", "DE, 12"); + + Assert.Equal(expected, formatted); + } + + [Fact] + public void Should_format_data_with_first_field_if_no_reference_field_defined() + { + var data = CreateData(); + + var schema = CreateNoRefSchema(); + + var formatted = data.FormatReferences(schema, languages); + + var expected = + new JsonObject() + .Add("en", "EN") + .Add("de", "DE"); + + Assert.Equal(expected, formatted); + } + + [Fact] + public void Should_return_default_value_if_no_value_found() + { + var data = new ContentData(); + + var schema = CreateNoRefSchema(); + + var formatted = data.FormatReferences(schema, languages); + + var expected = + new JsonObject() + .Add("en", string.Empty) + .Add("de", string.Empty); + + Assert.Equal(expected, formatted); + } + + private static Schema CreateNoRefSchema() + { + return new Schema("my-schema") + .AddString(1, "ref1", Partitioning.Invariant) + .AddString(2, "ref2", Partitioning.Invariant) + .AddString(3, "non-ref", Partitioning.Invariant); + } + + private static ContentData CreateData() { - private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); - - [Fact] - public void Should_format_data_with_reference_fields() - { - var data = CreateData(); - - var schema = - new Schema("my-schema") - .AddString(1, "ref1", Partitioning.Invariant, - new StringFieldProperties()) - .AddString(2, "ref2", Partitioning.Invariant, - new StringFieldProperties()) - .AddString(3, "non-ref", Partitioning.Invariant) - .SetFieldsInReferences("ref1", "ref2"); - - var formatted = data.FormatReferences(schema, languages); - - var expected = - new JsonObject() - .Add("en", "EN, 12") - .Add("de", "DE, 12"); - - Assert.Equal(expected, formatted); - } - - [Fact] - public void Should_format_data_with_first_field_if_no_reference_field_defined() - { - var data = CreateData(); - - var schema = CreateNoRefSchema(); - - var formatted = data.FormatReferences(schema, languages); - - var expected = - new JsonObject() - .Add("en", "EN") - .Add("de", "DE"); - - Assert.Equal(expected, formatted); - } - - [Fact] - public void Should_return_default_value_if_no_value_found() - { - var data = new ContentData(); - - var schema = CreateNoRefSchema(); - - var formatted = data.FormatReferences(schema, languages); - - var expected = - new JsonObject() - .Add("en", string.Empty) - .Add("de", string.Empty); - - Assert.Equal(expected, formatted); - } - - private static Schema CreateNoRefSchema() - { - return new Schema("my-schema") - .AddString(1, "ref1", Partitioning.Invariant) - .AddString(2, "ref2", Partitioning.Invariant) - .AddString(3, "non-ref", Partitioning.Invariant); - } - - private static ContentData CreateData() - { - return new ContentData() - .AddField("ref1", - new ContentFieldData() - .AddLocalized("en", "EN") - .AddLocalized("de", "DE")) - .AddField("ref2", - new ContentFieldData() - .AddInvariant(12)) - .AddField("non-ref", - new ContentFieldData() - .AddInvariant("Ignored")); - } + return new ContentData() + .AddField("ref1", + new ContentFieldData() + .AddLocalized("en", "EN") + .AddLocalized("de", "DE")) + .AddField("ref2", + new ContentFieldData() + .AddInvariant(12)) + .AddField("non-ref", + new ContentFieldData() + .AddInvariant("Ignored")); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/StringReferenceExtractorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/StringReferenceExtractorTests.cs index 604f49d002..13bb3d109b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/StringReferenceExtractorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/StringReferenceExtractorTests.cs @@ -10,55 +10,54 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds +namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds; + +public class StringReferenceExtractorTests { - public class StringReferenceExtractorTests - { - private readonly StringReferenceExtractor sut; + private readonly StringReferenceExtractor sut; - public StringReferenceExtractorTests() - { - var urlGenerator = A.Fake<IUrlGenerator>(); + public StringReferenceExtractorTests() + { + var urlGenerator = A.Fake<IUrlGenerator>(); - A.CallTo(() => urlGenerator.ContentBase()) - .Returns("https://cloud.squidex.io/api/content/"); + A.CallTo(() => urlGenerator.ContentBase()) + .Returns("https://cloud.squidex.io/api/content/"); - A.CallTo(() => urlGenerator.ContentCDNBase()) - .Returns("https://contents.squidex.io/"); + A.CallTo(() => urlGenerator.ContentCDNBase()) + .Returns("https://contents.squidex.io/"); - A.CallTo(() => urlGenerator.AssetContentBase()) - .Returns("https://cloud.squidex.io/api/assets/"); + A.CallTo(() => urlGenerator.AssetContentBase()) + .Returns("https://cloud.squidex.io/api/assets/"); - A.CallTo(() => urlGenerator.AssetContentCDNBase()) - .Returns("https://assets.squidex.io/"); + A.CallTo(() => urlGenerator.AssetContentCDNBase()) + .Returns("https://assets.squidex.io/"); - sut = new StringReferenceExtractor(urlGenerator); - } + sut = new StringReferenceExtractor(urlGenerator); + } - [Theory] - [InlineData("before content:a_b-123|after")] - [InlineData("before contents:a_b-123|after")] - [InlineData("before https://cloud.squidex.io/api/content/my-app/my-schema/a_b-123|after")] - [InlineData("before https://contents.squidex.io/my-app/my-schema/a_b-123|after")] - public void Should_extract_content_id(string input) - { - var ids = sut.GetEmbeddedContentIds(input); + [Theory] + [InlineData("before content:a_b-123|after")] + [InlineData("before contents:a_b-123|after")] + [InlineData("before https://cloud.squidex.io/api/content/my-app/my-schema/a_b-123|after")] + [InlineData("before https://contents.squidex.io/my-app/my-schema/a_b-123|after")] + public void Should_extract_content_id(string input) + { + var ids = sut.GetEmbeddedContentIds(input); - Assert.Contains(DomainId.Create("a_b-123"), ids.ToList()); - } + Assert.Contains(DomainId.Create("a_b-123"), ids.ToList()); + } - [Theory] - [InlineData("before asset:a_b-123|after")] - [InlineData("before assets:a_b-123|after")] - [InlineData("before https://cloud.squidex.io/api/assets/a_b-123|after")] - [InlineData("before https://cloud.squidex.io/api/assets/my-app/a_b-123|after")] - [InlineData("before https://assets.squidex.io/a_b-123|after")] - [InlineData("before https://assets.squidex.io/my-app/a_b-123|after")] - public void Should_extract_asset_id(string input) - { - var ids = sut.GetEmbeddedAssetIds(input); + [Theory] + [InlineData("before asset:a_b-123|after")] + [InlineData("before assets:a_b-123|after")] + [InlineData("before https://cloud.squidex.io/api/assets/a_b-123|after")] + [InlineData("before https://cloud.squidex.io/api/assets/my-app/a_b-123|after")] + [InlineData("before https://assets.squidex.io/a_b-123|after")] + [InlineData("before https://assets.squidex.io/my-app/a_b-123|after")] + public void Should_extract_asset_id(string input) + { + var ids = sut.GetEmbeddedAssetIds(input); - Assert.Contains(DomainId.Create("a_b-123"), ids.ToList()); - } + Assert.Contains(DomainId.Create("a_b-123"), ids.ToList()); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs index dd88c33254..c9fdec6209 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs @@ -13,92 +13,91 @@ using Squidex.Infrastructure.Queries; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.GenerateFilters +namespace Squidex.Domain.Apps.Core.Operations.GenerateFilters; + +public class FiltersTests { - public class FiltersTests + [Fact] + public void Should_build_content_query_model() { - [Fact] - public void Should_build_content_query_model() - { - var languagesConfig = LanguagesConfig.English.Set(Language.DE); + var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var (schema, components) = TestSchema.MixedSchema(); + var (schema, components) = TestSchema.MixedSchema(); - var queryModel = ContentQueryModel.Build(schema, languagesConfig.ToResolver(), components); + var queryModel = ContentQueryModel.Build(schema, languagesConfig.ToResolver(), components); - CheckFields(queryModel.Schema, schema); - } + CheckFields(queryModel.Schema, schema); + } - [Fact] - public void Should_build_dynamic_content_query_model() - { - var languagesConfig = LanguagesConfig.English.Set(Language.DE); + [Fact] + public void Should_build_dynamic_content_query_model() + { + var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var queryModel = ContentQueryModel.Build(null, languagesConfig.ToResolver(), ResolvedComponents.Empty); + var queryModel = ContentQueryModel.Build(null, languagesConfig.ToResolver(), ResolvedComponents.Empty); - Assert.NotNull(queryModel); - } + Assert.NotNull(queryModel); + } - [Fact] - public void Should_build_asset_query_model() - { - var queryModel = AssetQueryModel.Build(); + [Fact] + public void Should_build_asset_query_model() + { + var queryModel = AssetQueryModel.Build(); - Assert.NotNull(queryModel); - } + Assert.NotNull(queryModel); + } - private static void CheckFields(FilterSchema filterSchema, Schema schema) - { - var filterProperties = AllPropertyNames(filterSchema); + private static void CheckFields(FilterSchema filterSchema, Schema schema) + { + var filterProperties = AllPropertyNames(filterSchema); - void CheckField(IField field) + void CheckField(IField field) + { + if (!field.IsForApi(true)) { - if (!field.IsForApi(true)) - { - Assert.DoesNotContain(field.Name, filterProperties); - } - else - { - Assert.Contains(field.Name, filterProperties); - } - - if (field is IArrayField array) - { - foreach (var nested in array.Fields) - { - CheckField(nested); - } - } + Assert.DoesNotContain(field.Name, filterProperties); + } + else + { + Assert.Contains(field.Name, filterProperties); } - foreach (var field in schema.Fields) + if (field is IArrayField array) { - CheckField(field); + foreach (var nested in array.Fields) + { + CheckField(nested); + } } } - private static HashSet<string> AllPropertyNames(FilterSchema schema) + foreach (var field in schema.Fields) { - var actual = new HashSet<string>(); + CheckField(field); + } + } + + private static HashSet<string> AllPropertyNames(FilterSchema schema) + { + var actual = new HashSet<string>(); - void AddProperties(FilterSchema current) + void AddProperties(FilterSchema current) + { + if (current == null) { - if (current == null) - { - return; - } + return; + } - foreach (var field in current.Fields.OrEmpty()) - { - actual.Add(field.Path); + foreach (var field in current.Fields.OrEmpty()) + { + actual.Add(field.Path); - AddProperties(field.Schema); - } + AddProperties(field.Schema); } + } - AddProperties(schema); + AddProperties(schema); - return actual; - } + return actual; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs index b857a3ce2a..a18090fd80 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -13,103 +13,102 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema +namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema; + +public class JsonSchemaTests { - public class JsonSchemaTests + [Fact] + public void Should_build_json_schema() { - [Fact] - public void Should_build_json_schema() - { - var languagesConfig = LanguagesConfig.English.Set(Language.DE); + var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var (schema, components) = TestSchema.MixedSchema(); + var (schema, components) = TestSchema.MixedSchema(); - var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), components); + var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), components); - CheckFields(jsonSchema, schema); - } + CheckFields(jsonSchema, schema); + } - [Fact] - public void Should_build_json_dynamic_json_schema() - { - var languagesConfig = LanguagesConfig.English.Set(Language.DE); + [Fact] + public void Should_build_json_dynamic_json_schema() + { + var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var (schema, components) = TestSchema.MixedSchema(); + var (schema, components) = TestSchema.MixedSchema(); - var jsonSchema = schema.BuildJsonSchemaDynamic(languagesConfig.ToResolver(), components); + var jsonSchema = schema.BuildJsonSchemaDynamic(languagesConfig.ToResolver(), components); - CheckFields(jsonSchema, schema); - } + CheckFields(jsonSchema, schema); + } - [Fact] - public void Should_build_flat_json_schema() - { - var languagesConfig = LanguagesConfig.English.Set(Language.DE); + [Fact] + public void Should_build_flat_json_schema() + { + var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var (schema, components) = TestSchema.MixedSchema(); + var (schema, components) = TestSchema.MixedSchema(); - var jsonSchema = schema.BuildJsonSchemaFlat(languagesConfig.ToResolver(), components); + var jsonSchema = schema.BuildJsonSchemaFlat(languagesConfig.ToResolver(), components); - CheckFields(jsonSchema, schema); - } + CheckFields(jsonSchema, schema); + } - private static void CheckFields(JsonSchema jsonSchema, Schema schema) - { - var jsonProperties = AllPropertyNames(jsonSchema); + private static void CheckFields(JsonSchema jsonSchema, Schema schema) + { + var jsonProperties = AllPropertyNames(jsonSchema); - void CheckField(IField field) + void CheckField(IField field) + { + if (!field.IsForApi()) { - if (!field.IsForApi()) - { - Assert.DoesNotContain(field.Name, jsonProperties); - } - else - { - Assert.Contains(field.Name, jsonProperties); - } - - if (field is IArrayField array) - { - foreach (var nested in array.Fields) - { - CheckField(nested); - } - } + Assert.DoesNotContain(field.Name, jsonProperties); } - - foreach (var field in schema.Fields) + else { - CheckField(field); + Assert.Contains(field.Name, jsonProperties); } - } - - private static HashSet<string> AllPropertyNames(JsonSchema schema) - { - var actual = new HashSet<string>(); - void AddProperties(JsonSchema current) + if (field is IArrayField array) { - if (current == null) + foreach (var nested in array.Fields) { - return; + CheckField(nested); } + } + } - foreach (var (key, value) in current.Properties.OrEmpty()) - { - actual.Add(key); + foreach (var field in schema.Fields) + { + CheckField(field); + } + } - AddProperties(value); - } + private static HashSet<string> AllPropertyNames(JsonSchema schema) + { + var actual = new HashSet<string>(); - AddProperties(current.Item); - AddProperties(current.Reference); - AddProperties(current.AdditionalItemsSchema); - AddProperties(current.AdditionalPropertiesSchema); + void AddProperties(JsonSchema current) + { + if (current == null) + { + return; } - AddProperties(schema); + foreach (var (key, value) in current.Properties.OrEmpty()) + { + actual.Add(key); + + AddProperties(value); + } - return actual; + AddProperties(current.Item); + AddProperties(current.Reference); + AddProperties(current.AdditionalItemsSchema); + AddProperties(current.AdditionalPropertiesSchema); } + + AddProperties(schema); + + return actual; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs index 1a12740b67..3d7caf59f4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs @@ -18,134 +18,133 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.HandleRules +namespace Squidex.Domain.Apps.Core.Operations.HandleRules; + +public class EventEnricherTests { - public class EventEnricherTests - { - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly EventEnricher sut; + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly EventEnricher sut; - public EventEnricherTests() - { - var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + public EventEnricherTests() + { + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - sut = new EventEnricher(cache, userResolver); - } + sut = new EventEnricher(cache, userResolver); + } - [Fact] - public async Task Should_enrich_with_timestamp() - { - var timestamp = SystemClock.Instance.GetCurrentInstant().WithoutMs(); + [Fact] + public async Task Should_enrich_with_timestamp() + { + var timestamp = SystemClock.Instance.GetCurrentInstant().WithoutMs(); - var @event = - Envelope.Create<AppEvent>(new ContentCreated()) - .SetTimestamp(timestamp); + var @event = + Envelope.Create<AppEvent>(new ContentCreated()) + .SetTimestamp(timestamp); - var enrichedEvent = new EnrichedContentEvent(); + var enrichedEvent = new EnrichedContentEvent(); - await sut.EnrichAsync(enrichedEvent, @event); + await sut.EnrichAsync(enrichedEvent, @event); - Assert.Equal(timestamp, enrichedEvent.Timestamp); - } + Assert.Equal(timestamp, enrichedEvent.Timestamp); + } - [Fact] - public async Task Should_enrich_with_appId() - { - var appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + [Fact] + public async Task Should_enrich_with_appId() + { + var appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - var @event = - Envelope.Create<AppEvent>(new ContentCreated - { - AppId = appId - }); + var @event = + Envelope.Create<AppEvent>(new ContentCreated + { + AppId = appId + }); - var enrichedEvent = new EnrichedContentEvent(); + var enrichedEvent = new EnrichedContentEvent(); - await sut.EnrichAsync(enrichedEvent, @event); + await sut.EnrichAsync(enrichedEvent, @event); - Assert.Equal(appId, enrichedEvent.AppId); - } + Assert.Equal(appId, enrichedEvent.AppId); + } - [Fact] - public async Task Should_not_enrich_with_user_if_token_is_null() - { - RefToken actor = null!; + [Fact] + public async Task Should_not_enrich_with_user_if_token_is_null() + { + RefToken actor = null!; - var @event = - Envelope.Create<AppEvent>(new ContentCreated - { - Actor = actor - }); + var @event = + Envelope.Create<AppEvent>(new ContentCreated + { + Actor = actor + }); - var enrichedEvent = new EnrichedContentEvent(); + var enrichedEvent = new EnrichedContentEvent(); - await sut.EnrichAsync(enrichedEvent, @event); + await sut.EnrichAsync(enrichedEvent, @event); - Assert.Null(enrichedEvent.User); + Assert.Null(enrichedEvent.User); - A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_enrich_with_user() - { - var actor = RefToken.Client("me"); + [Fact] + public async Task Should_enrich_with_user() + { + var actor = RefToken.Client("me"); - var user = A.Dummy<IUser>(); + var user = A.Dummy<IUser>(); - A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier, default)) - .Returns(user); + A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier, default)) + .Returns(user); - var @event = - Envelope.Create<AppEvent>(new ContentCreated - { - Actor = actor - }); + var @event = + Envelope.Create<AppEvent>(new ContentCreated + { + Actor = actor + }); - var enrichedEvent = new EnrichedContentEvent(); + var enrichedEvent = new EnrichedContentEvent(); - await sut.EnrichAsync(enrichedEvent, @event); + await sut.EnrichAsync(enrichedEvent, @event); - Assert.Equal(user, enrichedEvent.User); + Assert.Equal(user, enrichedEvent.User); - A.CallTo(() => userResolver.FindByIdAsync(A<string>._, default)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => userResolver.FindByIdAsync(A<string>._, default)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_enrich_with_user_and_cache() - { - var actor = RefToken.Client("me"); + [Fact] + public async Task Should_enrich_with_user_and_cache() + { + var actor = RefToken.Client("me"); - var user = A.Dummy<IUser>(); + var user = A.Dummy<IUser>(); - A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier, default)) - .Returns(user); + A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier, default)) + .Returns(user); - var event1 = - Envelope.Create<AppEvent>(new ContentCreated - { - Actor = actor - }); + var event1 = + Envelope.Create<AppEvent>(new ContentCreated + { + Actor = actor + }); - var event2 = - Envelope.Create<AppEvent>(new ContentCreated - { - Actor = actor - }); + var event2 = + Envelope.Create<AppEvent>(new ContentCreated + { + Actor = actor + }); - var enrichedEvent1 = new EnrichedContentEvent(); - var enrichedEvent2 = new EnrichedContentEvent(); + var enrichedEvent1 = new EnrichedContentEvent(); + var enrichedEvent2 = new EnrichedContentEvent(); - await sut.EnrichAsync(enrichedEvent1, event1); - await sut.EnrichAsync(enrichedEvent2, event2); + await sut.EnrichAsync(enrichedEvent1, event1); + await sut.EnrichAsync(enrichedEvent2, event2); - Assert.Equal(user, enrichedEvent1.User); - Assert.Equal(user, enrichedEvent2.User); + Assert.Equal(user, enrichedEvent1.User); + Assert.Equal(user, enrichedEvent2.User); - A.CallTo(() => userResolver.FindByIdAsync(A<string>._, default)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => userResolver.FindByIdAsync(A<string>._, default)) + .MustHaveHappenedOnceExactly(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventJsonSchemaGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventJsonSchemaGeneratorTests.cs index 019980ea85..9158aeea02 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventJsonSchemaGeneratorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventJsonSchemaGeneratorTests.cs @@ -10,59 +10,58 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.HandleRules +namespace Squidex.Domain.Apps.Core.Operations.HandleRules; + +public class EventJsonSchemaGeneratorTests { - public class EventJsonSchemaGeneratorTests - { - private readonly EventJsonSchemaGenerator sut; + private readonly EventJsonSchemaGenerator sut; - public EventJsonSchemaGeneratorTests() - { - var jsonSchemaGenerator = - new JsonSchemaGenerator( - new JsonSchemaGeneratorSettings()); + public EventJsonSchemaGeneratorTests() + { + var jsonSchemaGenerator = + new JsonSchemaGenerator( + new JsonSchemaGeneratorSettings()); - sut = new EventJsonSchemaGenerator(jsonSchemaGenerator); - } + sut = new EventJsonSchemaGenerator(jsonSchemaGenerator); + } - public static IEnumerable<string> AllTypes() - { - yield return nameof(EnrichedAssetEvent); - yield return nameof(EnrichedCommentEvent); - yield return nameof(EnrichedContentEvent); - yield return nameof(EnrichedManualEvent); - yield return nameof(EnrichedSchemaEvent); - yield return nameof(EnrichedUsageExceededEvent); - } + public static IEnumerable<string> AllTypes() + { + yield return nameof(EnrichedAssetEvent); + yield return nameof(EnrichedCommentEvent); + yield return nameof(EnrichedContentEvent); + yield return nameof(EnrichedManualEvent); + yield return nameof(EnrichedSchemaEvent); + yield return nameof(EnrichedUsageExceededEvent); + } - public static IEnumerable<object[]> AllTypesData() - { - return AllTypes().Select(x => new object[] { x }); - } + public static IEnumerable<object[]> AllTypesData() + { + return AllTypes().Select(x => new object[] { x }); + } - [Fact] - public void Should_return_null_for_unknown_type_name() - { - var schema = sut.GetSchema("Unknown"); + [Fact] + public void Should_return_null_for_unknown_type_name() + { + var schema = sut.GetSchema("Unknown"); - Assert.Null(schema); - } + Assert.Null(schema); + } - [Fact] - public void Should_provide_all_types() - { - var allTypes = sut.AllTypes; + [Fact] + public void Should_provide_all_types() + { + var allTypes = sut.AllTypes; - Assert.Equal(AllTypes().ToList(), allTypes); - } + Assert.Equal(AllTypes().ToList(), allTypes); + } - [Theory] - [MemberData(nameof(AllTypesData))] - public void Should_generate_json_schema_for_known_event(string typeName) - { - var schema = sut.GetSchema(typeName); + [Theory] + [MemberData(nameof(AllTypesData))] + public void Should_generate_json_schema_for_known_event(string typeName) + { + var schema = sut.GetSchema(typeName); - Assert.NotNull(schema); - } + Assert.NotNull(schema); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/ExpressionsAttribute.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/ExpressionsAttribute.cs index 79e0061a69..42bcafcbf0 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/ExpressionsAttribute.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/ExpressionsAttribute.cs @@ -8,52 +8,51 @@ using System.Reflection; using Xunit.Sdk; -namespace Squidex.Domain.Apps.Core.Operations.HandleRules +namespace Squidex.Domain.Apps.Core.Operations.HandleRules; + +public sealed class ExpressionsAttribute : DataAttribute { - public sealed class ExpressionsAttribute : DataAttribute + private readonly string? script; + private readonly string? interpolationOld; + private readonly string? interpolationNew; + private readonly string? liquid; + + public ExpressionsAttribute(string? interpolationOld, string? interpolationNew, string? script, string? liquid) { - private readonly string? script; - private readonly string? interpolationOld; - private readonly string? interpolationNew; - private readonly string? liquid; + this.liquid = liquid; - public ExpressionsAttribute(string? interpolationOld, string? interpolationNew, string? script, string? liquid) - { - this.liquid = liquid; + this.interpolationOld = interpolationOld; + this.interpolationNew = interpolationNew; - this.interpolationOld = interpolationOld; - this.interpolationNew = interpolationNew; + this.script = script; + } - this.script = script; + public override IEnumerable<object[]> GetData(MethodInfo testMethod) + { + if (interpolationOld != null) + { + yield return new object[] { interpolationOld }; } - public override IEnumerable<object[]> GetData(MethodInfo testMethod) + if (interpolationNew != null) { - if (interpolationOld != null) - { - yield return new object[] { interpolationOld }; - } - - if (interpolationNew != null) - { - yield return new object[] { interpolationNew }; - } + yield return new object[] { interpolationNew }; + } - if (script != null) + if (script != null) + { + yield return new object[] { - yield return new object[] - { - $"Script(`{script}`)" - }; - } + $"Script(`{script}`)" + }; + } - if (liquid != null) + if (liquid != null) + { + yield return new object[] { - yield return new object[] - { - $"Liquid({liquid})" - }; - } + $"Liquid({liquid})" + }; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs index 11f5d9287e..b56ff1dbf9 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleElementRegistryTests.cs @@ -12,193 +12,192 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.HandleRules +namespace Squidex.Domain.Apps.Core.Operations.HandleRules; + +public class RuleElementRegistryTests { - public class RuleElementRegistryTests - { - private readonly RuleTypeProvider sut = new RuleTypeProvider(); + private readonly RuleTypeProvider sut = new RuleTypeProvider(); - private abstract class MyRuleActionHandler : RuleActionHandler<MyRuleAction, string> + private abstract class MyRuleActionHandler : RuleActionHandler<MyRuleAction, string> + { + protected MyRuleActionHandler(RuleEventFormatter formatter) + : base(formatter) { - protected MyRuleActionHandler(RuleEventFormatter formatter) - : base(formatter) - { - } } + } - public enum ActionEnum - { - Yes, - No - } + public enum ActionEnum + { + Yes, + No + } - [RuleAction( - Title = "Action", - IconImage = "<svg></svg>", - IconColor = "#1e5470", - Display = "Action display", - Description = "Action description.", - ReadMore = "https://www.readmore.com/")] - public sealed record MyRuleAction : RuleAction - { - [LocalizedRequired] - [Display(Name = "Url Name", Description = "Url Description")] - [Editor(RuleFieldEditor.Url)] - [Formattable] - public Uri Url { get; set; } + [RuleAction( + Title = "Action", + IconImage = "<svg></svg>", + IconColor = "#1e5470", + Display = "Action display", + Description = "Action description.", + ReadMore = "https://www.readmore.com/")] + public sealed record MyRuleAction : RuleAction + { + [LocalizedRequired] + [Display(Name = "Url Name", Description = "Url Description")] + [Editor(RuleFieldEditor.Url)] + [Formattable] + public Uri Url { get; set; } - [Editor(RuleFieldEditor.Javascript)] - public string Script { get; set; } + [Editor(RuleFieldEditor.Javascript)] + public string Script { get; set; } - [Editor(RuleFieldEditor.Text)] - public string Text { get; set; } + [Editor(RuleFieldEditor.Text)] + public string Text { get; set; } - [Editor(RuleFieldEditor.TextArea)] - public string TextMultiline { get; set; } + [Editor(RuleFieldEditor.TextArea)] + public string TextMultiline { get; set; } - [Editor(RuleFieldEditor.Password)] - public string Password { get; set; } + [Editor(RuleFieldEditor.Password)] + public string Password { get; set; } - [Editor(RuleFieldEditor.Text)] - public ActionEnum Enum { get; set; } + [Editor(RuleFieldEditor.Text)] + public ActionEnum Enum { get; set; } - [Editor(RuleFieldEditor.Text)] - public ActionEnum? EnumOptional { get; set; } + [Editor(RuleFieldEditor.Text)] + public ActionEnum? EnumOptional { get; set; } - [Editor(RuleFieldEditor.Text)] - public bool Boolean { get; set; } + [Editor(RuleFieldEditor.Text)] + public bool Boolean { get; set; } - [Editor(RuleFieldEditor.Text)] - public bool? BooleanOptional { get; set; } + [Editor(RuleFieldEditor.Text)] + public bool? BooleanOptional { get; set; } - [Editor(RuleFieldEditor.Text)] - public int Number { get; set; } + [Editor(RuleFieldEditor.Text)] + public int Number { get; set; } - [Editor(RuleFieldEditor.Text)] - public int? NumberOptional { get; set; } - } + [Editor(RuleFieldEditor.Text)] + public int? NumberOptional { get; set; } + } - [Fact] - public void Should_create_definition() + [Fact] + public void Should_create_definition() + { + var expected = new RuleActionDefinition { - var expected = new RuleActionDefinition - { - Type = typeof(MyRuleAction), - Title = "Action", - IconImage = "<svg></svg>", - IconColor = "#1e5470", - Display = "Action display", - Description = "Action description.", - ReadMore = "https://www.readmore.com/" - }; - - expected.Properties.Add(new RuleActionProperty - { - Name = "url", - Display = "Url Name", - Description = "Url Description", - Editor = RuleFieldEditor.Url, - IsFormattable = true, - IsRequired = true - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "script", - Display = "Script", - Description = null, - Editor = RuleFieldEditor.Javascript, - IsRequired = false - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "text", - Display = "Text", - Description = null, - Editor = RuleFieldEditor.Text, - IsRequired = false - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "textMultiline", - Display = "TextMultiline", - Description = null, - Editor = RuleFieldEditor.TextArea, - IsRequired = false - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "password", - Display = "Password", - Description = null, - Editor = RuleFieldEditor.Password, - IsRequired = false - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "enum", - Display = "Enum", - Description = null, - Editor = RuleFieldEditor.Dropdown, - IsRequired = false, - Options = new[] { "Yes", "No" } - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "enumOptional", - Display = "EnumOptional", - Description = null, - Editor = RuleFieldEditor.Dropdown, - IsRequired = false, - Options = new[] { "Yes", "No" } - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "boolean", - Display = "Boolean", - Description = null, - Editor = RuleFieldEditor.Checkbox, - IsRequired = false - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "booleanOptional", - Display = "BooleanOptional", - Description = null, - Editor = RuleFieldEditor.Checkbox, - IsRequired = false - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "number", - Display = "Number", - Description = null, - Editor = RuleFieldEditor.Number, - IsRequired = true - }); - - expected.Properties.Add(new RuleActionProperty - { - Name = "numberOptional", - Display = "NumberOptional", - Description = null, - Editor = RuleFieldEditor.Number, - IsRequired = false - }); - - sut.Add<MyRuleAction>(); - - var currentDefinition = sut.Actions.Values.First(); - - currentDefinition.Should().BeEquivalentTo(expected); - } + Type = typeof(MyRuleAction), + Title = "Action", + IconImage = "<svg></svg>", + IconColor = "#1e5470", + Display = "Action display", + Description = "Action description.", + ReadMore = "https://www.readmore.com/" + }; + + expected.Properties.Add(new RuleActionProperty + { + Name = "url", + Display = "Url Name", + Description = "Url Description", + Editor = RuleFieldEditor.Url, + IsFormattable = true, + IsRequired = true + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "script", + Display = "Script", + Description = null, + Editor = RuleFieldEditor.Javascript, + IsRequired = false + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "text", + Display = "Text", + Description = null, + Editor = RuleFieldEditor.Text, + IsRequired = false + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "textMultiline", + Display = "TextMultiline", + Description = null, + Editor = RuleFieldEditor.TextArea, + IsRequired = false + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "password", + Display = "Password", + Description = null, + Editor = RuleFieldEditor.Password, + IsRequired = false + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "enum", + Display = "Enum", + Description = null, + Editor = RuleFieldEditor.Dropdown, + IsRequired = false, + Options = new[] { "Yes", "No" } + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "enumOptional", + Display = "EnumOptional", + Description = null, + Editor = RuleFieldEditor.Dropdown, + IsRequired = false, + Options = new[] { "Yes", "No" } + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "boolean", + Display = "Boolean", + Description = null, + Editor = RuleFieldEditor.Checkbox, + IsRequired = false + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "booleanOptional", + Display = "BooleanOptional", + Description = null, + Editor = RuleFieldEditor.Checkbox, + IsRequired = false + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "number", + Display = "Number", + Description = null, + Editor = RuleFieldEditor.Number, + IsRequired = true + }); + + expected.Properties.Add(new RuleActionProperty + { + Name = "numberOptional", + Display = "NumberOptional", + Description = null, + Editor = RuleFieldEditor.Number, + IsRequired = false + }); + + sut.Add<MyRuleAction>(); + + var currentDefinition = sut.Actions.Values.First(); + + currentDefinition.Should().BeEquivalentTo(expected); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs index 81f14d55f9..4a8958b5f8 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs @@ -28,914 +28,913 @@ #pragma warning disable CA2012 // Use ValueTasks correctly -namespace Squidex.Domain.Apps.Core.Operations.HandleRules +namespace Squidex.Domain.Apps.Core.Operations.HandleRules; + +public class RuleEventFormatterCompareTests { - public class RuleEventFormatterCompareTests - { - private readonly IUser user = UserMocks.User("user123", "me@email.com", "me"); - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - private readonly DomainId contentId = DomainId.NewGuid(); - private readonly DomainId assetId = DomainId.NewGuid(); - private readonly RuleEventFormatter sut; - - private sealed class FakeContentResolver : IRuleEventFormatter + private readonly IUser user = UserMocks.User("user123", "me@email.com", "me"); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + private readonly DomainId contentId = DomainId.NewGuid(); + private readonly DomainId assetId = DomainId.NewGuid(); + private readonly RuleEventFormatter sut; + + private sealed class FakeContentResolver : IRuleEventFormatter + { + public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path) { - public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path) + if (path[0] == "data" && value is JsonArray) { - if (path[0] == "data" && value is JsonArray) - { - return (true, GetValueAsync()); - } - - return default; + return (true, GetValueAsync()); } - private static async ValueTask<string?> GetValueAsync() - { - await Task.Delay(5); - - return "Reference"; - } + return default; } - public RuleEventFormatterCompareTests() + private static async ValueTask<string?> GetValueAsync() { - A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) - .Returns("content-url"); + await Task.Delay(5); - A.CallTo(() => urlGenerator.AssetContent(appId, assetId.ToString())) - .Returns("asset-content-url"); + return "Reference"; + } + } - A.CallTo(() => urlGenerator.AssetContent(appId, "file-name")) - .Returns("asset-content-slug-url"); + public RuleEventFormatterCompareTests() + { + A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) + .Returns("content-url"); - var formatters = new IRuleEventFormatter[] - { - new PredefinedPatternsFormatter(urlGenerator), - new FakeContentResolver() - }; + A.CallTo(() => urlGenerator.AssetContent(appId, assetId.ToString())) + .Returns("asset-content-url"); - sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, BuildTemplateEngine(), BuildScriptEngine()); - } + A.CallTo(() => urlGenerator.AssetContent(appId, "file-name")) + .Returns("asset-content-slug-url"); - private FluidTemplateEngine BuildTemplateEngine() + var formatters = new IRuleEventFormatter[] { - var extensions = new IFluidExtension[] - { - new ContentFluidExtension(), - new DateTimeFluidExtension(), - new EventFluidExtensions(urlGenerator), - new StringFluidExtension(), - new StringWordsFluidExtension(), - new UserFluidExtension() - }; - - return new FluidTemplateEngine(extensions); - } + new PredefinedPatternsFormatter(urlGenerator), + new FakeContentResolver() + }; - private JintScriptEngine BuildScriptEngine() + sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, BuildTemplateEngine(), BuildScriptEngine()); + } + + private FluidTemplateEngine BuildTemplateEngine() + { + var extensions = new IFluidExtension[] { - var extensions = new IJintExtension[] - { - new DateTimeJintExtension(), - new EventJintExtension(urlGenerator), - new StringJintExtension(), - new StringWordsJintExtension() - }; - - return new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - }), - extensions); - } + new ContentFluidExtension(), + new DateTimeFluidExtension(), + new EventFluidExtensions(urlGenerator), + new StringFluidExtension(), + new StringWordsFluidExtension(), + new UserFluidExtension() + }; + + return new FluidTemplateEngine(extensions); + } - [Theory] - [Expressions( - "Name $APP_NAME has id $APP_ID", - "Name ${EVENT_APPID.NAME} has id ${EVENT_APPID.ID}", - "Name ${event.appId.name} has id ${event.appId.id}", - "Name {{event.appId.name}} has id {{event.appId.id}}" - )] - public async Task Should_format_app_information_from_event(string script) + private JintScriptEngine BuildScriptEngine() + { + var extensions = new IJintExtension[] { - var @event = new EnrichedContentEvent { AppId = appId }; + new DateTimeJintExtension(), + new EventJintExtension(urlGenerator), + new StringJintExtension(), + new StringWordsJintExtension() + }; - var actual = await sut.FormatAsync(script, @event); + return new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + }), + extensions); + } - Assert.Equal($"Name my-app has id {appId.Id}", actual); - } + [Theory] + [Expressions( + "Name $APP_NAME has id $APP_ID", + "Name ${EVENT_APPID.NAME} has id ${EVENT_APPID.ID}", + "Name ${event.appId.name} has id ${event.appId.id}", + "Name {{event.appId.name}} has id {{event.appId.id}}" + )] + public async Task Should_format_app_information_from_event(string script) + { + var @event = new EnrichedContentEvent { AppId = appId }; - [Theory] - [Expressions( - "Name $SCHEMA_NAME has id $SCHEMA_ID", - "Name ${EVENT_SCHEMAID.NAME} has id ${EVENT_SCHEMAID.ID}", - "Name ${event.schemaId.name} has id ${event.schemaId.id}", - "Name {{event.schemaId.name}} has id {{event.schemaId.id}}" - )] - public async Task Should_format_schema_information_from_event(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, SchemaId = schemaId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal($"Name my-app has id {appId.Id}", actual); + } - Assert.Equal($"Name my-schema has id {schemaId.Id}", actual); - } + [Theory] + [Expressions( + "Name $SCHEMA_NAME has id $SCHEMA_ID", + "Name ${EVENT_SCHEMAID.NAME} has id ${EVENT_SCHEMAID.ID}", + "Name ${event.schemaId.name} has id ${event.schemaId.id}", + "Name {{event.schemaId.name}} has id {{event.schemaId.id}}" + )] + public async Task Should_format_schema_information_from_event(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, SchemaId = schemaId }; - [Theory] - [Expressions( - "DateTime: $TIMESTAMP_DATETIME", - null, - "DateTime: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}", - "DateTime: {{event.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss'}}" - )] - public async Task Should_format_timestamp_information_from_event(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, Timestamp = now }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal($"Name my-schema has id {schemaId.Id}", actual); + } - Assert.Equal($"DateTime: {now:yyyy-MM-dd-hh-mm-ss}", actual); - } + [Theory] + [Expressions( + "DateTime: $TIMESTAMP_DATETIME", + null, + "DateTime: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}", + "DateTime: {{event.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss'}}" + )] + public async Task Should_format_timestamp_information_from_event(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, Timestamp = now }; - [Theory] - [Expressions( - "Date: $TIMESTAMP_DATE", - null, - "Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}", - "Date: {{event.timestamp | format_date: 'yyyy-MM-dd'}}" - )] - public async Task Should_format_timestamp_date_information_from_event(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, Timestamp = now }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal($"DateTime: {now:yyyy-MM-dd-hh-mm-ss}", actual); + } - Assert.Equal($"Date: {now:yyyy-MM-dd}", actual); - } + [Theory] + [Expressions( + "Date: $TIMESTAMP_DATE", + null, + "Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}", + "Date: {{event.timestamp | format_date: 'yyyy-MM-dd'}}" + )] + public async Task Should_format_timestamp_date_information_from_event(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, Timestamp = now }; - [Theory] - [Expressions( - "From $MENTIONED_NAME ($MENTIONED_EMAIL, $MENTIONED_ID)", - "From ${EVENT_MENTIONEDUSER.NAME} (${EVENT_MENTIONEDUSER.EMAIL}, ${EVENT_MENTIONEDUSER.ID})", - "From ${event.mentionedUser.name} (${event.mentionedUser.email}, ${event.mentionedUser.id})", - "From {{event.mentionedUser.name}} ({{event.mentionedUser.email}}, {{event.mentionedUser.id}})" - )] - public async Task Should_format_email_and_display_name_from_mentioned_user(string script) - { - var @event = new EnrichedCommentEvent { AppId = appId, MentionedUser = user }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal($"Date: {now:yyyy-MM-dd}", actual); + } - Assert.Equal("From me (me@email.com, user123)", actual); - } + [Theory] + [Expressions( + "From $MENTIONED_NAME ($MENTIONED_EMAIL, $MENTIONED_ID)", + "From ${EVENT_MENTIONEDUSER.NAME} (${EVENT_MENTIONEDUSER.EMAIL}, ${EVENT_MENTIONEDUSER.ID})", + "From ${event.mentionedUser.name} (${event.mentionedUser.email}, ${event.mentionedUser.id})", + "From {{event.mentionedUser.name}} ({{event.mentionedUser.email}}, {{event.mentionedUser.id}})" + )] + public async Task Should_format_email_and_display_name_from_mentioned_user(string script) + { + var @event = new EnrichedCommentEvent { AppId = appId, MentionedUser = user }; - [Theory] - [Expressions( - "From $USER_NAME ($USER_EMAIL, $USER_ID)", - "From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", - "From ${event.user.name} (${event.user.email}, ${event.user.id})", - "From {{event.user.name}} ({{event.user.email}}, {{event.user.id}})" - )] - public async Task Should_format_email_and_display_name_from_user(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, User = user }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("From me (me@email.com, user123)", actual); + } - Assert.Equal("From me (me@email.com, user123)", actual); - } + [Theory] + [Expressions( + "From $USER_NAME ($USER_EMAIL, $USER_ID)", + "From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", + "From ${event.user.name} (${event.user.email}, ${event.user.id})", + "From {{event.user.name}} ({{event.user.email}}, {{event.user.id}})" + )] + public async Task Should_format_email_and_display_name_from_user(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, User = user }; - [Theory] - [Expressions( - "From $USER_NAME ($USER_EMAIL, $USER_ID)", - "From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", - "From ${event.user.name} (${event.user.email}, ${event.user.id})", - "From {{event.user.name | default: 'null'}} ({{event.user.email | default: 'null'}}, {{event.user.id | default: 'null'}})" - )] - public async Task Should_return_null_if_user_is_not_found(string script) - { - var @event = new EnrichedContentEvent { AppId = appId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("From me (me@email.com, user123)", actual); + } - Assert.Equal("From null (null, null)", actual); - } + [Theory] + [Expressions( + "From $USER_NAME ($USER_EMAIL, $USER_ID)", + "From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", + "From ${event.user.name} (${event.user.email}, ${event.user.id})", + "From {{event.user.name | default: 'null'}} ({{event.user.email | default: 'null'}}, {{event.user.id | default: 'null'}})" + )] + public async Task Should_return_null_if_user_is_not_found(string script) + { + var @event = new EnrichedContentEvent { AppId = appId }; - [Theory] - [Expressions( - "From $USER_NAME ($USER_EMAIL, $USER_ID)", - "From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", - "From ${event.user.name} (${event.user.email}, ${event.user.id})", - "From {{event.user.name}} ({{event.user.email}}, {{event.user.id}})" - )] - public async Task Should_format_email_and_display_name_from_client(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, User = new ClientUser(RefToken.Client("android")) }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("From null (null, null)", actual); + } - Assert.Equal("From android (client:android, android)", actual); - } + [Theory] + [Expressions( + "From $USER_NAME ($USER_EMAIL, $USER_ID)", + "From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", + "From ${event.user.name} (${event.user.email}, ${event.user.id})", + "From {{event.user.name}} ({{event.user.email}}, {{event.user.id}})" + )] + public async Task Should_format_email_and_display_name_from_client(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, User = new ClientUser(RefToken.Client("android")) }; - [Theory] - [Expressions( - "Version: $ASSET_VERSION", - "Version: ${EVENT_VERSION}", - "Version: ${event.version}", - "Version: {{event.version}}" - )] - public async Task Should_format_base_property(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId, Version = 13 }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("From android (client:android, android)", actual); + } - Assert.Equal("Version: 13", actual); - } + [Theory] + [Expressions( + "Version: $ASSET_VERSION", + "Version: ${EVENT_VERSION}", + "Version: ${event.version}", + "Version: {{event.version}}" + )] + public async Task Should_format_base_property(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, Version = 13 }; - [Theory] - [Expressions( - "File: $ASSET_FILENAME", - "File: ${EVENT_FILENAME}", - "File: ${event.fileName}", - "File: {{event.fileName}}" - )] - public async Task Should_format_asset_file_name_from_event(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId, FileName = "my-file.png" }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Version: 13", actual); + } - Assert.Equal("File: my-file.png", actual); - } + [Theory] + [Expressions( + "File: $ASSET_FILENAME", + "File: ${EVENT_FILENAME}", + "File: ${event.fileName}", + "File: {{event.fileName}}" + )] + public async Task Should_format_asset_file_name_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, FileName = "my-file.png" }; - [Theory] - [Expressions( - "Type: $ASSSET_ASSETTYPE", - "Type: ${EVENT_ASSETTYPE}", - "Type: ${event.assetType}", - "Type: {{event.assetType}}" - )] - public async Task Should_format_asset_asset_type_from_event(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId, AssetType = AssetType.Audio }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("File: my-file.png", actual); + } - Assert.Equal("Type: Audio", actual); - } + [Theory] + [Expressions( + "Type: $ASSSET_ASSETTYPE", + "Type: ${EVENT_ASSETTYPE}", + "Type: ${event.assetType}", + "Type: {{event.assetType}}" + )] + public async Task Should_format_asset_asset_type_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, AssetType = AssetType.Audio }; - [Theory] - [Expressions( - "Download at $ASSET_CONTENT_URL", - null, - "Download at ${assetContentUrl()}", - "Download at {{event.id | assetContentUrl}}" - )] - [InlineData("Liquid(Download at {{event | assetContentUrl}})")] - public async Task Should_format_asset_content_url_from_event(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Type: Audio", actual); + } - Assert.Equal("Download at asset-content-url", actual); - } + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_URL", + null, + "Download at ${assetContentUrl()}", + "Download at {{event.id | assetContentUrl}}" + )] + [InlineData("Liquid(Download at {{event | assetContentUrl}})")] + public async Task Should_format_asset_content_url_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId }; - [Theory] - [Expressions( - "Download at $ASSET_CONTENT_URL", - null, - "Download at ${assetContentUrl()}", - "Download at {{event.id | assetContentUrl | default: 'null'}}" - )] - [InlineData("Liquid(Download at {{event | assetContentUrl | default: 'null'}})")] - public async Task Should_return_null_if_asset_content_url_not_found(string script) - { - var @event = new EnrichedContentEvent { AppId = appId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Download at asset-content-url", actual); + } - Assert.Equal("Download at null", actual); - } + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_URL", + null, + "Download at ${assetContentUrl()}", + "Download at {{event.id | assetContentUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentUrl | default: 'null'}})")] + public async Task Should_return_null_if_asset_content_url_not_found(string script) + { + var @event = new EnrichedContentEvent { AppId = appId }; - [Theory] - [Expressions( - "Download at $ASSET_CONTENT_APP_URL", - null, - "Download at ${assetContentAppUrl()}", - "Download at {{event.id | assetContentAppUrl | default: 'null'}}" - )] - [InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")] - public async Task Should_format_asset_content_app_url_from_event(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Download at null", actual); + } - Assert.Equal("Download at asset-content-url", actual); - } + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_APP_URL", + null, + "Download at ${assetContentAppUrl()}", + "Download at {{event.id | assetContentAppUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")] + public async Task Should_format_asset_content_app_url_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; - [Theory] - [Expressions( - "Download at $ASSET_CONTENT_APP_URL", - null, - "Download at ${assetContentAppUrl()}", - "Download at {{event.id | assetContentAppUrl | default: 'null'}}" - )] - [InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")] - public async Task Should_return_null_if_asset_content_app_url_not_found(string script) - { - var @event = new EnrichedContentEvent { AppId = appId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Download at asset-content-url", actual); + } - Assert.Equal("Download at null", actual); - } + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_APP_URL", + null, + "Download at ${assetContentAppUrl()}", + "Download at {{event.id | assetContentAppUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")] + public async Task Should_return_null_if_asset_content_app_url_not_found(string script) + { + var @event = new EnrichedContentEvent { AppId = appId }; - [Theory] - [Expressions( - "Download at $ASSET_CONTENT_SLUG_URL", - null, - "Download at ${assetContentSlugUrl()}", - "Download at {{event.fileName | assetContentSlugUrl | default: 'null'}}" - )] - [InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")] - public async Task Should_format_asset_content_slug_url_from_event(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Download at null", actual); + } - Assert.Equal("Download at asset-content-slug-url", actual); - } + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_SLUG_URL", + null, + "Download at ${assetContentSlugUrl()}", + "Download at {{event.fileName | assetContentSlugUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")] + public async Task Should_format_asset_content_slug_url_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; - [Theory] - [Expressions( - "Download at $ASSET_CONTENT_SLUG_URL", - null, - "Download at ${assetContentSlugUrl()}", - "Download at {{event.id | assetContentSlugUrl | default: 'null'}}" - )] - [InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")] - public async Task Should_return_null_if_asset_content_slug_url_not_found(string script) - { - var @event = new EnrichedContentEvent { AppId = appId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Download at asset-content-slug-url", actual); + } - Assert.Equal("Download at null", actual); - } + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_SLUG_URL", + null, + "Download at ${assetContentSlugUrl()}", + "Download at {{event.id | assetContentSlugUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")] + public async Task Should_return_null_if_asset_content_slug_url_not_found(string script) + { + var @event = new EnrichedContentEvent { AppId = appId }; - [Theory] - [Expressions( - "Go to $CONTENT_URL", - null, - "Go to ${contentUrl()}", - "Go to {{event.id | contentUrl | default: 'null'}}" - )] - public async Task Should_format_content_url_from_event(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Download at null", actual); + } - Assert.Equal("Go to content-url", actual); - } + [Theory] + [Expressions( + "Go to $CONTENT_URL", + null, + "Go to ${contentUrl()}", + "Go to {{event.id | contentUrl | default: 'null'}}" + )] + public async Task Should_format_content_url_from_event(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId }; - [Theory] - [Expressions( - "Go to $CONTENT_URL", - null, - "Go to ${contentUrl()}", - "Go to {{event.id | contentUrl | default: 'null'}}" - )] - public async Task Should_return_null_if_content_url_if_not_found(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Go to content-url", actual); + } - Assert.Equal("Go to null", actual); - } + [Theory] + [Expressions( + "Go to $CONTENT_URL", + null, + "Go to ${contentUrl()}", + "Go to {{event.id | contentUrl | default: 'null'}}" + )] + public async Task Should_return_null_if_content_url_if_not_found(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId }; - [Theory] - [Expressions( - "$CONTENT_STATUS", - "${EVENT_STATUS}", - "${contentAction()}", - "{{event.status}}" - )] - public async Task Should_format_content_status_if_found(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, Status = Status.Published }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Go to null", actual); + } - Assert.Equal("Published", actual); - } + [Theory] + [Expressions( + "$CONTENT_STATUS", + "${EVENT_STATUS}", + "${contentAction()}", + "{{event.status}}" + )] + public async Task Should_format_content_status_if_found(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, Status = Status.Published }; - [Theory] - [Expressions( - "$CONTENT_STATUS", - "${EVENT_STATUS}", - "${contentAction()}", - "{{event.status | default: 'null'}}" - )] - public async Task Should_return_null_if_content_status_not_found(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Published", actual); + } - Assert.Equal("null", actual); - } + [Theory] + [Expressions( + "$CONTENT_STATUS", + "${EVENT_STATUS}", + "${contentAction()}", + "{{event.status | default: 'null'}}" + )] + public async Task Should_return_null_if_content_status_not_found(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId }; - [Theory] - [Expressions( - "$CONTENT_ACTION", - "${EVENT_TYPE}", - "${event.type}", - "{{event.type}}" - )] - public async Task Should_format_content_actions_if_found(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("null", actual); + } - Assert.Equal("Created", actual); - } + [Theory] + [Expressions( + "$CONTENT_ACTION", + "${EVENT_TYPE}", + "${event.type}", + "{{event.type}}" + )] + public async Task Should_format_content_actions_if_found(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; - [Theory] - [Expressions( - "$CONTENT_STATUS", - "${CONTENT_STATUS}", - "${contentAction()}", - null - )] - public async Task Should_return_null_if_content_action_not_found(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId }; + var actual = await sut.FormatAsync(script, @event); - var actual = await sut.FormatAsync(script, @event); + Assert.Equal("Created", actual); + } - Assert.Equal("null", actual); - } + [Theory] + [Expressions( + "$CONTENT_STATUS", + "${CONTENT_STATUS}", + "${contentAction()}", + null + )] + public async Task Should_return_null_if_content_action_not_found(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId }; - [Theory] - [Expressions( - "$CONTENT_DATA.country.zh-TW", - "${CONTENT_DATA.country.zh-TW}", - "${event.data.country['zh-TW']}", - "{{event.data.country.zh-TW}}" - )] - public async Task Should_return_country_based_culture(string script) + var actual = await sut.FormatAsync(script, @event); + + Assert.Equal("null", actual); + } + + [Theory] + [Expressions( + "$CONTENT_DATA.country.zh-TW", + "${CONTENT_DATA.country.zh-TW}", + "${event.data.country['zh-TW']}", + "{{event.data.country.zh-TW}}" + )] + public async Task Should_return_country_based_culture(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("country", - new ContentFieldData() - .AddLocalized("zh-TW", "Berlin")) - }; + AppId = appId, + Data = + new ContentData() + .AddField("country", + new ContentFieldData() + .AddLocalized("zh-TW", "Berlin")) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Berlin", actual); - } + Assert.Equal("Berlin", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.country.iv", - "${CONTENT_DATA.country.iv}", - "${event.data.country.iv}", - "{{event.data.country.iv | default: 'null'}}" - )] - public async Task Should_return_null_if_field_not_found(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.country.iv", + "${CONTENT_DATA.country.iv}", + "${event.data.country.iv}", + "{{event.data.country.iv | default: 'null'}}" + )] + public async Task Should_return_null_if_field_not_found(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant("Berlin")) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant("Berlin")) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", actual); - } + Assert.Equal("null", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.country.iv", - "${CONTENT_DATA.country.iv}", - "${event.data.country.iv}", - "{{event.data.country.iv | default: 'null'}}" - )] - public async Task Should_return_null_if_partition_not_found(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.country.iv", + "${CONTENT_DATA.country.iv}", + "${event.data.country.iv}", + "{{event.data.country.iv | default: 'null'}}" + )] + public async Task Should_return_null_if_partition_not_found(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant("Berlin")) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant("Berlin")) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", actual); - } + Assert.Equal("null", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.country.iv.10", - "${CONTENT_DATA.country.iv.10}", - "${event.data.country.iv[10]}", - "{{event.data.country.iv[10] | default: 'null'}}" - )] - public async Task Should_return_null_if_array_item_not_found(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.country.iv.10", + "${CONTENT_DATA.country.iv.10}", + "${event.data.country.iv[10]}", + "{{event.data.country.iv[10] | default: 'null'}}" + )] + public async Task Should_return_null_if_array_item_not_found(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant(new JsonArray())) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant(new JsonArray())) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", actual); - } + Assert.Equal("null", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.country.iv.Location", - "${CONTENT_DATA.country.iv.Location}", - "${event.data.country.iv.Location}", - "{{event.data.country.iv.Location | default: 'null'}}" - )] - public async Task Should_return_null_if_property_not_found(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.country.iv.Location", + "${CONTENT_DATA.country.iv.Location}", + "${event.data.country.iv.Location}", + "{{event.data.country.iv.Location | default: 'null'}}" + )] + public async Task Should_return_null_if_property_not_found(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("name", "Berlin"))) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("name", "Berlin"))) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("null", actual); - } + Assert.Equal("null", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.city.iv", - "${CONTENT_DATA.city.iv}", - "${event.data.city.iv}", - "{{event.data.city.iv}}" - )] - public async Task Should_return_plain_value_if_found(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.city.iv", + "${CONTENT_DATA.city.iv}", + "${event.data.city.iv}", + "{{event.data.city.iv}}" + )] + public async Task Should_return_plain_value_if_found(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant("Berlin")) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant("Berlin")) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Berlin", actual); - } + Assert.Equal("Berlin", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.city.iv.0", - "${CONTENT_DATA.city.iv.0}", - "${event.data.city.iv[0]}", - "{{event.data.city.iv[0]}}" - )] - public async Task Should_return_plain_value_from_array_if_found(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.city.iv.0", + "${CONTENT_DATA.city.iv.0}", + "${event.data.city.iv[0]}", + "{{event.data.city.iv[0]}}" + )] + public async Task Should_return_plain_value_from_array_if_found(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant(JsonValue.Array("Berlin"))) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant(JsonValue.Array("Berlin"))) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Berlin", actual); - } + Assert.Equal("Berlin", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.city.iv.name", - "${CONTENT_DATA.city.iv.name}", - "${event.data.city.iv.name}", - "{{event.data.city.iv.name}}" - )] - public async Task Should_return_plain_value_from_object_if_found(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.city.iv.name", + "${CONTENT_DATA.city.iv.name}", + "${event.data.city.iv.name}", + "{{event.data.city.iv.name}}" + )] + public async Task Should_return_plain_value_from_object_if_found(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("name", "Berlin"))) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("name", "Berlin"))) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Berlin", actual); - } + Assert.Equal("Berlin", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.city.iv", - "${CONTENT_DATA.city.iv}", - "${JSON.stringify(event.data.city.iv)}", - "{{event.data.city.iv}}" - )] - public async Task Should_return_json_string_if_object(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.city.iv", + "${CONTENT_DATA.city.iv}", + "${JSON.stringify(event.data.city.iv)}", + "{{event.data.city.iv}}" + )] + public async Task Should_return_json_string_if_object(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("name", "Berlin"))) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("name", "Berlin"))) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("{\"name\":\"Berlin\"}", actual); - } + Assert.Equal("{\"name\":\"Berlin\"}", actual); + } - [Theory] - [Expressions( - "$CONTENT_DATA.city.iv", - "${CONTENT_DATA.city.iv}", - "${JSON.stringify(event.data.city.iv)}", - "{{event.data.city.iv}}" - )] - public async Task Should_return_json_string_if_array(string script) + [Theory] + [Expressions( + "$CONTENT_DATA.city.iv", + "${CONTENT_DATA.city.iv}", + "${JSON.stringify(event.data.city.iv)}", + "{{event.data.city.iv}}" + )] + public async Task Should_return_json_string_if_array(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1, 2, 3))) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1, 2, 3))) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("[1,2,3]", actual?.Replace(" ", string.Empty, StringComparison.Ordinal)); - } + Assert.Equal("[1,2,3]", actual?.Replace(" ", string.Empty, StringComparison.Ordinal)); + } - [Theory] - [Expressions( - "$CONTENT_DATA", - "${CONTENT_DATA}", - "${JSON.stringify(event.data)}", - null - )] - public async Task Should_return_json_string_if_data(string script) + [Theory] + [Expressions( + "$CONTENT_DATA", + "${CONTENT_DATA}", + "${JSON.stringify(event.data)}", + null + )] + public async Task Should_return_json_string_if_data(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("name", "Berlin"))) - }; + AppId = appId, + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("name", "Berlin"))) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("{\"city\":{\"iv\":{\"name\":\"Berlin\"}}}", actual); - } + Assert.Equal("{\"city\":{\"iv\":{\"name\":\"Berlin\"}}}", actual); + } - [Theory] - [Expressions( - null, - "From ${EVENT_ACTOR}", - "From ${event.actor}", - "From {{event.actor}}" - )] - public async Task Should_format_actor(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("android") }; + [Theory] + [Expressions( + null, + "From ${EVENT_ACTOR}", + "From ${event.actor}", + "From {{event.actor}}" + )] + public async Task Should_format_actor(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("android") }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("From client:android", actual); - } + Assert.Equal("From client:android", actual); + } - [Theory] - [Expressions( - null, - "${ASSET_LASTMODIFIED | timestamp}", - "${event.lastModified.getTime()}", - "{{event.lastModified | timestamp}}" - )] - public async Task Should_transform_timestamp(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId, LastModified = Instant.FromUnixTimeSeconds(1590769584) }; + [Theory] + [Expressions( + null, + "${ASSET_LASTMODIFIED | timestamp}", + "${event.lastModified.getTime()}", + "{{event.lastModified | timestamp}}" + )] + public async Task Should_transform_timestamp(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, LastModified = Instant.FromUnixTimeSeconds(1590769584) }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("1590769584000", actual); - } + Assert.Equal("1590769584000", actual); + } - [Theory] - [Expressions( - null, - "${CONTENT_DATA.time.iv | timestamp}", - null, - "{{event.data.time.iv | timestamp}}")] - public async Task Should_return_timestamp_if_string(string script) + [Theory] + [Expressions( + null, + "${CONTENT_DATA.time.iv | timestamp}", + null, + "{{event.data.time.iv | timestamp}}")] + public async Task Should_return_timestamp_if_string(string script) + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("time", - new ContentFieldData() - .AddInvariant("2020-06-01T10:10:20Z")) - }; + AppId = appId, + Data = + new ContentData() + .AddField("time", + new ContentFieldData() + .AddInvariant("2020-06-01T10:10:20Z")) + }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("1591006220000", actual); - } + Assert.Equal("1591006220000", actual); + } - [Theory] - [Expressions( - null, - "${ASSET_LASTMODIFIED | timestamp_sec}", - "${event.lastModified.getTime() / 1000}", - "{{event.lastModified | timestamp_sec}}" - )] - public async Task Should_transform_timestamp_seconds(string script) - { - var @event = new EnrichedAssetEvent { AppId = appId, LastModified = Instant.FromUnixTimeSeconds(1590769584) }; + [Theory] + [Expressions( + null, + "${ASSET_LASTMODIFIED | timestamp_sec}", + "${event.lastModified.getTime() / 1000}", + "{{event.lastModified | timestamp_sec}}" + )] + public async Task Should_transform_timestamp_seconds(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, LastModified = Instant.FromUnixTimeSeconds(1590769584) }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("1590769584", actual); - } + Assert.Equal("1590769584", actual); + } - [Theory] - [Expressions( - "${USER_NAME | Upper}", - "${EVENT_USER.NAME | Upper}", - "${event.user.name.toUpperCase()}", - "{{event.user.name | upcase}}" - )] - public async Task Should_transform_upper(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, User = user }; + [Theory] + [Expressions( + "${USER_NAME | Upper}", + "${EVENT_USER.NAME | Upper}", + "${event.user.name.toUpperCase()}", + "{{event.user.name | upcase}}" + )] + public async Task Should_transform_upper(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, User = user }; - A.CallTo(() => user.Claims) - .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); + A.CallTo(() => user.Claims) + .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("DONALD DUCK", actual); - } + Assert.Equal("DONALD DUCK", actual); + } - [Theory] - [Expressions( - "${USER_NAME | Lower}", - "${EVENT_USER.NAME | Lower}", - "${event.user.name.toLowerCase()}", - "{{event.user.name | downcase}}" - )] - public async Task Should_transform_lower(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, User = user }; + [Theory] + [Expressions( + "${USER_NAME | Lower}", + "${EVENT_USER.NAME | Lower}", + "${event.user.name.toLowerCase()}", + "{{event.user.name | downcase}}" + )] + public async Task Should_transform_lower(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, User = user }; - A.CallTo(() => user.Claims) - .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); + A.CallTo(() => user.Claims) + .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("donald duck", actual); - } + Assert.Equal("donald duck", actual); + } - [Theory] - [Expressions( - "${USER_NAME | Trim}", - "${EVENT_USER.NAME | Trim}", - "${event.user.name.trim()}", - "{{event.user.name | trim}}" - )] - public async Task Should_transform_trimmed(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, User = user }; + [Theory] + [Expressions( + "${USER_NAME | Trim}", + "${EVENT_USER.NAME | Trim}", + "${event.user.name.trim()}", + "{{event.user.name | trim}}" + )] + public async Task Should_transform_trimmed(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, User = user }; - A.CallTo(() => user.Claims) - .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") }); + A.CallTo(() => user.Claims) + .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") }); - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Donald Duck", actual); - } + Assert.Equal("Donald Duck", actual); + } - [Theory] - [Expressions( - "${USER_NAME | Slugify}", - "${EVENT_USER.NAME | Slugify}", - "${slugify(event.user.name)}", - "{{event.user.name | slugify}}" - )] - public async Task Should_transform_slugify(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, User = user }; + [Theory] + [Expressions( + "${USER_NAME | Slugify}", + "${EVENT_USER.NAME | Slugify}", + "${slugify(event.user.name)}", + "{{event.user.name | slugify}}" + )] + public async Task Should_transform_slugify(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, User = user }; - A.CallTo(() => user.Claims) - .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); + A.CallTo(() => user.Claims) + .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("donald-duck", actual); - } + Assert.Equal("donald-duck", actual); + } - [Theory] - [Expressions( - "${USER_NAME | Upper | Trim}", - "${EVENT_USER.NAME | Upper | Trim}", - "${event.user.name.toUpperCase().trim()}", - "{{event.user.name | upcase | trim}}" - )] - public async Task Should_transform_chained(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, User = user }; + [Theory] + [Expressions( + "${USER_NAME | Upper | Trim}", + "${EVENT_USER.NAME | Upper | Trim}", + "${event.user.name.toUpperCase().trim()}", + "{{event.user.name | upcase | trim}}" + )] + public async Task Should_transform_chained(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, User = user }; - A.CallTo(() => user.Claims) - .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") }); + A.CallTo(() => user.Claims) + .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") }); - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("DONALD DUCK", actual); - } + Assert.Equal("DONALD DUCK", actual); + } - [Theory] - [Expressions( - "${USER_NAME | Escape}", - "${EVENT_USER.NAME | Escape}", - null, - "{{event.user.name | escape}}" - )] - public async Task Should_transform_json_escape(string script) - { - var @event = new EnrichedContentEvent { AppId = appId, User = user }; + [Theory] + [Expressions( + "${USER_NAME | Escape}", + "${EVENT_USER.NAME | Escape}", + null, + "{{event.user.name | escape}}" + )] + public async Task Should_transform_json_escape(string script) + { + var @event = new EnrichedContentEvent { AppId = appId, User = user }; - A.CallTo(() => user.Claims) - .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald\"Duck") }); + A.CallTo(() => user.Claims) + .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald\"Duck") }); - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("Donald\\\"Duck", actual); - } + Assert.Equal("Donald\\\"Duck", actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index f2280c982d..591cab079c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -26,276 +26,275 @@ #pragma warning disable CA2012 // Use ValueTasks correctly -namespace Squidex.Domain.Apps.Core.Operations.HandleRules +namespace Squidex.Domain.Apps.Core.Operations.HandleRules; + +public class RuleEventFormatterTests { - public class RuleEventFormatterTests + private readonly IUser user = UserMocks.User("user123", "me@email.com", "me"); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly DomainId contentId = DomainId.NewGuid(); + private readonly DomainId assetId = DomainId.NewGuid(); + private readonly RuleEventFormatter sut; + + private sealed class FakeContentResolver : IRuleEventFormatter { - private readonly IUser user = UserMocks.User("user123", "me@email.com", "me"); - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly DomainId contentId = DomainId.NewGuid(); - private readonly DomainId assetId = DomainId.NewGuid(); - private readonly RuleEventFormatter sut; - - private sealed class FakeContentResolver : IRuleEventFormatter + public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path) { - public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path) + if (path[0] == "data" && value is JsonValue { Value: JsonArray }) { - if (path[0] == "data" && value is JsonValue { Value: JsonArray }) - { - return (true, GetValueAsync()); - } - - return default; + return (true, GetValueAsync()); } - private static async ValueTask<string?> GetValueAsync() - { - await Task.Delay(5); - - return "Reference"; - } + return default; } - public RuleEventFormatterTests() + private static async ValueTask<string?> GetValueAsync() { - A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) - .Returns("content-url"); + await Task.Delay(5); - A.CallTo(() => urlGenerator.AssetContent(appId, assetId.ToString())) - .Returns("asset-content-url"); + return "Reference"; + } + } - var formatters = new IRuleEventFormatter[] - { - new PredefinedPatternsFormatter(urlGenerator), - new FakeContentResolver() - }; + public RuleEventFormatterTests() + { + A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) + .Returns("content-url"); - sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, BuildTemplateEngine(), BuildScriptEngine()); - } + A.CallTo(() => urlGenerator.AssetContent(appId, assetId.ToString())) + .Returns("asset-content-url"); - private static FluidTemplateEngine BuildTemplateEngine() + var formatters = new IRuleEventFormatter[] { - var extensions = new IFluidExtension[] - { - new DateTimeFluidExtension(), - new UserFluidExtension() - }; + new PredefinedPatternsFormatter(urlGenerator), + new FakeContentResolver() + }; - return new FluidTemplateEngine(extensions); - } + sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, BuildTemplateEngine(), BuildScriptEngine()); + } - private JintScriptEngine BuildScriptEngine() + private static FluidTemplateEngine BuildTemplateEngine() + { + var extensions = new IFluidExtension[] { - var extensions = new IJintExtension[] - { - new DateTimeJintExtension(), - new EventJintExtension(urlGenerator), - new StringJintExtension(), - new StringWordsJintExtension() - }; - - return new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - }), - extensions); - } + new DateTimeFluidExtension(), + new UserFluidExtension() + }; - [Fact] - public void Should_serialize_object_to_json() + return new FluidTemplateEngine(extensions); + } + + private JintScriptEngine BuildScriptEngine() + { + var extensions = new IJintExtension[] { - var actual = sut.ToPayload(new { Value = 1 }); + new DateTimeJintExtension(), + new EventJintExtension(urlGenerator), + new StringJintExtension(), + new StringWordsJintExtension() + }; + + return new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + }), + extensions); + } - Assert.NotNull(actual); - } + [Fact] + public void Should_serialize_object_to_json() + { + var actual = sut.ToPayload(new { Value = 1 }); - [Fact] - public void Should_create_payload() - { - var @event = new EnrichedContentEvent { AppId = appId }; + Assert.NotNull(actual); + } - var actual = sut.ToPayload(@event); + [Fact] + public void Should_create_payload() + { + var @event = new EnrichedContentEvent { AppId = appId }; - Assert.NotNull(actual); - } + var actual = sut.ToPayload(@event); - [Fact] - public void Should_create_envelope_data_from_event() - { - var @event = new EnrichedContentEvent { AppId = appId, Name = "MyEventName" }; + Assert.NotNull(actual); + } - var actual = sut.ToEnvelope(@event); + [Fact] + public void Should_create_envelope_data_from_event() + { + var @event = new EnrichedContentEvent { AppId = appId, Name = "MyEventName" }; - Assert.Contains("MyEventName", actual, StringComparison.Ordinal); - } + var actual = sut.ToEnvelope(@event); - [Fact] - public async Task Should_resolve_reference() + Assert.Contains("MyEventName", actual, StringComparison.Ordinal); + } + + [Fact] + public async Task Should_resolve_reference() + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - Data = - new ContentData() - .AddField("city", - new ContentFieldData() - .AddInvariant(new JsonArray())) - }; + Data = + new ContentData() + .AddField("city", + new ContentFieldData() + .AddInvariant(new JsonArray())) + }; - var actual = await sut.FormatAsync("${CONTENT_DATA.city.iv.data.name}", @event); + var actual = await sut.FormatAsync("${CONTENT_DATA.city.iv.data.name}", @event); - Assert.Equal("Reference", actual); - } + Assert.Equal("Reference", actual); + } - [Theory] - [InlineData("${EVENT_INVALID ? file}", "file")] - public async Task Should_provide_fallback_if_path_is_invalid(string script, string expect) - { - var @event = new EnrichedAssetEvent { AppId = appId, FileName = null! }; + [Theory] + [InlineData("${EVENT_INVALID ? file}", "file")] + public async Task Should_provide_fallback_if_path_is_invalid(string script, string expect) + { + var @event = new EnrichedAssetEvent { AppId = appId, FileName = null! }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, actual); - } + Assert.Equal(expect, actual); + } - [Theory] - [InlineData("${ASSET_FILENAME ? file}", "file")] - public async Task Should_provide_fallback_if_value_is_null(string script, string expect) - { - var @event = new EnrichedAssetEvent { AppId = appId, FileName = null! }; + [Theory] + [InlineData("${ASSET_FILENAME ? file}", "file")] + public async Task Should_provide_fallback_if_value_is_null(string script, string expect) + { + var @event = new EnrichedAssetEvent { AppId = appId, FileName = null! }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, actual); - } + Assert.Equal(expect, actual); + } - [Theory] - [InlineData("Found in ${ASSET_FILENAME | Upper}.docx", "Found in DONALD DUCK.docx")] - [InlineData("Found in ${ASSET_FILENAME| Upper }.docx", "Found in DONALD DUCK.docx")] - [InlineData("Found in ${ASSET_FILENAME|Upper }.docx", "Found in DONALD DUCK.docx")] - public async Task Should_transform_replacements_and_igore_whitepsaces(string script, string expect) - { - var @event = new EnrichedAssetEvent { AppId = appId, FileName = "Donald Duck" }; + [Theory] + [InlineData("Found in ${ASSET_FILENAME | Upper}.docx", "Found in DONALD DUCK.docx")] + [InlineData("Found in ${ASSET_FILENAME| Upper }.docx", "Found in DONALD DUCK.docx")] + [InlineData("Found in ${ASSET_FILENAME|Upper }.docx", "Found in DONALD DUCK.docx")] + public async Task Should_transform_replacements_and_igore_whitepsaces(string script, string expect) + { + var @event = new EnrichedAssetEvent { AppId = appId, FileName = "Donald Duck" }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, actual); - } + Assert.Equal(expect, actual); + } - [Theory] - [InlineData("Found in ${ASSET_FILENAME | Escape | Upper}.docx", "Found in DONALD\\\"DUCK.docx", "Donald\"Duck")] - [InlineData("Found in ${ASSET_FILENAME | Escape}.docx", "Found in Donald\\\"Duck.docx", "Donald\"Duck")] - [InlineData("Found in ${ASSET_FILENAME | Upper}.docx", "Found in DONALD DUCK.docx", "Donald Duck")] - [InlineData("Found in ${ASSET_FILENAME | Lower}.docx", "Found in donald duck.docx", "Donald Duck")] - [InlineData("Found in ${ASSET_FILENAME | Slugify}.docx", "Found in donald-duck.docx", "Donald Duck")] - [InlineData("Found in ${ASSET_FILENAME | Trim}.docx", "Found in Donald Duck.docx", "Donald Duck ")] - public async Task Should_transform_replacements(string script, string expect, string name) - { - var @event = new EnrichedAssetEvent { AppId = appId, FileName = name }; + [Theory] + [InlineData("Found in ${ASSET_FILENAME | Escape | Upper}.docx", "Found in DONALD\\\"DUCK.docx", "Donald\"Duck")] + [InlineData("Found in ${ASSET_FILENAME | Escape}.docx", "Found in Donald\\\"Duck.docx", "Donald\"Duck")] + [InlineData("Found in ${ASSET_FILENAME | Upper}.docx", "Found in DONALD DUCK.docx", "Donald Duck")] + [InlineData("Found in ${ASSET_FILENAME | Lower}.docx", "Found in donald duck.docx", "Donald Duck")] + [InlineData("Found in ${ASSET_FILENAME | Slugify}.docx", "Found in donald-duck.docx", "Donald Duck")] + [InlineData("Found in ${ASSET_FILENAME | Trim}.docx", "Found in Donald Duck.docx", "Donald Duck ")] + public async Task Should_transform_replacements(string script, string expect, string name) + { + var @event = new EnrichedAssetEvent { AppId = appId, FileName = name }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, actual); - } + Assert.Equal(expect, actual); + } - [Theory] - [InlineData("From ${USER_NAME | Escape | Upper}", "From DONALD\\\"DUCK", "Donald\"Duck")] - [InlineData("From ${USER_NAME | Escape}", "From Donald\\\"Duck", "Donald\"Duck")] - [InlineData("From ${USER_NAME | Upper}", "From DONALD DUCK", "Donald Duck")] - [InlineData("From ${USER_NAME | Lower}", "From donald duck", "Donald Duck")] - [InlineData("From ${USER_NAME | Slugify}", "From donald-duck", "Donald Duck")] - [InlineData("From ${USER_NAME | Trim}", "From Donald Duck", "Donald Duck ")] - public async Task Should_transform_replacements_with_simple_pattern(string script, string expect, string name) - { - var @event = new EnrichedContentEvent { AppId = appId, User = user }; + [Theory] + [InlineData("From ${USER_NAME | Escape | Upper}", "From DONALD\\\"DUCK", "Donald\"Duck")] + [InlineData("From ${USER_NAME | Escape}", "From Donald\\\"Duck", "Donald\"Duck")] + [InlineData("From ${USER_NAME | Upper}", "From DONALD DUCK", "Donald Duck")] + [InlineData("From ${USER_NAME | Lower}", "From donald duck", "Donald Duck")] + [InlineData("From ${USER_NAME | Slugify}", "From donald-duck", "Donald Duck")] + [InlineData("From ${USER_NAME | Trim}", "From Donald Duck", "Donald Duck ")] + public async Task Should_transform_replacements_with_simple_pattern(string script, string expect, string name) + { + var @event = new EnrichedContentEvent { AppId = appId, User = user }; - A.CallTo(() => user.Claims) - .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, name) }); + A.CallTo(() => user.Claims) + .Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, name) }); - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, actual); - } + Assert.Equal(expect, actual); + } - [Theory] - [InlineData("{'Key':'${ASSET_FILENAME | Upper}'}", "{'Key':'DONALD DUCK'}")] - [InlineData("{'Key':'${ASSET_FILENAME}'}", "{'Key':'Donald Duck'}")] - public async Task Should_transform_json_examples(string script, string expect) - { - var @event = new EnrichedAssetEvent { AppId = appId, FileName = "Donald Duck" }; + [Theory] + [InlineData("{'Key':'${ASSET_FILENAME | Upper}'}", "{'Key':'DONALD DUCK'}")] + [InlineData("{'Key':'${ASSET_FILENAME}'}", "{'Key':'Donald Duck'}")] + public async Task Should_transform_json_examples(string script, string expect) + { + var @event = new EnrichedAssetEvent { AppId = appId, FileName = "Donald Duck" }; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal(expect, actual); - } + Assert.Equal(expect, actual); + } - [Fact] - public async Task Should_format_json() - { - var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("android") }; + [Fact] + public async Task Should_format_json() + { + var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("android") }; - var actual = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); + var actual = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); - Assert.Equal("{\"actor\":\"client:android\"}", actual); - } + Assert.Equal("{\"actor\":\"client:android\"}", actual); + } - [Fact] - public async Task Should_format_json_with_special_characters() - { - var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("mobile\"android") }; + [Fact] + public async Task Should_format_json_with_special_characters() + { + var @event = new EnrichedContentEvent { AppId = appId, Actor = RefToken.Client("mobile\"android") }; - var actual = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); + var actual = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); - Assert.Equal("{\"actor\":\"client:mobile\\\"android\"}", actual); - } + Assert.Equal("{\"actor\":\"client:mobile\\\"android\"}", actual); + } - [Fact] - public async Task Should_evaluate_script_if_starting_with_whitespace() - { - var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; + [Fact] + public async Task Should_evaluate_script_if_starting_with_whitespace() + { + var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; - var actual = await sut.FormatAsync(" Script(`${event.type}`)", @event); + var actual = await sut.FormatAsync(" Script(`${event.type}`)", @event); - Assert.Equal("Created", actual); - } + Assert.Equal("Created", actual); + } - [Fact] - public async Task Should_evaluate_script_if_ends_with_whitespace() - { - var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; + [Fact] + public async Task Should_evaluate_script_if_ends_with_whitespace() + { + var @event = new EnrichedContentEvent { AppId = appId, Type = EnrichedContentEventType.Created }; - var actual = await sut.FormatAsync("Script(`${event.type}`) ", @event); + var actual = await sut.FormatAsync("Script(`${event.type}`) ", @event); - Assert.Equal("Created", actual); - } + Assert.Equal("Created", actual); + } - [Fact] - public async Task Should_return_json_string_if_array() + [Fact] + public async Task Should_return_json_string_if_array() + { + var @event = new EnrichedContentEvent { - var @event = new EnrichedContentEvent - { - AppId = appId, - Data = - new ContentData() - .AddField("categories", - new ContentFieldData() - .AddInvariant(JsonValue.Array("ref1", "ref2", "ref3"))) - }; - - var script = @" + AppId = appId, + Data = + new ContentData() + .AddField("categories", + new ContentFieldData() + .AddInvariant(JsonValue.Array("ref1", "ref2", "ref3"))) + }; + + var script = @" Script(JSON.stringify( { 'categories': event.data.categories.iv }))"; - var actual = await sut.FormatAsync(script, @event); + var actual = await sut.FormatAsync(script, @event); - Assert.Equal("{'categories':['ref1','ref2','ref3']}", actual? - .Replace(" ", string.Empty, StringComparison.Ordinal) - .Replace("\"", "'", StringComparison.Ordinal)); - } + Assert.Equal("{'categories':['ref1','ref2','ref3']}", actual? + .Replace(" ", string.Empty, StringComparison.Ordinal) + .Replace("\"", "'", StringComparison.Ordinal)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 26eee7b6ad..82f32abb93 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -21,847 +21,846 @@ using Squidex.Infrastructure.Reflection; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.HandleRules +namespace Squidex.Domain.Apps.Core.Operations.HandleRules; + +public class RuleServiceTests { - public class RuleServiceTests - { - private readonly IRuleTriggerHandler ruleTriggerHandler = A.Fake<IRuleTriggerHandler>(); - private readonly IRuleActionHandler ruleActionHandler = A.Fake<IRuleActionHandler>(); - private readonly IEventEnricher eventEnricher = A.Fake<IEventEnricher>(); - private readonly IClock clock = A.Fake<IClock>(); - private readonly string actionData = "{\"value\":10}"; - private readonly string actionDump = "MyDump"; - private readonly string actionName = "ValidAction"; - private readonly string actionDescription = "MyDescription"; - private readonly DomainId ruleId = DomainId.NewGuid(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); - private readonly RuleService sut; - - public sealed class InvalidEvent : IEvent - { - } + private readonly IRuleTriggerHandler ruleTriggerHandler = A.Fake<IRuleTriggerHandler>(); + private readonly IRuleActionHandler ruleActionHandler = A.Fake<IRuleActionHandler>(); + private readonly IEventEnricher eventEnricher = A.Fake<IEventEnricher>(); + private readonly IClock clock = A.Fake<IClock>(); + private readonly string actionData = "{\"value\":10}"; + private readonly string actionDump = "MyDump"; + private readonly string actionName = "ValidAction"; + private readonly string actionDescription = "MyDescription"; + private readonly DomainId ruleId = DomainId.NewGuid(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + private readonly RuleService sut; + + public sealed class InvalidEvent : IEvent + { + } - public sealed record InvalidAction : RuleAction - { - } + public sealed record InvalidAction : RuleAction + { + } - public sealed record ValidAction : RuleAction - { - } + public sealed record ValidAction : RuleAction + { + } - public sealed class ValidData - { - public int Value { get; set; } - } + public sealed class ValidData + { + public int Value { get; set; } + } - public sealed record InvalidTrigger : RuleTrigger + public sealed record InvalidTrigger : RuleTrigger + { + public override T Accept<T>(IRuleTriggerVisitor<T> visitor) { - public override T Accept<T>(IRuleTriggerVisitor<T> visitor) - { - return default!; - } + return default!; } + } - public RuleServiceTests() - { - typeNameRegistry.Map(typeof(ContentCreated)); - typeNameRegistry.Map(typeof(ValidAction), actionName); - - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + public RuleServiceTests() + { + typeNameRegistry.Map(typeof(ContentCreated)); + typeNameRegistry.Map(typeof(ValidAction), actionName); - A.CallTo(() => ruleActionHandler.ActionType) - .Returns(typeof(ValidAction)); + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - A.CallTo(() => ruleActionHandler.DataType) - .Returns(typeof(ValidData)); + A.CallTo(() => ruleActionHandler.ActionType) + .Returns(typeof(ValidAction)); - A.CallTo(() => ruleTriggerHandler.TriggerType) - .Returns(typeof(ContentChangedTriggerV2)); + A.CallTo(() => ruleActionHandler.DataType) + .Returns(typeof(ValidData)); - var log = A.Fake<ILogger<RuleService>>(); + A.CallTo(() => ruleTriggerHandler.TriggerType) + .Returns(typeof(ContentChangedTriggerV2)); - sut = new RuleService(Options.Create(new RuleOptions()), - new[] { ruleTriggerHandler }, - new[] { ruleActionHandler }, - eventEnricher, TestUtils.DefaultSerializer, log, typeNameRegistry) - { - Clock = clock - }; - } + var log = A.Fake<ILogger<RuleService>>(); - [Fact] - public void Should_calculate_event_name_from_trigger_handler() + sut = new RuleService(Options.Create(new RuleOptions()), + new[] { ruleTriggerHandler }, + new[] { ruleActionHandler }, + eventEnricher, TestUtils.DefaultSerializer, log, typeNameRegistry) { - var eventEnvelope = new ContentCreated(); + Clock = clock + }; + } - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) - .Returns(true); + [Fact] + public void Should_calculate_event_name_from_trigger_handler() + { + var eventEnvelope = new ContentCreated(); - A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) - .Returns("custom-name"); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) + .Returns(true); - var name = sut.GetName(eventEnvelope); + A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) + .Returns("custom-name"); - Assert.Equal("custom-name", name); - } + var name = sut.GetName(eventEnvelope); - [Fact] - public void Should_calculate_default_name_if_trigger_handler_returns_no_name() - { - var eventEnvelope = new ContentCreated(); + Assert.Equal("custom-name", name); + } - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) - .Returns(true); + [Fact] + public void Should_calculate_default_name_if_trigger_handler_returns_no_name() + { + var eventEnvelope = new ContentCreated(); - A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) - .Returns(null); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) + .Returns(true); - var name = sut.GetName(eventEnvelope); + A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) + .Returns(null); - Assert.Equal("ContentCreated", name); + var name = sut.GetName(eventEnvelope); - A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) - .MustHaveHappened(); - } + Assert.Equal("ContentCreated", name); - [Fact] - public void Should_calculate_default_name_if_trigger_handler_cannot_not_handle_event() - { - var eventEnvelope = new ContentCreated(); + A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) + .MustHaveHappened(); + } - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) - .Returns(false); + [Fact] + public void Should_calculate_default_name_if_trigger_handler_cannot_not_handle_event() + { + var eventEnvelope = new ContentCreated(); - var name = sut.GetName(eventEnvelope); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) + .Returns(false); - Assert.Equal("ContentCreated", name); + var name = sut.GetName(eventEnvelope); - A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) - .MustNotHaveHappened(); - } + Assert.Equal("ContentCreated", name); - [Fact] - public void Should_not_run_from_snapshots_if_no_trigger_handler_registered() - { - var context = RuleInvalidTrigger(); + A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) + .MustNotHaveHappened(); + } - var actual = sut.CanCreateSnapshotEvents(context); + [Fact] + public void Should_not_run_from_snapshots_if_no_trigger_handler_registered() + { + var context = RuleInvalidTrigger(); - Assert.False(actual); - } + var actual = sut.CanCreateSnapshotEvents(context); - [Fact] - public void Should_not_run_from_snapshots_if_trigger_handler_does_not_support_it() - { - var context = Rule(); + Assert.False(actual); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(false); + [Fact] + public void Should_not_run_from_snapshots_if_trigger_handler_does_not_support_it() + { + var context = Rule(); - var actual = sut.CanCreateSnapshotEvents(context); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(false); - Assert.False(actual); - } + var actual = sut.CanCreateSnapshotEvents(context); - [Fact] - public void Should_run_from_snapshots_if_trigger_handler_does_support_it() - { - var context = Rule(); + Assert.False(actual); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(true); + [Fact] + public void Should_run_from_snapshots_if_trigger_handler_does_support_it() + { + var context = Rule(); - var actual = sut.CanCreateSnapshotEvents(context); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(true); - Assert.True(actual); - } + var actual = sut.CanCreateSnapshotEvents(context); - [Fact] - public async Task Should_not_create_job_from_snapshots_if_trigger_handler_does_not_support_it() - { - var context = Rule(); + Assert.True(actual); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(false); + [Fact] + public async Task Should_not_create_job_from_snapshots_if_trigger_handler_does_not_support_it() + { + var context = Rule(); - var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(false); - Assert.Empty(jobs); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A<RuleContext>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + Assert.Empty(jobs); - [Fact] - public async Task Should_not_create_job_from_snapshots_if_rule_disabled() - { - var context = Rule(disable: true); + A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A<RuleContext>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(true); + [Fact] + public async Task Should_not_create_job_from_snapshots_if_rule_disabled() + { + var context = Rule(disable: true); - var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(true); - Assert.Empty(jobs); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A<RuleContext>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + Assert.Empty(jobs); - [Fact] - public async Task Should_not_create_job_from_snapshots_if_no_trigger_handler_registered() - { - var context = RuleInvalidTrigger(); + A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A<RuleContext>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(true); + [Fact] + public async Task Should_not_create_job_from_snapshots_if_no_trigger_handler_registered() + { + var context = RuleInvalidTrigger(); - var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(true); - Assert.Empty(jobs); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A<RuleContext>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + Assert.Empty(jobs); - [Fact] - public async Task Should_not_create_job_from_snapshots_if_no_action_handler_registered() - { - var context = RuleInvalidAction(); + A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A<RuleContext>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(true); + [Fact] + public async Task Should_not_create_job_from_snapshots_if_no_action_handler_registered() + { + var context = RuleInvalidAction(); - var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(true); - Assert.Empty(jobs); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A<RuleContext>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + Assert.Empty(jobs); - [Fact] - public async Task Should_create_jobs_from_snapshots_if_rule_disabled_but_included() - { - var context = Rule(disable: true, includeSkipped: true); + A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(A<RuleContext>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(true); + [Fact] + public async Task Should_create_jobs_from_snapshots_if_rule_disabled_but_included() + { + var context = Rule(disable: true, includeSkipped: true); - A.CallTo(() => ruleTriggerHandler.Trigger(A<EnrichedEvent>._, context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(context, default)) - .Returns(new List<EnrichedEvent> - { - new EnrichedContentEvent { AppId = appId }, - new EnrichedContentEvent { AppId = appId } - }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.Trigger(A<EnrichedEvent>._, context)) + .Returns(true); - var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(context, default)) + .Returns(new List<EnrichedEvent> + { + new EnrichedContentEvent { AppId = appId }, + new EnrichedContentEvent { AppId = appId } + }.ToAsyncEnumerable()); - Assert.Equal(2, jobs.Count(x => x.Job != null && x.EnrichmentError == null)); - } + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - [Fact] - public async Task Should_create_jobs_from_snapshots() - { - var context = Rule(); + Assert.Equal(2, jobs.Count(x => x.Job != null && x.EnrichmentError == null)); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(true); + [Fact] + public async Task Should_create_jobs_from_snapshots() + { + var context = Rule(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<EnrichedEvent>._, context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(context, default)) - .Returns(new List<EnrichedEvent> - { - new EnrichedContentEvent { AppId = appId }, - new EnrichedContentEvent { AppId = appId } - }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.Trigger(A<EnrichedEvent>._, context)) + .Returns(true); - var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(context, default)) + .Returns(new List<EnrichedEvent> + { + new EnrichedContentEvent { AppId = appId }, + new EnrichedContentEvent { AppId = appId } + }.ToAsyncEnumerable()); - Assert.Equal(2, jobs.Count(x => x.Job != null && x.EnrichmentError == null)); - } + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - [Fact] - public async Task Should_create_jobs_with_exceptions_from_snapshots() - { - var context = Rule(); + Assert.Equal(2, jobs.Count(x => x.Job != null && x.EnrichmentError == null)); + } - A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) - .Returns(true); + [Fact] + public async Task Should_create_jobs_with_exceptions_from_snapshots() + { + var context = Rule(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<EnrichedEvent>._, context)) - .Throws(new InvalidOperationException()); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(context, default)) - .Returns(new List<EnrichedEvent> - { - new EnrichedContentEvent { AppId = appId }, - new EnrichedContentEvent { AppId = appId } - }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.Trigger(A<EnrichedEvent>._, context)) + .Throws(new InvalidOperationException()); - var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); + A.CallTo(() => ruleTriggerHandler.CreateSnapshotEventsAsync(context, default)) + .Returns(new List<EnrichedEvent> + { + new EnrichedContentEvent { AppId = appId }, + new EnrichedContentEvent { AppId = appId } + }.ToAsyncEnumerable()); - Assert.Equal(2, jobs.Count(x => x.Job == null && x.EnrichmentError != null)); - } + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_create_debug_job_for_invalid_event(bool includeSkipped) - { - var context = Rule(includeSkipped: includeSkipped); + Assert.Equal(2, jobs.Count(x => x.Job == null && x.EnrichmentError != null)); + } - var eventEnvelope = CreateEnvelope(new InvalidEvent()); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_create_debug_job_for_invalid_event(bool includeSkipped) + { + var context = Rule(includeSkipped: includeSkipped); - var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = CreateEnvelope(new InvalidEvent()); - Assert.Equal(SkipReason.WrongEvent, actual.SkipReason); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) - .MustNotHaveHappened(); - } + Assert.Equal(SkipReason.WrongEvent, actual.SkipReason); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_create_debug_job_if_no_trigger_handler_registered(bool includeSkipped) - { - var context = RuleInvalidTrigger(includeSkipped); + A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) + .MustNotHaveHappened(); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_create_debug_job_if_no_trigger_handler_registered(bool includeSkipped) + { + var context = RuleInvalidTrigger(includeSkipped); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - Assert.Equal(SkipReason.NoTrigger, job.SkipReason); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) - .MustNotHaveHappened(); - } + Assert.Equal(SkipReason.NoTrigger, job.SkipReason); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_create_debug_job_if_trigger_handler_does_not_handle_event(bool includeSkipped) - { - var context = Rule(includeSkipped: includeSkipped); + A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) + .MustNotHaveHappened(); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_create_debug_job_if_trigger_handler_does_not_handle_event(bool includeSkipped) + { + var context = Rule(includeSkipped: includeSkipped); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - Assert.Equal(SkipReason.WrongEventForTrigger, job.SkipReason); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) - .MustNotHaveHappened(); - } + Assert.Equal(SkipReason.WrongEventForTrigger, job.SkipReason); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_create_debug_job_if_no_action_handler_registered(bool includeSkipped) - { - var context = RuleInvalidAction(includeSkipped); + A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) + .MustNotHaveHappened(); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_create_debug_job_if_no_action_handler_registered(bool includeSkipped) + { + var context = RuleInvalidAction(includeSkipped); - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - Assert.Equal(SkipReason.NoAction, job.SkipReason); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) - .MustNotHaveHappened(); - } + Assert.Equal(SkipReason.NoAction, job.SkipReason); - [Fact] - public async Task Should_create_debug_job_if_rule_disabled() - { - var context = Rule(disable: true); + A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) + .MustNotHaveHappened(); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); + [Fact] + public async Task Should_create_debug_job_if_rule_disabled() + { + var context = Rule(disable: true); - var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - Assert.Equal(SkipReason.Disabled, actual.SkipReason); + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) - .MustNotHaveHappened(); - } + Assert.Equal(SkipReason.Disabled, actual.SkipReason); - [Fact] - public async Task Should_create_job_if_rule_disabled_and_skipped_included() - { - var context = Rule(disable: true, includeSkipped: true); + A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) + .MustNotHaveHappened(); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); - var eventEnriched = SetupFullFlow(context, eventEnvelope); + [Fact] + public async Task Should_create_job_if_rule_disabled_and_skipped_included() + { + var context = Rule(disable: true, includeSkipped: true); - var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - AssertJob(eventEnriched, actual, SkipReason.Disabled); - } + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_create_debug_job_if_too_old() - { - var context = Rule(); + AssertJob(eventEnriched, actual, SkipReason.Disabled); + } - var eventEnvelope = - Envelope.Create(new ContentCreated()) - .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); + [Fact] + public async Task Should_create_debug_job_if_too_old() + { + var context = Rule(); - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + var eventEnvelope = + Envelope.Create(new ContentCreated()) + .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - Assert.Equal(SkipReason.TooOld, job.SkipReason); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) - .MustNotHaveHappened(); - } + Assert.Equal(SkipReason.TooOld, job.SkipReason); - [Fact] - public async Task Should_create_job_if_too_old_but_stale_events_are_included() - { - var context = Rule(includeStale: true); + A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) + .MustNotHaveHappened(); + } - var eventEnvelope = - Envelope.Create(new ContentCreated()) - .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); - var eventEnriched = SetupFullFlow(context, eventEnvelope); + [Fact] + public async Task Should_create_job_if_too_old_but_stale_events_are_included() + { + var context = Rule(includeStale: true); - var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = + Envelope.Create(new ContentCreated()) + .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - AssertJob(eventEnriched, actual, SkipReason.None); - } + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_create_job_if_too_old_but_skipped_are_included() - { - var context = Rule(includeSkipped: true); + AssertJob(eventEnriched, actual, SkipReason.None); + } - var eventEnvelope = - Envelope.Create(new ContentCreated()) - .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); - var eventEnriched = SetupFullFlow(context, eventEnvelope); + [Fact] + public async Task Should_create_job_if_too_old_but_skipped_are_included() + { + var context = Rule(includeSkipped: true); - var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = + Envelope.Create(new ContentCreated()) + .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - AssertJob(eventEnriched, actual, SkipReason.TooOld); - } + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_create_debug_job_if_event_created_by_rule() - { - var context = Rule(); + AssertJob(eventEnriched, actual, SkipReason.TooOld); + } - var eventEnvelope = CreateEnvelope(new ContentCreated { FromRule = true }); + [Fact] + public async Task Should_create_debug_job_if_event_created_by_rule() + { + var context = Rule(); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated { FromRule = true }); - Assert.Equal(SkipReason.FromRule, job.SkipReason); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) - .MustNotHaveHappened(); + Assert.Equal(SkipReason.FromRule, job.SkipReason); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>._, A<RuleContext>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => ruleTriggerHandler.Trigger(A<Envelope<AppEvent>>._, A<RuleContext>._)) + .MustNotHaveHappened(); - [Fact] - public async Task Should_job_if_event_created_by_rule_but_skipped_are_included() - { - var context = Rule(includeSkipped: true); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>._, A<RuleContext>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - var eventEnvelope = CreateEnvelope(new ContentCreated { FromRule = true }); - var eventEnriched = SetupFullFlow(context, eventEnvelope); + [Fact] + public async Task Should_job_if_event_created_by_rule_but_skipped_are_included() + { + var context = Rule(includeSkipped: true); - var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated { FromRule = true }); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - AssertJob(eventEnriched, actual, SkipReason.FromRule); - } + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_create_debug_job_if_not_triggered_with_precheck() - { - var context = Rule(); + AssertJob(eventEnriched, actual, SkipReason.FromRule); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); - var eventEnriched = new EnrichedContentEvent { AppId = appId }; + [Fact] + public async Task Should_create_debug_job_if_not_triggered_with_precheck() + { + var context = Rule(); - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = new EnrichedContentEvent { AppId = appId }; - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) - .Returns(false); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) - .Returns(new List<EnrichedEvent> { eventEnriched }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(false); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(new List<EnrichedEvent> { eventEnriched }.ToAsyncEnumerable()); - Assert.Equal(SkipReason.ConditionPrecheckDoesNotMatch, job.SkipReason); - Assert.Null(job.EnrichedEvent); - Assert.Null(job.Job); - } + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_create_job_if_not_triggered_with_precheck_but_skipped_are_included() - { - var context = Rule(includeSkipped: true); + Assert.Equal(SkipReason.ConditionPrecheckDoesNotMatch, job.SkipReason); + Assert.Null(job.EnrichedEvent); + Assert.Null(job.Job); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); - var eventEnriched = SetupFullFlow(context, eventEnvelope); + [Fact] + public async Task Should_create_job_if_not_triggered_with_precheck_but_skipped_are_included() + { + var context = Rule(includeSkipped: true); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) - .Returns(false); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(false); - AssertJob(eventEnriched, actual, SkipReason.ConditionPrecheckDoesNotMatch); - } + var actual = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_create_debug_job_if_condition_check_failed() - { - var context = Rule(); + AssertJob(eventEnriched, actual, SkipReason.ConditionPrecheckDoesNotMatch); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); + [Fact] + public async Task Should_create_debug_job_if_condition_check_failed() + { + var context = Rule(); - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) - .Throws(new InvalidOperationException()); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Throws(new InvalidOperationException()); - Assert.Equal(SkipReason.Failed, job.SkipReason); - } + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_not_create_jobs_if_enriched_event_not_created() - { - var context = Rule(); + Assert.Equal(SkipReason.Failed, job.SkipReason); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); + [Fact] + public async Task Should_not_create_jobs_if_enriched_event_not_created() + { + var context = Rule(); - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) - .Returns(AsyncEnumerable.Empty<EnrichedEvent>()); + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(true); - var job = await sut.CreateJobsAsync(eventEnvelope, context).ToListAsync(); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(AsyncEnumerable.Empty<EnrichedEvent>()); - Assert.Empty(job); - } + var job = await sut.CreateJobsAsync(eventEnvelope, context).ToListAsync(); - [Fact] - public async Task Should_create_debug_job_if_not_triggered() - { - var context = Rule(includeSkipped: true); + Assert.Empty(job); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); - var eventEnriched = new EnrichedContentEvent { AppId = appId }; + [Fact] + public async Task Should_create_debug_job_if_not_triggered() + { + var context = Rule(includeSkipped: true); - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = new EnrichedContentEvent { AppId = appId }; - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) - .Returns(false); + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) - .Returns(new List<EnrichedEvent> { eventEnriched }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) + .Returns(false); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(new List<EnrichedEvent> { eventEnriched }.ToAsyncEnumerable()); - Assert.Equal(SkipReason.ConditionDoesNotMatch, job.SkipReason); - Assert.Equal(eventEnriched, job.EnrichedEvent); - } + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_debug_job_if_not_triggered_but_skipped_included() - { - var context = Rule(includeSkipped: true); + Assert.Equal(SkipReason.ConditionDoesNotMatch, job.SkipReason); + Assert.Equal(eventEnriched, job.EnrichedEvent); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); - var eventEnriched = SetupFullFlow(context, eventEnvelope); + [Fact] + public async Task Should_debug_job_if_not_triggered_but_skipped_included() + { + var context = Rule(includeSkipped: true); - A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) - .Returns(false); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) + .Returns(false); - AssertJob(eventEnriched, job, SkipReason.ConditionDoesNotMatch); - } + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_create_debug_job_if_enrichment_failed() - { - var context = Rule(); + AssertJob(eventEnriched, job, SkipReason.ConditionDoesNotMatch); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); + [Fact] + public async Task Should_create_debug_job_if_enrichment_failed() + { + var context = Rule(); - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) - .Throws(new InvalidOperationException()); + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(true); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Throws(new InvalidOperationException()); - Assert.Equal(SkipReason.Failed, job.SkipReason); - } + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - [Fact] - public async Task Should_create_job_if_triggered() - { - var context = Rule(); + Assert.Equal(SkipReason.Failed, job.SkipReason); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); - var eventEnriched = SetupFullFlow(context, eventEnvelope); + [Fact] + public async Task Should_create_job_if_triggered() + { + var context = Rule(); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - AssertJob(eventEnriched, job, SkipReason.None); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched, MatchPayload(eventEnvelope))) - .MustHaveHappened(); - } + AssertJob(eventEnriched, job, SkipReason.None); - [Fact] - public async Task Should_create_job_with_exception_if_trigger_failed() - { - var context = Rule(); + A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched, MatchPayload(eventEnvelope))) + .MustHaveHappened(); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); - var eventEnriched = SetupFullFlow(context, eventEnvelope); + [Fact] + public async Task Should_create_job_with_exception_if_trigger_failed() + { + var context = Rule(); - A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched, context.Rule.Action)) - .Throws(new InvalidOperationException()); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched, context.Rule.Action)) + .Throws(new InvalidOperationException()); - Assert.NotNull(job.EnrichmentError); - Assert.NotNull(job.Job?.ActionData); - Assert.NotNull(job.Job?.Description); - Assert.Equal(eventEnriched, job.EnrichedEvent); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched, MatchPayload(eventEnvelope))) - .MustHaveHappened(); - } + Assert.NotNull(job.EnrichmentError); + Assert.NotNull(job.Job?.ActionData); + Assert.NotNull(job.Job?.Description); + Assert.Equal(eventEnriched, job.EnrichedEvent); - [Fact] - public async Task Should_create_multiple_jobs_if_triggered() - { - var context = Rule(); + A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched, MatchPayload(eventEnvelope))) + .MustHaveHappened(); + } - var eventEnvelope = CreateEnvelope(new ContentCreated()); - var eventEnriched1 = new EnrichedContentEvent { AppId = appId }; - var eventEnriched2 = new EnrichedContentEvent { AppId = appId }; + [Fact] + public async Task Should_create_multiple_jobs_if_triggered() + { + var context = Rule(); - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched1 = new EnrichedContentEvent { AppId = appId }; + var eventEnriched2 = new EnrichedContentEvent { AppId = appId }; - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched1, context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched2, context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched1, context)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) - .Returns(new List<EnrichedEvent> { eventEnriched1, eventEnriched2 }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched2, context)) + .Returns(true); - A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched1, context.Rule.Action)) - .Returns((actionDescription, new ValidData { Value = 10 })); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(new List<EnrichedEvent> { eventEnriched1, eventEnriched2 }.ToAsyncEnumerable()); - A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched2, context.Rule.Action)) - .Returns((actionDescription, new ValidData { Value = 10 })); + A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched1, context.Rule.Action)) + .Returns((actionDescription, new ValidData { Value = 10 })); - var jobs = await sut.CreateJobsAsync(eventEnvelope, context, default).ToListAsync(); + A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched2, context.Rule.Action)) + .Returns((actionDescription, new ValidData { Value = 10 })); - AssertJob(eventEnriched1, jobs[0], SkipReason.None); - AssertJob(eventEnriched2, jobs[1], SkipReason.None); + var jobs = await sut.CreateJobsAsync(eventEnvelope, context, default).ToListAsync(); - A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched1, MatchPayload(eventEnvelope))) - .MustHaveHappened(); + AssertJob(eventEnriched1, jobs[0], SkipReason.None); + AssertJob(eventEnriched2, jobs[1], SkipReason.None); - A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched2, MatchPayload(eventEnvelope))) - .MustHaveHappened(); - } + A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched1, MatchPayload(eventEnvelope))) + .MustHaveHappened(); - [Fact] - public async Task Should_return_succeeded_job_with_full_dump_if_handler_returns_no_exception() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A<ValidData>.That.Matches(x => x.Value == 10), A<CancellationToken>._)) - .Returns(Result.Success(actionDump)); + A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched2, MatchPayload(eventEnvelope))) + .MustHaveHappened(); + } - var actual = await sut.InvokeAsync(actionName, actionData); + [Fact] + public async Task Should_return_succeeded_job_with_full_dump_if_handler_returns_no_exception() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A<ValidData>.That.Matches(x => x.Value == 10), A<CancellationToken>._)) + .Returns(Result.Success(actionDump)); - Assert.Equal(RuleResult.Success, actual.Result.Status); + var actual = await sut.InvokeAsync(actionName, actionData); - Assert.True(actual.Elapsed >= TimeSpan.Zero); - Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - } + Assert.Equal(RuleResult.Success, actual.Result.Status); - [Fact] - public async Task Should_return_failed_job_with_full_dump_if_handler_returns_exception() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A<ValidData>.That.Matches(x => x.Value == 10), A<CancellationToken>._)) - .Returns(Result.Failed(new InvalidOperationException(), actionDump)); + Assert.True(actual.Elapsed >= TimeSpan.Zero); + Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + } - var actual = await sut.InvokeAsync(actionName, actionData); + [Fact] + public async Task Should_return_failed_job_with_full_dump_if_handler_returns_exception() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A<ValidData>.That.Matches(x => x.Value == 10), A<CancellationToken>._)) + .Returns(Result.Failed(new InvalidOperationException(), actionDump)); - Assert.Equal(RuleResult.Failed, actual.Result.Status); + var actual = await sut.InvokeAsync(actionName, actionData); - Assert.True(actual.Elapsed >= TimeSpan.Zero); - Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - } + Assert.Equal(RuleResult.Failed, actual.Result.Status); - [Fact] - public async Task Should_return_timedout_job_with_full_dump_if_exception_from_handler_indicates_timeout() - { - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A<ValidData>.That.Matches(x => x.Value == 10), A<CancellationToken>._)) - .Returns(Result.Failed(new TimeoutException(), actionDump)); + Assert.True(actual.Elapsed >= TimeSpan.Zero); + Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + } - var actual = await sut.InvokeAsync(actionName, actionData); + [Fact] + public async Task Should_return_timedout_job_with_full_dump_if_exception_from_handler_indicates_timeout() + { + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A<ValidData>.That.Matches(x => x.Value == 10), A<CancellationToken>._)) + .Returns(Result.Failed(new TimeoutException(), actionDump)); - Assert.Equal(RuleResult.Timeout, actual.Result.Status); + var actual = await sut.InvokeAsync(actionName, actionData); - Assert.True(actual.Elapsed >= TimeSpan.Zero); - Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); + Assert.Equal(RuleResult.Timeout, actual.Result.Status); - Assert.True(actual.Result.Dump?.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); - } + Assert.True(actual.Elapsed >= TimeSpan.Zero); + Assert.True(actual.Result.Dump?.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); - [Fact] - public async Task Should_create_exception_details_if_job_to_execute_failed() - { - var ex = new InvalidOperationException(); + Assert.True(actual.Result.Dump?.IndexOf("Action timed out.", StringComparison.OrdinalIgnoreCase) >= 0); + } - A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A<ValidData>.That.Matches(x => x.Value == 10), A<CancellationToken>._)) - .Throws(ex); + [Fact] + public async Task Should_create_exception_details_if_job_to_execute_failed() + { + var ex = new InvalidOperationException(); - var actual = await sut.InvokeAsync(actionName, actionData); + A.CallTo(() => ruleActionHandler.ExecuteJobAsync(A<ValidData>.That.Matches(x => x.Value == 10), A<CancellationToken>._)) + .Throws(ex); - Assert.Equal(ex, actual.Result.Exception); - } + var actual = await sut.InvokeAsync(actionName, actionData); - private EnrichedContentEvent SetupFullFlow<T>(RuleContext context, Envelope<T> eventEnvelope) where T : AppEvent - { - var eventEnriched = new EnrichedContentEvent { AppId = appId }; + Assert.Equal(ex, actual.Result.Exception); + } - A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) - .Returns(true); + private EnrichedContentEvent SetupFullFlow<T>(RuleContext context, Envelope<T> eventEnvelope) where T : AppEvent + { + var eventEnriched = new EnrichedContentEvent { AppId = appId }; - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) - .Returns(true); + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) - .Returns(new List<EnrichedEvent> { eventEnriched }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) + .Returns(true); - A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched, context.Rule.Action)) - .Returns((actionDescription, new ValidData { Value = 10 })); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(new List<EnrichedEvent> { eventEnriched }.ToAsyncEnumerable()); - return eventEnriched; - } + A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched, context.Rule.Action)) + .Returns((actionDescription, new ValidData { Value = 10 })); - private RuleContext RuleInvalidAction(bool includeSkipped = false) - { - return new RuleContext - { - AppId = appId, - Rule = new Rule(new ContentChangedTriggerV2(), new InvalidAction()), - RuleId = ruleId, - IncludeSkipped = includeSkipped - }; - } + return eventEnriched; + } - private RuleContext RuleInvalidTrigger(bool includeSkipped = false) + private RuleContext RuleInvalidAction(bool includeSkipped = false) + { + return new RuleContext { - return new RuleContext - { - AppId = appId, - Rule = new Rule(new InvalidTrigger(), new ValidAction()), - RuleId = ruleId, - IncludeSkipped = includeSkipped - }; - } + AppId = appId, + Rule = new Rule(new ContentChangedTriggerV2(), new InvalidAction()), + RuleId = ruleId, + IncludeSkipped = includeSkipped + }; + } - private RuleContext Rule(bool disable = false, bool includeStale = false, bool includeSkipped = false) + private RuleContext RuleInvalidTrigger(bool includeSkipped = false) + { + return new RuleContext { - var rule = new Rule(new ContentChangedTriggerV2(), new ValidAction()); - - if (disable) - { - rule = rule.Disable(); - } + AppId = appId, + Rule = new Rule(new InvalidTrigger(), new ValidAction()), + RuleId = ruleId, + IncludeSkipped = includeSkipped + }; + } - return new RuleContext - { - AppId = appId, - Rule = rule, - RuleId = ruleId, - IncludeStale = includeStale, - IncludeSkipped = includeSkipped - }; - } + private RuleContext Rule(bool disable = false, bool includeStale = false, bool includeSkipped = false) + { + var rule = new Rule(new ContentChangedTriggerV2(), new ValidAction()); - private Envelope<T> CreateEnvelope<T>(T @event) where T : class, IEvent + if (disable) { - return Envelope.Create(@event).SetTimestamp(clock.GetCurrentInstant()); + rule = rule.Disable(); } - private static Envelope<AppEvent> MatchPayload(Envelope<IEvent> eventEnvelope) + return new RuleContext { - return A<Envelope<AppEvent>>.That.Matches(x => x.Payload == eventEnvelope.Payload); - } + AppId = appId, + Rule = rule, + RuleId = ruleId, + IncludeStale = includeStale, + IncludeSkipped = includeSkipped + }; + } - private void AssertJob(EnrichedContentEvent eventEnriched, JobResult actual, SkipReason skipped) - { - var now = clock.GetCurrentInstant(); + private Envelope<T> CreateEnvelope<T>(T @event) where T : class, IEvent + { + return Envelope.Create(@event).SetTimestamp(clock.GetCurrentInstant()); + } - var job = actual.Job!; + private static Envelope<AppEvent> MatchPayload(Envelope<IEvent> eventEnvelope) + { + return A<Envelope<AppEvent>>.That.Matches(x => x.Payload == eventEnvelope.Payload); + } + + private void AssertJob(EnrichedContentEvent eventEnriched, JobResult actual, SkipReason skipped) + { + var now = clock.GetCurrentInstant(); - Assert.Equal(skipped, actual.SkipReason); + var job = actual.Job!; - Assert.Equal(eventEnriched, actual.EnrichedEvent); - Assert.Equal(eventEnriched.AppId.Id, job.AppId); + Assert.Equal(skipped, actual.SkipReason); - Assert.Equal(actionData, job.ActionData); - Assert.Equal(actionName, job.ActionName); - Assert.Equal(actionDescription, job.Description); + Assert.Equal(eventEnriched, actual.EnrichedEvent); + Assert.Equal(eventEnriched.AppId.Id, job.AppId); - Assert.Equal(now, job.Created); - Assert.Equal(now.Plus(Duration.FromDays(30)), job.Expires); + Assert.Equal(actionData, job.ActionData); + Assert.Equal(actionName, job.ActionName); + Assert.Equal(actionDescription, job.Description); - Assert.NotEqual(DomainId.Empty, job.Id); - } + Assert.Equal(now, job.Created); + Assert.Equal(now.Plus(Duration.FromDays(30)), job.Expires); + + Assert.NotEqual(DomainId.Empty, job.Id); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs index 265f068eb9..480ad1cea2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs @@ -12,335 +12,335 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting; + +public class ContentDataObjectTests { - public class ContentDataObjectTests + [Fact] + public void Should_update_data_if_setting_field() { - [Fact] - public void Should_update_data_if_setting_field() - { - var original = new ContentData(); - - var expected = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(1.0)); - - const string script = @" + var original = new ContentData(); + + var expected = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(1.0)); + + const string script = @" data.number = { iv: 1 } "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_update_data_if_setting_lazy_field() - { - var original = new ContentData(); + [Fact] + public void Should_update_data_if_setting_lazy_field() + { + var original = new ContentData(); - var expected = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(1.0)); + var expected = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(1.0)); - const string script = @" + const string script = @" data.number.iv = 1 "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_update_data_defining_property_for_content() - { - var original = new ContentData(); + [Fact] + public void Should_update_data_defining_property_for_content() + { + var original = new ContentData(); - var expected = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(1.0)); + var expected = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(1.0)); - const string script = @" + const string script = @" Object.defineProperty(data, 'number', { value: { iv: 1 } }) "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_throw_exception_if_assigning_non_object_as_field() - { - var original = new ContentData(); + [Fact] + public void Should_throw_exception_if_assigning_non_object_as_field() + { + var original = new ContentData(); - const string script = @" + const string script = @" data.number = 1 "; - Assert.Throws<JavaScriptException>(() => ExecuteScript(original, script)); - } + Assert.Throws<JavaScriptException>(() => ExecuteScript(original, script)); + } - [Fact] - public void Should_update_data_if_deleting_field() - { - var original = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(1.0)); + [Fact] + public void Should_update_data_if_deleting_field() + { + var original = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(1.0)); - var expected = new ContentData(); + var expected = new ContentData(); - const string script = @" + const string script = @" delete data.number "; - var actual = ExecuteScript(original, script); - - Assert.Equal(expected, actual); - } + var actual = ExecuteScript(original, script); - [Fact] - public void Should_update_data_if_setting_field_value_with_string() - { - var original = - new ContentData() - .AddField("string", - new ContentFieldData() - .AddInvariant("1")); - - var expected = - new ContentData() - .AddField("string", - new ContentFieldData() - .AddInvariant("1new")); + Assert.Equal(expected, actual); + } - const string script = @" + [Fact] + public void Should_update_data_if_setting_field_value_with_string() + { + var original = + new ContentData() + .AddField("string", + new ContentFieldData() + .AddInvariant("1")); + + var expected = + new ContentData() + .AddField("string", + new ContentFieldData() + .AddInvariant("1new")); + + const string script = @" data.string.iv = data.string.iv + 'new' "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_update_data_if_setting_field_value_with_number() - { - var original = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(1.0)); - - var expected = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(3.0)); + Assert.Equal(expected, actual); + } - const string script = @" + [Fact] + public void Should_update_data_if_setting_field_value_with_number() + { + var original = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(1.0)); + + var expected = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(3.0)); + + const string script = @" data.number.iv = data.number.iv + 2 "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_update_data_if_setting_field_value_with_boolean() - { - var original = - new ContentData() - .AddField("boolean", - new ContentFieldData() - .AddInvariant(false)); - - var expected = - new ContentData() - .AddField("boolean", - new ContentFieldData() - .AddInvariant(true)); + Assert.Equal(expected, actual); + } - const string script = @" + [Fact] + public void Should_update_data_if_setting_field_value_with_boolean() + { + var original = + new ContentData() + .AddField("boolean", + new ContentFieldData() + .AddInvariant(false)); + + var expected = + new ContentData() + .AddField("boolean", + new ContentFieldData() + .AddInvariant(true)); + + const string script = @" data.boolean.iv = !data.boolean.iv "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_update_data_if_setting_field_value_with_array() - { - var original = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1.0, 2.0))); - - var expected = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(JsonValue.Array(1.0, 4.0, 5.0))); + Assert.Equal(expected, actual); + } - const string script = @" + [Fact] + public void Should_update_data_if_setting_field_value_with_array() + { + var original = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1.0, 2.0))); + + var expected = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(JsonValue.Array(1.0, 4.0, 5.0))); + + const string script = @" data.number.iv = [data.number.iv[0], data.number.iv[1] + 2, 5] "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_update_data_if_setting_field_value_with_object() - { - var original = - new ContentData() - .AddField("geo", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("lat", 1.0))); - - var expected = - new ContentData() - .AddField("geo", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("lat", 1.0).Add("lon", 4.0))); + Assert.Equal(expected, actual); + } - const string script = @" + [Fact] + public void Should_update_data_if_setting_field_value_with_object() + { + var original = + new ContentData() + .AddField("geo", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("lat", 1.0))); + + var expected = + new ContentData() + .AddField("geo", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("lat", 1.0).Add("lon", 4.0))); + + const string script = @" data.geo.iv = { lat: data.geo.iv.lat, lon: data.geo.iv.lat + 3 } "; - var actual = ExecuteScript(original, script); - - Assert.Equal(expected, actual); - } + var actual = ExecuteScript(original, script); - [Fact] - public void Should_update_data_if_changing_nested_field() - { - var original = - new ContentData() - .AddField("geo", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("lat", 1.0))); - - var expected = - new ContentData() - .AddField("geo", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("lat", 2.0).Add("lon", 4.0))); + Assert.Equal(expected, actual); + } - const string script = @" + [Fact] + public void Should_update_data_if_changing_nested_field() + { + var original = + new ContentData() + .AddField("geo", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("lat", 1.0))); + + var expected = + new ContentData() + .AddField("geo", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("lat", 2.0).Add("lon", 4.0))); + + const string script = @" var nested = data.geo.iv; nested.lat = 2; nested.lon = 4; data.geo.iv = nested "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_not_update_data_if_not_changing_nested_field() - { - var original = - new ContentData() - .AddField("geo", - new ContentFieldData() - .AddInvariant(new JsonObject().Add("lat", 2.0).Add("lon", 4.0))); + [Fact] + public void Should_not_update_data_if_not_changing_nested_field() + { + var original = + new ContentData() + .AddField("geo", + new ContentFieldData() + .AddInvariant(new JsonObject().Add("lat", 2.0).Add("lon", 4.0))); - const string script = @" + const string script = @" var nested = data.geo.iv; nested.lat = 2; nested.lon = 4; data.geo.iv = nested "; - var actual = ExecuteScript(original, script); - - Assert.Same(original, actual); - } + var actual = ExecuteScript(original, script); - [Fact] - public void Should_throw_if_defining_property_for_field() - { - var original = - new ContentData() - .AddField("number", - new ContentFieldData()); - - var expected = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(1.0)); + Assert.Same(original, actual); + } - const string script = @" + [Fact] + public void Should_throw_if_defining_property_for_field() + { + var original = + new ContentData() + .AddField("number", + new ContentFieldData()); + + var expected = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(1.0)); + + const string script = @" Object.defineProperty(data.number, 'iv', { value: 1 }) "; - var actual = ExecuteScript(original, script); - - Assert.Equal(expected, actual); - } + var actual = ExecuteScript(original, script); - [Fact] - public void Should_update_data_if_deleting_field_value() - { - var original = - new ContentData() - .AddField("string", - new ContentFieldData() - .AddInvariant("hello")); - - var expected = - new ContentData() - .AddField("string", - new ContentFieldData()); + Assert.Equal(expected, actual); + } - const string script = @" + [Fact] + public void Should_update_data_if_deleting_field_value() + { + var original = + new ContentData() + .AddField("string", + new ContentFieldData() + .AddInvariant("hello")); + + var expected = + new ContentData() + .AddField("string", + new ContentFieldData()); + + const string script = @" delete data.string.iv "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_be_able_to_iterate_over_fields() - { - var content = - new ContentData() - .AddField("f1", - new ContentFieldData() - .AddLocalized("v11", "1") - .AddLocalized("v12", "2")) - .AddField("f2", - new ContentFieldData() - .AddLocalized("v21", "3") - .AddLocalized("v22", "4")); - - var engine = new Engine(); - - engine.SetValue("data", new ContentDataObject(engine, content)); + Assert.Equal(expected, actual); + } - const string script = @" + [Fact] + public void Should_be_able_to_iterate_over_fields() + { + var content = + new ContentData() + .AddField("f1", + new ContentFieldData() + .AddLocalized("v11", "1") + .AddLocalized("v12", "2")) + .AddField("f2", + new ContentFieldData() + .AddLocalized("v21", "3") + .AddLocalized("v22", "4")); + + var engine = new Engine(); + + engine.SetValue("data", new ContentDataObject(engine, content)); + + const string script = @" var actual = []; for (var x in data) { var field = data[x]; @@ -352,76 +352,75 @@ public void Should_be_able_to_iterate_over_fields() actual; "; - var actual = engine.Evaluate(script).ToObject(); + var actual = engine.Evaluate(script).ToObject(); - Assert.Equal(new[] { "1", "2", "3", "4" }, actual); - } + Assert.Equal(new[] { "1", "2", "3", "4" }, actual); + } - [Fact] - public void Should_not_throw_exceptions_if_changing_arrays() - { - var original = - new ContentData() - .AddField("obj", - new ContentFieldData() - .AddInvariant(new JsonArray())); + [Fact] + public void Should_not_throw_exceptions_if_changing_arrays() + { + var original = + new ContentData() + .AddField("obj", + new ContentFieldData() + .AddInvariant(new JsonArray())); - const string script = @" + const string script = @" data.obj.iv[0] = 1 "; - ExecuteScript(original, script); - } - - [Theory] - [InlineData("NaN")] - [InlineData("Number.POSITIVE_INFINITY")] - [InlineData("Number.NEGATIVE_INFINITY")] - public void Should_not_throw_exceptions_if_invalid_numbers(string input) - { - var original = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(new JsonArray())); - - var expected = - new ContentData() - .AddField("number", - new ContentFieldData() - .AddInvariant(JsonValue.Zero)); - - string script = $@" + ExecuteScript(original, script); + } + + [Theory] + [InlineData("NaN")] + [InlineData("Number.POSITIVE_INFINITY")] + [InlineData("Number.NEGATIVE_INFINITY")] + public void Should_not_throw_exceptions_if_invalid_numbers(string input) + { + var original = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(new JsonArray())); + + var expected = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(JsonValue.Zero)); + + string script = $@" data.number.iv = {input}; "; - var actual = ExecuteScript(original, script); + var actual = ExecuteScript(original, script); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_null_propagate_unknown_fields() - { - const string script = @" + [Fact] + public void Should_null_propagate_unknown_fields() + { + const string script = @" data.string.iv = 'hello' "; - ExecuteScript(new ContentData(), script); - } + ExecuteScript(new ContentData(), script); + } - private static ContentData ExecuteScript(ContentData original, string script) - { - var engine = new Engine(o => o.Strict()); + private static ContentData ExecuteScript(ContentData original, string script) + { + var engine = new Engine(o => o.Strict()); - var value = new ContentDataObject(engine, original); + var value = new ContentDataObject(engine, original); - engine.SetValue("data", value); - engine.Execute(script); + engine.SetValue("data", value); + engine.Execute(script); - value.TryUpdate(out var actual); + value.TryUpdate(out var actual); - return actual; - } + return actual; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs index b6802c391b..5287daf46f 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs @@ -18,361 +18,361 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting; + +public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture> { - public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture> - { - private readonly IHttpClientFactory httpClientFactory = A.Fake<IHttpClientFactory>(); - private readonly JintScriptEngine sut; + private readonly IHttpClientFactory httpClientFactory = A.Fake<IHttpClientFactory>(); + private readonly JintScriptEngine sut; - public JintScriptEngineHelperTests() + public JintScriptEngineHelperTests() + { + var extensions = new IJintExtension[] { - var extensions = new IJintExtension[] + new DateTimeJintExtension(), + new HttpJintExtension(httpClientFactory), + new StringJintExtension(), + new StringWordsJintExtension() + }; + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions { - new DateTimeJintExtension(), - new HttpJintExtension(httpClientFactory), - new StringJintExtension(), - new StringWordsJintExtension() - }; - - sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - }), - extensions); - } - - [Fact] - public void Should_convert_html_to_text() + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + }), + extensions); + } + + [Fact] + public void Should_convert_html_to_text() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "<script>Invalid</script><STYLE>Invalid</STYLE><p>Hello World</p>" - }; + ["value"] = "<script>Invalid</script><STYLE>Invalid</STYLE><p>Hello World</p>" + }; - const string script = @" + const string script = @" return html2Text(value); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("Hello World", actual); - } + Assert.Equal("Hello World", actual); + } - [Fact] - public void Should_convert_markdown_to_text() + [Fact] + public void Should_convert_markdown_to_text() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "## Hello World" - }; + ["value"] = "## Hello World" + }; - const string script = @" + const string script = @" return markdown2Text(value); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("Hello World", actual); - } + Assert.Equal("Hello World", actual); + } - [Fact] - public void Should_count_words() + [Fact] + public void Should_count_words() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "Hello, World" - }; + ["value"] = "Hello, World" + }; - const string script = @" + const string script = @" return wordCount(value); "; - var actual = sut.Execute(vars, script).AsNumber; + var actual = sut.Execute(vars, script).AsNumber; - Assert.Equal(2, actual); - } + Assert.Equal(2, actual); + } - [Fact] - public void Should_count_characters() + [Fact] + public void Should_count_characters() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "Hello, World" - }; + ["value"] = "Hello, World" + }; - const string script = @" + const string script = @" return characterCount(value); "; - var actual = sut.Execute(vars, script).AsNumber; + var actual = sut.Execute(vars, script).AsNumber; - Assert.Equal(10, actual); - } + Assert.Equal(10, actual); + } - [Fact] - public void Should_camel_case_value() + [Fact] + public void Should_camel_case_value() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "Hello World" - }; + ["value"] = "Hello World" + }; - const string script = @" + const string script = @" return toCamelCase(value); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("helloWorld", actual); - } + Assert.Equal("helloWorld", actual); + } - [Fact] - public void Should_pascal_case_value() + [Fact] + public void Should_pascal_case_value() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "Hello World" - }; + ["value"] = "Hello World" + }; - const string script = @" + const string script = @" return toPascalCase(value); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("HelloWorld", actual); - } + Assert.Equal("HelloWorld", actual); + } - [Fact] - public void Should_slugify_value() + [Fact] + public void Should_slugify_value() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "4 Häuser" - }; + ["value"] = "4 Häuser" + }; - const string script = @" + const string script = @" return slugify(value); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("4-haeuser", actual); - } + Assert.Equal("4-haeuser", actual); + } - [Fact] - public void Should_slugify_value_with_single_char() + [Fact] + public void Should_slugify_value_with_single_char() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "4 Häuser" - }; + ["value"] = "4 Häuser" + }; - const string script = @" + const string script = @" return slugify(value, true); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("4-hauser", actual); - } + Assert.Equal("4-hauser", actual); + } - [Fact] - public void Should_compute_sha256_hash() + [Fact] + public void Should_compute_sha256_hash() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "HelloWorld" - }; + ["value"] = "HelloWorld" + }; - const string script = @" + const string script = @" return sha256(value); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("HelloWorld".ToSha256(), actual); - } + Assert.Equal("HelloWorld".ToSha256(), actual); + } - [Fact] - public void Should_compute_sha512_hash() + [Fact] + public void Should_compute_sha512_hash() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "HelloWorld" - }; + ["value"] = "HelloWorld" + }; - const string script = @" + const string script = @" return sha512(value); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("HelloWorld".ToSha512(), actual); - } + Assert.Equal("HelloWorld".ToSha512(), actual); + } - [Fact] - public void Should_compute_md5_hash() + [Fact] + public void Should_compute_md5_hash() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = "HelloWorld" - }; + ["value"] = "HelloWorld" + }; - const string script = @" + const string script = @" return md5(value); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.Equal("HelloWorld".ToMD5(), actual); - } + Assert.Equal("HelloWorld".ToMD5(), actual); + } - [Fact] - public void Should_compute_guid() + [Fact] + public void Should_compute_guid() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - }; + }; - const string script = @" + const string script = @" return guid(); "; - var actual = sut.Execute(vars, script).AsString; + var actual = sut.Execute(vars, script).AsString; - Assert.True(Guid.TryParse(actual, out _)); - } + Assert.True(Guid.TryParse(actual, out _)); + } - [Fact] - public async Task Should_throw_validation_exception_if_calling_reject() + [Fact] + public async Task Should_throw_validation_exception_if_calling_reject() + { + var options = new ScriptOptions { - var options = new ScriptOptions - { - CanReject = true - }; + CanReject = true + }; - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" reject() "; - var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options)); + var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options)); - Assert.NotEmpty(ex.Errors); - } + Assert.NotEmpty(ex.Errors); + } - [Fact] - public async Task Should_throw_validation_exception_if_calling_reject_with_message() + [Fact] + public async Task Should_throw_validation_exception_if_calling_reject_with_message() + { + var options = new ScriptOptions { - var options = new ScriptOptions - { - CanReject = true - }; + CanReject = true + }; - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" reject('Not valid') "; - var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options)); + var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options)); - Assert.Equal("Not valid", ex.Errors.Single().Message); - } + Assert.Equal("Not valid", ex.Errors.Single().Message); + } - [Fact] - public async Task Should_throw_security_exception_if_calling_reject() + [Fact] + public async Task Should_throw_security_exception_if_calling_reject() + { + var options = new ScriptOptions { - var options = new ScriptOptions - { - CanDisallow = true - }; + CanDisallow = true + }; - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" disallow() "; - var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options)); + var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options)); - Assert.Equal("Script has forbidden the operation.", ex.Message); - } + Assert.Equal("Script has forbidden the operation.", ex.Message); + } - [Fact] - public async Task Should_throw_security_exception_if_calling_reject_with_message() - { - const string script = @" + [Fact] + public async Task Should_throw_security_exception_if_calling_reject_with_message() + { + const string script = @" disallow('Operation not allowed') "; - var options = new ScriptOptions - { - CanDisallow = true - }; + var options = new ScriptOptions + { + CanDisallow = true + }; - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options)); + var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options)); - Assert.Equal("Operation not allowed", ex.Message); - } + Assert.Equal("Operation not allowed", ex.Message); + } - [Fact] - public async Task Should_throw_exception_if_getJson_url_is_null() + [Fact] + public async Task Should_throw_exception_if_getJson_url_is_null() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - }; + }; - const string script = @" + const string script = @" getJSON(null, function(actual) { complete(actual); }); "; - await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script)); - } + await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script)); + } - [Fact] - public async Task Should_throw_exception_if_getJson_callback_is_null() + [Fact] + public async Task Should_throw_exception_if_getJson_callback_is_null() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - }; + }; - const string script = @" + const string script = @" var url = 'http://squidex.io'; getJSON(url, null); "; - await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script)); - } + await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script)); + } - [Fact] - public async Task Should_make_getJson_request() - { - var httpHandler = SetupRequest(); + [Fact] + public async Task Should_make_getJson_request() + { + var httpHandler = SetupRequest(); - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" var url = 'http://squidex.io'; getJSON(url, function(actual) { @@ -380,26 +380,26 @@ public async Task Should_make_getJson_request() }); "; - var actual = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); - httpHandler.ShouldBeMethod(HttpMethod.Get); - httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeMethod(HttpMethod.Get); + httpHandler.ShouldBeUrl("http://squidex.io/"); - var expectedResult = new JsonObject().Add("key", 42); + var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, actual); - } + Assert.Equal(expectedResult, actual); + } - [Fact] - public async Task Should_make_getJson_request_with_headers() - { - var httpHandler = SetupRequest(); + [Fact] + public async Task Should_make_getJson_request_with_headers() + { + var httpHandler = SetupRequest(); - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" var headers = { 'X-Header1': 1, 'X-Header2': '2' @@ -412,28 +412,28 @@ public async Task Should_make_getJson_request_with_headers() }, headers); "; - var actual = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); - httpHandler.ShouldBeMethod(HttpMethod.Get); - httpHandler.ShouldBeUrl("http://squidex.io/"); - httpHandler.ShouldBeHeader("X-Header1", "1"); - httpHandler.ShouldBeHeader("X-Header2", "2"); + httpHandler.ShouldBeMethod(HttpMethod.Get); + httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeHeader("X-Header1", "1"); + httpHandler.ShouldBeHeader("X-Header2", "2"); - var expectedResult = new JsonObject().Add("key", 42); + var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, actual); - } + Assert.Equal(expectedResult, actual); + } - [Fact] - public async Task Should_make_deleteJson_request() - { - var httpHandler = SetupRequest(); + [Fact] + public async Task Should_make_deleteJson_request() + { + var httpHandler = SetupRequest(); - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" var url = 'http://squidex.io'; deleteJSON(url, function(actual) { @@ -441,26 +441,26 @@ public async Task Should_make_deleteJson_request() }); "; - var actual = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); - httpHandler.ShouldBeMethod(HttpMethod.Delete); - httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeMethod(HttpMethod.Delete); + httpHandler.ShouldBeUrl("http://squidex.io/"); - var expectedResult = new JsonObject().Add("key", 42); + var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, actual); - } + Assert.Equal(expectedResult, actual); + } - [Fact] - public async Task Should_make_patchJson_request() - { - var httpHandler = SetupRequest(); + [Fact] + public async Task Should_make_patchJson_request() + { + var httpHandler = SetupRequest(); - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" var url = 'http://squidex.io'; var body = { key: 42 }; @@ -470,27 +470,27 @@ public async Task Should_make_patchJson_request() }); "; - var actual = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); - httpHandler.ShouldBeMethod(HttpMethod.Patch); - httpHandler.ShouldBeUrl("http://squidex.io/"); - httpHandler.ShouldBeBody("{\"key\":42}", "text/json"); + httpHandler.ShouldBeMethod(HttpMethod.Patch); + httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeBody("{\"key\":42}", "text/json"); - var expectedResult = new JsonObject().Add("key", 42); + var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, actual); - } + Assert.Equal(expectedResult, actual); + } - [Fact] - public async Task Should_make_postJson_request() - { - var httpHandler = SetupRequest(); + [Fact] + public async Task Should_make_postJson_request() + { + var httpHandler = SetupRequest(); - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" var url = 'http://squidex.io'; var body = { key: 42 }; @@ -500,27 +500,27 @@ public async Task Should_make_postJson_request() }); "; - var actual = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); - httpHandler.ShouldBeMethod(HttpMethod.Post); - httpHandler.ShouldBeUrl("http://squidex.io/"); - httpHandler.ShouldBeBody("{\"key\":42}", "text/json"); + httpHandler.ShouldBeMethod(HttpMethod.Post); + httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeBody("{\"key\":42}", "text/json"); - var expectedResult = new JsonObject().Add("key", 42); + var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, actual); - } + Assert.Equal(expectedResult, actual); + } - [Fact] - public async Task Should_make_putJson_request() - { - var httpHandler = SetupRequest(); + [Fact] + public async Task Should_make_putJson_request() + { + var httpHandler = SetupRequest(); - var vars = new ScriptVars - { - }; + var vars = new ScriptVars + { + }; - const string script = @" + const string script = @" var url = 'http://squidex.io'; var body = { key: 42 }; @@ -530,30 +530,29 @@ public async Task Should_make_putJson_request() }); "; - var actual = await sut.ExecuteAsync(vars, script); + var actual = await sut.ExecuteAsync(vars, script); - httpHandler.ShouldBeMethod(HttpMethod.Put); - httpHandler.ShouldBeUrl("http://squidex.io/"); - httpHandler.ShouldBeBody("{\"key\":42}", "text/json"); + httpHandler.ShouldBeMethod(HttpMethod.Put); + httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeBody("{\"key\":42}", "text/json"); - var expectedResult = new JsonObject().Add("key", 42); + var expectedResult = new JsonObject().Add("key", 42); - Assert.Equal(expectedResult, actual); - } + Assert.Equal(expectedResult, actual); + } - private MockupHttpHandler SetupRequest() + private MockupHttpHandler SetupRequest() + { + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) { - var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{ \"key\": 42 }") - }; + Content = new StringContent("{ \"key\": 42 }") + }; - var httpHandler = new MockupHttpHandler(httpResponse); + var httpHandler = new MockupHttpHandler(httpResponse); - A.CallTo(() => httpClientFactory.CreateClient(A<string>._)) - .Returns(new HttpClient(httpHandler)); + A.CallTo(() => httpClientFactory.CreateClient(A<string>._)) + .Returns(new HttpClient(httpHandler)); - return httpHandler; - } + return httpHandler; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index 6b1dd5f9dd..b07d648465 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -20,114 +20,114 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting; + +public class JintScriptEngineTests : IClassFixture<TranslationsFixture> { - public class JintScriptEngineTests : IClassFixture<TranslationsFixture> + private readonly ScriptOptions contentOptions = new ScriptOptions { - private readonly ScriptOptions contentOptions = new ScriptOptions - { - CanReject = true, - CanDisallow = true, - AsContext = true - }; + CanReject = true, + CanDisallow = true, + AsContext = true + }; - private readonly IHttpClientFactory httpClientFactory = A.Fake<IHttpClientFactory>(); - private readonly JintScriptEngine sut; + private readonly IHttpClientFactory httpClientFactory = A.Fake<IHttpClientFactory>(); + private readonly JintScriptEngine sut; - public JintScriptEngineTests() + public JintScriptEngineTests() + { + var extensions = new IJintExtension[] { - var extensions = new IJintExtension[] - { - new DateTimeJintExtension(), - new HttpJintExtension(httpClientFactory), - new StringJintExtension(), - new StringWordsJintExtension() - }; + new DateTimeJintExtension(), + new HttpJintExtension(httpClientFactory), + new StringJintExtension(), + new StringWordsJintExtension() + }; - var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{ \"key\": 42 }") - }; + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{ \"key\": 42 }") + }; - var httpHandler = new MockupHttpHandler(httpResponse); + var httpHandler = new MockupHttpHandler(httpResponse); - A.CallTo(() => httpClientFactory.CreateClient(A<string>._)) - .Returns(new HttpClient(httpHandler)); + A.CallTo(() => httpClientFactory.CreateClient(A<string>._)) + .Returns(new HttpClient(httpHandler)); - sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - }), - extensions); - } + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + }), + extensions); + } - [Fact] - public async Task ExecuteAsync_should_catch_script_syntax_errors() - { - const string script = @" + [Fact] + public async Task ExecuteAsync_should_catch_script_syntax_errors() + { + const string script = @" invalid(() "; - await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script)); + } - [Fact] - public async Task ExecuteAsync_should_catch_script_runtime_errors() - { - const string script = @" + [Fact] + public async Task ExecuteAsync_should_catch_script_runtime_errors() + { + const string script = @" throw 'Error'; "; - await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script)); + } - [Fact] - public async Task TransformAsync_should_return_original_content_if_script_failed() - { - var content = new ContentData(); + [Fact] + public async Task TransformAsync_should_return_original_content_if_script_failed() + { + var content = new ContentData(); - var vars = new DataScriptVars - { - ["data"] = content - }; + var vars = new DataScriptVars + { + ["data"] = content + }; - const string script = @" + const string script = @" x => x "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task TransformAsync_should_transform_content() + [Fact] + public async Task TransformAsync_should_transform_content() + { + var content = + new ContentData() + .AddField("number0", + new ContentFieldData() + .AddInvariant(1.0)) + .AddField("number1", + new ContentFieldData() + .AddInvariant(1.0)); + var expected = + new ContentData() + .AddField("number1", + new ContentFieldData() + .AddInvariant(2.0)) + .AddField("number2", + new ContentFieldData() + .AddInvariant(10.0)); + + var vars = new DataScriptVars { - var content = - new ContentData() - .AddField("number0", - new ContentFieldData() - .AddInvariant(1.0)) - .AddField("number1", - new ContentFieldData() - .AddInvariant(1.0)); - var expected = - new ContentData() - .AddField("number1", - new ContentFieldData() - .AddInvariant(2.0)) - .AddField("number2", - new ContentFieldData() - .AddInvariant(10.0)); - - var vars = new DataScriptVars - { - ["data"] = content - }; + ["data"] = content + }; - const string script = @" + const string script = @" var data = ctx.data; delete data.number0; @@ -138,62 +138,62 @@ public async Task TransformAsync_should_transform_content() replace(data); "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public async Task TransformAsync_should_catch_javascript_error() - { - const string script = @" + [Fact] + public async Task TransformAsync_should_catch_javascript_error() + { + const string script = @" throw 'Error'; "; - await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(new DataScriptVars(), script)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(new DataScriptVars(), script)); + } - [Fact] - public async Task TransformAsync_should_throw_exception_if_script_failed() + [Fact] + public async Task TransformAsync_should_throw_exception_if_script_failed() + { + var vars = new DataScriptVars { - var vars = new DataScriptVars - { - ["data"] = new ContentData() - }; + ["data"] = new ContentData() + }; - const string script = @" + const string script = @" invalid((); "; - await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(vars, script, contentOptions)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(vars, script, contentOptions)); + } - [Fact] - public async Task TransformAsync_should_return_original_content_if_not_replaced() + [Fact] + public async Task TransformAsync_should_return_original_content_if_not_replaced() + { + var vars = new DataScriptVars { - var vars = new DataScriptVars - { - ["data"] = new ContentData() - }; + ["data"] = new ContentData() + }; - const string script = @" + const string script = @" var x = 0; "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task TransformAsync_should_return_original_content_if_not_replaced_async() + [Fact] + public async Task TransformAsync_should_return_original_content_if_not_replaced_async() + { + var vars = new DataScriptVars { - var vars = new DataScriptVars - { - ["data"] = new ContentData() - }; + ["data"] = new ContentData() + }; - const string script = @" + const string script = @" var x = 0; getJSON('http://mockup.squidex.io', function(actual) { @@ -201,30 +201,30 @@ public async Task TransformAsync_should_return_original_content_if_not_replaced_ }); "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task TransformAsync_should_transform_object() - { - var content = new ContentData(); + [Fact] + public async Task TransformAsync_should_transform_object() + { + var content = new ContentData(); - var expected = - new ContentData() - .AddField("operation", - new ContentFieldData() - .AddInvariant("MyOperation")); + var expected = + new ContentData() + .AddField("operation", + new ContentFieldData() + .AddInvariant("MyOperation")); - var vars = new DataScriptVars - { - ["data"] = content, - ["dataOld"] = null, - ["operation"] = "MyOperation" - }; + var vars = new DataScriptVars + { + ["data"] = content, + ["dataOld"] = null, + ["operation"] = "MyOperation" + }; - const string script = @" + const string script = @" var data = ctx.data; data.operation = { iv: ctx.operation }; @@ -232,30 +232,30 @@ public async Task TransformAsync_should_transform_object() replace(data); "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public async Task TransformAsync_should_transform_object_async() - { - var content = new ContentData(); + [Fact] + public async Task TransformAsync_should_transform_object_async() + { + var content = new ContentData(); - var expected = - new ContentData() - .AddField("operation", - new ContentFieldData() - .AddInvariant(42)); + var expected = + new ContentData() + .AddField("operation", + new ContentFieldData() + .AddInvariant(42)); - var vars = new DataScriptVars - { - ["data"] = content, - ["dataOld"] = null, - ["operation"] = "MyOperation" - }; + var vars = new DataScriptVars + { + ["data"] = content, + ["dataOld"] = null, + ["operation"] = "MyOperation" + }; - const string script = @" + const string script = @" var data = ctx.data; getJSON('http://mockup.squidex.io', function(actual) { @@ -266,22 +266,22 @@ public async Task TransformAsync_should_transform_object_async() "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public async Task TransformAsync_should_not_ignore_transformation_if_async_not_set() + [Fact] + public async Task TransformAsync_should_not_ignore_transformation_if_async_not_set() + { + var vars = new DataScriptVars { - var vars = new DataScriptVars - { - ["data"] = new ContentData(), - ["dataOld"] = null, - ["operation"] = "MyOperation" - }; + ["data"] = new ContentData(), + ["dataOld"] = null, + ["operation"] = "MyOperation" + }; - const string script = @" + const string script = @" var data = ctx.data; getJSON('http://mockup.squidex.io', function(actual) { @@ -292,22 +292,22 @@ public async Task TransformAsync_should_not_ignore_transformation_if_async_not_s "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.NotEmpty(actual); - } + Assert.NotEmpty(actual); + } - [Fact] - public async Task TransformAsync_should_not_timeout_if_replace_never_called() + [Fact] + public async Task TransformAsync_should_not_timeout_if_replace_never_called() + { + var vars = new DataScriptVars { - var vars = new DataScriptVars - { - ["data"] = new ContentData(), - ["dataOld"] = null, - ["operation"] = "MyOperation" - }; + ["data"] = new ContentData(), + ["dataOld"] = null, + ["operation"] = "MyOperation" + }; - const string script = @" + const string script = @" var data = ctx.data; getJSON('http://cloud.squidex.io/healthz', function(actual) { @@ -315,35 +315,35 @@ public async Task TransformAsync_should_not_timeout_if_replace_never_called() }); "; - await sut.TransformAsync(vars, script, contentOptions); - } + await sut.TransformAsync(vars, script, contentOptions); + } - [Fact] - public async Task TransformAsync_should_transform_content_and_return_with_execute_transform() + [Fact] + public async Task TransformAsync_should_transform_content_and_return_with_execute_transform() + { + var content = + new ContentData() + .AddField("number0", + new ContentFieldData() + .AddInvariant(1.0)) + .AddField("number1", + new ContentFieldData() + .AddInvariant(1.0)); + var expected = + new ContentData() + .AddField("number1", + new ContentFieldData() + .AddInvariant(2.0)) + .AddField("number2", + new ContentFieldData() + .AddInvariant(10.0)); + + var vars = new DataScriptVars { - var content = - new ContentData() - .AddField("number0", - new ContentFieldData() - .AddInvariant(1.0)) - .AddField("number1", - new ContentFieldData() - .AddInvariant(1.0)); - var expected = - new ContentData() - .AddField("number1", - new ContentFieldData() - .AddInvariant(2.0)) - .AddField("number2", - new ContentFieldData() - .AddInvariant(10.0)); - - var vars = new DataScriptVars - { - ["data"] = content - }; + ["data"] = content + }; - const string script = @" + const string script = @" var data = ctx.data; delete data.number0; @@ -354,225 +354,224 @@ public async Task TransformAsync_should_transform_content_and_return_with_execut replace(data); "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public async Task TransformAsync_should_transform_content_with_old_content() + [Fact] + public async Task TransformAsync_should_transform_content_with_old_content() + { + var content = + new ContentData() + .AddField("number0", + new ContentFieldData() + .AddInvariant(3.0)); + + var oldContent = + new ContentData() + .AddField("number0", + new ContentFieldData() + .AddInvariant(5.0)); + + var expected = + new ContentData() + .AddField("number0", + new ContentFieldData() + .AddInvariant(13.0)); + + var userIdentity = new ClaimsIdentity(); + var userPrincipal = new ClaimsPrincipal(userIdentity); + + userIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, "2")); + + var vars = new DataScriptVars { - var content = - new ContentData() - .AddField("number0", - new ContentFieldData() - .AddInvariant(3.0)); - - var oldContent = - new ContentData() - .AddField("number0", - new ContentFieldData() - .AddInvariant(5.0)); - - var expected = - new ContentData() - .AddField("number0", - new ContentFieldData() - .AddInvariant(13.0)); - - var userIdentity = new ClaimsIdentity(); - var userPrincipal = new ClaimsPrincipal(userIdentity); - - userIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, "2")); - - var vars = new DataScriptVars - { - ["data"] = content, - ["dataOld"] = oldContent, - ["user"] = userPrincipal - }; + ["data"] = content, + ["dataOld"] = oldContent, + ["user"] = userPrincipal + }; - const string script = @" + const string script = @" ctx.data.number0.iv = ctx.data.number0.iv + ctx.dataOld.number0.iv * parseInt(ctx.user.id, 10); replace(ctx.data); "; - var actual = await sut.TransformAsync(vars, script, contentOptions); + var actual = await sut.TransformAsync(vars, script, contentOptions); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Evaluate_should_return_true_if_expression_match() + [Fact] + public void Evaluate_should_return_true_if_expression_match() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = new { i = 2 } - }; + ["value"] = new { i = 2 } + }; - const string script = @" + const string script = @" value.i == 2 "; - var actual = ((IScriptEngine)sut).Evaluate(vars, script); + var actual = ((IScriptEngine)sut).Evaluate(vars, script); - Assert.True(actual); - } + Assert.True(actual); + } - [Fact] - public void Evaluate_should_return_true_if_status_match() + [Fact] + public void Evaluate_should_return_true_if_status_match() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = new { status = Status.Published } - }; + ["value"] = new { status = Status.Published } + }; - const string script = @" + const string script = @" value.status == 'Published' "; - var actual = ((IScriptEngine)sut).Evaluate(vars, script); + var actual = ((IScriptEngine)sut).Evaluate(vars, script); - Assert.True(actual); - } + Assert.True(actual); + } - [Fact] - public void Evaluate_should_return_false_if_expression_match() + [Fact] + public void Evaluate_should_return_false_if_expression_match() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = new { i = 2 } - }; + ["value"] = new { i = 2 } + }; - const string script = @" + const string script = @" value.i == 3 "; - var actual = ((IScriptEngine)sut).Evaluate(vars, script); + var actual = ((IScriptEngine)sut).Evaluate(vars, script); - Assert.False(actual); - } + Assert.False(actual); + } - [Fact] - public void Evaluate_should_return_false_if_script_is_invalid() + [Fact] + public void Evaluate_should_return_false_if_script_is_invalid() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = new { i = 2 } - }; + ["value"] = new { i = 2 } + }; - const string script = @" + const string script = @" function(); "; - var actual = ((IScriptEngine)sut).Evaluate(vars, script); + var actual = ((IScriptEngine)sut).Evaluate(vars, script); - Assert.False(actual); - } + Assert.False(actual); + } - [Fact] - public void Should_handle_domain_id_as_string() - { - var id = DomainId.NewGuid(); + [Fact] + public void Should_handle_domain_id_as_string() + { + var id = DomainId.NewGuid(); - var vars = new ScriptVars - { - ["value"] = id - }; + var vars = new ScriptVars + { + ["value"] = id + }; - const string script = @" + const string script = @" return value; "; - var actual = sut.Execute(vars, script); + var actual = sut.Execute(vars, script); - Assert.Equal(id.ToString(), actual.ToString()); - } + Assert.Equal(id.ToString(), actual.ToString()); + } - [Fact] - public void Should_share_vars_between_executions() + [Fact] + public void Should_share_vars_between_executions() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = 13 - }; + ["value"] = 13 + }; - const string script1 = @" + const string script1 = @" ctx.value = ctx.value * 2; "; - const string script2 = @" + const string script2 = @" return ctx.value + 2; "; - sut.Execute(vars, script1, new ScriptOptions { AsContext = true }); + sut.Execute(vars, script1, new ScriptOptions { AsContext = true }); - var actual = sut.Execute(vars, script2, new ScriptOptions { AsContext = true }); + var actual = sut.Execute(vars, script2, new ScriptOptions { AsContext = true }); - Assert.Equal(JsonValue.Create(28), actual); - } + Assert.Equal(JsonValue.Create(28), actual); + } - [Fact] - public void Should_share_complex_vars_between_executions() + [Fact] + public void Should_share_complex_vars_between_executions() + { + var vars = new ScriptVars { - var vars = new ScriptVars - { - ["value"] = 13 - }; + ["value"] = 13 + }; - const string script1 = @" + const string script1 = @" ctx.obj = { number: ctx.value * 2 }; "; - const string script2 = @" + const string script2 = @" return ctx.obj.number + 2; "; - sut.Execute(vars, script1, new ScriptOptions { AsContext = true }); + sut.Execute(vars, script1, new ScriptOptions { AsContext = true }); - var actual = sut.Execute(vars, script2, new ScriptOptions { AsContext = true }); + var actual = sut.Execute(vars, script2, new ScriptOptions { AsContext = true }); - Assert.Equal(JsonValue.Create(28), actual); - } + Assert.Equal(JsonValue.Create(28), actual); + } - [Fact] - public async Task Should_share_vars_between_execution_for_transform() + [Fact] + public async Task Should_share_vars_between_execution_for_transform() + { + var vars = new DataScriptVars { - var vars = new DataScriptVars - { - ["value"] = 13 - }; + ["value"] = 13 + }; - const string script1 = @" + const string script1 = @" ctx.obj = { number: ctx.value * 2 }; "; - const string script2 = @" + const string script2 = @" ctx.data.test = { iv: ctx.obj.number + 2 }; replace(); "; #pragma warning disable MA0042 // Do not use blocking calls in an async method - sut.Execute(vars, script1, new ScriptOptions { AsContext = true }); + sut.Execute(vars, script1, new ScriptOptions { AsContext = true }); #pragma warning restore MA0042 // Do not use blocking calls in an async method - var vars2 = new DataScriptVars - { - ["data"] = new ContentData() - }; + var vars2 = new DataScriptVars + { + ["data"] = new ContentData() + }; - foreach (var (key, value) in vars) + foreach (var (key, value) in vars) + { + if (!vars2.ContainsKey(key)) { - if (!vars2.ContainsKey(key)) - { - vars2[key] = value; - } + vars2[key] = value; } + } - var actual = await sut.TransformAsync(vars2, script2, new ScriptOptions { AsContext = true }); + var actual = await sut.TransformAsync(vars2, script2, new ScriptOptions { AsContext = true }); - Assert.Equal(JsonValue.Create(28), actual["test"]!["iv"]); - } + Assert.Equal(JsonValue.Create(28), actual["test"]!["iv"]); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs index f852742e16..aa775079fe 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs @@ -12,109 +12,108 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting; + +public class JintUserTests { - public class JintUserTests + [Fact] + public void Should_set_user_id_from_client_id() { - [Fact] - public void Should_set_user_id_from_client_id() - { - var identity = new ClaimsIdentity(); + var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(OpenIdClaims.ClientId, "1")); + identity.AddClaim(new Claim(OpenIdClaims.ClientId, "1")); - AssertUser(identity, "1", true, false); - } + AssertUser(identity, "1", true, false); + } - [Fact] - public void Should_set_user_id_from_subject_id() - { - var identity = new ClaimsIdentity(); + [Fact] + public void Should_set_user_id_from_subject_id() + { + var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(OpenIdClaims.Subject, "2")); - identity.AddClaim(new Claim(OpenIdClaims.Name, "user")); + identity.AddClaim(new Claim(OpenIdClaims.Subject, "2")); + identity.AddClaim(new Claim(OpenIdClaims.Name, "user")); - AssertUser(identity, "2", false, true); - } + AssertUser(identity, "2", false, true); + } - [Fact] - public void Should_set_email_from_claim() - { - var identity = new ClaimsIdentity(); + [Fact] + public void Should_set_email_from_claim() + { + var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(OpenIdClaims.Email, "hello@squidex.io")); + identity.AddClaim(new Claim(OpenIdClaims.Email, "hello@squidex.io")); - const string script = @" + const string script = @" return user.email; "; - Assert.Equal("hello@squidex.io", GetValue(identity, script)); - } + Assert.Equal("hello@squidex.io", GetValue(identity, script)); + } - [Fact] - public void Should_simplify_squidex_claims() - { - var identity = new ClaimsIdentity(); + [Fact] + public void Should_simplify_squidex_claims() + { + var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, "my-picture")); + identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, "my-picture")); - const string script = @" + const string script = @" return user.claims.picture; "; - Assert.Equal(new[] { "my-picture" }, GetValue(identity, script)); - } + Assert.Equal(new[] { "my-picture" }, GetValue(identity, script)); + } - [Fact] - public void Should_simplify_default_claims() - { - var identity = new ClaimsIdentity(); + [Fact] + public void Should_simplify_default_claims() + { + var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(ClaimTypes.Role, "my-role")); + identity.AddClaim(new Claim(ClaimTypes.Role, "my-role")); - const string script = @" + const string script = @" return user.claims.role; "; - Assert.Equal(new[] { "my-role" }, GetValue(identity, script)); - } + Assert.Equal(new[] { "my-role" }, GetValue(identity, script)); + } - [Fact] - public void Should_set_claims() - { - var identity = new ClaimsIdentity(); + [Fact] + public void Should_set_claims() + { + var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim("prefix1.claim1", "1a")); - identity.AddClaim(new Claim("prefix2.claim1", "1b")); - identity.AddClaim(new Claim("claim2", "2a")); - identity.AddClaim(new Claim("claim2", "2b")); + identity.AddClaim(new Claim("prefix1.claim1", "1a")); + identity.AddClaim(new Claim("prefix2.claim1", "1b")); + identity.AddClaim(new Claim("claim2", "2a")); + identity.AddClaim(new Claim("claim2", "2b")); - const string script1 = @" + const string script1 = @" return user.claims.claim1; "; - const string script2 = @" + const string script2 = @" return user.claims.claim2; "; - Assert.Equal(new[] { "1a", "1b" }, GetValue(identity, script1)); - Assert.Equal(new[] { "2a", "2b" }, GetValue(identity, script2)); - } + Assert.Equal(new[] { "1a", "1b" }, GetValue(identity, script1)); + Assert.Equal(new[] { "2a", "2b" }, GetValue(identity, script2)); + } - private static void AssertUser(ClaimsIdentity identity, string id, bool isClient, bool isUser) - { - Assert.Equal(id, GetValue(identity, "user.id")); - Assert.Equal(isUser, GetValue(identity, "user.isUser")); - Assert.Equal(isClient, GetValue(identity, "user.isClient")); - } + private static void AssertUser(ClaimsIdentity identity, string id, bool isClient, bool isUser) + { + Assert.Equal(id, GetValue(identity, "user.id")); + Assert.Equal(isUser, GetValue(identity, "user.isUser")); + Assert.Equal(isClient, GetValue(identity, "user.isClient")); + } - private static object GetValue(ClaimsIdentity identity, string script) - { - var engine = new Engine(); + private static object GetValue(ClaimsIdentity identity, string script) + { + var engine = new Engine(); - engine.SetValue("user", JintUser.Create(engine, new ClaimsPrincipal(new[] { identity }))); + engine.SetValue("user", JintUser.Create(engine, new ClaimsPrincipal(new[] { identity }))); - return engine.Evaluate(script).ToObject(); - } + return engine.Evaluate(script).ToObject(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs index cb0bf4ad59..c00ca00279 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs @@ -7,55 +7,54 @@ using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Scripting -{ - internal sealed class MockupHttpHandler : HttpMessageHandler - { - private readonly HttpResponseMessage response; - private HttpRequestMessage currentRequest; - private string? currentContent; - private string? currentContentType; +namespace Squidex.Domain.Apps.Core.Operations.Scripting; - public void ShouldBeMethod(HttpMethod method) - { - Assert.Equal(method, currentRequest.Method); - } +internal sealed class MockupHttpHandler : HttpMessageHandler +{ + private readonly HttpResponseMessage response; + private HttpRequestMessage currentRequest; + private string? currentContent; + private string? currentContentType; - public void ShouldBeUrl(string url) - { - Assert.Equal(url, currentRequest.RequestUri?.ToString()); - } + public void ShouldBeMethod(HttpMethod method) + { + Assert.Equal(method, currentRequest.Method); + } - public void ShouldBeHeader(string key, string value) - { - Assert.Equal(value, currentRequest.Headers.GetValues(key).FirstOrDefault()); - } + public void ShouldBeUrl(string url) + { + Assert.Equal(url, currentRequest.RequestUri?.ToString()); + } - public void ShouldBeBody(string content, string contentType) - { - Assert.Equal(content, currentContent); - Assert.Equal(contentType, currentContentType); - } + public void ShouldBeHeader(string key, string value) + { + Assert.Equal(value, currentRequest.Headers.GetValues(key).FirstOrDefault()); + } - public MockupHttpHandler(HttpResponseMessage response) - { - this.response = response; - } + public void ShouldBeBody(string content, string contentType) + { + Assert.Equal(content, currentContent); + Assert.Equal(contentType, currentContentType); + } - protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) - { - await Task.Delay(1000, cancellationToken); + public MockupHttpHandler(HttpResponseMessage response) + { + this.response = response; + } - currentRequest = request; + protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + await Task.Delay(1000, cancellationToken); - if (request.Content is StringContent body) - { - currentContent = await body.ReadAsStringAsync(cancellationToken); - currentContentType = body.Headers.ContentType?.MediaType; - } + currentRequest = request; - return response; + if (request.Content is StringContent body) + { + currentContent = await body.ReadAsStringAsync(cancellationToken); + currentContentType = body.Headers.ContentType?.MediaType; } + + return response; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs index f55b08fb83..ea27490d50 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs @@ -13,369 +13,368 @@ using Squidex.Infrastructure.Queries; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting; + +public class ScriptingCompleterTests { - public class ScriptingCompleterTests + private readonly IScriptDescriptor scriptDescriptor1 = A.Fake<IScriptDescriptor>(); + private readonly IScriptDescriptor scriptDescriptor2 = A.Fake<IScriptDescriptor>(); + private readonly FilterSchema dataSchema; + private readonly ScriptingCompleter sut; + + public ScriptingCompleterTests() { - private readonly IScriptDescriptor scriptDescriptor1 = A.Fake<IScriptDescriptor>(); - private readonly IScriptDescriptor scriptDescriptor2 = A.Fake<IScriptDescriptor>(); - private readonly FilterSchema dataSchema; - private readonly ScriptingCompleter sut; + var schema = + new Schema("simple") + .AddString(1, "my-field", Partitioning.Invariant); - public ScriptingCompleterTests() - { - var schema = - new Schema("simple") - .AddString(1, "my-field", Partitioning.Invariant); + dataSchema = schema.BuildDataSchema(LanguagesConfig.English.ToResolver(), ResolvedComponents.Empty); - dataSchema = schema.BuildDataSchema(LanguagesConfig.English.ToResolver(), ResolvedComponents.Empty); + sut = new ScriptingCompleter(new[] { scriptDescriptor1, scriptDescriptor2 }); + } - sut = new ScriptingCompleter(new[] { scriptDescriptor1, scriptDescriptor2 }); - } + [Fact] + public void Should_calls_descriptors() + { + sut.UsageTrigger(); - [Fact] - public void Should_calls_descriptors() - { - sut.UsageTrigger(); + A.CallTo(() => scriptDescriptor1.Describe(A<AddDescription>._, A<ScriptScope>._)) + .MustHaveHappened(); - A.CallTo(() => scriptDescriptor1.Describe(A<AddDescription>._, A<ScriptScope>._)) - .MustHaveHappened(); + A.CallTo(() => scriptDescriptor2.Describe(A<AddDescription>._, A<ScriptScope>._)) + .MustHaveHappened(); + } - A.CallTo(() => scriptDescriptor2.Describe(A<AddDescription>._, A<ScriptScope>._)) - .MustHaveHappened(); - } + [Fact] + public void Should_describe_content_script() + { + var actual = sut.ContentScript(dataSchema); - [Fact] - public void Should_describe_content_script() - { - var actual = sut.ContentScript(dataSchema); - - AssertCompletion(actual, - PresetUser("ctx.user"), - new[] - { - "ctx", - "ctx.appId", - "ctx.appName", - "ctx.contentId", - "ctx.data", - "ctx.data['my-field']", - "ctx.data['my-field'].iv", - "ctx.dataOld", - "ctx.dataOld['my-field']", - "ctx.dataOld['my-field'].iv", - "ctx.oldData", - "ctx.oldData['my-field']", - "ctx.oldData['my-field'].iv", - "ctx.oldStatus", - "ctx.operation", - "ctx.permanent", - "ctx.schemaId", - "ctx.schemaName", - "ctx.status", - "ctx.statusOld", - "ctx.validate" - }); - } + AssertCompletion(actual, + PresetUser("ctx.user"), + new[] + { + "ctx", + "ctx.appId", + "ctx.appName", + "ctx.contentId", + "ctx.data", + "ctx.data['my-field']", + "ctx.data['my-field'].iv", + "ctx.dataOld", + "ctx.dataOld['my-field']", + "ctx.dataOld['my-field'].iv", + "ctx.oldData", + "ctx.oldData['my-field']", + "ctx.oldData['my-field'].iv", + "ctx.oldStatus", + "ctx.operation", + "ctx.permanent", + "ctx.schemaId", + "ctx.schemaName", + "ctx.status", + "ctx.statusOld", + "ctx.validate" + }); + } - [Fact] - public void Should_describe_asset_script() - { - var actual = sut.AssetScript(); - - AssertCompletion(actual, - PresetUser("ctx.user"), - new[] - { - "ctx", - "ctx.appId", - "ctx.appName", - "ctx.asset", - "ctx.asset.fileHash", - "ctx.asset.fileName", - "ctx.asset.fileSize", - "ctx.asset.fileSlug", - "ctx.asset.fileVersion", - "ctx.asset.isProtected", - "ctx.asset.metadata", - "ctx.asset.metadata['my-name']", - "ctx.asset.mimeType", - "ctx.asset.parentId", - "ctx.asset.parentPath", - "ctx.asset.tags", - "ctx.assetId", - "ctx.command", - "ctx.command.fileHash", - "ctx.command.fileName", - "ctx.command.fileSize", - "ctx.command.fileSlug", - "ctx.command.isProtected", - "ctx.command.metadata", - "ctx.command.metadata['my-name']", - "ctx.command.mimeType", - "ctx.command.parentId", - "ctx.command.parentPath", - "ctx.command.permanent", - "ctx.command.tags", - "ctx.operation" - }); - } + [Fact] + public void Should_describe_asset_script() + { + var actual = sut.AssetScript(); - [Fact] - public void Should_describe_content_trigger() - { - var actual = sut.ContentTrigger(dataSchema); + AssertCompletion(actual, + PresetUser("ctx.user"), + new[] + { + "ctx", + "ctx.appId", + "ctx.appName", + "ctx.asset", + "ctx.asset.fileHash", + "ctx.asset.fileName", + "ctx.asset.fileSize", + "ctx.asset.fileSlug", + "ctx.asset.fileVersion", + "ctx.asset.isProtected", + "ctx.asset.metadata", + "ctx.asset.metadata['my-name']", + "ctx.asset.mimeType", + "ctx.asset.parentId", + "ctx.asset.parentPath", + "ctx.asset.tags", + "ctx.assetId", + "ctx.command", + "ctx.command.fileHash", + "ctx.command.fileName", + "ctx.command.fileSize", + "ctx.command.fileSlug", + "ctx.command.isProtected", + "ctx.command.metadata", + "ctx.command.metadata['my-name']", + "ctx.command.mimeType", + "ctx.command.parentId", + "ctx.command.parentPath", + "ctx.command.permanent", + "ctx.command.tags", + "ctx.operation" + }); + } - AssertContentTrigger(actual); - } + [Fact] + public void Should_describe_content_trigger() + { + var actual = sut.ContentTrigger(dataSchema); - [Fact] - public void Should_describe_dynamic_content_trigger() - { - var actual = sut.Trigger("ContentChanged"); + AssertContentTrigger(actual); + } - AssertContentTrigger(actual); - } + [Fact] + public void Should_describe_dynamic_content_trigger() + { + var actual = sut.Trigger("ContentChanged"); - private static void AssertContentTrigger(IReadOnlyList<ScriptingValue> actual) - { - AssertCompletion(actual, - PresetActor("event.actor"), - PresetUser("event.user"), - new[] - { - "event", - "event.appId", - "event.appId.id", - "event.appId.name", - "event.created", - "event.createdBy", - "event.createdBy.identifier", - "event.createdBy.type", - "event.data", - "event.data['my-field']", - "event.data['my-field'].iv", - "event.dataOld", - "event.dataOld['my-field']", - "event.dataOld['my-field'].iv", - "event.lastModified", - "event.lastModifiedBy", - "event.lastModifiedBy.identifier", - "event.lastModifiedBy.type", - "event.id", - "event.name", - "event.newStatus", - "event.schemaId", - "event.schemaId.id", - "event.schemaId.name", - "event.status", - "event.timestamp", - "event.type", - "event.version" - }); - } + AssertContentTrigger(actual); + } - [Fact] - public void Should_describe_asset_trigger() - { - var actual = sut.AssetTrigger(); + private static void AssertContentTrigger(IReadOnlyList<ScriptingValue> actual) + { + AssertCompletion(actual, + PresetActor("event.actor"), + PresetUser("event.user"), + new[] + { + "event", + "event.appId", + "event.appId.id", + "event.appId.name", + "event.created", + "event.createdBy", + "event.createdBy.identifier", + "event.createdBy.type", + "event.data", + "event.data['my-field']", + "event.data['my-field'].iv", + "event.dataOld", + "event.dataOld['my-field']", + "event.dataOld['my-field'].iv", + "event.lastModified", + "event.lastModifiedBy", + "event.lastModifiedBy.identifier", + "event.lastModifiedBy.type", + "event.id", + "event.name", + "event.newStatus", + "event.schemaId", + "event.schemaId.id", + "event.schemaId.name", + "event.status", + "event.timestamp", + "event.type", + "event.version" + }); + } - AssertAssetTrigger(actual); - } + [Fact] + public void Should_describe_asset_trigger() + { + var actual = sut.AssetTrigger(); - [Fact] - public void Should_describe_dynamicasset_trigger() - { - var actual = sut.Trigger("AssetChanged"); + AssertAssetTrigger(actual); + } - AssertAssetTrigger(actual); - } + [Fact] + public void Should_describe_dynamicasset_trigger() + { + var actual = sut.Trigger("AssetChanged"); - private static void AssertAssetTrigger(IReadOnlyList<ScriptingValue> actual) - { - AssertCompletion(actual, - PresetActor("event.actor"), - PresetUser("event.user"), - new[] - { - "event", - "event.appId", - "event.appId.id", - "event.appId.name", - "event.assetType", - "event.created", - "event.createdBy", - "event.createdBy.identifier", - "event.createdBy.type", - "event.fileHash", - "event.fileName", - "event.fileSize", - "event.fileVersion", - "event.isImage", - "event.isProtected", - "event.lastModified", - "event.lastModifiedBy", - "event.lastModifiedBy.identifier", - "event.lastModifiedBy.type", - "event.id", - "event.metadata", - "event.metadata['my-name']", - "event.mimeType", - "event.name", - "event.parentId", - "event.pixelHeight", - "event.pixelWidth", - "event.slug", - "event.timestamp", - "event.type", - "event.version" - }); - } + AssertAssetTrigger(actual); + } - [Fact] - public void Should_describe_comment_trigger() - { - var actual = sut.CommentTrigger(); + private static void AssertAssetTrigger(IReadOnlyList<ScriptingValue> actual) + { + AssertCompletion(actual, + PresetActor("event.actor"), + PresetUser("event.user"), + new[] + { + "event", + "event.appId", + "event.appId.id", + "event.appId.name", + "event.assetType", + "event.created", + "event.createdBy", + "event.createdBy.identifier", + "event.createdBy.type", + "event.fileHash", + "event.fileName", + "event.fileSize", + "event.fileVersion", + "event.isImage", + "event.isProtected", + "event.lastModified", + "event.lastModifiedBy", + "event.lastModifiedBy.identifier", + "event.lastModifiedBy.type", + "event.id", + "event.metadata", + "event.metadata['my-name']", + "event.mimeType", + "event.name", + "event.parentId", + "event.pixelHeight", + "event.pixelWidth", + "event.slug", + "event.timestamp", + "event.type", + "event.version" + }); + } - AssertCommentTrigger(actual); - } + [Fact] + public void Should_describe_comment_trigger() + { + var actual = sut.CommentTrigger(); - [Fact] - public void Should_describe_dynamic_comment_trigger() - { - var actual = sut.Trigger("Comment"); + AssertCommentTrigger(actual); + } - AssertCommentTrigger(actual); - } + [Fact] + public void Should_describe_dynamic_comment_trigger() + { + var actual = sut.Trigger("Comment"); - private static void AssertCommentTrigger(IReadOnlyList<ScriptingValue> actual) - { - AssertCompletion(actual, - PresetActor("event.actor"), - PresetUser("event.user"), - PresetUser("event.mentionedUser"), - new[] - { - "event", - "event.appId", - "event.appId.id", - "event.appId.name", - "event.name", - "event.text", - "event.timestamp", - "event.version" - }); - } + AssertCommentTrigger(actual); + } - [Fact] - public void Should_describe_schema_trigger() - { - var actual = sut.SchemaTrigger(); + private static void AssertCommentTrigger(IReadOnlyList<ScriptingValue> actual) + { + AssertCompletion(actual, + PresetActor("event.actor"), + PresetUser("event.user"), + PresetUser("event.mentionedUser"), + new[] + { + "event", + "event.appId", + "event.appId.id", + "event.appId.name", + "event.name", + "event.text", + "event.timestamp", + "event.version" + }); + } - AssertSchemaTrigger(actual); - } + [Fact] + public void Should_describe_schema_trigger() + { + var actual = sut.SchemaTrigger(); - [Fact] - public void Should_describe_dynamic_schema_trigger() - { - var actual = sut.Trigger("SchemaChanged"); + AssertSchemaTrigger(actual); + } - AssertSchemaTrigger(actual); - } + [Fact] + public void Should_describe_dynamic_schema_trigger() + { + var actual = sut.Trigger("SchemaChanged"); - private static void AssertSchemaTrigger(IReadOnlyList<ScriptingValue> actual) - { - AssertCompletion(actual, - PresetActor("event.actor"), - PresetUser("event.user"), - new[] - { - "event", - "event.appId", - "event.appId.id", - "event.appId.name", - "event.name", - "event.schemaId", - "event.schemaId.id", - "event.schemaId.name", - "event.timestamp", - "event.type", - "event.version" - }); - } + AssertSchemaTrigger(actual); + } - [Fact] - public void Should_describe_usage_trigger() - { - var actual = sut.UsageTrigger(); + private static void AssertSchemaTrigger(IReadOnlyList<ScriptingValue> actual) + { + AssertCompletion(actual, + PresetActor("event.actor"), + PresetUser("event.user"), + new[] + { + "event", + "event.appId", + "event.appId.id", + "event.appId.name", + "event.name", + "event.schemaId", + "event.schemaId.id", + "event.schemaId.name", + "event.timestamp", + "event.type", + "event.version" + }); + } - AssertUsageTrigger(actual); - } + [Fact] + public void Should_describe_usage_trigger() + { + var actual = sut.UsageTrigger(); - [Fact] - public void Should_describe_dynamic_usage_trigger() - { - var actual = sut.Trigger("Usage"); + AssertUsageTrigger(actual); + } - AssertUsageTrigger(actual); - } + [Fact] + public void Should_describe_dynamic_usage_trigger() + { + var actual = sut.Trigger("Usage"); - private static void AssertUsageTrigger(IReadOnlyList<ScriptingValue> actual) - { - AssertCompletion(actual, - new[] - { - "event", - "event.appId", - "event.appId.id", - "event.appId.name", - "event.callsCurrent", - "event.callsLimit", - "event.name", - "event.timestamp", - "event.version" - }); - } + AssertUsageTrigger(actual); + } - private static void AssertCompletion(IReadOnlyList<ScriptingValue> actual, params string[][] expected) - { - var allExpected = expected.SelectMany(x => x).ToArray(); + private static void AssertUsageTrigger(IReadOnlyList<ScriptingValue> actual) + { + AssertCompletion(actual, + new[] + { + "event", + "event.appId", + "event.appId.id", + "event.appId.name", + "event.callsCurrent", + "event.callsLimit", + "event.name", + "event.timestamp", + "event.version" + }); + } - var paths = actual.Select(x => x.Path).ToArray(); + private static void AssertCompletion(IReadOnlyList<ScriptingValue> actual, params string[][] expected) + { + var allExpected = expected.SelectMany(x => x).ToArray(); - foreach (var value in paths) - { - Assert.Contains(value, allExpected); - } + var paths = actual.Select(x => x.Path).ToArray(); - foreach (var value in allExpected) - { - Assert.Contains(value, paths); - } + foreach (var value in paths) + { + Assert.Contains(value, allExpected); } - private static string[] PresetActor(string path) + foreach (var value in allExpected) { - return new[] - { - $"{path}", - $"{path}.identifier", - $"{path}.type" - }; + Assert.Contains(value, paths); } + } + + private static string[] PresetActor(string path) + { + return new[] + { + $"{path}", + $"{path}.identifier", + $"{path}.type" + }; + } - private static string[] PresetUser(string path) + private static string[] PresetUser(string path) + { + return new[] { - return new[] - { - $"{path}", - $"{path}.claims", - $"{path}.claims.name", - $"{path}.email", - $"{path}.id", - $"{path}.isClient", - $"{path}.isUser" - }; - } + $"{path}", + $"{path}.claims", + $"{path}.claims.name", + $"{path}.email", + $"{path}.id", + $"{path}.isClient", + $"{path}.isUser" + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs index 60793f9e94..af8542ce6b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs @@ -14,98 +14,97 @@ using Squidex.Shared; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Subscriptions +namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; + +public class AssetSubscriptionTests { - public class AssetSubscriptionTests - { - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - [Fact] - public async Task Should_return_true_for_enriched_asset_event() - { - var sut = WithPermission(new AssetSubscription()); + [Fact] + public async Task Should_return_true_for_enriched_asset_event() + { + var sut = WithPermission(new AssetSubscription()); - var @event = Enrich(new EnrichedAssetEvent()); + var @event = Enrich(new EnrichedAssetEvent()); - Assert.True(await sut.ShouldHandle(@event)); - } + Assert.True(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_false_for_wrong_event() - { - var sut = WithPermission(new AssetSubscription()); + [Fact] + public async Task Should_return_false_for_wrong_event() + { + var sut = WithPermission(new AssetSubscription()); - var @event = new AppCreated(); + var @event = new AppCreated(); - Assert.False(await sut.ShouldHandle(@event)); - } + Assert.False(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_true_for_asset_event() - { - var sut = WithPermission(new AssetSubscription()); + [Fact] + public async Task Should_return_true_for_asset_event() + { + var sut = WithPermission(new AssetSubscription()); - var @event = Enrich(new AssetCreated()); + var @event = Enrich(new AssetCreated()); - Assert.True(await sut.ShouldHandle(@event)); - } + Assert.True(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_true_for_asset_event_with_correct_type() - { - var sut = WithPermission(new AssetSubscription { Type = EnrichedAssetEventType.Created }); + [Fact] + public async Task Should_return_true_for_asset_event_with_correct_type() + { + var sut = WithPermission(new AssetSubscription { Type = EnrichedAssetEventType.Created }); - var @event = Enrich(new AssetCreated()); + var @event = Enrich(new AssetCreated()); - Assert.True(await sut.ShouldHandle(@event)); - } + Assert.True(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_false_for_asset_event_with_wrong_type() - { - var sut = WithPermission(new AssetSubscription { Type = EnrichedAssetEventType.Deleted }); + [Fact] + public async Task Should_return_false_for_asset_event_with_wrong_type() + { + var sut = WithPermission(new AssetSubscription { Type = EnrichedAssetEventType.Deleted }); - var @event = Enrich(new AssetCreated()); + var @event = Enrich(new AssetCreated()); - Assert.False(await sut.ShouldHandle(@event)); - } + Assert.False(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_false_for_asset_event_invalid_permissions() - { - var sut = WithPermission(new AssetSubscription(), PermissionIds.AppCommentsCreate); + [Fact] + public async Task Should_return_false_for_asset_event_invalid_permissions() + { + var sut = WithPermission(new AssetSubscription(), PermissionIds.AppCommentsCreate); - var @event = Enrich(new AssetCreated()); + var @event = Enrich(new AssetCreated()); - Assert.False(await sut.ShouldHandle(@event)); - } + Assert.False(await sut.ShouldHandle(@event)); + } - private object Enrich(EnrichedAssetEvent source) - { - source.AppId = appId; + private object Enrich(EnrichedAssetEvent source) + { + source.AppId = appId; - return source; - } + return source; + } - private object Enrich(AssetEvent source) - { - source.AppId = appId; + private object Enrich(AssetEvent source) + { + source.AppId = appId; - return source; - } + return source; + } - private AssetSubscription WithPermission(AssetSubscription subscription, string? permissionId = null) - { - subscription.AppId = appId.Id; + private AssetSubscription WithPermission(AssetSubscription subscription, string? permissionId = null) + { + subscription.AppId = appId.Id; - permissionId ??= PermissionIds.AppAssetsRead; + permissionId ??= PermissionIds.AppAssetsRead; - var permission = PermissionIds.ForApp(permissionId, appId.Name); - var permissions = new PermissionSet(permission); + var permission = PermissionIds.ForApp(permissionId, appId.Name); + var permissions = new PermissionSet(permission); - subscription.Permissions = permissions; + subscription.Permissions = permissions; - return subscription; - } + return subscription; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs index 3afe8c6746..ddb3fdaca3 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs @@ -14,121 +14,120 @@ using Squidex.Shared; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Subscriptions +namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; + +public class ContentSubscriptionTests { - public class ContentSubscriptionTests - { - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - [Fact] - public async Task Should_return_true_for_enriched_content_event() - { - var sut = WithPermission(new ContentSubscription()); + [Fact] + public async Task Should_return_true_for_enriched_content_event() + { + var sut = WithPermission(new ContentSubscription()); - var @event = Enrich(new EnrichedContentEvent()); + var @event = Enrich(new EnrichedContentEvent()); - Assert.True(await sut.ShouldHandle(@event)); - } + Assert.True(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_false_for_wrong_event() - { - var sut = WithPermission(new ContentSubscription()); + [Fact] + public async Task Should_return_false_for_wrong_event() + { + var sut = WithPermission(new ContentSubscription()); - var @event = new AppCreated { AppId = appId }; + var @event = new AppCreated { AppId = appId }; - Assert.False(await sut.ShouldHandle(@event)); - } + Assert.False(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_true_for_content_event() - { - var sut = WithPermission(new ContentSubscription()); + [Fact] + public async Task Should_return_true_for_content_event() + { + var sut = WithPermission(new ContentSubscription()); - var @event = Enrich(new ContentCreated()); + var @event = Enrich(new ContentCreated()); - Assert.True(await sut.ShouldHandle(@event)); - } + Assert.True(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_true_for_content_event_with_correct_type() - { - var sut = WithPermission(new ContentSubscription { Type = EnrichedContentEventType.Created }); + [Fact] + public async Task Should_return_true_for_content_event_with_correct_type() + { + var sut = WithPermission(new ContentSubscription { Type = EnrichedContentEventType.Created }); - var @event = Enrich(new ContentCreated()); + var @event = Enrich(new ContentCreated()); - Assert.True(await sut.ShouldHandle(@event)); - } + Assert.True(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_false_for_content_event_with_wrong_type() - { - var sut = WithPermission(new ContentSubscription { Type = EnrichedContentEventType.Deleted }); + [Fact] + public async Task Should_return_false_for_content_event_with_wrong_type() + { + var sut = WithPermission(new ContentSubscription { Type = EnrichedContentEventType.Deleted }); - var @event = Enrich(new ContentCreated()); + var @event = Enrich(new ContentCreated()); - Assert.False(await sut.ShouldHandle(@event)); - } + Assert.False(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_true_for_content_event_with_correct_schema() - { - var sut = WithPermission(new ContentSubscription { SchemaName = schemaId.Name }); + [Fact] + public async Task Should_return_true_for_content_event_with_correct_schema() + { + var sut = WithPermission(new ContentSubscription { SchemaName = schemaId.Name }); - var @event = Enrich(new ContentCreated()); + var @event = Enrich(new ContentCreated()); - Assert.True(await sut.ShouldHandle(@event)); - } + Assert.True(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_false_for_content_event_with_wrong_schema() - { - var sut = WithPermission(new ContentSubscription { SchemaName = "wrong-schema" }); + [Fact] + public async Task Should_return_false_for_content_event_with_wrong_schema() + { + var sut = WithPermission(new ContentSubscription { SchemaName = "wrong-schema" }); - var @event = Enrich(new ContentCreated()); + var @event = Enrich(new ContentCreated()); - Assert.False(await sut.ShouldHandle(@event)); - } + Assert.False(await sut.ShouldHandle(@event)); + } - [Fact] - public async Task Should_return_false_for_content_event_invalid_permissions() - { - var sut = WithPermission(new ContentSubscription(), PermissionIds.AppCommentsCreate); + [Fact] + public async Task Should_return_false_for_content_event_invalid_permissions() + { + var sut = WithPermission(new ContentSubscription(), PermissionIds.AppCommentsCreate); - var @event = Enrich(new ContentCreated()); + var @event = Enrich(new ContentCreated()); - Assert.False(await sut.ShouldHandle(@event)); - } + Assert.False(await sut.ShouldHandle(@event)); + } - private object Enrich(EnrichedContentEvent source) - { - source.AppId = appId; - source.SchemaId = schemaId; + private object Enrich(EnrichedContentEvent source) + { + source.AppId = appId; + source.SchemaId = schemaId; - return source; - } + return source; + } - private object Enrich(ContentEvent source) - { - source.AppId = appId; - source.SchemaId = schemaId; + private object Enrich(ContentEvent source) + { + source.AppId = appId; + source.SchemaId = schemaId; - return source; - } + return source; + } - private ContentSubscription WithPermission(ContentSubscription subscription, string? permissionId = null) - { - subscription.AppId = appId.Id; + private ContentSubscription WithPermission(ContentSubscription subscription, string? permissionId = null) + { + subscription.AppId = appId.Id; - permissionId ??= PermissionIds.AppContentsRead; + permissionId ??= PermissionIds.AppContentsRead; - var permission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name); - var permissions = new PermissionSet(permission); + var permission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name); + var permissions = new PermissionSet(permission); - subscription.Permissions = permissions; + subscription.Permissions = permissions; - return subscription; - } + return subscription; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs index 18bcadc9a6..73cd9eb6cd 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs @@ -15,84 +15,83 @@ using Squidex.Shared; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Subscriptions +namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; + +public class EventMessageEvaluatorTests { - public class EventMessageEvaluatorTests - { - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly EventMessageEvaluator sut = new EventMessageEvaluator(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly EventMessageEvaluator sut = new EventMessageEvaluator(); - [Fact] - public async Task Should_return_empty_list_when_nothing_registered() - { - var assetEvent = new ContentCreated { AppId = NamedId.Of(DomainId.NewGuid(), "my-app2") }; + [Fact] + public async Task Should_return_empty_list_when_nothing_registered() + { + var assetEvent = new ContentCreated { AppId = NamedId.Of(DomainId.NewGuid(), "my-app2") }; - var subscriptions = await sut.GetSubscriptionsAsync(assetEvent); + var subscriptions = await sut.GetSubscriptionsAsync(assetEvent); - Assert.Empty(subscriptions); - } + Assert.Empty(subscriptions); + } - [Fact] - public async Task Should_return_matching_subscriptions() - { - var contentSubscriptionId = Guid.NewGuid(); - var contentSubscription = WithPermission(new ContentSubscription(), PermissionIds.AppContentsRead); + [Fact] + public async Task Should_return_matching_subscriptions() + { + var contentSubscriptionId = Guid.NewGuid(); + var contentSubscription = WithPermission(new ContentSubscription(), PermissionIds.AppContentsRead); - var assetSubscriptionId = Guid.NewGuid(); - var assetSubscription = WithPermission(new AssetSubscription(), PermissionIds.AppAssetsRead); + var assetSubscriptionId = Guid.NewGuid(); + var assetSubscription = WithPermission(new AssetSubscription(), PermissionIds.AppAssetsRead); - sut.SubscriptionAdded(contentSubscriptionId, contentSubscription); - sut.SubscriptionAdded(assetSubscriptionId, assetSubscription); + sut.SubscriptionAdded(contentSubscriptionId, contentSubscription); + sut.SubscriptionAdded(assetSubscriptionId, assetSubscription); - Assert.Equal(new[] { contentSubscriptionId }, - await sut.GetSubscriptionsAsync(Enrich(new ContentCreated()))); + Assert.Equal(new[] { contentSubscriptionId }, + await sut.GetSubscriptionsAsync(Enrich(new ContentCreated()))); - Assert.Equal(new[] { assetSubscriptionId }, - await sut.GetSubscriptionsAsync(Enrich(new AssetCreated()))); + Assert.Equal(new[] { assetSubscriptionId }, + await sut.GetSubscriptionsAsync(Enrich(new AssetCreated()))); - Assert.Empty( - await sut.GetSubscriptionsAsync(Enrich(new AppCreated()))); + Assert.Empty( + await sut.GetSubscriptionsAsync(Enrich(new AppCreated()))); - Assert.Empty( - await sut.GetSubscriptionsAsync(new ContentCreated { AppId = NamedId.Of(DomainId.NewGuid(), "my-app2") })); + Assert.Empty( + await sut.GetSubscriptionsAsync(new ContentCreated { AppId = NamedId.Of(DomainId.NewGuid(), "my-app2") })); - sut.SubscriptionRemoved(contentSubscriptionId, contentSubscription); - sut.SubscriptionRemoved(assetSubscriptionId, assetSubscription); + sut.SubscriptionRemoved(contentSubscriptionId, contentSubscription); + sut.SubscriptionRemoved(assetSubscriptionId, assetSubscription); - Assert.Empty( - await sut.GetSubscriptionsAsync(Enrich(new ContentCreated()))); + Assert.Empty( + await sut.GetSubscriptionsAsync(Enrich(new ContentCreated()))); - Assert.Empty( - await sut.GetSubscriptionsAsync(Enrich(new AssetCreated()))); - } + Assert.Empty( + await sut.GetSubscriptionsAsync(Enrich(new AssetCreated()))); + } - private object Enrich(ContentEvent source) - { - source.SchemaId = schemaId; - source.AppId = appId; + private object Enrich(ContentEvent source) + { + source.SchemaId = schemaId; + source.AppId = appId; - return source; - } + return source; + } - private object Enrich(AppEvent source) - { - source.Actor = null!; - source.AppId = appId; + private object Enrich(AppEvent source) + { + source.Actor = null!; + source.AppId = appId; - return source; - } + return source; + } - private AppSubscription WithPermission(AppSubscription subscription, string permissionId) - { - subscription.AppId = appId.Id; + private AppSubscription WithPermission(AppSubscription subscription, string permissionId) + { + subscription.AppId = appId.Id; - var permission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name); - var permissions = new PermissionSet(permission); + var permission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name); + var permissions = new PermissionSet(permission); - subscription.Permissions = permissions; + subscription.Permissions = permissions; - return subscription; - } + return subscription; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageWrapperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageWrapperTests.cs index 08cffe61ea..faba7ee0d4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageWrapperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageWrapperTests.cs @@ -13,55 +13,54 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Subscriptions +namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; + +public class EventMessageWrapperTests { - public class EventMessageWrapperTests - { - private readonly ISubscriptionEventCreator creator1 = A.Fake<ISubscriptionEventCreator>(); - private readonly ISubscriptionEventCreator creator2 = A.Fake<ISubscriptionEventCreator>(); + private readonly ISubscriptionEventCreator creator1 = A.Fake<ISubscriptionEventCreator>(); + private readonly ISubscriptionEventCreator creator2 = A.Fake<ISubscriptionEventCreator>(); - [Fact] - public async Task Should_return_event_from_first_creator() - { - var enrichedEvent = new EnrichedContentEvent(); + [Fact] + public async Task Should_return_event_from_first_creator() + { + var enrichedEvent = new EnrichedContentEvent(); - var envelope = Envelope.Create<AppEvent>(new AppCreated()); + var envelope = Envelope.Create<AppEvent>(new AppCreated()); - A.CallTo(() => creator1.Handles(envelope.Payload)) - .Returns(true); + A.CallTo(() => creator1.Handles(envelope.Payload)) + .Returns(true); - A.CallTo(() => creator1.CreateEnrichedEventsAsync(envelope, default)) - .Returns(null!); + A.CallTo(() => creator1.CreateEnrichedEventsAsync(envelope, default)) + .Returns(null!); - A.CallTo(() => creator2.Handles(envelope.Payload)) - .Returns(true); + A.CallTo(() => creator2.Handles(envelope.Payload)) + .Returns(true); - A.CallTo(() => creator2.CreateEnrichedEventsAsync(envelope, default)) - .Returns(enrichedEvent); + A.CallTo(() => creator2.CreateEnrichedEventsAsync(envelope, default)) + .Returns(enrichedEvent); - var sut = new EventMessageWrapper(envelope, new[] { creator1, creator2 }); + var sut = new EventMessageWrapper(envelope, new[] { creator1, creator2 }); - var actual = await sut.CreatePayloadAsync(); + var actual = await sut.CreatePayloadAsync(); - Assert.Same(enrichedEvent, actual); - } + Assert.Same(enrichedEvent, actual); + } - [Fact] - public async Task Should_not_invoke_creator_if_it_does_not_handle_event() - { - var enrichedEvent = new EnrichedContentEvent(); + [Fact] + public async Task Should_not_invoke_creator_if_it_does_not_handle_event() + { + var enrichedEvent = new EnrichedContentEvent(); - var envelope = Envelope.Create<AppEvent>(new AppCreated()); + var envelope = Envelope.Create<AppEvent>(new AppCreated()); - A.CallTo(() => creator1.Handles(envelope.Payload)) - .Returns(false); + A.CallTo(() => creator1.Handles(envelope.Payload)) + .Returns(false); - var sut = new EventMessageWrapper(envelope, new[] { creator1 }); + var sut = new EventMessageWrapper(envelope, new[] { creator1 }); - Assert.Null(await sut.CreatePayloadAsync()); + Assert.Null(await sut.CreatePayloadAsync()); - A.CallTo(() => creator1.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => creator1.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs index 3be7f422ec..ec4467562b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs @@ -12,95 +12,94 @@ using Squidex.Messaging.Subscriptions; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Subscriptions +namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; + +public class SubscriptionPublisherTests { - public class SubscriptionPublisherTests - { - private readonly ISubscriptionService subscriptionService = A.Fake<ISubscriptionService>(); - private readonly SubscriptionPublisher sut; + private readonly ISubscriptionService subscriptionService = A.Fake<ISubscriptionService>(); + private readonly SubscriptionPublisher sut; - private sealed class MyEvent : IEvent - { - } + private sealed class MyEvent : IEvent + { + } - public SubscriptionPublisherTests() - { - sut = new SubscriptionPublisher(subscriptionService, Enumerable.Empty<ISubscriptionEventCreator>()); - } + public SubscriptionPublisherTests() + { + sut = new SubscriptionPublisher(subscriptionService, Enumerable.Empty<ISubscriptionEventCreator>()); + } - [Fact] - public void Should_return_content_and_asset_filter_for_events_filter() - { - IEventConsumer consumer = sut; + [Fact] + public void Should_return_content_and_asset_filter_for_events_filter() + { + IEventConsumer consumer = sut; - Assert.Equal("^(content-|asset-)", consumer.EventsFilter); - } + Assert.Equal("^(content-|asset-)", consumer.EventsFilter); + } - [Fact] - public async Task Should_do_nothing_on_clear() - { - IEventConsumer consumer = sut; + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; - await consumer.ClearAsync(); - } + await consumer.ClearAsync(); + } - [Fact] - public void Should_return_custom_name_for_name() - { - IEventConsumer consumer = sut; + [Fact] + public void Should_return_custom_name_for_name() + { + IEventConsumer consumer = sut; - Assert.Equal("Subscriptions", consumer.Name); - } + Assert.Equal("Subscriptions", consumer.Name); + } - [Fact] - public void Should_not_support_clear() - { - IEventConsumer consumer = sut; + [Fact] + public void Should_not_support_clear() + { + IEventConsumer consumer = sut; - Assert.False(consumer.CanClear); - } + Assert.False(consumer.CanClear); + } - [Fact] - public void Should_start_from_latest() - { - IEventConsumer consumer = sut; + [Fact] + public void Should_start_from_latest() + { + IEventConsumer consumer = sut; - Assert.True(consumer.StartLatest); - } + Assert.True(consumer.StartLatest); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Should_handle_events_when_subscription_exists(bool hasSubscriptions) - { - A.CallTo(() => subscriptionService.HasSubscriptions) - .Returns(hasSubscriptions); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Should_handle_events_when_subscription_exists(bool hasSubscriptions) + { + A.CallTo(() => subscriptionService.HasSubscriptions) + .Returns(hasSubscriptions); - IEventConsumer consumer = sut; + IEventConsumer consumer = sut; - Assert.Equal(hasSubscriptions, consumer.Handles(null!)); - } + Assert.Equal(hasSubscriptions, consumer.Handles(null!)); + } - [Fact] - public async Task Should_not_publish_if_not_app_event() - { - var envelope = Envelope.Create(new MyEvent()); + [Fact] + public async Task Should_not_publish_if_not_app_event() + { + var envelope = Envelope.Create(new MyEvent()); - await sut.On(envelope); + await sut.On(envelope); - A.CallTo(subscriptionService) - .MustNotHaveHappened(); - } + A.CallTo(subscriptionService) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_publish_app_event() - { - var envelope = Envelope.Create(new AppCreated()); + [Fact] + public async Task Should_publish_app_event() + { + var envelope = Envelope.Create(new AppCreated()); - await sut.On(envelope); + await sut.On(envelope); - A.CallTo(subscriptionService).Where(x => x.Method.Name.StartsWith("Publish")) - .MustHaveHappened(); - } + A.CallTo(subscriptionService).Where(x => x.Method.Name.StartsWith("Publish")) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs index 873dc25e6e..f0c511b523 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs @@ -12,224 +12,223 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.Templates +namespace Squidex.Domain.Apps.Core.Operations.Templates; + +public class FluidTemplateEngineTests { - public class FluidTemplateEngineTests - { - private readonly FluidTemplateEngine sut; + private readonly FluidTemplateEngine sut; - public FluidTemplateEngineTests() + public FluidTemplateEngineTests() + { + var extensions = new IFluidExtension[] { - var extensions = new IFluidExtension[] - { - new ContentFluidExtension(), - new DateTimeFluidExtension(), - new StringFluidExtension(), - new StringWordsFluidExtension() - }; - - sut = new FluidTemplateEngine(extensions); - } - - [Theory] - [InlineData("{{ e.user }}", "subject:me")] - [InlineData("{{ e.user.type }}", "subject")] - [InlineData("{{ e.user.identifier }}", "me")] - public async Task Should_render_ref_token(string template, string expected) + new ContentFluidExtension(), + new DateTimeFluidExtension(), + new StringFluidExtension(), + new StringWordsFluidExtension() + }; + + sut = new FluidTemplateEngine(extensions); + } + + [Theory] + [InlineData("{{ e.user }}", "subject:me")] + [InlineData("{{ e.user.type }}", "subject")] + [InlineData("{{ e.user.identifier }}", "me")] + public async Task Should_render_ref_token(string template, string expected) + { + var value = new { - var value = new - { - User = RefToken.User("me") - }; + User = RefToken.User("me") + }; - var actual = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("{{ e.id }}", "42,my-app")] - [InlineData("{{ e.id.name }}", "my-app")] - [InlineData("{{ e.id.id }}", "42")] - public async Task Should_render_named_id(string template, string expected) + [Theory] + [InlineData("{{ e.id }}", "42,my-app")] + [InlineData("{{ e.id.name }}", "my-app")] + [InlineData("{{ e.id.id }}", "42")] + public async Task Should_render_named_id(string template, string expected) + { + var value = new { - var value = new - { - Id = NamedId.Of("42", "my-app") - }; + Id = NamedId.Of("42", "my-app") + }; - var actual = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public async Task Should_format_domain_id() + [Fact] + public async Task Should_format_domain_id() + { + var value = new { - var value = new - { - Id = DomainId.NewGuid() - }; + Id = DomainId.NewGuid() + }; - var template = "{{ e.id }}"; + var template = "{{ e.id }}"; - var actual = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal(value.Id.ToString(), actual); - } + Assert.Equal(value.Id.ToString(), actual); + } - [Fact] - public async Task Should_format_enum() + [Fact] + public async Task Should_format_enum() + { + var value = new { - var value = new - { - Type = EnrichedContentEventType.Created - }; + Type = EnrichedContentEventType.Created + }; - var template = "{{ e.type }}"; + var template = "{{ e.type }}"; - var actual = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal(value.Type.ToString(), actual); - } + Assert.Equal(value.Type.ToString(), actual); + } - [Fact] - public async Task Should_format_date() + [Fact] + public async Task Should_format_date() + { + var now = DateTime.UtcNow; + + var value = new { - var now = DateTime.UtcNow; + Timestamp = now + }; - var value = new - { - Timestamp = now - }; + var template = "{{ e.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss' }}"; - var template = "{{ e.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss' }}"; + var actual = await RenderAync(template, value); - var actual = await RenderAync(template, value); + Assert.Equal($"{now:yyyy-MM-dd-hh-mm-ss}", actual); + } - Assert.Equal($"{now:yyyy-MM-dd-hh-mm-ss}", actual); - } + [Fact] + public async Task Should_format_content_data() + { + var template = "{{ e.data.value.en }}"; - [Fact] - public async Task Should_format_content_data() + var value = new { - var template = "{{ e.data.value.en }}"; + Data = + new ContentData() + .AddField("value", + new ContentFieldData() + .AddLocalized("en", "Hello")) + }; - var value = new - { - Data = - new ContentData() - .AddField("value", - new ContentFieldData() - .AddLocalized("en", "Hello")) - }; + var actual = await RenderAync(template, value); - var actual = await RenderAync(template, value); + Assert.Equal("Hello", actual); + } - Assert.Equal("Hello", actual); - } + [Fact] + public async Task Should_format_html_to_text() + { + var template = "{{ e.text | html2text }}"; - [Fact] - public async Task Should_format_html_to_text() + var value = new { - var template = "{{ e.text | html2text }}"; + Text = "<script>Invalid</script><STYLE>Invalid</STYLE><p>Hello World</p>" + }; - var value = new - { - Text = "<script>Invalid</script><STYLE>Invalid</STYLE><p>Hello World</p>" - }; + var actual = await RenderAync(template, value); - var actual = await RenderAync(template, value); + Assert.Equal("Hello World", actual); + } - Assert.Equal("Hello World", actual); - } + [Fact] + public async Task Should_convert_markdown_to_text() + { + var template = "{{ e.text | markdown2text }}"; - [Fact] - public async Task Should_convert_markdown_to_text() + var value = new { - var template = "{{ e.text | markdown2text }}"; + Text = "## Hello World" + }; - var value = new - { - Text = "## Hello World" - }; + var actual = await RenderAync(template, value); - var actual = await RenderAync(template, value); + Assert.Equal("Hello World", actual); + } - Assert.Equal("Hello World", actual); - } + [Fact] + public async Task Should_format_word_count() + { + var template = "{{ e.text | word_count }}"; - [Fact] - public async Task Should_format_word_count() + var value = new { - var template = "{{ e.text | word_count }}"; + Text = "Hello World" + }; - var value = new - { - Text = "Hello World" - }; + var actual = await RenderAync(template, value); - var actual = await RenderAync(template, value); + Assert.Equal("2", actual); + } - Assert.Equal("2", actual); - } + [Fact] + public async Task Should_format_character_count() + { + var template = "{{ e.text | character_count }}"; - [Fact] - public async Task Should_format_character_count() + var value = new { - var template = "{{ e.text | character_count }}"; + text = "Hello World" + }; - var value = new - { - text = "Hello World" - }; + var actual = await RenderAync(template, value); - var actual = await RenderAync(template, value); + Assert.Equal("10", actual); + } - Assert.Equal("10", actual); - } + [Fact] + public async Task Should_compute_md5_hash() + { + var template = "{{ e.text | md5 }}"; - [Fact] - public async Task Should_compute_md5_hash() + var value = new { - var template = "{{ e.text | md5 }}"; + text = "HelloWorld" + }; - var value = new - { - text = "HelloWorld" - }; + var actual = await RenderAync(template, value); - var actual = await RenderAync(template, value); + Assert.Equal("HelloWorld".ToMD5(), actual); + } - Assert.Equal("HelloWorld".ToMD5(), actual); - } + [Fact] + public async Task Should_compute_sha256_hash() + { + var template = "{{ e.text | sha256 }}"; - [Fact] - public async Task Should_compute_sha256_hash() + var value = new { - var template = "{{ e.text | sha256 }}"; - - var value = new - { - text = "HelloWorld" - }; + text = "HelloWorld" + }; - var actual = await RenderAync(template, value); + var actual = await RenderAync(template, value); - Assert.Equal("HelloWorld".ToSha256(), actual); - } + Assert.Equal("HelloWorld".ToSha256(), actual); + } - [Fact] - public async Task Should_throw_exception_if_template_invalid() - { - var template = "{% for x of event %}"; + [Fact] + public async Task Should_throw_exception_if_template_invalid() + { + var template = "{% for x of event %}"; - await Assert.ThrowsAsync<TemplateParseException>(() => sut.RenderAsync(template, new TemplateVars())); - } + await Assert.ThrowsAsync<TemplateParseException>(() => sut.RenderAsync(template, new TemplateVars())); + } - private Task<string> RenderAync(string template, object value) - { - return sut.RenderAsync(template, new TemplateVars { ["e"] = value }); - } + private Task<string> RenderAync(string template, object value) + { + return sut.RenderAsync(template, new TemplateVars { ["e"] = value }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs index 39d1b8d69c..9cbe0a8640 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs @@ -12,144 +12,143 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class ArrayFieldTests : IClassFixture<TranslationsFixture> { - public class ArrayFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new ArrayFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new ArrayFieldProperties()); - Assert.Equal("myArray", sut.Name); - } + Assert.Equal("myArray", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_items_are_valid() - { - var sut = Field(new ArrayFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_items_are_valid() + { + var sut = Field(new ArrayFieldProperties()); - await sut.ValidateAsync(CreateValue(Object()), errors); + await sut.ValidateAsync(CreateValue(Object()), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_items_are_null_and_valid() - { - var sut = Field(new ArrayFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_items_are_null_and_valid() + { + var sut = Field(new ArrayFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_number_of_items_is_equal_to_min_and_max_items() - { - var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }); + [Fact] + public async Task Should_not_add_error_if_number_of_items_is_equal_to_min_and_max_items() + { + var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue(Object(), Object()), errors); + await sut.ValidateAsync(CreateValue(Object(), Object()), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_value_has_not_duplicates() - { - var sut = Field(new ArrayFieldProperties { UniqueFields = ReadonlyList.Create("myString") }); + [Fact] + public async Task Should_not_add_error_if_value_has_not_duplicates() + { + var sut = Field(new ArrayFieldProperties { UniqueFields = ReadonlyList.Create("myString") }); - await sut.ValidateAsync(CreateValue(Object("myString", "1"), Object("myString", "2")), errors); + await sut.ValidateAsync(CreateValue(Object("myString", "1"), Object("myString", "2")), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_items_are_required_and_null() - { - var sut = Field(new ArrayFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_items_are_required_and_null() + { + var sut = Field(new ArrayFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_items_are_required_and_empty() - { - var sut = Field(new ArrayFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_items_are_required_and_empty() + { + var sut = Field(new ArrayFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new ArrayFieldProperties()); + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new ArrayFieldProperties()); - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); - errors.Should().BeEquivalentTo( - new[] { "Invalid json type, expected array of objects." }); - } + errors.Should().BeEquivalentTo( + new[] { "Invalid json type, expected array of objects." }); + } - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new ArrayFieldProperties { MinItems = 3 }); + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new ArrayFieldProperties { MinItems = 3 }); - await sut.ValidateAsync(CreateValue(Object(), Object()), errors); + await sut.ValidateAsync(CreateValue(Object(), Object()), errors); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(Object(), Object()), errors); + await sut.ValidateAsync(CreateValue(Object(), Object()), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_has_duplicates() - { - var sut = Field(new ArrayFieldProperties { UniqueFields = ReadonlyList.Create("myString") }); + [Fact] + public async Task Should_add_error_if_value_has_duplicates() + { + var sut = Field(new ArrayFieldProperties { UniqueFields = ReadonlyList.Create("myString") }); - await sut.ValidateAsync(CreateValue(Object("myString", "1"), Object("myString", "1")), errors); + await sut.ValidateAsync(CreateValue(Object("myString", "1"), Object("myString", "1")), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not contain items with duplicate 'myString' fields." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not contain items with duplicate 'myString' fields." }); + } - private static JsonValue CreateValue(params JsonObject[]? objects) - { - return objects == null ? default : JsonValue.Array(objects); - } + private static JsonValue CreateValue(params JsonObject[]? objects) + { + return objects == null ? default : JsonValue.Array(objects); + } - private static JsonObject Object() - { - return new JsonObject(); - } + private static JsonObject Object() + { + return new JsonObject(); + } - private static JsonObject Object(string key, JsonValue value) - { - return new JsonObject().Add(key, value); - } + private static JsonObject Object(string key, JsonValue value) + { + return new JsonObject().Add(key, value); + } - private static RootField<ArrayFieldProperties> Field(ArrayFieldProperties properties) - { - return Fields.Array(1, "myArray", Partitioning.Invariant, properties, null, Fields.String(2, "myString")); - } + private static RootField<ArrayFieldProperties> Field(ArrayFieldProperties properties) + { + return Fields.Array(1, "myArray", Partitioning.Invariant, properties, null, Fields.String(2, "myString")); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 1c47cf9971..5ac593271a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -14,154 +14,153 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class AssetsFieldTests : IClassFixture<TranslationsFixture> { - public class AssetsFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); - private readonly DomainId asset1 = DomainId.NewGuid(); - private readonly DomainId asset2 = DomainId.NewGuid(); - private readonly IValidatorsFactory factory; + private readonly List<string> errors = new List<string>(); + private readonly DomainId asset1 = DomainId.NewGuid(); + private readonly DomainId asset2 = DomainId.NewGuid(); + private readonly IValidatorsFactory factory; - private sealed class CustomFactory : IValidatorsFactory + private sealed class CustomFactory : IValidatorsFactory + { + public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) { - public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) + if (field is IField<AssetsFieldProperties> assets) { - if (field is IField<AssetsFieldProperties> assets) + yield return new AssetsValidator(assets.Properties.IsRequired, assets.Properties, ids => { - yield return new AssetsValidator(assets.Properties.IsRequired, assets.Properties, ids => - { - var actual = ids.Select(TestAssets.Document).ToList(); + var actual = ids.Select(TestAssets.Document).ToList(); - return Task.FromResult<IReadOnlyList<IAssetInfo>>(actual); - }); - } + return Task.FromResult<IReadOnlyList<IAssetInfo>>(actual); + }); } } + } - public AssetsFieldTests() - { - factory = new CustomFactory(); - } + public AssetsFieldTests() + { + factory = new CustomFactory(); + } - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new AssetsFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new AssetsFieldProperties()); - Assert.Equal("myAssets", sut.Name); - } + Assert.Equal("myAssets", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_assets_are_valid() + [Fact] + public async Task Should_not_add_error_if_assets_are_valid() + { + var sut = Field(new AssetsFieldProperties { - var sut = Field(new AssetsFieldProperties - { - IsRequired = true, - MinItems = 1, - MaxItems = 3 - }); + IsRequired = true, + MinItems = 1, + MaxItems = 3 + }); - await sut.ValidateAsync(CreateValue(asset1), errors, factory: factory); + await sut.ValidateAsync(CreateValue(asset1), errors, factory: factory); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_assets_are_null_and_valid() - { - var sut = Field(new AssetsFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_assets_are_null_and_valid() + { + var sut = Field(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors, factory: factory); + await sut.ValidateAsync(CreateValue(null), errors, factory: factory); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_number_of_assets_is_equal_to_min_and_max_items() - { - var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); + [Fact] + public async Task Should_not_add_error_if_number_of_assets_is_equal_to_min_and_max_items() + { + var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue(asset1, asset2), errors, factory: factory); + await sut.ValidateAsync(CreateValue(asset1, asset2), errors, factory: factory); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_duplicate_values_are_allowed() - { - var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); + [Fact] + public async Task Should_not_add_error_if_duplicate_values_are_allowed() + { + var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); - await sut.ValidateAsync(CreateValue(asset1, asset2), errors, factory: factory); + await sut.ValidateAsync(CreateValue(asset1, asset2), errors, factory: factory); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_assets_are_required_and_null() - { - var sut = Field(new AssetsFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_assets_are_required_and_null() + { + var sut = Field(new AssetsFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors, factory: factory); + await sut.ValidateAsync(CreateValue(null), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_assets_are_required_and_empty() - { - var sut = Field(new AssetsFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_assets_are_required_and_empty() + { + var sut = Field(new AssetsFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(), errors, factory: factory); + await sut.ValidateAsync(CreateValue(), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new AssetsFieldProperties { MinItems = 3 }); + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new AssetsFieldProperties { MinItems = 3 }); - await sut.ValidateAsync(CreateValue(asset1, asset2), errors, factory: factory); + await sut.ValidateAsync(CreateValue(asset1, asset2), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(asset1, asset2), errors, factory: factory); + await sut.ValidateAsync(CreateValue(asset1, asset2), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_values_contains_duplicate() - { - var sut = Field(new AssetsFieldProperties()); + [Fact] + public async Task Should_add_error_if_values_contains_duplicate() + { + var sut = Field(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(asset1, asset1), errors, factory: factory); + await sut.ValidateAsync(CreateValue(asset1, asset1), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } - private static JsonValue CreateValue(params DomainId[]? ids) - { - return ids == null ? - JsonValue.Null : - JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } + private static JsonValue CreateValue(params DomainId[]? ids) + { + return ids == null ? + JsonValue.Null : + JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); + } - private static RootField<AssetsFieldProperties> Field(AssetsFieldProperties properties) - { - return Fields.Assets(1, "myAssets", Partitioning.Invariant, properties); - } + private static RootField<AssetsFieldProperties> Field(AssetsFieldProperties properties) + { + return Fields.Assets(1, "myAssets", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs index 2b85d495c8..5be459f044 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs @@ -11,70 +11,69 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class BooleanFieldTests : IClassFixture<TranslationsFixture> { - public class BooleanFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new BooleanFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new BooleanFieldProperties()); - Assert.Equal("myBoolean", sut.Name); - } + Assert.Equal("myBoolean", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_null_boolean_is_valid() - { - var sut = Field(new BooleanFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_null_boolean_is_valid() + { + var sut = Field(new BooleanFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_boolean_is_valid() - { - var sut = Field(new BooleanFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_boolean_is_valid() + { + var sut = Field(new BooleanFieldProperties()); - await sut.ValidateAsync(CreateValue(true), errors); + await sut.ValidateAsync(CreateValue(true), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_boolean_is_required() - { - var sut = Field(new BooleanFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_boolean_is_required() + { + var sut = Field(new BooleanFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new BooleanFieldProperties()); + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new BooleanFieldProperties()); - await sut.ValidateAsync(JsonValue.Create("Invalid"), errors); + await sut.ValidateAsync(JsonValue.Create("Invalid"), errors); - errors.Should().BeEquivalentTo( - new[] { "Invalid json type, expected boolean." }); - } + errors.Should().BeEquivalentTo( + new[] { "Invalid json type, expected boolean." }); + } - private static JsonValue CreateValue(bool? v) - { - return JsonValue.Create(v); - } + private static JsonValue CreateValue(bool? v) + { + return JsonValue.Create(v); + } - private static RootField<BooleanFieldProperties> Field(BooleanFieldProperties properties) - { - return Fields.Boolean(1, "myBoolean", Partitioning.Invariant, properties); - } + private static RootField<BooleanFieldProperties> Field(BooleanFieldProperties properties) + { + return Fields.Boolean(1, "myBoolean", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs index b152548f05..0239cad2eb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs @@ -14,166 +14,165 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class ComponentFieldTests : IClassFixture<TranslationsFixture> { - public class ComponentFieldTests : IClassFixture<TranslationsFixture> + private readonly DomainId schemaId1 = DomainId.NewGuid(); + private readonly DomainId schemaId2 = DomainId.NewGuid(); + private readonly List<string> errors = new List<string>(); + + [Fact] + public void Should_instantiate_field() { - private readonly DomainId schemaId1 = DomainId.NewGuid(); - private readonly DomainId schemaId2 = DomainId.NewGuid(); - private readonly List<string> errors = new List<string>(); + var (_, sut, _) = Field(new ComponentFieldProperties()); - [Fact] - public void Should_instantiate_field() - { - var (_, sut, _) = Field(new ComponentFieldProperties()); + Assert.Equal("myComponent", sut.Name); + } - Assert.Equal("myComponent", sut.Name); - } + [Fact] + public async Task Should_not_add_error_if_component_is_null_and_valid() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_not_add_error_if_component_is_null_and_valid() - { - var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync(null, errors, components: components); - await sut.ValidateAsync(null, errors, components: components); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_not_add_error_if_component_is_valid() + { + var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_not_add_error_if_component_is_valid() - { - var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync(CreateValue(id.ToString(), "componentField", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(id.ToString(), "componentField", 1), errors, components: components); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_add_error_if_component_is_required() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1, IsRequired = true }); - [Fact] - public async Task Should_add_error_if_component_is_required() - { - var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1, IsRequired = true }); + await sut.ValidateAsync(null, errors, components: components); - await sut.ValidateAsync(null, errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + [Fact] + public async Task Should_add_error_if_component_value_is_required() + { + var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1, IsRequired = true }, true); - [Fact] - public async Task Should_add_error_if_component_value_is_required() - { - var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1, IsRequired = true }, true); + await sut.ValidateAsync(CreateValue(id.ToString(), "componentField", default), errors, components: components); - await sut.ValidateAsync(CreateValue(id.ToString(), "componentField", default), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "componentField: Field is required." }); + } - errors.Should().BeEquivalentTo( - new[] { "componentField: Field is required." }); - } + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync(JsonValue.Create("Invalid"), errors, components: components); - await sut.ValidateAsync(JsonValue.Create("Invalid"), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid json object, expected object with 'schemaId' field." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid json object, expected object with 'schemaId' field." }); - } + [Fact] + public async Task Should_add_error_if_value_has_no_discriminator() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaIds = ReadonlyList.Create(schemaId1, schemaId2) }); - [Fact] - public async Task Should_add_error_if_value_has_no_discriminator() - { - var (_, sut, components) = Field(new ComponentFieldProperties { SchemaIds = ReadonlyList.Create(schemaId1, schemaId2) }); + await sut.ValidateAsync(CreateValue(null, "field", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(null, "field", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid component. No 'schemaId' field found." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid component. No 'schemaId' field found." }); - } + [Fact] + public async Task Should_add_error_if_value_has_invalid_discriminator_format() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_add_error_if_value_has_invalid_discriminator_format() - { - var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync(CreateValue("invalid", "field", 1), errors, components: components); - await sut.ValidateAsync(CreateValue("invalid", "field", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid component. Cannot find schema." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid component. Cannot find schema." }); - } + [Fact] + public async Task Should_add_error_if_value_has_invalid_discriminator_schema() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId2 }); - [Fact] - public async Task Should_add_error_if_value_has_invalid_discriminator_schema() - { - var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId2 }); + await sut.ValidateAsync(CreateValue(schemaId1.ToString(), "field", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(schemaId1.ToString(), "field", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid component. Cannot find schema." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid component. Cannot find schema." }); - } + [Fact] + public async Task Should_resolve_schema_id_from_name() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_resolve_schema_id_from_name() - { - var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + var value = CreateValue("my-component", "componentField", 1, "schemaName"); - var value = CreateValue("my-component", "componentField", 1, "schemaName"); + await sut.ValidateAsync(value, errors, components: components); - await sut.ValidateAsync(value, errors, components: components); + Assert.Empty(errors); + Assert.Equal(value.AsObject[Component.Discriminator].AsString, schemaId1.ToString()); + } - Assert.Empty(errors); - Assert.Equal(value.AsObject[Component.Discriminator].AsString, schemaId1.ToString()); - } + [Fact] + public async Task Should_resolve_schema_from_single_component() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_resolve_schema_from_single_component() - { - var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + var value = CreateValue(null, "componentField", 1); - var value = CreateValue(null, "componentField", 1); + await sut.ValidateAsync(value, errors, components: components); - await sut.ValidateAsync(value, errors, components: components); + Assert.Empty(errors); + Assert.Equal(value.AsObject[Component.Discriminator].AsString, schemaId1.ToString()); + } - Assert.Empty(errors); - Assert.Equal(value.AsObject[Component.Discriminator].AsString, schemaId1.ToString()); - } + private static JsonValue CreateValue(string? type, string key, JsonValue value, string? discriminator = null) + { + var obj = new JsonObject(); - private static JsonValue CreateValue(string? type, string key, JsonValue value, string? discriminator = null) + if (type != null) { - var obj = new JsonObject(); - - if (type != null) - { - discriminator ??= Component.Discriminator; + discriminator ??= Component.Discriminator; - obj[discriminator] = JsonValue.Create(type); - } + obj[discriminator] = JsonValue.Create(type); + } - obj.Add(key, value); + obj.Add(key, value); - return obj; - } + return obj; + } - private (DomainId, RootField<ComponentFieldProperties>, ResolvedComponents) Field(ComponentFieldProperties properties, bool isRequired = false) - { - var schema = - new Schema("my-component") - .AddNumber(1, "componentField", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = isRequired }); + private (DomainId, RootField<ComponentFieldProperties>, ResolvedComponents) Field(ComponentFieldProperties properties, bool isRequired = false) + { + var schema = + new Schema("my-component") + .AddNumber(1, "componentField", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = isRequired }); - var field = Fields.Component(1, "myComponent", Partitioning.Invariant, properties); + var field = Fields.Component(1, "myComponent", Partitioning.Invariant, properties); - var components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [schemaId1] = schema, - [schemaId2] = schema - }); + var components = new ResolvedComponents(new Dictionary<DomainId, Schema> + { + [schemaId1] = schema, + [schemaId2] = schema + }); - return (schemaId1, field, components); - } + return (schemaId1, field, components); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs index af1560d229..03e1fd81aa 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs @@ -14,227 +14,226 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class ComponentsFieldTests : IClassFixture<TranslationsFixture> { - public class ComponentsFieldTests : IClassFixture<TranslationsFixture> + private readonly DomainId schemaId1 = DomainId.NewGuid(); + private readonly DomainId schemaId2 = DomainId.NewGuid(); + private readonly List<string> errors = new List<string>(); + + [Fact] + public void Should_instantiate_field() { - private readonly DomainId schemaId1 = DomainId.NewGuid(); - private readonly DomainId schemaId2 = DomainId.NewGuid(); - private readonly List<string> errors = new List<string>(); + var (_, sut, _) = Field(new ComponentsFieldProperties()); - [Fact] - public void Should_instantiate_field() - { - var (_, sut, _) = Field(new ComponentsFieldProperties()); + Assert.Equal("myComponents", sut.Name); + } - Assert.Equal("myComponents", sut.Name); - } + [Fact] + public async Task Should_not_add_error_if_components_are_null_and_valid() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_not_add_error_if_components_are_null_and_valid() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync(null, errors, components: components); - await sut.ValidateAsync(null, errors, components: components); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_not_add_error_if_components_is_valid() + { + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_not_add_error_if_components_is_valid() - { - var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync(CreateValue(1, id.ToString(), "componentField", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(1, id.ToString(), "componentField", 1), errors, components: components); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_not_add_error_if_number_of_components_is_equal_to_min_and_max_components() + { + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 2, MaxItems = 2 }); - [Fact] - public async Task Should_not_add_error_if_number_of_components_is_equal_to_min_and_max_components() - { - var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 2, MaxItems = 2 }); + await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_add_error_if_components_are_required() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, IsRequired = true }); - [Fact] - public async Task Should_add_error_if_components_are_required() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, IsRequired = true }); + await sut.ValidateAsync(null, errors, components: components); - await sut.ValidateAsync(null, errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + [Fact] + public async Task Should_add_error_if_components_value_is_required() + { + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, IsRequired = true }, true); - [Fact] - public async Task Should_add_error_if_components_value_is_required() - { - var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, IsRequired = true }, true); + await sut.ValidateAsync(CreateValue(1, id.ToString(), "componentField", default), errors, components: components); - await sut.ValidateAsync(CreateValue(1, id.ToString(), "componentField", default), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "[1].componentField: Field is required." }); + } - errors.Should().BeEquivalentTo( - new[] { "[1].componentField: Field is required." }); - } + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync(JsonValue.Create("Invalid"), errors, components: components); - await sut.ValidateAsync(JsonValue.Create("Invalid"), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid json type, expected array of objects." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid json type, expected array of objects." }); - } + [Fact] + public async Task Should_add_error_if_component_is_not_valid() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_add_error_if_component_is_not_valid() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync((JsonValue)JsonValue.Array(JsonValue.Create("Invalid")), errors, components: components); - await sut.ValidateAsync((JsonValue)JsonValue.Array(JsonValue.Create("Invalid")), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid json object, expected object with 'schemaId' field." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid json object, expected object with 'schemaId' field." }); - } + [Fact] + public async Task Should_add_error_if_component_has_no_discriminator() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaIds = ReadonlyList.Create(schemaId1, schemaId2) }); - [Fact] - public async Task Should_add_error_if_component_has_no_discriminator() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaIds = ReadonlyList.Create(schemaId1, schemaId2) }); + await sut.ValidateAsync(CreateValue(1, null, "field", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(1, null, "field", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid component. No 'schemaId' field found." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid component. No 'schemaId' field found." }); - } + [Fact] + public async Task Should_add_error_if_value_has_invalid_discriminator_format() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_add_error_if_value_has_invalid_discriminator_format() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + await sut.ValidateAsync(CreateValue(1, "invalid", "field", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(1, "invalid", "field", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid component. Cannot find schema." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid component. Cannot find schema." }); - } + [Fact] + public async Task Should_add_error_if_value_has_invalid_discriminator_schema() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId2 }); - [Fact] - public async Task Should_add_error_if_value_has_invalid_discriminator_schema() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId2 }); + await sut.ValidateAsync(CreateValue(1, schemaId1.ToString(), "field", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(1, schemaId1.ToString(), "field", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Invalid component. Cannot find schema." }); + } - errors.Should().BeEquivalentTo( - new[] { "Invalid component. Cannot find schema." }); - } + [Fact] + public async Task Should_add_error_if_value_has_not_enough_components() + { + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 3 }); - [Fact] - public async Task Should_add_error_if_value_has_not_enough_components() - { - var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 3 }); + await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } + [Fact] + public async Task Should_add_error_if_value_has_too_much_components() + { + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MaxItems = 1 }); - [Fact] - public async Task Should_add_error_if_value_has_too_much_components() - { - var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MaxItems = 1 }); + await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } + [Fact] + public async Task Should_add_error_if_value_has_duplicates() + { + var (id, sut, components) = Field(new ComponentsFieldProperties { UniqueFields = ReadonlyList.Create("componentField") }); - [Fact] - public async Task Should_add_error_if_value_has_duplicates() - { - var (id, sut, components) = Field(new ComponentsFieldProperties { UniqueFields = ReadonlyList.Create("componentField") }); + await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); - await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); + errors.Should().BeEquivalentTo( + new[] { "Must not contain items with duplicate 'componentField' fields." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must not contain items with duplicate 'componentField' fields." }); - } + [Fact] + public async Task Should_resolve_schema_id_from_name() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_resolve_schema_id_from_name() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + var value = CreateValue(1, "my-component", "componentField", 1, "schemaName"); - var value = CreateValue(1, "my-component", "componentField", 1, "schemaName"); + await sut.ValidateAsync(value, errors, components: components); - await sut.ValidateAsync(value, errors, components: components); + Assert.Empty(errors); + Assert.Equal(value.AsArray[0].AsObject[Component.Discriminator].AsString, schemaId1.ToString()); + } - Assert.Empty(errors); - Assert.Equal(value.AsArray[0].AsObject[Component.Discriminator].AsString, schemaId1.ToString()); - } + [Fact] + public async Task Should_resolve_schema_from_single_component() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - [Fact] - public async Task Should_resolve_schema_from_single_component() - { - var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + var value = CreateValue(1, null, "componentField", 1); - var value = CreateValue(1, null, "componentField", 1); + await sut.ValidateAsync(value, errors, components: components); - await sut.ValidateAsync(value, errors, components: components); + Assert.Empty(errors); + Assert.Equal(value.AsArray[0].AsObject[Component.Discriminator].AsString, schemaId1.ToString()); + } - Assert.Empty(errors); - Assert.Equal(value.AsArray[0].AsObject[Component.Discriminator].AsString, schemaId1.ToString()); - } + private static JsonValue CreateValue(int count, string? type, string key, JsonValue value, string? discriminator = null) + { + var actual = new JsonArray(); - private static JsonValue CreateValue(int count, string? type, string key, JsonValue value, string? discriminator = null) + for (var i = 0; i < count; i++) { - var actual = new JsonArray(); + var obj = new JsonObject(); - for (var i = 0; i < count; i++) + if (type != null) { - var obj = new JsonObject(); - - if (type != null) - { - discriminator ??= Component.Discriminator; - - obj[discriminator] = JsonValue.Create(type); - } + discriminator ??= Component.Discriminator; - obj.Add(key, value); - - actual.Add(obj); + obj[discriminator] = JsonValue.Create(type); } - return actual; + obj.Add(key, value); + + actual.Add(obj); } - private (DomainId, RootField<ComponentsFieldProperties>, ResolvedComponents) Field(ComponentsFieldProperties properties, bool isRequired = false) - { - var schema = - new Schema("my-component") - .AddNumber(1, "componentField", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = isRequired }); + return actual; + } - var field = Fields.Components(1, "myComponents", Partitioning.Invariant, properties); + private (DomainId, RootField<ComponentsFieldProperties>, ResolvedComponents) Field(ComponentsFieldProperties properties, bool isRequired = false) + { + var schema = + new Schema("my-component") + .AddNumber(1, "componentField", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = isRequired }); - var components = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [schemaId1] = schema, - [schemaId2] = schema - }); + var field = Fields.Components(1, "myComponents", Partitioning.Invariant, properties); - return (schemaId1, field, components); - } + var components = new ResolvedComponents(new Dictionary<DomainId, Schema> + { + [schemaId1] = schema, + [schemaId2] = schema + }); + + return (schemaId1, field, components); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 09d69095e5..7b282cfeaf 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -17,444 +17,443 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class ContentValidationTests : IClassFixture<TranslationsFixture> { - public class ContentValidationTests : IClassFixture<TranslationsFixture> + private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); + private readonly List<ValidationError> errors = new List<ValidationError>(); + private Schema schema = new Schema("my-schema"); + + [Fact] + public async Task Should_add_error_if_value_validator_throws_exception() + { + var validator = A.Fake<IValidator>(); + + A.CallTo(() => validator.Validate(A<object?>._, A<ValidationContext>._)) + .Throws(new InvalidOperationException()); + + var validatorFactory = A.Fake<IValidatorsFactory>(); + + A.CallTo(() => validatorFactory.CreateValueValidators(A<ValidationContext>._, A<IField>._, A<ValidatorFactory>._)) + .Returns(Enumerable.Repeat(validator, 1)); + + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, + new NumberFieldProperties()); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddInvariant(1000)); + + await data.ValidateAsync(languages.ToResolver(), errors, schema, factory: validatorFactory); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Validation failed with internal error.", "myField.iv") + }); + } + + [Fact] + public async Task Should_add_error_if_field_validator_throws_exception() + { + var validator = A.Fake<IValidator>(); + + A.CallTo(() => validator.Validate(A<object?>._, A<ValidationContext>._)) + .Throws(new InvalidOperationException()); + + var validatorFactory = A.Fake<IValidatorsFactory>(); + + A.CallTo(() => validatorFactory.CreateFieldValidators(A<ValidationContext>._, A<IField>._, A<ValidatorFactory>._)) + .Returns(Enumerable.Repeat(validator, 1)); + + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, + new NumberFieldProperties()); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddInvariant(1000)); + + await data.ValidateAsync(languages.ToResolver(), errors, schema, factory: validatorFactory); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Validation failed with internal error.", "myField") + }); + } + + [Fact] + public async Task Should_add_error_if_validating_data_with_unknown_field() + { + var data = + new ContentData() + .AddField("unknown", + new ContentFieldData()); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Not a known field.", "unknown") + }); + } + + [Fact] + public async Task Should_add_error_if_validating_data_with_invalid_field() { - private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); - private readonly List<ValidationError> errors = new List<ValidationError>(); - private Schema schema = new Schema("my-schema"); - - [Fact] - public async Task Should_add_error_if_value_validator_throws_exception() - { - var validator = A.Fake<IValidator>(); - - A.CallTo(() => validator.Validate(A<object?>._, A<ValidationContext>._)) - .Throws(new InvalidOperationException()); - - var validatorFactory = A.Fake<IValidatorsFactory>(); - - A.CallTo(() => validatorFactory.CreateValueValidators(A<ValidationContext>._, A<IField>._, A<ValidatorFactory>._)) - .Returns(Enumerable.Repeat(validator, 1)); - - schema = schema.AddNumber(1, "myField", Partitioning.Invariant, - new NumberFieldProperties()); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddInvariant(1000)); - - await data.ValidateAsync(languages.ToResolver(), errors, schema, factory: validatorFactory); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Validation failed with internal error.", "myField.iv") - }); - } - - [Fact] - public async Task Should_add_error_if_field_validator_throws_exception() - { - var validator = A.Fake<IValidator>(); - - A.CallTo(() => validator.Validate(A<object?>._, A<ValidationContext>._)) - .Throws(new InvalidOperationException()); - - var validatorFactory = A.Fake<IValidatorsFactory>(); - - A.CallTo(() => validatorFactory.CreateFieldValidators(A<ValidationContext>._, A<IField>._, A<ValidatorFactory>._)) - .Returns(Enumerable.Repeat(validator, 1)); - - schema = schema.AddNumber(1, "myField", Partitioning.Invariant, - new NumberFieldProperties()); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddInvariant(1000)); - - await data.ValidateAsync(languages.ToResolver(), errors, schema, factory: validatorFactory); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Validation failed with internal error.", "myField") - }); - } - - [Fact] - public async Task Should_add_error_if_validating_data_with_unknown_field() - { - var data = - new ContentData() - .AddField("unknown", - new ContentFieldData()); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Not a known field.", "unknown") - }); - } - - [Fact] - public async Task Should_add_error_if_validating_data_with_invalid_field() - { - schema = schema.AddNumber(1, "myField", Partitioning.Invariant, - new NumberFieldProperties { MaxValue = 100 }); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddInvariant(1000)); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Must be less or equal to 100.", "myField.iv") - }); - } - - [Fact] - public async Task Should_add_error_if_non_localizable_data_field_contains_language() - { - schema = schema.AddNumber(1, "myField", Partitioning.Invariant); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddLocalized("es", 1) - .AddLocalized("it", 1)); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Not a known invariant value.", "myField.es"), - new ValidationError("Not a known invariant value.", "myField.it") - }); - } - - [Fact] - public async Task Should_add_error_if_validating_data_with_invalid_localizable_field() - { - schema = schema.AddNumber(1, "myField", Partitioning.Language, - new NumberFieldProperties { IsRequired = true }); - - var data = - new ContentData(); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Field is required.", "myField.de"), - new ValidationError("Field is required.", "myField.en") - }); - } - - [Fact] - public async Task Should_add_error_if_required_data_field_is_not_in_bag() - { - schema = schema.AddNumber(1, "myField", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = true }); - - var data = - new ContentData(); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Field is required.", "myField.iv") - }); - } - - [Fact] - public async Task Should_add_error_if_required_data_string_field_is_not_in_bag() - { - schema = schema.AddString(1, "myField", Partitioning.Invariant, - new StringFieldProperties { IsRequired = true }); - - var data = - new ContentData(); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Field is required.", "myField.iv") - }); - } - - [Fact] - public async Task Should_add_error_if_data_contains_invalid_language() - { - schema = schema.AddNumber(1, "myField", Partitioning.Language); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddLocalized("de", 1) - .AddLocalized("ru", 1)); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Not a known language.", "myField.ru") - }); - } - - [Fact] - public async Task Should_not_add_error_if_required_field_has_no_value_for_optional_language() - { - var optionalConfig = - LanguagesConfig.English - .Set(Language.ES) - .Set(Language.IT, true) - .Remove(Language.EN); - - schema = schema.AddString(1, "myField", Partitioning.Language, - new StringFieldProperties { IsRequired = true }); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddLocalized("es", "value")); - - await data.ValidateAsync(optionalConfig.ToResolver(), errors, schema); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_data_contains_unsupported_language() - { - schema = schema.AddNumber(1, "myField", Partitioning.Language); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddLocalized("es", 1) - .AddLocalized("it", 1)); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Not a known language.", "myField.es"), - new ValidationError("Not a known language.", "myField.it") - }); - } - - [Fact] - public async Task Should_add_error_if_validating_partial_data_with_unknown_field() - { - var data = - new ContentData() - .AddField("unknown", - new ContentFieldData()); - - await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Not a known field.", "unknown") - }); - } - - [Fact] - public async Task Should_add_error_if_validating_partial_data_with_invalid_field() - { - schema = schema.AddNumber(1, "myField", Partitioning.Invariant, - new NumberFieldProperties { MaxValue = 100 }); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddInvariant(1000)); - - await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Must be less or equal to 100.", "myField.iv") - }); - } - - [Fact] - public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language() - { - schema = schema.AddNumber(1, "myField", Partitioning.Invariant); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddLocalized("es", 1) - .AddLocalized("it", 1)); - - await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Not a known invariant value.", "myField.es"), - new ValidationError("Not a known invariant value.", "myField.it") - }); - } - - [Fact] - public async Task Should_not_add_error_if_validating_partial_data_with_invalid_localizable_field() - { - schema = schema.AddNumber(1, "myField", Partitioning.Language, - new NumberFieldProperties { IsRequired = true }); - - var data = - new ContentData(); - - await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_required_partial_data_field_is_not_in_bag() - { - schema = schema.AddNumber(1, "myField", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = true }); - - var data = - new ContentData(); - - await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_partial_data_contains_invalid_language() - { - schema = schema.AddNumber(1, "myField", Partitioning.Language); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddLocalized("de", 1) - .AddLocalized("ru", 1)); - - await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Not a known language.", "myField.ru") - }); - } - - [Fact] - public async Task Should_add_error_if_partial_data_contains_unsupported_language() - { - schema = schema.AddNumber(1, "myField", Partitioning.Language); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddLocalized("es", 1) - .AddLocalized("it", 1)); - - await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Not a known language.", "myField.es"), - new ValidationError("Not a known language.", "myField.it") - }); - } - - [Fact] - public async Task Should_add_error_if_array_field_has_required_nested_field() - { - schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f. - AddNumber(2, "myNested", new NumberFieldProperties { IsRequired = true })); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject(), - new JsonObject().Add("myNested", 1), - new JsonObject()))); - - await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Field is required.", "myField.iv[1].myNested"), - new ValidationError("Field is required.", "myField.iv[3].myNested") - }); - } - - [Fact] - public async Task Should_not_add_error_if_separator_not_defined() - { - schema = schema.AddUI(2, "ui", Partitioning.Invariant); - - var data = - new ContentData(); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_nested_separator_not_defined() - { - schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f. - AddUI(2, "myNested")); - - var data = - new ContentData() - .AddField("myField", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject()))); - - await data.ValidateAsync(languages.ToResolver(), errors, schema); - - Assert.Empty(errors); - } + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, + new NumberFieldProperties { MaxValue = 100 }); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddInvariant(1000)); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Must be less or equal to 100.", "myField.iv") + }); + } + + [Fact] + public async Task Should_add_error_if_non_localizable_data_field_contains_language() + { + schema = schema.AddNumber(1, "myField", Partitioning.Invariant); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("es", 1) + .AddLocalized("it", 1)); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Not a known invariant value.", "myField.es"), + new ValidationError("Not a known invariant value.", "myField.it") + }); + } + + [Fact] + public async Task Should_add_error_if_validating_data_with_invalid_localizable_field() + { + schema = schema.AddNumber(1, "myField", Partitioning.Language, + new NumberFieldProperties { IsRequired = true }); + + var data = + new ContentData(); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Field is required.", "myField.de"), + new ValidationError("Field is required.", "myField.en") + }); + } + + [Fact] + public async Task Should_add_error_if_required_data_field_is_not_in_bag() + { + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true }); + + var data = + new ContentData(); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Field is required.", "myField.iv") + }); + } + + [Fact] + public async Task Should_add_error_if_required_data_string_field_is_not_in_bag() + { + schema = schema.AddString(1, "myField", Partitioning.Invariant, + new StringFieldProperties { IsRequired = true }); + + var data = + new ContentData(); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Field is required.", "myField.iv") + }); + } + + [Fact] + public async Task Should_add_error_if_data_contains_invalid_language() + { + schema = schema.AddNumber(1, "myField", Partitioning.Language); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("de", 1) + .AddLocalized("ru", 1)); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Not a known language.", "myField.ru") + }); + } + + [Fact] + public async Task Should_not_add_error_if_required_field_has_no_value_for_optional_language() + { + var optionalConfig = + LanguagesConfig.English + .Set(Language.ES) + .Set(Language.IT, true) + .Remove(Language.EN); + + schema = schema.AddString(1, "myField", Partitioning.Language, + new StringFieldProperties { IsRequired = true }); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("es", "value")); + + await data.ValidateAsync(optionalConfig.ToResolver(), errors, schema); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_data_contains_unsupported_language() + { + schema = schema.AddNumber(1, "myField", Partitioning.Language); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("es", 1) + .AddLocalized("it", 1)); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Not a known language.", "myField.es"), + new ValidationError("Not a known language.", "myField.it") + }); + } + + [Fact] + public async Task Should_add_error_if_validating_partial_data_with_unknown_field() + { + var data = + new ContentData() + .AddField("unknown", + new ContentFieldData()); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Not a known field.", "unknown") + }); + } + + [Fact] + public async Task Should_add_error_if_validating_partial_data_with_invalid_field() + { + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, + new NumberFieldProperties { MaxValue = 100 }); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddInvariant(1000)); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Must be less or equal to 100.", "myField.iv") + }); + } + + [Fact] + public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language() + { + schema = schema.AddNumber(1, "myField", Partitioning.Invariant); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("es", 1) + .AddLocalized("it", 1)); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Not a known invariant value.", "myField.es"), + new ValidationError("Not a known invariant value.", "myField.it") + }); + } + + [Fact] + public async Task Should_not_add_error_if_validating_partial_data_with_invalid_localizable_field() + { + schema = schema.AddNumber(1, "myField", Partitioning.Language, + new NumberFieldProperties { IsRequired = true }); + + var data = + new ContentData(); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_required_partial_data_field_is_not_in_bag() + { + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true }); + + var data = + new ContentData(); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_partial_data_contains_invalid_language() + { + schema = schema.AddNumber(1, "myField", Partitioning.Language); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("de", 1) + .AddLocalized("ru", 1)); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Not a known language.", "myField.ru") + }); + } + + [Fact] + public async Task Should_add_error_if_partial_data_contains_unsupported_language() + { + schema = schema.AddNumber(1, "myField", Partitioning.Language); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("es", 1) + .AddLocalized("it", 1)); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Not a known language.", "myField.es"), + new ValidationError("Not a known language.", "myField.it") + }); + } + + [Fact] + public async Task Should_add_error_if_array_field_has_required_nested_field() + { + schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f. + AddNumber(2, "myNested", new NumberFieldProperties { IsRequired = true })); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject(), + new JsonObject().Add("myNested", 1), + new JsonObject()))); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Field is required.", "myField.iv[1].myNested"), + new ValidationError("Field is required.", "myField.iv[3].myNested") + }); + } + + [Fact] + public async Task Should_not_add_error_if_separator_not_defined() + { + schema = schema.AddUI(2, "ui", Partitioning.Invariant); + + var data = + new ContentData(); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_nested_separator_not_defined() + { + schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f. + AddUI(2, "myNested")); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject()))); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs index 34ec7fef12..5898616a40 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs @@ -13,98 +13,97 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class DateTimeFieldTests : IClassFixture<TranslationsFixture> { - public class DateTimeFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new DateTimeFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new DateTimeFieldProperties()); - Assert.Equal("myDatetime", sut.Name); - } + Assert.Equal("myDatetime", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_datetime_is_valid() - { - var sut = Field(new DateTimeFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_datetime_is_valid() + { + var sut = Field(new DateTimeFieldProperties()); - await sut.ValidateAsync(JsonValue.Null, errors); + await sut.ValidateAsync(JsonValue.Null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_datetime_is_required() - { - var sut = Field(new DateTimeFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_datetime_is_required() + { + var sut = Field(new DateTimeFieldProperties { IsRequired = true }); - await sut.ValidateAsync(JsonValue.Null, errors); + await sut.ValidateAsync(JsonValue.Null, errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_datetime_is_less_than_min() - { - var sut = Field(new DateTimeFieldProperties { MinValue = FutureDays(10) }); + [Fact] + public async Task Should_add_error_if_datetime_is_less_than_min() + { + var sut = Field(new DateTimeFieldProperties { MinValue = FutureDays(10) }); - await sut.ValidateAsync(CreateValue(FutureDays(0)), errors); + await sut.ValidateAsync(CreateValue(FutureDays(0)), errors); - errors.Should().BeEquivalentTo( - new[] { $"Must be greater or equal to {sut.Properties.MinValue}." }); - } + errors.Should().BeEquivalentTo( + new[] { $"Must be greater or equal to {sut.Properties.MinValue}." }); + } - [Fact] - public async Task Should_add_error_if_datetime_is_greater_than_max() - { - var sut = Field(new DateTimeFieldProperties { MaxValue = FutureDays(10) }); + [Fact] + public async Task Should_add_error_if_datetime_is_greater_than_max() + { + var sut = Field(new DateTimeFieldProperties { MaxValue = FutureDays(10) }); - await sut.ValidateAsync(CreateValue(FutureDays(20)), errors); + await sut.ValidateAsync(CreateValue(FutureDays(20)), errors); - errors.Should().BeEquivalentTo( - new[] { $"Must be less or equal to {FutureDays(10)}." }); - } + errors.Should().BeEquivalentTo( + new[] { $"Must be less or equal to {FutureDays(10)}." }); + } - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new DateTimeFieldProperties()); + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new DateTimeFieldProperties()); - await sut.ValidateAsync(JsonValue.Create("Invalid"), errors); + await sut.ValidateAsync(JsonValue.Create("Invalid"), errors); - errors.Should().BeEquivalentTo( - new[] { "The value string does not match the required number from the format string \"uuuu\". Value being parsed: '^Invalid'. (^ indicates error position.)" }); - } + errors.Should().BeEquivalentTo( + new[] { "The value string does not match the required number from the format string \"uuuu\". Value being parsed: '^Invalid'. (^ indicates error position.)" }); + } - [Fact] - public async Task Should_add_error_if_value_is_another_type() - { - var sut = Field(new DateTimeFieldProperties()); + [Fact] + public async Task Should_add_error_if_value_is_another_type() + { + var sut = Field(new DateTimeFieldProperties()); - await sut.ValidateAsync(JsonValue.Create(123), errors); + await sut.ValidateAsync(JsonValue.Create(123), errors); - errors.Should().BeEquivalentTo( - new[] { "Invalid json type, expected string." }); - } + errors.Should().BeEquivalentTo( + new[] { "Invalid json type, expected string." }); + } - private static Instant FutureDays(int days) - { - return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days)); - } + private static Instant FutureDays(int days) + { + return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days)); + } - private static JsonValue CreateValue(Instant v) - { - return JsonValue.Create(v); - } + private static JsonValue CreateValue(Instant v) + { + return JsonValue.Create(v); + } - private static RootField<DateTimeFieldProperties> Field(DateTimeFieldProperties properties) - { - return Fields.DateTime(1, "myDatetime", Partitioning.Invariant, properties); - } + private static RootField<DateTimeFieldProperties> Field(DateTimeFieldProperties properties) + { + return Fields.DateTime(1, "myDatetime", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs index 41ccb23212..49ce6564a2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs @@ -11,99 +11,98 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class GeolocationFieldTests : IClassFixture<TranslationsFixture> { - public class GeolocationFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new GeolocationFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new GeolocationFieldProperties()); - Assert.Equal("myGeolocation", sut.Name); - } + Assert.Equal("myGeolocation", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_geolocation_is_valid_null() - { - var sut = Field(new GeolocationFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_geolocation_is_valid_null() + { + var sut = Field(new GeolocationFieldProperties()); - await sut.ValidateAsync(JsonValue.Null, errors); + await sut.ValidateAsync(JsonValue.Null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_geolocation_is_valid_geojson() - { - var sut = Field(new GeolocationFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_geolocation_is_valid_geojson() + { + var sut = Field(new GeolocationFieldProperties()); - var geolocation = new JsonObject() - .Add("coordinates", - JsonValue.Array( - JsonValue.Create(12), - JsonValue.Create(45) - )) - .Add("type", "Point"); + var geolocation = new JsonObject() + .Add("coordinates", + JsonValue.Array( + JsonValue.Create(12), + JsonValue.Create(45) + )) + .Add("type", "Point"); - await sut.ValidateAsync(geolocation, errors); + await sut.ValidateAsync(geolocation, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_geolocation_is_valid() - { - var sut = Field(new GeolocationFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_geolocation_is_valid() + { + var sut = Field(new GeolocationFieldProperties()); - await sut.ValidateAsync(CreateValue(0, 0), errors); + await sut.ValidateAsync(CreateValue(0, 0), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_geolocation_has_invalid_latitude() - { - var sut = Field(new GeolocationFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_geolocation_has_invalid_latitude() + { + var sut = Field(new GeolocationFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(200, 0), errors); + await sut.ValidateAsync(CreateValue(200, 0), errors); - errors.Should().BeEquivalentTo( - new[] { "Latitude must be between -90 and 90." }); - } + errors.Should().BeEquivalentTo( + new[] { "Latitude must be between -90 and 90." }); + } - [Fact] - public async Task Should_add_error_if_geolocation_has_invalid_longitude() - { - var sut = Field(new GeolocationFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_geolocation_has_invalid_longitude() + { + var sut = Field(new GeolocationFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(0, 200), errors); + await sut.ValidateAsync(CreateValue(0, 200), errors); - errors.Should().BeEquivalentTo( - new[] { "Longitude must be between -180 and 180." }); - } + errors.Should().BeEquivalentTo( + new[] { "Longitude must be between -180 and 180." }); + } - [Fact] - public async Task Should_add_error_if_geolocation_is_required() - { - var sut = Field(new GeolocationFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_geolocation_is_required() + { + var sut = Field(new GeolocationFieldProperties { IsRequired = true }); - await sut.ValidateAsync(JsonValue.Null, errors); + await sut.ValidateAsync(JsonValue.Null, errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - private static JsonValue CreateValue(double lat, double lon) - { - return new JsonObject().Add("latitude", lat).Add("longitude", lon); - } + private static JsonValue CreateValue(double lat, double lon) + { + return new JsonObject().Add("latitude", lat).Add("longitude", lon); + } - private static RootField<GeolocationFieldProperties> Field(GeolocationFieldProperties properties) - { - return Fields.Geolocation(1, "myGeolocation", Partitioning.Invariant, properties); - } + private static RootField<GeolocationFieldProperties> Field(GeolocationFieldProperties properties) + { + return Fields.Geolocation(1, "myGeolocation", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs index 5a2bc54b23..551a452b4a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs @@ -11,49 +11,48 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class JsonFieldTests : IClassFixture<TranslationsFixture> { - public class JsonFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new JsonFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new JsonFieldProperties()); - Assert.Equal("myJson", sut.Name); - } + Assert.Equal("myJson", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_json_is_valid() - { - var sut = Field(new JsonFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_json_is_valid() + { + var sut = Field(new JsonFieldProperties()); - await sut.ValidateAsync(CreateValue(JsonValue.Create(1)), errors); + await sut.ValidateAsync(CreateValue(JsonValue.Create(1)), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_json_is_required() - { - var sut = Field(new JsonFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_json_is_required() + { + var sut = Field(new JsonFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(JsonValue.Null), errors); + await sut.ValidateAsync(CreateValue(JsonValue.Null), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - private static JsonValue CreateValue(JsonValue v) - { - return v; - } + private static JsonValue CreateValue(JsonValue v) + { + return v; + } - private static RootField<JsonFieldProperties> Field(JsonFieldProperties properties) - { - return Fields.Json(1, "myJson", Partitioning.Invariant, properties); - } + private static RootField<JsonFieldProperties> Field(JsonFieldProperties properties) + { + return Fields.Json(1, "myJson", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs index 0aa42106cc..2b70374974 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs @@ -12,93 +12,92 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class NumberFieldTests : IClassFixture<TranslationsFixture> { - public class NumberFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new NumberFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new NumberFieldProperties()); - Assert.Equal("myNumber", sut.Name); - } + Assert.Equal("myNumber", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_number_is_valid() - { - var sut = Field(new NumberFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_number_is_valid() + { + var sut = Field(new NumberFieldProperties()); - await sut.ValidateAsync(JsonValue.Null, errors); + await sut.ValidateAsync(JsonValue.Null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_number_is_required() - { - var sut = Field(new NumberFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_number_is_required() + { + var sut = Field(new NumberFieldProperties { IsRequired = true }); - await sut.ValidateAsync(JsonValue.Null, errors); + await sut.ValidateAsync(JsonValue.Null, errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_number_is_less_than_min() - { - var sut = Field(new NumberFieldProperties { MinValue = 10 }); + [Fact] + public async Task Should_add_error_if_number_is_less_than_min() + { + var sut = Field(new NumberFieldProperties { MinValue = 10 }); - await sut.ValidateAsync(CreateValue(5), errors); + await sut.ValidateAsync(CreateValue(5), errors); - errors.Should().BeEquivalentTo( - new[] { "Must be greater or equal to 10." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must be greater or equal to 10." }); + } - [Fact] - public async Task Should_add_error_if_number_is_greater_than_max() - { - var sut = Field(new NumberFieldProperties { MaxValue = 10 }); + [Fact] + public async Task Should_add_error_if_number_is_greater_than_max() + { + var sut = Field(new NumberFieldProperties { MaxValue = 10 }); - await sut.ValidateAsync(CreateValue(20), errors); + await sut.ValidateAsync(CreateValue(20), errors); - errors.Should().BeEquivalentTo( - new[] { "Must be less or equal to 10." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must be less or equal to 10." }); + } - [Fact] - public async Task Should_add_error_if_number_is_not_allowed() - { - var sut = Field(new NumberFieldProperties { AllowedValues = ReadonlyList.Create(10d) }); + [Fact] + public async Task Should_add_error_if_number_is_not_allowed() + { + var sut = Field(new NumberFieldProperties { AllowedValues = ReadonlyList.Create(10d) }); - await sut.ValidateAsync(CreateValue(20), errors); + await sut.ValidateAsync(CreateValue(20), errors); - errors.Should().BeEquivalentTo( - new[] { "Not an allowed value." }); - } + errors.Should().BeEquivalentTo( + new[] { "Not an allowed value." }); + } - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new NumberFieldProperties()); + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new NumberFieldProperties()); - await sut.ValidateAsync(JsonValue.Create("Invalid"), errors); + await sut.ValidateAsync(JsonValue.Create("Invalid"), errors); - errors.Should().BeEquivalentTo( - new[] { "Invalid json type, expected number." }); - } + errors.Should().BeEquivalentTo( + new[] { "Invalid json type, expected number." }); + } - private static JsonValue CreateValue(double v) - { - return JsonValue.Create(v); - } + private static JsonValue CreateValue(double v) + { + return JsonValue.Create(v); + } - private static RootField<NumberFieldProperties> Field(NumberFieldProperties properties) - { - return Fields.Number(1, "myNumber", Partitioning.Invariant, properties); - } + private static RootField<NumberFieldProperties> Field(NumberFieldProperties properties) + { + return Fields.Number(1, "myNumber", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 0f46c9b6d9..859aa0f86c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -15,162 +15,161 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class ReferencesFieldTests : IClassFixture<TranslationsFixture> { - public class ReferencesFieldTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + private readonly DomainId schemaId = DomainId.NewGuid(); + private readonly DomainId ref1 = DomainId.NewGuid(); + private readonly DomainId ref2 = DomainId.NewGuid(); + private readonly IValidatorsFactory factory; + + private sealed class CustomFactory : IValidatorsFactory { - private readonly List<string> errors = new List<string>(); - private readonly DomainId schemaId = DomainId.NewGuid(); - private readonly DomainId ref1 = DomainId.NewGuid(); - private readonly DomainId ref2 = DomainId.NewGuid(); - private readonly IValidatorsFactory factory; + private readonly DomainId schemaId; - private sealed class CustomFactory : IValidatorsFactory + public CustomFactory(DomainId schemaId) { - private readonly DomainId schemaId; - - public CustomFactory(DomainId schemaId) - { - this.schemaId = schemaId; - } + this.schemaId = schemaId; + } - public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) + public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) + { + if (field is IField<ReferencesFieldProperties> references) { - if (field is IField<ReferencesFieldProperties> references) + yield return new ReferencesValidator(references.Properties.IsRequired, references.Properties, ids => { - yield return new ReferencesValidator(references.Properties.IsRequired, references.Properties, ids => - { - var actual = ids.Select(x => new ContentIdStatus(schemaId, x, Status.Published)).ToList(); + var actual = ids.Select(x => new ContentIdStatus(schemaId, x, Status.Published)).ToList(); - return Task.FromResult<IReadOnlyList<ContentIdStatus>>(actual); - }); - } + return Task.FromResult<IReadOnlyList<ContentIdStatus>>(actual); + }); } } + } - public ReferencesFieldTests() - { - factory = new CustomFactory(schemaId); - } + public ReferencesFieldTests() + { + factory = new CustomFactory(schemaId); + } - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new ReferencesFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new ReferencesFieldProperties()); - Assert.Equal("myRefs", sut.Name); - } + Assert.Equal("myRefs", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_references_are_valid() + [Fact] + public async Task Should_not_add_error_if_references_are_valid() + { + var sut = Field(new ReferencesFieldProperties { - var sut = Field(new ReferencesFieldProperties - { - IsRequired = true, - MinItems = 1, - MaxItems = 3 - }); + IsRequired = true, + MinItems = 1, + MaxItems = 3 + }); - await sut.ValidateAsync(CreateValue(ref1), errors, factory: factory); + await sut.ValidateAsync(CreateValue(ref1), errors, factory: factory); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_references_are_null_and_valid() - { - var sut = Field(new ReferencesFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_references_are_null_and_valid() + { + var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors, factory: factory); + await sut.ValidateAsync(CreateValue(null), errors, factory: factory); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_number_of_references_is_equal_to_min_and_max_items() - { - var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); + [Fact] + public async Task Should_not_add_error_if_number_of_references_is_equal_to_min_and_max_items() + { + var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, factory: factory); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, factory: factory); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_duplicate_values_are_allowed() - { - var sut = Field(new ReferencesFieldProperties { AllowDuplicates = true }); + [Fact] + public async Task Should_not_add_error_if_duplicate_values_are_allowed() + { + var sut = Field(new ReferencesFieldProperties { AllowDuplicates = true }); - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, factory: factory); + await sut.ValidateAsync(CreateValue(ref1, ref1), errors, factory: factory); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_references_are_required_and_null() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); + [Fact] + public async Task Should_add_error_if_references_are_required_and_null() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors, factory: factory); + await sut.ValidateAsync(CreateValue(null), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_references_are_required_and_empty() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); + [Fact] + public async Task Should_add_error_if_references_are_required_and_empty() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - await sut.ValidateAsync(CreateValue(), errors, factory: factory); + await sut.ValidateAsync(CreateValue(), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, factory: factory); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, factory: factory); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_reference_contains_duplicate_values() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + [Fact] + public async Task Should_add_error_if_reference_contains_duplicate_values() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, factory: factory); + await sut.ValidateAsync(CreateValue(ref1, ref1), errors, factory: factory); - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } - private static JsonValue CreateValue(params DomainId[]? ids) - { - return ids == null ? - JsonValue.Null : - JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } + private static JsonValue CreateValue(params DomainId[]? ids) + { + return ids == null ? + JsonValue.Null : + JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); + } - private static RootField<ReferencesFieldProperties> Field(ReferencesFieldProperties properties) - { - return Fields.References(1, "myRefs", Partitioning.Invariant, properties); - } + private static RootField<ReferencesFieldProperties> Field(ReferencesFieldProperties properties) + { + return Fields.References(1, "myRefs", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs index 11b6a8fa23..c1a385f42a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -12,137 +12,136 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class StringFieldTests : IClassFixture<TranslationsFixture> { - public class StringFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new StringFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new StringFieldProperties()); - Assert.Equal("myString", sut.Name); - } + Assert.Equal("myString", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_string_is_valid() - { - var sut = Field(new StringFieldProperties { Label = "<FIELD>" }); + [Fact] + public async Task Should_not_add_error_if_string_is_valid() + { + var sut = Field(new StringFieldProperties { Label = "<FIELD>" }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_string_is_required_but_null() - { - var sut = Field(new StringFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_string_is_required_but_null() + { + var sut = Field(new StringFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_string_is_required_but_empty() - { - var sut = Field(new StringFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_string_is_required_but_empty() + { + var sut = Field(new StringFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(string.Empty), errors); + await sut.ValidateAsync(CreateValue(string.Empty), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_string_is_shorter_than_min_length() - { - var sut = Field(new StringFieldProperties { MinLength = 10 }); + [Fact] + public async Task Should_add_error_if_string_is_shorter_than_min_length() + { + var sut = Field(new StringFieldProperties { MinLength = 10 }); - await sut.ValidateAsync(CreateValue("123"), errors); + await sut.ValidateAsync(CreateValue("123"), errors); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 10 character(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 10 character(s)." }); + } - [Fact] - public async Task Should_add_error_if_string_is_longer_than_max_length() - { - var sut = Field(new StringFieldProperties { MaxLength = 5 }); + [Fact] + public async Task Should_add_error_if_string_is_longer_than_max_length() + { + var sut = Field(new StringFieldProperties { MaxLength = 5 }); - await sut.ValidateAsync(CreateValue("12345678"), errors); + await sut.ValidateAsync(CreateValue("12345678"), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 5 character(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 5 character(s)." }); + } - [Fact] - public async Task Should_add_error_if_string_is_shorter_than_min_characters() - { - var sut = Field(new StringFieldProperties { MinCharacters = 10 }); + [Fact] + public async Task Should_add_error_if_string_is_shorter_than_min_characters() + { + var sut = Field(new StringFieldProperties { MinCharacters = 10 }); - await sut.ValidateAsync(CreateValue("123"), errors); + await sut.ValidateAsync(CreateValue("123"), errors); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 10 text character(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 10 text character(s)." }); + } - [Fact] - public async Task Should_add_error_if_string_is_longer_than_max_characters() - { - var sut = Field(new StringFieldProperties { MaxCharacters = 5 }); + [Fact] + public async Task Should_add_error_if_string_is_longer_than_max_characters() + { + var sut = Field(new StringFieldProperties { MaxCharacters = 5 }); - await sut.ValidateAsync(CreateValue("12345678"), errors); + await sut.ValidateAsync(CreateValue("12345678"), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 5 text character(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 5 text character(s)." }); + } - [Fact] - public async Task Should_add_error_if_string_not_allowed() - { - var sut = Field(new StringFieldProperties { AllowedValues = ReadonlyList.Create("Foo") }); + [Fact] + public async Task Should_add_error_if_string_not_allowed() + { + var sut = Field(new StringFieldProperties { AllowedValues = ReadonlyList.Create("Foo") }); - await sut.ValidateAsync(CreateValue("Bar"), errors); + await sut.ValidateAsync(CreateValue("Bar"), errors); - errors.Should().BeEquivalentTo( - new[] { "Not an allowed value." }); - } + errors.Should().BeEquivalentTo( + new[] { "Not an allowed value." }); + } - [Fact] - public async Task Should_add_error_if_number_is_not_valid_pattern() - { - var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}" }); + [Fact] + public async Task Should_add_error_if_number_is_not_valid_pattern() + { + var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}" }); - await sut.ValidateAsync(CreateValue("abc"), errors); + await sut.ValidateAsync(CreateValue("abc"), errors); - errors.Should().BeEquivalentTo( - new[] { "Must follow the pattern." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must follow the pattern." }); + } - [Fact] - public async Task Should_add_error_if_number_is_not_valid_pattern_with_message() - { - var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message." }); + [Fact] + public async Task Should_add_error_if_number_is_not_valid_pattern_with_message() + { + var sut = Field(new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message." }); - await sut.ValidateAsync(CreateValue("abc"), errors); + await sut.ValidateAsync(CreateValue("abc"), errors); - errors.Should().BeEquivalentTo( - new[] { "Custom Error Message." }); - } + errors.Should().BeEquivalentTo( + new[] { "Custom Error Message." }); + } - private static JsonValue CreateValue(string? v) - { - return JsonValue.Create(v); - } + private static JsonValue CreateValue(string? v) + { + return JsonValue.Create(v); + } - private static RootField<StringFieldProperties> Field(StringFieldProperties properties) - { - return Fields.String(1, "myString", Partitioning.Invariant, properties); - } + private static RootField<StringFieldProperties> Field(StringFieldProperties properties) + { + return Fields.String(1, "myString", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs index aea73ca348..cfc759d7b1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs @@ -12,146 +12,145 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class TagsFieldTests : IClassFixture<TranslationsFixture> { - public class TagsFieldTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new TagsFieldProperties()); + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new TagsFieldProperties()); - Assert.Equal("myTags", sut.Name); - } + Assert.Equal("myTags", sut.Name); + } - [Fact] - public async Task Should_not_add_error_if_tags_are_valid() - { - var sut = Field(new TagsFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_tags_are_valid() + { + var sut = Field(new TagsFieldProperties()); - await sut.ValidateAsync(CreateValue("tag"), errors); + await sut.ValidateAsync(CreateValue("tag"), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_tags_are_null_and_valid() - { - var sut = Field(new TagsFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_tags_are_null_and_valid() + { + var sut = Field(new TagsFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_number_of_tags_is_equal_to_min_and_max_items() - { - var sut = Field(new TagsFieldProperties { MinItems = 2, MaxItems = 2 }); + [Fact] + public async Task Should_not_add_error_if_number_of_tags_is_equal_to_min_and_max_items() + { + var sut = Field(new TagsFieldProperties { MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue("tag1", "tag2"), errors); + await sut.ValidateAsync(CreateValue("tag1", "tag2"), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_tags_are_required_but_null() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_tags_are_required_but_null() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_tags_are_required_but_empty() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_tags_are_required_but_empty() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_tag_value_is_null() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_tag_value_is_null() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); - await sut.ValidateAsync((JsonValue)JsonValue.Array(JsonValue.Null), errors); + await sut.ValidateAsync((JsonValue)JsonValue.Array(JsonValue.Null), errors); - errors.Should().BeEquivalentTo( - new[] { "Invalid json type, expected array of strings." }); - } + errors.Should().BeEquivalentTo( + new[] { "Invalid json type, expected array of strings." }); + } - [Fact] - public async Task Should_add_error_if_tag_value_is_empty() - { - var sut = Field(new TagsFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_tag_value_is_empty() + { + var sut = Field(new TagsFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(string.Empty), errors); + await sut.ValidateAsync(CreateValue(string.Empty), errors); - errors.Should().BeEquivalentTo( - new[] { "Invalid json type, expected array of strings." }); - } + errors.Should().BeEquivalentTo( + new[] { "Invalid json type, expected array of strings." }); + } - [Fact] - public async Task Should_add_error_if_value_is_not_valid() - { - var sut = Field(new TagsFieldProperties()); + [Fact] + public async Task Should_add_error_if_value_is_not_valid() + { + var sut = Field(new TagsFieldProperties()); - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + await sut.ValidateAsync(JsonValue.Create("invalid"), errors); - errors.Should().BeEquivalentTo( - new[] { "Invalid json type, expected array of strings." }); - } + errors.Should().BeEquivalentTo( + new[] { "Invalid json type, expected array of strings." }); + } - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new TagsFieldProperties { MinItems = 3 }); + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Field(new TagsFieldProperties { MinItems = 3 }); - await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); + await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 3 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new TagsFieldProperties { MaxItems = 1 }); + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Field(new TagsFieldProperties { MaxItems = 1 }); - await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); + await sut.ValidateAsync(CreateValue("tag-1", "tag-2"), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_contains_an_not_allowed_values() - { - var sut = Field(new TagsFieldProperties { AllowedValues = ReadonlyList.Create("tag-2", "tag-3") }); + [Fact] + public async Task Should_add_error_if_value_contains_an_not_allowed_values() + { + var sut = Field(new TagsFieldProperties { AllowedValues = ReadonlyList.Create("tag-2", "tag-3") }); - await sut.ValidateAsync(CreateValue("tag-1", "tag-2", null), errors); + await sut.ValidateAsync(CreateValue("tag-1", "tag-2", null), errors); - errors.Should().BeEquivalentTo( - new[] { "[1]: Not an allowed value." }); - } + errors.Should().BeEquivalentTo( + new[] { "[1]: Not an allowed value." }); + } - private static JsonValue CreateValue(params string?[]? ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids); - } + private static JsonValue CreateValue(params string?[]? ids) + { + return ids == null ? JsonValue.Null : JsonValue.Array(ids); + } - private static RootField<TagsFieldProperties> Field(TagsFieldProperties properties) - { - return Fields.Tags(1, "myTags", Partitioning.Invariant, properties); - } + private static RootField<TagsFieldProperties> Field(TagsFieldProperties properties) + { + return Fields.Tags(1, "myTags", Partitioning.Invariant, properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs index ecc4ef92ef..f1f02f58d9 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs @@ -14,108 +14,107 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public class UIFieldTests : IClassFixture<TranslationsFixture> { - public class UIFieldTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + + [Fact] + public void Should_instantiate_field() + { + var sut = Field(new UIFieldProperties()); + + Assert.Equal("myUI", sut.Name); + } + + [Fact] + public async Task Should_not_add_error_if_value_is_undefined() + { + var sut = Field(new UIFieldProperties()); + + await sut.ValidateAsync(Undefined.Value, errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_value_is_json_null() + { + var sut = Field(new UIFieldProperties()); + + await sut.ValidateAsync(JsonValue.Null, errors); + + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_valid() + { + var sut = Field(new UIFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(JsonValue.True, errors); + + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); + } + + [Fact] + public async Task Should_add_error_if_field_object_is_defined() + { + var schema = + new Schema("my-schema") + .AddUI(1, "myUI1", Partitioning.Invariant) + .AddUI(2, "myUI2", Partitioning.Invariant); + + var data = + new ContentData() + .AddField("myUI1", new ContentFieldData()) + .AddField("myUI2", new ContentFieldData() + .AddInvariant(JsonValue.Null)); + + var dataErrors = new List<ValidationError>(); + + await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema); + + dataErrors.Should().BeEquivalentTo( + new[] + { + new ValidationError("Value must not be defined.", "myUI1"), + new ValidationError("Value must not be defined.", "myUI2") + }); + } + + [Fact] + public async Task Should_add_error_if_array_item_field_is_defined() + { + var schema = + new Schema("my-schema") + .AddArray(1, "myArray", Partitioning.Invariant, array => array + .AddUI(101, "myUI")); + + var data = + new ContentData() + .AddField("myArray", new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject() + .Add("myUI", default)))); + + var dataErrors = new List<ValidationError>(); + + await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema); + + dataErrors.Should().BeEquivalentTo( + new[] + { + new ValidationError("Value must not be defined.", "myArray.iv[1].myUI") + }); + } + + private static NestedField<UIFieldProperties> Field(UIFieldProperties properties) { - private readonly List<string> errors = new List<string>(); - - [Fact] - public void Should_instantiate_field() - { - var sut = Field(new UIFieldProperties()); - - Assert.Equal("myUI", sut.Name); - } - - [Fact] - public async Task Should_not_add_error_if_value_is_undefined() - { - var sut = Field(new UIFieldProperties()); - - await sut.ValidateAsync(Undefined.Value, errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_value_is_json_null() - { - var sut = Field(new UIFieldProperties()); - - await sut.ValidateAsync(JsonValue.Null, errors); - - errors.Should().BeEquivalentTo( - new[] { "Value must not be defined." }); - } - - [Fact] - public async Task Should_add_error_if_value_is_valid() - { - var sut = Field(new UIFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(JsonValue.True, errors); - - errors.Should().BeEquivalentTo( - new[] { "Value must not be defined." }); - } - - [Fact] - public async Task Should_add_error_if_field_object_is_defined() - { - var schema = - new Schema("my-schema") - .AddUI(1, "myUI1", Partitioning.Invariant) - .AddUI(2, "myUI2", Partitioning.Invariant); - - var data = - new ContentData() - .AddField("myUI1", new ContentFieldData()) - .AddField("myUI2", new ContentFieldData() - .AddInvariant(JsonValue.Null)); - - var dataErrors = new List<ValidationError>(); - - await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema); - - dataErrors.Should().BeEquivalentTo( - new[] - { - new ValidationError("Value must not be defined.", "myUI1"), - new ValidationError("Value must not be defined.", "myUI2") - }); - } - - [Fact] - public async Task Should_add_error_if_array_item_field_is_defined() - { - var schema = - new Schema("my-schema") - .AddArray(1, "myArray", Partitioning.Invariant, array => array - .AddUI(101, "myUI")); - - var data = - new ContentData() - .AddField("myArray", new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("myUI", default)))); - - var dataErrors = new List<ValidationError>(); - - await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema); - - dataErrors.Should().BeEquivalentTo( - new[] - { - new ValidationError("Value must not be defined.", "myArray.iv[1].myUI") - }); - } - - private static NestedField<UIFieldProperties> Field(UIFieldProperties properties) - { - return new NestedField<UIFieldProperties>(1, "myUI", properties); - } + return new NestedField<UIFieldProperties>(1, "myUI", properties); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index 3d2a04b3d0..881f87b476 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -15,171 +15,170 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent -{ - public delegate ValidationContext ValidationUpdater(ValidationContext context); +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; + +public delegate ValidationContext ValidationUpdater(ValidationContext context); - public static class ValidationTestExtensions +public static class ValidationTestExtensions +{ + private static readonly NamedId<DomainId> AppId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private static readonly NamedId<DomainId> SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + + public static async ValueTask ValidateAsync(this IValidator validator, object? value, IList<string> errors, + Schema? schema = null, + ValidationMode mode = ValidationMode.Default, + ValidationUpdater? updater = null, + ValidationAction action = ValidationAction.Upsert, + ResolvedComponents? components = null, + DomainId? contentId = null) { - private static readonly NamedId<DomainId> AppId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private static readonly NamedId<DomainId> SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - - public static async ValueTask ValidateAsync(this IValidator validator, object? value, IList<string> errors, - Schema? schema = null, - ValidationMode mode = ValidationMode.Default, - ValidationUpdater? updater = null, - ValidationAction action = ValidationAction.Upsert, - ResolvedComponents? components = null, - DomainId? contentId = null) - { - var context = CreateContext(schema, mode, updater, action, components, contentId); + var context = CreateContext(schema, mode, updater, action, components, contentId); - validator.Validate(value, context); + validator.Validate(value, context); - await context.Root.CompleteAsync(); + await context.Root.CompleteAsync(); - AddErrors(context.Root, errors); - } + AddErrors(context.Root, errors); + } - public static async ValueTask ValidateAsync(this IField field, object? value, IList<string> errors, - Schema? schema = null, - ValidationMode mode = ValidationMode.Default, - ValidationUpdater? updater = null, - IValidatorsFactory? factory = null, - ValidationAction action = ValidationAction.Upsert, - ResolvedComponents? components = null, - DomainId? contentId = null) - { - var context = CreateContext(schema, mode, updater, action, components, contentId); + public static async ValueTask ValidateAsync(this IField field, object? value, IList<string> errors, + Schema? schema = null, + ValidationMode mode = ValidationMode.Default, + ValidationUpdater? updater = null, + IValidatorsFactory? factory = null, + ValidationAction action = ValidationAction.Upsert, + ResolvedComponents? components = null, + DomainId? contentId = null) + { + var context = CreateContext(schema, mode, updater, action, components, contentId); - new ValidatorBuilder(factory, context).ValueValidator(field) - .Validate(value, context); + new ValidatorBuilder(factory, context).ValueValidator(field) + .Validate(value, context); - await context.Root.CompleteAsync(); + await context.Root.CompleteAsync(); - AddErrors(context.Root, errors); - } + AddErrors(context.Root, errors); + } - public static async Task ValidatePartialAsync(this ContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors, - Schema? schema = null, - ValidationMode mode = ValidationMode.Default, - ValidationUpdater? updater = null, - IValidatorsFactory? factory = null, - ValidationAction action = ValidationAction.Upsert, - ResolvedComponents? components = null, - DomainId? contentId = null) - { - var context = CreateContext(schema, mode, updater, action, components, contentId); + public static async Task ValidatePartialAsync(this ContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors, + Schema? schema = null, + ValidationMode mode = ValidationMode.Default, + ValidationUpdater? updater = null, + IValidatorsFactory? factory = null, + ValidationAction action = ValidationAction.Upsert, + ResolvedComponents? components = null, + DomainId? contentId = null) + { + var context = CreateContext(schema, mode, updater, action, components, contentId); - await new ValidatorBuilder(factory, context).ContentValidator(partitionResolver) - .ValidateInputPartialAsync(data); + await new ValidatorBuilder(factory, context).ContentValidator(partitionResolver) + .ValidateInputPartialAsync(data); - errors.AddRange(context.Root.Errors); - } + errors.AddRange(context.Root.Errors); + } - public static async Task ValidateAsync(this ContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors, - Schema? schema = null, - ValidationMode mode = ValidationMode.Default, - ValidationUpdater? updater = null, - IValidatorsFactory? factory = null, - ValidationAction action = ValidationAction.Upsert, - ResolvedComponents? components = null, - DomainId? contentId = null) - { - var context = CreateContext(schema, mode, updater, action, components, contentId); + public static async Task ValidateAsync(this ContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors, + Schema? schema = null, + ValidationMode mode = ValidationMode.Default, + ValidationUpdater? updater = null, + IValidatorsFactory? factory = null, + ValidationAction action = ValidationAction.Upsert, + ResolvedComponents? components = null, + DomainId? contentId = null) + { + var context = CreateContext(schema, mode, updater, action, components, contentId); - await new ValidatorBuilder(factory, context).ContentValidator(partitionResolver) - .ValidateInputAsync(data); + await new ValidatorBuilder(factory, context).ContentValidator(partitionResolver) + .ValidateInputAsync(data); - errors.AddRange(context.Root.Errors); - } + errors.AddRange(context.Root.Errors); + } - private static void AddErrors(RootContext context, IList<string> errors) + private static void AddErrors(RootContext context, IList<string> errors) + { + foreach (var error in context.Errors) { - foreach (var error in context.Errors) + var propertyName = error.PropertyNames?.FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(propertyName)) { - var propertyName = error.PropertyNames?.FirstOrDefault(); - - if (string.IsNullOrWhiteSpace(propertyName)) - { - errors.Add(error.Message); - } - else - { - errors.Add($"{propertyName}: {error.Message}"); - } + errors.Add(error.Message); } - } - - private static ValidationContext CreateContext( - Schema? schema, - ValidationMode mode, - ValidationUpdater? updater, - ValidationAction action = ValidationAction.Upsert, - ResolvedComponents? components = null, - DomainId? contentId = null) - { - var rootContext = new RootContext( - TestUtils.DefaultSerializer, - AppId, - SchemaId, - schema ?? new Schema(SchemaId.Name), - contentId ?? DomainId.NewGuid(), - components ?? ResolvedComponents.Empty); - - var context = - new ValidationContext(rootContext) - .WithMode(mode) - .WithAction(action); - - if (updater != null) + else { - context = updater(context); + errors.Add($"{propertyName}: {error.Message}"); } + } + } - return context; + private static ValidationContext CreateContext( + Schema? schema, + ValidationMode mode, + ValidationUpdater? updater, + ValidationAction action = ValidationAction.Upsert, + ResolvedComponents? components = null, + DomainId? contentId = null) + { + var rootContext = new RootContext( + TestUtils.DefaultSerializer, + AppId, + SchemaId, + schema ?? new Schema(SchemaId.Name), + contentId ?? DomainId.NewGuid(), + components ?? ResolvedComponents.Empty); + + var context = + new ValidationContext(rootContext) + .WithMode(mode) + .WithAction(action); + + if (updater != null) + { + context = updater(context); } - private sealed class ValidatorBuilder + return context; + } + + private sealed class ValidatorBuilder + { + private static readonly IValidatorsFactory Default = new DefaultValidatorsFactory(); + private readonly IValidatorsFactory? validatorFactory; + private readonly ValidationContext validationContext; + + public ValidatorBuilder(IValidatorsFactory? validatorFactory, ValidationContext validationContext) { - private static readonly IValidatorsFactory Default = new DefaultValidatorsFactory(); - private readonly IValidatorsFactory? validatorFactory; - private readonly ValidationContext validationContext; + this.validatorFactory = validatorFactory; + this.validationContext = validationContext; + } - public ValidatorBuilder(IValidatorsFactory? validatorFactory, ValidationContext validationContext) - { - this.validatorFactory = validatorFactory; - this.validationContext = validationContext; - } + public ContentValidator ContentValidator(PartitionResolver partitionResolver) + { + return new ContentValidator(partitionResolver, validationContext, CreateFactories()); + } - public ContentValidator ContentValidator(PartitionResolver partitionResolver) - { - return new ContentValidator(partitionResolver, validationContext, CreateFactories()); - } + private IValidator CreateValueValidator(IField field) + { + return new FieldValidator(new AggregateValidator(CreateValueValidators(field)), field); + } - private IValidator CreateValueValidator(IField field) - { - return new FieldValidator(new AggregateValidator(CreateValueValidators(field)), field); - } + public IValidator ValueValidator(IField field) + { + return CreateValueValidator(field); + } - public IValidator ValueValidator(IField field) - { - return CreateValueValidator(field); - } + private IEnumerable<IValidator> CreateValueValidators(IField field) + { + return CreateFactories().SelectMany(x => x.CreateValueValidators(validationContext, field, CreateValueValidator)); + } - private IEnumerable<IValidator> CreateValueValidators(IField field) - { - return CreateFactories().SelectMany(x => x.CreateValueValidators(validationContext, field, CreateValueValidator)); - } + private IEnumerable<IValidatorsFactory> CreateFactories() + { + yield return Default; - private IEnumerable<IValidatorsFactory> CreateFactories() + if (validatorFactory != null) { - yield return Default; - - if (validatorFactory != null) - { - yield return validatorFactory; - } + yield return validatorFactory; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs index 2e2fbb9f25..bf6530c983 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs @@ -10,41 +10,40 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class AllowedValuesValidatorTests : IClassFixture<TranslationsFixture> { - public class AllowedValuesValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public async Task Should_not_add_error_if_value_null() - { - var sut = new AllowedValuesValidator<int>(100, 200); + [Fact] + public async Task Should_not_add_error_if_value_null() + { + var sut = new AllowedValuesValidator<int>(100, 200); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_value_is_allowed() - { - var sut = new AllowedValuesValidator<int>(100, 200); + [Fact] + public async Task Should_not_add_error_if_value_is_allowed() + { + var sut = new AllowedValuesValidator<int>(100, 200); - await sut.ValidateAsync(100, errors); + await sut.ValidateAsync(100, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_value_is_not_allowed() - { - var sut = new AllowedValuesValidator<int>(100, 200); + [Fact] + public async Task Should_add_error_if_value_is_not_allowed() + { + var sut = new AllowedValuesValidator<int>(100, 200); - await sut.ValidateAsync(50, errors); + await sut.ValidateAsync(50, errors); - errors.Should().BeEquivalentTo( - new[] { "Not an allowed value." }); - } + errors.Should().BeEquivalentTo( + new[] { "Not an allowed value." }); } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs index 32c506e817..f6434fe1f6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs @@ -15,256 +15,255 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class AssetsValidatorTests : IClassFixture<TranslationsFixture> { - public class AssetsValidatorTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + private static readonly IAssetInfo Document = TestAssets.Document(DomainId.NewGuid()); + private static readonly IAssetInfo Image1 = TestAssets.Image(DomainId.NewGuid()); + private static readonly IAssetInfo Image2 = TestAssets.Image(DomainId.NewGuid()); + private static readonly IAssetInfo ImageSvg = TestAssets.Svg(DomainId.NewGuid()); + private static readonly IAssetInfo Video = TestAssets.Video(DomainId.NewGuid()); + + public static IEnumerable<object[]> AssetsWithDimensions() { - private readonly List<string> errors = new List<string>(); - private static readonly IAssetInfo Document = TestAssets.Document(DomainId.NewGuid()); - private static readonly IAssetInfo Image1 = TestAssets.Image(DomainId.NewGuid()); - private static readonly IAssetInfo Image2 = TestAssets.Image(DomainId.NewGuid()); - private static readonly IAssetInfo ImageSvg = TestAssets.Svg(DomainId.NewGuid()); - private static readonly IAssetInfo Video = TestAssets.Video(DomainId.NewGuid()); - - public static IEnumerable<object[]> AssetsWithDimensions() - { - yield return new object[] { Image1.AssetId }; - yield return new object[] { Video.AssetId }; - } + yield return new object[] { Image1.AssetId }; + yield return new object[] { Video.AssetId }; + } - [Fact] - public async Task Should_not_add_error_if_assets_are_valid() - { - var sut = Validator(new AssetsFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_assets_are_valid() + { + var sut = Validator(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(Document.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_assets_are_null_but_not_required() - { - var sut = Validator(new AssetsFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_assets_are_null_but_not_required() + { + var sut = Validator(new AssetsFieldProperties()); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_assets_are_empty_but_not_required() - { - var sut = Validator(new AssetsFieldProperties()); + [Fact] + public async Task Should_not_add_error_if_assets_are_empty_but_not_required() + { + var sut = Validator(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_duplicates_are_allowed() - { - var sut = Validator(new AssetsFieldProperties { AllowDuplicates = true }); + [Fact] + public async Task Should_not_add_error_if_duplicates_are_allowed() + { + var sut = Validator(new AssetsFieldProperties { AllowDuplicates = true }); - await sut.ValidateAsync(CreateValue(Image1.AssetId, Image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Image1.AssetId, Image1.AssetId), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_asset_is_an_image() - { - var sut = Validator(new AssetsFieldProperties { ExpectedType = AssetType.Image }); + [Fact] + public async Task Should_not_add_error_if_asset_is_an_image() + { + var sut = Validator(new AssetsFieldProperties { ExpectedType = AssetType.Image }); - await sut.ValidateAsync(CreateValue(ImageSvg.AssetId, Image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(ImageSvg.AssetId, Image1.AssetId), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_references_are_required() - { - var sut = Validator(new AssetsFieldProperties { IsRequired = true }); + [Fact] + public async Task Should_add_error_if_references_are_required() + { + var sut = Validator(new AssetsFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_asset_are_not_valid() - { - var assetId = DomainId.NewGuid(); + [Fact] + public async Task Should_add_error_if_asset_are_not_valid() + { + var assetId = DomainId.NewGuid(); - var sut = Validator(new AssetsFieldProperties()); + var sut = Validator(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(assetId), errors); + await sut.ValidateAsync(CreateValue(assetId), errors); - errors.Should().BeEquivalentTo( - new[] { $"[1]: Id {assetId} not found." }); - } + errors.Should().BeEquivalentTo( + new[] { $"[1]: Id {assetId} not found." }); + } - [Fact] - public async Task Should_add_error_if_document_is_too_small() - { - var sut = Validator(new AssetsFieldProperties { MinSize = 5 * 1024 }); + [Fact] + public async Task Should_add_error_if_document_is_too_small() + { + var sut = Validator(new AssetsFieldProperties { MinSize = 5 * 1024 }); - await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); - errors.Should().BeEquivalentTo( - new[] { "[1]: Size of 4 kB must be greater than 5 kB." }); - } + errors.Should().BeEquivalentTo( + new[] { "[1]: Size of 4 kB must be greater than 5 kB." }); + } - [Fact] - public async Task Should_add_error_if_document_is_too_big() - { - var sut = Validator(new AssetsFieldProperties { MaxSize = 5 * 1024 }); + [Fact] + public async Task Should_add_error_if_document_is_too_big() + { + var sut = Validator(new AssetsFieldProperties { MaxSize = 5 * 1024 }); - await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); - errors.Should().BeEquivalentTo( - new[] { "[2]: Size of 8 kB must be less than 5 kB." }); - } + errors.Should().BeEquivalentTo( + new[] { "[2]: Size of 8 kB must be less than 5 kB." }); + } - [Fact] - public async Task Should_add_error_if_document_is_not_an_image() - { - var sut = Validator(new AssetsFieldProperties { ExpectedType = AssetType.Image }); + [Fact] + public async Task Should_add_error_if_document_is_not_an_image() + { + var sut = Validator(new AssetsFieldProperties { ExpectedType = AssetType.Image }); - await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); - errors.Should().BeEquivalentTo( - new[] { "[1]: Not of expected type: Image." }); - } + errors.Should().BeEquivalentTo( + new[] { "[1]: Not of expected type: Image." }); + } - [Theory] - [MemberData(nameof(AssetsWithDimensions))] - public async Task Should_add_error_if_asset_width_is_too_small(DomainId videoOrImageId) - { - var sut = Validator(new AssetsFieldProperties { MinWidth = 1000 }); + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_width_is_too_small(DomainId videoOrImageId) + { + var sut = Validator(new AssetsFieldProperties { MinWidth = 1000 }); - await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); - errors.Should().BeEquivalentTo( - new[] { "[2]: Width 800px must be greater than 1000px." }); - } + errors.Should().BeEquivalentTo( + new[] { "[2]: Width 800px must be greater than 1000px." }); + } - [Theory] - [MemberData(nameof(AssetsWithDimensions))] - public async Task Should_add_error_if_asset_width_is_too_big(DomainId videoOrImageId) - { - var sut = Validator(new AssetsFieldProperties { MaxWidth = 700 }); + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_width_is_too_big(DomainId videoOrImageId) + { + var sut = Validator(new AssetsFieldProperties { MaxWidth = 700 }); - await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); - errors.Should().BeEquivalentTo( - new[] { "[2]: Width 800px must be less than 700px." }); - } + errors.Should().BeEquivalentTo( + new[] { "[2]: Width 800px must be less than 700px." }); + } - [Theory] - [MemberData(nameof(AssetsWithDimensions))] - public async Task Should_add_error_if_asset_height_is_too_small(DomainId videoOrImageId) - { - var sut = Validator(new AssetsFieldProperties { MinHeight = 800 }); + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_height_is_too_small(DomainId videoOrImageId) + { + var sut = Validator(new AssetsFieldProperties { MinHeight = 800 }); - await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); - errors.Should().BeEquivalentTo( - new[] { "[2]: Height 600px must be greater than 800px." }); - } + errors.Should().BeEquivalentTo( + new[] { "[2]: Height 600px must be greater than 800px." }); + } - [Theory] - [MemberData(nameof(AssetsWithDimensions))] - public async Task Should_add_error_if_asset_height_is_too_big(DomainId videoOrImageId) - { - var sut = Validator(new AssetsFieldProperties { MaxHeight = 500 }); + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_height_is_too_big(DomainId videoOrImageId) + { + var sut = Validator(new AssetsFieldProperties { MaxHeight = 500 }); - await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); - errors.Should().BeEquivalentTo( - new[] { "[2]: Height 600px must be less than 500px." }); - } + errors.Should().BeEquivalentTo( + new[] { "[2]: Height 600px must be less than 500px." }); + } - [Theory] - [MemberData(nameof(AssetsWithDimensions))] - public async Task Should_add_error_if_asset_has_invalid_aspect_ratio(DomainId videoOrImageId) - { - var sut = Validator(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_has_invalid_aspect_ratio(DomainId videoOrImageId) + { + var sut = Validator(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); - await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); - errors.Should().BeEquivalentTo( - new[] { "[2]: Must have aspect ratio 1:1." }); - } + errors.Should().BeEquivalentTo( + new[] { "[2]: Must have aspect ratio 1:1." }); + } - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Validator(new AssetsFieldProperties { MinItems = 2 }); + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Validator(new AssetsFieldProperties { MinItems = 2 }); - await sut.ValidateAsync(CreateValue(Image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Image1.AssetId), errors); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 2 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Validator(new AssetsFieldProperties { MaxItems = 1 }); + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Validator(new AssetsFieldProperties { MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(Image1.AssetId, Image2.AssetId), errors); + await sut.ValidateAsync(CreateValue(Image1.AssetId, Image2.AssetId), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_reference_contains_duplicate_values() - { - var sut = Validator(new AssetsFieldProperties()); + [Fact] + public async Task Should_add_error_if_reference_contains_duplicate_values() + { + var sut = Validator(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(Image1.AssetId, Image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Image1.AssetId, Image1.AssetId), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } - [Fact] - public async Task Should_add_error_if_image_has_invalid_extension() - { - var sut = Validator(new AssetsFieldProperties { AllowedExtensions = ReadonlyList.Create("mp4") }); + [Fact] + public async Task Should_add_error_if_image_has_invalid_extension() + { + var sut = Validator(new AssetsFieldProperties { AllowedExtensions = ReadonlyList.Create("mp4") }); - await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); - errors.Should().BeEquivalentTo( - new[] - { - "[1]: Must be an allowed extension.", - "[2]: Must be an allowed extension." - }); - } + errors.Should().BeEquivalentTo( + new[] + { + "[1]: Must be an allowed extension.", + "[2]: Must be an allowed extension." + }); + } - private static object CreateValue(params DomainId[] ids) - { - return ids.ToList(); - } + private static object CreateValue(params DomainId[] ids) + { + return ids.ToList(); + } - private static IValidator Validator(AssetsFieldProperties properties) - { - return new AssetsValidator(properties.IsRequired, properties, FoundAssets()); - } + private static IValidator Validator(AssetsFieldProperties properties) + { + return new AssetsValidator(properties.IsRequired, properties, FoundAssets()); + } - private static CheckAssets FoundAssets() + private static CheckAssets FoundAssets() + { + return ids => { - return ids => - { - var actual = new List<IAssetInfo> { Document, Image1, Image2, ImageSvg, Video }; + var actual = new List<IAssetInfo> { Document, Image1, Image2, ImageSvg, Video }; - return Task.FromResult<IReadOnlyList<IAssetInfo>>(actual); - }; - } + return Task.FromResult<IReadOnlyList<IAssetInfo>>(actual); + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs index 53e7027645..07b8244394 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs @@ -10,45 +10,44 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class CollectionItemValidatorTests : IClassFixture<TranslationsFixture> { - public class CollectionItemValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public async Task Should_not_add_error_if_value_is_wrong_type() - { - var sut = new CollectionItemValidator(new RangeValidator<int>(2, 4)); + [Fact] + public async Task Should_not_add_error_if_value_is_wrong_type() + { + var sut = new CollectionItemValidator(new RangeValidator<int>(2, 4)); - await sut.ValidateAsync(true, errors); + await sut.ValidateAsync(true, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_all_values_are_valid() - { - var sut = new CollectionItemValidator(new RangeValidator<int>(2, 4)); + [Fact] + public async Task Should_not_add_error_if_all_values_are_valid() + { + var sut = new CollectionItemValidator(new RangeValidator<int>(2, 4)); - await sut.ValidateAsync(new List<int> { 2, 3, 4 }, errors); + await sut.ValidateAsync(new List<int> { 2, 3, 4 }, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_at_least_one_item_is_not_valid() - { - var sut = new CollectionItemValidator(new RangeValidator<int>(2, 4)); + [Fact] + public async Task Should_add_error_if_at_least_one_item_is_not_valid() + { + var sut = new CollectionItemValidator(new RangeValidator<int>(2, 4)); - await sut.ValidateAsync(new List<int> { 2, 1, 4, 5 }, errors); + await sut.ValidateAsync(new List<int> { 2, 1, 4, 5 }, errors); - errors.Should().BeEquivalentTo( - new[] - { - "[2]: Must be between 2 and 4.", - "[4]: Must be between 2 and 4." - }); - } + errors.Should().BeEquivalentTo( + new[] + { + "[2]: Must be between 2 and 4.", + "[4]: Must be between 2 and 4." + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs index e6f514ca8b..06be77222c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs @@ -10,92 +10,91 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class CollectionValidatorTests : IClassFixture<TranslationsFixture> { - public class CollectionValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Theory] - [InlineData(20, 10)] - public void Should_throw_error_if_min_greater_than_max(int? min, int? max) - { - Assert.Throws<ArgumentException>(() => new CollectionValidator(true, min, max)); - } + [Theory] + [InlineData(20, 10)] + public void Should_throw_error_if_min_greater_than_max(int? min, int? max) + { + Assert.Throws<ArgumentException>(() => new CollectionValidator(true, min, max)); + } - [Fact] - public async Task Should_not_add_error_if_value_is_valid() - { - var sut = new CollectionValidator(true, 1, 3); + [Fact] + public async Task Should_not_add_error_if_value_is_valid() + { + var sut = new CollectionValidator(true, 1, 3); - await sut.ValidateAsync(new List<int> { 1, 2 }, errors); + await sut.ValidateAsync(new List<int> { 1, 2 }, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_optional() - { - var sut = new CollectionValidator(true, 1, 3); + [Fact] + public async Task Should_not_add_error_if_optional() + { + var sut = new CollectionValidator(true, 1, 3); - await sut.ValidateAsync(null, errors, updater: c => c.Optional(true)); + await sut.ValidateAsync(null, errors, updater: c => c.Optional(true)); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_value_is_null() - { - var sut = new CollectionValidator(true, 1, 3); + [Fact] + public async Task Should_add_error_if_value_is_null() + { + var sut = new CollectionValidator(true, 1, 3); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_collection_has_not_exact_number_of_items() - { - var sut = new CollectionValidator(true, 2, 2); + [Fact] + public async Task Should_add_error_if_collection_has_not_exact_number_of_items() + { + var sut = new CollectionValidator(true, 2, 2); - await sut.ValidateAsync(new List<int> { 1 }, errors); + await sut.ValidateAsync(new List<int> { 1 }, errors); - errors.Should().BeEquivalentTo( - new[] { "Must have exactly 2 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have exactly 2 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_collection_has_too_few_items() - { - var sut = new CollectionValidator(true, 2, null); + [Fact] + public async Task Should_add_error_if_collection_has_too_few_items() + { + var sut = new CollectionValidator(true, 2, null); - await sut.ValidateAsync(new List<int> { 1 }, errors); + await sut.ValidateAsync(new List<int> { 1 }, errors); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 2 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_collection_has_too_many_items() - { - var sut = new CollectionValidator(true, null, 3); + [Fact] + public async Task Should_add_error_if_collection_has_too_many_items() + { + var sut = new CollectionValidator(true, null, 3); - await sut.ValidateAsync(new List<int> { 1, 2, 3, 4 }, errors); + await sut.ValidateAsync(new List<int> { 1, 2, 3, 4 }, errors); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 3 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 3 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_collection_count_is_not_in_range() - { - var sut = new CollectionValidator(true, 2, 5); + [Fact] + public async Task Should_add_error_if_collection_count_is_not_in_range() + { + var sut = new CollectionValidator(true, 2, 5); - await sut.ValidateAsync(new List<int> { 1 }, errors); + await sut.ValidateAsync(new List<int> { 1 }, errors); - errors.Should().BeEquivalentTo( - new[] { "Must have between 2 and 5 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have between 2 and 5 item(s)." }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs index cb0e589ac7..aab779619e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs @@ -14,50 +14,49 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class ComponentValidatorTests : IClassFixture<TranslationsFixture> { - public class ComponentValidatorTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + + [Fact] + public async Task Should_create_validator_from_component_and_invoke() { - private readonly List<string> errors = new List<string>(); + var validator = A.Fake<IValidator>(); - [Fact] - public async Task Should_create_validator_from_component_and_invoke() - { - var validator = A.Fake<IValidator>(); + var componentData = new JsonObject(); + var componentObject = new Component("type", componentData, new Schema("my-schema")); - var componentData = new JsonObject(); - var componentObject = new Component("type", componentData, new Schema("my-schema")); + var isFactoryCalled = false; - var isFactoryCalled = false; + var sut = new ComponentValidator(_ => + { + isFactoryCalled = true; + return validator; + }); - var sut = new ComponentValidator(_ => - { - isFactoryCalled = true; - return validator; - }); + await sut.ValidateAsync(componentObject, errors); - await sut.ValidateAsync(componentObject, errors); + Assert.True(isFactoryCalled); - Assert.True(isFactoryCalled); + A.CallTo(() => validator.Validate(componentData, A<ValidationContext>._)) + .MustHaveHappened(); + } - A.CallTo(() => validator.Validate(componentData, A<ValidationContext>._)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_do_nothing_if_value_is_not_a_component() + { + var isFactoryCalled = false; - [Fact] - public async Task Should_do_nothing_if_value_is_not_a_component() + var sut = new ComponentValidator(_ => { - var isFactoryCalled = false; - - var sut = new ComponentValidator(_ => - { - isFactoryCalled = true; - return null!; - }); + isFactoryCalled = true; + return null!; + }); - await sut.ValidateAsync(1, errors); + await sut.ValidateAsync(1, errors); - Assert.False(isFactoryCalled); - } + Assert.False(isFactoryCalled); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs index 7894b87ab2..1a1c84c1f2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs @@ -12,42 +12,41 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class NoValueValidatorTests : IClassFixture<TranslationsFixture> { - public class NoValueValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public async Task Should_not_add_error_if_value_is_undefined() - { - var sut = NoValueValidator.Instance; + [Fact] + public async Task Should_not_add_error_if_value_is_undefined() + { + var sut = NoValueValidator.Instance; - await sut.ValidateAsync(Undefined.Value, errors); + await sut.ValidateAsync(Undefined.Value, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_value_is_json_null() - { - var sut = NoValueValidator.Instance; + [Fact] + public async Task Should_add_error_if_value_is_json_null() + { + var sut = NoValueValidator.Instance; - await sut.ValidateAsync(JsonValue.Null, errors); + await sut.ValidateAsync(JsonValue.Null, errors); - errors.Should().BeEquivalentTo( - new[] { "Value must not be defined." }); - } + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); + } - [Fact] - public async Task Should_add_error_if_value_is_valid() - { - var sut = NoValueValidator.Instance; + [Fact] + public async Task Should_add_error_if_value_is_valid() + { + var sut = NoValueValidator.Instance; - await sut.ValidateAsync(JsonValue.True, errors); + await sut.ValidateAsync(JsonValue.True, errors); - errors.Should().BeEquivalentTo( - new[] { "Value must not be defined." }); - } + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs index 85f510236a..88b5e00957 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs @@ -10,73 +10,72 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class PatternValidatorTests : IClassFixture<TranslationsFixture> { - public class PatternValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public async Task Should_not_add_error_if_value_is_valid() - { - var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); + [Fact] + public async Task Should_not_add_error_if_value_is_valid() + { + var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync("abc:12", errors); + await sut.ValidateAsync("abc:12", errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_value_is_null() - { - var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); + [Fact] + public async Task Should_not_add_error_if_value_is_null() + { + var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_value_is_empty() - { - var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); + [Fact] + public async Task Should_not_add_error_if_value_is_empty() + { + var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync(string.Empty, errors); + await sut.ValidateAsync(string.Empty, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_with_default_message_if_value_is_not_valid() - { - var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); + [Fact] + public async Task Should_add_error_with_default_message_if_value_is_not_valid() + { + var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync("foo", errors); + await sut.ValidateAsync("foo", errors); - errors.Should().BeEquivalentTo( - new[] { "Must follow the pattern." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must follow the pattern." }); + } - [Fact] - public async Task Should_add_error_with_custom_message_if_value_is_not_valid() - { - var sut = new PatternValidator("[a-z]{3}:[0-9]{2}", "Custom Error Message."); + [Fact] + public async Task Should_add_error_with_custom_message_if_value_is_not_valid() + { + var sut = new PatternValidator("[a-z]{3}:[0-9]{2}", "Custom Error Message."); - await sut.ValidateAsync("foo", errors); + await sut.ValidateAsync("foo", errors); - errors.Should().BeEquivalentTo( - new[] { "Custom Error Message." }); - } + errors.Should().BeEquivalentTo( + new[] { "Custom Error Message." }); + } - [Fact] - public async Task Should_add_timeout_error_if_regex_is_too_slow() - { - var sut = new PatternValidator(@"^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$", capture: true); + [Fact] + public async Task Should_add_timeout_error_if_regex_is_too_slow() + { + var sut = new PatternValidator(@"^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$", capture: true); - await sut.ValidateAsync("https://archiverbx.blob.core.windows.net/static/C:/Users/USR/Documents/Projects/PROJ/static/images/full/1234567890.jpg", errors); + await sut.ValidateAsync("https://archiverbx.blob.core.windows.net/static/C:/Users/USR/Documents/Projects/PROJ/static/images/full/1234567890.jpg", errors); - errors.Should().BeEquivalentTo( - new[] { "Regex is too slow." }); - } + errors.Should().BeEquivalentTo( + new[] { "Regex is too slow." }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs index 63fdf8adb8..3c16b226d8 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs @@ -10,74 +10,73 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class RangeValidatorTests : IClassFixture<TranslationsFixture> { - public class RangeValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public async Task Should_not_add_error_if_value_is_null() - { - var sut = new RangeValidator<int>(100, 200); + [Fact] + public async Task Should_not_add_error_if_value_is_null() + { + var sut = new RangeValidator<int>(100, 200); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Theory] - [InlineData(null, null)] - [InlineData(1000, null)] - [InlineData(1000, 2000)] - [InlineData(null, 2000)] - public async Task Should_not_add_error_if_value_is_within_range(int? min, int? max) - { - var sut = new RangeValidator<int>(min, max); + [Theory] + [InlineData(null, null)] + [InlineData(1000, null)] + [InlineData(1000, 2000)] + [InlineData(null, 2000)] + public async Task Should_not_add_error_if_value_is_within_range(int? min, int? max) + { + var sut = new RangeValidator<int>(min, max); - await sut.ValidateAsync(1500, errors); + await sut.ValidateAsync(1500, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Theory] - [InlineData(20, 10)] - public void Should_throw_error_if_min_greater_than_max(int? min, int? max) - { - Assert.Throws<ArgumentException>(() => new RangeValidator<int>(min, max)); - } + [Theory] + [InlineData(20, 10)] + public void Should_throw_error_if_min_greater_than_max(int? min, int? max) + { + Assert.Throws<ArgumentException>(() => new RangeValidator<int>(min, max)); + } - [Fact] - public async Task Should_add_error_if_value_is_not_equal_to_min_and_max() - { - var sut = new RangeValidator<int>(2000, 2000); + [Fact] + public async Task Should_add_error_if_value_is_not_equal_to_min_and_max() + { + var sut = new RangeValidator<int>(2000, 2000); - await sut.ValidateAsync(1500, errors); + await sut.ValidateAsync(1500, errors); - errors.Should().BeEquivalentTo( - new[] { "Must be exactly 2000." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must be exactly 2000." }); + } - [Fact] - public async Task Should_add_error_if_value_is_smaller_than_min() - { - var sut = new RangeValidator<int>(2000, null); + [Fact] + public async Task Should_add_error_if_value_is_smaller_than_min() + { + var sut = new RangeValidator<int>(2000, null); - await sut.ValidateAsync(1500, errors); + await sut.ValidateAsync(1500, errors); - errors.Should().BeEquivalentTo( - new[] { "Must be greater or equal to 2000." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must be greater or equal to 2000." }); + } - [Fact] - public async Task Should_add_error_if_value_is_greater_than_max() - { - var sut = new RangeValidator<int>(null, 1000); + [Fact] + public async Task Should_add_error_if_value_is_greater_than_max() + { + var sut = new RangeValidator<int>(null, 1000); - await sut.ValidateAsync(1500, errors); + await sut.ValidateAsync(1500, errors); - errors.Should().BeEquivalentTo( - new[] { "Must be less or equal to 1000." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must be less or equal to 1000." }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs index 93661fc9fc..9814dfbe2d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs @@ -14,215 +14,214 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class ReferencesValidatorTests : IClassFixture<TranslationsFixture> { - public class ReferencesValidatorTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + private readonly DomainId schemaId1 = DomainId.NewGuid(); + private readonly DomainId schemaId2 = DomainId.NewGuid(); + private readonly DomainId ref1 = DomainId.NewGuid(); + private readonly DomainId ref2 = DomainId.NewGuid(); + + [Fact] + public async Task Should_not_add_error_if_reference_invalid_but_publishing() { - private readonly List<string> errors = new List<string>(); - private readonly DomainId schemaId1 = DomainId.NewGuid(); - private readonly DomainId schemaId2 = DomainId.NewGuid(); - private readonly DomainId ref1 = DomainId.NewGuid(); - private readonly DomainId ref2 = DomainId.NewGuid(); - - [Fact] - public async Task Should_not_add_error_if_reference_invalid_but_publishing() - { - var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; + var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; - var sut = Validator(properties, schemaId2, (ref2, Status.Published)); + var sut = Validator(properties, schemaId2, (ref2, Status.Published)); - await sut.ValidateAsync(CreateValue(ref2), errors, action: ValidationAction.Publish); + await sut.ValidateAsync(CreateValue(ref2), errors, action: ValidationAction.Publish); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_schemas_not_defined() - { - var properties = new ReferencesFieldProperties(); + [Fact] + public async Task Should_not_add_error_if_schemas_not_defined() + { + var properties = new ReferencesFieldProperties(); - var sut = Validator(properties, schemaId2, (ref2, Status.Published)); + var sut = Validator(properties, schemaId2, (ref2, Status.Published)); - await sut.ValidateAsync(CreateValue(ref2), errors); + await sut.ValidateAsync(CreateValue(ref2), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_schema_is_valid() - { - var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; + [Fact] + public async Task Should_not_add_error_if_schema_is_valid() + { + var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; - var sut = Validator(properties, schemaId1, (ref2, Status.Published)); + var sut = Validator(properties, schemaId1, (ref2, Status.Published)); - await sut.ValidateAsync(CreateValue(ref2), errors); + await sut.ValidateAsync(CreateValue(ref2), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_references_are_null_but_not_required() - { - var properties = new ReferencesFieldProperties(); + [Fact] + public async Task Should_not_add_error_if_references_are_null_but_not_required() + { + var properties = new ReferencesFieldProperties(); - var sut = Validator(properties); + var sut = Validator(properties); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_references_are_empty_but_not_required() - { - var properties = new ReferencesFieldProperties(); + [Fact] + public async Task Should_not_add_error_if_references_are_empty_but_not_required() + { + var properties = new ReferencesFieldProperties(); - var sut = Validator(properties); + var sut = Validator(properties); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_duplicates_are_allowed() - { - var properties = new ReferencesFieldProperties { AllowDuplicates = true }; + [Fact] + public async Task Should_not_add_error_if_duplicates_are_allowed() + { + var properties = new ReferencesFieldProperties { AllowDuplicates = true }; - var sut = Validator(properties, schemaId1, (ref1, Status.Published)); + var sut = Validator(properties, schemaId1, (ref1, Status.Published)); - await sut.ValidateAsync(CreateValue(ref1, ref1), errors); + await sut.ValidateAsync(CreateValue(ref1, ref1), errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_references_are_required() - { - var properties = new ReferencesFieldProperties { IsRequired = true }; + [Fact] + public async Task Should_add_error_if_references_are_required() + { + var properties = new ReferencesFieldProperties { IsRequired = true }; - var sut = Validator(properties, schemaId1); + var sut = Validator(properties, schemaId1); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_references_are_published_required() - { - var properties = new ReferencesFieldProperties { MustBePublished = true, IsRequired = true }; + [Fact] + public async Task Should_add_error_if_references_are_published_required() + { + var properties = new ReferencesFieldProperties { MustBePublished = true, IsRequired = true }; - var sut = Validator(properties, schemaId1, (ref1, Status.Published)); + var sut = Validator(properties, schemaId1, (ref1, Status.Published)); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_references_are_not_valid() - { - var properties = new ReferencesFieldProperties(); + [Fact] + public async Task Should_add_error_if_references_are_not_valid() + { + var properties = new ReferencesFieldProperties(); - var sut = Validator(properties); + var sut = Validator(properties); - await sut.ValidateAsync(CreateValue(ref1), errors); + await sut.ValidateAsync(CreateValue(ref1), errors); - errors.Should().BeEquivalentTo( - new[] { $"[1]: Reference '{ref1}' not found." }); - } + errors.Should().BeEquivalentTo( + new[] { $"[1]: Reference '{ref1}' not found." }); + } - [Fact] - public async Task Should_add_error_if_reference_schema_is_not_valid() - { - var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; + [Fact] + public async Task Should_add_error_if_reference_schema_is_not_valid() + { + var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; - var sut = Validator(properties, schemaId2, (ref2, Status.Draft)); + var sut = Validator(properties, schemaId2, (ref2, Status.Draft)); - await sut.ValidateAsync(CreateValue(ref2), errors); + await sut.ValidateAsync(CreateValue(ref2), errors); - errors.Should().BeEquivalentTo( - new[] { $"[1]: Reference '{ref2}' has invalid schema." }); - } + errors.Should().BeEquivalentTo( + new[] { $"[1]: Reference '{ref2}' has invalid schema." }); + } - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var properties = new ReferencesFieldProperties { MinItems = 2 }; + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var properties = new ReferencesFieldProperties { MinItems = 2 }; - var sut = Validator(properties, schemaId2, (ref2, Status.Draft)); + var sut = Validator(properties, schemaId2, (ref2, Status.Draft)); - await sut.ValidateAsync(CreateValue(ref2), errors); + await sut.ValidateAsync(CreateValue(ref2), errors); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 2 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_has_not_enough_published_items() - { - var properties = new ReferencesFieldProperties { MinItems = 2, MustBePublished = true }; + [Fact] + public async Task Should_add_error_if_value_has_not_enough_published_items() + { + var properties = new ReferencesFieldProperties { MinItems = 2, MustBePublished = true }; - var sut = Validator(properties, schemaId1, (ref1, Status.Published), (ref2, Status.Draft)); + var sut = Validator(properties, schemaId1, (ref1, Status.Published), (ref2, Status.Draft)); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors); - errors.Should().BeEquivalentTo( - new[] { "Must have at least 2 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var properties = new ReferencesFieldProperties { MaxItems = 1 }; + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var properties = new ReferencesFieldProperties { MaxItems = 1 }; - var sut = Validator(properties, schemaId1, (ref1, Status.Published), (ref2, Status.Draft)); + var sut = Validator(properties, schemaId1, (ref1, Status.Published), (ref2, Status.Draft)); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } - [Fact] - public async Task Should_add_error_if_reference_contains_duplicate_values() - { - var properties = new ReferencesFieldProperties(); + [Fact] + public async Task Should_add_error_if_reference_contains_duplicate_values() + { + var properties = new ReferencesFieldProperties(); - var sut = Validator(properties, schemaId1, (ref1, Status.Published)); + var sut = Validator(properties, schemaId1, (ref1, Status.Published)); - await sut.ValidateAsync(CreateValue(ref1, ref1), errors); + await sut.ValidateAsync(CreateValue(ref1, ref1), errors); - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } - private static List<DomainId> CreateValue(params DomainId[] ids) - { - return ids.ToList(); - } + private static List<DomainId> CreateValue(params DomainId[] ids) + { + return ids.ToList(); + } - private IValidator Validator(ReferencesFieldProperties properties) - { - return new ReferencesValidator(properties.IsRequired, properties, FoundReferences(schemaId1)); - } + private IValidator Validator(ReferencesFieldProperties properties) + { + return new ReferencesValidator(properties.IsRequired, properties, FoundReferences(schemaId1)); + } - private static IValidator Validator(ReferencesFieldProperties properties, DomainId schemaId, params (DomainId Id, Status Status)[] references) - { - return new ReferencesValidator(properties.IsRequired, properties, FoundReferences(schemaId, references)); - } + private static IValidator Validator(ReferencesFieldProperties properties, DomainId schemaId, params (DomainId Id, Status Status)[] references) + { + return new ReferencesValidator(properties.IsRequired, properties, FoundReferences(schemaId, references)); + } - private static CheckContentsByIds FoundReferences(DomainId schemaId, params (DomainId Id, Status Status)[] references) + private static CheckContentsByIds FoundReferences(DomainId schemaId, params (DomainId Id, Status Status)[] references) + { + return x => { - return x => - { - var actual = references.Select(x => new ContentIdStatus(schemaId, x.Id, x.Status)).ToList(); + var actual = references.Select(x => new ContentIdStatus(schemaId, x.Id, x.Status)).ToList(); - return Task.FromResult<IReadOnlyList<ContentIdStatus>>(actual); - }; - } + return Task.FromResult<IReadOnlyList<ContentIdStatus>>(actual); + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs index 96e6ae77bf..562102a0ee 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs @@ -10,66 +10,65 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class RequiredStringValidatorTests : IClassFixture<TranslationsFixture> { - public class RequiredStringValidatorTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + + [Theory] + [InlineData("MyString")] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public async Task Should_not_add_error_if_value_is_valid(string value) { - private readonly List<string> errors = new List<string>(); - - [Theory] - [InlineData("MyString")] - [InlineData("")] - [InlineData(" ")] - [InlineData(" ")] - public async Task Should_not_add_error_if_value_is_valid(string value) - { - var sut = new RequiredStringValidator(); + var sut = new RequiredStringValidator(); - await sut.ValidateAsync(value, errors); + await sut.ValidateAsync(value, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_optional() - { - var sut = new RequiredStringValidator(); + [Fact] + public async Task Should_not_add_error_if_optional() + { + var sut = new RequiredStringValidator(); - await sut.ValidateAsync(string.Empty, errors, updater: c => c.Optional(true)); + await sut.ValidateAsync(string.Empty, errors, updater: c => c.Optional(true)); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_value_is_another_type() - { - var sut = new RequiredStringValidator(); + [Fact] + public async Task Should_not_add_error_if_value_is_another_type() + { + var sut = new RequiredStringValidator(); - await sut.ValidateAsync(true, errors); + await sut.ValidateAsync(true, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_empty_strings_are_not_allowed() - { - var sut = new RequiredStringValidator(true); + [Fact] + public async Task Should_add_error_if_empty_strings_are_not_allowed() + { + var sut = new RequiredStringValidator(true); - await sut.ValidateAsync(string.Empty, errors); + await sut.ValidateAsync(string.Empty, errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } - [Fact] - public async Task Should_add_error_if_value_is_null() - { - var sut = new RequiredStringValidator(); + [Fact] + public async Task Should_add_error_if_value_is_null() + { + var sut = new RequiredStringValidator(); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs index 088f9ea856..e6386bfeea 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs @@ -10,51 +10,50 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class RequiredValidatorTests : IClassFixture<TranslationsFixture> { - public class RequiredValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public async Task Should_not_add_error_if_value_is_valid() - { - var sut = new RequiredValidator(); + [Fact] + public async Task Should_not_add_error_if_value_is_valid() + { + var sut = new RequiredValidator(); - await sut.ValidateAsync(true, errors); + await sut.ValidateAsync(true, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_for_empty_string() - { - var sut = new RequiredValidator(); + [Fact] + public async Task Should_not_add_error_for_empty_string() + { + var sut = new RequiredValidator(); - await sut.ValidateAsync(string.Empty, errors); + await sut.ValidateAsync(string.Empty, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_optional() - { - var sut = new RequiredValidator(); + [Fact] + public async Task Should_not_add_error_if_optional() + { + var sut = new RequiredValidator(); - await sut.ValidateAsync(null, errors, updater: c => c.Optional(true)); + await sut.ValidateAsync(null, errors, updater: c => c.Optional(true)); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_value_is_null() - { - var sut = new RequiredValidator(); + [Fact] + public async Task Should_add_error_if_value_is_null() + { + var sut = new RequiredValidator(); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs index b6bc6fdfde..63d66e5b41 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs @@ -11,107 +11,106 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class StringLengthValidatorTests : IClassFixture<TranslationsFixture> { - public class StringLengthValidatorTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + + [Fact] + public async Task Should_not_add_error_if_value_is_null() { - private readonly List<string> errors = new List<string>(); + var sut = new StringLengthValidator(); - [Fact] - public async Task Should_not_add_error_if_value_is_null() - { - var sut = new StringLengthValidator(); + await sut.ValidateAsync(null, errors); - await sut.ValidateAsync(null, errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_not_add_error_if_value_is_empty() + { + var sut = new StringLengthValidator(); - [Fact] - public async Task Should_not_add_error_if_value_is_empty() - { - var sut = new StringLengthValidator(); + await sut.ValidateAsync(string.Empty, errors); - await sut.ValidateAsync(string.Empty, errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Theory] + [InlineData(null, null)] + [InlineData(1000, null)] + [InlineData(1000, 2000)] + [InlineData(null, 2000)] + public async Task Should_not_add_error_if_value_is_within_range(int? min, int? max) + { + var sut = new StringLengthValidator(min, max); - [Theory] - [InlineData(null, null)] - [InlineData(1000, null)] - [InlineData(1000, 2000)] - [InlineData(null, 2000)] - public async Task Should_not_add_error_if_value_is_within_range(int? min, int? max) - { - var sut = new StringLengthValidator(min, max); + await sut.ValidateAsync(CreateString(1500), errors); - await sut.ValidateAsync(CreateString(1500), errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Theory] + [InlineData(20, 10)] + public void Should_throw_error_if_min_greater_than_max(int? min, int? max) + { + Assert.Throws<ArgumentException>(() => new StringLengthValidator(min, max)); + } - [Theory] - [InlineData(20, 10)] - public void Should_throw_error_if_min_greater_than_max(int? min, int? max) - { - Assert.Throws<ArgumentException>(() => new StringLengthValidator(min, max)); - } + [Fact] + public async Task Should_add_error_if_value_has_not_exact_number_of_characters() + { + var sut = new StringLengthValidator(2000, 2000); - [Fact] - public async Task Should_add_error_if_value_has_not_exact_number_of_characters() - { - var sut = new StringLengthValidator(2000, 2000); + await sut.ValidateAsync(CreateString(4), errors); - await sut.ValidateAsync(CreateString(4), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have exactly 2000 character(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have exactly 2000 character(s)." }); - } + [Fact] + public async Task Should_add_error_if_value_is_smaller_than_min() + { + var sut = new StringLengthValidator(2000, null); - [Fact] - public async Task Should_add_error_if_value_is_smaller_than_min() - { - var sut = new StringLengthValidator(2000, null); + await sut.ValidateAsync(CreateString(1500), errors); - await sut.ValidateAsync(CreateString(1500), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2000 character(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have at least 2000 character(s)." }); - } + [Fact] + public async Task Should_add_error_if_value_is_greater_than_max() + { + var sut = new StringLengthValidator(null, 1000); - [Fact] - public async Task Should_add_error_if_value_is_greater_than_max() - { - var sut = new StringLengthValidator(null, 1000); + await sut.ValidateAsync(CreateString(1500), errors); - await sut.ValidateAsync(CreateString(1500), errors); + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1000 character(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1000 character(s)." }); - } + [Fact] + public async Task Should_add_error_if_collection_count_is_not_in_range() + { + var sut = new StringLengthValidator(2000, 5000); - [Fact] - public async Task Should_add_error_if_collection_count_is_not_in_range() - { - var sut = new StringLengthValidator(2000, 5000); + await sut.ValidateAsync(CreateString(1), errors); - await sut.ValidateAsync(CreateString(1), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have between 2000 and 5000 character(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have between 2000 and 5000 character(s)." }); - } + private static string CreateString(int size) + { + var sb = new StringBuilder(); - private static string CreateString(int size) + for (var i = 0; i < size; i++) { - var sb = new StringBuilder(); - - for (var i = 0; i < size; i++) - { - sb.Append('x'); - } - - return sb.ToString(); + sb.Append('x'); } + + return sb.ToString(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs index 575cf2c6e0..cdd27af80b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs @@ -11,185 +11,184 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class StringTextValidatorTests : IClassFixture<TranslationsFixture> { - public class StringTextValidatorTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + + [Theory] + [InlineData(20, 10)] + public void Should_throw_error_if_min_characters_greater_than_max(int? min, int? max) { - private readonly List<string> errors = new List<string>(); + Assert.Throws<ArgumentException>(() => new StringTextValidator(minCharacters: min, maxCharacters: max)); + } - [Theory] - [InlineData(20, 10)] - public void Should_throw_error_if_min_characters_greater_than_max(int? min, int? max) - { - Assert.Throws<ArgumentException>(() => new StringTextValidator(minCharacters: min, maxCharacters: max)); - } + [Theory] + [InlineData(20, 10)] + public void Should_throw_error_if_min_words_greater_than_max(int? min, int? max) + { + Assert.Throws<ArgumentException>(() => new StringTextValidator(minWords: min, maxWords: max)); + } - [Theory] - [InlineData(20, 10)] - public void Should_throw_error_if_min_words_greater_than_max(int? min, int? max) - { - Assert.Throws<ArgumentException>(() => new StringTextValidator(minWords: min, maxWords: max)); - } + [Fact] + public async Task Should_not_add_error_if_value_is_null() + { + var sut = new StringTextValidator(minCharacters: 5); - [Fact] - public async Task Should_not_add_error_if_value_is_null() - { - var sut = new StringTextValidator(minCharacters: 5); + await sut.ValidateAsync(null, errors); - await sut.ValidateAsync(null, errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_not_add_error_if_value_is_empty() + { + var sut = new StringTextValidator(minCharacters: 5); - [Fact] - public async Task Should_not_add_error_if_value_is_empty() - { - var sut = new StringTextValidator(minCharacters: 5); + await sut.ValidateAsync(string.Empty, errors); - await sut.ValidateAsync(string.Empty, errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Theory] + [InlineData(null, null)] + [InlineData(1000, null)] + [InlineData(1000, 2000)] + [InlineData(null, 2000)] + public async Task Should_not_add_error_if_value_is_within_character_range(int? min, int? max) + { + var sut = new StringTextValidator(minCharacters: min, maxCharacters: max); - [Theory] - [InlineData(null, null)] - [InlineData(1000, null)] - [InlineData(1000, 2000)] - [InlineData(null, 2000)] - public async Task Should_not_add_error_if_value_is_within_character_range(int? min, int? max) - { - var sut = new StringTextValidator(minCharacters: min, maxCharacters: max); + await sut.ValidateAsync(CreateString(1500), errors); - await sut.ValidateAsync(CreateString(1500), errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_add_error_if_value_has_not_exact_number_of_characters() + { + var sut = new StringTextValidator(minCharacters: 5, maxCharacters: 5); - [Fact] - public async Task Should_add_error_if_value_has_not_exact_number_of_characters() - { - var sut = new StringTextValidator(minCharacters: 5, maxCharacters: 5); + await sut.ValidateAsync(CreateString(4), errors); - await sut.ValidateAsync(CreateString(4), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have exactly 5 text character(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have exactly 5 text character(s)." }); - } + [Fact] + public async Task Should_add_error_if_value_is_smaller_than_min_characters() + { + var sut = new StringTextValidator(minCharacters: 2000); - [Fact] - public async Task Should_add_error_if_value_is_smaller_than_min_characters() - { - var sut = new StringTextValidator(minCharacters: 2000); + await sut.ValidateAsync(CreateString(1500), errors); - await sut.ValidateAsync(CreateString(1500), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2000 text character(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have at least 2000 text character(s)." }); - } + [Fact] + public async Task Should_add_error_if_value_is_greater_than_max_characters() + { + var sut = new StringTextValidator(maxCharacters: 1000); - [Fact] - public async Task Should_add_error_if_value_is_greater_than_max_characters() - { - var sut = new StringTextValidator(maxCharacters: 1000); + await sut.ValidateAsync(CreateString(1500), errors); - await sut.ValidateAsync(CreateString(1500), errors); + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1000 text character(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1000 text character(s)." }); - } + [Fact] + public async Task Should_add_error_if_collection_count_is_not_in_character_range() + { + var sut = new StringTextValidator(minCharacters: 2000, maxCharacters: 5000); - [Fact] - public async Task Should_add_error_if_collection_count_is_not_in_character_range() - { - var sut = new StringTextValidator(minCharacters: 2000, maxCharacters: 5000); + await sut.ValidateAsync(CreateString(1), errors); - await sut.ValidateAsync(CreateString(1), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have between 2000 and 5000 text character(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have between 2000 and 5000 text character(s)." }); - } + [Theory] + [InlineData(null, null, 1000)] + [InlineData(1000, null, 1000)] + [InlineData(1000, 2000, 1000)] + [InlineData(null, 2000, 1000)] + public async Task Should_not_add_error_if_value_is_within_word_range(int? min, int? max, int length) + { + var sut = new StringTextValidator(minWords: min, maxWords: max); - [Theory] - [InlineData(null, null, 1000)] - [InlineData(1000, null, 1000)] - [InlineData(1000, 2000, 1000)] - [InlineData(null, 2000, 1000)] - public async Task Should_not_add_error_if_value_is_within_word_range(int? min, int? max, int length) - { - var sut = new StringTextValidator(minWords: min, maxWords: max); + await sut.ValidateAsync(CreateSentence(length), errors); - await sut.ValidateAsync(CreateSentence(length), errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_add_error_if_value_has_not_exact_number_of_words() + { + var sut = new StringTextValidator(minWords: 5, maxWords: 5); - [Fact] - public async Task Should_add_error_if_value_has_not_exact_number_of_words() - { - var sut = new StringTextValidator(minWords: 5, maxWords: 5); + await sut.ValidateAsync(CreateSentence(4), errors); - await sut.ValidateAsync(CreateSentence(4), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have exactly 5 word(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have exactly 5 word(s)." }); - } + [Fact] + public async Task Should_add_error_if_value_is_smaller_than_min_words() + { + var sut = new StringTextValidator(minWords: 2000); - [Fact] - public async Task Should_add_error_if_value_is_smaller_than_min_words() - { - var sut = new StringTextValidator(minWords: 2000); + await sut.ValidateAsync(CreateSentence(1500), errors); - await sut.ValidateAsync(CreateSentence(1500), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2000 word(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have at least 2000 word(s)." }); - } + [Fact] + public async Task Should_add_error_if_value_is_greater_than_max_words() + { + var sut = new StringTextValidator(maxWords: 1000); - [Fact] - public async Task Should_add_error_if_value_is_greater_than_max_words() - { - var sut = new StringTextValidator(maxWords: 1000); + await sut.ValidateAsync(CreateSentence(1500), errors); - await sut.ValidateAsync(CreateSentence(1500), errors); + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1000 word(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1000 word(s)." }); - } + [Fact] + public async Task Should_add_error_if_collection_count_is_not_in_word_range() + { + var sut = new StringTextValidator(minWords: 2000, maxWords: 5000); - [Fact] - public async Task Should_add_error_if_collection_count_is_not_in_word_range() - { - var sut = new StringTextValidator(minWords: 2000, maxWords: 5000); + await sut.ValidateAsync(CreateSentence(1), errors); - await sut.ValidateAsync(CreateSentence(1), errors); + errors.Should().BeEquivalentTo( + new[] { "Must have between 2000 and 5000 word(s)." }); + } - errors.Should().BeEquivalentTo( - new[] { "Must have between 2000 and 5000 word(s)." }); - } + private static string CreateString(int size) + { + var sb = new StringBuilder(); - private static string CreateString(int size) + for (var i = 0; i < size; i++) { - var sb = new StringBuilder(); - - for (var i = 0; i < size; i++) - { - sb.Append('x'); - } - - return sb.ToString(); + sb.Append('x'); } - private static string CreateSentence(int size) - { - var sb = new StringBuilder(); + return sb.ToString(); + } - for (var i = 0; i < size; i++) - { - sb.Append('x'); - sb.Append(' '); - } + private static string CreateSentence(int size) + { + var sb = new StringBuilder(); - return sb.ToString(); + for (var i = 0; i < size; i++) + { + sb.Append('x'); + sb.Append(' '); } + + return sb.ToString(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidatorTests.cs index 4d010517c3..0bc0f80641 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidatorTests.cs @@ -11,106 +11,105 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class UniqueObjectValuesValidatorTests : IClassFixture<TranslationsFixture> { - public class UniqueObjectValuesValidatorTests : IClassFixture<TranslationsFixture> + private readonly List<string> errors = new List<string>(); + + [Fact] + public async Task Should_not_add_errors_if_value_is_invalid() { - private readonly List<string> errors = new List<string>(); + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); - [Fact] - public async Task Should_not_add_errors_if_value_is_invalid() - { - var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + await sut.ValidateAsync(1, errors); - await sut.ValidateAsync(1, errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_not_add_errors_if_value_is_null() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); - [Fact] - public async Task Should_not_add_errors_if_value_is_null() - { - var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + await sut.ValidateAsync(null, errors); - await sut.ValidateAsync(null, errors); + Assert.Empty(errors); + } - Assert.Empty(errors); - } + [Fact] + public async Task Should_not_add_error_if_objects_contain_not_duplicates() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); - [Fact] - public async Task Should_not_add_error_if_objects_contain_not_duplicates() + await sut.ValidateAsync(new[] { - var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + new JsonObject() + .Add("myString", "1"), + new JsonObject() + .Add("myString", "2") + }, + errors); + + Assert.Empty(errors); + } - await sut.ValidateAsync(new[] - { - new JsonObject() - .Add("myString", "1"), - new JsonObject() - .Add("myString", "2") - }, - errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_objects_contain_unchecked_duplicates() - { - var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + [Fact] + public async Task Should_not_add_error_if_objects_contain_unchecked_duplicates() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); - await sut.ValidateAsync(new[] - { - new JsonObject() - .Add("other", "1"), - new JsonObject() - .Add("other", "1") - }, - errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_objects_contain_duplicates() + await sut.ValidateAsync(new[] { - var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + new JsonObject() + .Add("other", "1"), + new JsonObject() + .Add("other", "1") + }, + errors); + + Assert.Empty(errors); + } - await sut.ValidateAsync(new[] - { - new JsonObject() - .Add("myString", "1"), - new JsonObject() - .Add("myString", "1") - }, - errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not contain items with duplicate 'myString' fields." }); - } - - [Fact] - public async Task Should_add_errors_if_objects_contain_multiple_duplicates() + [Fact] + public async Task Should_add_error_if_objects_contain_duplicates() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + + await sut.ValidateAsync(new[] { - var sut = new UniqueObjectValuesValidator(new[] { "myString", "myNumber" }); + new JsonObject() + .Add("myString", "1"), + new JsonObject() + .Add("myString", "1") + }, + errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain items with duplicate 'myString' fields." }); + } - await sut.ValidateAsync(new[] + [Fact] + public async Task Should_add_errors_if_objects_contain_multiple_duplicates() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString", "myNumber" }); + + await sut.ValidateAsync(new[] + { + new JsonObject() + .Add("myString", "1") + .Add("myNumber", 1), + new JsonObject() + .Add("myString", "1") + .Add("myNumber", 1) + }, + errors); + + errors.Should().BeEquivalentTo( + new[] { - new JsonObject() - .Add("myString", "1") - .Add("myNumber", 1), - new JsonObject() - .Add("myString", "1") - .Add("myNumber", 1) - }, - errors); - - errors.Should().BeEquivalentTo( - new[] - { - "Must not contain items with duplicate 'myString' fields.", - "Must not contain items with duplicate 'myNumber' fields." - }); - } + "Must not contain items with duplicate 'myString' fields.", + "Must not contain items with duplicate 'myNumber' fields." + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs index db8fd1f9ec..d5af9a497a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs @@ -12,102 +12,101 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class UniqueValidatorTests : IClassFixture<TranslationsFixture> { - public class UniqueValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public async Task Should_not_check_uniqueness_if_localized_string() - { - var filter = string.Empty; + [Fact] + public async Task Should_not_check_uniqueness_if_localized_string() + { + var filter = string.Empty; - var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); + var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); - await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("de")); + await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("de")); - Assert.Empty(errors); - Assert.Empty(filter); - } + Assert.Empty(errors); + Assert.Empty(filter); + } - [Fact] - public async Task Should_not_add_error_if_value_is_null() - { - var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid())); + [Fact] + public async Task Should_not_add_error_if_value_is_null() + { + var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid())); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_same_content_with_string_value_found() - { - var contentId = DomainId.NewGuid(); + [Fact] + public async Task Should_not_add_error_if_same_content_with_string_value_found() + { + var contentId = DomainId.NewGuid(); - var sut = new UniqueValidator(FoundDuplicates(contentId)); + var sut = new UniqueValidator(FoundDuplicates(contentId)); - await sut.ValidateAsync("hello", errors, contentId: contentId, updater: ctx => ctx.Nested("property").Nested("iv")); + await sut.ValidateAsync("hello", errors, contentId: contentId, updater: ctx => ctx.Nested("property").Nested("iv")); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_same_content_with_double_value_found() - { - var contentId = DomainId.NewGuid(); + [Fact] + public async Task Should_not_add_error_if_same_content_with_double_value_found() + { + var contentId = DomainId.NewGuid(); - var sut = new UniqueValidator(FoundDuplicates(contentId)); + var sut = new UniqueValidator(FoundDuplicates(contentId)); - await sut.ValidateAsync(12.5, errors, contentId: contentId, updater: ctx => ctx.Nested("property").Nested("iv")); + await sut.ValidateAsync(12.5, errors, contentId: contentId, updater: ctx => ctx.Nested("property").Nested("iv")); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_other_content_with_string_value_found() - { - var filter = string.Empty; + [Fact] + public async Task Should_add_error_if_other_content_with_string_value_found() + { + var filter = string.Empty; - var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); + var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); - await sut.ValidateAsync("hello", errors, updater: c => c.Nested("property").Nested("iv")); + await sut.ValidateAsync("hello", errors, updater: c => c.Nested("property").Nested("iv")); - errors.Should().BeEquivalentTo( - new[] { "property.iv: Another content with the same value exists." }); + errors.Should().BeEquivalentTo( + new[] { "property.iv: Another content with the same value exists." }); - Assert.Equal("Data.property.iv == 'hello'", filter); - } + Assert.Equal("Data.property.iv == 'hello'", filter); + } - [Fact] - public async Task Should_add_error_if_other_content_with_double_value_found() - { - var filter = string.Empty; + [Fact] + public async Task Should_add_error_if_other_content_with_double_value_found() + { + var filter = string.Empty; - var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); + var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); - await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("iv")); + await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("iv")); - errors.Should().BeEquivalentTo( - new[] { "property.iv: Another content with the same value exists." }); + errors.Should().BeEquivalentTo( + new[] { "property.iv: Another content with the same value exists." }); - Assert.Equal("Data.property.iv == 12.5", filter); - } + Assert.Equal("Data.property.iv == 12.5", filter); + } - private static CheckUniqueness FoundDuplicates(DomainId id, Action<string>? filter = null) + private static CheckUniqueness FoundDuplicates(DomainId id, Action<string>? filter = null) + { + return filterNode => { - return filterNode => - { - filter?.Invoke(filterNode.ToString()); + filter?.Invoke(filterNode.ToString()); - var foundIds = new List<ContentIdStatus> - { - new ContentIdStatus(id, id, Status.Draft) - }; - - return Task.FromResult<IReadOnlyList<ContentIdStatus>>(foundIds); + var foundIds = new List<ContentIdStatus> + { + new ContentIdStatus(id, id, Status.Draft) }; - } + + return Task.FromResult<IReadOnlyList<ContentIdStatus>>(foundIds); + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs index 68a8ae65c1..324e6fab15 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValuesValidatorTests.cs @@ -10,51 +10,50 @@ using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; + +public class UniqueValuesValidatorTests : IClassFixture<TranslationsFixture> { - public class UniqueValuesValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly List<string> errors = new List<string>(); + private readonly List<string> errors = new List<string>(); - [Fact] - public async Task Should_not_add_error_if_value_is_null() - { - var sut = new UniqueValuesValidator<int>(); + [Fact] + public async Task Should_not_add_error_if_value_is_null() + { + var sut = new UniqueValuesValidator<int>(); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_value_is_not_collection() - { - var sut = new UniqueValuesValidator<int>(); + [Fact] + public async Task Should_not_add_error_if_value_is_not_collection() + { + var sut = new UniqueValuesValidator<int>(); - await sut.ValidateAsync("value", errors); + await sut.ValidateAsync("value", errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_array_contains_no_duplicates() - { - var sut = new UniqueValuesValidator<int>(); + [Fact] + public async Task Should_not_add_error_if_array_contains_no_duplicates() + { + var sut = new UniqueValuesValidator<int>(); - await sut.ValidateAsync(new[] { 1, 2, 3 }, errors); + await sut.ValidateAsync(new[] { 1, 2, 3 }, errors); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_add_error_if_array_contains_duplicates() - { - var sut = new UniqueValuesValidator<int>(); + [Fact] + public async Task Should_add_error_if_array_contains_duplicates() + { + var sut = new UniqueValuesValidator<int>(); - await sut.ValidateAsync(new[] { 1, 2, 2, 3 }, errors); + await sut.ValidateAsync(new[] { 1, 2, 2, 3 }, errors); - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs index 1387ba6987..52a2d6d2db 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs @@ -7,28 +7,27 @@ using FakeItEasy; -namespace Squidex.Domain.Apps.Core.TestHelpers +namespace Squidex.Domain.Apps.Core.TestHelpers; + +public static class AExtensions { - public static class AExtensions + public static T[] Is<T>(this INegatableArgumentConstraintManager<T[]> that, params T[]? values) { - public static T[] Is<T>(this INegatableArgumentConstraintManager<T[]> that, params T[]? values) - { - return values == null ? that.IsNull() : that.IsSameSequenceAs(values); - } + return values == null ? that.IsNull() : that.IsSameSequenceAs(values); + } - public static IEnumerable<T> Is<T>(this INegatableArgumentConstraintManager<IEnumerable<T>> that, params T[]? values) - { - return values == null ? that.IsNull() : that.IsSameSequenceAs(values); - } + public static IEnumerable<T> Is<T>(this INegatableArgumentConstraintManager<IEnumerable<T>> that, params T[]? values) + { + return values == null ? that.IsNull() : that.IsSameSequenceAs(values); + } - public static HashSet<T> Is<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, IEnumerable<T>? values) - { - return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Count()); - } + public static HashSet<T> Is<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, IEnumerable<T>? values) + { + return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Count()); + } - public static HashSet<T> Is<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, params T[]? values) - { - return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Length); - } + public static HashSet<T> Is<T>(this INegatableArgumentConstraintManager<HashSet<T>> that, params T[]? values) + { + return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Length); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs index 462da7f843..802cd9229a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs @@ -9,86 +9,85 @@ using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core.TestHelpers +namespace Squidex.Domain.Apps.Core.TestHelpers; + +public static class TestAssets { - public static class TestAssets + public sealed class AssetInfo : IAssetInfo { - public sealed class AssetInfo : IAssetInfo - { - public DomainId AssetId { get; set; } + public DomainId AssetId { get; set; } - public string FileName { get; set; } + public string FileName { get; set; } - public string FileHash { get; set; } + public string FileHash { get; set; } - public string MimeType { get; set; } + public string MimeType { get; set; } - public string Slug { get; set; } + public string Slug { get; set; } - public long FileSize { get; set; } + public long FileSize { get; set; } - public bool IsImage { get; set; } + public bool IsImage { get; set; } - public int? PixelWidth { get; set; } + public int? PixelWidth { get; set; } - public int? PixelHeight { get; set; } + public int? PixelHeight { get; set; } - public AssetMetadata Metadata { get; set; } + public AssetMetadata Metadata { get; set; } - public AssetType Type { get; set; } - } + public AssetType Type { get; set; } + } - public static AssetInfo Document(DomainId id) + public static AssetInfo Document(DomainId id) + { + return new AssetInfo { - return new AssetInfo - { - AssetId = id, - FileName = "MyDocument.pdf", - FileSize = 1024 * 4, - Type = AssetType.Unknown - }; - } - - public static AssetInfo Image(DomainId id) + AssetId = id, + FileName = "MyDocument.pdf", + FileSize = 1024 * 4, + Type = AssetType.Unknown + }; + } + + public static AssetInfo Image(DomainId id) + { + return new AssetInfo { - return new AssetInfo - { - AssetId = id, - FileName = "MyImage.png", - FileSize = 1024 * 8, - Type = AssetType.Image, - Metadata = - new AssetMetadata() - .SetPixelWidth(800) - .SetPixelHeight(600) - }; - } - - public static AssetInfo Video(DomainId id) + AssetId = id, + FileName = "MyImage.png", + FileSize = 1024 * 8, + Type = AssetType.Image, + Metadata = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600) + }; + } + + public static AssetInfo Video(DomainId id) + { + return new AssetInfo { - return new AssetInfo - { - AssetId = id, - FileName = "MyImage.png", - FileSize = 1024 * 8, - Type = AssetType.Video, - Metadata = - new AssetMetadata() - .SetVideoWidth(800) - .SetVideoHeight(600) - }; - } - - public static AssetInfo Svg(DomainId id) + AssetId = id, + FileName = "MyImage.png", + FileSize = 1024 * 8, + Type = AssetType.Video, + Metadata = + new AssetMetadata() + .SetVideoWidth(800) + .SetVideoHeight(600) + }; + } + + public static AssetInfo Svg(DomainId id) + { + return new AssetInfo { - return new AssetInfo - { - AssetId = id, - FileName = "MyImage.png", - FileSize = 1024 * 8, - Type = AssetType.Unknown, - MimeType = "image/svg+xml" - }; - } + AssetId = id, + FileName = "MyImage.png", + FileSize = 1024 * 8, + Type = AssetType.Unknown, + MimeType = "image/svg+xml" + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs index 0b6de2307f..c432080e93 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs @@ -9,97 +9,96 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Core.TestHelpers +namespace Squidex.Domain.Apps.Core.TestHelpers; + +public static class TestSchema { - public static class TestSchema + public static (Schema Schema, ResolvedComponents) MixedSchema(SchemaType type = SchemaType.Default) { - public static (Schema Schema, ResolvedComponents) MixedSchema(SchemaType type = SchemaType.Default) - { - var componentId1 = DomainId.NewGuid(); - var componentId2 = DomainId.NewGuid(); - var componentIds = ReadonlyList.Create(componentId1, componentId2); + var componentId1 = DomainId.NewGuid(); + var componentId2 = DomainId.NewGuid(); + var componentIds = ReadonlyList.Create(componentId1, componentId2); - var component1 = new Schema("component1") - .Publish() - .AddString(1, "unique1", Partitioning.Invariant) - .AddString(2, "shared1", Partitioning.Invariant) - .AddBoolean(3, "shared2", Partitioning.Invariant); + var component1 = new Schema("component1") + .Publish() + .AddString(1, "unique1", Partitioning.Invariant) + .AddString(2, "shared1", Partitioning.Invariant) + .AddBoolean(3, "shared2", Partitioning.Invariant); - var component2 = new Schema("component2") - .Publish() - .AddNumber(1, "unique1", Partitioning.Invariant) - .AddNumber(2, "shared1", Partitioning.Invariant) - .AddBoolean(3, "shared2", Partitioning.Invariant); + var component2 = new Schema("component2") + .Publish() + .AddNumber(1, "unique1", Partitioning.Invariant) + .AddNumber(2, "shared1", Partitioning.Invariant) + .AddBoolean(3, "shared2", Partitioning.Invariant); - var resolvedComponents = new ResolvedComponents(new Dictionary<DomainId, Schema> - { - [componentId1] = component1, - [componentId2] = component2 - }); + var resolvedComponents = new ResolvedComponents(new Dictionary<DomainId, Schema> + { + [componentId1] = component1, + [componentId2] = component2 + }); - var schema = new Schema("user", type: type) - .Publish() - .AddArray(101, "root-array", Partitioning.Language, f => f - .AddAssets(201, "nested-assets", - new AssetsFieldProperties()) - .AddBoolean(202, "nested-boolean", - new BooleanFieldProperties()) - .AddDateTime(203, "nested-datetime", - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) - .AddDateTime(204, "nested-date", - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) - .AddGeolocation(205, "nested-geolocation", - new GeolocationFieldProperties()) - .AddJson(206, "nested-json", - new JsonFieldProperties()) - .AddJson(207, "nested-json2", - new JsonFieldProperties()) - .AddNumber(208, "nested-number", - new NumberFieldProperties()) - .AddReferences(209, "nested-references", - new ReferencesFieldProperties()) - .AddString(210, "nested-string", - new StringFieldProperties()) - .AddTags(211, "nested-tags", - new TagsFieldProperties()) - .AddUI(212, "nested-ui", - new UIFieldProperties())) - .AddAssets(102, "root-assets", Partitioning.Invariant, + var schema = new Schema("user", type: type) + .Publish() + .AddArray(101, "root-array", Partitioning.Language, f => f + .AddAssets(201, "nested-assets", new AssetsFieldProperties()) - .AddBoolean(103, "root-boolean", Partitioning.Invariant, + .AddBoolean(202, "nested-boolean", new BooleanFieldProperties()) - .AddDateTime(104, "root-datetime", Partitioning.Invariant, + .AddDateTime(203, "nested-datetime", new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) - .AddDateTime(105, "root-date", Partitioning.Invariant, + .AddDateTime(204, "nested-date", new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) - .AddGeolocation(106, "root-geolocation", Partitioning.Invariant, + .AddGeolocation(205, "nested-geolocation", new GeolocationFieldProperties()) - .AddJson(107, "root-json", Partitioning.Invariant, + .AddJson(206, "nested-json", + new JsonFieldProperties()) + .AddJson(207, "nested-json2", new JsonFieldProperties()) - .AddNumber(108, "root-number", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) - .AddReferences(109, "root-references", Partitioning.Invariant, + .AddNumber(208, "nested-number", + new NumberFieldProperties()) + .AddReferences(209, "nested-references", new ReferencesFieldProperties()) - .AddString(110, "root-string1", Partitioning.Invariant, - new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ReadonlyList.Create("a", "b") }) - .AddString(111, "root-string2", Partitioning.Invariant, - new StringFieldProperties { Hints = "My String1" }) - .AddTags(112, "root-tags", Partitioning.Language, + .AddString(210, "nested-string", + new StringFieldProperties()) + .AddTags(211, "nested-tags", new TagsFieldProperties()) - .AddUI(113, "root-ui", Partitioning.Language, - new UIFieldProperties()) - .AddComponent(114, "root-component", Partitioning.Language, - new ComponentFieldProperties { SchemaIds = componentIds }) - .AddComponents(115, "root-components", Partitioning.Language, - new ComponentsFieldProperties { SchemaIds = componentIds }) - .Update(new SchemaProperties { Hints = "The User" }) - .HideField(104) - .HideField(211, 101) - .DisableField(109) - .DisableField(212, 101) - .LockField(105); + .AddUI(212, "nested-ui", + new UIFieldProperties())) + .AddAssets(102, "root-assets", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddBoolean(103, "root-boolean", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddDateTime(104, "root-datetime", Partitioning.Invariant, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) + .AddDateTime(105, "root-date", Partitioning.Invariant, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) + .AddGeolocation(106, "root-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties()) + .AddJson(107, "root-json", Partitioning.Invariant, + new JsonFieldProperties()) + .AddNumber(108, "root-number", Partitioning.Invariant, + new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) + .AddReferences(109, "root-references", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddString(110, "root-string1", Partitioning.Invariant, + new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ReadonlyList.Create("a", "b") }) + .AddString(111, "root-string2", Partitioning.Invariant, + new StringFieldProperties { Hints = "My String1" }) + .AddTags(112, "root-tags", Partitioning.Language, + new TagsFieldProperties()) + .AddUI(113, "root-ui", Partitioning.Language, + new UIFieldProperties()) + .AddComponent(114, "root-component", Partitioning.Language, + new ComponentFieldProperties { SchemaIds = componentIds }) + .AddComponents(115, "root-components", Partitioning.Language, + new ComponentsFieldProperties { SchemaIds = componentIds }) + .Update(new SchemaProperties { Hints = "The User" }) + .HideField(104) + .HideField(211, 101) + .DisableField(109) + .DisableField(212, 101) + .LockField(105); - return (schema, resolvedComponents); - } + return (schema, resolvedComponents); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs index a446501077..b3a56ac0f6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs @@ -37,176 +37,175 @@ #pragma warning disable SYSLIB0011 // Type or member is obsolete -namespace Squidex.Domain.Apps.Core.TestHelpers +namespace Squidex.Domain.Apps.Core.TestHelpers; + +public static class TestUtils { - public static class TestUtils - { - public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); + public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - public sealed class ObjectHolder<T> - { - [BsonRequired] - public T Value1 { get; set; } + public sealed class ObjectHolder<T> + { + [BsonRequired] + public T Value1 { get; set; } - [BsonRequired] - public T Value2 { get; set; } - } + [BsonRequired] + public T Value2 { get; set; } + } - static TestUtils() - { - SetupBson(); - } + static TestUtils() + { + SetupBson(); + } - public static void SetupBson() - { - BsonDomainIdSerializer.Register(); - BsonEscapedDictionarySerializer<ContentFieldData, ContentData>.Register(); - BsonEscapedDictionarySerializer<JsonValue, ContentFieldData>.Register(); - BsonEscapedDictionarySerializer<JsonValue, JsonObject>.Register(); - BsonEscapedDictionarySerializer<JsonValue, JsonObject>.Register(); - BsonInstantSerializer.Register(); - BsonJsonConvention.Register(DefaultOptions()); - BsonJsonValueSerializer.Register(); - BsonStringSerializer<RefToken>.Register(); - BsonStringSerializer<Status>.Register(); - } + public static void SetupBson() + { + BsonDomainIdSerializer.Register(); + BsonEscapedDictionarySerializer<ContentFieldData, ContentData>.Register(); + BsonEscapedDictionarySerializer<JsonValue, ContentFieldData>.Register(); + BsonEscapedDictionarySerializer<JsonValue, JsonObject>.Register(); + BsonEscapedDictionarySerializer<JsonValue, JsonObject>.Register(); + BsonInstantSerializer.Register(); + BsonJsonConvention.Register(DefaultOptions()); + BsonJsonValueSerializer.Register(); + BsonStringSerializer<RefToken>.Register(); + BsonStringSerializer<Status>.Register(); + } - public static IJsonSerializer CreateSerializer(Action<JsonSerializerOptions>? configure = null) - { - var serializerSettings = DefaultOptions(configure); + public static IJsonSerializer CreateSerializer(Action<JsonSerializerOptions>? configure = null) + { + var serializerSettings = DefaultOptions(configure); - return new SystemJsonSerializer(serializerSettings); - } + return new SystemJsonSerializer(serializerSettings); + } - public static JsonSerializerOptions DefaultOptions(Action<JsonSerializerOptions>? configure = null) - { - var typeNameRegistry = - new TypeNameRegistry() - .Map(new FieldTypeProvider()) - .Map(new RuleTypeProvider()) - .MapUnmapped(typeof(TestUtils).Assembly); - - var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); - - options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. - options.Converters.Add(new StringConverter<PropertyPath>(x => x)); - options.Converters.Add(new GeoJsonConverterFactory()); - options.Converters.Add(new InheritanceConverter<IEvent>(typeNameRegistry)); - options.Converters.Add(new InheritanceConverter<FieldProperties>(typeNameRegistry)); - options.Converters.Add(new InheritanceConverter<RuleAction>(typeNameRegistry)); - options.Converters.Add(new InheritanceConverter<RuleTrigger>(typeNameRegistry)); - options.Converters.Add(new JsonValueConverter()); - options.Converters.Add(new ReadonlyDictionaryConverterFactory()); - options.Converters.Add(new ReadonlyListConverterFactory()); - options.Converters.Add(new SurrogateJsonConverter<ClaimsPrincipal, ClaimsPrincipalSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<FilterNode<JsonValue>, JsonFilterSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<LanguageConfig, LanguageConfigSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<LanguagesConfig, LanguagesConfigSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<Roles, RolesSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<Rule, RuleSorrgate>()); - options.Converters.Add(new SurrogateJsonConverter<Schema, SchemaSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<WorkflowStep, WorkflowStepSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<WorkflowTransition, WorkflowTransitionSurrogate>()); - options.Converters.Add(new StringConverter<CompareOperator>()); - options.Converters.Add(new StringConverter<DomainId>()); - options.Converters.Add(new StringConverter<NamedId<DomainId>>()); - options.Converters.Add(new StringConverter<NamedId<Guid>>()); - options.Converters.Add(new StringConverter<NamedId<long>>()); - options.Converters.Add(new StringConverter<NamedId<string>>()); - options.Converters.Add(new StringConverter<Language>()); - options.Converters.Add(new StringConverter<RefToken>()); - options.Converters.Add(new StringConverter<Status>()); - options.Converters.Add(new JsonStringEnumConverter()); - configure?.Invoke(options); - - return options; - } + public static JsonSerializerOptions DefaultOptions(Action<JsonSerializerOptions>? configure = null) + { + var typeNameRegistry = + new TypeNameRegistry() + .Map(new FieldTypeProvider()) + .Map(new RuleTypeProvider()) + .MapUnmapped(typeof(TestUtils).Assembly); + + var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. + options.Converters.Add(new StringConverter<PropertyPath>(x => x)); + options.Converters.Add(new GeoJsonConverterFactory()); + options.Converters.Add(new InheritanceConverter<IEvent>(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter<FieldProperties>(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter<RuleAction>(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter<RuleTrigger>(typeNameRegistry)); + options.Converters.Add(new JsonValueConverter()); + options.Converters.Add(new ReadonlyDictionaryConverterFactory()); + options.Converters.Add(new ReadonlyListConverterFactory()); + options.Converters.Add(new SurrogateJsonConverter<ClaimsPrincipal, ClaimsPrincipalSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<FilterNode<JsonValue>, JsonFilterSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<LanguageConfig, LanguageConfigSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<LanguagesConfig, LanguagesConfigSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<Roles, RolesSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<Rule, RuleSorrgate>()); + options.Converters.Add(new SurrogateJsonConverter<Schema, SchemaSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<WorkflowStep, WorkflowStepSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<WorkflowTransition, WorkflowTransitionSurrogate>()); + options.Converters.Add(new StringConverter<CompareOperator>()); + options.Converters.Add(new StringConverter<DomainId>()); + options.Converters.Add(new StringConverter<NamedId<DomainId>>()); + options.Converters.Add(new StringConverter<NamedId<Guid>>()); + options.Converters.Add(new StringConverter<NamedId<long>>()); + options.Converters.Add(new StringConverter<NamedId<string>>()); + options.Converters.Add(new StringConverter<Language>()); + options.Converters.Add(new StringConverter<RefToken>()); + options.Converters.Add(new StringConverter<Status>()); + options.Converters.Add(new JsonStringEnumConverter()); + configure?.Invoke(options); + + return options; + } - public static T SerializeAndDeserializeBinary<T>(this T source) + public static T SerializeAndDeserializeBinary<T>(this T source) + { + using (var stream = new MemoryStream()) { - using (var stream = new MemoryStream()) - { - var formatter = new BinaryFormatter(); + var formatter = new BinaryFormatter(); - formatter.Serialize(stream, source!); + formatter.Serialize(stream, source!); - stream.Position = 0; + stream.Position = 0; - return (T)formatter.Deserialize(stream); - } + return (T)formatter.Deserialize(stream); } + } - public static T SerializeAndDeserializeBson<T>(this T value) + public static T SerializeAndDeserializeBson<T>(this T value) + { + return SerializeAndDeserializeBson<T, T>(value); + } + + public static TOut SerializeAndDeserializeBson<TOut, TIn>(this TIn value) + { + using var stream = new MemoryStream(); + + using (var writer = new BsonBinaryWriter(stream)) { - return SerializeAndDeserializeBson<T, T>(value); + BsonSerializer.Serialize(writer, new ObjectHolder<TIn> { Value1 = value, Value2 = value }); } - public static TOut SerializeAndDeserializeBson<TOut, TIn>(this TIn value) + stream.Position = 0; + + using (var reader = new BsonBinaryReader(stream)) { - using var stream = new MemoryStream(); + return BsonSerializer.Deserialize<ObjectHolder<TOut>>(reader).Value1; + } + } - using (var writer = new BsonBinaryWriter(stream)) - { - BsonSerializer.Serialize(writer, new ObjectHolder<TIn> { Value1 = value, Value2 = value }); - } + public static T SerializeAndDeserialize<T>(this T value) + { + return SerializeAndDeserialize<T, T>(value); + } - stream.Position = 0; + public static TOut SerializeAndDeserialize<TOut, TIn>(this TIn value) + { + var json = DefaultSerializer.Serialize(new ObjectHolder<TIn> { Value1 = value, Value2 = value }); - using (var reader = new BsonBinaryReader(stream)) - { - return BsonSerializer.Deserialize<ObjectHolder<TOut>>(reader).Value1; - } - } + return DefaultSerializer.Deserialize<ObjectHolder<TOut>>(json).Value1; + } - public static T SerializeAndDeserialize<T>(this T value) - { - return SerializeAndDeserialize<T, T>(value); - } + public static T Deserialize<T>(string value) + { + var json = DefaultSerializer.Serialize(new ObjectHolder<string> { Value1 = value, Value2 = value }); - public static TOut SerializeAndDeserialize<TOut, TIn>(this TIn value) - { - var json = DefaultSerializer.Serialize(new ObjectHolder<TIn> { Value1 = value, Value2 = value }); + return DefaultSerializer.Deserialize<ObjectHolder<T>>(json).Value1; + } - return DefaultSerializer.Deserialize<ObjectHolder<TOut>>(json).Value1; - } + public static string CleanJson(this string json) + { + using var document = JsonDocument.Parse(json); - public static T Deserialize<T>(string value) - { - var json = DefaultSerializer.Serialize(new ObjectHolder<string> { Value1 = value, Value2 = value }); + return DefaultSerializer.Serialize(document, true); + } - return DefaultSerializer.Deserialize<ObjectHolder<T>>(json).Value1; - } + public static TEvent CreateEvent<TEvent>(Action<TEvent>? init = null) where TEvent : IEvent, new() + { + var actual = new TEvent(); - public static string CleanJson(this string json) + if (actual is SquidexEvent squidexEvent) { - using var document = JsonDocument.Parse(json); - - return DefaultSerializer.Serialize(document, true); + squidexEvent.Actor = RefToken.Client("my-client"); } - public static TEvent CreateEvent<TEvent>(Action<TEvent>? init = null) where TEvent : IEvent, new() + if (actual is AppEvent appEvent) { - var actual = new TEvent(); - - if (actual is SquidexEvent squidexEvent) - { - squidexEvent.Actor = RefToken.Client("my-client"); - } - - if (actual is AppEvent appEvent) - { - appEvent.AppId = NamedId.Of(DomainId.NewGuid(), "my-app"); - } + appEvent.AppId = NamedId.Of(DomainId.NewGuid(), "my-app"); + } - if (actual is SchemaEvent schemaEvent) - { - schemaEvent.SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - } + if (actual is SchemaEvent schemaEvent) + { + schemaEvent.SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + } - init?.Invoke(actual); + init?.Invoke(actual); - return actual; - } + return actual; } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TranslationsFixture.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TranslationsFixture.cs index 61daedd834..8a0a97a276 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TranslationsFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TranslationsFixture.cs @@ -8,13 +8,12 @@ using Squidex.Infrastructure.Translations; using Squidex.Shared; -namespace Squidex.Domain.Apps.Core.TestHelpers +namespace Squidex.Domain.Apps.Core.TestHelpers; + +public class TranslationsFixture { - public class TranslationsFixture + public TranslationsFixture() { - public TranslationsFixture() - { - T.Setup(new ResourcesLocalizer(Texts.ResourceManager)); - } + T.Setup(new ResourcesLocalizer(Texts.ResourceManager)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/UserMocks.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/UserMocks.cs index 860283f75c..f440767c3d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/UserMocks.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/UserMocks.cs @@ -10,36 +10,35 @@ using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Core.TestHelpers +namespace Squidex.Domain.Apps.Core.TestHelpers; + +public static class UserMocks { - public static class UserMocks + public static IUser User(string id, string? email = null, string? name = null, bool consent = false) { - public static IUser User(string id, string? email = null, string? name = null, bool consent = false) - { - var claims = new List<Claim>(); + var claims = new List<Claim>(); - if (!string.IsNullOrWhiteSpace(name)) - { - claims.Add(new Claim(SquidexClaimTypes.DisplayName, name)); - } + if (!string.IsNullOrWhiteSpace(name)) + { + claims.Add(new Claim(SquidexClaimTypes.DisplayName, name)); + } - if (consent) - { - claims.Add(new Claim(SquidexClaimTypes.Consent, "True")); - } + if (consent) + { + claims.Add(new Claim(SquidexClaimTypes.Consent, "True")); + } - var user = A.Fake<IUser>(); + var user = A.Fake<IUser>(); - A.CallTo(() => user.Id) - .Returns(id); + A.CallTo(() => user.Id) + .Returns(id); - A.CallTo(() => user.Email) - .Returns(email ?? id); + A.CallTo(() => user.Email) + .Returns(email ?? id); - A.CallTo(() => user.Claims) - .Returns(claims); + A.CallTo(() => user.Claims) + .Returns(claims); - return user; - } + return user; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs index fc168fbd4e..4296f090ca 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs @@ -12,182 +12,181 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public class AppProviderExtensionsTests { - public class AppProviderExtensionsTests + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly NamedId<DomainId> componentId1 = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly NamedId<DomainId> componentId2 = NamedId.Of(DomainId.NewGuid(), "my-schema"); + + [Fact] + public async Task Should_do_nothing_if_no_component_found() { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly NamedId<DomainId> componentId1 = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly NamedId<DomainId> componentId2 = NamedId.Of(DomainId.NewGuid(), "my-schema"); - - [Fact] - public async Task Should_do_nothing_if_no_component_found() - { - var schema = Mocks.Schema(appId, schemaId); - - var components = await appProvider.GetComponentsAsync(schema); - - Assert.Empty(components); - - A.CallTo(() => appProvider.GetSchemaAsync(A<DomainId>._, A<DomainId>._, false, A<CancellationToken>._)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_resolve_self_as_component() - { - var schema = - Mocks.Schema(appId, schemaId, - new Schema(schemaId.Name) - .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties - { - SchemaId = schemaId.Id - })); + var schema = Mocks.Schema(appId, schemaId); - var components = await appProvider.GetComponentsAsync(schema); + var components = await appProvider.GetComponentsAsync(schema); - Assert.Single(components); - Assert.Same(schema.SchemaDef, components[schemaId.Id]); + Assert.Empty(components); - A.CallTo(() => appProvider.GetSchemaAsync(A<DomainId>._, A<DomainId>._, false, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetSchemaAsync(A<DomainId>._, A<DomainId>._, false, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_resolve_from_component() - { - var component = Mocks.Schema(appId, componentId1); + [Fact] + public async Task Should_resolve_self_as_component() + { + var schema = + Mocks.Schema(appId, schemaId, + new Schema(schemaId.Name) + .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties + { + SchemaId = schemaId.Id + })); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) - .Returns(component); + var components = await appProvider.GetComponentsAsync(schema); - var schema = - Mocks.Schema(appId, schemaId, - new Schema(schemaId.Name) - .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties - { - SchemaId = componentId1.Id - })); + Assert.Single(components); + Assert.Same(schema.SchemaDef, components[schemaId.Id]); - var components = await appProvider.GetComponentsAsync(schema); + A.CallTo(() => appProvider.GetSchemaAsync(A<DomainId>._, A<DomainId>._, false, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - Assert.Single(components); - Assert.Same(component.SchemaDef, components[componentId1.Id]); - } + [Fact] + public async Task Should_resolve_from_component() + { + var component = Mocks.Schema(appId, componentId1); - [Fact] - public async Task Should_resolve_from_components() - { - var component = Mocks.Schema(appId, componentId1); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) + .Returns(component); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) - .Returns(component); + var schema = + Mocks.Schema(appId, schemaId, + new Schema(schemaId.Name) + .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties + { + SchemaId = componentId1.Id + })); - var schema = - Mocks.Schema(appId, schemaId, - new Schema(schemaId.Name) - .AddComponents(1, "1", Partitioning.Invariant, new ComponentsFieldProperties - { - SchemaId = componentId1.Id - })); - - var components = await appProvider.GetComponentsAsync(schema); - - Assert.Single(components); - Assert.Same(component.SchemaDef, components[componentId1.Id]); - } - - [Fact] - public async Task Should_resolve_from_array() - { - var component = Mocks.Schema(appId, componentId1); - - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) - .Returns(component); - - var schema = - Mocks.Schema(appId, schemaId, - new Schema(schemaId.Name) - .AddArray(1, "1", Partitioning.Invariant, a => a - .AddComponent(2, "2", new ComponentFieldProperties - { - SchemaId = componentId1.Id - }))); - - var components = await appProvider.GetComponentsAsync(schema); - - Assert.Single(components); - Assert.Same(component.SchemaDef, components[componentId1.Id]); - } - - [Fact] - public async Task Should_resolve_self_referencing_component() - { - var component = - Mocks.Schema(appId, componentId1, - new Schema(componentId1.Name) - .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties - { - SchemaId = componentId1.Id - })); + var components = await appProvider.GetComponentsAsync(schema); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) - .Returns(component); + Assert.Single(components); + Assert.Same(component.SchemaDef, components[componentId1.Id]); + } - var schema = - Mocks.Schema(appId, schemaId, - new Schema(schemaId.Name) - .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties - { - SchemaId = componentId1.Id - })); + [Fact] + public async Task Should_resolve_from_components() + { + var component = Mocks.Schema(appId, componentId1); - var components = await appProvider.GetComponentsAsync(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) + .Returns(component); - Assert.Single(components); - Assert.Same(component.SchemaDef, components[componentId1.Id]); - } + var schema = + Mocks.Schema(appId, schemaId, + new Schema(schemaId.Name) + .AddComponents(1, "1", Partitioning.Invariant, new ComponentsFieldProperties + { + SchemaId = componentId1.Id + })); - [Fact] - public async Task Should_resolve_component_of_component() - { - var component1 = - Mocks.Schema(appId, componentId1, - new Schema(componentId1.Name) - .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties - { - SchemaId = componentId2.Id - })); + var components = await appProvider.GetComponentsAsync(schema); - var component2 = - Mocks.Schema(appId, componentId2, - new Schema(componentId2.Name) - .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties - { - SchemaId = componentId2.Id - })); + Assert.Single(components); + Assert.Same(component.SchemaDef, components[componentId1.Id]); + } - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) - .Returns(component1); + [Fact] + public async Task Should_resolve_from_array() + { + var component = Mocks.Schema(appId, componentId1); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId2.Id, false, default)) - .Returns(component2); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) + .Returns(component); - var schema = - Mocks.Schema(appId, schemaId, - new Schema(schemaId.Name) - .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties + var schema = + Mocks.Schema(appId, schemaId, + new Schema(schemaId.Name) + .AddArray(1, "1", Partitioning.Invariant, a => a + .AddComponent(2, "2", new ComponentFieldProperties { SchemaId = componentId1.Id - })); + }))); - var components = await appProvider.GetComponentsAsync(schema); + var components = await appProvider.GetComponentsAsync(schema); - Assert.Equal(2, components.Count); - Assert.Same(component1.SchemaDef, components[componentId1.Id]); - Assert.Same(component2.SchemaDef, components[componentId2.Id]); - } + Assert.Single(components); + Assert.Same(component.SchemaDef, components[componentId1.Id]); + } + + [Fact] + public async Task Should_resolve_self_referencing_component() + { + var component = + Mocks.Schema(appId, componentId1, + new Schema(componentId1.Name) + .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties + { + SchemaId = componentId1.Id + })); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) + .Returns(component); + + var schema = + Mocks.Schema(appId, schemaId, + new Schema(schemaId.Name) + .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties + { + SchemaId = componentId1.Id + })); + + var components = await appProvider.GetComponentsAsync(schema); + + Assert.Single(components); + Assert.Same(component.SchemaDef, components[componentId1.Id]); + } + + [Fact] + public async Task Should_resolve_component_of_component() + { + var component1 = + Mocks.Schema(appId, componentId1, + new Schema(componentId1.Name) + .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties + { + SchemaId = componentId2.Id + })); + + var component2 = + Mocks.Schema(appId, componentId2, + new Schema(componentId2.Name) + .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties + { + SchemaId = componentId2.Id + })); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) + .Returns(component1); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId2.Id, false, default)) + .Returns(component2); + + var schema = + Mocks.Schema(appId, schemaId, + new Schema(schemaId.Name) + .AddComponent(1, "1", Partitioning.Invariant, new ComponentFieldProperties + { + SchemaId = componentId1.Id + })); + + var components = await appProvider.GetComponentsAsync(schema); + + Assert.Equal(2, components.Count); + Assert.Same(component1.SchemaDef, components[componentId1.Id]); + Assert.Same(component2.SchemaDef, components[componentId2.Id]); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs index 8cabce08f6..de5293904a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs @@ -20,183 +20,182 @@ using Squidex.Infrastructure.Security; using Xunit; -namespace Squidex.Domain.Apps.Entities +namespace Squidex.Domain.Apps.Entities; + +public class AppProviderTests { - public class AppProviderTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAppsIndex indexForApps = A.Fake<IAppsIndex>(); + private readonly IRulesIndex indexForRules = A.Fake<IRulesIndex>(); + private readonly ISchemasIndex indexForSchemas = A.Fake<ISchemasIndex>(); + private readonly ITeamsIndex indexForTeams = A.Fake<ITeamsIndex>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly IAppEntity app; + private readonly AppProvider sut; + + public AppProviderTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAppsIndex indexForApps = A.Fake<IAppsIndex>(); - private readonly IRulesIndex indexForRules = A.Fake<IRulesIndex>(); - private readonly ISchemasIndex indexForSchemas = A.Fake<ISchemasIndex>(); - private readonly ITeamsIndex indexForTeams = A.Fake<ITeamsIndex>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly IAppEntity app; - private readonly AppProvider sut; - - public AppProviderTests() - { - ct = cts.Token; + ct = cts.Token; - app = Mocks.App(appId); + app = Mocks.App(appId); - sut = new AppProvider(indexForApps, indexForRules, indexForSchemas, indexForTeams, new AsyncLocalCache()); - } + sut = new AppProvider(indexForApps, indexForRules, indexForSchemas, indexForTeams, new AsyncLocalCache()); + } - [Fact] - public async Task Should_get_app_with_schema_from_index() - { - var schema = Mocks.Schema(app.NamedId(), schemaId); + [Fact] + public async Task Should_get_app_with_schema_from_index() + { + var schema = Mocks.Schema(app.NamedId(), schemaId); - A.CallTo(() => indexForApps.GetAppAsync(app.Id, false, ct)) - .Returns(app); + A.CallTo(() => indexForApps.GetAppAsync(app.Id, false, ct)) + .Returns(app); - A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schema.Id, false, ct)) - .Returns(schema); + A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schema.Id, false, ct)) + .Returns(schema); - var actual = await sut.GetAppWithSchemaAsync(app.Id, schemaId.Id, false, ct); + var actual = await sut.GetAppWithSchemaAsync(app.Id, schemaId.Id, false, ct); - Assert.Equal(schema, actual.Item2); - } + Assert.Equal(schema, actual.Item2); + } - [Fact] - public async Task Should_get_team_apps_from_index() - { - var team = Mocks.Team(DomainId.NewGuid()); + [Fact] + public async Task Should_get_team_apps_from_index() + { + var team = Mocks.Team(DomainId.NewGuid()); - A.CallTo(() => indexForApps.GetAppsForTeamAsync(team.Id, ct)) - .Returns(new List<IAppEntity> { app }); + A.CallTo(() => indexForApps.GetAppsForTeamAsync(team.Id, ct)) + .Returns(new List<IAppEntity> { app }); - var actual = await sut.GetTeamAppsAsync(team.Id, ct); + var actual = await sut.GetTeamAppsAsync(team.Id, ct); - Assert.Equal(app, actual.Single()); - } + Assert.Equal(app, actual.Single()); + } - [Fact] - public async Task Should_get_apps_from_index() - { - var permissions = new PermissionSet("*"); + [Fact] + public async Task Should_get_apps_from_index() + { + var permissions = new PermissionSet("*"); - A.CallTo(() => indexForApps.GetAppsForUserAsync("user1", permissions, ct)) - .Returns(new List<IAppEntity> { app }); + A.CallTo(() => indexForApps.GetAppsForUserAsync("user1", permissions, ct)) + .Returns(new List<IAppEntity> { app }); - var actual = await sut.GetUserAppsAsync("user1", permissions, ct); + var actual = await sut.GetUserAppsAsync("user1", permissions, ct); - Assert.Equal(app, actual.Single()); - } + Assert.Equal(app, actual.Single()); + } - [Fact] - public async Task Should_get_app_from_index() - { - A.CallTo(() => indexForApps.GetAppAsync(app.Id, false, ct)) - .Returns(app); + [Fact] + public async Task Should_get_app_from_index() + { + A.CallTo(() => indexForApps.GetAppAsync(app.Id, false, ct)) + .Returns(app); - var actual = await sut.GetAppAsync(app.Id, false, ct); + var actual = await sut.GetAppAsync(app.Id, false, ct); - Assert.Equal(app, actual); - } + Assert.Equal(app, actual); + } - [Fact] - public async Task Should_get_app_by_name_from_index() - { - A.CallTo(() => indexForApps.GetAppAsync(app.Name, false, ct)) - .Returns(app); + [Fact] + public async Task Should_get_app_by_name_from_index() + { + A.CallTo(() => indexForApps.GetAppAsync(app.Name, false, ct)) + .Returns(app); - var actual = await sut.GetAppAsync(app.Name, false, ct); + var actual = await sut.GetAppAsync(app.Name, false, ct); - Assert.Equal(app, actual); - } + Assert.Equal(app, actual); + } - [Fact] - public async Task Should_get_team_from_index() - { - var team = Mocks.Team(DomainId.NewGuid()); + [Fact] + public async Task Should_get_team_from_index() + { + var team = Mocks.Team(DomainId.NewGuid()); - A.CallTo(() => indexForTeams.GetTeamAsync(team.Id, ct)) - .Returns(team); + A.CallTo(() => indexForTeams.GetTeamAsync(team.Id, ct)) + .Returns(team); - var actual = await sut.GetTeamAsync(team.Id, ct); + var actual = await sut.GetTeamAsync(team.Id, ct); - Assert.Equal(team, actual); - } + Assert.Equal(team, actual); + } - [Fact] - public async Task Should_get_teams_from_index() - { - var team = Mocks.Team(DomainId.NewGuid()); + [Fact] + public async Task Should_get_teams_from_index() + { + var team = Mocks.Team(DomainId.NewGuid()); - A.CallTo(() => indexForTeams.GetTeamsAsync("user1", ct)) - .Returns(new List<ITeamEntity> { team }); + A.CallTo(() => indexForTeams.GetTeamsAsync("user1", ct)) + .Returns(new List<ITeamEntity> { team }); - var actual = await sut.GetUserTeamsAsync("user1", ct); + var actual = await sut.GetUserTeamsAsync("user1", ct); - Assert.Equal(team, actual.Single()); - } + Assert.Equal(team, actual.Single()); + } - [Fact] - public async Task Should_get_schema_from_index() - { - var schema = Mocks.Schema(app.NamedId(), schemaId); + [Fact] + public async Task Should_get_schema_from_index() + { + var schema = Mocks.Schema(app.NamedId(), schemaId); - A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schema.Id, false, ct)) - .Returns(schema); + A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schema.Id, false, ct)) + .Returns(schema); - var actual = await sut.GetSchemaAsync(app.Id, schema.Id, false, ct); + var actual = await sut.GetSchemaAsync(app.Id, schema.Id, false, ct); - Assert.Equal(schema, actual); - } + Assert.Equal(schema, actual); + } - [Fact] - public async Task Should_get_schema_by_name_from_index() - { - var schema = Mocks.Schema(app.NamedId(), schemaId); + [Fact] + public async Task Should_get_schema_by_name_from_index() + { + var schema = Mocks.Schema(app.NamedId(), schemaId); - A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schemaId.Name, false, ct)) - .Returns(schema); + A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schemaId.Name, false, ct)) + .Returns(schema); - var actual = await sut.GetSchemaAsync(app.Id, schemaId.Name, false, ct); + var actual = await sut.GetSchemaAsync(app.Id, schemaId.Name, false, ct); - Assert.Equal(schema, actual); - } + Assert.Equal(schema, actual); + } - [Fact] - public async Task Should_get_schemas_from_index() - { - var schema = Mocks.Schema(app.NamedId(), schemaId); + [Fact] + public async Task Should_get_schemas_from_index() + { + var schema = Mocks.Schema(app.NamedId(), schemaId); - A.CallTo(() => indexForSchemas.GetSchemasAsync(app.Id, ct)) - .Returns(new List<ISchemaEntity> { schema }); + A.CallTo(() => indexForSchemas.GetSchemasAsync(app.Id, ct)) + .Returns(new List<ISchemaEntity> { schema }); - var actual = await sut.GetSchemasAsync(app.Id, ct); + var actual = await sut.GetSchemasAsync(app.Id, ct); - Assert.Equal(schema, actual.Single()); - } + Assert.Equal(schema, actual.Single()); + } - [Fact] - public async Task Should_get_rules_from_index() - { - var rule = new RuleEntity(); + [Fact] + public async Task Should_get_rules_from_index() + { + var rule = new RuleEntity(); - A.CallTo(() => indexForRules.GetRulesAsync(app.Id, ct)) - .Returns(new List<IRuleEntity> { rule }); + A.CallTo(() => indexForRules.GetRulesAsync(app.Id, ct)) + .Returns(new List<IRuleEntity> { rule }); - var actual = await sut.GetRulesAsync(app.Id, ct); + var actual = await sut.GetRulesAsync(app.Id, ct); - Assert.Equal(rule, actual.Single()); - } + Assert.Equal(rule, actual.Single()); + } - [Fact] - public async Task Should_get_rule_from_index() - { - var rule = new RuleEntity { Id = DomainId.NewGuid() }; + [Fact] + public async Task Should_get_rule_from_index() + { + var rule = new RuleEntity { Id = DomainId.NewGuid() }; - A.CallTo(() => indexForRules.GetRulesAsync(app.Id, ct)) - .Returns(new List<IRuleEntity> { rule }); + A.CallTo(() => indexForRules.GetRulesAsync(app.Id, ct)) + .Returns(new List<IRuleEntity> { rule }); - var actual = await sut.GetRuleAsync(app.Id, rule.Id, ct); + var actual = await sut.GetRuleAsync(app.Id, rule.Id, ct); - Assert.Equal(rule, actual); - } + Assert.Equal(rule, actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AlwaysCreateClientCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AlwaysCreateClientCommandMiddlewareTests.cs index 3e1999ea6a..59cd36ed47 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AlwaysCreateClientCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AlwaysCreateClientCommandMiddlewareTests.cs @@ -11,27 +11,26 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public class AlwaysCreateClientCommandMiddlewareTests { - public class AlwaysCreateClientCommandMiddlewareTests - { - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - [Fact] - public async Task Should_create_client() - { - var command = new CreateApp { AppId = DomainId.NewGuid(), Name = "my-app" }; + [Fact] + public async Task Should_create_client() + { + var command = new CreateApp { AppId = DomainId.NewGuid(), Name = "my-app" }; - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - var sut = new AlwaysCreateClientCommandMiddleware(); + var sut = new AlwaysCreateClientCommandMiddleware(); - await sut.HandleAsync(context, default); + await sut.HandleAsync(context, default); - A.CallTo(() => commandBus.PublishAsync(A<AttachClient>.That.Matches(x => x.Id == "default"), default)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<AttachClient>.That.Matches(x => x.Id == "default"), default)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleterTests.cs index f9fffee6ac..a4570ef1fe 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleterTests.cs @@ -11,39 +11,38 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public class AppEventDeleterTests { - public class AppEventDeleterTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly AppEventDeleter sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly AppEventDeleter sut; - public AppEventDeleterTests() - { - ct = cts.Token; + public AppEventDeleterTests() + { + ct = cts.Token; - sut = new AppEventDeleter(eventStore); - } + sut = new AppEventDeleter(eventStore); + } - [Fact] - public void Should_run_last() - { - var order = sut.Order; + [Fact] + public void Should_run_last() + { + var order = sut.Order; - Assert.Equal(int.MaxValue, order); - } + Assert.Equal(int.MaxValue, order); + } - [Fact] - public async Task Should_remove_events_from_streams() - { - var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + [Fact] + public async Task Should_remove_events_from_streams() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - await sut.DeleteAppAsync(app, ct); + await sut.DeleteAppAsync(app, ct); - A.CallTo(() => eventStore.DeleteAsync($"^[a-zA-Z0-9]-{app.Id}", A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => eventStore.DeleteAsync($"^[a-zA-Z0-9]-{app.Id}", A<CancellationToken>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs index 5da66ca15a..3b769f1c5e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs @@ -15,122 +15,121 @@ using Squidex.Infrastructure.Reflection; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public class AppPermanentDeleterTests { - public class AppPermanentDeleterTests + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly IDeleter deleter1 = A.Fake<IDeleter>(); + private readonly IDeleter deleter2 = A.Fake<IDeleter>(); + private readonly TypeNameRegistry typeNameRegistry; + private readonly AppPermanentDeleter sut; + + public AppPermanentDeleterTests() { - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly IDeleter deleter1 = A.Fake<IDeleter>(); - private readonly IDeleter deleter2 = A.Fake<IDeleter>(); - private readonly TypeNameRegistry typeNameRegistry; - private readonly AppPermanentDeleter sut; + typeNameRegistry = + new TypeNameRegistry() + .Map(typeof(AppCreated)) + .Map(typeof(AppContributorRemoved)) + .Map(typeof(AppDeleted)); - public AppPermanentDeleterTests() - { - typeNameRegistry = - new TypeNameRegistry() - .Map(typeof(AppCreated)) - .Map(typeof(AppContributorRemoved)) - .Map(typeof(AppDeleted)); + sut = new AppPermanentDeleter(new[] { deleter1, deleter2 }, domainObjectFactory, typeNameRegistry); + } - sut = new AppPermanentDeleter(new[] { deleter1, deleter2 }, domainObjectFactory, typeNameRegistry); - } + [Fact] + public void Should_return_assets_filter_for_events_filter() + { + IEventConsumer consumer = sut; - [Fact] - public void Should_return_assets_filter_for_events_filter() - { - IEventConsumer consumer = sut; + Assert.Equal("^app-", consumer.EventsFilter); + } - Assert.Equal("^app-", consumer.EventsFilter); - } + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; - [Fact] - public async Task Should_do_nothing_on_clear() - { - IEventConsumer consumer = sut; + await consumer.ClearAsync(); + } - await consumer.ClearAsync(); - } + [Fact] + public void Should_return_type_name_for_name() + { + IEventConsumer consumer = sut; - [Fact] - public void Should_return_type_name_for_name() - { - IEventConsumer consumer = sut; + Assert.Equal(nameof(AppPermanentDeleter), consumer.Name); + } - Assert.Equal(nameof(AppPermanentDeleter), consumer.Name); - } + [Fact] + public void Should_handle_delete_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName<AppDeleted>(), new EnvelopeHeaders(), "payload")); - [Fact] - public void Should_handle_delete_event() - { - var storedEvent = - new StoredEvent("stream", "1", 1, - new EventData(typeNameRegistry.GetName<AppDeleted>(), new EnvelopeHeaders(), "payload")); + Assert.True(sut.Handles(storedEvent)); + } - Assert.True(sut.Handles(storedEvent)); - } + [Fact] + public void Should_handle_contributor_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName<AppContributorRemoved>(), new EnvelopeHeaders(), "payload")); - [Fact] - public void Should_handle_contributor_event() - { - var storedEvent = - new StoredEvent("stream", "1", 1, - new EventData(typeNameRegistry.GetName<AppContributorRemoved>(), new EnvelopeHeaders(), "payload")); + Assert.True(sut.Handles(storedEvent)); + } - Assert.True(sut.Handles(storedEvent)); - } + [Fact] + public void Should_not_handle_creation_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName<AppCreated>(), new EnvelopeHeaders(), "payload")); - [Fact] - public void Should_not_handle_creation_event() - { - var storedEvent = - new StoredEvent("stream", "1", 1, - new EventData(typeNameRegistry.GetName<AppCreated>(), new EnvelopeHeaders(), "payload")); + Assert.False(sut.Handles(storedEvent)); + } - Assert.False(sut.Handles(storedEvent)); - } + [Fact] + public async Task Should_call_deleters_when_contributor_removed() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - [Fact] - public async Task Should_call_deleters_when_contributor_removed() + await sut.On(Envelope.Create(new AppContributorRemoved { - var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + AppId = app.NamedId(), + ContributorId = "user1" + })); - await sut.On(Envelope.Create(new AppContributorRemoved - { - AppId = app.NamedId(), - ContributorId = "user1" - })); + A.CallTo(() => deleter1.DeleteContributorAsync(app.Id, "user1", default)) + .MustHaveHappened(); - A.CallTo(() => deleter1.DeleteContributorAsync(app.Id, "user1", default)) - .MustHaveHappened(); - - A.CallTo(() => deleter2.DeleteContributorAsync(app.Id, "user1", default)) - .MustHaveHappened(); - } + A.CallTo(() => deleter2.DeleteContributorAsync(app.Id, "user1", default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_call_deleters_when_app_deleted() - { - var app = new AppDomainObject.State { Id = DomainId.NewGuid(), Name = "my-app" }; + [Fact] + public async Task Should_call_deleters_when_app_deleted() + { + var app = new AppDomainObject.State { Id = DomainId.NewGuid(), Name = "my-app" }; - var domainObject = A.Fake<AppDomainObject>(); + var domainObject = A.Fake<AppDomainObject>(); - A.CallTo(() => domainObject.Snapshot) - .Returns(app); + A.CallTo(() => domainObject.Snapshot) + .Returns(app); - A.CallTo(() => domainObjectFactory.Create<AppDomainObject>(app.Id)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<AppDomainObject>(app.Id)) + .Returns(domainObject); - await sut.On(Envelope.Create(new AppDeleted - { - AppId = app.NamedId() - })); + await sut.On(Envelope.Create(new AppDeleted + { + AppId = app.NamedId() + })); - A.CallTo(() => deleter1.DeleteAppAsync(app, default)) - .MustHaveHappened(); + A.CallTo(() => deleter1.DeleteAppAsync(app, default)) + .MustHaveHappened(); - A.CallTo(() => deleter2.DeleteAppAsync(app, default)) - .MustHaveHappened(); - } + A.CallTo(() => deleter2.DeleteAppAsync(app, default)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs index 66f3bb30fb..1c1af8fcff 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs @@ -16,347 +16,346 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppSettingsSearchSourceTests { - public sealed class AppSettingsSearchSourceTests + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly AppSettingsSearchSource sut; + + public AppSettingsSearchSourceTests() { - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly AppSettingsSearchSource sut; + sut = new AppSettingsSearchSource(urlGenerator); + } - public AppSettingsSearchSourceTests() - { - sut = new AppSettingsSearchSource(urlGenerator); - } + [Fact] + public async Task Should_return_empty_if_nothing_matching() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_return_empty_if_nothing_matching() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("xyz", ctx, default); - var actual = await sut.SearchAsync("xyz", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_dashboard_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppUsage, appId.Name); - [Fact] - public async Task Should_return_dashboard_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppUsage, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.DashboardUI(appId)) + .Returns("dashboard-url"); - A.CallTo(() => urlGenerator.DashboardUI(appId)) - .Returns("dashboard-url"); + var actual = await sut.SearchAsync("dashboard", ctx, default); - var actual = await sut.SearchAsync("dashboard", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Dashboard", SearchResultType.Dashboard, "dashboard-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Dashboard", SearchResultType.Dashboard, "dashboard-url")); - } + [Fact] + public async Task Should_not_return_dashboard_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_dashboard_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("assets", ctx, default); - var actual = await sut.SearchAsync("assets", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_languages_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppLanguagesRead, appId.Name); - [Fact] - public async Task Should_return_languages_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppLanguagesRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.LanguagesUI(appId)) + .Returns("languages-url"); - A.CallTo(() => urlGenerator.LanguagesUI(appId)) - .Returns("languages-url"); + var actual = await sut.SearchAsync("languages", ctx, default); - var actual = await sut.SearchAsync("languages", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Languages", SearchResultType.Setting, "languages-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Languages", SearchResultType.Setting, "languages-url")); - } + [Fact] + public async Task Should_not_return_languages_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_languages_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("assets", ctx, default); - var actual = await sut.SearchAsync("assets", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_not_return_patterns_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_patterns_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("patterns", ctx, default); - var actual = await sut.SearchAsync("patterns", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_schemas_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppSchemasRead, appId.Name); - [Fact] - public async Task Should_return_schemas_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppSchemasRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.SchemasUI(appId)) + .Returns("schemas-url"); - A.CallTo(() => urlGenerator.SchemasUI(appId)) - .Returns("schemas-url"); + var actual = await sut.SearchAsync("schemas", ctx, default); - var actual = await sut.SearchAsync("schemas", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Schemas", SearchResultType.Schema, "schemas-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Schemas", SearchResultType.Schema, "schemas-url")); - } + [Fact] + public async Task Should_not_return_schemas_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_schemas_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("schemas", ctx, default); - var actual = await sut.SearchAsync("schemas", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_assets_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appId.Name); - [Fact] - public async Task Should_return_assets_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.AssetsUI(appId, A<string?>._)) + .Returns("assets-url"); - A.CallTo(() => urlGenerator.AssetsUI(appId, A<string?>._)) - .Returns("assets-url"); + var actual = await sut.SearchAsync("assets", ctx, default); - var actual = await sut.SearchAsync("assets", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Assets", SearchResultType.Asset, "assets-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Assets", SearchResultType.Asset, "assets-url")); - } + [Fact] + public async Task Should_not_return_assets_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_assets_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("assets", ctx, default); - var actual = await sut.SearchAsync("assets", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_backups_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppBackupsRead, appId.Name); - [Fact] - public async Task Should_return_backups_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppBackupsRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.BackupsUI(appId)) + .Returns("backups-url"); - A.CallTo(() => urlGenerator.BackupsUI(appId)) - .Returns("backups-url"); + var actual = await sut.SearchAsync("backups", ctx, default); - var actual = await sut.SearchAsync("backups", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Backups", SearchResultType.Setting, "backups-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Backups", SearchResultType.Setting, "backups-url")); - } + [Fact] + public async Task Should_not_return_backups_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_backups_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("backups", ctx, default); - var actual = await sut.SearchAsync("backups", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_clients_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppClientsRead, appId.Name); - [Fact] - public async Task Should_return_clients_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppClientsRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.ClientsUI(appId)) + .Returns("clients-url"); - A.CallTo(() => urlGenerator.ClientsUI(appId)) - .Returns("clients-url"); + var actual = await sut.SearchAsync("clients", ctx, default); - var actual = await sut.SearchAsync("clients", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Clients", SearchResultType.Setting, "clients-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Clients", SearchResultType.Setting, "clients-url")); - } + [Fact] + public async Task Should_not_return_clients_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_clients_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("clients", ctx, default); - var actual = await sut.SearchAsync("clients", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_contributors_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppContributorsRead, appId.Name); - [Fact] - public async Task Should_return_contributors_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppContributorsRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.ContributorsUI(appId)) + .Returns("contributors-url"); - A.CallTo(() => urlGenerator.ContributorsUI(appId)) - .Returns("contributors-url"); + var actual = await sut.SearchAsync("contributors", ctx, default); - var actual = await sut.SearchAsync("contributors", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Contributors", SearchResultType.Setting, "contributors-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Contributors", SearchResultType.Setting, "contributors-url")); - } + [Fact] + public async Task Should_not_contributors_clients_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_contributors_clients_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("contributors", ctx, default); - var actual = await sut.SearchAsync("contributors", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_subscription_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppPlansRead, appId.Name); - [Fact] - public async Task Should_return_subscription_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppPlansRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.PlansUI(appId)) + .Returns("subscription-url"); - A.CallTo(() => urlGenerator.PlansUI(appId)) - .Returns("subscription-url"); + var actual = await sut.SearchAsync("subscription", ctx, default); - var actual = await sut.SearchAsync("subscription", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Subscription", SearchResultType.Setting, "subscription-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Subscription", SearchResultType.Setting, "subscription-url")); - } + [Fact] + public async Task Should_not_subscription_clients_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_subscription_clients_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("subscription", ctx, default); - var actual = await sut.SearchAsync("subscription", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_roles_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppRolesRead, appId.Name); - [Fact] - public async Task Should_return_roles_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppRolesRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.RolesUI(appId)) + .Returns("roles-url"); - A.CallTo(() => urlGenerator.RolesUI(appId)) - .Returns("roles-url"); + var actual = await sut.SearchAsync("roles", ctx, default); - var actual = await sut.SearchAsync("roles", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Roles", SearchResultType.Setting, "roles-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Roles", SearchResultType.Setting, "roles-url")); - } + [Fact] + public async Task Should_not_roles_clients_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_roles_clients_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("roles", ctx, default); - var actual = await sut.SearchAsync("roles", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_rules_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppRulesRead, appId.Name); - [Fact] - public async Task Should_return_rules_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppRulesRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.RulesUI(appId)) + .Returns("rules-url"); - A.CallTo(() => urlGenerator.RulesUI(appId)) - .Returns("rules-url"); + var actual = await sut.SearchAsync("rules", ctx, default); - var actual = await sut.SearchAsync("rules", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Rules", SearchResultType.Rule, "rules-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Rules", SearchResultType.Rule, "rules-url")); - } + [Fact] + public async Task Should_not_return_rules_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_rules_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("assets", ctx, default); - var actual = await sut.SearchAsync("assets", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + [Fact] + public async Task Should_return_workflows_actual_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppWorkflowsRead, appId.Name); - [Fact] - public async Task Should_return_workflows_actual_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppWorkflowsRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + A.CallTo(() => urlGenerator.WorkflowsUI(appId)) + .Returns("workflows-url"); - A.CallTo(() => urlGenerator.WorkflowsUI(appId)) - .Returns("workflows-url"); + var actual = await sut.SearchAsync("workflows", ctx, default); - var actual = await sut.SearchAsync("workflows", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Workflows", SearchResultType.Setting, "workflows-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Workflows", SearchResultType.Setting, "workflows-url")); - } + [Fact] + public async Task Should_not_return_workflows_actual_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_return_workflows_actual_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("workflows", ctx, default); - var actual = await sut.SearchAsync("workflows", ctx, default); + Assert.Empty(actual); + } - Assert.Empty(actual); - } + private Context ContextWithPermission(string? permission = null) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - private Context ContextWithPermission(string? permission = null) + if (permission != null) { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - - if (permission != null) - { - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); - } - - return new Context(claimsPrincipal, Mocks.App(appId)); + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); } + + return new Context(claimsPrincipal, Mocks.App(appId)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs index ef9ab078c9..5000bcf40a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs @@ -14,193 +14,192 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppUISettingsTests { - public sealed class AppUISettingsTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState<AppUISettings.State> state; + private readonly DomainId appId = DomainId.NewGuid(); + private readonly string userId = Guid.NewGuid().ToString(); + private readonly string stateId; + private readonly AppUISettings sut; + + public AppUISettingsTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly TestState<AppUISettings.State> state; - private readonly DomainId appId = DomainId.NewGuid(); - private readonly string userId = Guid.NewGuid().ToString(); - private readonly string stateId; - private readonly AppUISettings sut; - - public AppUISettingsTests() - { - ct = cts.Token; + ct = cts.Token; - stateId = $"{appId}_{userId}"; - state = new TestState<AppUISettings.State>(stateId); + stateId = $"{appId}_{userId}"; + state = new TestState<AppUISettings.State>(stateId); - sut = new AppUISettings(state.PersistenceFactory); - } + sut = new AppUISettings(state.PersistenceFactory); + } - [Fact] - public void Should_run_with_default_order() - { - var order = ((IDeleter)sut).Order; + [Fact] + public void Should_run_with_default_order() + { + var order = ((IDeleter)sut).Order; - Assert.Equal(0, order); - } + Assert.Equal(0, order); + } - [Fact] - public async Task Should_delete_contributor_state() - { - await ((IDeleter)sut).DeleteContributorAsync(appId, userId, ct); + [Fact] + public async Task Should_delete_contributor_state() + { + await ((IDeleter)sut).DeleteContributorAsync(appId, userId, ct); - A.CallTo(() => state.Persistence.DeleteAsync(ct)) - .MustHaveHappened(); - } + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_delete_app_and_contributors() - { - var app = Mocks.App(NamedId.Of(appId, "my-app")); + [Fact] + public async Task Should_delete_app_and_contributors() + { + var app = Mocks.App(NamedId.Of(appId, "my-app")); - A.CallTo(() => app.Contributors) - .Returns(Contributors.Empty.Assign(userId, Role.Owner)); + A.CallTo(() => app.Contributors) + .Returns(Contributors.Empty.Assign(userId, Role.Owner)); - var rootState = new TestState<AppUISettings.State>(appId, state.PersistenceFactory); + var rootState = new TestState<AppUISettings.State>(appId, state.PersistenceFactory); - await ((IDeleter)sut).DeleteAppAsync(app, ct); + await ((IDeleter)sut).DeleteAppAsync(app, ct); - A.CallTo(() => state.Persistence.DeleteAsync(ct)) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); - A.CallTo(() => rootState.Persistence.DeleteAsync(ct)) - .MustHaveHappened(); - } + A.CallTo(() => rootState.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_set_setting() - { - await sut.SetAsync(appId, userId, new JsonObject().Add("key", 42), ct); + [Fact] + public async Task Should_set_setting() + { + await sut.SetAsync(appId, userId, new JsonObject().Add("key", 42), ct); - var actual = await sut.GetAsync(appId, userId, ct); + var actual = await sut.GetAsync(appId, userId, ct); - var expected = - new JsonObject().Add("key", 42); + var expected = + new JsonObject().Add("key", 42); - Assert.Equal(expected.ToString(), actual.ToString()); + Assert.Equal(expected.ToString(), actual.ToString()); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) - .MustHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_set_root_value() - { - await sut.SetAsync(appId, userId, "key", 42, ct); + [Fact] + public async Task Should_set_root_value() + { + await sut.SetAsync(appId, userId, "key", 42, ct); - var actual = await sut.GetAsync(appId, userId, ct); + var actual = await sut.GetAsync(appId, userId, ct); - var expected = - new JsonObject().Add("key", 42); + var expected = + new JsonObject().Add("key", 42); - Assert.Equal(expected.ToString(), actual.ToString()); + Assert.Equal(expected.ToString(), actual.ToString()); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) - .MustHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_remove_root_value() - { - await sut.SetAsync(appId, userId, "key", 42, ct); + [Fact] + public async Task Should_remove_root_value() + { + await sut.SetAsync(appId, userId, "key", 42, ct); - await sut.RemoveAsync(appId, userId, "key", ct); - await sut.RemoveAsync(appId, userId, "key", ct); + await sut.RemoveAsync(appId, userId, "key", ct); + await sut.RemoveAsync(appId, userId, "key", ct); - var actual = await sut.GetAsync(appId, userId, ct); + var actual = await sut.GetAsync(appId, userId, ct); - var expected = new JsonObject(); + var expected = new JsonObject(); - Assert.Equal(expected.ToString(), actual.ToString()); + Assert.Equal(expected.ToString(), actual.ToString()); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) - .MustHaveHappenedTwiceExactly(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) + .MustHaveHappenedTwiceExactly(); + } - [Fact] - public async Task Should_set_nested_value() - { - await sut.SetAsync(appId, userId, "root.nested", 42, ct); + [Fact] + public async Task Should_set_nested_value() + { + await sut.SetAsync(appId, userId, "root.nested", 42, ct); - var actual = await sut.GetAsync(appId, userId, ct); + var actual = await sut.GetAsync(appId, userId, ct); - var expected = - new JsonObject().Add("root", - new JsonObject().Add("nested", 42)); + var expected = + new JsonObject().Add("root", + new JsonObject().Add("nested", 42)); - Assert.Equal(expected.ToString(), actual.ToString()); + Assert.Equal(expected.ToString(), actual.ToString()); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) - .MustHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_write_state_if_value_not_changed() - { - await sut.SetAsync(appId, userId, "root.nested", 42, ct); - await sut.SetAsync(appId, userId, "root.nested", 42, ct); + [Fact] + public async Task Should_not_write_state_if_value_not_changed() + { + await sut.SetAsync(appId, userId, "root.nested", 42, ct); + await sut.SetAsync(appId, userId, "root.nested", 42, ct); - var actual = await sut.GetAsync(appId, userId, ct); + var actual = await sut.GetAsync(appId, userId, ct); - var expected = - new JsonObject().Add("root", - new JsonObject().Add("nested", 42)); + var expected = + new JsonObject().Add("root", + new JsonObject().Add("nested", 42)); - Assert.Equal(expected.ToString(), actual.ToString()); + Assert.Equal(expected.ToString(), actual.ToString()); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_remove_nested_value() - { - await sut.SetAsync(appId, userId, "root.nested", 42, ct); + [Fact] + public async Task Should_remove_nested_value() + { + await sut.SetAsync(appId, userId, "root.nested", 42, ct); - await sut.RemoveAsync(appId, userId, "root.nested", ct); - await sut.RemoveAsync(appId, userId, "key", ct); + await sut.RemoveAsync(appId, userId, "root.nested", ct); + await sut.RemoveAsync(appId, userId, "key", ct); - var actual = await sut.GetAsync(appId, userId, ct); + var actual = await sut.GetAsync(appId, userId, ct); - var expected = - new JsonObject().Add("root", - new JsonObject()); + var expected = + new JsonObject().Add("root", + new JsonObject()); - Assert.Equal(expected.ToString(), actual.ToString()); + Assert.Equal(expected.ToString(), actual.ToString()); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) - .MustHaveHappenedTwiceExactly(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, ct)) + .MustHaveHappenedTwiceExactly(); + } - [Fact] - public async Task Should_throw_exception_if_nested_not_an_object() - { - await sut.SetAsync(appId, userId, "root.nested", 42, ct); + [Fact] + public async Task Should_throw_exception_if_nested_not_an_object() + { + await sut.SetAsync(appId, userId, "root.nested", 42, ct); - await Assert.ThrowsAsync<InvalidOperationException>(() => sut.SetAsync(appId, userId, "root.nested.value", 42, ct)); - } + await Assert.ThrowsAsync<InvalidOperationException>(() => sut.SetAsync(appId, userId, "root.nested.value", 42, ct)); + } - [Fact] - public async Task Should_do_nothing_if_deleting_and_nested_not_found() - { - await sut.RemoveAsync(appId, userId, "root.nested", ct); + [Fact] + public async Task Should_do_nothing_if_deleting_and_nested_not_found() + { + await sut.RemoveAsync(appId, userId, "root.nested", ct); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_do_nothing_if_deleting_and_key_not_found() - { - await sut.RemoveAsync(appId, userId, "root", ct); + [Fact] + public async Task Should_do_nothing_if_deleting_and_key_not_found() + { + await sut.RemoveAsync(appId, userId, "root", ct); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<AppUISettings.State>._, A<CancellationToken>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUsageDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUsageDeleterTests.cs index 85cda0f1fa..ba0f2fe3d7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUsageDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUsageDeleterTests.cs @@ -11,39 +11,38 @@ using Squidex.Infrastructure.UsageTracking; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public class AppUsageDeleterTests { - public class AppUsageDeleterTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IApiUsageTracker usageTracker = A.Fake<IApiUsageTracker>(); - private readonly AppUsageDeleter sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IApiUsageTracker usageTracker = A.Fake<IApiUsageTracker>(); + private readonly AppUsageDeleter sut; - public AppUsageDeleterTests() - { - ct = cts.Token; + public AppUsageDeleterTests() + { + ct = cts.Token; - sut = new AppUsageDeleter(usageTracker); - } + sut = new AppUsageDeleter(usageTracker); + } - [Fact] - public void Should_run_with_default_order() - { - var order = ((IDeleter)sut).Order; + [Fact] + public void Should_run_with_default_order() + { + var order = ((IDeleter)sut).Order; - Assert.Equal(0, order); - } + Assert.Equal(0, order); + } - [Fact] - public async Task Should_remove_events_from_streams() - { - var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + [Fact] + public async Task Should_remove_events_from_streams() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - await sut.DeleteAppAsync(app, ct); + await sut.DeleteAppAsync(app, ct); - A.CallTo(() => usageTracker.DeleteAsync(app.Id.ToString(), ct)) - .MustHaveHappened(); - } + A.CallTo(() => usageTracker.DeleteAsync(app.Id.ToString(), ct)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs index 4b99d3ee23..aa1bc1e67f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs @@ -18,312 +18,311 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public class BackupAppsTests { - public class BackupAppsTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); + private readonly IAppsIndex appsIndex = A.Fake<IAppsIndex>(); + private readonly IAppUISettings appUISettings = A.Fake<IAppUISettings>(); + private readonly IAppImageStore appImageStore = A.Fake<IAppImageStore>(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly RefToken actor = RefToken.User("123"); + private readonly BackupApps sut; + + public BackupAppsTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); - private readonly IAppsIndex appsIndex = A.Fake<IAppsIndex>(); - private readonly IAppUISettings appUISettings = A.Fake<IAppUISettings>(); - private readonly IAppImageStore appImageStore = A.Fake<IAppImageStore>(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly RefToken actor = RefToken.User("123"); - private readonly BackupApps sut; - - public BackupAppsTests() - { - ct = cts.Token; + ct = cts.Token; - sut = new BackupApps(rebuilder, appImageStore, appsIndex, appUISettings); - } + sut = new BackupApps(rebuilder, appImageStore, appsIndex, appUISettings); + } - [Fact] - public void Should_provide_name() - { - Assert.Equal("Apps", sut.Name); - } + [Fact] + public void Should_provide_name() + { + Assert.Equal("Apps", sut.Name); + } + + [Fact] + public async Task Should_reserve_app_name() + { + const string appName = "my-app"; - [Fact] - public async Task Should_reserve_app_name() + var context = CreateRestoreContext(); + + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, A<CancellationToken>._)) + .Returns("Reservation"); + + await sut.RestoreEventAsync(Envelope.Create(new AppCreated { - const string appName = "my-app"; + Name = appName + }), context, ct); - var context = CreateRestoreContext(); + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, A<CancellationToken>._)) + .MustHaveHappened(); + } - A.CallTo(() => appsIndex.ReserveAsync(appId, appName, A<CancellationToken>._)) - .Returns("Reservation"); + [Fact] + public async Task Should_complete_reservation_with_previous_token() + { + const string appName = "my-app"; - await sut.RestoreEventAsync(Envelope.Create(new AppCreated - { - Name = appName - }), context, ct); + var context = CreateRestoreContext(); - A.CallTo(() => appsIndex.ReserveAsync(appId, appName, A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) + .Returns("Reservation"); - [Fact] - public async Task Should_complete_reservation_with_previous_token() + await sut.RestoreEventAsync(Envelope.Create(new AppCreated { - const string appName = "my-app"; + Name = appName + }), context, ct); - var context = CreateRestoreContext(); + await sut.CompleteRestoreAsync(context, appName); - A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) - .Returns("Reservation"); + A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", default)) + .MustHaveHappened(); - await sut.RestoreEventAsync(Envelope.Create(new AppCreated - { - Name = appName - }), context, ct); + A.CallTo(() => rebuilder.InsertManyAsync<AppDomainObject, AppDomainObject.State>(A<IEnumerable<DomainId>>.That.Is(appId), 1, default)) + .MustHaveHappened(); + } - await sut.CompleteRestoreAsync(context, appName); + [Fact] + public async Task Should_cleanup_reservation_with_previous_token() + { + const string appName = "my-app"; - A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", default)) - .MustHaveHappened(); + var context = CreateRestoreContext(); - A.CallTo(() => rebuilder.InsertManyAsync<AppDomainObject, AppDomainObject.State>(A<IEnumerable<DomainId>>.That.Is(appId), 1, default)) - .MustHaveHappened(); - } + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) + .Returns("Reservation"); - [Fact] - public async Task Should_cleanup_reservation_with_previous_token() + await sut.RestoreEventAsync(Envelope.Create(new AppCreated { - const string appName = "my-app"; + Name = appName + }), context, ct); - var context = CreateRestoreContext(); + await sut.CleanupRestoreErrorAsync(appId); - A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) - .Returns("Reservation"); + A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", default)) + .MustHaveHappened(); + } - await sut.RestoreEventAsync(Envelope.Create(new AppCreated - { - Name = appName - }), context, ct); + [Fact] + public async Task Should_throw_exception_if_no_reservation_token_returned() + { + const string appName = "my-app"; - await sut.CleanupRestoreErrorAsync(appId); + var context = CreateRestoreContext(); - A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", default)) - .MustHaveHappened(); - } + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) + .Returns(Task.FromResult<string?>(null)); - [Fact] - public async Task Should_throw_exception_if_no_reservation_token_returned() + var @event = Envelope.Create(new AppCreated { - const string appName = "my-app"; - - var context = CreateRestoreContext(); + Name = appName + }); - A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) - .Returns(Task.FromResult<string?>(null)); - - var @event = Envelope.Create(new AppCreated - { - Name = appName - }); + await Assert.ThrowsAsync<BackupRestoreException>(() => sut.RestoreEventAsync(@event, context, ct)); + } - await Assert.ThrowsAsync<BackupRestoreException>(() => sut.RestoreEventAsync(@event, context, ct)); - } + [Fact] + public async Task Should_not_cleanup_reservation_if_no_reservation_token_hold() + { + await sut.CleanupRestoreErrorAsync(appId); - [Fact] - public async Task Should_not_cleanup_reservation_if_no_reservation_token_hold() - { - await sut.CleanupRestoreErrorAsync(appId); + A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_write_user_settings() + { + var settings = new JsonObject(); - [Fact] - public async Task Should_write_user_settings() - { - var settings = new JsonObject(); + var context = CreateBackupContext(); - var context = CreateBackupContext(); + A.CallTo(() => appUISettings.GetAsync(appId, null, ct)) + .Returns(settings); - A.CallTo(() => appUISettings.GetAsync(appId, null, ct)) - .Returns(settings); + await sut.BackupAsync(context, ct); - await sut.BackupAsync(context, ct); + A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, settings, ct)) + .MustHaveHappened(); + } - A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, settings, ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_read_user_settings() + { + var settings = new JsonObject(); - [Fact] - public async Task Should_read_user_settings() - { - var settings = new JsonObject(); + var context = CreateRestoreContext(); - var context = CreateRestoreContext(); + A.CallTo(() => context.Reader.ReadJsonAsync<JsonObject>(A<string>._, ct)) + .Returns(settings); - A.CallTo(() => context.Reader.ReadJsonAsync<JsonObject>(A<string>._, ct)) - .Returns(settings); + await sut.RestoreAsync(context, ct); - await sut.RestoreAsync(context, ct); + A.CallTo(() => appUISettings.SetAsync(appId, null, settings, ct)) + .MustHaveHappened(); + } - A.CallTo(() => appUISettings.SetAsync(appId, null, settings, ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_map_contributor_id_if_assigned() + { + var context = CreateRestoreContext(); - [Fact] - public async Task Should_map_contributor_id_if_assigned() + var @event = Envelope.Create(new AppContributorAssigned { - var context = CreateRestoreContext(); + ContributorId = "found" + }); - var @event = Envelope.Create(new AppContributorAssigned - { - ContributorId = "found" - }); + var actual = await sut.RestoreEventAsync(@event, context, ct); - var actual = await sut.RestoreEventAsync(@event, context, ct); + Assert.True(actual); + Assert.Equal("found_mapped", @event.Payload.ContributorId); + } - Assert.True(actual); - Assert.Equal("found_mapped", @event.Payload.ContributorId); - } + [Fact] + public async Task Should_ignore_contributor_event_if_assigned_user_not_mapped() + { + var context = CreateRestoreContext(); - [Fact] - public async Task Should_ignore_contributor_event_if_assigned_user_not_mapped() + var @event = Envelope.Create(new AppContributorAssigned { - var context = CreateRestoreContext(); + ContributorId = "unknown" + }); - var @event = Envelope.Create(new AppContributorAssigned - { - ContributorId = "unknown" - }); + var actual = await sut.RestoreEventAsync(@event, context, ct); - var actual = await sut.RestoreEventAsync(@event, context, ct); + Assert.False(actual); + Assert.Equal("unknown", @event.Payload.ContributorId); + } - Assert.False(actual); - Assert.Equal("unknown", @event.Payload.ContributorId); - } + [Fact] + public async Task Should_map_contributor_id_if_revoked() + { + var context = CreateRestoreContext(); - [Fact] - public async Task Should_map_contributor_id_if_revoked() + var @event = Envelope.Create(new AppContributorRemoved { - var context = CreateRestoreContext(); + ContributorId = "found" + }); - var @event = Envelope.Create(new AppContributorRemoved - { - ContributorId = "found" - }); + var actual = await sut.RestoreEventAsync(@event, context, ct); - var actual = await sut.RestoreEventAsync(@event, context, ct); + Assert.True(actual); + Assert.Equal("found_mapped", @event.Payload.ContributorId); + } - Assert.True(actual); - Assert.Equal("found_mapped", @event.Payload.ContributorId); - } + [Fact] + public async Task Should_ignore_contributor_event_if_removed_user_not_mapped() + { + var context = CreateRestoreContext(); - [Fact] - public async Task Should_ignore_contributor_event_if_removed_user_not_mapped() + var @event = Envelope.Create(new AppContributorRemoved { - var context = CreateRestoreContext(); - - var @event = Envelope.Create(new AppContributorRemoved - { - ContributorId = "unknown" - }); + ContributorId = "unknown" + }); - var actual = await sut.RestoreEventAsync(@event, context, ct); + var actual = await sut.RestoreEventAsync(@event, context, ct); - Assert.False(actual); - Assert.Equal("unknown", @event.Payload.ContributorId); - } + Assert.False(actual); + Assert.Equal("unknown", @event.Payload.ContributorId); + } - [Fact] - public async Task Should_ignore_exception_if_app_image_to_backup_does_not_exist() - { - var imageStream = new MemoryStream(); + [Fact] + public async Task Should_ignore_exception_if_app_image_to_backup_does_not_exist() + { + var imageStream = new MemoryStream(); - var context = CreateBackupContext(); + var context = CreateBackupContext(); - A.CallTo(() => context.Writer.OpenBlobAsync(A<string>._, ct)) - .Returns(imageStream); + A.CallTo(() => context.Writer.OpenBlobAsync(A<string>._, ct)) + .Returns(imageStream); - A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, ct)) - .Throws(new AssetNotFoundException("Image")); + A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, ct)) + .Throws(new AssetNotFoundException("Image")); - await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); - } + await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); + } - [Fact] - public async Task Should_backup_app_image() - { - var imageStream = new MemoryStream(); + [Fact] + public async Task Should_backup_app_image() + { + var imageStream = new MemoryStream(); - var context = CreateBackupContext(); + var context = CreateBackupContext(); - A.CallTo(() => context.Writer.OpenBlobAsync(A<string>._, ct)) - .Returns(imageStream); + A.CallTo(() => context.Writer.OpenBlobAsync(A<string>._, ct)) + .Returns(imageStream); - await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); + await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); - A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, ct)) - .MustHaveHappened(); - } + A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_restore_app_image() - { - var imageStream = new MemoryStream(); + [Fact] + public async Task Should_restore_app_image() + { + var imageStream = new MemoryStream(); - var context = CreateRestoreContext(); + var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.OpenBlobAsync(A<string>._, ct)) - .Returns(imageStream); + A.CallTo(() => context.Reader.OpenBlobAsync(A<string>._, ct)) + .Returns(imageStream); - await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); + await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); - A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, ct)) - .MustHaveHappened(); - } + A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_ignore_exception_if_app_image_cannot_be_overriden() - { - var imageStream = new MemoryStream(); + [Fact] + public async Task Should_ignore_exception_if_app_image_cannot_be_overriden() + { + var imageStream = new MemoryStream(); - var context = CreateRestoreContext(); + var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.OpenBlobAsync(A<string>._, ct)) - .Returns(imageStream); + A.CallTo(() => context.Reader.OpenBlobAsync(A<string>._, ct)) + .Returns(imageStream); - A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, ct)) - .Throws(new AssetAlreadyExistsException("Image")); + A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, ct)) + .Throws(new AssetAlreadyExistsException("Image")); - await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); - } + await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); + } - private BackupContext CreateBackupContext() - { - return new BackupContext(appId, CreateUserMapping(), A.Fake<IBackupWriter>()); - } + private BackupContext CreateBackupContext() + { + return new BackupContext(appId, CreateUserMapping(), A.Fake<IBackupWriter>()); + } - private RestoreContext CreateRestoreContext() - { - return new RestoreContext(appId, CreateUserMapping(), A.Fake<IBackupReader>(), DomainId.NewGuid()); - } + private RestoreContext CreateRestoreContext() + { + return new RestoreContext(appId, CreateUserMapping(), A.Fake<IBackupReader>(), DomainId.NewGuid()); + } - private IUserMapping CreateUserMapping() - { - var mapping = A.Fake<IUserMapping>(); + private IUserMapping CreateUserMapping() + { + var mapping = A.Fake<IUserMapping>(); - A.CallTo(() => mapping.Initiator).Returns(actor); + A.CallTo(() => mapping.Initiator).Returns(actor); - RefToken mapped; + RefToken mapped; - A.CallTo(() => mapping.TryMap(A<string>.That.Matches(x => x.StartsWith("found", StringComparison.OrdinalIgnoreCase)), out mapped)) - .Returns(true) - .AssignsOutAndRefParametersLazily( - new Func<string, RefToken, object[]>((x, _) => - new[] { RefToken.User($"{x}_mapped") })); + A.CallTo(() => mapping.TryMap(A<string>.That.Matches(x => x.StartsWith("found", StringComparison.OrdinalIgnoreCase)), out mapped)) + .Returns(true) + .AssignsOutAndRefParametersLazily( + new Func<string, RefToken, object[]>((x, _) => + new[] { RefToken.User($"{x}_mapped") })); - A.CallTo(() => mapping.TryMap("notfound", out mapped)) - .Returns(false); + A.CallTo(() => mapping.TryMap("notfound", out mapped)) + .Returns(false); - return mapping; - } + return mapping; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppImageStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppImageStoreTests.cs index beac9a647f..ebfa99ef6c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppImageStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppImageStoreTests.cs @@ -12,62 +12,61 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public class DefaultAppImageStoreTests { - public class DefaultAppImageStoreTests - { - private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly string fileNameDefault; - private readonly string fileNameFolder; - private readonly AssetOptions options = new AssetOptions(); - private readonly DefaultAppImageStore sut; + private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly string fileNameDefault; + private readonly string fileNameFolder; + private readonly AssetOptions options = new AssetOptions(); + private readonly DefaultAppImageStore sut; - public DefaultAppImageStoreTests() - { - fileNameDefault = appId.ToString(); - fileNameFolder = $"{appId}/thumbnail"; + public DefaultAppImageStoreTests() + { + fileNameDefault = appId.ToString(); + fileNameFolder = $"{appId}/thumbnail"; - sut = new DefaultAppImageStore(assetStore, Options.Create(options)); - } + sut = new DefaultAppImageStore(assetStore, Options.Create(options)); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_invoke_asset_store_to_upload_archive(bool folderPerApp) - { - var stream = new MemoryStream(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_invoke_asset_store_to_upload_archive(bool folderPerApp) + { + var stream = new MemoryStream(); - options.FolderPerApp = folderPerApp; + options.FolderPerApp = folderPerApp; - var fileName = GetFileName(folderPerApp); + var fileName = GetFileName(folderPerApp); - await sut.UploadAsync(appId, stream); + await sut.UploadAsync(appId, stream); - A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, default)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, default)) + .MustHaveHappened(); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_invoke_asset_store_to_download_archive(bool folderPerApp) - { - var stream = new MemoryStream(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_invoke_asset_store_to_download_archive(bool folderPerApp) + { + var stream = new MemoryStream(); - options.FolderPerApp = folderPerApp; + options.FolderPerApp = folderPerApp; - var fileName = GetFileName(folderPerApp); + var fileName = GetFileName(folderPerApp); - await sut.DownloadAsync(appId, stream); + await sut.DownloadAsync(appId, stream); - A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, default)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, default)) + .MustHaveHappened(); + } - private string GetFileName(bool folderPerApp) - { - return folderPerApp ? fileNameFolder : fileNameDefault; - } + private string GetFileName(bool folderPerApp) + { + return folderPerApp ? fileNameFolder : fileNameDefault; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs index eb8d13288d..bd8476c601 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs @@ -12,146 +12,145 @@ using Squidex.Infrastructure.Log; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public class DefaultAppLogStoreTests { - public class DefaultAppLogStoreTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IRequestLogStore requestLogStore = A.Fake<IRequestLogStore>(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly DefaultAppLogStore sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IRequestLogStore requestLogStore = A.Fake<IRequestLogStore>(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly DefaultAppLogStore sut; - public DefaultAppLogStoreTests() - { - ct = cts.Token; + public DefaultAppLogStoreTests() + { + ct = cts.Token; - sut = new DefaultAppLogStore(requestLogStore); - } + sut = new DefaultAppLogStore(requestLogStore); + } - [Fact] - public void Should_run_deletion_in_default_order() - { - var order = ((IDeleter)sut).Order; + [Fact] + public void Should_run_deletion_in_default_order() + { + var order = ((IDeleter)sut).Order; - Assert.Equal(0, order); - } + Assert.Equal(0, order); + } - [Fact] - public async Task Should_remove_events_from_streams() - { - var app = Mocks.App(NamedId.Of(appId, "my-app")); + [Fact] + public async Task Should_remove_events_from_streams() + { + var app = Mocks.App(NamedId.Of(appId, "my-app")); - await ((IDeleter)sut).DeleteAppAsync(app, ct); + await ((IDeleter)sut).DeleteAppAsync(app, ct); - A.CallTo(() => requestLogStore.DeleteAsync($"^[a-z]-{app.Id}", A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => requestLogStore.DeleteAsync($"^[a-z]-{app.Id}", A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_forward_request_if_disabled() - { - A.CallTo(() => requestLogStore.IsEnabled) - .Returns(false); + [Fact] + public async Task Should_not_forward_request_if_disabled() + { + A.CallTo(() => requestLogStore.IsEnabled) + .Returns(false); - await sut.LogAsync(appId, default, ct); + await sut.LogAsync(appId, default, ct); - A.CallTo(() => requestLogStore.LogAsync(A<Request>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => requestLogStore.LogAsync(A<Request>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_forward_request_log_to_store() - { - Request? recordedRequest = null; - - A.CallTo(() => requestLogStore.IsEnabled) - .Returns(true); - - A.CallTo(() => requestLogStore.LogAsync(A<Request>._, ct)) - .Invokes(x => recordedRequest = x.GetArgument<Request>(0)!); - - var request = default(RequestLog); - request.Bytes = 1024; - request.CacheHits = 10; - request.CacheServer = "server-fra"; - request.CacheStatus = "MISS"; - request.CacheTTL = 3600; - request.Costs = 1.5; - request.ElapsedMs = 120; - request.RequestMethod = "GET"; - request.RequestPath = "/my-path"; - request.StatusCode = 200; - request.Timestamp = default; - request.UserClientId = "frontend"; - request.UserId = "user1"; - - await sut.LogAsync(appId, request, ct); - - Assert.NotNull(recordedRequest); - - Contains(request.Bytes, recordedRequest); - Contains(request.CacheHits, recordedRequest); - Contains(request.CacheServer, recordedRequest); - Contains(request.CacheStatus, recordedRequest); - Contains(request.CacheTTL, recordedRequest); - Contains(request.ElapsedMs.ToString(CultureInfo.InvariantCulture), recordedRequest); - Contains(request.RequestMethod, recordedRequest); - Contains(request.RequestPath, recordedRequest); - Contains(request.StatusCode, recordedRequest); - Contains(request.UserClientId, recordedRequest); - Contains(request.UserId, recordedRequest); - - Assert.Equal(appId.ToString(), recordedRequest?.Key); - } + [Fact] + public async Task Should_forward_request_log_to_store() + { + Request? recordedRequest = null; + + A.CallTo(() => requestLogStore.IsEnabled) + .Returns(true); + + A.CallTo(() => requestLogStore.LogAsync(A<Request>._, ct)) + .Invokes(x => recordedRequest = x.GetArgument<Request>(0)!); + + var request = default(RequestLog); + request.Bytes = 1024; + request.CacheHits = 10; + request.CacheServer = "server-fra"; + request.CacheStatus = "MISS"; + request.CacheTTL = 3600; + request.Costs = 1.5; + request.ElapsedMs = 120; + request.RequestMethod = "GET"; + request.RequestPath = "/my-path"; + request.StatusCode = 200; + request.Timestamp = default; + request.UserClientId = "frontend"; + request.UserId = "user1"; + + await sut.LogAsync(appId, request, ct); + + Assert.NotNull(recordedRequest); + + Contains(request.Bytes, recordedRequest); + Contains(request.CacheHits, recordedRequest); + Contains(request.CacheServer, recordedRequest); + Contains(request.CacheStatus, recordedRequest); + Contains(request.CacheTTL, recordedRequest); + Contains(request.ElapsedMs.ToString(CultureInfo.InvariantCulture), recordedRequest); + Contains(request.RequestMethod, recordedRequest); + Contains(request.RequestPath, recordedRequest); + Contains(request.StatusCode, recordedRequest); + Contains(request.UserClientId, recordedRequest); + Contains(request.UserId, recordedRequest); + + Assert.Equal(appId.ToString(), recordedRequest?.Key); + } - [Fact] - public async Task Should_write_to_stream() - { - var dateFrom = DateTime.UtcNow.Date.AddDays(-30); - var dateTo = DateTime.UtcNow.Date; + [Fact] + public async Task Should_write_to_stream() + { + var dateFrom = DateTime.UtcNow.Date.AddDays(-30); + var dateTo = DateTime.UtcNow.Date; - A.CallTo(() => requestLogStore.QueryAllAsync(appId.ToString(), dateFrom, dateTo, ct)) - .Returns(new[] - { - CreateRecord(), - CreateRecord(), - CreateRecord(), - CreateRecord() - }.ToAsyncEnumerable()); + A.CallTo(() => requestLogStore.QueryAllAsync(appId.ToString(), dateFrom, dateTo, ct)) + .Returns(new[] + { + CreateRecord(), + CreateRecord(), + CreateRecord(), + CreateRecord() + }.ToAsyncEnumerable()); - var stream = new MemoryStream(); + var stream = new MemoryStream(); - await sut.ReadLogAsync(appId, dateFrom, dateTo, stream, ct); + await sut.ReadLogAsync(appId, dateFrom, dateTo, stream, ct); - stream.Position = 0; + stream.Position = 0; - var lines = 0; + var lines = 0; - using (var reader = new StreamReader(stream)) + using (var reader = new StreamReader(stream)) + { + while (await reader.ReadLineAsync() != null) { - while (await reader.ReadLineAsync() != null) - { - lines++; - } + lines++; } - - Assert.Equal(5, lines); } - private static void Contains(string value, Request? request) - { - Assert.Contains(value, request!.Properties.Values); - } + Assert.Equal(5, lines); + } - private static void Contains(object value, Request? request) - { - Assert.Contains(Convert.ToString(value, CultureInfo.InvariantCulture), request!.Properties.Values); - } + private static void Contains(string value, Request? request) + { + Assert.Contains(value, request!.Properties.Values); + } - private static Request CreateRecord() - { - return new Request { Properties = new Dictionary<string, string>() }; - } + private static void Contains(object value, Request? request) + { + Assert.Contains(Convert.ToString(value, CultureInfo.InvariantCulture), request!.Properties.Values); + } + + private static Request CreateRecord() + { + return new Request { Properties = new Dictionary<string, string>() }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs index 4d1ec842fa..1cc10c2404 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs @@ -14,87 +14,86 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject; + +public class AppCommandMiddlewareTests : HandlerTestBase<AppDomainObject.State> { - public class AppCommandMiddlewareTests : HandlerTestBase<AppDomainObject.State> + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); + private readonly IAppImageStore appImageStore = A.Fake<IAppImageStore>(); + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly AppCommandMiddleware sut; + + public sealed class MyCommand : SquidexCommand { - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); - private readonly IAppImageStore appImageStore = A.Fake<IAppImageStore>(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext; - private readonly AppCommandMiddleware sut; - - public sealed class MyCommand : SquidexCommand - { - } + } - protected override DomainId Id - { - get => appId.Id; - } + protected override DomainId Id + { + get => appId.Id; + } - public AppCommandMiddlewareTests() - { - requestContext = Context.Anonymous(Mocks.App(appId)); + public AppCommandMiddlewareTests() + { + requestContext = Context.Anonymous(Mocks.App(appId)); - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); - sut = new AppCommandMiddleware(domainObjectFactory, appImageStore, assetThumbnailGenerator, contextProvider); - } + sut = new AppCommandMiddleware(domainObjectFactory, appImageStore, assetThumbnailGenerator, contextProvider); + } - [Fact] - public async Task Should_replace_context_app_with_domain_object_actual() - { - var actual = A.Fake<IAppEntity>(); + [Fact] + public async Task Should_replace_context_app_with_domain_object_actual() + { + var actual = A.Fake<IAppEntity>(); - await HandleAsync(new UpdateApp(), actual); + await HandleAsync(new UpdateApp(), actual); - Assert.Same(actual, requestContext.App); - } + Assert.Same(actual, requestContext.App); + } - [Fact] - public async Task Should_upload_image_to_store() - { - var file = new NoopAssetFile(); + [Fact] + public async Task Should_upload_image_to_store() + { + var file = new NoopAssetFile(); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, default)) - .Returns(new ImageInfo(100, 100, ImageOrientation.None, ImageFormat.PNG)); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, default)) + .Returns(new ImageInfo(100, 100, ImageOrientation.None, ImageFormat.PNG)); - await HandleAsync(new UploadAppImage { File = file }, None.Value); + await HandleAsync(new UploadAppImage { File = file }, None.Value); - A.CallTo(() => appImageStore.UploadAsync(appId.Id, A<Stream>._, A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => appImageStore.UploadAsync(appId.Id, A<Stream>._, A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_exception_if_file_to_upload_is_not_an_image() - { - var file = new NoopAssetFile(); + [Fact] + public async Task Should_throw_exception_if_file_to_upload_is_not_an_image() + { + var file = new NoopAssetFile(); - var command = new UploadAppImage { File = file }; + var command = new UploadAppImage { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, default)) - .Returns(Task.FromResult<ImageInfo?>(null)); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, default)) + .Returns(Task.FromResult<ImageInfo?>(null)); - await Assert.ThrowsAsync<ValidationException>(() => HandleAsync(sut, command)); - } + await Assert.ThrowsAsync<ValidationException>(() => HandleAsync(sut, command)); + } - private Task<CommandContext> HandleAsync(AppCommand command, object actual) - { - command.AppId = appId; + private Task<CommandContext> HandleAsync(AppCommand command, object actual) + { + command.AppId = appId; - var domainObject = A.Fake<AppDomainObject>(); + var domainObject = A.Fake<AppDomainObject>(); - A.CallTo(() => domainObject.ExecuteAsync(A<IAggregateCommand>._, A<CancellationToken>._)) - .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); + A.CallTo(() => domainObject.ExecuteAsync(A<IAggregateCommand>._, A<CancellationToken>._)) + .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); - A.CallTo(() => domainObjectFactory.Create<AppDomainObject>(command.AggregateId)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<AppDomainObject>(command.AggregateId)) + .Returns(domainObject); - return HandleAsync(sut, command); - } + return HandleAsync(sut, command); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs index fdb2e3a3a0..bf4249bbfb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs @@ -21,763 +21,762 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject; + +public class AppDomainObjectTests : HandlerTestBase<AppDomainObject.State> { - public class AppDomainObjectTests : HandlerTestBase<AppDomainObject.State> - { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); - private readonly IBillingManager billingManager = A.Fake<IBillingManager>(); - private readonly IUser user; - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly string contributorId = DomainId.NewGuid().ToString(); - private readonly string clientId = "client"; - private readonly string clientNewName = "My Client"; - private readonly string roleName = "My Role"; - private readonly string planIdPaid = "premium"; - private readonly string planIdFree = "free"; - private readonly InitialSettings initialSettings; - private readonly DomainId teamId = DomainId.NewGuid(); - private readonly DomainId workflowId = DomainId.NewGuid(); - private readonly AppDomainObject sut; - - protected override DomainId Id - { - get => AppId; - } + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); + private readonly IBillingManager billingManager = A.Fake<IBillingManager>(); + private readonly IUser user; + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly string contributorId = DomainId.NewGuid().ToString(); + private readonly string clientId = "client"; + private readonly string clientNewName = "My Client"; + private readonly string roleName = "My Role"; + private readonly string planIdPaid = "premium"; + private readonly string planIdFree = "free"; + private readonly InitialSettings initialSettings; + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly DomainId workflowId = DomainId.NewGuid(); + private readonly AppDomainObject sut; + + protected override DomainId Id + { + get => AppId; + } - public AppDomainObjectTests() - { - user = UserMocks.User(contributorId); + public AppDomainObjectTests() + { + user = UserMocks.User(contributorId); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId, default)) - .Returns(user); + A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId, default)) + .Returns(user); - A.CallTo(() => billingPlans.GetFreePlan()) - .Returns(new Plan { Id = planIdFree, MaxContributors = 10 }); + A.CallTo(() => billingPlans.GetFreePlan()) + .Returns(new Plan { Id = planIdFree, MaxContributors = 10 }); - A.CallTo(() => billingPlans.GetPlan(planIdFree)) - .Returns(new Plan { Id = planIdFree, MaxContributors = 10 }); + A.CallTo(() => billingPlans.GetPlan(planIdFree)) + .Returns(new Plan { Id = planIdFree, MaxContributors = 10 }); - A.CallTo(() => billingPlans.GetPlan(planIdPaid)) - .Returns(new Plan { Id = planIdPaid, MaxContributors = 30 }); + A.CallTo(() => billingPlans.GetPlan(planIdPaid)) + .Returns(new Plan { Id = planIdPaid, MaxContributors = 30 }); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, A<string>._, default)) - .Returns(Task.FromResult<Uri?>(null)); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, A<string>._, default)) + .Returns(Task.FromResult<Uri?>(null)); - A.CallTo(() => appProvider.GetTeamAsync(teamId, default)) - .Returns(Mocks.Team(teamId, contributor: Actor.Identifier)); + A.CallTo(() => appProvider.GetTeamAsync(teamId, default)) + .Returns(Mocks.Team(teamId, contributor: Actor.Identifier)); - // Create a non-empty setting, otherwise the event is not raised as it does not change the domain object. - initialSettings = new InitialSettings + // Create a non-empty setting, otherwise the event is not raised as it does not change the domain object. + initialSettings = new InitialSettings + { + Settings = new AppSettings { - Settings = new AppSettings - { - HideScheduler = true - } - }; - - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(initialSettings) - .AddSingleton(billingPlans) - .AddSingleton(billingManager) - .AddSingleton(userResolver) - .BuildServiceProvider(); - - var log = A.Fake<ILogger<AppDomainObject>>(); + HideScheduler = true + } + }; + + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(initialSettings) + .AddSingleton(billingPlans) + .AddSingleton(billingManager) + .AddSingleton(userResolver) + .BuildServiceProvider(); + + var log = A.Fake<ILogger<AppDomainObject>>(); #pragma warning disable MA0056 // Do not call overridable members in constructor - sut = new AppDomainObject(Id, PersistenceFactory, log, serviceProvider); + sut = new AppDomainObject(Id, PersistenceFactory, log, serviceProvider); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - [Fact] - public async Task Command_should_throw_exception_if_app_is_deleted() - { - await ExecuteCreateAsync(); - await ExecuteArchiveAsync(); + [Fact] + public async Task Command_should_throw_exception_if_app_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteArchiveAsync(); - await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteAttachClientAsync); - } + await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteAttachClientAsync); + } - [Fact] - public async Task Create_should_create_events_and_set_intitial_state() - { - var command = new CreateApp { Name = AppName, AppId = AppId }; + [Fact] + public async Task Create_should_create_events_and_set_intitial_state() + { + var command = new CreateApp { Name = AppName, AppId = AppId }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(AppName, sut.Snapshot.Name); + Assert.Equal(AppName, sut.Snapshot.Name); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppCreated { Name = AppName }), - CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }), - CreateEvent(new AppSettingsUpdated { Settings = initialSettings.Settings }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppCreated { Name = AppName }), + CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }), + CreateEvent(new AppSettingsUpdated { Settings = initialSettings.Settings }) + ); + } - [Fact] - public async Task Create_should_not_assign_client_as_contributor() - { - var command = new CreateApp { Name = AppName, Actor = ActorClient, AppId = AppId }; + [Fact] + public async Task Create_should_not_assign_client_as_contributor() + { + var command = new CreateApp { Name = AppName, Actor = ActorClient, AppId = AppId }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(AppName, sut.Snapshot.Name); + Assert.Equal(AppName, sut.Snapshot.Name); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppCreated { Name = AppName }, true), // Must be with client actor. - CreateEvent(new AppSettingsUpdated { Settings = initialSettings.Settings }, true) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppCreated { Name = AppName }, true), // Must be with client actor. + CreateEvent(new AppSettingsUpdated { Settings = initialSettings.Settings }, true) + ); + } - [Fact] - public async Task Update_should_create_events_and_update_label_and_description() - { - var command = new UpdateApp { Label = "my-label", Description = "my-description" }; + [Fact] + public async Task Update_should_create_events_and_update_label_and_description() + { + var command = new UpdateApp { Label = "my-label", Description = "my-description" }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.Label, sut.Snapshot.Label); - Assert.Equal(command.Description, sut.Snapshot.Description); + Assert.Equal(command.Label, sut.Snapshot.Label); + Assert.Equal(command.Description, sut.Snapshot.Description); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppUpdated { Label = command.Label, Description = command.Description }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppUpdated { Label = command.Label, Description = command.Description }) + ); + } - [Fact] - public async Task UpdateSettings_should_create_event_and_update_settings() + [Fact] + public async Task UpdateSettings_should_create_event_and_update_settings() + { + var settings = new AppSettings { - var settings = new AppSettings - { - HideDateTimeModeButton = true - }; + HideDateTimeModeButton = true + }; - var command = new UpdateAppSettings { Settings = settings }; + var command = new UpdateAppSettings { Settings = settings }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(settings, sut.Snapshot.Settings); + Assert.Equal(settings, sut.Snapshot.Settings); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppSettingsUpdated { Settings = settings }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppSettingsUpdated { Settings = settings }) + ); + } - [Fact] - public async Task UploadImage_should_create_events_and_update_image() - { - var command = new UploadAppImage { File = new NoopAssetFile() }; + [Fact] + public async Task UploadImage_should_create_events_and_update_image() + { + var command = new UploadAppImage { File = new NoopAssetFile() }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.File.MimeType, sut.Snapshot.Image!.MimeType); + Assert.Equal(command.File.MimeType, sut.Snapshot.Image!.MimeType); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppImageUploaded { Image = sut.Snapshot.Image }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppImageUploaded { Image = sut.Snapshot.Image }) + ); + } - [Fact] - public async Task RemoveImage_should_create_events_and_update_image() - { - var command = new RemoveAppImage(); + [Fact] + public async Task RemoveImage_should_create_events_and_update_image() + { + var command = new RemoveAppImage(); - await ExecuteCreateAsync(); - await ExecuteUploadImage(); + await ExecuteCreateAsync(); + await ExecuteUploadImage(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(sut.Snapshot.Image); + Assert.Null(sut.Snapshot.Image); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppImageRemoved()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppImageRemoved()) + ); + } - [Fact] - public async Task ChangePlan_should_create_events_and_update_plan() - { - var command = new ChangePlan { PlanId = planIdPaid }; + [Fact] + public async Task ChangePlan_should_create_events_and_update_plan() + { + var command = new ChangePlan { PlanId = planIdPaid }; - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) - .Returns(Task.FromResult<Uri?>(null)); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) + .Returns(Task.FromResult<Uri?>(null)); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); - Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); + Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) + ); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) - .MustHaveHappened(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) + .MustHaveHappened(); - A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) - .MustHaveHappened(); - } + A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangePlan_from_callback_should_create_events_and_update_plan() - { - var command = new ChangePlan { PlanId = planIdPaid, FromCallback = true }; + [Fact] + public async Task ChangePlan_from_callback_should_create_events_and_update_plan() + { + var command = new ChangePlan { PlanId = planIdPaid, FromCallback = true }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); - Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); + Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) + ); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(A<string>._, A<IAppEntity>._, A<string?>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(A<string>._, A<IAppEntity>._, A<string?>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => billingManager.SubscribeAsync(A<string>._, A<IAppEntity>._, A<string?>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => billingManager.SubscribeAsync(A<string>._, A<IAppEntity>._, A<string?>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task ChangePlan_from_callback_should_reset_plan_for_free_plan() - { - var command = new ChangePlan { PlanId = planIdFree, FromCallback = true }; + [Fact] + public async Task ChangePlan_from_callback_should_reset_plan_for_free_plan() + { + var command = new ChangePlan { PlanId = planIdFree, FromCallback = true }; - await ExecuteCreateAsync(); - await ExecuteChangePlanAsync(); + await ExecuteCreateAsync(); + await ExecuteChangePlanAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planIdFree, true)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdFree, true)); - Assert.Null(sut.Snapshot.Plan); + Assert.Null(sut.Snapshot.Plan); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPlanReset()) - ); + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanReset()) + ); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(A<string>._, A<IAppEntity>._, A<string?>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(A<string>._, A<IAppEntity>._, A<string?>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => billingManager.UnsubscribeAsync(A<string>._, A<IAppEntity>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => billingManager.UnsubscribeAsync(A<string>._, A<IAppEntity>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task ChangePlan_should_reset_plan_for_free_plan() - { - var command = new ChangePlan { PlanId = planIdFree }; + [Fact] + public async Task ChangePlan_should_reset_plan_for_free_plan() + { + var command = new ChangePlan { PlanId = planIdFree }; - await ExecuteCreateAsync(); - await ExecuteChangePlanAsync(); + await ExecuteCreateAsync(); + await ExecuteChangePlanAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planIdFree, true)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdFree, true)); - Assert.Null(sut.Snapshot.Plan); + Assert.Null(sut.Snapshot.Plan); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppPlanReset()) - ); + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanReset()) + ); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => billingManager.UnsubscribeAsync(A<string>._, A<IAppEntity>._, A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => billingManager.UnsubscribeAsync(A<string>._, A<IAppEntity>._, A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangePlan_should_not_make_update_for_redirect_actual() - { - var command = new ChangePlan { PlanId = planIdPaid }; + [Fact] + public async Task ChangePlan_should_not_make_update_for_redirect_actual() + { + var command = new ChangePlan { PlanId = planIdPaid }; - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) - .Returns(new Uri("http://squidex.io")); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, default)) + .Returns(new Uri("http://squidex.io")); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid, false, new Uri("http://squidex.io"))); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid, false, new Uri("http://squidex.io"))); - Assert.Null(sut.Snapshot.Plan); - } + Assert.Null(sut.Snapshot.Plan); + } - [Fact] - public async Task ChangePlan_should_not_call_billing_manager_for_callback() - { - var command = new ChangePlan { PlanId = planIdPaid, FromCallback = true }; + [Fact] + public async Task ChangePlan_should_not_call_billing_manager_for_callback() + { + var command = new ChangePlan { PlanId = planIdPaid, FromCallback = true }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); + actual.ShouldBeEquivalent(new PlanChangedResult(planIdPaid)); - Assert.Equal(planIdPaid, sut.Snapshot.Plan?.PlanId); + Assert.Equal(planIdPaid, sut.Snapshot.Plan?.PlanId); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, A<IAppEntity>._, planIdPaid, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task AssignContributor_should_create_events_and_add_contributor() - { - var command = new AssignContributor { ContributorId = contributorId, Role = Role.Editor }; + [Fact] + public async Task AssignContributor_should_create_events_and_add_contributor() + { + var command = new AssignContributor { ContributorId = contributorId, Role = Role.Editor }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); + Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor, IsAdded = true }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor, IsAdded = true }) + ); + } - [Fact] - public async Task AssignContributor_should_create_update_events_and_update_contributor() - { - var command = new AssignContributor { ContributorId = contributorId, Role = Role.Owner }; + [Fact] + public async Task AssignContributor_should_create_update_events_and_update_contributor() + { + var command = new AssignContributor { ContributorId = contributorId, Role = Role.Owner }; - await ExecuteCreateAsync(); - await ExecuteAssignContributorAsync(); + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); + Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Owner }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Owner }) + ); + } - [Fact] - public async Task RemoveContributor_should_create_events_and_remove_contributor() - { - var command = new RemoveContributor { ContributorId = contributorId }; + [Fact] + public async Task RemoveContributor_should_create_events_and_remove_contributor() + { + var command = new RemoveContributor { ContributorId = contributorId }; - await ExecuteCreateAsync(); - await ExecuteAssignContributorAsync(); + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); + Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) + ); + } - [Fact] - public async Task Transfer_should_create_events_and_set_team() - { - var command = new TransferToTeam { TeamId = teamId }; + [Fact] + public async Task Transfer_should_create_events_and_set_team() + { + var command = new TransferToTeam { TeamId = teamId }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(teamId, sut.Snapshot.TeamId); + Assert.Equal(teamId, sut.Snapshot.TeamId); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppTransfered { TeamId = teamId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppTransfered { TeamId = teamId }) + ); + } - [Fact] - public async Task Transfer_from_team_should_create_events_and_set_team() - { - var command = new TransferToTeam { TeamId = null }; + [Fact] + public async Task Transfer_from_team_should_create_events_and_set_team() + { + var command = new TransferToTeam { TeamId = null }; - await ExecuteCreateAsync(); - await ExecuteTransferAsync(); + await ExecuteCreateAsync(); + await ExecuteTransferAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(sut.Snapshot.TeamId); + Assert.Null(sut.Snapshot.TeamId); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppTransfered { TeamId = null }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppTransfered { TeamId = null }) + ); + } - [Fact] - public async Task AttachClient_should_create_events_and_add_client() - { - var command = new AttachClient { Id = clientId }; + [Fact] + public async Task AttachClient_should_create_events_and_add_client() + { + var command = new AttachClient { Id = clientId }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); + Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) + ); + } - [Fact] - public async Task UpdateClient_should_create_events_and_update_client() - { - var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer }; + [Fact] + public async Task UpdateClient_should_create_events_and_update_client() + { + var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer }; - await ExecuteCreateAsync(); - await ExecuteAttachClientAsync(); + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); + Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientUpdated { Id = clientId, Name = clientNewName, Role = Role.Developer }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientUpdated { Id = clientId, Name = clientNewName, Role = Role.Developer }) + ); + } - [Fact] - public async Task RevokeClient_should_create_events_and_remove_client() - { - var command = new RevokeClient { Id = clientId }; + [Fact] + public async Task RevokeClient_should_create_events_and_remove_client() + { + var command = new RevokeClient { Id = clientId }; - await ExecuteCreateAsync(); - await ExecuteAttachClientAsync(); + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); + Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppClientRevoked { Id = clientId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientRevoked { Id = clientId }) + ); + } - [Fact] - public async Task AddWorkflow_should_create_events_and_add_workflow() - { - var command = new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }; + [Fact] + public async Task AddWorkflow_should_create_events_and_add_workflow() + { + var command = new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.NotEmpty(sut.Snapshot.Workflows); + Assert.NotEmpty(sut.Snapshot.Workflows); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowAdded { WorkflowId = workflowId, Name = "my-workflow" }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowAdded { WorkflowId = workflowId, Name = "my-workflow" }) + ); + } - [Fact] - public async Task UpdateWorkflow_should_create_events_and_update_workflow() - { - var command = new UpdateWorkflow { WorkflowId = workflowId, Workflow = Workflow.Default }; + [Fact] + public async Task UpdateWorkflow_should_create_events_and_update_workflow() + { + var command = new UpdateWorkflow { WorkflowId = workflowId, Workflow = Workflow.Default }; - await ExecuteCreateAsync(); - await ExecuteAddWorkflowAsync(); + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.NotEmpty(sut.Snapshot.Workflows); + Assert.NotEmpty(sut.Snapshot.Workflows); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowUpdated { WorkflowId = workflowId, Workflow = Workflow.Default }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowUpdated { WorkflowId = workflowId, Workflow = Workflow.Default }) + ); + } - [Fact] - public async Task DeleteWorkflow_should_create_events_and_remove_workflow() - { - var command = new DeleteWorkflow { WorkflowId = workflowId }; + [Fact] + public async Task DeleteWorkflow_should_create_events_and_remove_workflow() + { + var command = new DeleteWorkflow { WorkflowId = workflowId }; - await ExecuteCreateAsync(); - await ExecuteAddWorkflowAsync(); + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Empty(sut.Snapshot.Workflows); + Assert.Empty(sut.Snapshot.Workflows); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowDeleted { WorkflowId = workflowId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowDeleted { WorkflowId = workflowId }) + ); + } - [Fact] - public async Task AddLanguage_should_create_events_and_add_language() - { - var command = new AddLanguage { Language = Language.DE }; + [Fact] + public async Task AddLanguage_should_create_events_and_add_language() + { + var command = new AddLanguage { Language = Language.DE }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(sut.Snapshot.Languages.Contains(Language.DE)); + Assert.True(sut.Snapshot.Languages.Contains(Language.DE)); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageAdded { Language = Language.DE }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageAdded { Language = Language.DE }) + ); + } - [Fact] - public async Task RemoveLanguage_should_create_events_and_remove_language() - { - var command = new RemoveLanguage { Language = Language.DE }; + [Fact] + public async Task RemoveLanguage_should_create_events_and_remove_language() + { + var command = new RemoveLanguage { Language = Language.DE }; - await ExecuteCreateAsync(); - await ExecuteAddLanguageAsync(Language.DE); + await ExecuteCreateAsync(); + await ExecuteAddLanguageAsync(Language.DE); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(sut.Snapshot.Languages.Contains(Language.DE)); + Assert.False(sut.Snapshot.Languages.Contains(Language.DE)); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageRemoved { Language = Language.DE }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageRemoved { Language = Language.DE }) + ); + } - [Fact] - public async Task UpdateLanguage_should_create_events_and_update_language() - { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.EN } }; + [Fact] + public async Task UpdateLanguage_should_create_events_and_update_language() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.EN } }; - await ExecuteCreateAsync(); - await ExecuteAddLanguageAsync(Language.DE); + await ExecuteCreateAsync(); + await ExecuteAddLanguageAsync(Language.DE); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(sut.Snapshot.Languages.Contains(Language.DE)); + Assert.True(sut.Snapshot.Languages.Contains(Language.DE)); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new[] { Language.EN } }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new[] { Language.EN } }) + ); + } - [Fact] - public async Task AddRole_should_create_events_and_add_role() - { - var command = new AddRole { Name = roleName }; + [Fact] + public async Task AddRole_should_create_events_and_add_role() + { + var command = new AddRole { Name = roleName }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(1, sut.Snapshot.Roles.CustomCount); + Assert.Equal(1, sut.Snapshot.Roles.CustomCount); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleAdded { Name = roleName }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleAdded { Name = roleName }) + ); + } - [Fact] - public async Task DeleteRole_should_create_events_and_delete_role() - { - var command = new DeleteRole { Name = roleName }; + [Fact] + public async Task DeleteRole_should_create_events_and_delete_role() + { + var command = new DeleteRole { Name = roleName }; - await ExecuteCreateAsync(); - await ExecuteAddRoleAsync(); + await ExecuteCreateAsync(); + await ExecuteAddRoleAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(0, sut.Snapshot.Roles.CustomCount); + Assert.Equal(0, sut.Snapshot.Roles.CustomCount); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleDeleted { Name = roleName }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleDeleted { Name = roleName }) + ); + } - [Fact] - public async Task UpdateRole_should_create_events_and_update_role() - { - var command = new UpdateRole { Name = roleName, Permissions = new[] { "clients.read" }, Properties = new JsonObject() }; + [Fact] + public async Task UpdateRole_should_create_events_and_update_role() + { + var command = new UpdateRole { Name = roleName, Permissions = new[] { "clients.read" }, Properties = new JsonObject() }; - await ExecuteCreateAsync(); - await ExecuteAddRoleAsync(); + await ExecuteCreateAsync(); + await ExecuteAddRoleAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = command.Permissions, Properties = command.Properties }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = command.Permissions, Properties = command.Properties }) + ); + } - [Fact] - public async Task ArchiveApp_should_create_events_and_update_deleted_flag() - { - var command = new DeleteApp(); + [Fact] + public async Task ArchiveApp_should_create_events_and_update_deleted_flag() + { + var command = new DeleteApp(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); - Assert.True(sut.Snapshot.IsDeleted); + Assert.True(sut.Snapshot.IsDeleted); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new AppDeleted()) - ); + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppDeleted()) + ); - A.CallTo(() => billingManager.UnsubscribeAsync(command.Actor.Identifier, A<IAppEntity>._, default)) - .MustHaveHappened(); - } + A.CallTo(() => billingManager.UnsubscribeAsync(command.Actor.Identifier, A<IAppEntity>._, default)) + .MustHaveHappened(); + } - private Task ExecuteCreateAsync() - { - return PublishAsync(new CreateApp { Name = AppName, AppId = AppId }); - } + private Task ExecuteCreateAsync() + { + return PublishAsync(new CreateApp { Name = AppName, AppId = AppId }); + } - private Task ExecuteUploadImage() - { - return PublishAsync(new UploadAppImage { File = new NoopAssetFile() }); - } + private Task ExecuteUploadImage() + { + return PublishAsync(new UploadAppImage { File = new NoopAssetFile() }); + } - private Task ExecuteAssignContributorAsync() - { - return PublishAsync(new AssignContributor { ContributorId = contributorId, Role = Role.Editor }); - } + private Task ExecuteAssignContributorAsync() + { + return PublishAsync(new AssignContributor { ContributorId = contributorId, Role = Role.Editor }); + } - private Task ExecuteAttachClientAsync() - { - return PublishAsync(new AttachClient { Id = clientId }); - } + private Task ExecuteAttachClientAsync() + { + return PublishAsync(new AttachClient { Id = clientId }); + } - private Task ExecuteAddRoleAsync() - { - return PublishAsync(new AddRole { Name = roleName }); - } + private Task ExecuteAddRoleAsync() + { + return PublishAsync(new AddRole { Name = roleName }); + } - private Task ExecuteAddLanguageAsync(Language language) - { - return PublishAsync(new AddLanguage { Language = language }); - } + private Task ExecuteAddLanguageAsync(Language language) + { + return PublishAsync(new AddLanguage { Language = language }); + } - private Task ExecuteAddWorkflowAsync() - { - return PublishAsync(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }); - } + private Task ExecuteAddWorkflowAsync() + { + return PublishAsync(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }); + } - private Task ExecuteChangePlanAsync() - { - return PublishAsync(new ChangePlan { PlanId = planIdPaid }); - } + private Task ExecuteChangePlanAsync() + { + return PublishAsync(new ChangePlan { PlanId = planIdPaid }); + } - private Task ExecuteTransferAsync() - { - return PublishAsync(new TransferToTeam { TeamId = teamId }); - } + private Task ExecuteTransferAsync() + { + return PublishAsync(new TransferToTeam { TeamId = teamId }); + } - private Task ExecuteArchiveAsync() - { - return PublishAsync(new DeleteApp()); - } + private Task ExecuteArchiveAsync() + { + return PublishAsync(new DeleteApp()); + } - private Task<object> PublishIdempotentAsync<T>(T command) where T : SquidexCommand, IAggregateCommand - { - return PublishIdempotentAsync(sut, CreateCommand(command)); - } + private Task<object> PublishIdempotentAsync<T>(T command) where T : SquidexCommand, IAggregateCommand + { + return PublishIdempotentAsync(sut, CreateCommand(command)); + } - private async Task<object> PublishAsync<T>(T command) where T : SquidexCommand, IAggregateCommand - { - var actual = await sut.ExecuteAsync(CreateCommand(command), default); + private async Task<object> PublishAsync<T>(T command) where T : SquidexCommand, IAggregateCommand + { + var actual = await sut.ExecuteAsync(CreateCommand(command), default); - return actual.Payload; - } + return actual.Payload; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppStateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppStateTests.cs index 454460a9d3..827dda8960 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppStateTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppStateTests.cs @@ -10,33 +10,32 @@ using Squidex.Infrastructure.Json; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject; + +public class AppStateTests { - public class AppStateTests + private readonly IJsonSerializer serializer = TestUtils.CreateSerializer(options => { - private readonly IJsonSerializer serializer = TestUtils.CreateSerializer(options => - { - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - }); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + }); - [Fact] - public void Should_deserialize_state() - { - var json = File.ReadAllText("Apps/DomainObject/AppState.json"); + [Fact] + public void Should_deserialize_state() + { + var json = File.ReadAllText("Apps/DomainObject/AppState.json"); - var deserialized = serializer.Deserialize<AppDomainObject.State>(json); + var deserialized = serializer.Deserialize<AppDomainObject.State>(json); - Assert.NotNull(deserialized); - } + Assert.NotNull(deserialized); + } - [Fact] - public void Should_serialize_deserialize_state() - { - var json = File.ReadAllText("Apps/DomainObject/AppState.json").CleanJson(); + [Fact] + public void Should_serialize_deserialize_state() + { + var json = File.ReadAllText("Apps/DomainObject/AppState.json").CleanJson(); - var serialized = serializer.Serialize(serializer.Deserialize<AppDomainObject.State>(json), true); + var serialized = serializer.Serialize(serializer.Deserialize<AppDomainObject.State>(json), true); - Assert.Equal(json, serialized); - } + Assert.Equal(json, serialized); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs index 52ac79a7d8..e75e8508f3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs @@ -16,158 +16,157 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public class GuardAppClientsTests : IClassFixture<TranslationsFixture> { - public class GuardAppClientsTests : IClassFixture<TranslationsFixture> - { - private readonly AppClients clients_0 = AppClients.Empty; - private readonly Roles roles = Roles.Empty; + private readonly AppClients clients_0 = AppClients.Empty; + private readonly Roles roles = Roles.Empty; - [Fact] - public void CanAttach_should_throw_execption_if_client_id_is_null() - { - var command = new AttachClient(); + [Fact] + public void CanAttach_should_throw_execption_if_client_id_is_null() + { + var command = new AttachClient(); - ValidationAssert.Throws(() => GuardAppClients.CanAttach(command, App(clients_0)), - new ValidationError("Client ID is required.", "Id")); - } + ValidationAssert.Throws(() => GuardAppClients.CanAttach(command, App(clients_0)), + new ValidationError("Client ID is required.", "Id")); + } - [Fact] - public void CanAttach_should_throw_exception_if_client_already_exists() - { - var command = new AttachClient { Id = "android" }; + [Fact] + public void CanAttach_should_throw_exception_if_client_already_exists() + { + var command = new AttachClient { Id = "android" }; - var clients_1 = clients_0.Add("android", "secret"); + var clients_1 = clients_0.Add("android", "secret"); - ValidationAssert.Throws(() => GuardAppClients.CanAttach(command, App(clients_1)), - new ValidationError("A client with the same id already exists.")); - } + ValidationAssert.Throws(() => GuardAppClients.CanAttach(command, App(clients_1)), + new ValidationError("A client with the same id already exists.")); + } - [Fact] - public void CanAttach_should_not_throw_exception_if_client_is_free() - { - var command = new AttachClient { Id = "ios" }; + [Fact] + public void CanAttach_should_not_throw_exception_if_client_is_free() + { + var command = new AttachClient { Id = "ios" }; - var clients_1 = clients_0.Add("android", "secret"); + var clients_1 = clients_0.Add("android", "secret"); - GuardAppClients.CanAttach(command, App(clients_1)); - } + GuardAppClients.CanAttach(command, App(clients_1)); + } - [Fact] - public void CanRevoke_should_throw_execption_if_client_id_is_null() - { - var command = new RevokeClient(); + [Fact] + public void CanRevoke_should_throw_execption_if_client_id_is_null() + { + var command = new RevokeClient(); - ValidationAssert.Throws(() => GuardAppClients.CanRevoke(command, App(clients_0)), - new ValidationError("Client ID is required.", "Id")); - } + ValidationAssert.Throws(() => GuardAppClients.CanRevoke(command, App(clients_0)), + new ValidationError("Client ID is required.", "Id")); + } - [Fact] - public void CanRevoke_should_throw_exception_if_client_is_not_found() - { - var command = new RevokeClient { Id = "ios" }; + [Fact] + public void CanRevoke_should_throw_exception_if_client_is_not_found() + { + var command = new RevokeClient { Id = "ios" }; - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanRevoke(command, App(clients_0))); - } + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanRevoke(command, App(clients_0))); + } - [Fact] - public void CanRevoke_should_not_throw_exception_if_client_is_found() - { - var command = new RevokeClient { Id = "ios" }; + [Fact] + public void CanRevoke_should_not_throw_exception_if_client_is_found() + { + var command = new RevokeClient { Id = "ios" }; - var clients_1 = clients_0.Add("ios", "secret"); + var clients_1 = clients_0.Add("ios", "secret"); - GuardAppClients.CanRevoke(command, App(clients_1)); - } + GuardAppClients.CanRevoke(command, App(clients_1)); + } - [Fact] - public void CanUpdate_should_throw_execption_if_client_id_is_null() - { - var command = new UpdateClient { Name = "iOS" }; + [Fact] + public void CanUpdate_should_throw_execption_if_client_id_is_null() + { + var command = new UpdateClient { Name = "iOS" }; - ValidationAssert.Throws(() => GuardAppClients.CanUpdate(command, App(clients_0)), - new ValidationError("Client ID is required.", "Id")); - } + ValidationAssert.Throws(() => GuardAppClients.CanUpdate(command, App(clients_0)), + new ValidationError("Client ID is required.", "Id")); + } - [Fact] - public void UpdateClient_should_throw_exception_if_client_is_not_found() - { - var command = new UpdateClient { Id = "ios", Name = "iOS" }; + [Fact] + public void UpdateClient_should_throw_exception_if_client_is_not_found() + { + var command = new UpdateClient { Id = "ios", Name = "iOS" }; - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanUpdate(command, App(clients_0))); - } + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanUpdate(command, App(clients_0))); + } - [Fact] - public void UpdateClient_should_throw_exception_if_client_has_invalid_role() - { - var command = new UpdateClient { Id = "ios", Role = "Invalid" }; + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_invalid_role() + { + var command = new UpdateClient { Id = "ios", Role = "Invalid" }; - var clients_1 = clients_0.Add("ios", "secret"); + var clients_1 = clients_0.Add("ios", "secret"); - ValidationAssert.Throws(() => GuardAppClients.CanUpdate(command, App(clients_1)), - new ValidationError("Role is not a valid value.", "Role")); - } + ValidationAssert.Throws(() => GuardAppClients.CanUpdate(command, App(clients_1)), + new ValidationError("Role is not a valid value.", "Role")); + } - [Fact] - public void UpdateClient_should_throw_exception_if_api_calls_limit_is_less_than_zero() - { - var command = new UpdateClient { Id = "ios", ApiCallsLimit = -10 }; + [Fact] + public void UpdateClient_should_throw_exception_if_api_calls_limit_is_less_than_zero() + { + var command = new UpdateClient { Id = "ios", ApiCallsLimit = -10 }; - var clients_1 = clients_0.Add("ios", "secret"); + var clients_1 = clients_0.Add("ios", "secret"); - ValidationAssert.Throws(() => GuardAppClients.CanUpdate(command, App(clients_1)), - new ValidationError("ApiCallsLimit must be greater or equal to 0.", "ApiCallsLimit")); - } + ValidationAssert.Throws(() => GuardAppClients.CanUpdate(command, App(clients_1)), + new ValidationError("ApiCallsLimit must be greater or equal to 0.", "ApiCallsLimit")); + } - [Fact] - public void UpdateClient_should_throw_exception_if_api_traffic_limit_is_less_than_zero() - { - var command = new UpdateClient { Id = "ios", ApiTrafficLimit = -10 }; + [Fact] + public void UpdateClient_should_throw_exception_if_api_traffic_limit_is_less_than_zero() + { + var command = new UpdateClient { Id = "ios", ApiTrafficLimit = -10 }; - var clients_1 = clients_0.Add("ios", "secret"); + var clients_1 = clients_0.Add("ios", "secret"); - ValidationAssert.Throws(() => GuardAppClients.CanUpdate(command, App(clients_1)), - new ValidationError("ApiTrafficLimit must be greater or equal to 0.", "ApiTrafficLimit")); - } + ValidationAssert.Throws(() => GuardAppClients.CanUpdate(command, App(clients_1)), + new ValidationError("ApiTrafficLimit must be greater or equal to 0.", "ApiTrafficLimit")); + } - [Fact] - public void UpdateClient_should_not_throw_exception_if_client_has_same_name() - { - var command = new UpdateClient { Id = "ios", Name = "ios" }; + [Fact] + public void UpdateClient_should_not_throw_exception_if_client_has_same_name() + { + var command = new UpdateClient { Id = "ios", Name = "ios" }; - var clients_1 = clients_0.Add("ios", "secret"); + var clients_1 = clients_0.Add("ios", "secret"); - GuardAppClients.CanUpdate(command, App(clients_1)); - } + GuardAppClients.CanUpdate(command, App(clients_1)); + } - [Fact] - public void UpdateClient_not_should_throw_exception_if_client_has_same_role() - { - var command = new UpdateClient { Id = "ios", Role = Role.Editor }; + [Fact] + public void UpdateClient_not_should_throw_exception_if_client_has_same_role() + { + var command = new UpdateClient { Id = "ios", Role = Role.Editor }; - var clients_1 = clients_0.Add("ios", "secret"); + var clients_1 = clients_0.Add("ios", "secret"); - GuardAppClients.CanUpdate(command, App(clients_1)); - } + GuardAppClients.CanUpdate(command, App(clients_1)); + } - [Fact] - public void UpdateClient_should_not_throw_exception_if_command_is_valid() - { - var command = new UpdateClient { Id = "ios", Name = "iOS", Role = Role.Reader }; + [Fact] + public void UpdateClient_should_not_throw_exception_if_command_is_valid() + { + var command = new UpdateClient { Id = "ios", Name = "iOS", Role = Role.Reader }; - var clients_1 = clients_0.Add("ios", "secret"); + var clients_1 = clients_0.Add("ios", "secret"); - GuardAppClients.CanUpdate(command, App(clients_1)); - } + GuardAppClients.CanUpdate(command, App(clients_1)); + } - private IAppEntity App(AppClients clients) - { - var app = A.Fake<IAppEntity>(); + private IAppEntity App(AppClients clients) + { + var app = A.Fake<IAppEntity>(); - A.CallTo(() => app.Clients).Returns(clients); - A.CallTo(() => app.Roles).Returns(roles); + A.CallTo(() => app.Clients).Returns(clients); + A.CallTo(() => app.Roles).Returns(roles); - return app; - } + return app; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs index 161eb8ea16..019a7a2303 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs @@ -19,208 +19,207 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public class GuardAppContributorsTests : IClassFixture<TranslationsFixture> { - public class GuardAppContributorsTests : IClassFixture<TranslationsFixture> + private readonly IUser user1 = UserMocks.User("1"); + private readonly IUser user2 = UserMocks.User("2"); + private readonly IUser user3 = UserMocks.User("3"); + private readonly IUserResolver users = A.Fake<IUserResolver>(); + private readonly Contributors contributors_0 = Contributors.Empty; + private readonly Plan planWithoutLimit = new Plan { MaxContributors = -1 }; + private readonly Plan planWithLimit = new Plan { MaxContributors = 2 }; + private readonly Roles roles = Roles.Empty; + + public GuardAppContributorsTests() { - private readonly IUser user1 = UserMocks.User("1"); - private readonly IUser user2 = UserMocks.User("2"); - private readonly IUser user3 = UserMocks.User("3"); - private readonly IUserResolver users = A.Fake<IUserResolver>(); - private readonly Contributors contributors_0 = Contributors.Empty; - private readonly Plan planWithoutLimit = new Plan { MaxContributors = -1 }; - private readonly Plan planWithLimit = new Plan { MaxContributors = 2 }; - private readonly Roles roles = Roles.Empty; - - public GuardAppContributorsTests() - { - A.CallTo(() => user1.Id) - .Returns("1"); + A.CallTo(() => user1.Id) + .Returns("1"); - A.CallTo(() => user2.Id) - .Returns("2"); + A.CallTo(() => user2.Id) + .Returns("2"); - A.CallTo(() => user3.Id) - .Returns("3"); + A.CallTo(() => user3.Id) + .Returns("3"); - A.CallTo(() => users.FindByIdAsync("1", default)) - .Returns(user1); + A.CallTo(() => users.FindByIdAsync("1", default)) + .Returns(user1); - A.CallTo(() => users.FindByIdAsync("2", default)) - .Returns(user2); + A.CallTo(() => users.FindByIdAsync("2", default)) + .Returns(user2); - A.CallTo(() => users.FindByIdAsync("3", default)) - .Returns(user3); + A.CallTo(() => users.FindByIdAsync("3", default)) + .Returns(user3); - A.CallTo(() => users.FindByIdAsync("notfound", default)) - .Returns(Task.FromResult<IUser?>(null)); - } + A.CallTo(() => users.FindByIdAsync("notfound", default)) + .Returns(Task.FromResult<IUser?>(null)); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() - { - var command = new AssignContributor(); + [Fact] + public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() + { + var command = new AssignContributor(); - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit), - new ValidationError("Contributor ID or email is required.", "ContributorId")); - } + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit), + new ValidationError("Contributor ID or email is required.", "ContributorId")); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_role_not_valid() - { - var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; + [Fact] + public async Task CanAssign_should_throw_exception_if_role_not_valid() + { + var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit), - new ValidationError("Role is not a valid value.", "Role")); - } + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit), + new ValidationError("Role is not a valid value.", "Role")); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_same_role() - { - var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_same_role() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_1 = contributors_0.Assign("1", Role.Owner); - await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); - } + await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() - { - var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IgnoreActor = true }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IgnoreActor = true }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_1 = contributors_0.Assign("1", Role.Owner); - await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); - } + await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_user_not_found() - { - var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; + [Fact] + public async Task CanAssign_should_throw_exception_if_user_not_found() + { + var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit)); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_user_is_actor() - { - var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = RefToken.User("3") }; + [Fact] + public async Task CanAssign_should_throw_exception_if_user_is_actor() + { + var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = RefToken.User("3") }; - await Assert.ThrowsAsync<DomainForbiddenException>(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit)); - } + await Assert.ThrowsAsync<DomainForbiddenException>(() => GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit)); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_contributor_max_reached() - { - var command = new AssignContributor { ContributorId = "3" }; + [Fact] + public async Task CanAssign_should_throw_exception_if_contributor_max_reached() + { + var command = new AssignContributor { ContributorId = "3" }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Editor); + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); - await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit), - new ValidationError("You have reached the maximum number of contributors for your plan.")); - } + await ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit), + new ValidationError("You have reached the maximum number of contributors for your plan.")); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ignored() - { - var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ignored() + { + var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Editor); + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); - await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit); - } + await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_found() - { - var command = new AssignContributor { ContributorId = "1" }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_found() + { + var command = new AssignContributor { ContributorId = "1" }; - await GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit); - } + await GuardAppContributors.CanAssign(command, App(contributors_0), users, planWithoutLimit); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_role_is_valid() - { - var command = new AssignContributor { ContributorId = "1" }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_role_is_valid() + { + var command = new AssignContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_1 = contributors_0.Assign("1", Role.Developer); - await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); - } + await GuardAppContributors.CanAssign(command, App(contributors_1), users, planWithoutLimit); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() - { - var command = new AssignContributor { ContributorId = "1" }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() + { + var command = new AssignContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Developer); - var contributors_2 = contributors_1.Assign("2", Role.Developer); + var contributors_1 = contributors_0.Assign("1", Role.Developer); + var contributors_2 = contributors_1.Assign("2", Role.Developer); - await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit); - } + await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithLimit); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ígnored() - { - var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ígnored() + { + var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; - var contributors_1 = contributors_0.Assign("1", Role.Editor); - var contributors_2 = contributors_1.Assign("2", Role.Editor); + var contributors_1 = contributors_0.Assign("1", Role.Editor); + var contributors_2 = contributors_1.Assign("2", Role.Editor); - await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithoutLimit); - } + await GuardAppContributors.CanAssign(command, App(contributors_2), users, planWithoutLimit); + } - [Fact] - public void CanRemove_should_throw_exception_if_contributor_id_is_null() - { - var command = new RemoveContributor(); + [Fact] + public void CanRemove_should_throw_exception_if_contributor_id_is_null() + { + var command = new RemoveContributor(); - ValidationAssert.Throws(() => GuardAppContributors.CanRemove(command, App(contributors_0)), - new ValidationError("Contributor ID or email is required.", "ContributorId")); - } + ValidationAssert.Throws(() => GuardAppContributors.CanRemove(command, App(contributors_0)), + new ValidationError("Contributor ID or email is required.", "ContributorId")); + } - [Fact] - public void CanRemove_should_throw_exception_if_contributor_not_found() - { - var command = new RemoveContributor { ContributorId = "1" }; + [Fact] + public void CanRemove_should_throw_exception_if_contributor_not_found() + { + var command = new RemoveContributor { ContributorId = "1" }; - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppContributors.CanRemove(command, App(contributors_0))); - } + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppContributors.CanRemove(command, App(contributors_0))); + } - [Fact] - public void CanRemove_should_throw_exception_if_contributor_is_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; + [Fact] + public void CanRemove_should_throw_exception_if_contributor_is_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Editor); + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); - ValidationAssert.Throws(() => GuardAppContributors.CanRemove(command, App(contributors_2)), - new ValidationError("Cannot remove the only owner.")); - } + ValidationAssert.Throws(() => GuardAppContributors.CanRemove(command, App(contributors_2)), + new ValidationError("Cannot remove the only owner.")); + } - [Fact] - public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; + [Fact] + public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Owner); + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Owner); - GuardAppContributors.CanRemove(command, App(contributors_2)); - } + GuardAppContributors.CanRemove(command, App(contributors_2)); + } - private IAppEntity App(Contributors contributors) - { - var app = A.Fake<IAppEntity>(); + private IAppEntity App(Contributors contributors) + { + var app = A.Fake<IAppEntity>(); - A.CallTo(() => app.Contributors).Returns(contributors); - A.CallTo(() => app.Roles).Returns(roles); + A.CallTo(() => app.Contributors).Returns(contributors); + A.CallTo(() => app.Roles).Returns(roles); - return app; - } + return app; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppLanguagesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppLanguagesTests.cs index e879cafccf..618340d02e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppLanguagesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppLanguagesTests.cs @@ -14,131 +14,130 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public class GuardAppLanguagesTests : IClassFixture<TranslationsFixture> { - public class GuardAppLanguagesTests : IClassFixture<TranslationsFixture> + private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); + + [Fact] + public void CanAddLanguage_should_throw_exception_if_language_is_null() + { + var command = new AddLanguage(); + + ValidationAssert.Throws(() => GuardAppLanguages.CanAdd(command, App(languages)), + new ValidationError("Language code is required.", "Language")); + } + + [Fact] + public void CanAddLanguage_should_throw_exception_if_language_already_added() + { + var command = new AddLanguage { Language = Language.EN }; + + ValidationAssert.Throws(() => GuardAppLanguages.CanAdd(command, App(languages)), + new ValidationError("Language has already been added.")); + } + + [Fact] + public void CanAddLanguage_should_not_throw_exception_if_language_valid() + { + var command = new AddLanguage { Language = Language.IT }; + + GuardAppLanguages.CanAdd(command, App(languages)); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_is_null() { - private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); - - [Fact] - public void CanAddLanguage_should_throw_exception_if_language_is_null() - { - var command = new AddLanguage(); - - ValidationAssert.Throws(() => GuardAppLanguages.CanAdd(command, App(languages)), - new ValidationError("Language code is required.", "Language")); - } - - [Fact] - public void CanAddLanguage_should_throw_exception_if_language_already_added() - { - var command = new AddLanguage { Language = Language.EN }; - - ValidationAssert.Throws(() => GuardAppLanguages.CanAdd(command, App(languages)), - new ValidationError("Language has already been added.")); - } - - [Fact] - public void CanAddLanguage_should_not_throw_exception_if_language_valid() - { - var command = new AddLanguage { Language = Language.IT }; - - GuardAppLanguages.CanAdd(command, App(languages)); - } - - [Fact] - public void CanRemoveLanguage_should_throw_exception_if_language_is_null() - { - var command = new RemoveLanguage(); - - ValidationAssert.Throws(() => GuardAppLanguages.CanRemove(command, App(languages)), - new ValidationError("Language code is required.", "Language")); - } - - [Fact] - public void CanRemoveLanguage_should_throw_exception_if_language_not_found() - { - var command = new RemoveLanguage { Language = Language.IT }; - - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanRemove(command, App(languages))); - } - - [Fact] - public void CanRemoveLanguage_should_throw_exception_if_language_is_master() - { - var command = new RemoveLanguage { Language = Language.EN }; - - ValidationAssert.Throws(() => GuardAppLanguages.CanRemove(command, App(languages)), - new ValidationError("Master language cannot be removed.")); - } - - [Fact] - public void CanRemoveLanguage_should_not_throw_exception_if_language_is_valid() - { - var command = new RemoveLanguage { Language = Language.DE }; - - GuardAppLanguages.CanRemove(command, App(languages)); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_language_is_null() - { - var command = new UpdateLanguage(); - - ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(command, App(languages)), - new ValidationError("Language code is required.", "Language")); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master() - { - var command = new UpdateLanguage { Language = Language.EN, IsOptional = true }; - - ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(command, App(languages)), - new ValidationError("Master language cannot be made optional.", "IsMaster")); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_fallback_language_defined_and_master() - { - var command = new UpdateLanguage { Language = Language.EN, Fallback = new[] { Language.DE } }; - - ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(command, App(languages)), - new ValidationError("Master language cannot have fallback languages.", "Fallback")); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() - { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.IT } }; - - ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(command, App(languages)), - new ValidationError("App does not have fallback language 'Italian'.", "Fallback")); - } - - [Fact] - public void CanUpdateLanguage_should_throw_exception_if_not_found() - { - var command = new UpdateLanguage { Language = Language.IT }; - - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanUpdate(command, App(languages))); - } - - [Fact] - public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() - { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.EN } }; - - GuardAppLanguages.CanUpdate(command, App(languages)); - } - - private static IAppEntity App(LanguagesConfig languages) - { - var app = A.Fake<IAppEntity>(); - - A.CallTo(() => app.Languages).Returns(languages); + var command = new RemoveLanguage(); + + ValidationAssert.Throws(() => GuardAppLanguages.CanRemove(command, App(languages)), + new ValidationError("Language code is required.", "Language")); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_not_found() + { + var command = new RemoveLanguage { Language = Language.IT }; + + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanRemove(command, App(languages))); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_is_master() + { + var command = new RemoveLanguage { Language = Language.EN }; + + ValidationAssert.Throws(() => GuardAppLanguages.CanRemove(command, App(languages)), + new ValidationError("Master language cannot be removed.")); + } + + [Fact] + public void CanRemoveLanguage_should_not_throw_exception_if_language_is_valid() + { + var command = new RemoveLanguage { Language = Language.DE }; + + GuardAppLanguages.CanRemove(command, App(languages)); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_is_null() + { + var command = new UpdateLanguage(); + + ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(command, App(languages)), + new ValidationError("Language code is required.", "Language")); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master() + { + var command = new UpdateLanguage { Language = Language.EN, IsOptional = true }; + + ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(command, App(languages)), + new ValidationError("Master language cannot be made optional.", "IsMaster")); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_fallback_language_defined_and_master() + { + var command = new UpdateLanguage { Language = Language.EN, Fallback = new[] { Language.DE } }; + + ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(command, App(languages)), + new ValidationError("Master language cannot have fallback languages.", "Fallback")); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.IT } }; + + ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(command, App(languages)), + new ValidationError("App does not have fallback language 'Italian'.", "Fallback")); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_not_found() + { + var command = new UpdateLanguage { Language = Language.IT }; + + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanUpdate(command, App(languages))); + } + + [Fact] + public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.EN } }; + + GuardAppLanguages.CanUpdate(command, App(languages)); + } + + private static IAppEntity App(LanguagesConfig languages) + { + var app = A.Fake<IAppEntity>(); + + A.CallTo(() => app.Languages).Returns(languages); - return app; - } + return app; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs index 1de0c24538..84e6bfcb0c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs @@ -17,173 +17,172 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public class GuardAppRolesTests : IClassFixture<TranslationsFixture> { - public class GuardAppRolesTests : IClassFixture<TranslationsFixture> - { - private readonly string roleName = "Role1"; - private readonly Roles roles_0 = Roles.Empty; - private readonly AppClients clients = AppClients.Empty.Add("client", "secret", "clientRole"); - private readonly Contributors contributors = Contributors.Empty.Assign("contributor", "contributorRole"); + private readonly string roleName = "Role1"; + private readonly Roles roles_0 = Roles.Empty; + private readonly AppClients clients = AppClients.Empty.Add("client", "secret", "clientRole"); + private readonly Contributors contributors = Contributors.Empty.Assign("contributor", "contributorRole"); - [Fact] - public void CanAdd_should_throw_exception_if_name_empty() - { - var command = new AddRole { Name = null! }; + [Fact] + public void CanAdd_should_throw_exception_if_name_empty() + { + var command = new AddRole { Name = null! }; - ValidationAssert.Throws(() => GuardAppRoles.CanAdd(command, App(roles_0)), - new ValidationError("Name is required.", "Name")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanAdd(command, App(roles_0)), + new ValidationError("Name is required.", "Name")); + } - [Fact] - public void CanAdd_should_throw_exception_if_name_exists() - { - var roles_1 = roles_0.Add(roleName); + [Fact] + public void CanAdd_should_throw_exception_if_name_exists() + { + var roles_1 = roles_0.Add(roleName); - var command = new AddRole { Name = roleName }; + var command = new AddRole { Name = roleName }; - ValidationAssert.Throws(() => GuardAppRoles.CanAdd(command, App(roles_1)), - new ValidationError("A role with the same name already exists.")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanAdd(command, App(roles_1)), + new ValidationError("A role with the same name already exists.")); + } - [Fact] - public void CanAdd_should_not_throw_exception_if_command_is_valid() - { - var command = new AddRole { Name = roleName }; + [Fact] + public void CanAdd_should_not_throw_exception_if_command_is_valid() + { + var command = new AddRole { Name = roleName }; - GuardAppRoles.CanAdd(command, App(roles_0)); - } + GuardAppRoles.CanAdd(command, App(roles_0)); + } - [Fact] - public void CanDelete_should_throw_exception_if_name_empty() - { - var command = new DeleteRole { Name = null! }; + [Fact] + public void CanDelete_should_throw_exception_if_name_empty() + { + var command = new DeleteRole { Name = null! }; - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(command, App(roles_0)), - new ValidationError("Name is required.", "Name")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(command, App(roles_0)), + new ValidationError("Name is required.", "Name")); + } - [Fact] - public void CanDelete_should_throw_exception_if_role_not_found() - { - var command = new DeleteRole { Name = roleName }; + [Fact] + public void CanDelete_should_throw_exception_if_role_not_found() + { + var command = new DeleteRole { Name = roleName }; - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppRoles.CanDelete(command, App(roles_0))); - } + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppRoles.CanDelete(command, App(roles_0))); + } - [Fact] - public void CanDelete_should_throw_exception_if_contributor_found() - { - var roles_1 = roles_0.Add("contributorRole"); + [Fact] + public void CanDelete_should_throw_exception_if_contributor_found() + { + var roles_1 = roles_0.Add("contributorRole"); - var command = new DeleteRole { Name = "contributorRole" }; + var command = new DeleteRole { Name = "contributorRole" }; - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(command, App(roles_1)), - new ValidationError("Cannot remove a role when a contributor is assigned.")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(command, App(roles_1)), + new ValidationError("Cannot remove a role when a contributor is assigned.")); + } - [Fact] - public void CanDelete_should_throw_exception_if_client_found() - { - var roles_1 = roles_0.Add("clientRole"); + [Fact] + public void CanDelete_should_throw_exception_if_client_found() + { + var roles_1 = roles_0.Add("clientRole"); - var command = new DeleteRole { Name = "clientRole" }; + var command = new DeleteRole { Name = "clientRole" }; - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(command, App(roles_1)), - new ValidationError("Cannot remove a role when a client is assigned.")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(command, App(roles_1)), + new ValidationError("Cannot remove a role when a client is assigned.")); + } - [Fact] - public void CanDelete_should_throw_exception_if_default_role() - { - var roles_1 = roles_0.Add(Role.Developer); + [Fact] + public void CanDelete_should_throw_exception_if_default_role() + { + var roles_1 = roles_0.Add(Role.Developer); - var command = new DeleteRole { Name = Role.Developer }; + var command = new DeleteRole { Name = Role.Developer }; - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(command, App(roles_1)), - new ValidationError("Cannot delete a default role.")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(command, App(roles_1)), + new ValidationError("Cannot delete a default role.")); + } - [Fact] - public void CanDelete_should_not_throw_exception_if_command_is_valid() - { - var roles_1 = roles_0.Add(roleName); + [Fact] + public void CanDelete_should_not_throw_exception_if_command_is_valid() + { + var roles_1 = roles_0.Add(roleName); - var command = new DeleteRole { Name = roleName }; + var command = new DeleteRole { Name = roleName }; - GuardAppRoles.CanDelete(command, App(roles_1)); - } + GuardAppRoles.CanDelete(command, App(roles_1)); + } - [Fact] - public void CanUpdate_should_throw_exception_if_name_empty() - { - var roles_1 = roles_0.Add(roleName); + [Fact] + public void CanUpdate_should_throw_exception_if_name_empty() + { + var roles_1 = roles_0.Add(roleName); - var command = new UpdateRole { Name = null!, Permissions = new[] { "P1" } }; + var command = new UpdateRole { Name = null!, Permissions = new[] { "P1" } }; - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(command, App(roles_1)), - new ValidationError("Name is required.", "Name")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(command, App(roles_1)), + new ValidationError("Name is required.", "Name")); + } - [Fact] - public void CanUpdate_should_throw_exception_if_permission_is_null() - { - var roles_1 = roles_0.Add(roleName); + [Fact] + public void CanUpdate_should_throw_exception_if_permission_is_null() + { + var roles_1 = roles_0.Add(roleName); - var command = new UpdateRole { Name = roleName }; + var command = new UpdateRole { Name = roleName }; - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(command, App(roles_1)), - new ValidationError("Permissions is required.", "Permissions")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(command, App(roles_1)), + new ValidationError("Permissions is required.", "Permissions")); + } - [Fact] - public void CanUpdate_should_throw_exception_if_default_role() - { - var roles_1 = roles_0.Add(Role.Developer); + [Fact] + public void CanUpdate_should_throw_exception_if_default_role() + { + var roles_1 = roles_0.Add(Role.Developer); - var command = new UpdateRole { Name = Role.Developer, Permissions = new[] { "P1" } }; + var command = new UpdateRole { Name = Role.Developer, Permissions = new[] { "P1" } }; - ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(command, App(roles_1)), - new ValidationError("Cannot update a default role.")); - } + ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(command, App(roles_1)), + new ValidationError("Cannot update a default role.")); + } - [Fact] - public void CanUpdate_should_throw_exception_if_role_does_not_exists() - { - var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; + [Fact] + public void CanUpdate_should_throw_exception_if_role_does_not_exists() + { + var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppRoles.CanUpdate(command, App(roles_0))); - } + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppRoles.CanUpdate(command, App(roles_0))); + } - [Fact] - public void CanUpdate_should_not_throw_exception_if_properties_is_null() - { - var roles_1 = roles_0.Add(roleName); + [Fact] + public void CanUpdate_should_not_throw_exception_if_properties_is_null() + { + var roles_1 = roles_0.Add(roleName); - var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; + var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; - GuardAppRoles.CanUpdate(command, App(roles_1)); - } + GuardAppRoles.CanUpdate(command, App(roles_1)); + } - [Fact] - public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command() - { - var roles_1 = roles_0.Add(roleName); + [Fact] + public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command() + { + var roles_1 = roles_0.Add(roleName); - var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; + var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } }; - GuardAppRoles.CanUpdate(command, App(roles_1)); - } + GuardAppRoles.CanUpdate(command, App(roles_1)); + } - private IAppEntity App(Roles roles) - { - var app = A.Fake<IAppEntity>(); + private IAppEntity App(Roles roles) + { + var app = A.Fake<IAppEntity>(); - A.CallTo(() => app.Contributors).Returns(contributors); - A.CallTo(() => app.Clients).Returns(clients); - A.CallTo(() => app.Roles).Returns(roles); + A.CallTo(() => app.Contributors).Returns(contributors); + A.CallTo(() => app.Clients).Returns(clients); + A.CallTo(() => app.Roles).Returns(roles); - return app; - } + return app; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs index 7e151e4456..e04a31243c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs @@ -19,313 +19,312 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public class GuardAppTests : IClassFixture<TranslationsFixture> { - public class GuardAppTests : IClassFixture<TranslationsFixture> - { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IUserResolver users = A.Fake<IUserResolver>(); - private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); - private readonly RefToken actor = RefToken.User("42"); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IUserResolver users = A.Fake<IUserResolver>(); + private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); + private readonly RefToken actor = RefToken.User("42"); - public GuardAppTests() - { - A.CallTo(() => users.FindByIdOrEmailAsync(A<string>._, default)) - .Returns(A.Dummy<IUser>()); + public GuardAppTests() + { + A.CallTo(() => users.FindByIdOrEmailAsync(A<string>._, default)) + .Returns(A.Dummy<IUser>()); - A.CallTo(() => billingPlans.GetPlan("notfound")) - .Returns(null!); + A.CallTo(() => billingPlans.GetPlan("notfound")) + .Returns(null!); - A.CallTo(() => billingPlans.GetPlan("basic")) - .Returns(new Plan()); - } + A.CallTo(() => billingPlans.GetPlan("basic")) + .Returns(new Plan()); + } - [Fact] - public void CanCreate_should_throw_exception_if_name_not_valid() - { - var command = new CreateApp { Name = "INVALID NAME" }; + [Fact] + public void CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateApp { Name = "INVALID NAME" }; - ValidationAssert.Throws(() => GuardApp.CanCreate(command), - new ValidationError("Name is not a valid slug.", "Name")); - } + ValidationAssert.Throws(() => GuardApp.CanCreate(command), + new ValidationError("Name is not a valid slug.", "Name")); + } - [Fact] - public void CanCreate_should_not_throw_exception_if_app_name_is_valid() - { - var command = new CreateApp { Name = "new-app" }; + [Fact] + public void CanCreate_should_not_throw_exception_if_app_name_is_valid() + { + var command = new CreateApp { Name = "new-app" }; - GuardApp.CanCreate(command); - } + GuardApp.CanCreate(command); + } - [Fact] - public void CanUploadImage_should_throw_exception_if_name_not_valid() - { - var command = new UploadAppImage(); + [Fact] + public void CanUploadImage_should_throw_exception_if_name_not_valid() + { + var command = new UploadAppImage(); - ValidationAssert.Throws(() => GuardApp.CanUploadImage(command), - new ValidationError("File is required.", "File")); - } + ValidationAssert.Throws(() => GuardApp.CanUploadImage(command), + new ValidationError("File is required.", "File")); + } - [Fact] - public void CanUploadImage_should_not_throw_exception_if_app_name_is_valid() - { - var command = new UploadAppImage { File = new NoopAssetFile() }; + [Fact] + public void CanUploadImage_should_not_throw_exception_if_app_name_is_valid() + { + var command = new UploadAppImage { File = new NoopAssetFile() }; - GuardApp.CanUploadImage(command); - } + GuardApp.CanUploadImage(command); + } - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_id_is_null() - { - var command = new ChangePlan { Actor = RefToken.User("me") }; + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_id_is_null() + { + var command = new ChangePlan { Actor = RefToken.User("me") }; - AssignedPlan? plan = null; + AssignedPlan? plan = null; - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), - new ValidationError("Plan ID is required.", "PlanId")); - } + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), + new ValidationError("Plan ID is required.", "PlanId")); + } - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_not_found() - { - var command = new ChangePlan { PlanId = "notfound", Actor = RefToken.User("me") }; + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_not_found() + { + var command = new ChangePlan { PlanId = "notfound", Actor = RefToken.User("me") }; - AssignedPlan? plan = null; + AssignedPlan? plan = null; - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), - new ValidationError("A plan with this id does not exist.", "PlanId")); - } + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), + new ValidationError("A plan with this id does not exist.", "PlanId")); + } - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() - { - var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() + { + var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; - var plan = new AssignedPlan(RefToken.User("other"), "premium"); + var plan = new AssignedPlan(RefToken.User("other"), "premium"); - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), - new ValidationError("Plan can only changed from the user who configured the plan initially.")); - } + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(plan), billingPlans), + new ValidationError("Plan can only changed from the user who configured the plan initially.")); + } - [Fact] - public void CanChangePlan_should_throw_exception_if_assigned_to_team() - { - var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; + [Fact] + public void CanChangePlan_should_throw_exception_if_assigned_to_team() + { + var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; - var teamId = DomainId.NewGuid(); + var teamId = DomainId.NewGuid(); - ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(null, teamId), billingPlans), - new ValidationError("Plan is managed by the team.")); - } + ValidationAssert.Throws(() => GuardApp.CanChangePlan(command, App(null, teamId), billingPlans), + new ValidationError("Plan is managed by the team.")); + } - [Fact] - public void CanChangePlan_should_not_throw_exception_if_plan_is_the_same() - { - var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; + [Fact] + public void CanChangePlan_should_not_throw_exception_if_plan_is_the_same() + { + var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; - var plan = new AssignedPlan(command.Actor, "basic"); + var plan = new AssignedPlan(command.Actor, "basic"); - GuardApp.CanChangePlan(command, App(plan), billingPlans); - } + GuardApp.CanChangePlan(command, App(plan), billingPlans); + } - [Fact] - public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() - { - var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; + [Fact] + public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() + { + var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; - var plan = new AssignedPlan(command.Actor, "premium"); + var plan = new AssignedPlan(command.Actor, "premium"); - GuardApp.CanChangePlan(command, App(plan), billingPlans); - } + GuardApp.CanChangePlan(command, App(plan), billingPlans); + } - [Fact] - public async Task CanTransfer_should_not_throw_exception_if_team_exists() - { - var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); + [Fact] + public async Task CanTransfer_should_not_throw_exception_if_team_exists() + { + var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); - A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(team); - var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; + var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; - await GuardApp.CanTransfer(command, App(null), appProvider, default); - } + await GuardApp.CanTransfer(command, App(null), appProvider, default); + } - [Fact] - public async Task CanTransfer_should_throw_exception_if_team_does_not_exist() - { - var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); + [Fact] + public async Task CanTransfer_should_throw_exception_if_team_does_not_exist() + { + var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); - A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) - .Returns(Task.FromResult<ITeamEntity?>(null)); + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(Task.FromResult<ITeamEntity?>(null)); - var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; + var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; - await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(null), appProvider, default), - new ValidationError("The team does not exist.")); - } + await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(null), appProvider, default), + new ValidationError("The team does not exist.")); + } - [Fact] - public async Task CanTransfer_should_throw_exception_if_actor_is_not_part_of_team() - { - var team = Mocks.Team(DomainId.NewGuid()); + [Fact] + public async Task CanTransfer_should_throw_exception_if_actor_is_not_part_of_team() + { + var team = Mocks.Team(DomainId.NewGuid()); - A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(team); - var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; + var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; - await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(null), appProvider, default), - new ValidationError("The team does not exist.")); - } + await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(null), appProvider, default), + new ValidationError("The team does not exist.")); + } - [Fact] - public async Task CanTransfer_should_throw_exception_if_app_has_plan() - { - var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); + [Fact] + public async Task CanTransfer_should_throw_exception_if_app_has_plan() + { + var team = Mocks.Team(DomainId.NewGuid(), contributor: actor.Identifier); - A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(team); - var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; + var command = new TransferToTeam { TeamId = team.Id, Actor = actor }; - var plan = new AssignedPlan(RefToken.User("me"), "premium"); + var plan = new AssignedPlan(RefToken.User("me"), "premium"); - await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(plan), appProvider, default), - new ValidationError("Subscription must be cancelled first before the app can be transfered.")); - } + await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App(plan), appProvider, default), + new ValidationError("Subscription must be cancelled first before the app can be transfered.")); + } - [Fact] - public void CanUpdateSettings_should_throw_exception_if_settings_is_null() - { - var command = new UpdateAppSettings(); + [Fact] + public void CanUpdateSettings_should_throw_exception_if_settings_is_null() + { + var command = new UpdateAppSettings(); - ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), - new ValidationError("Settings is required.", "Settings")); - } + ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), + new ValidationError("Settings is required.", "Settings")); + } - [Fact] - public void CanUpdateSettings_should_throw_exception_if_patterns_is_null() + [Fact] + public void CanUpdateSettings_should_throw_exception_if_patterns_is_null() + { + var command = new UpdateAppSettings { - var command = new UpdateAppSettings + Settings = new AppSettings { - Settings = new AppSettings - { - Patterns = null! - } - }; - - ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), - new ValidationError("Patterns is required.", "Settings.Patterns")); - } - - [Fact] - public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_name() + Patterns = null! + } + }; + + ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), + new ValidationError("Patterns is required.", "Settings.Patterns")); + } + + [Fact] + public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_name() + { + var command = new UpdateAppSettings { - var command = new UpdateAppSettings + Settings = new AppSettings { - Settings = new AppSettings - { - Patterns = ReadonlyList.Create( - new Pattern(null!, "[a-z]")) - } - }; - - ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), - new ValidationError("Name is required.", "Settings.Patterns[0].Name")); - } - - [Fact] - public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_regex() + Patterns = ReadonlyList.Create( + new Pattern(null!, "[a-z]")) + } + }; + + ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), + new ValidationError("Name is required.", "Settings.Patterns[0].Name")); + } + + [Fact] + public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_regex() + { + var command = new UpdateAppSettings { - var command = new UpdateAppSettings + Settings = new AppSettings { - Settings = new AppSettings - { - Patterns = ReadonlyList.Create( - new Pattern("name", null!)) - } - }; - - ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), - new ValidationError("Regex is required.", "Settings.Patterns[0].Regex")); - } - - [Fact] - public void CanUpdateSettings_should_throw_exception_if_editors_is_null() + Patterns = ReadonlyList.Create( + new Pattern("name", null!)) + } + }; + + ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), + new ValidationError("Regex is required.", "Settings.Patterns[0].Regex")); + } + + [Fact] + public void CanUpdateSettings_should_throw_exception_if_editors_is_null() + { + var command = new UpdateAppSettings { - var command = new UpdateAppSettings + Settings = new AppSettings { - Settings = new AppSettings - { - Editors = null! - } - }; - - ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), - new ValidationError("Editors is required.", "Settings.Editors")); - } - - [Fact] - public void CanUpdateSettings_should_throw_exception_if_editors_has_null_name() + Editors = null! + } + }; + + ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), + new ValidationError("Editors is required.", "Settings.Editors")); + } + + [Fact] + public void CanUpdateSettings_should_throw_exception_if_editors_has_null_name() + { + var command = new UpdateAppSettings { - var command = new UpdateAppSettings + Settings = new AppSettings { - Settings = new AppSettings - { - Editors = ReadonlyList.Create( - new Editor(null!, "[a-z]")) - } - }; - - ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), - new ValidationError("Name is required.", "Settings.Editors[0].Name")); - } - - [Fact] - public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_url() + Editors = ReadonlyList.Create( + new Editor(null!, "[a-z]")) + } + }; + + ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), + new ValidationError("Name is required.", "Settings.Editors[0].Name")); + } + + [Fact] + public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_url() + { + var command = new UpdateAppSettings { - var command = new UpdateAppSettings + Settings = new AppSettings { - Settings = new AppSettings - { - Editors = ReadonlyList.Create( - new Editor("name", null!)) - } - }; - - ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), - new ValidationError("Url is required.", "Settings.Editors[0].Url")); - } - - [Fact] - public void CanUpdateSettings_should_not_throw_exception_if_setting_is_valid() + Editors = ReadonlyList.Create( + new Editor("name", null!)) + } + }; + + ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command), + new ValidationError("Url is required.", "Settings.Editors[0].Url")); + } + + [Fact] + public void CanUpdateSettings_should_not_throw_exception_if_setting_is_valid() + { + var command = new UpdateAppSettings { - var command = new UpdateAppSettings + Settings = new AppSettings { - Settings = new AppSettings - { - Patterns = ReadonlyList.Create( - new Pattern("name", "[a-z]")), - Editors = ReadonlyList.Create( - new Editor("name", "url/to/editor")) - } - }; - - GuardApp.CanUpdateSettings(command); - } - - private static IAppEntity App(AssignedPlan? plan, DomainId? teamId = null) - { - var app = A.Fake<IAppEntity>(); + Patterns = ReadonlyList.Create( + new Pattern("name", "[a-z]")), + Editors = ReadonlyList.Create( + new Editor("name", "url/to/editor")) + } + }; + + GuardApp.CanUpdateSettings(command); + } + + private static IAppEntity App(AssignedPlan? plan, DomainId? teamId = null) + { + var app = A.Fake<IAppEntity>(); - A.CallTo(() => app.Plan) - .Returns(plan); + A.CallTo(() => app.Plan) + .Returns(plan); - A.CallTo(() => app.TeamId) - .Returns(teamId); + A.CallTo(() => app.TeamId) + .Returns(teamId); - return app; - } + return app; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppWorkflowTests.cs index 8c10a06dc2..b4cbd822d6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppWorkflowTests.cs @@ -15,209 +15,208 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; + +public class GuardAppWorkflowTests : IClassFixture<TranslationsFixture> { - public class GuardAppWorkflowTests : IClassFixture<TranslationsFixture> - { - private readonly DomainId workflowId = DomainId.NewGuid(); - private readonly Workflows workflows; + private readonly DomainId workflowId = DomainId.NewGuid(); + private readonly Workflows workflows; - public GuardAppWorkflowTests() - { - workflows = Workflows.Empty.Add(workflowId, "name"); - } + public GuardAppWorkflowTests() + { + workflows = Workflows.Empty.Add(workflowId, "name"); + } - [Fact] - public void CanAdd_should_throw_exception_if_name_is_not_defined() - { - var command = new AddWorkflow(); + [Fact] + public void CanAdd_should_throw_exception_if_name_is_not_defined() + { + var command = new AddWorkflow(); - ValidationAssert.Throws(() => GuardAppWorkflows.CanAdd(command), - new ValidationError("Name is required.", "Name")); - } + ValidationAssert.Throws(() => GuardAppWorkflows.CanAdd(command), + new ValidationError("Name is required.", "Name")); + } - [Fact] - public void CanAdd_should_not_throw_exception_if_command_is_valid() - { - var command = new AddWorkflow { Name = "my-workflow" }; + [Fact] + public void CanAdd_should_not_throw_exception_if_command_is_valid() + { + var command = new AddWorkflow { Name = "my-workflow" }; - GuardAppWorkflows.CanAdd(command); - } + GuardAppWorkflows.CanAdd(command); + } - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_not_found() + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_not_found() + { + var command = new UpdateWorkflow { - var command = new UpdateWorkflow - { - Workflow = Workflow.Empty, - WorkflowId = DomainId.NewGuid() - }; + Workflow = Workflow.Empty, + WorkflowId = DomainId.NewGuid() + }; - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppWorkflows.CanUpdate(command, App())); - } + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppWorkflows.CanUpdate(command, App())); + } - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_is_not_defined() - { - var command = new UpdateWorkflow { WorkflowId = workflowId }; + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_is_not_defined() + { + var command = new UpdateWorkflow { WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), - new ValidationError("Workflow is required.", "Workflow")); - } + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), + new ValidationError("Workflow is required.", "Workflow")); + } - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_has_no_initial_step() - { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - default, - new Dictionary<Status, WorkflowStep> - { - [Status.Published] = new WorkflowStep() - }.ToReadonlyDictionary()), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), - new ValidationError("Initial step is required.", "Workflow.Initial")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_initial_step_is_published() + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_has_no_initial_step() + { + var command = new UpdateWorkflow { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Published, - new Dictionary<Status, WorkflowStep> - { - [Status.Published] = new WorkflowStep() - }.ToReadonlyDictionary()), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), - new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_does_not_have_published_state() + Workflow = new Workflow( + default, + new Dictionary<Status, WorkflowStep> + { + [Status.Published] = new WorkflowStep() + }.ToReadonlyDictionary()), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), + new ValidationError("Initial step is required.", "Workflow.Initial")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_initial_step_is_published() + { + var command = new UpdateWorkflow { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary<Status, WorkflowStep> - { - [Status.Draft] = new WorkflowStep() - }.ToReadonlyDictionary()), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), - new ValidationError("Workflow must have a published step.", "Workflow.Steps")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_step_is_not_defined() + Workflow = new Workflow( + Status.Published, + new Dictionary<Status, WorkflowStep> + { + [Status.Published] = new WorkflowStep() + }.ToReadonlyDictionary()), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), + new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_does_not_have_published_state() + { + var command = new UpdateWorkflow { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary<Status, WorkflowStep> - { - [Status.Published] = null!, - [Status.Draft] = new WorkflowStep() - }.ToReadonlyDictionary()), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), - new ValidationError("Step is required.", "Workflow.Steps.Published")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_transition_is_invalid() + Workflow = new Workflow( + Status.Draft, + new Dictionary<Status, WorkflowStep> + { + [Status.Draft] = new WorkflowStep() + }.ToReadonlyDictionary()), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), + new ValidationError("Workflow must have a published step.", "Workflow.Steps")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_step_is_not_defined() + { + var command = new UpdateWorkflow { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary<Status, WorkflowStep> - { - [Status.Published] = - new WorkflowStep( - new Dictionary<Status, WorkflowTransition> - { - [Status.Archived] = WorkflowTransition.Always - }.ToReadonlyDictionary()), - [Status.Draft] = new WorkflowStep() - }.ToReadonlyDictionary()), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), - new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_workflow_transition_is_not_defined() + Workflow = new Workflow( + Status.Draft, + new Dictionary<Status, WorkflowStep> + { + [Status.Published] = null!, + [Status.Draft] = new WorkflowStep() + }.ToReadonlyDictionary()), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), + new ValidationError("Step is required.", "Workflow.Steps.Published")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_transition_is_invalid() + { + var command = new UpdateWorkflow { - var command = new UpdateWorkflow - { - Workflow = new Workflow( - Status.Draft, - new Dictionary<Status, WorkflowStep> - { - [Status.Draft] = - new WorkflowStep(), - [Status.Published] = - new WorkflowStep( - new Dictionary<Status, WorkflowTransition> - { - [Status.Draft] = null! - }.ToReadonlyDictionary()) - }.ToReadonlyDictionary()), - WorkflowId = workflowId - }; - - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), - new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_workflow_is_valid() + Workflow = new Workflow( + Status.Draft, + new Dictionary<Status, WorkflowStep> + { + [Status.Published] = + new WorkflowStep( + new Dictionary<Status, WorkflowTransition> + { + [Status.Archived] = WorkflowTransition.Always + }.ToReadonlyDictionary()), + [Status.Draft] = new WorkflowStep() + }.ToReadonlyDictionary()), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), + new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_transition_is_not_defined() + { + var command = new UpdateWorkflow { - var command = new UpdateWorkflow { Workflow = Workflow.Default, WorkflowId = workflowId }; + Workflow = new Workflow( + Status.Draft, + new Dictionary<Status, WorkflowStep> + { + [Status.Draft] = + new WorkflowStep(), + [Status.Published] = + new WorkflowStep( + new Dictionary<Status, WorkflowTransition> + { + [Status.Draft] = null! + }.ToReadonlyDictionary()) + }.ToReadonlyDictionary()), + WorkflowId = workflowId + }; + + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command, App()), + new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); + } - GuardAppWorkflows.CanUpdate(command, App()); - } + [Fact] + public void CanUpdate_should_not_throw_exception_if_workflow_is_valid() + { + var command = new UpdateWorkflow { Workflow = Workflow.Default, WorkflowId = workflowId }; - [Fact] - public void CanDelete_should_throw_exception_if_workflow_not_found() - { - var command = new DeleteWorkflow { WorkflowId = DomainId.NewGuid() }; + GuardAppWorkflows.CanUpdate(command, App()); + } - Assert.Throws<DomainObjectNotFoundException>(() => GuardAppWorkflows.CanDelete(command, App())); - } + [Fact] + public void CanDelete_should_throw_exception_if_workflow_not_found() + { + var command = new DeleteWorkflow { WorkflowId = DomainId.NewGuid() }; - [Fact] - public void CanDelete_should_not_throw_exception_if_workflow_is_found() - { - var command = new DeleteWorkflow { WorkflowId = workflowId }; + Assert.Throws<DomainObjectNotFoundException>(() => GuardAppWorkflows.CanDelete(command, App())); + } - GuardAppWorkflows.CanDelete(command, App()); - } + [Fact] + public void CanDelete_should_not_throw_exception_if_workflow_is_found() + { + var command = new DeleteWorkflow { WorkflowId = workflowId }; - private IAppEntity App() - { - var app = A.Fake<IAppEntity>(); + GuardAppWorkflows.CanDelete(command, App()); + } + + private IAppEntity App() + { + var app = A.Fake<IAppEntity>(); - A.CallTo(() => app.Workflows).Returns(workflows); + A.CallTo(() => app.Workflows).Returns(workflows); - return app; - } + return app; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs index c2135600b8..7be353b426 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -21,296 +21,295 @@ using Squidex.Messaging; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.Indexes +namespace Squidex.Domain.Apps.Entities.Apps.Indexes; + +public class AppsIndexTests { - public class AppsIndexTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState<NameReservationState.State> state; + private readonly IAppRepository appRepository = A.Fake<IAppRepository>(); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly string userId = "user1"; + private readonly AppsIndex sut; + + public AppsIndexTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly TestState<NameReservationState.State> state; - private readonly IAppRepository appRepository = A.Fake<IAppRepository>(); - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly string userId = "user1"; - private readonly AppsIndex sut; - - public AppsIndexTests() - { - state = new TestState<NameReservationState.State>("Apps"); + state = new TestState<NameReservationState.State>("Apps"); - ct = cts.Token; + ct = cts.Token; - var replicatedCache = - new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake<IMessageBus>(), - Options.Create(new ReplicatedCacheOptions { Enable = true })); + var replicatedCache = + new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake<IMessageBus>(), + Options.Create(new ReplicatedCacheOptions { Enable = true })); - sut = new AppsIndex(appRepository, replicatedCache, state.PersistenceFactory); - } + sut = new AppsIndex(appRepository, replicatedCache, state.PersistenceFactory); + } - [Fact] - public async Task Should_resolve_app_by_name() - { - var expected = CreateApp(); + [Fact] + public async Task Should_resolve_app_by_name() + { + var expected = CreateApp(); - A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) - .Returns(expected); + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(expected); - var actual1 = await sut.GetAppAsync(appId.Name, false, ct); - var actual2 = await sut.GetAppAsync(appId.Name, false, ct); + var actual1 = await sut.GetAppAsync(appId.Name, false, ct); + var actual2 = await sut.GetAppAsync(appId.Name, false, ct); - Assert.Same(expected, actual1); - Assert.Same(expected, actual2); + Assert.Same(expected, actual1); + Assert.Same(expected, actual2); - A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) - .MustHaveHappenedTwiceExactly(); - } + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .MustHaveHappenedTwiceExactly(); + } - [Fact] - public async Task Should_resolve_app_by_name_and_id_if_cached_before() - { - var expected = CreateApp(); + [Fact] + public async Task Should_resolve_app_by_name_and_id_if_cached_before() + { + var expected = CreateApp(); - A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) - .Returns(expected); + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(expected); - var actual1 = await sut.GetAppAsync(appId.Name, true, ct); - var actual2 = await sut.GetAppAsync(appId.Name, true, ct); - var actual3 = await sut.GetAppAsync(appId.Id, true, ct); + var actual1 = await sut.GetAppAsync(appId.Name, true, ct); + var actual2 = await sut.GetAppAsync(appId.Name, true, ct); + var actual3 = await sut.GetAppAsync(appId.Id, true, ct); - Assert.Same(expected, actual1); - Assert.Same(expected, actual2); - Assert.Same(expected, actual3); + Assert.Same(expected, actual1); + Assert.Same(expected, actual2); + Assert.Same(expected, actual3); - A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_resolve_app_by_id() - { - var expected = CreateApp(); + [Fact] + public async Task Should_resolve_app_by_id() + { + var expected = CreateApp(); - A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) - .Returns(expected); + A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) + .Returns(expected); - var actual1 = await sut.GetAppAsync(appId.Id, false, ct); - var actual2 = await sut.GetAppAsync(appId.Id, false, ct); + var actual1 = await sut.GetAppAsync(appId.Id, false, ct); + var actual2 = await sut.GetAppAsync(appId.Id, false, ct); - Assert.Same(expected, actual1); - Assert.Same(expected, actual2); + Assert.Same(expected, actual1); + Assert.Same(expected, actual2); - A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) - .MustHaveHappenedTwiceExactly(); - } + A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) + .MustHaveHappenedTwiceExactly(); + } - [Fact] - public async Task Should_resolve_app_by_id_and_name_if_cached_before() - { - var expected = CreateApp(); + [Fact] + public async Task Should_resolve_app_by_id_and_name_if_cached_before() + { + var expected = CreateApp(); - A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) - .Returns(expected); + A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) + .Returns(expected); - var actual1 = await sut.GetAppAsync(appId.Id, true, ct); - var actual2 = await sut.GetAppAsync(appId.Id, true, ct); - var actual3 = await sut.GetAppAsync(appId.Name, true, ct); + var actual1 = await sut.GetAppAsync(appId.Id, true, ct); + var actual2 = await sut.GetAppAsync(appId.Id, true, ct); + var actual3 = await sut.GetAppAsync(appId.Name, true, ct); - Assert.Same(expected, actual1); - Assert.Same(expected, actual2); - Assert.Same(expected, actual3); + Assert.Same(expected, actual1); + Assert.Same(expected, actual2); + Assert.Same(expected, actual3); - A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_resolve_all_apps_from_user_permissions() - { - var expected = CreateApp(); + [Fact] + public async Task Should_resolve_all_apps_from_user_permissions() + { + var expected = CreateApp(); - A.CallTo(() => appRepository.QueryAllAsync(userId, A<IEnumerable<string>>.That.Is(appId.Name), ct)) - .Returns(new List<IAppEntity> { expected }); + A.CallTo(() => appRepository.QueryAllAsync(userId, A<IEnumerable<string>>.That.Is(appId.Name), ct)) + .Returns(new List<IAppEntity> { expected }); - var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}"), ct); + var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}"), ct); - Assert.Same(expected, actual[0]); - } + Assert.Same(expected, actual[0]); + } - [Fact] - public async Task Should_resolve_all_apps_from_user() - { - var expected = CreateApp(); + [Fact] + public async Task Should_resolve_all_apps_from_user() + { + var expected = CreateApp(); - A.CallTo(() => appRepository.QueryAllAsync(userId, A<IEnumerable<string>>.That.IsEmpty(), ct)) - .Returns(new List<IAppEntity> { expected }); + A.CallTo(() => appRepository.QueryAllAsync(userId, A<IEnumerable<string>>.That.IsEmpty(), ct)) + .Returns(new List<IAppEntity> { expected }); - var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); - Assert.Same(expected, actual[0]); - } + Assert.Same(expected, actual[0]); + } - [Fact] - public async Task Should_resolve_all_apps_from_team() - { - var teamId = DomainId.NewGuid(); + [Fact] + public async Task Should_resolve_all_apps_from_team() + { + var teamId = DomainId.NewGuid(); - var expected = CreateApp(); + var expected = CreateApp(); - A.CallTo(() => appRepository.QueryAllAsync(teamId, ct)) - .Returns(new List<IAppEntity> { expected }); + A.CallTo(() => appRepository.QueryAllAsync(teamId, ct)) + .Returns(new List<IAppEntity> { expected }); - var actual = await sut.GetAppsForTeamAsync(teamId, ct); + var actual = await sut.GetAppsForTeamAsync(teamId, ct); - Assert.Same(expected, actual[0]); - } + Assert.Same(expected, actual[0]); + } - [Fact] - public async Task Should_return_empty_apps_if_app_not_created() - { - var expected = CreateApp(EtagVersion.Empty); + [Fact] + public async Task Should_return_empty_apps_if_app_not_created() + { + var expected = CreateApp(EtagVersion.Empty); - A.CallTo(() => appRepository.QueryAllAsync(userId, A<IEnumerable<string>>.That.IsEmpty(), ct)) - .Returns(new List<IAppEntity> { expected }); + A.CallTo(() => appRepository.QueryAllAsync(userId, A<IEnumerable<string>>.That.IsEmpty(), ct)) + .Returns(new List<IAppEntity> { expected }); - var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_return_empty_apps_if_app_deleted() - { - var expected = CreateApp(0, true); + [Fact] + public async Task Should_return_empty_apps_if_app_deleted() + { + var expected = CreateApp(0, true); - A.CallTo(() => appRepository.QueryAllAsync(userId, A<IEnumerable<string>>.That.IsEmpty(), ct)) - .Returns(new List<IAppEntity> { expected }); + A.CallTo(() => appRepository.QueryAllAsync(userId, A<IEnumerable<string>>.That.IsEmpty(), ct)) + .Returns(new List<IAppEntity> { expected }); - var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_take_and_remove_reservation_if_created() - { - A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) - .Returns(Task.FromResult<IAppEntity?>(null)); + [Fact] + public async Task Should_take_and_remove_reservation_if_created() + { + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(Task.FromResult<IAppEntity?>(null)); - var command = Create(appId.Name); + var command = Create(appId.Name); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - NameReservation? madeReservation = null; + NameReservation? madeReservation = null; - await sut.HandleAsync(context, (c, ct) => - { - madeReservation = state.Snapshot.Reservations.FirstOrDefault(); + await sut.HandleAsync(context, (c, ct) => + { + madeReservation = state.Snapshot.Reservations.FirstOrDefault(); - return Task.CompletedTask; - }, ct); + return Task.CompletedTask; + }, ct); - Assert.Empty(state.Snapshot.Reservations); + Assert.Empty(state.Snapshot.Reservations); - Assert.Equal(appId.Id, madeReservation?.Id); - Assert.Equal(appId.Name, madeReservation?.Name); - } + Assert.Equal(appId.Id, madeReservation?.Id); + Assert.Equal(appId.Name, madeReservation?.Name); + } - [Fact] - public async Task Should_clear_reservation_if_app_creation_failed() - { - A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) - .Returns(Task.FromResult<IAppEntity?>(null)); + [Fact] + public async Task Should_clear_reservation_if_app_creation_failed() + { + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(Task.FromResult<IAppEntity?>(null)); - var command = Create(appId.Name); + var command = Create(appId.Name); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - NameReservation? madeReservation = null; + NameReservation? madeReservation = null; - await Assert.ThrowsAnyAsync<Exception>(() => sut.HandleAsync(context, (c, ct) => - { - madeReservation = state.Snapshot.Reservations.FirstOrDefault(); + await Assert.ThrowsAnyAsync<Exception>(() => sut.HandleAsync(context, (c, ct) => + { + madeReservation = state.Snapshot.Reservations.FirstOrDefault(); - throw new InvalidOperationException(); - }, ct)); + throw new InvalidOperationException(); + }, ct)); - Assert.Empty(state.Snapshot.Reservations); + Assert.Empty(state.Snapshot.Reservations); - Assert.Equal(appId.Id, madeReservation?.Id); - Assert.Equal(appId.Name, madeReservation?.Name); - } + Assert.Equal(appId.Id, madeReservation?.Id); + Assert.Equal(appId.Name, madeReservation?.Name); + } - [Fact] - public async Task Should_not_create_app_if_name_is_reserved() - { - state.Snapshot.Reservations.Add(new NameReservation(RandomHash.Simple(), appId.Name, DomainId.NewGuid())); + [Fact] + public async Task Should_not_create_app_if_name_is_reserved() + { + state.Snapshot.Reservations.Add(new NameReservation(RandomHash.Simple(), appId.Name, DomainId.NewGuid())); - A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) - .Returns(Task.FromResult<IAppEntity?>(null)); + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(Task.FromResult<IAppEntity?>(null)); - var command = Create(appId.Name); + var command = Create(appId.Name); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context, ct)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context, ct)); + } - [Fact] - public async Task Should_not_create_app_if_name_is_taken() - { - A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) - .Returns(CreateApp()); + [Fact] + public async Task Should_not_create_app_if_name_is_taken() + { + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(CreateApp()); - var command = Create(appId.Name); + var command = Create(appId.Name); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context, ct)); + await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context, ct)); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<NameReservationState.State>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<NameReservationState.State>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_make_an_update_for_other_command() - { - var app = CreateApp(); + [Fact] + public async Task Should_not_make_an_update_for_other_command() + { + var app = CreateApp(); - var command = new UpdateApp { AppId = appId }; + var command = new UpdateApp { AppId = appId }; - var context = - new CommandContext(command, commandBus) - .Complete(app); + var context = + new CommandContext(command, commandBus) + .Complete(app); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<NameReservationState.State>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<NameReservationState.State>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - private CreateApp Create(string name) - { - return new CreateApp { AppId = appId.Id, Name = name }; - } + private CreateApp Create(string name) + { + return new CreateApp { AppId = appId.Id, Name = name }; + } - private IAppEntity CreateApp(long version = 0, bool isDeleted = false) - { - var app = A.Fake<IAppEntity>(); + private IAppEntity CreateApp(long version = 0, bool isDeleted = false) + { + var app = A.Fake<IAppEntity>(); - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - A.CallTo(() => app.Version).Returns(version); - A.CallTo(() => app.IsDeleted).Returns(isDeleted); + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + A.CallTo(() => app.Version).Returns(version); + A.CallTo(() => app.IsDeleted).Returns(isDeleted); - return app; - } + return app; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/RestrictAppsCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/RestrictAppsCommandMiddlewareTests.cs index a34b50bc6f..0935e78b98 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/RestrictAppsCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/RestrictAppsCommandMiddlewareTests.cs @@ -16,164 +16,163 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.Plans +namespace Squidex.Domain.Apps.Entities.Apps.Plans; + +public sealed class RestrictAppsCommandMiddlewareTests { - public sealed class RestrictAppsCommandMiddlewareTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly RestrictAppsOptions options = new RestrictAppsOptions(); + private readonly RestrictAppsCommandMiddleware sut; + + public RestrictAppsCommandMiddlewareTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - private readonly RestrictAppsOptions options = new RestrictAppsOptions(); - private readonly RestrictAppsCommandMiddleware sut; - - public RestrictAppsCommandMiddlewareTests() - { - ct = cts.Token; + ct = cts.Token; - sut = new RestrictAppsCommandMiddleware(Options.Create(options), userResolver); - } + sut = new RestrictAppsCommandMiddleware(Options.Create(options), userResolver); + } + + [Fact] + public async Task Should_throw_exception_if_number_of_apps_reached() + { + var userId = Guid.NewGuid().ToString(); - [Fact] - public async Task Should_throw_exception_if_number_of_apps_reached() + var command = new CreateApp { - var userId = Guid.NewGuid().ToString(); + Actor = RefToken.User(userId) + }; - var command = new CreateApp - { - Actor = RefToken.User(userId) - }; + var commandContext = new CommandContext(command, commandBus); - var commandContext = new CommandContext(command, commandBus); + options.MaximumNumberOfApps = 3; - options.MaximumNumberOfApps = 3; + var user = A.Fake<IUser>(); - var user = A.Fake<IUser>(); + A.CallTo(() => user.Id) + .Returns(userId); - A.CallTo(() => user.Id) - .Returns(userId); + A.CallTo(() => user.Claims) + .Returns(Enumerable.Repeat(new Claim(SquidexClaimTypes.TotalApps, "5"), 1).ToList()); - A.CallTo(() => user.Claims) - .Returns(Enumerable.Repeat(new Claim(SquidexClaimTypes.TotalApps, "5"), 1).ToList()); + A.CallTo(() => userResolver.FindByIdAsync(userId, ct)) + .Returns(user); - A.CallTo(() => userResolver.FindByIdAsync(userId, ct)) - .Returns(user); + var isNextCalled = false; - var isNextCalled = false; + await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(commandContext, (c, ct) => + { + isNextCalled = true; - await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(commandContext, (c, ct) => - { - isNextCalled = true; + return Task.CompletedTask; + }, ct)); - return Task.CompletedTask; - }, ct)); + Assert.False(isNextCalled); + } - Assert.False(isNextCalled); - } + [Fact] + public async Task Should_increment_total_apps_if_maximum_not_reached_and_completed() + { + var userId = Guid.NewGuid().ToString(); - [Fact] - public async Task Should_increment_total_apps_if_maximum_not_reached_and_completed() + var command = new CreateApp { - var userId = Guid.NewGuid().ToString(); + Actor = RefToken.User(userId) + }; - var command = new CreateApp - { - Actor = RefToken.User(userId) - }; + var commandContext = new CommandContext(command, commandBus); - var commandContext = new CommandContext(command, commandBus); + options.MaximumNumberOfApps = 10; - options.MaximumNumberOfApps = 10; + var user = A.Fake<IUser>(); - var user = A.Fake<IUser>(); + A.CallTo(() => user.Id) + .Returns(userId); - A.CallTo(() => user.Id) - .Returns(userId); + A.CallTo(() => user.Claims) + .Returns(Enumerable.Repeat(new Claim(SquidexClaimTypes.TotalApps, "5"), 1).ToList()); - A.CallTo(() => user.Claims) - .Returns(Enumerable.Repeat(new Claim(SquidexClaimTypes.TotalApps, "5"), 1).ToList()); + A.CallTo(() => userResolver.FindByIdAsync(userId, ct)) + .Returns(user); - A.CallTo(() => userResolver.FindByIdAsync(userId, ct)) - .Returns(user); - - await sut.HandleAsync(commandContext, (c, ct) => - { - c.Complete(true); + await sut.HandleAsync(commandContext, (c, ct) => + { + c.Complete(true); - return Task.CompletedTask; - }, ct); + return Task.CompletedTask; + }, ct); - A.CallTo(() => userResolver.SetClaimAsync(userId, SquidexClaimTypes.TotalApps, "6", true, default)) - .MustHaveHappened(); - } + A.CallTo(() => userResolver.SetClaimAsync(userId, SquidexClaimTypes.TotalApps, "6", true, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_check_usage_if_app_is_created_by_client() + [Fact] + public async Task Should_not_check_usage_if_app_is_created_by_client() + { + var command = new CreateApp { - var command = new CreateApp - { - Actor = RefToken.Client(Guid.NewGuid().ToString()) - }; + Actor = RefToken.Client(Guid.NewGuid().ToString()) + }; - var commandContext = new CommandContext(command, commandBus); + var commandContext = new CommandContext(command, commandBus); - options.MaximumNumberOfApps = 10; + options.MaximumNumberOfApps = 10; - await sut.HandleAsync(commandContext, (c, ct) => - { - c.Complete(true); + await sut.HandleAsync(commandContext, (c, ct) => + { + c.Complete(true); - return Task.CompletedTask; - }, ct); + return Task.CompletedTask; + }, ct); - A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_check_usage_if_no_maximum_configured() + [Fact] + public async Task Should_not_check_usage_if_no_maximum_configured() + { + var command = new CreateApp { - var command = new CreateApp - { - Actor = RefToken.User(Guid.NewGuid().ToString()) - }; + Actor = RefToken.User(Guid.NewGuid().ToString()) + }; - var commandContext = new CommandContext(command, commandBus); + var commandContext = new CommandContext(command, commandBus); - options.MaximumNumberOfApps = 0; + options.MaximumNumberOfApps = 0; - await sut.HandleAsync(commandContext, (c, ct) => - { - c.Complete(true); + await sut.HandleAsync(commandContext, (c, ct) => + { + c.Complete(true); - return Task.CompletedTask; - }, ct); + return Task.CompletedTask; + }, ct); - A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_check_usage_for_other_commands() + [Fact] + public async Task Should_not_check_usage_for_other_commands() + { + var command = new UpdateApp { - var command = new UpdateApp - { - Actor = RefToken.User(Guid.NewGuid().ToString()) - }; + Actor = RefToken.User(Guid.NewGuid().ToString()) + }; - var commandContext = new CommandContext(command, commandBus); + var commandContext = new CommandContext(command, commandBus); - options.MaximumNumberOfApps = 10; + options.MaximumNumberOfApps = 10; - await sut.HandleAsync(commandContext, (c, ct) => - { - c.Complete(true); + await sut.HandleAsync(commandContext, (c, ct) => + { + c.Complete(true); - return Task.CompletedTask; - }, ct); + return Task.CompletedTask; + }, ct); - A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs index a0d427f3a6..728d562e7a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs @@ -13,39 +13,38 @@ #pragma warning disable xUnit2017 // Do not use Contains() to check if a value exists in a collection -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps; + +public class RolePermissionsProviderTests { - public class RolePermissionsProviderTests + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RolePermissionsProvider sut; + + public RolePermissionsProviderTests() + { + app = Mocks.App(appId); + + sut = new RolePermissionsProvider(appProvider); + } + + [Fact] + public async Task Should_provide_all_permissions() { - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RolePermissionsProvider sut; - - public RolePermissionsProviderTests() - { - app = Mocks.App(appId); - - sut = new RolePermissionsProvider(appProvider); - } - - [Fact] - public async Task Should_provide_all_permissions() - { - A.CallTo(() => appProvider.GetSchemasAsync(A<DomainId>._, default)) - .Returns(new List<ISchemaEntity> - { - Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "schema1")), - Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "schema2")) - }); - - var actual = await sut.GetPermissionsAsync(app); - - Assert.True(actual.Contains("*")); - Assert.True(actual.Contains("clients.read")); - Assert.True(actual.Contains("schemas.*.update")); - Assert.True(actual.Contains("schemas.schema1.update")); - Assert.True(actual.Contains("schemas.schema2.update")); - } + A.CallTo(() => appProvider.GetSchemasAsync(A<DomainId>._, default)) + .Returns(new List<ISchemaEntity> + { + Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "schema1")), + Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "schema2")) + }); + + var actual = await sut.GetPermissionsAsync(app); + + Assert.True(actual.Contains("*")); + Assert.True(actual.Contains("clients.read")); + Assert.True(actual.Contains("schemas.*.update")); + Assert.True(actual.Contains("schemas.schema1.update")); + Assert.True(actual.Contains("schemas.schema2.update")); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesClientTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesClientTests.cs index 16a7138ced..b18f93ca60 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesClientTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesClientTests.cs @@ -9,72 +9,71 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps.Templates +namespace Squidex.Domain.Apps.Entities.Apps.Templates; + +public class TemplatesClientTests { - public class TemplatesClientTests - { - private readonly TemplatesClient sut; + private readonly TemplatesClient sut; - public TemplatesClientTests() - { - var httpClientFactory = A.Fake<IHttpClientFactory>(); + public TemplatesClientTests() + { + var httpClientFactory = A.Fake<IHttpClientFactory>(); - A.CallTo(() => httpClientFactory.CreateClient(null)) - .Returns(new HttpClient()); + A.CallTo(() => httpClientFactory.CreateClient(null)) + .Returns(new HttpClient()); - sut = new TemplatesClient(httpClientFactory, Options.Create(new TemplatesOptions + sut = new TemplatesClient(httpClientFactory, Options.Create(new TemplatesOptions + { + Repositories = new[] { - Repositories = new[] + new TemplateRepository { - new TemplateRepository - { - ContentUrl = "https://raw.githubusercontent.com/Squidex/templates/main" - } + ContentUrl = "https://raw.githubusercontent.com/Squidex/templates/main" } - })); - } + } + })); + } - [Fact] - public async Task Should_get_templates() - { - var templates = await sut.GetTemplatesAsync(); + [Fact] + public async Task Should_get_templates() + { + var templates = await sut.GetTemplatesAsync(); - Assert.NotEmpty(templates); - Assert.Contains(templates, x => x.IsStarter); - } + Assert.NotEmpty(templates); + Assert.Contains(templates, x => x.IsStarter); + } - [Fact] - public async Task Should_get_details_from_templates() - { - var templates = await sut.GetTemplatesAsync(); + [Fact] + public async Task Should_get_details_from_templates() + { + var templates = await sut.GetTemplatesAsync(); - foreach (var template in templates) - { - var details = await sut.GetDetailAsync(template.Name); + foreach (var template in templates) + { + var details = await sut.GetDetailAsync(template.Name); - Assert.NotNull(details); - } + Assert.NotNull(details); } + } - [Fact] - public async Task Should_get_repository_from_templates() - { - var templates = await sut.GetTemplatesAsync(); + [Fact] + public async Task Should_get_repository_from_templates() + { + var templates = await sut.GetTemplatesAsync(); - foreach (var template in templates) - { - var repository = await sut.GetRepositoryUrl(template.Name); + foreach (var template in templates) + { + var repository = await sut.GetRepositoryUrl(template.Name); - Assert.NotNull(repository); - } + Assert.NotNull(repository); } + } - [Fact] - public async Task Should_return_null_details_if_not_found() - { - var details = await sut.GetDetailAsync("invalid"); + [Fact] + public async Task Should_return_null_details_if_not_found() + { + var details = await sut.GetDetailAsync("invalid"); - Assert.Null(details); - } + Assert.Null(details); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index 92fbaab3e9..973297b13a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -20,173 +20,172 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class AssetChangedTriggerHandlerTests { - public class AssetChangedTriggerHandlerTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); + private readonly IAssetLoader assetLoader = A.Fake<IAssetLoader>(); + private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); + private readonly IRuleTriggerHandler sut; + + public AssetChangedTriggerHandlerTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); - private readonly IAssetLoader assetLoader = A.Fake<IAssetLoader>(); - private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); - private readonly IRuleTriggerHandler sut; - - public AssetChangedTriggerHandlerTests() - { - ct = cts.Token; + ct = cts.Token; - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true", default)) - .Returns(true); + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true", default)) + .Returns(true); - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false", default)) - .Returns(false); + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false", default)) + .Returns(false); - sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader, assetRepository); - } + sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader, assetRepository); + } - public static IEnumerable<object[]> TestEvents() - { - yield return new object[] { TestUtils.CreateEvent<AssetCreated>(), EnrichedAssetEventType.Created }; - yield return new object[] { TestUtils.CreateEvent<AssetUpdated>(), EnrichedAssetEventType.Updated }; - yield return new object[] { TestUtils.CreateEvent<AssetAnnotated>(), EnrichedAssetEventType.Annotated }; - yield return new object[] { TestUtils.CreateEvent<AssetDeleted>(), EnrichedAssetEventType.Deleted }; - } + public static IEnumerable<object[]> TestEvents() + { + yield return new object[] { TestUtils.CreateEvent<AssetCreated>(), EnrichedAssetEventType.Created }; + yield return new object[] { TestUtils.CreateEvent<AssetUpdated>(), EnrichedAssetEventType.Updated }; + yield return new object[] { TestUtils.CreateEvent<AssetAnnotated>(), EnrichedAssetEventType.Annotated }; + yield return new object[] { TestUtils.CreateEvent<AssetDeleted>(), EnrichedAssetEventType.Deleted }; + } - [Fact] - public void Should_return_true_if_asking_for_snapshot_support() - { - Assert.True(sut.CanCreateSnapshotEvents); - } + [Fact] + public void Should_return_true_if_asking_for_snapshot_support() + { + Assert.True(sut.CanCreateSnapshotEvents); + } - [Fact] - public void Should_handle_asset_event() - { - Assert.True(sut.Handles(new AssetCreated())); - } + [Fact] + public void Should_handle_asset_event() + { + Assert.True(sut.Handles(new AssetCreated())); + } - [Fact] - public void Should_not_handle_asset_moved_event() - { - Assert.False(sut.Handles(new AssetMoved())); - } + [Fact] + public void Should_not_handle_asset_moved_event() + { + Assert.False(sut.Handles(new AssetMoved())); + } - [Fact] - public void Should_not_handle_other_event() - { - Assert.False(sut.Handles(new ContentCreated())); - } + [Fact] + public void Should_not_handle_other_event() + { + Assert.False(sut.Handles(new ContentCreated())); + } - [Fact] - public async Task Should_create_events_from_snapshots() - { - var ctx = Context(); + [Fact] + public async Task Should_create_events_from_snapshots() + { + var ctx = Context(); - A.CallTo(() => assetRepository.StreamAll(ctx.AppId.Id, ct)) - .Returns(new List<AssetEntity> - { - new AssetEntity(), - new AssetEntity() - }.ToAsyncEnumerable()); + A.CallTo(() => assetRepository.StreamAll(ctx.AppId.Id, ct)) + .Returns(new List<AssetEntity> + { + new AssetEntity(), + new AssetEntity() + }.ToAsyncEnumerable()); - var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); + var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); - var typed = actual.OfType<EnrichedAssetEvent>().ToList(); + var typed = actual.OfType<EnrichedAssetEvent>().ToList(); - Assert.Equal(2, typed.Count); - Assert.Equal(2, typed.Count(x => x.Type == EnrichedAssetEventType.Created && x.Name == "AssetQueried")); - } + Assert.Equal(2, typed.Count); + Assert.Equal(2, typed.Count(x => x.Type == EnrichedAssetEventType.Created && x.Name == "AssetQueried")); + } - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_create_enriched_events(AssetEvent @event, EnrichedAssetEventType type) - { - var ctx = Context(appId: @event.AppId); + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_create_enriched_events(AssetEvent @event, EnrichedAssetEventType type) + { + var ctx = Context(appId: @event.AppId); - var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); + var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); - A.CallTo(() => assetLoader.GetAsync(ctx.AppId.Id, @event.AssetId, 12, ct)) - .Returns(new AssetEntity()); + A.CallTo(() => assetLoader.GetAsync(ctx.AppId.Id, @event.AssetId, 12, ct)) + .Returns(new AssetEntity()); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); - var enrichedEvent = (EnrichedAssetEvent)actual.Single(); + var enrichedEvent = (EnrichedAssetEvent)actual.Single(); - Assert.Equal(type, enrichedEvent.Type); - Assert.Equal(@event.Actor, enrichedEvent.Actor); - Assert.Equal(@event.AppId, enrichedEvent.AppId); - Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); - } + Assert.Equal(type, enrichedEvent.Type); + Assert.Equal(@event.Actor, enrichedEvent.Actor); + Assert.Equal(@event.AppId, enrichedEvent.AppId); + Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); + } - [Fact] - public void Should_trigger_check_if_condition_is_empty() + [Fact] + public void Should_trigger_check_if_condition_is_empty() + { + TestForCondition(string.Empty, ctx => { - TestForCondition(string.Empty, ctx => - { - var @event = new EnrichedAssetEvent(); + var @event = new EnrichedAssetEvent(); - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_check_if_condition_matchs() + [Fact] + public void Should_trigger_check_if_condition_matchs() + { + TestForCondition("true", ctx => { - TestForCondition("true", ctx => - { - var @event = new EnrichedAssetEvent(); + var @event = new EnrichedAssetEvent(); - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_not_trigger_check_if_condition_does_not_matchs() + [Fact] + public void Should_not_trigger_check_if_condition_does_not_matchs() + { + TestForCondition("false", ctx => { - TestForCondition("false", ctx => - { - var @event = new EnrichedAssetEvent(); + var @event = new EnrichedAssetEvent(); - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - private void TestForCondition(string condition, Action<RuleContext> action) + private void TestForCondition(string condition, Action<RuleContext> action) + { + var trigger = new AssetChangedTriggerV2 { - var trigger = new AssetChangedTriggerV2 - { - Condition = condition - }; + Condition = condition + }; - action(Context(trigger)); + action(Context(trigger)); - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) - .MustHaveHappened(); - } + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) + .MustNotHaveHappened(); } - - private static RuleContext Context(RuleTrigger? trigger = null, NamedId<DomainId>? appId = null) + else { - trigger ??= new AssetChangedTriggerV2(); - - return new RuleContext - { - AppId = appId ?? NamedId.Of(DomainId.NewGuid(), "my-app"), - Rule = new Rule(trigger, A.Fake<RuleAction>()), - RuleId = DomainId.NewGuid() - }; + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) + .MustHaveHappened(); } } + + private static RuleContext Context(RuleTrigger? trigger = null, NamedId<DomainId>? appId = null) + { + trigger ??= new AssetChangedTriggerV2(); + + return new RuleContext + { + AppId = appId ?? NamedId.Of(DomainId.NewGuid(), "my-app"), + Rule = new Rule(trigger, A.Fake<RuleAction>()), + RuleId = DomainId.NewGuid() + }; + } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupProcessTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupProcessTests.cs index e44492fb33..8d7c70d4fd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupProcessTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupProcessTests.cs @@ -9,31 +9,30 @@ using tusdotnet.Interfaces; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class AssetCleanupProcessTests { - public class AssetCleanupProcessTests - { - private readonly ITusExpirationStore expirationStore = A.Fake<ITusExpirationStore>(); - private readonly AssetCleanupProcess sut; + private readonly ITusExpirationStore expirationStore = A.Fake<ITusExpirationStore>(); + private readonly AssetCleanupProcess sut; - public AssetCleanupProcessTests() - { - sut = new AssetCleanupProcess(expirationStore); - } + public AssetCleanupProcessTests() + { + sut = new AssetCleanupProcess(expirationStore); + } - [Fact] - public async Task Should_stop_when_start_not_called() - { - await sut.StopAsync(default); - } + [Fact] + public async Task Should_stop_when_start_not_called() + { + await sut.StopAsync(default); + } - [Fact] - public async Task Should_call_expiration_store_when_reminder_invoked() - { - await sut.CleanupAsync(default); + [Fact] + public async Task Should_call_expiration_store_when_reminder_invoked() + { + await sut.CleanupAsync(default); - A.CallTo(() => expirationStore.RemoveExpiredFilesAsync(default)) - .MustHaveHappened(); - } + A.CallTo(() => expirationStore.RemoveExpiredFilesAsync(default)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs index a751ec4848..1550012c5f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs @@ -13,111 +13,110 @@ using Squidex.Infrastructure.Reflection; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class AssetPermanentDeleterTests { - public class AssetPermanentDeleterTests + private readonly IAssetFileStore assetFiletore = A.Fake<IAssetFileStore>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly TypeNameRegistry typeNameRegistry; + private readonly AssetPermanentDeleter sut; + + public AssetPermanentDeleterTests() + { + typeNameRegistry = + new TypeNameRegistry() + .Map(typeof(AssetCreated)) + .Map(typeof(AssetDeleted)); + + sut = new AssetPermanentDeleter(assetFiletore, typeNameRegistry); + } + + [Fact] + public void Should_return_assets_filter_for_events_filter() + { + IEventConsumer consumer = sut; + + Assert.Equal("^asset-", consumer.EventsFilter); + } + + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; + + await consumer.ClearAsync(); + } + + [Fact] + public void Should_return_type_name_for_name() + { + IEventConsumer consumer = sut; + + Assert.Equal(nameof(AssetPermanentDeleter), consumer.Name); + } + + [Fact] + public void Should_handle_deletion_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName<AssetDeleted>(), new EnvelopeHeaders(), "payload")); + + Assert.True(sut.Handles(storedEvent)); + } + + [Fact] + public void Should_not_handle_creation_event() { - private readonly IAssetFileStore assetFiletore = A.Fake<IAssetFileStore>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly TypeNameRegistry typeNameRegistry; - private readonly AssetPermanentDeleter sut; - - public AssetPermanentDeleterTests() - { - typeNameRegistry = - new TypeNameRegistry() - .Map(typeof(AssetCreated)) - .Map(typeof(AssetDeleted)); - - sut = new AssetPermanentDeleter(assetFiletore, typeNameRegistry); - } - - [Fact] - public void Should_return_assets_filter_for_events_filter() - { - IEventConsumer consumer = sut; - - Assert.Equal("^asset-", consumer.EventsFilter); - } - - [Fact] - public async Task Should_do_nothing_on_clear() - { - IEventConsumer consumer = sut; - - await consumer.ClearAsync(); - } - - [Fact] - public void Should_return_type_name_for_name() - { - IEventConsumer consumer = sut; - - Assert.Equal(nameof(AssetPermanentDeleter), consumer.Name); - } - - [Fact] - public void Should_handle_deletion_event() - { - var storedEvent = - new StoredEvent("stream", "1", 1, - new EventData(typeNameRegistry.GetName<AssetDeleted>(), new EnvelopeHeaders(), "payload")); - - Assert.True(sut.Handles(storedEvent)); - } - - [Fact] - public void Should_not_handle_creation_event() - { - var storedEvent = - new StoredEvent("stream", "1", 1, - new EventData(typeNameRegistry.GetName<AssetCreated>(), new EnvelopeHeaders(), "payload")); - - Assert.False(sut.Handles(storedEvent)); - } - - [Fact] - public async Task Should_not_delete_assets_if_event_restored() - { - var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - - await sut.On(Envelope.Create(@event).SetRestored()); - - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A<CancellationToken>._)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_delete_asset() - { - var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - - await sut.On(Envelope.Create(@event)); - - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_ignore_not_found_assets() - { - var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) - .Throws(new AssetNotFoundException("fileName")); - - await sut.On(Envelope.Create(@event)); - } - - [Fact] - public async Task Should_not_ignore_exceptions() - { - var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync<InvalidOperationException>(() => sut.On(Envelope.Create(@event))); - } + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName<AssetCreated>(), new EnvelopeHeaders(), "payload")); + + Assert.False(sut.Handles(storedEvent)); + } + + [Fact] + public async Task Should_not_delete_assets_if_event_restored() + { + var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; + + await sut.On(Envelope.Create(@event).SetRestored()); + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A<CancellationToken>._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_delete_asset() + { + var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; + + await sut.On(Envelope.Create(@event)); + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_ignore_not_found_assets() + { + var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) + .Throws(new AssetNotFoundException("fileName")); + + await sut.On(Envelope.Create(@event)); + } + + [Fact] + public async Task Should_not_ignore_exceptions() + { + var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync<InvalidOperationException>(() => sut.On(Envelope.Create(@event))); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs index e6d329ea66..f84bf2a912 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs @@ -11,39 +11,38 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class AssetTagsDeleterTests { - public class AssetTagsDeleterTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly ITagService tagService = A.Fake<ITagService>(); - private readonly AssetTagsDeleter sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly ITagService tagService = A.Fake<ITagService>(); + private readonly AssetTagsDeleter sut; - public AssetTagsDeleterTests() - { - ct = cts.Token; + public AssetTagsDeleterTests() + { + ct = cts.Token; - sut = new AssetTagsDeleter(tagService); - } + sut = new AssetTagsDeleter(tagService); + } - [Fact] - public void Should_run_with_default_order() - { - var order = ((IDeleter)sut).Order; + [Fact] + public void Should_run_with_default_order() + { + var order = ((IDeleter)sut).Order; - Assert.Equal(0, order); - } + Assert.Equal(0, order); + } - [Fact] - public async Task Should_remove_events_from_streams() - { - var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + [Fact] + public async Task Should_remove_events_from_streams() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - await sut.DeleteAppAsync(app, ct); + await sut.DeleteAppAsync(app, ct); - A.CallTo(() => tagService.ClearAsync(app.Id, TagGroups.Assets, ct)) - .MustHaveHappened(); - } + A.CallTo(() => tagService.ClearAsync(app.Id, TagGroups.Assets, ct)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs index 9669816279..40adc90f51 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs @@ -16,373 +16,372 @@ using Squidex.Infrastructure.States; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class AssetUsageTrackerTests { - public class AssetUsageTrackerTests + private readonly IAssetLoader assetLoader = A.Fake<IAssetLoader>(); + private readonly ISnapshotStore<AssetUsageTracker.State> store = A.Fake<ISnapshotStore<AssetUsageTracker.State>>(); + private readonly ITagService tagService = A.Fake<ITagService>(); + private readonly IUsageGate usageGate = A.Fake<IUsageGate>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly DomainId assetId = DomainId.NewGuid(); + private readonly DomainId assetKey; + private readonly AssetUsageTracker sut; + + public AssetUsageTrackerTests() { - private readonly IAssetLoader assetLoader = A.Fake<IAssetLoader>(); - private readonly ISnapshotStore<AssetUsageTracker.State> store = A.Fake<ISnapshotStore<AssetUsageTracker.State>>(); - private readonly ITagService tagService = A.Fake<ITagService>(); - private readonly IUsageGate usageGate = A.Fake<IUsageGate>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly DomainId assetId = DomainId.NewGuid(); - private readonly DomainId assetKey; - private readonly AssetUsageTracker sut; - - public AssetUsageTrackerTests() - { - assetKey = DomainId.Combine(appId, assetId); + assetKey = DomainId.Combine(appId, assetId); - sut = new AssetUsageTracker(usageGate, assetLoader, tagService, store); - } + sut = new AssetUsageTracker(usageGate, assetLoader, tagService, store); + } - [Fact] - public void Should_return_assets_filter_for_events_filter() - { - IEventConsumer consumer = sut; + [Fact] + public void Should_return_assets_filter_for_events_filter() + { + IEventConsumer consumer = sut; - Assert.Equal("^asset-", consumer.EventsFilter); - } + Assert.Equal("^asset-", consumer.EventsFilter); + } - [Fact] - public async Task Should_do_nothing_on_clear() - { - IEventConsumer consumer = sut; + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; - await consumer.ClearAsync(); - } + await consumer.ClearAsync(); + } - [Fact] - public void Should_return_type_name_for_name() - { - IEventConsumer consumer = sut; + [Fact] + public void Should_return_type_name_for_name() + { + IEventConsumer consumer = sut; - Assert.Equal(nameof(AssetUsageTracker), consumer.Name); - } + Assert.Equal(nameof(AssetUsageTracker), consumer.Name); + } - public static IEnumerable<object[]> EventData() + public static IEnumerable<object[]> EventData() + { + yield return new object[] { - yield return new object[] - { - new AssetCreated { FileSize = 128 }, 128, 1 - }; + new AssetCreated { FileSize = 128 }, 128, 1 + }; - yield return new object[] - { - new AssetUpdated { FileSize = 512 }, 512, 0 - }; - - yield return new object[] - { - new AssetDeleted { DeletedSize = 512 }, -512, -1 - }; - } + yield return new object[] + { + new AssetUpdated { FileSize = 512 }, 512, 0 + }; - [Theory] - [MemberData(nameof(EventData))] - public async Task Should_increase_usage_if_for_event(AssetEvent @event, long sizeDiff, long countDiff) + yield return new object[] { - var date = DateTime.UtcNow.Date.AddDays(13); + new AssetDeleted { DeletedSize = 512 }, -512, -1 + }; + } - @event.AppId = appId; + [Theory] + [MemberData(nameof(EventData))] + public async Task Should_increase_usage_if_for_event(AssetEvent @event, long sizeDiff, long countDiff) + { + var date = DateTime.UtcNow.Date.AddDays(13); + + @event.AppId = appId; - var envelope = - Envelope.Create<IEvent>(@event) - .SetTimestamp(Instant.FromDateTimeUtc(date)); + var envelope = + Envelope.Create<IEvent>(@event) + .SetTimestamp(Instant.FromDateTimeUtc(date)); - await sut.On(new[] { envelope }); + await sut.On(new[] { envelope }); - A.CallTo(() => usageGate.TrackAssetAsync(appId.Id, date, sizeDiff, countDiff, default)) - .MustHaveHappened(); - } + A.CallTo(() => usageGate.TrackAssetAsync(appId.Id, date, sizeDiff, countDiff, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_write_tags_when_asset_created() + [Fact] + public async Task Should_write_tags_when_asset_created() + { + var @event = new AssetCreated { - var @event = new AssetCreated + AppId = appId, + Tags = new HashSet<string> { - AppId = appId, - Tags = new HashSet<string> - { - "tag1", - "tag2" - }, - AssetId = assetId - }; + "tag1", + "tag2" + }, + AssetId = assetId + }; - var envelope = - Envelope.Create<IEvent>(@event) - .SetAggregateId(assetKey); + var envelope = + Envelope.Create<IEvent>(@event) + .SetAggregateId(assetKey); - Dictionary<string, int>? update = null; + Dictionary<string, int>? update = null; - A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) - .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); + A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) + .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); - await sut.On(new[] { envelope }); + await sut.On(new[] { envelope }); - update.Should().BeEquivalentTo(new Dictionary<string, int> - { - ["tag1"] = 1, - ["tag2"] = 1 - }); - } + update.Should().BeEquivalentTo(new Dictionary<string, int> + { + ["tag1"] = 1, + ["tag2"] = 1 + }); + } - [Fact] - public async Task Should_group_tags_by_app() + [Fact] + public async Task Should_group_tags_by_app() + { + var @event1 = new AssetCreated { - var @event1 = new AssetCreated + AppId = appId, + Tags = new HashSet<string> { - AppId = appId, - Tags = new HashSet<string> - { - "tag1", - "tag2" - }, - AssetId = assetId - }; - - var @event2 = new AssetCreated + "tag1", + "tag2" + }, + AssetId = assetId + }; + + var @event2 = new AssetCreated + { + AppId = appId, + Tags = new HashSet<string> { - AppId = appId, - Tags = new HashSet<string> - { - "tag2", - "tag3" - }, - AssetId = assetId - }; + "tag2", + "tag3" + }, + AssetId = assetId + }; - var envelope1 = - Envelope.Create<IEvent>(@event1) - .SetAggregateId(assetKey); + var envelope1 = + Envelope.Create<IEvent>(@event1) + .SetAggregateId(assetKey); - var envelope2 = - Envelope.Create<IEvent>(@event2) - .SetAggregateId(assetKey); + var envelope2 = + Envelope.Create<IEvent>(@event2) + .SetAggregateId(assetKey); - Dictionary<string, int>? update = null; + Dictionary<string, int>? update = null; - A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) - .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); + A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) + .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); - await sut.On(new[] { envelope1, envelope2 }); + await sut.On(new[] { envelope1, envelope2 }); - update.Should().BeEquivalentTo(new Dictionary<string, int> - { - ["tag1"] = 1, - ["tag2"] = 2, - ["tag3"] = 1 - }); + update.Should().BeEquivalentTo(new Dictionary<string, int> + { + ["tag1"] = 1, + ["tag2"] = 2, + ["tag3"] = 1 + }); - A.CallTo(() => store.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<AssetUsageTracker.State>>>._, default)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => store.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<AssetUsageTracker.State>>>._, default)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_merge_tags_with_previous_event_on_annotate() + [Fact] + public async Task Should_merge_tags_with_previous_event_on_annotate() + { + var @event1 = new AssetCreated { - var @event1 = new AssetCreated + AppId = appId, + Tags = new HashSet<string> { - AppId = appId, - Tags = new HashSet<string> - { - "tag1", - "tag2" - }, - AssetId = assetId - }; - - var @event2 = new AssetAnnotated + "tag1", + "tag2" + }, + AssetId = assetId + }; + + var @event2 = new AssetAnnotated + { + AppId = appId, + Tags = new HashSet<string> { - AppId = appId, - Tags = new HashSet<string> - { - "tag2", - "tag3" - }, - AssetId = assetId - }; + "tag2", + "tag3" + }, + AssetId = assetId + }; - var envelope1 = - Envelope.Create<IEvent>(@event1) - .SetAggregateId(assetKey); + var envelope1 = + Envelope.Create<IEvent>(@event1) + .SetAggregateId(assetKey); - var envelope2 = - Envelope.Create<IEvent>(@event2) - .SetAggregateId(assetKey); + var envelope2 = + Envelope.Create<IEvent>(@event2) + .SetAggregateId(assetKey); - Dictionary<string, int>? update = null; + Dictionary<string, int>? update = null; - A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) - .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); + A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) + .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); - await sut.On(new[] { envelope1, envelope2 }); + await sut.On(new[] { envelope1, envelope2 }); - update.Should().BeEquivalentTo(new Dictionary<string, int> - { - ["tag1"] = 0, - ["tag2"] = 1, - ["tag3"] = 1 - }); - } - - [Fact] - public async Task Should_merge_tags_with_previous_event_on_annotate_from_other_batch() + update.Should().BeEquivalentTo(new Dictionary<string, int> { - var @event1 = new AssetCreated + ["tag1"] = 0, + ["tag2"] = 1, + ["tag3"] = 1 + }); + } + + [Fact] + public async Task Should_merge_tags_with_previous_event_on_annotate_from_other_batch() + { + var @event1 = new AssetCreated + { + AppId = appId, + Tags = new HashSet<string> { - AppId = appId, - Tags = new HashSet<string> - { - "tag1", - "tag2" - }, - AssetId = assetId - }; - - var @event2 = new AssetAnnotated + "tag1", + "tag2" + }, + AssetId = assetId + }; + + var @event2 = new AssetAnnotated + { + AppId = appId, + Tags = new HashSet<string> { - AppId = appId, - Tags = new HashSet<string> - { - "tag2", - "tag3" - }, - AssetId = assetId - }; + "tag2", + "tag3" + }, + AssetId = assetId + }; - var envelope1 = - Envelope.Create<IEvent>(@event1) - .SetAggregateId(assetKey); + var envelope1 = + Envelope.Create<IEvent>(@event1) + .SetAggregateId(assetKey); - var envelope2 = - Envelope.Create<IEvent>(@event2) - .SetAggregateId(assetKey); + var envelope2 = + Envelope.Create<IEvent>(@event2) + .SetAggregateId(assetKey); - Dictionary<string, int>? update = null; + Dictionary<string, int>? update = null; - A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) - .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); + A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) + .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); - await sut.On(new[] { envelope1 }); - await sut.On(new[] { envelope2 }); + await sut.On(new[] { envelope1 }); + await sut.On(new[] { envelope2 }); - update.Should().BeEquivalentTo(new Dictionary<string, int> - { - ["tag1"] = -1, - ["tag2"] = 0, - ["tag3"] = 1 - }); - } - - [Fact] - public async Task Should_merge_tags_with_previous_event_on_delete() + update.Should().BeEquivalentTo(new Dictionary<string, int> { - var @event1 = new AssetCreated + ["tag1"] = -1, + ["tag2"] = 0, + ["tag3"] = 1 + }); + } + + [Fact] + public async Task Should_merge_tags_with_previous_event_on_delete() + { + var @event1 = new AssetCreated + { + AppId = appId, + Tags = new HashSet<string> { - AppId = appId, - Tags = new HashSet<string> - { - "tag1", - "tag2" - }, - AssetId = assetId - }; + "tag1", + "tag2" + }, + AssetId = assetId + }; - var @event2 = new AssetDeleted { AppId = appId, AssetId = assetId }; + var @event2 = new AssetDeleted { AppId = appId, AssetId = assetId }; - var envelope1 = - Envelope.Create<IEvent>(@event1) - .SetAggregateId(assetKey); + var envelope1 = + Envelope.Create<IEvent>(@event1) + .SetAggregateId(assetKey); - var envelope2 = - Envelope.Create<IEvent>(@event2) - .SetAggregateId(assetKey); + var envelope2 = + Envelope.Create<IEvent>(@event2) + .SetAggregateId(assetKey); - Dictionary<string, int>? update = null; + Dictionary<string, int>? update = null; - A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) - .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); + A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) + .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); - await sut.On(new[] { Envelope.Create<IEvent>(@event1), Envelope.Create<IEvent>(@event2) }); + await sut.On(new[] { Envelope.Create<IEvent>(@event1), Envelope.Create<IEvent>(@event2) }); - update.Should().BeEquivalentTo(new Dictionary<string, int> - { - ["tag1"] = 0, - ["tag2"] = 0 - }); - } + update.Should().BeEquivalentTo(new Dictionary<string, int> + { + ["tag1"] = 0, + ["tag2"] = 0 + }); + } - [Fact] - public async Task Should_merge_tags_with_stored_state_if_previous_event_not_in_cached() + [Fact] + public async Task Should_merge_tags_with_stored_state_if_previous_event_not_in_cached() + { + var state = new AssetUsageTracker.State { - var state = new AssetUsageTracker.State + Tags = new HashSet<string> { - Tags = new HashSet<string> - { - "tag1", - "tag2" - } - }; + "tag1", + "tag2" + } + }; - A.CallTo(() => store.ReadAsync(assetKey, default)) - .Returns(new SnapshotResult<AssetUsageTracker.State>(assetKey, state, 0)); + A.CallTo(() => store.ReadAsync(assetKey, default)) + .Returns(new SnapshotResult<AssetUsageTracker.State>(assetKey, state, 0)); - var @event = new AssetDeleted { AppId = appId, AssetId = assetId }; + var @event = new AssetDeleted { AppId = appId, AssetId = assetId }; - var envelope = - Envelope.Create<IEvent>(@event) - .SetAggregateId(assetKey); + var envelope = + Envelope.Create<IEvent>(@event) + .SetAggregateId(assetKey); - Dictionary<string, int>? update = null; + Dictionary<string, int>? update = null; - A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) - .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); + A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) + .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); - await sut.On(new[] { envelope }); + await sut.On(new[] { envelope }); - update.Should().BeEquivalentTo(new Dictionary<string, int> - { - ["tag1"] = -1, - ["tag2"] = -1 - }); - } + update.Should().BeEquivalentTo(new Dictionary<string, int> + { + ["tag1"] = -1, + ["tag2"] = -1 + }); + } - [Fact] - public async Task Should_merge_tags_with_asset_if_previous_tags_not_in_store() + [Fact] + public async Task Should_merge_tags_with_asset_if_previous_tags_not_in_store() + { + IAssetEntity asset = new AssetEntity { - IAssetEntity asset = new AssetEntity + Tags = new HashSet<string> { - Tags = new HashSet<string> - { - "tag1", - "tag2" - } - }; + "tag1", + "tag2" + } + }; - A.CallTo(() => assetLoader.GetAsync(appId.Id, assetId, 41, default)) - .Returns(asset); + A.CallTo(() => assetLoader.GetAsync(appId.Id, assetId, 41, default)) + .Returns(asset); - var @event = new AssetDeleted { AppId = appId, AssetId = assetId }; + var @event = new AssetDeleted { AppId = appId, AssetId = assetId }; - var envelope = - Envelope.Create<IEvent>(@event) - .SetEventStreamNumber(42) - .SetAggregateId(assetKey); + var envelope = + Envelope.Create<IEvent>(@event) + .SetEventStreamNumber(42) + .SetAggregateId(assetKey); - Dictionary<string, int>? update = null; + Dictionary<string, int>? update = null; - A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) - .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); + A.CallTo(() => tagService.UpdateAsync(appId.Id, TagGroups.Assets, A<Dictionary<string, int>>._, default)) + .Invokes(x => { update = x.GetArgument<Dictionary<string, int>>(2); }); - await sut.On(new[] { envelope }); + await sut.On(new[] { envelope }); - update.Should().BeEquivalentTo(new Dictionary<string, int> - { - ["tag1"] = -1, - ["tag2"] = -1 - }); - } + update.Should().BeEquivalentTo(new Dictionary<string, int> + { + ["tag1"] = -1, + ["tag2"] = -1 + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs index a4a6fd5fd3..f50f5233e8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -19,367 +19,366 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class AssetsFluidExtensionTests { - public class AssetsFluidExtensionTests + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly FluidTemplateEngine sut; + + public AssetsFluidExtensionTests() { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly FluidTemplateEngine sut; - - public AssetsFluidExtensionTests() + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(assetFileStore) + .AddSingleton(assetQuery) + .AddSingleton(assetThumbnailGenerator) + .BuildServiceProvider(); + + var extensions = new IFluidExtension[] { - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(assetFileStore) - .AddSingleton(assetQuery) - .AddSingleton(assetThumbnailGenerator) - .BuildServiceProvider(); - - var extensions = new IFluidExtension[] - { - new ContentFluidExtension(), - new AssetsFluidExtension(serviceProvider) - }; - - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) - .Returns(Mocks.App(appId)); - - sut = new FluidTemplateEngine(extensions); - } + new ContentFluidExtension(), + new AssetsFluidExtension(serviceProvider) + }; - public static IEnumerable<object[]> Encodings() - { - yield return new object[] { "ascii" }; - yield return new object[] { "unicode" }; - yield return new object[] { "utf8" }; - yield return new object[] { "base64" }; - } + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) + .Returns(Mocks.App(appId)); + + sut = new FluidTemplateEngine(extensions); + } - public static byte[] Encode(string encoding, string text) + public static IEnumerable<object[]> Encodings() + { + yield return new object[] { "ascii" }; + yield return new object[] { "unicode" }; + yield return new object[] { "utf8" }; + yield return new object[] { "base64" }; + } + + public static byte[] Encode(string encoding, string text) + { + switch (encoding) { - switch (encoding) - { - case "base64": - return Convert.FromBase64String(text); - case "ascii": - return Encoding.ASCII.GetBytes(text); - case "unicode": - return Encoding.Unicode.GetBytes(text); - default: - return Encoding.UTF8.GetBytes(text); - } + case "base64": + return Convert.FromBase64String(text); + case "ascii": + return Encoding.ASCII.GetBytes(text); + case "unicode": + return Encoding.Unicode.GetBytes(text); + default: + return Encoding.UTF8.GetBytes(text); } + } - [Fact] - public async Task Should_resolve_assets_in_loop() - { - var (vars, assets) = SetupAssetsVars(); + [Fact] + public async Task Should_resolve_assets_in_loop() + { + var (vars, assets) = SetupAssetsVars(); - var template = @" + var template = @" {% for id in event.data.assets.iv %} {% asset 'ref', id %} Text: {{ ref.fileName }} {{ ref.id }} {% endfor %} "; - var expected = $@" + var expected = $@" Text: {assets[0].FileName} {assets[0].Id} Text: {assets[1].FileName} {assets[1].Id} "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_resolve_assets_in_loop_with_filter() - { - var (vars, assets) = SetupAssetsVars(); + [Fact] + public async Task Should_resolve_assets_in_loop_with_filter() + { + var (vars, assets) = SetupAssetsVars(); - var template = @" + var template = @" {% for id in event.data.assets.iv %} {% assign ref = id | asset %} Text: {{ ref.fileName }} {{ ref.id }} {% endfor %} "; - var expected = $@" + var expected = $@" Text: {assets[0].FileName} {assets[0].Id} Text: {assets[1].FileName} {assets[1].Id} "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Theory] - [MemberData(nameof(Encodings))] - public async Task Should_resolve_text(string encoding) - { - var (vars, asset) = SetupAssetVars(); + [Theory] + [MemberData(nameof(Encodings))] + public async Task Should_resolve_text(string encoding) + { + var (vars, asset) = SetupAssetVars(); - SetupText(asset.ToRef(), Encode(encoding, "hello+assets")); + SetupText(asset.ToRef(), Encode(encoding, "hello+assets")); - var template = $@" + var template = $@" {{% assign ref = event.data.assets.iv[0] | asset %}} Text: {{{{ ref | assetText: '{encoding}' }}}} "; - var expected = $@" + var expected = $@" Text: hello+assets "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_not_resolve_text_if_too_big() - { - var (vars, _) = SetupAssetVars(1_000_000); + [Fact] + public async Task Should_not_resolve_text_if_too_big() + { + var (vars, _) = SetupAssetVars(1_000_000); - var template = @" + var template = @" {% assign ref = event.data.assets.iv[0] | asset %} Text: {{ ref | assetText }} "; - var expected = $@" + var expected = $@" Text: ErrorTooBig "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); - A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } + + [Theory] + [MemberData(nameof(Encodings))] + public async Task Should_resolve_text_from_event(string encoding) + { + var @event = new EnrichedAssetEvent + { + Id = DomainId.NewGuid(), + FileVersion = 0, + FileSize = 100, + AppId = appId + }; - [Theory] - [MemberData(nameof(Encodings))] - public async Task Should_resolve_text_from_event(string encoding) + SetupText(@event.ToRef(), Encode(encoding, "hello+assets")); + + var vars = new TemplateVars { - var @event = new EnrichedAssetEvent - { - Id = DomainId.NewGuid(), - FileVersion = 0, - FileSize = 100, - AppId = appId - }; - - SetupText(@event.ToRef(), Encode(encoding, "hello+assets")); - - var vars = new TemplateVars - { - ["event"] = @event - }; - - var template = $@" + ["event"] = @event + }; + + var template = $@" Text: {{{{ event | assetText: '{encoding}' }}}} "; - var expected = $@" + var expected = $@" Text: hello+assets "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_resolve_blur_hash() - { - var (vars, asset) = SetupAssetVars(); + [Fact] + public async Task Should_resolve_blur_hash() + { + var (vars, asset) = SetupAssetVars(); - SetupBlurHash(asset.ToRef(), "Hash"); + SetupBlurHash(asset.ToRef(), "Hash"); - var template = @" + var template = @" {% assign ref = event.data.assets.iv[0] | asset %} Text: {{ ref | assetBlurHash: 3,4 }} "; - var expected = $@" + var expected = $@" Text: Hash "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_not_resolve_blur_hash_if_too_big() - { - var (vars, _) = SetupAssetVars(1_000_000); + [Fact] + public async Task Should_not_resolve_blur_hash_if_too_big() + { + var (vars, _) = SetupAssetVars(1_000_000); - var template = @" + var template = @" {% assign ref = event.data.assets.iv[0] | asset %} Text: {{ ref | assetBlurHash }} "; - var expected = $@" + var expected = $@" Text: ErrorTooBig "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); - A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_resolve_blur_hash_if_not_an_image() - { - var (vars, _) = SetupAssetVars(type: AssetType.Unknown); + [Fact] + public async Task Should_not_resolve_blur_hash_if_not_an_image() + { + var (vars, _) = SetupAssetVars(type: AssetType.Unknown); - var template = @" + var template = @" {% assign ref = event.data.assets.iv[0] | asset %} Text: {{ ref | assetBlurHash }} "; - var expected = $@" + var expected = $@" Text: NoImage "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); - A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_resolve_blur_hash_from_event() + { + var @event = new EnrichedAssetEvent + { + Id = DomainId.NewGuid(), + AssetType = AssetType.Image, + FileVersion = 0, + FileSize = 100, + AppId = appId + }; + + SetupBlurHash(@event.ToRef(), "Hash"); - [Fact] - public async Task Should_resolve_blur_hash_from_event() + var vars = new TemplateVars { - var @event = new EnrichedAssetEvent - { - Id = DomainId.NewGuid(), - AssetType = AssetType.Image, - FileVersion = 0, - FileSize = 100, - AppId = appId - }; - - SetupBlurHash(@event.ToRef(), "Hash"); - - var vars = new TemplateVars - { - ["event"] = @event - }; - - var template = @" + ["event"] = @event + }; + + var template = @" Text: {{ event | assetBlurHash }} "; - var expected = $@" + var expected = $@" Text: Hash "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - private void SetupBlurHash(AssetRef asset, string hash) - { - A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._)) - .Returns(hash); - } + private void SetupBlurHash(AssetRef asset, string hash) + { + A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._)) + .Returns(hash); + } - private void SetupText(AssetRef asset, byte[] bytes) - { - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, asset.Id, asset.FileVersion, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) - .Invokes(x => x.GetArgument<Stream>(4)?.Write(bytes)); - } + private void SetupText(AssetRef asset, byte[] bytes) + { + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, asset.Id, asset.FileVersion, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) + .Invokes(x => x.GetArgument<Stream>(4)?.Write(bytes)); + } + + private (TemplateVars, IAssetEntity) SetupAssetVars(int fileSize = 100, AssetType type = AssetType.Image) + { + var assetId = DomainId.NewGuid(); + var asset = CreateAsset(assetId, 1, fileSize, type); - private (TemplateVars, IAssetEntity) SetupAssetVars(int fileSize = 100, AssetType type = AssetType.Image) + var @event = new EnrichedContentEvent { - var assetId = DomainId.NewGuid(); - var asset = CreateAsset(assetId, 1, fileSize, type); - - var @event = new EnrichedContentEvent - { - Data = - new ContentData() - .AddField("assets", - new ContentFieldData() - .AddInvariant(JsonValue.Array(assetId))), - AppId = appId - }; - - A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId, EtagVersion.Any, A<CancellationToken>._)) - .Returns(asset); - - var vars = new TemplateVars - { - ["event"] = @event - }; - - return (vars, asset); - } + Data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(assetId))), + AppId = appId + }; + + A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId, EtagVersion.Any, A<CancellationToken>._)) + .Returns(asset); + + var vars = new TemplateVars + { + ["event"] = @event + }; - private (TemplateVars, IAssetEntity[]) SetupAssetsVars(int fileSize = 100, AssetType type = AssetType.Image) + return (vars, asset); + } + + private (TemplateVars, IAssetEntity[]) SetupAssetsVars(int fileSize = 100, AssetType type = AssetType.Image) + { + var assetId1 = DomainId.NewGuid(); + var asset1 = CreateAsset(assetId1, 1, fileSize, type); + var assetId2 = DomainId.NewGuid(); + var asset2 = CreateAsset(assetId2, 2, fileSize, type); + + var @event = new EnrichedContentEvent { - var assetId1 = DomainId.NewGuid(); - var asset1 = CreateAsset(assetId1, 1, fileSize, type); - var assetId2 = DomainId.NewGuid(); - var asset2 = CreateAsset(assetId2, 2, fileSize, type); - - var @event = new EnrichedContentEvent - { - Data = - new ContentData() - .AddField("assets", - new ContentFieldData() - .AddInvariant(JsonValue.Array(assetId1, assetId2))), - AppId = appId - }; - - A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId1, EtagVersion.Any, A<CancellationToken>._)) - .Returns(asset1); - - A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId2, EtagVersion.Any, A<CancellationToken>._)) - .Returns(asset2); - - var vars = new TemplateVars - { - ["event"] = @event - }; - - return (vars, new[] { asset1, asset2 }); - } + Data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(assetId1, assetId2))), + AppId = appId + }; - private IEnrichedAssetEntity CreateAsset(DomainId assetId, int index, int fileSize = 100, AssetType type = AssetType.Unknown) + A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId1, EtagVersion.Any, A<CancellationToken>._)) + .Returns(asset1); + + A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId2, EtagVersion.Any, A<CancellationToken>._)) + .Returns(asset2); + + var vars = new TemplateVars { - return new AssetEntity - { - AppId = appId, - Id = assetId, - FileSize = fileSize, - FileName = $"file{index}.jpg", - MimeType = "image/jpg", - Type = type - }; - } + ["event"] = @event + }; + + return (vars, new[] { asset1, asset2 }); + } - private static string Cleanup(string text) + private IEnrichedAssetEntity CreateAsset(DomainId assetId, int index, int fileSize = 100, AssetType type = AssetType.Unknown) + { + return new AssetEntity { - return text - .Replace("\r", string.Empty, StringComparison.Ordinal) - .Replace("\n", string.Empty, StringComparison.Ordinal) - .Replace(" ", string.Empty, StringComparison.Ordinal); - } + AppId = appId, + Id = assetId, + FileSize = fileSize, + FileName = $"file{index}.jpg", + MimeType = "image/jpg", + Type = type + }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty, StringComparison.Ordinal) + .Replace("\n", string.Empty, StringComparison.Ordinal) + .Replace(" ", string.Empty, StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs index 346721556c..ff810a4161 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -22,99 +22,99 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class AssetsJintExtensionTests : IClassFixture<TranslationsFixture> { - public class AssetsJintExtensionTests : IClassFixture<TranslationsFixture> + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly JintScriptEngine sut; + + public AssetsJintExtensionTests() { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly JintScriptEngine sut; - - public AssetsJintExtensionTests() + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(assetFileStore) + .AddSingleton(assetQuery) + .AddSingleton(assetThumbnailGenerator) + .BuildServiceProvider(); + + var extensions = new IJintExtension[] { - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(assetFileStore) - .AddSingleton(assetQuery) - .AddSingleton(assetThumbnailGenerator) - .BuildServiceProvider(); - - var extensions = new IJintExtension[] + new AssetsJintExtension(serviceProvider) + }; + + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, A<CancellationToken>._)) + .Returns(Mocks.App(appId)); + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions { - new AssetsJintExtension(serviceProvider) - }; - - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, A<CancellationToken>._)) - .Returns(Mocks.App(appId)); - - sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - }), - extensions); - } + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + }), + extensions); + } - public static IEnumerable<object[]> Encodings() - { - yield return new object[] { "ascii" }; - yield return new object[] { "unicode" }; - yield return new object[] { "utf8" }; - yield return new object[] { "base64" }; - } + public static IEnumerable<object[]> Encodings() + { + yield return new object[] { "ascii" }; + yield return new object[] { "unicode" }; + yield return new object[] { "utf8" }; + yield return new object[] { "base64" }; + } - public static byte[] Encode(string encoding, string text) + public static byte[] Encode(string encoding, string text) + { + switch (encoding) { - switch (encoding) - { - case "base64": - return Convert.FromBase64String(text); - case "ascii": - return Encoding.ASCII.GetBytes(text); - case "unicode": - return Encoding.Unicode.GetBytes(text); - default: - return Encoding.UTF8.GetBytes(text); - } + case "base64": + return Convert.FromBase64String(text); + case "ascii": + return Encoding.ASCII.GetBytes(text); + case "unicode": + return Encoding.Unicode.GetBytes(text); + default: + return Encoding.UTF8.GetBytes(text); } + } - [Fact] - public async Task Should_resolve_asset() - { - var (vars, assets) = SetupAssetsVars(1); + [Fact] + public async Task Should_resolve_asset() + { + var (vars, assets) = SetupAssetsVars(1); - var expected = $@" + var expected = $@" Text: {assets[0].FileName} {assets[0].Id} "; - var script = @" + var script = @" getAsset(data.assets.iv[0], function (assets) { var actual1 = `Text: ${assets[0].fileName} ${assets[0].id}`; complete(`${actual1}`); });"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_resolve_assets() - { - var (vars, assets) = SetupAssetsVars(2); + [Fact] + public async Task Should_resolve_assets() + { + var (vars, assets) = SetupAssetsVars(2); - var expected = $@" + var expected = $@" Text: {assets[0].FileName} {assets[0].Id} Text: {assets[1].FileName} {assets[1].Id} "; - var script = @" + var script = @" getAssets(data.assets.iv, function (assets) { var actual1 = `Text: ${assets[0].fileName} ${assets[0].id}`; var actual2 = `Text: ${assets[1].fileName} ${assets[1].id}`; @@ -122,24 +122,24 @@ public async Task Should_resolve_assets() complete(`${actual1}\n${actual2}`); });"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Theory] - [MemberData(nameof(Encodings))] - public async Task Should_resolve_text(string encoding) - { - var (vars, assets) = SetupAssetsVars(1); + [Theory] + [MemberData(nameof(Encodings))] + public async Task Should_resolve_text(string encoding) + { + var (vars, assets) = SetupAssetsVars(1); - SetupText(assets[0].ToRef(), Encode(encoding, "hello+assets")); + SetupText(assets[0].ToRef(), Encode(encoding, "hello+assets")); - var expected = @" + var expected = @" Text: hello+assets "; - var script = $@" + var script = $@" getAssets(data.assets.iv, function (assets) {{ getAssetText(assets[0], function (text) {{ var actual = `Text: ${{text}}`; @@ -148,21 +148,21 @@ public async Task Should_resolve_text(string encoding) }}, '{encoding}'); }});"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_not_resolve_text_if_too_big() - { - var (vars, _) = SetupAssetsVars(1, 1_000_000); + [Fact] + public async Task Should_not_resolve_text_if_too_big() + { + var (vars, _) = SetupAssetsVars(1, 1_000_000); - var expected = @" + var expected = @" Text: ErrorTooBig "; - var script = @" + var script = @" getAssets(data.assets.iv, function (assets) { getAssetText(assets[0], function (text) { var actual = `Text: ${text}`; @@ -171,61 +171,61 @@ public async Task Should_not_resolve_text_if_too_big() }); });"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); + Assert.Equal(Cleanup(expected), Cleanup(actual)); - A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetFileStore.DownloadAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Theory] - [MemberData(nameof(Encodings))] - public async Task Should_resolve_text_from_event(string encoding) + [Theory] + [MemberData(nameof(Encodings))] + public async Task Should_resolve_text_from_event(string encoding) + { + var @event = new EnrichedAssetEvent { - var @event = new EnrichedAssetEvent - { - Id = DomainId.NewGuid(), - FileVersion = 0, - FileSize = 100, - AppId = appId - }; + Id = DomainId.NewGuid(), + FileVersion = 0, + FileSize = 100, + AppId = appId + }; - SetupText(@event.ToRef(), Encode(encoding, "hello+assets")); + SetupText(@event.ToRef(), Encode(encoding, "hello+assets")); - var vars = new ScriptVars - { - ["event"] = @event - }; + var vars = new ScriptVars + { + ["event"] = @event + }; - var expected = @" + var expected = @" Text: hello+assets "; - var script = $@" + var script = $@" getAssetText(event, function (text) {{ var actual = `Text: ${{text}}`; complete(actual); }}, '{encoding}');"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_resolve_blur_hash() - { - var (vars, assets) = SetupAssetsVars(1); + [Fact] + public async Task Should_resolve_blur_hash() + { + var (vars, assets) = SetupAssetsVars(1); - SetupBlurHash(assets[0].ToRef(), "Hash"); + SetupBlurHash(assets[0].ToRef(), "Hash"); - var expected = @" + var expected = @" Hash: Hash "; - var script = @" + var script = @" getAssets(data.assets.iv, function (assets) { getAssetBlurHash(assets[0], function (text) { var actual = `Hash: ${text}`; @@ -234,23 +234,23 @@ public async Task Should_resolve_blur_hash() }); });"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_not_resolve_blur_hash_if_too_big() - { - var (vars, assets) = SetupAssetsVars(1, 1_000_000); + [Fact] + public async Task Should_not_resolve_blur_hash_if_too_big() + { + var (vars, assets) = SetupAssetsVars(1, 1_000_000); - SetupBlurHash(assets[0].ToRef(), "Hash"); + SetupBlurHash(assets[0].ToRef(), "Hash"); - var expected = @" + var expected = @" Hash: null "; - var script = @" + var script = @" getAssets(data.assets.iv, function (assets) { getAssetBlurHash(assets[0], function (text) { var actual = `Hash: ${text}`; @@ -259,23 +259,23 @@ public async Task Should_not_resolve_blur_hash_if_too_big() }); });"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_not_resolve_blue_hash_if_not_an_image() - { - var (vars, assets) = SetupAssetsVars(1, type: AssetType.Audio); + [Fact] + public async Task Should_not_resolve_blue_hash_if_not_an_image() + { + var (vars, assets) = SetupAssetsVars(1, type: AssetType.Audio); - SetupBlurHash(assets[0].ToRef(), "Hash"); + SetupBlurHash(assets[0].ToRef(), "Hash"); - var expected = @" + var expected = @" Hash: null "; - var script = @" + var script = @" getAssets(data.assets.iv, function (assets) { getAssetBlurHash(assets[0], function (text) { var actual = `Hash: ${text}`; @@ -284,105 +284,104 @@ public async Task Should_not_resolve_blue_hash_if_not_an_image() }); });"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_resolve_blur_hash_from_event() + [Fact] + public async Task Should_resolve_blur_hash_from_event() + { + var @event = new EnrichedAssetEvent { - var @event = new EnrichedAssetEvent - { - Id = DomainId.NewGuid(), - AssetType = AssetType.Image, - FileVersion = 0, - FileSize = 100, - AppId = appId - }; + Id = DomainId.NewGuid(), + AssetType = AssetType.Image, + FileVersion = 0, + FileSize = 100, + AppId = appId + }; - SetupBlurHash(@event.ToRef(), "Hash"); + SetupBlurHash(@event.ToRef(), "Hash"); - var vars = new ScriptVars - { - ["event"] = @event - }; + var vars = new ScriptVars + { + ["event"] = @event + }; - var expected = @" + var expected = @" Text: Hash "; - var script = @" + var script = @" getAssetBlurHash(event, function (text) { var actual = `Text: ${text}`; complete(actual); });"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - private void SetupBlurHash(AssetRef asset, string hash) - { - A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._)) - .Returns(hash); - } + private void SetupBlurHash(AssetRef asset, string hash) + { + A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._)) + .Returns(hash); + } - private void SetupText(AssetRef asset, byte[] bytes) - { - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, asset.Id, asset.FileVersion, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) - .Invokes(x => x.GetArgument<Stream>(4)?.Write(bytes)); - } + private void SetupText(AssetRef asset, byte[] bytes) + { + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, asset.Id, asset.FileVersion, null, A<Stream>._, A<BytesRange>._, A<CancellationToken>._)) + .Invokes(x => x.GetArgument<Stream>(4)?.Write(bytes)); + } - private (ScriptVars, IAssetEntity[]) SetupAssetsVars(int count, int fileSize = 100, AssetType type = AssetType.Image) - { - var assets = Enumerable.Range(0, count).Select(x => CreateAsset(1, fileSize, type)).ToArray(); - var assetIds = assets.Select(x => x.Id); + private (ScriptVars, IAssetEntity[]) SetupAssetsVars(int count, int fileSize = 100, AssetType type = AssetType.Image) + { + var assets = Enumerable.Range(0, count).Select(x => CreateAsset(1, fileSize, type)).ToArray(); + var assetIds = assets.Select(x => x.Id); - var user = new ClaimsPrincipal(); + var user = new ClaimsPrincipal(); - var data = - new ContentData() - .AddField("assets", - new ContentFieldData() - .AddInvariant(JsonValue.Array(assetIds))); + var data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(assetIds))); - A.CallTo(() => assetQuery.QueryAsync( - A<Context>.That.Matches(x => x.App.Id == appId.Id && x.UserPrincipal == user), null, A<Q>.That.HasIds(assetIds), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(2, assets)); + A.CallTo(() => assetQuery.QueryAsync( + A<Context>.That.Matches(x => x.App.Id == appId.Id && x.UserPrincipal == user), null, A<Q>.That.HasIds(assetIds), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(2, assets)); - var vars = new ScriptVars - { - ["data"] = data, - ["appId"] = appId.Id, - ["appName"] = appId.Name, - ["user"] = user - }; + var vars = new ScriptVars + { + ["data"] = data, + ["appId"] = appId.Id, + ["appName"] = appId.Name, + ["user"] = user + }; - return (vars, assets); - } + return (vars, assets); + } - private IEnrichedAssetEntity CreateAsset(int index, int fileSize = 100, AssetType type = AssetType.Image) + private IEnrichedAssetEntity CreateAsset(int index, int fileSize = 100, AssetType type = AssetType.Image) + { + return new AssetEntity { - return new AssetEntity - { - AppId = appId, - Id = DomainId.NewGuid(), - FileSize = fileSize, - FileName = $"file{index}.jpg", - MimeType = "image/jpg", - Type = type - }; - } + AppId = appId, + Id = DomainId.NewGuid(), + FileSize = fileSize, + FileName = $"file{index}.jpg", + MimeType = "image/jpg", + Type = type + }; + } - private static string Cleanup(string text) - { - return text - .Replace("\r", string.Empty, StringComparison.Ordinal) - .Replace("\n", string.Empty, StringComparison.Ordinal) - .Replace(" ", string.Empty, StringComparison.Ordinal); - } + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty, StringComparison.Ordinal) + .Replace("\n", string.Empty, StringComparison.Ordinal) + .Replace(" ", string.Empty, StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsSearchSourceTests.cs index c944725b67..f6e71b5d33 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsSearchSourceTests.cs @@ -16,76 +16,75 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class AssetsSearchSourceTests { - public class AssetsSearchSourceTests + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly AssetsSearchSource sut; + + public AssetsSearchSourceTests() { - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly AssetsSearchSource sut; + sut = new AssetsSearchSource(assetQuery, urlGenerator); + } - public AssetsSearchSourceTests() - { - sut = new AssetsSearchSource(assetQuery, urlGenerator); - } + [Fact] + public async Task Should_return_empty_actuals_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_return_empty_actuals_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var actual = await sut.SearchAsync("logo", ctx, default); - var actual = await sut.SearchAsync("logo", ctx, default); + Assert.Empty(actual); - Assert.Empty(actual); + A.CallTo(() => assetQuery.QueryAsync(A<Context>._, A<DomainId?>._, A<Q>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => assetQuery.QueryAsync(A<Context>._, A<DomainId?>._, A<Q>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_return_assets_actuals_if_found() + { + var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appId.Name); - [Fact] - public async Task Should_return_assets_actuals_if_found() - { - var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appId.Name); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + var asset1 = CreateAsset("logo1.png"); + var asset2 = CreateAsset("logo2.png"); - var asset1 = CreateAsset("logo1.png"); - var asset2 = CreateAsset("logo2.png"); + A.CallTo(() => urlGenerator.AssetsUI(appId, asset1.Id.ToString())) + .Returns("assets-url1"); - A.CallTo(() => urlGenerator.AssetsUI(appId, asset1.Id.ToString())) - .Returns("assets-url1"); + A.CallTo(() => urlGenerator.AssetsUI(appId, asset2.Id.ToString())) + .Returns("assets-url2"); - A.CallTo(() => urlGenerator.AssetsUI(appId, asset2.Id.ToString())) - .Returns("assets-url2"); + A.CallTo(() => assetQuery.QueryAsync(ctx, null, A<Q>.That.HasQuery("Filter: contains(fileName, 'logo'); Take: 5"), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(2, asset1, asset2)); - A.CallTo(() => assetQuery.QueryAsync(ctx, null, A<Q>.That.HasQuery("Filter: contains(fileName, 'logo'); Take: 5"), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(2, asset1, asset2)); + var actual = await sut.SearchAsync("logo", ctx, default); - var actual = await sut.SearchAsync("logo", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("logo1.png", SearchResultType.Asset, "assets-url1") + .Add("logo2.png", SearchResultType.Asset, "assets-url2")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("logo1.png", SearchResultType.Asset, "assets-url1") - .Add("logo2.png", SearchResultType.Asset, "assets-url2")); - } + private static IEnrichedAssetEntity CreateAsset(string fileName) + { + return new AssetEntity { FileName = fileName, Id = DomainId.NewGuid() }; + } - private static IEnrichedAssetEntity CreateAsset(string fileName) - { - return new AssetEntity { FileName = fileName, Id = DomainId.NewGuid() }; - } + private Context ContextWithPermission(string? permission = null) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - private Context ContextWithPermission(string? permission = null) + if (permission != null) { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - - if (permission != null) - { - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); - } - - return new Context(claimsPrincipal, Mocks.App(appId)); + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); } + + return new Context(claimsPrincipal, Mocks.App(appId)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs index f503ab58fc..51df6f8cbf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs @@ -17,394 +17,393 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class BackupAssetsTests { - public class BackupAssetsTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); + private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); + private readonly ITagService tagService = A.Fake<ITagService>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RefToken actor = RefToken.User("123"); + private readonly BackupAssets sut; + + public BackupAssetsTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); - private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); - private readonly ITagService tagService = A.Fake<ITagService>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RefToken actor = RefToken.User("123"); - private readonly BackupAssets sut; - - public BackupAssetsTests() - { - ct = cts.Token; + ct = cts.Token; - sut = new BackupAssets(rebuilder, assetFileStore, tagService); - } + sut = new BackupAssets(rebuilder, assetFileStore, tagService); + } - [Fact] - public void Should_provide_name() - { - Assert.Equal("Assets", sut.Name); - } + [Fact] + public void Should_provide_name() + { + Assert.Equal("Assets", sut.Name); + } - [Fact] - public async Task Should_write_tags() + [Fact] + public async Task Should_write_tags() + { + var tags = new TagsExport { - var tags = new TagsExport - { - Tags = new Dictionary<string, Tag>() - }; + Tags = new Dictionary<string, Tag>() + }; - var context = CreateBackupContext(); + var context = CreateBackupContext(); - A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets, ct)) - .Returns(tags); + A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets, ct)) + .Returns(tags); - await sut.BackupAsync(context, ct); + await sut.BackupAsync(context, ct); - A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, tags.Tags, ct)) - .MustHaveHappened(); + A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, tags.Tags, ct)) + .MustHaveHappened(); - A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, tags.Alias!, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, tags.Alias!, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_write_tags_with_alias() + [Fact] + public async Task Should_write_tags_with_alias() + { + var tags = new TagsExport { - var tags = new TagsExport + Alias = new Dictionary<string, string> { - Alias = new Dictionary<string, string> - { - ["tag1"] = "new-name" - }, - Tags = new Dictionary<string, Tag>() - }; - - var context = CreateBackupContext(); + ["tag1"] = "new-name" + }, + Tags = new Dictionary<string, Tag>() + }; - A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets, ct)) - .Returns(tags); + var context = CreateBackupContext(); - await sut.BackupAsync(context, ct); + A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets, ct)) + .Returns(tags); - A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, tags.Tags, ct)) - .MustHaveHappened(); + await sut.BackupAsync(context, ct); - A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, tags.Alias, ct)) - .MustHaveHappened(); - } + A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, tags.Tags, ct)) + .MustHaveHappened(); - [Fact] - public async Task Should_read_tags_if_file_exists() - { - var tags = new Dictionary<string, Tag>(); + A.CallTo(() => context.Writer.WriteJsonAsync(A<string>._, tags.Alias, ct)) + .MustHaveHappened(); + } - var context = CreateRestoreContext(); + [Fact] + public async Task Should_read_tags_if_file_exists() + { + var tags = new Dictionary<string, Tag>(); - var envelope = - new Envelope<IEvent>(new AppCreated - { - AppId = appId - }); + var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.HasFileAsync(A<string>._, ct)) - .Returns(true); + var envelope = + new Envelope<IEvent>(new AppCreated + { + AppId = appId + }); - A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, Tag>>(A<string>._, ct)) - .Returns(tags); + A.CallTo(() => context.Reader.HasFileAsync(A<string>._, ct)) + .Returns(true); - await sut.RestoreEventAsync(envelope, context, ct); + A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, Tag>>(A<string>._, ct)) + .Returns(tags); - A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A<TagsExport>.That.Matches(x => x.Tags == tags), ct)) - .MustHaveHappened(); - } + await sut.RestoreEventAsync(envelope, context, ct); - [Fact] - public async Task Should_read_tags_alias_if_file_exists() - { - var alias = new Dictionary<string, string>(); + A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A<TagsExport>.That.Matches(x => x.Tags == tags), ct)) + .MustHaveHappened(); + } - var context = CreateRestoreContext(); + [Fact] + public async Task Should_read_tags_alias_if_file_exists() + { + var alias = new Dictionary<string, string>(); - var envelope = - new Envelope<IEvent>(new AppCreated - { - AppId = appId - }); + var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.HasFileAsync(A<string>._, ct)) - .Returns(false).Once().Then.Returns(true); + var envelope = + new Envelope<IEvent>(new AppCreated + { + AppId = appId + }); - A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, string>>(A<string>._, ct)) - .Returns(alias); + A.CallTo(() => context.Reader.HasFileAsync(A<string>._, ct)) + .Returns(false).Once().Then.Returns(true); - await sut.RestoreEventAsync(envelope, context, ct); + A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, string>>(A<string>._, ct)) + .Returns(alias); - A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A<TagsExport>.That.Matches(x => x.Alias == alias), ct)) - .MustHaveHappened(); - } + await sut.RestoreEventAsync(envelope, context, ct); - [Fact] - public async Task Should_not_read_tags_if_no_file_exists() - { - var alias = new Dictionary<string, string>(); + A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A<TagsExport>.That.Matches(x => x.Alias == alias), ct)) + .MustHaveHappened(); + } - var context = CreateRestoreContext(); + [Fact] + public async Task Should_not_read_tags_if_no_file_exists() + { + var alias = new Dictionary<string, string>(); - var envelope = - new Envelope<IEvent>(new AppCreated - { - AppId = appId - }); + var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.HasFileAsync(A<string>._, ct)) - .Returns(false); + var envelope = + new Envelope<IEvent>(new AppCreated + { + AppId = appId + }); - A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, string>>(A<string>._, ct)) - .Returns(alias); + A.CallTo(() => context.Reader.HasFileAsync(A<string>._, ct)) + .Returns(false); - await sut.RestoreEventAsync(envelope, context, ct); + A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, string>>(A<string>._, ct)) + .Returns(alias); - A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, string>>(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + await sut.RestoreEventAsync(envelope, context, ct); - A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, Tag>>(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, string>>(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A<TagsExport>.That.Matches(x => x.Alias == alias), A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => context.Reader.ReadJsonAsync<Dictionary<string, Tag>>(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - [Fact] - public async Task Should_backup_created_asset() - { - var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; + A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A<TagsExport>.That.Matches(x => x.Alias == alias), A<CancellationToken>._)) + .MustNotHaveHappened(); + } - await TestBackupAsync(@event, 0); - } + [Fact] + public async Task Should_backup_created_asset() + { + var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; - [Fact] - public async Task Should_backup_created_asset_with_missing_file() - { - var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; + await TestBackupAsync(@event, 0); + } - await TestBackupFailedAsync(@event, 0); - } + [Fact] + public async Task Should_backup_created_asset_with_missing_file() + { + var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; - [Fact] - public async Task Should_backup_updated_asset() - { - var @event = new AssetUpdated { AssetId = DomainId.NewGuid(), FileVersion = 3 }; + await TestBackupFailedAsync(@event, 0); + } - await TestBackupAsync(@event, @event.FileVersion); - } + [Fact] + public async Task Should_backup_updated_asset() + { + var @event = new AssetUpdated { AssetId = DomainId.NewGuid(), FileVersion = 3 }; - [Fact] - public async Task Should_backup_updated_asset_with_missing_file() - { - var @event = new AssetUpdated { AssetId = DomainId.NewGuid(), FileVersion = 3 }; + await TestBackupAsync(@event, @event.FileVersion); + } - await TestBackupFailedAsync(@event, @event.FileVersion); - } + [Fact] + public async Task Should_backup_updated_asset_with_missing_file() + { + var @event = new AssetUpdated { AssetId = DomainId.NewGuid(), FileVersion = 3 }; - private async Task TestBackupAsync(AssetEvent @event, long version) - { - var assetStream = new MemoryStream(); - var assetId = @event.AssetId; + await TestBackupFailedAsync(@event, @event.FileVersion); + } - var context = CreateBackupContext(); + private async Task TestBackupAsync(AssetEvent @event, long version) + { + var assetStream = new MemoryStream(); + var assetId = @event.AssetId; - A.CallTo(() => context.Writer.OpenBlobAsync($"{assetId}_{version}.asset", ct)) - .Returns(assetStream); + var context = CreateBackupContext(); - await sut.BackupEventAsync(AppEvent(@event), context, ct); + A.CallTo(() => context.Writer.OpenBlobAsync($"{assetId}_{version}.asset", ct)) + .Returns(assetStream); - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, ct)) - .MustHaveHappened(); - } + await sut.BackupEventAsync(AppEvent(@event), context, ct); - private async Task TestBackupFailedAsync(AssetEvent @event, long version) - { - var assetStream = new MemoryStream(); - var assetId = @event.AssetId; + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, ct)) + .MustHaveHappened(); + } - var context = CreateBackupContext(); + private async Task TestBackupFailedAsync(AssetEvent @event, long version) + { + var assetStream = new MemoryStream(); + var assetId = @event.AssetId; - A.CallTo(() => context.Writer.OpenBlobAsync($"{assetId}_{version}.asset", ct)) - .Returns(assetStream); + var context = CreateBackupContext(); - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, ct)) - .Throws(new AssetNotFoundException(assetId.ToString())); + A.CallTo(() => context.Writer.OpenBlobAsync($"{assetId}_{version}.asset", ct)) + .Returns(assetStream); - await sut.BackupEventAsync(AppEvent(@event), context, ct); - } + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, ct)) + .Throws(new AssetNotFoundException(assetId.ToString())); - [Fact] - public async Task Should_restore_created_asset() - { - var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; + await sut.BackupEventAsync(AppEvent(@event), context, ct); + } - await TestRestoreAsync(@event, 0); - } + [Fact] + public async Task Should_restore_created_asset() + { + var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; - [Fact] - public async Task Should_restore_created_asset_with_missing_file() - { - var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; + await TestRestoreAsync(@event, 0); + } - await TestRestoreFailedAsync(@event, 0); - } + [Fact] + public async Task Should_restore_created_asset_with_missing_file() + { + var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; - [Fact] - public async Task Should_restore_updated_asset() - { - var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; + await TestRestoreFailedAsync(@event, 0); + } - await TestRestoreAsync(@event, @event.FileVersion); - } + [Fact] + public async Task Should_restore_updated_asset() + { + var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; - [Fact] - public async Task Should_restore_updated_asset_with_missing_file() - { - var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; + await TestRestoreAsync(@event, @event.FileVersion); + } - await TestRestoreFailedAsync(@event, @event.FileVersion); - } + [Fact] + public async Task Should_restore_updated_asset_with_missing_file() + { + var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; - private async Task TestRestoreAsync(AssetEvent @event, long version) - { - var assetStream = new MemoryStream(); - var assetId = @event.AssetId; + await TestRestoreFailedAsync(@event, @event.FileVersion); + } - var context = CreateRestoreContext(); + private async Task TestRestoreAsync(AssetEvent @event, long version) + { + var assetStream = new MemoryStream(); + var assetId = @event.AssetId; - A.CallTo(() => context.Reader.OpenBlobAsync($"{assetId}_{version}.asset", ct)) - .Returns(assetStream); + var context = CreateRestoreContext(); - await sut.RestoreEventAsync(AppEvent(@event), context, ct); + A.CallTo(() => context.Reader.OpenBlobAsync($"{assetId}_{version}.asset", ct)) + .Returns(assetStream); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, ct)) - .MustHaveHappened(); - } + await sut.RestoreEventAsync(AppEvent(@event), context, ct); - private async Task TestRestoreFailedAsync(AssetEvent @event, long version) - { - var assetStream = new MemoryStream(); - var assetId = @event.AssetId; + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, ct)) + .MustHaveHappened(); + } - var context = CreateRestoreContext(); + private async Task TestRestoreFailedAsync(AssetEvent @event, long version) + { + var assetStream = new MemoryStream(); + var assetId = @event.AssetId; - A.CallTo(() => context.Reader.OpenBlobAsync($"{assetId}_{version}.asset", ct)) - .Throws(new FileNotFoundException()); + var context = CreateRestoreContext(); - await sut.RestoreEventAsync(AppEvent(@event), context, ct); + A.CallTo(() => context.Reader.OpenBlobAsync($"{assetId}_{version}.asset", ct)) + .Throws(new FileNotFoundException()); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + await sut.RestoreEventAsync(AppEvent(@event), context, ct); - [Fact] - public async Task Should_restore_states_for_all_assets() - { - var assetId1 = DomainId.NewGuid(); - var assetId2 = DomainId.NewGuid(); + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - var context = CreateRestoreContext(); + [Fact] + public async Task Should_restore_states_for_all_assets() + { + var assetId1 = DomainId.NewGuid(); + var assetId2 = DomainId.NewGuid(); - await sut.RestoreEventAsync(AppEvent(new AssetCreated - { - AssetId = assetId1 - }), context, ct); + var context = CreateRestoreContext(); - await sut.RestoreEventAsync(AppEvent(new AssetCreated - { - AssetId = assetId2 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new AssetCreated + { + AssetId = assetId1 + }), context, ct); - await sut.RestoreEventAsync(AppEvent(new AssetDeleted - { - AssetId = assetId2 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new AssetCreated + { + AssetId = assetId2 + }), context, ct); - var rebuildAssets = new HashSet<DomainId>(); + await sut.RestoreEventAsync(AppEvent(new AssetDeleted + { + AssetId = assetId2 + }), context, ct); - A.CallTo(() => rebuilder.InsertManyAsync<AssetDomainObject, AssetDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) - .Invokes(x => rebuildAssets.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); + var rebuildAssets = new HashSet<DomainId>(); - await sut.RestoreAsync(context, ct); + A.CallTo(() => rebuilder.InsertManyAsync<AssetDomainObject, AssetDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) + .Invokes(x => rebuildAssets.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); - Assert.Equal(new HashSet<DomainId> - { - DomainId.Combine(appId, assetId1), - DomainId.Combine(appId, assetId2) - }, rebuildAssets); - } + await sut.RestoreAsync(context, ct); - [Fact] - public async Task Should_restore_states_for_all_asset_folders() + Assert.Equal(new HashSet<DomainId> { - var assetFolderId1 = DomainId.NewGuid(); - var assetFolderId2 = DomainId.NewGuid(); + DomainId.Combine(appId, assetId1), + DomainId.Combine(appId, assetId2) + }, rebuildAssets); + } - var context = CreateRestoreContext(); + [Fact] + public async Task Should_restore_states_for_all_asset_folders() + { + var assetFolderId1 = DomainId.NewGuid(); + var assetFolderId2 = DomainId.NewGuid(); - await sut.RestoreEventAsync(AppEvent(new AssetFolderCreated - { - AssetFolderId = assetFolderId1 - }), context, ct); + var context = CreateRestoreContext(); - await sut.RestoreEventAsync(AppEvent(new AssetFolderCreated - { - AssetFolderId = assetFolderId2 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new AssetFolderCreated + { + AssetFolderId = assetFolderId1 + }), context, ct); - await sut.RestoreEventAsync(AppEvent(new AssetFolderDeleted - { - AssetFolderId = assetFolderId2 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new AssetFolderCreated + { + AssetFolderId = assetFolderId2 + }), context, ct); - var rebuildAssetFolders = new HashSet<DomainId>(); + await sut.RestoreEventAsync(AppEvent(new AssetFolderDeleted + { + AssetFolderId = assetFolderId2 + }), context, ct); - A.CallTo(() => rebuilder.InsertManyAsync<AssetFolderDomainObject, AssetFolderDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) - .Invokes(x => rebuildAssetFolders.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); + var rebuildAssetFolders = new HashSet<DomainId>(); - await sut.RestoreAsync(context, ct); + A.CallTo(() => rebuilder.InsertManyAsync<AssetFolderDomainObject, AssetFolderDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) + .Invokes(x => rebuildAssetFolders.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); - Assert.Equal(new HashSet<DomainId> - { - DomainId.Combine(appId, assetFolderId1), - DomainId.Combine(appId, assetFolderId2) - }, rebuildAssetFolders); - } + await sut.RestoreAsync(context, ct); - private BackupContext CreateBackupContext() + Assert.Equal(new HashSet<DomainId> { - return new BackupContext(appId.Id, CreateUserMapping(), A.Fake<IBackupWriter>()); - } + DomainId.Combine(appId, assetFolderId1), + DomainId.Combine(appId, assetFolderId2) + }, rebuildAssetFolders); + } - private RestoreContext CreateRestoreContext() - { - return new RestoreContext(appId.Id, CreateUserMapping(), A.Fake<IBackupReader>(), DomainId.NewGuid()); - } + private BackupContext CreateBackupContext() + { + return new BackupContext(appId.Id, CreateUserMapping(), A.Fake<IBackupWriter>()); + } - private Envelope<AssetEvent> AppEvent(AssetEvent @event) - { - @event.AppId = appId; + private RestoreContext CreateRestoreContext() + { + return new RestoreContext(appId.Id, CreateUserMapping(), A.Fake<IBackupReader>(), DomainId.NewGuid()); + } - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.AssetId)); - } + private Envelope<AssetEvent> AppEvent(AssetEvent @event) + { + @event.AppId = appId; - private Envelope<AssetFolderEvent> AppEvent(AssetFolderEvent @event) - { - @event.AppId = appId; + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.AssetId)); + } - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.AssetFolderId)); - } + private Envelope<AssetFolderEvent> AppEvent(AssetFolderEvent @event) + { + @event.AppId = appId; - private IUserMapping CreateUserMapping() - { - var mapping = A.Fake<IUserMapping>(); + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.AssetFolderId)); + } + + private IUserMapping CreateUserMapping() + { + var mapping = A.Fake<IUserMapping>(); - A.CallTo(() => mapping.Initiator).Returns(actor); + A.CallTo(() => mapping.Initiator).Returns(actor); - return mapping; - } + return mapping; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs index e785eb1365..bdc100bff1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs @@ -14,263 +14,262 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class DefaultAssetFileStoreTests { - public class DefaultAssetFileStoreTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); + private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly DomainId assetId = DomainId.NewGuid(); + private readonly long assetFileVersion = 21; + private readonly AssetOptions options = new AssetOptions(); + private readonly DefaultAssetFileStore sut; + + public DefaultAssetFileStoreTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); - private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly DomainId assetId = DomainId.NewGuid(); - private readonly long assetFileVersion = 21; - private readonly AssetOptions options = new AssetOptions(); - private readonly DefaultAssetFileStore sut; - - public DefaultAssetFileStoreTests() - { - ct = cts.Token; + ct = cts.Token; - sut = new DefaultAssetFileStore(assetStore, assetRepository, Options.Create(options)); - } + sut = new DefaultAssetFileStore(assetStore, assetRepository, Options.Create(options)); + } - public static IEnumerable<object[]> PathCases() - { - yield return new object[] { true, "resize=100", "{appId}/{assetId}_{assetFileVersion}_resize=100" }; - yield return new object[] { true, string.Empty, "{appId}/{assetId}_{assetFileVersion}" }; - yield return new object[] { false, "resize=100", "{appId}_{assetId}_{assetFileVersion}_resize=100" }; - yield return new object[] { false, string.Empty, "{appId}_{assetId}_{assetFileVersion}" }; - } + public static IEnumerable<object[]> PathCases() + { + yield return new object[] { true, "resize=100", "{appId}/{assetId}_{assetFileVersion}_resize=100" }; + yield return new object[] { true, string.Empty, "{appId}/{assetId}_{assetFileVersion}" }; + yield return new object[] { false, "resize=100", "{appId}_{assetId}_{assetFileVersion}_resize=100" }; + yield return new object[] { false, string.Empty, "{appId}_{assetId}_{assetFileVersion}" }; + } - public static IEnumerable<object?[]> PathCasesOld() - { - yield return new object?[] { "resize=100", "{assetId}_{assetFileVersion}_resize=100" }; - yield return new object?[] { string.Empty, "{assetId}_{assetFileVersion}" }; - } + public static IEnumerable<object?[]> PathCasesOld() + { + yield return new object?[] { "resize=100", "{assetId}_{assetFileVersion}_resize=100" }; + yield return new object?[] { string.Empty, "{assetId}_{assetFileVersion}" }; + } - [Theory] - [MemberData(nameof(PathCases))] - public void Should_get_public_url_from_store(bool folderPerApp, string? suffix, string fileName) - { - var fullName = GetFullName(fileName); + [Theory] + [MemberData(nameof(PathCases))] + public void Should_get_public_url_from_store(bool folderPerApp, string? suffix, string fileName) + { + var fullName = GetFullName(fileName); - options.FolderPerApp = folderPerApp; + options.FolderPerApp = folderPerApp; - var url = "http_//squidex.io/assets"; + var url = "http_//squidex.io/assets"; - A.CallTo(() => assetStore.GeneratePublicUrl(fullName)) - .Returns(url); + A.CallTo(() => assetStore.GeneratePublicUrl(fullName)) + .Returns(url); - var actual = sut.GeneratePublicUrl(appId, assetId, assetFileVersion, suffix); + var actual = sut.GeneratePublicUrl(appId, assetId, assetFileVersion, suffix); - Assert.Equal(url, actual); - } + Assert.Equal(url, actual); + } - [Theory] - [MemberData(nameof(PathCases))] - public async Task Should_get_file_size_from_store(bool folderPerApp, string? suffix, string fileName) - { - var fullName = GetFullName(fileName); + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_get_file_size_from_store(bool folderPerApp, string? suffix, string fileName) + { + var fullName = GetFullName(fileName); - options.FolderPerApp = folderPerApp; + options.FolderPerApp = folderPerApp; - var size = 1024L; + var size = 1024L; - A.CallTo(() => assetStore.GetSizeAsync(fullName, ct)) - .Returns(size); + A.CallTo(() => assetStore.GetSizeAsync(fullName, ct)) + .Returns(size); - var actual = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix, ct); + var actual = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix, ct); - Assert.Equal(size, actual); - } + Assert.Equal(size, actual); + } - [Theory] - [MemberData(nameof(PathCasesOld))] - public async Task Should_get_file_size_from_store_with_old_file_name_if_new_name_not_found(string? suffix, string fileName) - { - var fullName = GetFullName(fileName); + [Theory] + [MemberData(nameof(PathCasesOld))] + public async Task Should_get_file_size_from_store_with_old_file_name_if_new_name_not_found(string? suffix, string fileName) + { + var fullName = GetFullName(fileName); - var size = 1024L; + var size = 1024L; - A.CallTo(() => assetStore.GetSizeAsync(A<string>._, ct)) - .Throws(new AssetNotFoundException(assetId.ToString())); + A.CallTo(() => assetStore.GetSizeAsync(A<string>._, ct)) + .Throws(new AssetNotFoundException(assetId.ToString())); - A.CallTo(() => assetStore.GetSizeAsync(fullName, ct)) - .Returns(size); + A.CallTo(() => assetStore.GetSizeAsync(fullName, ct)) + .Returns(size); - var actual = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix, ct); + var actual = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix, ct); - Assert.Equal(size, actual); - } + Assert.Equal(size, actual); + } - [Fact] - public async Task Should_upload_temporary_filet_to_store() - { - var stream = new MemoryStream(); + [Fact] + public async Task Should_upload_temporary_filet_to_store() + { + var stream = new MemoryStream(); - await sut.UploadAsync("Temp", stream, ct); + await sut.UploadAsync("Temp", stream, ct); - A.CallTo(() => assetStore.UploadAsync("Temp", stream, false, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.UploadAsync("Temp", stream, false, ct)) + .MustHaveHappened(); + } - [Theory] - [MemberData(nameof(PathCases))] - public async Task Should_upload_file_to_store(bool folderPerApp, string? suffix, string fileName) - { - var fullName = GetFullName(fileName); + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_upload_file_to_store(bool folderPerApp, string? suffix, string fileName) + { + var fullName = GetFullName(fileName); - options.FolderPerApp = folderPerApp; + options.FolderPerApp = folderPerApp; - var stream = new MemoryStream(); + var stream = new MemoryStream(); - await sut.UploadAsync(appId, assetId, assetFileVersion, suffix, stream, true, ct); + await sut.UploadAsync(appId, assetId, assetFileVersion, suffix, stream, true, ct); - A.CallTo(() => assetStore.UploadAsync(fullName, stream, true, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.UploadAsync(fullName, stream, true, ct)) + .MustHaveHappened(); + } - [Theory] - [MemberData(nameof(PathCases))] - public async Task Should_download_file_from_store(bool folderPerApp, string? suffix, string fileName) - { - var fullName = GetFullName(fileName); + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_download_file_from_store(bool folderPerApp, string? suffix, string fileName) + { + var fullName = GetFullName(fileName); - options.FolderPerApp = folderPerApp; + options.FolderPerApp = folderPerApp; - var stream = new MemoryStream(); + var stream = new MemoryStream(); - await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream, default, ct); + await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream, default, ct); - A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_download_file_from_store_with_folder_only_if_configured() - { - options.FolderPerApp = true; + [Fact] + public async Task Should_download_file_from_store_with_folder_only_if_configured() + { + options.FolderPerApp = true; - var stream = new MemoryStream(); + var stream = new MemoryStream(); - A.CallTo(() => assetStore.DownloadAsync(A<string>._, stream, default, ct)) - .Throws(new AssetNotFoundException(assetId.ToString())).Once(); + A.CallTo(() => assetStore.DownloadAsync(A<string>._, stream, default, ct)) + .Throws(new AssetNotFoundException(assetId.ToString())).Once(); - await Assert.ThrowsAsync<AssetNotFoundException>(() => sut.DownloadAsync(appId, assetId, assetFileVersion, null, stream, default, ct)); + await Assert.ThrowsAsync<AssetNotFoundException>(() => sut.DownloadAsync(appId, assetId, assetFileVersion, null, stream, default, ct)); - A.CallTo(() => assetStore.DownloadAsync(A<string>._, stream, default, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => assetStore.DownloadAsync(A<string>._, stream, default, ct)) + .MustHaveHappenedOnceExactly(); + } - [Theory] - [MemberData(nameof(PathCasesOld))] - public async Task Should_download_file_from_store_with_old_file_name_if_new_name_not_found(string suffix, string fileName) - { - var fullName = GetFullName(fileName); + [Theory] + [MemberData(nameof(PathCasesOld))] + public async Task Should_download_file_from_store_with_old_file_name_if_new_name_not_found(string suffix, string fileName) + { + var fullName = GetFullName(fileName); - var stream = new MemoryStream(); + var stream = new MemoryStream(); - A.CallTo(() => assetStore.DownloadAsync(A<string>.That.Matches(x => x != fileName), stream, default, ct)) - .Throws(new AssetNotFoundException(assetId.ToString())).Once(); + A.CallTo(() => assetStore.DownloadAsync(A<string>.That.Matches(x => x != fileName), stream, default, ct)) + .Throws(new AssetNotFoundException(assetId.ToString())).Once(); - await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream, default, ct); + await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream, default, ct); - A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, ct)) + .MustHaveHappened(); + } - [Theory] - [MemberData(nameof(PathCases))] - public async Task Should_copy_file_to_store(bool folderPerApp, string? suffix, string fileName) - { - var fullName = GetFullName(fileName); + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_copy_file_to_store(bool folderPerApp, string? suffix, string fileName) + { + var fullName = GetFullName(fileName); - options.FolderPerApp = folderPerApp; + options.FolderPerApp = folderPerApp; - await sut.CopyAsync("Temp", appId, assetId, assetFileVersion, suffix, ct); + await sut.CopyAsync("Temp", appId, assetId, assetFileVersion, suffix, ct); - A.CallTo(() => assetStore.CopyAsync("Temp", fullName, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.CopyAsync("Temp", fullName, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_delete_temporary_file_from_store() - { - await sut.DeleteAsync("Temp", ct); + [Fact] + public async Task Should_delete_temporary_file_from_store() + { + await sut.DeleteAsync("Temp", ct); - A.CallTo(() => assetStore.DeleteAsync("Temp", ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DeleteAsync("Temp", ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_delete_file_from_store() - { - await sut.DeleteAsync(appId, assetId, ct); + [Fact] + public async Task Should_delete_file_from_store() + { + await sut.DeleteAsync(appId, assetId, ct); - A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{assetId}", ct)) - .MustHaveHappened(); + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{assetId}", ct)) + .MustHaveHappened(); - A.CallTo(() => assetStore.DeleteByPrefixAsync(assetId.ToString(), ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DeleteByPrefixAsync(assetId.ToString(), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_delete_file_from_store_when_folders_are_used() - { - options.FolderPerApp = true; + [Fact] + public async Task Should_delete_file_from_store_when_folders_are_used() + { + options.FolderPerApp = true; - await sut.DeleteAsync(appId, assetId, ct); + await sut.DeleteAsync(appId, assetId, ct); - A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}/{assetId}", ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}/{assetId}", ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_delete_assets_invidually__on_app_deletion() - { - var asset1 = new AssetEntity { Id = DomainId.NewGuid() }; - var asset2 = new AssetEntity { Id = DomainId.NewGuid() }; + [Fact] + public async Task Should_delete_assets_invidually__on_app_deletion() + { + var asset1 = new AssetEntity { Id = DomainId.NewGuid() }; + var asset2 = new AssetEntity { Id = DomainId.NewGuid() }; - A.CallTo(() => assetRepository.StreamAll(appId, ct)) - .Returns(new[] { asset1, asset2 }.ToAsyncEnumerable()); + A.CallTo(() => assetRepository.StreamAll(appId, ct)) + .Returns(new[] { asset1, asset2 }.ToAsyncEnumerable()); - var app = Mocks.App(NamedId.Of(appId, "my-app")); + var app = Mocks.App(NamedId.Of(appId, "my-app")); - await ((IDeleter)sut).DeleteAppAsync(app, ct); + await ((IDeleter)sut).DeleteAppAsync(app, ct); - A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{asset1.Id}", ct)) - .MustHaveHappened(); + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{asset1.Id}", ct)) + .MustHaveHappened(); - A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{asset2.Id}", ct)) - .MustHaveHappened(); + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{asset2.Id}", ct)) + .MustHaveHappened(); - A.CallTo(() => assetStore.DeleteByPrefixAsync(asset1.Id.ToString(), ct)) - .MustHaveHappened(); + A.CallTo(() => assetStore.DeleteByPrefixAsync(asset1.Id.ToString(), ct)) + .MustHaveHappened(); - A.CallTo(() => assetStore.DeleteByPrefixAsync(asset2.Id.ToString(), ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DeleteByPrefixAsync(asset2.Id.ToString(), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_delete_app_folder_on_app_deletion_when_folders_are_used() - { - options.FolderPerApp = true; + [Fact] + public async Task Should_delete_app_folder_on_app_deletion_when_folders_are_used() + { + options.FolderPerApp = true; - var app = Mocks.App(NamedId.Of(appId, "my-app")); + var app = Mocks.App(NamedId.Of(appId, "my-app")); - await ((IDeleter)sut).DeleteAppAsync(app, ct); + await ((IDeleter)sut).DeleteAppAsync(app, ct); - A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}/", ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}/", ct)) + .MustHaveHappened(); + } - private string GetFullName(string fileName) - { - return fileName - .Replace("{appId}", appId.ToString(), StringComparison.Ordinal) - .Replace("{assetId}", assetId.ToString(), StringComparison.Ordinal) - .Replace("{assetFileVersion}", assetFileVersion.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); - } + private string GetFullName(string fileName) + { + return fileName + .Replace("{appId}", appId.ToString(), StringComparison.Ordinal) + .Replace("{assetId}", assetId.ToString(), StringComparison.Ordinal) + .Replace("{assetFileVersion}", assetFileVersion.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs index 41f44b852e..16b64d8b7e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs @@ -13,260 +13,259 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public class AssetCommandMiddlewareTests : HandlerTestBase<AssetDomainObject.State> { - public class AssetCommandMiddlewareTests : HandlerTestBase<AssetDomainObject.State> + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectCache domainObjectCache = A.Fake<IDomainObjectCache>(); + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>(); + private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); + private readonly IAssetMetadataSource assetMetadataSource = A.Fake<IAssetMetadataSource>(); + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); + private readonly DomainId assetId = DomainId.NewGuid(); + private readonly AssetFile file = new NoopAssetFile(); + private readonly Context requestContext; + private readonly AssetCommandMiddleware sut; + + public sealed class MyCommand : SquidexCommand { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IDomainObjectCache domainObjectCache = A.Fake<IDomainObjectCache>(); - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>(); - private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); - private readonly IAssetMetadataSource assetMetadataSource = A.Fake<IAssetMetadataSource>(); - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); - private readonly DomainId assetId = DomainId.NewGuid(); - private readonly AssetFile file = new NoopAssetFile(); - private readonly Context requestContext; - private readonly AssetCommandMiddleware sut; - - public sealed class MyCommand : SquidexCommand - { - } + } - protected override DomainId Id - { - get => DomainId.Combine(AppId, assetId); - } + protected override DomainId Id + { + get => DomainId.Combine(AppId, assetId); + } - public AssetCommandMiddlewareTests() - { - ct = cts.Token; + public AssetCommandMiddlewareTests() + { + ct = cts.Token; - file = new NoopAssetFile(); + file = new NoopAssetFile(); - requestContext = Context.Anonymous(Mocks.App(AppNamedId)); + requestContext = Context.Anonymous(Mocks.App(AppNamedId)); - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); - A.CallTo(() => assetQuery.FindByHashAsync(A<Context>._, A<string>._, A<string>._, A<long>._, ct)) - .Returns(Task.FromResult<IEnrichedAssetEntity?>(null)); + A.CallTo(() => assetQuery.FindByHashAsync(A<Context>._, A<string>._, A<string>._, A<long>._, ct)) + .Returns(Task.FromResult<IEnrichedAssetEntity?>(null)); - sut = new AssetCommandMiddleware( - domainObjectFactory, - domainObjectCache, - assetEnricher, - assetFileStore, - assetQuery, - contextProvider, new[] { assetMetadataSource }); - } + sut = new AssetCommandMiddleware( + domainObjectFactory, + domainObjectCache, + assetEnricher, + assetFileStore, + assetQuery, + contextProvider, new[] { assetMetadataSource }); + } - [Fact] - public async Task Should_not_invoke_enricher_for_other_actual() - { - await HandleAsync(new AnnotateAsset(), 12); + [Fact] + public async Task Should_not_invoke_enricher_for_other_actual() + { + await HandleAsync(new AnnotateAsset(), 12); - A.CallTo(() => assetEnricher.EnrichAsync(A<IEnrichedAssetEntity>._, requestContext, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetEnricher.EnrichAsync(A<IEnrichedAssetEntity>._, requestContext, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_invoke_enricher_if_already_enriched() - { - var actual = new AssetEntity(); + [Fact] + public async Task Should_not_invoke_enricher_if_already_enriched() + { + var actual = new AssetEntity(); - var context = - await HandleAsync(new AnnotateAsset(), - actual); + var context = + await HandleAsync(new AnnotateAsset(), + actual); - A.CallTo(() => assetEnricher.EnrichAsync(A<IEnrichedAssetEntity>._, requestContext, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetEnricher.EnrichAsync(A<IEnrichedAssetEntity>._, requestContext, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_enrich_asset_actual() - { - var actual = A.Fake<IAssetEntity>(); + [Fact] + public async Task Should_enrich_asset_actual() + { + var actual = A.Fake<IAssetEntity>(); - var enriched = new AssetEntity(); + var enriched = new AssetEntity(); - A.CallTo(() => assetEnricher.EnrichAsync(actual, requestContext, ct)) - .Returns(enriched); + A.CallTo(() => assetEnricher.EnrichAsync(actual, requestContext, ct)) + .Returns(enriched); - var context = - await HandleAsync(new AnnotateAsset(), - actual); + var context = + await HandleAsync(new AnnotateAsset(), + actual); - Assert.Same(enriched, context.Result<IEnrichedAssetEntity>()); - } + Assert.Same(enriched, context.Result<IEnrichedAssetEntity>()); + } - [Fact] - public async Task Create_should_upload_file() - { - var actual = CreateAsset(); + [Fact] + public async Task Create_should_upload_file() + { + var actual = CreateAsset(); - var context = - await HandleAsync(new CreateAsset { File = file }, - actual); + var context = + await HandleAsync(new CreateAsset { File = file }, + actual); - Assert.Same(actual, context.Result<IEnrichedAssetEntity>()); + Assert.Same(actual, context.Result<IEnrichedAssetEntity>()); - AssertAssetHasBeenUploaded(0); - AssertMetadataEnriched(); - } + AssertAssetHasBeenUploaded(0); + AssertMetadataEnriched(); + } - [Fact] - public async Task Create_should_calculate_hash() - { - var command = new CreateAsset { File = file }; + [Fact] + public async Task Create_should_calculate_hash() + { + var command = new CreateAsset { File = file }; - await HandleAsync(command, CreateAsset()); + await HandleAsync(command, CreateAsset()); - Assert.True(command.FileHash.Length > 10); - } + Assert.True(command.FileHash.Length > 10); + } - [Fact] - public async Task Create_should_not_return_duplicate_actual_if_file_with_same_hash_found_but_duplicate_allowed() - { - var actual = CreateAsset(); + [Fact] + public async Task Create_should_not_return_duplicate_actual_if_file_with_same_hash_found_but_duplicate_allowed() + { + var actual = CreateAsset(); - SetupSameHashAsset(file.FileName, file.FileSize, out _); + SetupSameHashAsset(file.FileName, file.FileSize, out _); - var context = - await HandleAsync(new CreateAsset { File = file, Duplicate = true }, - actual); + var context = + await HandleAsync(new CreateAsset { File = file, Duplicate = true }, + actual); - Assert.Same(actual, context.Result<IEnrichedAssetEntity>()); - } + Assert.Same(actual, context.Result<IEnrichedAssetEntity>()); + } - [Fact] - public async Task Create_should_return_duplicate_actual_if_file_with_same_hash_found() - { - SetupSameHashAsset(file.FileName, file.FileSize, out var duplicate); + [Fact] + public async Task Create_should_return_duplicate_actual_if_file_with_same_hash_found() + { + SetupSameHashAsset(file.FileName, file.FileSize, out var duplicate); - var context = - await HandleAsync(new CreateAsset { File = file }, - CreateAsset()); + var context = + await HandleAsync(new CreateAsset { File = file }, + CreateAsset()); - Assert.Same(duplicate, context.Result<AssetDuplicate>().Asset); - } + Assert.Same(duplicate, context.Result<AssetDuplicate>().Asset); + } - [Fact] - public async Task Update_should_upload_file() - { - await HandleAsync(new UpdateAsset { File = file }, CreateAsset(1)); + [Fact] + public async Task Update_should_upload_file() + { + await HandleAsync(new UpdateAsset { File = file }, CreateAsset(1)); - AssertAssetHasBeenUploaded(1); - AssertMetadataEnriched(); - } + AssertAssetHasBeenUploaded(1); + AssertMetadataEnriched(); + } - [Fact] - public async Task Update_should_calculate_hash() - { - var command = new UpdateAsset { File = file }; + [Fact] + public async Task Update_should_calculate_hash() + { + var command = new UpdateAsset { File = file }; - await HandleAsync(command, CreateAsset()); + await HandleAsync(command, CreateAsset()); - Assert.True(command.FileHash.Length > 10); - } + Assert.True(command.FileHash.Length > 10); + } - [Fact] - public async Task Upsert_should_upload_file() - { - await HandleAsync(new UpsertAsset { File = file, Duplicate = false }, CreateAsset(1)); + [Fact] + public async Task Upsert_should_upload_file() + { + await HandleAsync(new UpsertAsset { File = file, Duplicate = false }, CreateAsset(1)); - AssertAssetHasBeenUploaded(1); - AssertMetadataEnriched(); - } + AssertAssetHasBeenUploaded(1); + AssertMetadataEnriched(); + } - [Fact] - public async Task Upsert_should_not_return_duplicate_actual_if_file_with_same_hash_found_but_duplicate_allowed() - { - var actual = CreateAsset(); + [Fact] + public async Task Upsert_should_not_return_duplicate_actual_if_file_with_same_hash_found_but_duplicate_allowed() + { + var actual = CreateAsset(); - SetupSameHashAsset(file.FileName, file.FileSize, out _); + SetupSameHashAsset(file.FileName, file.FileSize, out _); - var context = - await HandleAsync(new UpsertAsset { File = file }, - actual); + var context = + await HandleAsync(new UpsertAsset { File = file }, + actual); - Assert.Same(actual, context.Result<IEnrichedAssetEntity>()); - } + Assert.Same(actual, context.Result<IEnrichedAssetEntity>()); + } - [Fact] - public async Task Upsert_should_return_duplicate_actual_if_file_with_same_hash_found() - { - SetupSameHashAsset(file.FileName, file.FileSize, out var duplicate); + [Fact] + public async Task Upsert_should_return_duplicate_actual_if_file_with_same_hash_found() + { + SetupSameHashAsset(file.FileName, file.FileSize, out var duplicate); - var context = - await HandleAsync(new UpsertAsset { File = file, Duplicate = false }, - CreateAsset()); + var context = + await HandleAsync(new UpsertAsset { File = file, Duplicate = false }, + CreateAsset()); - Assert.Same(duplicate, context.Result<AssetDuplicate>().Asset); - } + Assert.Same(duplicate, context.Result<AssetDuplicate>().Asset); + } - [Fact] - public async Task Upsert_should_calculate_hash() - { - var command = new UpsertAsset { File = file }; + [Fact] + public async Task Upsert_should_calculate_hash() + { + var command = new UpsertAsset { File = file }; - await HandleAsync(command, CreateAsset()); + await HandleAsync(command, CreateAsset()); - Assert.True(command.FileHash.Length > 10); - } + Assert.True(command.FileHash.Length > 10); + } - private void AssertAssetHasBeenUploaded(long fileVersion) - { - A.CallTo(() => assetFileStore.UploadAsync(A<string>._, A<HasherStream>._, ct)) - .MustHaveHappened(); + private void AssertAssetHasBeenUploaded(long fileVersion) + { + A.CallTo(() => assetFileStore.UploadAsync(A<string>._, A<HasherStream>._, ct)) + .MustHaveHappened(); - A.CallTo(() => assetFileStore.CopyAsync(A<string>._, AppId, assetId, fileVersion, null, ct)) - .MustHaveHappened(); + A.CallTo(() => assetFileStore.CopyAsync(A<string>._, AppId, assetId, fileVersion, null, ct)) + .MustHaveHappened(); - A.CallTo(() => assetFileStore.DeleteAsync(A<string>._, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetFileStore.DeleteAsync(A<string>._, ct)) + .MustHaveHappened(); + } - private void SetupSameHashAsset(string fileName, long fileSize, out IEnrichedAssetEntity duplicate) + private void SetupSameHashAsset(string fileName, long fileSize, out IEnrichedAssetEntity duplicate) + { + duplicate = new AssetEntity { - duplicate = new AssetEntity - { - FileName = fileName, - FileSize = fileSize - }; + FileName = fileName, + FileSize = fileSize + }; - A.CallTo(() => assetQuery.FindByHashAsync(requestContext, A<string>._, fileName, fileSize, ct)) - .Returns(duplicate); - } + A.CallTo(() => assetQuery.FindByHashAsync(requestContext, A<string>._, fileName, fileSize, ct)) + .Returns(duplicate); + } - private void AssertMetadataEnriched() - { - A.CallTo(() => assetMetadataSource.EnhanceAsync(A<UploadAssetCommand>._, ct)) - .MustHaveHappened(); - } + private void AssertMetadataEnriched() + { + A.CallTo(() => assetMetadataSource.EnhanceAsync(A<UploadAssetCommand>._, ct)) + .MustHaveHappened(); + } - private Task<CommandContext> HandleAsync(AssetCommand command, object actual) - { - command.AssetId = assetId; + private Task<CommandContext> HandleAsync(AssetCommand command, object actual) + { + command.AssetId = assetId; - CreateCommand(command); + CreateCommand(command); - var domainObject = A.Fake<AssetDomainObject>(); + var domainObject = A.Fake<AssetDomainObject>(); - A.CallTo(() => domainObject.ExecuteAsync(A<IAggregateCommand>._, ct)) - .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); + A.CallTo(() => domainObject.ExecuteAsync(A<IAggregateCommand>._, ct)) + .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); - A.CallTo(() => domainObjectFactory.Create<AssetDomainObject>(command.AggregateId)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<AssetDomainObject>(command.AggregateId)) + .Returns(domainObject); - return HandleAsync(sut, command, ct); - } + return HandleAsync(sut, command, ct); + } - private IAssetEntity CreateAsset(long fileVersion = 0) - { - return new AssetEntity { AppId = AppNamedId, Id = assetId, FileVersion = fileVersion }; - } + private IAssetEntity CreateAsset(long fileVersion = 0) + { + return new AssetEntity { AppId = AppNamedId, Id = assetId, FileVersion = fileVersion }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs index ffbf9c24fa..0aa952223e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs @@ -21,471 +21,470 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public class AssetDomainObjectTests : HandlerTestBase<AssetDomainObject.State> { - public class AssetDomainObjectTests : HandlerTestBase<AssetDomainObject.State> + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); + private readonly ITagService tagService = A.Fake<ITagService>(); + private readonly DomainId parentId = DomainId.NewGuid(); + private readonly DomainId assetId = DomainId.NewGuid(); + private readonly AssetFile file = new NoopAssetFile(); + private readonly AssetDomainObject sut; + + protected override DomainId Id { - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); - private readonly ITagService tagService = A.Fake<ITagService>(); - private readonly DomainId parentId = DomainId.NewGuid(); - private readonly DomainId assetId = DomainId.NewGuid(); - private readonly AssetFile file = new NoopAssetFile(); - private readonly AssetDomainObject sut; - - protected override DomainId Id - { - get => assetId; - } + get => assetId; + } - public AssetDomainObjectTests() - { - app = Mocks.App(AppNamedId, Language.DE); + public AssetDomainObjectTests() + { + app = Mocks.App(AppNamedId, Language.DE); - var scripts = new AssetScripts - { - Annotate = "<annotate-script>", - Create = "<create-script>", - Delete = "<delete-script>", - Move = "<move-script>", - Update = "<update-script>" - }; + var scripts = new AssetScripts + { + Annotate = "<annotate-script>", + Create = "<create-script>", + Delete = "<delete-script>", + Move = "<move-script>", + Update = "<update-script>" + }; - A.CallTo(() => app.AssetScripts) - .Returns(scripts); + A.CallTo(() => app.AssetScripts) + .Returns(scripts); - A.CallTo(() => appProvider.GetAppAsync(AppId, false, default)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(AppId, false, default)) + .Returns(app); - A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._)) - .Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() }); + A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._)) + .Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() }); - A.CallTo(() => tagService.GetTagIdsAsync(AppId, TagGroups.Assets, A<HashSet<string>>._, default)) - .ReturnsLazily(x => Task.FromResult(x.GetArgument<HashSet<string>>(2)?.ToDictionary(x => x) ?? new Dictionary<string, string>())); + A.CallTo(() => tagService.GetTagIdsAsync(AppId, TagGroups.Assets, A<HashSet<string>>._, default)) + .ReturnsLazily(x => Task.FromResult(x.GetArgument<HashSet<string>>(2)?.ToDictionary(x => x) ?? new Dictionary<string, string>())); - var log = A.Fake<ILogger<AssetDomainObject>>(); + var log = A.Fake<ILogger<AssetDomainObject>>(); - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(assetQuery) - .AddSingleton(contentRepository) - .AddSingleton(log) - .AddSingleton(scriptEngine) - .AddSingleton(tagService) - .BuildServiceProvider(); + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(assetQuery) + .AddSingleton(contentRepository) + .AddSingleton(log) + .AddSingleton(scriptEngine) + .AddSingleton(tagService) + .BuildServiceProvider(); #pragma warning disable MA0056 // Do not call overridable members in constructor - sut = new AssetDomainObject(Id, PersistenceFactory, log, serviceProvider); + sut = new AssetDomainObject(Id, PersistenceFactory, log, serviceProvider); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - [Fact] - public async Task Command_should_throw_exception_if_asset_is_deleted() - { - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); + [Fact] + public async Task Command_should_throw_exception_if_asset_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); - await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteUpdateAsync); - } + await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteUpdateAsync); + } - [Fact] - public async Task Create_should_create_events_and_set_intitial_state() - { - var command = new CreateAsset { File = file, FileHash = "NewHash" }; - - var actual = await PublishAsync(command); - - actual.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(0, sut.Snapshot.FileVersion); - Assert.Equal(command.FileHash, sut.Snapshot.FileHash); - - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetCreated - { - FileName = file.FileName, - FileSize = file.FileSize, - FileHash = command.FileHash, - FileVersion = 0, - Metadata = new AssetMetadata(), - MimeType = file.MimeType, - Tags = new HashSet<string>(), - Slug = file.FileName.ToAssetSlug() - }) - ); - - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<create-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } - - [Fact] - public async Task Create_should_recreate_deleted_content() - { - var command = new CreateAsset { File = file, FileHash = "NewHash" }; + [Fact] + public async Task Create_should_create_events_and_set_intitial_state() + { + var command = new CreateAsset { File = file, FileHash = "NewHash" }; + + var actual = await PublishAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(0, sut.Snapshot.FileVersion); + Assert.Equal(command.FileHash, sut.Snapshot.FileHash); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetCreated + { + FileName = file.FileName, + FileSize = file.FileSize, + FileHash = command.FileHash, + FileVersion = 0, + Metadata = new AssetMetadata(), + MimeType = file.MimeType, + Tags = new HashSet<string>(), + Slug = file.FileName.ToAssetSlug() + }) + ); + + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<create-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); + [Fact] + public async Task Create_should_recreate_deleted_content() + { + var command = new CreateAsset { File = file, FileHash = "NewHash" }; - await PublishAsync(command); - } + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); - [Fact] - public async Task Create_should_recreate_permanently_deleted_content() - { - var command = new CreateAsset { File = file, FileHash = "NewHash" }; + await PublishAsync(command); + } - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(true); + [Fact] + public async Task Create_should_recreate_permanently_deleted_content() + { + var command = new CreateAsset { File = file, FileHash = "NewHash" }; - await PublishAsync(command); - } + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(true); - [Fact] - public async Task Upsert_should_create_events_and_set_intitial_state_if_not_found() - { - var command = new UpsertAsset { File = file, FileHash = "NewHash" }; - - var actual = await PublishAsync(command); - - actual.ShouldBeEquivalent(sut.Snapshot); - - Assert.Equal(0, sut.Snapshot.FileVersion); - Assert.Equal(command.FileHash, sut.Snapshot.FileHash); - - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetCreated - { - FileName = file.FileName, - FileSize = file.FileSize, - FileHash = command.FileHash, - FileVersion = 0, - Metadata = new AssetMetadata(), - MimeType = file.MimeType, - Tags = new HashSet<string>(), - Slug = file.FileName.ToAssetSlug() - }) - ); - - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<create-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } - - [Fact] - public async Task Upsert_should_create_events_and_update_file_state_if_found() - { - var command = new UpsertAsset { File = file, FileHash = "NewHash" }; + await PublishAsync(command); + } - await ExecuteCreateAsync(); + [Fact] + public async Task Upsert_should_create_events_and_set_intitial_state_if_not_found() + { + var command = new UpsertAsset { File = file, FileHash = "NewHash" }; + + var actual = await PublishAsync(command); + + actual.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(0, sut.Snapshot.FileVersion); + Assert.Equal(command.FileHash, sut.Snapshot.FileHash); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetCreated + { + FileName = file.FileName, + FileSize = file.FileSize, + FileHash = command.FileHash, + FileVersion = 0, + Metadata = new AssetMetadata(), + MimeType = file.MimeType, + Tags = new HashSet<string>(), + Slug = file.FileName.ToAssetSlug() + }) + ); + + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<create-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - var actual = await PublishAsync(command); + [Fact] + public async Task Upsert_should_create_events_and_update_file_state_if_found() + { + var command = new UpsertAsset { File = file, FileHash = "NewHash" }; - actual.ShouldBeEquivalent(sut.Snapshot); + await ExecuteCreateAsync(); - Assert.Equal(1, sut.Snapshot.FileVersion); - Assert.Equal(command.FileHash, sut.Snapshot.FileHash); + var actual = await PublishAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetUpdated - { - FileSize = file.FileSize, - FileHash = command.FileHash, - FileVersion = 1, - Metadata = new AssetMetadata(), - MimeType = file.MimeType - }) - ); + actual.ShouldBeEquivalent(sut.Snapshot); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + Assert.Equal(1, sut.Snapshot.FileVersion); + Assert.Equal(command.FileHash, sut.Snapshot.FileHash); - [Fact] - public async Task Update_should_create_events_and_update_file_state() - { - var command = new UpdateAsset { File = file, FileHash = "NewHash" }; + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetUpdated + { + FileSize = file.FileSize, + FileHash = command.FileHash, + FileVersion = 1, + Metadata = new AssetMetadata(), + MimeType = file.MimeType + }) + ); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - var actual = await PublishAsync(command); + [Fact] + public async Task Update_should_create_events_and_update_file_state() + { + var command = new UpdateAsset { File = file, FileHash = "NewHash" }; - actual.ShouldBeEquivalent(sut.Snapshot); + await ExecuteCreateAsync(); - Assert.Equal(1, sut.Snapshot.FileVersion); - Assert.Equal(command.FileHash, sut.Snapshot.FileHash); + var actual = await PublishAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetUpdated - { - FileSize = file.FileSize, - FileHash = command.FileHash, - FileVersion = 1, - Metadata = new AssetMetadata(), - MimeType = file.MimeType - }) - ); + actual.ShouldBeEquivalent(sut.Snapshot); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + Assert.Equal(1, sut.Snapshot.FileVersion); + Assert.Equal(command.FileHash, sut.Snapshot.FileHash); - [Fact] - public async Task AnnotateName_should_create_events_and_update_file_name() - { - var command = new AnnotateAsset { FileName = "My New Image.png" }; + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetUpdated + { + FileSize = file.FileSize, + FileHash = command.FileHash, + FileVersion = 1, + Metadata = new AssetMetadata(), + MimeType = file.MimeType + }) + ); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - var actual = await PublishIdempotentAsync(command); + [Fact] + public async Task AnnotateName_should_create_events_and_update_file_name() + { + var command = new AnnotateAsset { FileName = "My New Image.png" }; - actual.ShouldBeEquivalent(sut.Snapshot); + await ExecuteCreateAsync(); - Assert.Equal(command.FileName, sut.Snapshot.FileName); + var actual = await PublishIdempotentAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetAnnotated { FileName = command.FileName }) - ); + actual.ShouldBeEquivalent(sut.Snapshot); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + Assert.Equal(command.FileName, sut.Snapshot.FileName); - [Fact] - public async Task AnnotateSlug_should_create_events_and_update_slug() - { - var command = new AnnotateAsset { Slug = "my-new-image.png" }; + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetAnnotated { FileName = command.FileName }) + ); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - var actual = await PublishIdempotentAsync(command); + [Fact] + public async Task AnnotateSlug_should_create_events_and_update_slug() + { + var command = new AnnotateAsset { Slug = "my-new-image.png" }; - actual.ShouldBeEquivalent(sut.Snapshot); + await ExecuteCreateAsync(); - Assert.Equal(command.Slug, sut.Snapshot.Slug); + var actual = await PublishIdempotentAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetAnnotated { Slug = command.Slug }) - ); + actual.ShouldBeEquivalent(sut.Snapshot); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + Assert.Equal(command.Slug, sut.Snapshot.Slug); - [Fact] - public async Task AnnotateProtected_should_create_events_and_update_protected_flag() - { - var command = new AnnotateAsset { IsProtected = true }; + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetAnnotated { Slug = command.Slug }) + ); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - var actual = await PublishIdempotentAsync(command); + [Fact] + public async Task AnnotateProtected_should_create_events_and_update_protected_flag() + { + var command = new AnnotateAsset { IsProtected = true }; - actual.ShouldBeEquivalent(sut.Snapshot); + await ExecuteCreateAsync(); - Assert.Equal(command.IsProtected, sut.Snapshot.IsProtected); + var actual = await PublishIdempotentAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetAnnotated { IsProtected = command.IsProtected }) - ); + actual.ShouldBeEquivalent(sut.Snapshot); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + Assert.Equal(command.IsProtected, sut.Snapshot.IsProtected); - [Fact] - public async Task AnnotateMetadata_should_create_events_and_update_metadata() - { - var command = new AnnotateAsset { Metadata = new AssetMetadata().SetPixelWidth(800) }; + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetAnnotated { IsProtected = command.IsProtected }) + ); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - var actual = await PublishIdempotentAsync(command); + [Fact] + public async Task AnnotateMetadata_should_create_events_and_update_metadata() + { + var command = new AnnotateAsset { Metadata = new AssetMetadata().SetPixelWidth(800) }; - actual.ShouldBeEquivalent(sut.Snapshot); + await ExecuteCreateAsync(); - Assert.Equal(command.Metadata, sut.Snapshot.Metadata); + var actual = await PublishIdempotentAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetAnnotated { Metadata = command.Metadata }) - ); + actual.ShouldBeEquivalent(sut.Snapshot); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<anootate-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + Assert.Equal(command.Metadata, sut.Snapshot.Metadata); - [Fact] - public async Task AnnotateTags_should_create_events_and_update_tags() - { - var command = new AnnotateAsset { Tags = new HashSet<string> { "tag1" } }; + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetAnnotated { Metadata = command.Metadata }) + ); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<anootate-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - var actual = await PublishIdempotentAsync(command); + [Fact] + public async Task AnnotateTags_should_create_events_and_update_tags() + { + var command = new AnnotateAsset { Tags = new HashSet<string> { "tag1" } }; - actual.ShouldBeEquivalent(sut.Snapshot); + await ExecuteCreateAsync(); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetAnnotated { Tags = new HashSet<string> { "tag1" } }) - ); + var actual = await PublishIdempotentAsync(command); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + actual.ShouldBeEquivalent(sut.Snapshot); - [Fact] - public async Task Move_should_create_events_and_update_parent_id() - { - var command = new MoveAsset { ParentId = parentId }; + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetAnnotated { Tags = new HashSet<string> { "tag1" } }) + ); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - var actual = await PublishIdempotentAsync(command); + [Fact] + public async Task Move_should_create_events_and_update_parent_id() + { + var command = new MoveAsset { ParentId = parentId }; - actual.ShouldBeEquivalent(sut.Snapshot); + await ExecuteCreateAsync(); - Assert.Equal(parentId, sut.Snapshot.ParentId); + var actual = await PublishIdempotentAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetMoved { ParentId = parentId }) - ); + actual.ShouldBeEquivalent(sut.Snapshot); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<move-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + Assert.Equal(parentId, sut.Snapshot.ParentId); - [Fact] - public async Task Delete_should_create_events_with_total_file_size_and_tags_and_update_deleted_flag() - { - var command = new DeleteAsset(); + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetMoved { ParentId = parentId }) + ); - await ExecuteCreateAsync(); - await ExecuteUpdateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<move-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - var actual = await PublishAsync(command); + [Fact] + public async Task Delete_should_create_events_with_total_file_size_and_tags_and_update_deleted_flag() + { + var command = new DeleteAsset(); - actual.ShouldBeEquivalent(None.Value); + await ExecuteCreateAsync(); + await ExecuteUpdateAsync(); - Assert.True(sut.Snapshot.IsDeleted); + var actual = await PublishAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetDeleted { DeletedSize = 2048, OldTags = new HashSet<string>() }) - ); + actual.ShouldBeEquivalent(None.Value); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + Assert.True(sut.Snapshot.IsDeleted); - [Fact] - public async Task Delete_should_not_create_events_if_permanent() - { - var command = new DeleteAsset { Permanent = true }; + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetDeleted { DeletedSize = 2048, OldTags = new HashSet<string>() }) + ); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._)) - .Returns(false); + [Fact] + public async Task Delete_should_not_create_events_if_permanent() + { + var command = new DeleteAsset { Permanent = true }; - var actual = await PublishAsync(command); + await ExecuteCreateAsync(); - actual.ShouldBeEquivalent(None.Value); + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._)) + .Returns(false); - Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); - Assert.Empty(LastEvents); + var actual = await PublishAsync(command); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + actual.ShouldBeEquivalent(None.Value); - [Fact] - public async Task Delete_should_throw_exception_if_referenced_by_other_item() - { - var command = new DeleteAsset { CheckReferrers = true }; + Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); + Assert.Empty(LastEvents); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._)) - .Returns(true); + [Fact] + public async Task Delete_should_throw_exception_if_referenced_by_other_item() + { + var command = new DeleteAsset { CheckReferrers = true }; - await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command)); + await ExecuteCreateAsync(); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._)) + .Returns(true); - [Fact] - public async Task Delete_should_not_throw_exception_if_referenced_by_other_item_but_forced() - { - var command = new DeleteAsset(); + await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command)); - await ExecuteCreateAsync(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._)) - .Returns(true); + [Fact] + public async Task Delete_should_not_throw_exception_if_referenced_by_other_item_but_forced() + { + var command = new DeleteAsset(); - await PublishAsync(command); + await ExecuteCreateAsync(); - A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._)) + .Returns(true); - private Task ExecuteCreateAsync() - { - return PublishAsync(new CreateAsset { File = file, FileHash = "123" }); - } + await PublishAsync(command); - private Task ExecuteUpdateAsync() - { - return PublishAsync(new UpdateAsset { File = file, FileHash = "456" }); - } + A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - private Task ExecuteDeleteAsync(bool permanent = false) - { - return PublishAsync(new DeleteAsset { Permanent = permanent }); - } + private Task ExecuteCreateAsync() + { + return PublishAsync(new CreateAsset { File = file, FileHash = "123" }); + } - private static ScriptOptions ScriptOptions() - { - return A<ScriptOptions>.That.Matches(x => x.CanDisallow && x.CanReject && x.AsContext); - } + private Task ExecuteUpdateAsync() + { + return PublishAsync(new UpdateAsset { File = file, FileHash = "456" }); + } - private T CreateAssetEvent<T>(T @event) where T : AssetEvent - { - @event.AssetId = assetId; + private Task ExecuteDeleteAsync(bool permanent = false) + { + return PublishAsync(new DeleteAsset { Permanent = permanent }); + } - return CreateEvent(@event); - } + private static ScriptOptions ScriptOptions() + { + return A<ScriptOptions>.That.Matches(x => x.CanDisallow && x.CanReject && x.AsContext); + } - private T CreateAssetCommand<T>(T command) where T : AssetCommand - { - command.AssetId = assetId; + private T CreateAssetEvent<T>(T @event) where T : AssetEvent + { + @event.AssetId = assetId; - return CreateCommand(command); - } + return CreateEvent(@event); + } - private Task<object> PublishIdempotentAsync(AssetCommand command) - { - return PublishIdempotentAsync(sut, CreateAssetCommand(command)); - } + private T CreateAssetCommand<T>(T command) where T : AssetCommand + { + command.AssetId = assetId; - private async Task<object> PublishAsync(AssetCommand command) - { - var actual = await sut.ExecuteAsync(CreateAssetCommand(command), default); + return CreateCommand(command); + } + + private Task<object> PublishIdempotentAsync(AssetCommand command) + { + return PublishIdempotentAsync(sut, CreateAssetCommand(command)); + } + + private async Task<object> PublishAsync(AssetCommand command) + { + var actual = await sut.ExecuteAsync(CreateAssetCommand(command), default); - return actual.Payload; - } + return actual.Payload; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs index 27efa89b80..d87a02d2f9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs @@ -16,170 +16,169 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public class AssetFolderDomainObjectTests : HandlerTestBase<AssetFolderDomainObject.State> { - public class AssetFolderDomainObjectTests : HandlerTestBase<AssetFolderDomainObject.State> + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly DomainId parentId = DomainId.NewGuid(); + private readonly DomainId assetFolderId = DomainId.NewGuid(); + private readonly AssetFolderDomainObject sut; + + protected override DomainId Id + { + get => DomainId.Combine(AppId, assetFolderId); + } + + public AssetFolderDomainObjectTests() { - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly DomainId parentId = DomainId.NewGuid(); - private readonly DomainId assetFolderId = DomainId.NewGuid(); - private readonly AssetFolderDomainObject sut; - - protected override DomainId Id - { - get => DomainId.Combine(AppId, assetFolderId); - } - - public AssetFolderDomainObjectTests() - { - app = Mocks.App(AppNamedId, Language.DE); - - A.CallTo(() => appProvider.GetAppAsync(AppId, false, default)) - .Returns(app); - - A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._)) - .Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() }); - - var log = A.Fake<ILogger<AssetFolderDomainObject>>(); - - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(assetQuery) - .AddSingleton(contentRepository) - .AddSingleton(log) - .BuildServiceProvider(); + app = Mocks.App(AppNamedId, Language.DE); + + A.CallTo(() => appProvider.GetAppAsync(AppId, false, default)) + .Returns(app); + + A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._)) + .Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() }); + + var log = A.Fake<ILogger<AssetFolderDomainObject>>(); + + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(assetQuery) + .AddSingleton(contentRepository) + .AddSingleton(log) + .BuildServiceProvider(); #pragma warning disable MA0056 // Do not call overridable members in constructor - sut = new AssetFolderDomainObject(Id, PersistenceFactory, log, serviceProvider); + sut = new AssetFolderDomainObject(Id, PersistenceFactory, log, serviceProvider); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - [Fact] - public async Task Command_should_throw_exception_if_rule_is_deleted() - { - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); + [Fact] + public async Task Command_should_throw_exception_if_rule_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); - await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteUpdateAsync); - } + await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteUpdateAsync); + } - [Fact] - public async Task Create_should_create_events_and_set_intitial_state() - { - var command = new CreateAssetFolder { FolderName = "New Name" }; + [Fact] + public async Task Create_should_create_events_and_set_intitial_state() + { + var command = new CreateAssetFolder { FolderName = "New Name" }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.FolderName, sut.Snapshot.FolderName); + Assert.Equal(command.FolderName, sut.Snapshot.FolderName); - LastEvents - .ShouldHaveSameEvents( - CreateAssetFolderEvent(new AssetFolderCreated { FolderName = command.FolderName }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateAssetFolderEvent(new AssetFolderCreated { FolderName = command.FolderName }) + ); + } - [Fact] - public async Task Update_should_create_events_and_update_state() - { - var command = new RenameAssetFolder { FolderName = "New Name" }; + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = new RenameAssetFolder { FolderName = "New Name" }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.FolderName, sut.Snapshot.FolderName); + Assert.Equal(command.FolderName, sut.Snapshot.FolderName); - LastEvents - .ShouldHaveSameEvents( - CreateAssetFolderEvent(new AssetFolderRenamed { FolderName = command.FolderName }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateAssetFolderEvent(new AssetFolderRenamed { FolderName = command.FolderName }) + ); + } - [Fact] - public async Task Move_should_create_events_and_update_state() - { - var command = new MoveAssetFolder { ParentId = parentId }; + [Fact] + public async Task Move_should_create_events_and_update_state() + { + var command = new MoveAssetFolder { ParentId = parentId }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(parentId, sut.Snapshot.ParentId); + Assert.Equal(parentId, sut.Snapshot.ParentId); - LastEvents - .ShouldHaveSameEvents( - CreateAssetFolderEvent(new AssetFolderMoved { ParentId = parentId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateAssetFolderEvent(new AssetFolderMoved { ParentId = parentId }) + ); + } - [Fact] - public async Task Delete_should_create_events_with_total_file_size() - { - var command = new DeleteAssetFolder(); + [Fact] + public async Task Delete_should_create_events_with_total_file_size() + { + var command = new DeleteAssetFolder(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); - Assert.True(sut.Snapshot.IsDeleted); + Assert.True(sut.Snapshot.IsDeleted); - LastEvents - .ShouldHaveSameEvents( - CreateAssetFolderEvent(new AssetFolderDeleted()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateAssetFolderEvent(new AssetFolderDeleted()) + ); + } - private Task ExecuteCreateAsync() - { - return PublishAsync(new CreateAssetFolder { FolderName = "My Folder" }); - } + private Task ExecuteCreateAsync() + { + return PublishAsync(new CreateAssetFolder { FolderName = "My Folder" }); + } - private Task ExecuteUpdateAsync() - { - return PublishAsync(new RenameAssetFolder { FolderName = "My Folder" }); - } + private Task ExecuteUpdateAsync() + { + return PublishAsync(new RenameAssetFolder { FolderName = "My Folder" }); + } - private Task ExecuteDeleteAsync() - { - return PublishAsync(new DeleteAssetFolder()); - } + private Task ExecuteDeleteAsync() + { + return PublishAsync(new DeleteAssetFolder()); + } - private T CreateAssetFolderEvent<T>(T @event) where T : AssetFolderEvent - { - @event.AssetFolderId = assetFolderId; + private T CreateAssetFolderEvent<T>(T @event) where T : AssetFolderEvent + { + @event.AssetFolderId = assetFolderId; - return CreateEvent(@event); - } + return CreateEvent(@event); + } - private T CreateAssetFolderCommand<T>(T command) where T : AssetFolderCommand - { - command.AssetFolderId = assetFolderId; + private T CreateAssetFolderCommand<T>(T command) where T : AssetFolderCommand + { + command.AssetFolderId = assetFolderId; - return CreateCommand(command); - } + return CreateCommand(command); + } - private Task<object> PublishIdempotentAsync(AssetFolderCommand command) - { - return PublishIdempotentAsync(sut, CreateAssetFolderCommand(command)); - } + private Task<object> PublishIdempotentAsync(AssetFolderCommand command) + { + return PublishIdempotentAsync(sut, CreateAssetFolderCommand(command)); + } - private async Task<object> PublishAsync(AssetFolderCommand command) - { - var actual = await sut.ExecuteAsync(CreateAssetFolderCommand(command), default); + private async Task<object> PublishAsync(AssetFolderCommand command) + { + var actual = await sut.ExecuteAsync(CreateAssetFolderCommand(command), default); - return actual.Payload; - } + return actual.Payload; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs index f5ce11563c..6fbfe6f816 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs @@ -16,196 +16,195 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject; + +public class AssetsBulkUpdateCommandMiddlewareTests { - public class AssetsBulkUpdateCommandMiddlewareTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); + private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly AssetsBulkUpdateCommandMiddleware sut; + + public AssetsBulkUpdateCommandMiddlewareTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); - private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly AssetsBulkUpdateCommandMiddleware sut; - - public AssetsBulkUpdateCommandMiddlewareTests() - { - ct = cts.Token; + ct = cts.Token; - var log = A.Fake<ILogger<AssetsBulkUpdateCommandMiddleware>>(); + var log = A.Fake<ILogger<AssetsBulkUpdateCommandMiddleware>>(); - sut = new AssetsBulkUpdateCommandMiddleware(contextProvider, log); - } + sut = new AssetsBulkUpdateCommandMiddleware(contextProvider, log); + } - [Fact] - public async Task Should_do_nothing_if_jobs_is_null() - { - var command = new BulkUpdateAssets(); + [Fact] + public async Task Should_do_nothing_if_jobs_is_null() + { + var command = new BulkUpdateAssets(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_do_nothing_if_jobs_is_empty() - { - var command = new BulkUpdateAssets { Jobs = Array.Empty<BulkUpdateJob>() }; + [Fact] + public async Task Should_do_nothing_if_jobs_is_empty() + { + var command = new BulkUpdateAssets { Jobs = Array.Empty<BulkUpdateJob>() }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_annotate_asset() - { - SetupContext(PermissionIds.AppAssetsUpdate); + [Fact] + public async Task Should_annotate_asset() + { + SetupContext(PermissionIds.AppAssetsUpdate); - var id = DomainId.NewGuid(); + var id = DomainId.NewGuid(); - var command = BulkCommand(BulkUpdateAssetType.Annotate, id, fileName: "file"); + var command = BulkCommand(BulkUpdateAssetType.Annotate, id, fileName: "file"); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync(A<AnnotateAsset>.That.Matches(x => x.AssetId == id && x.FileName == "file"), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<AnnotateAsset>.That.Matches(x => x.AssetId == id && x.FileName == "file"), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_annotating() - { - SetupContext(PermissionIds.AppAssetsRead); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_annotating() + { + SetupContext(PermissionIds.AppAssetsRead); - var id = DomainId.NewGuid(); + var id = DomainId.NewGuid(); - var command = BulkCommand(BulkUpdateAssetType.Move, id); + var command = BulkCommand(BulkUpdateAssetType.Move, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_move_asset() - { - SetupContext(PermissionIds.AppAssetsUpdate); + [Fact] + public async Task Should_move_asset() + { + SetupContext(PermissionIds.AppAssetsUpdate); - var id = DomainId.NewGuid(); + var id = DomainId.NewGuid(); - var command = BulkCommand(BulkUpdateAssetType.Move, id); + var command = BulkCommand(BulkUpdateAssetType.Move, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync(A<MoveAsset>.That.Matches(x => x.AssetId == id), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<MoveAsset>.That.Matches(x => x.AssetId == id), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_moving() - { - SetupContext(PermissionIds.AppAssetsRead); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_moving() + { + SetupContext(PermissionIds.AppAssetsRead); - var id = DomainId.NewGuid(); + var id = DomainId.NewGuid(); - var command = BulkCommand(BulkUpdateAssetType.Move, id); + var command = BulkCommand(BulkUpdateAssetType.Move, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_delete_asset() - { - SetupContext(PermissionIds.AppAssetsDelete); + [Fact] + public async Task Should_delete_asset() + { + SetupContext(PermissionIds.AppAssetsDelete); - var id = DomainId.NewGuid(); + var id = DomainId.NewGuid(); - var command = BulkCommand(BulkUpdateAssetType.Delete, id); + var command = BulkCommand(BulkUpdateAssetType.Delete, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<DeleteAsset>.That.Matches(x => x.AssetId == id), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<DeleteAsset>.That.Matches(x => x.AssetId == id), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_deletion() - { - SetupContext(PermissionIds.AppAssetsRead); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_deletion() + { + SetupContext(PermissionIds.AppAssetsRead); - var id = DomainId.NewGuid(); + var id = DomainId.NewGuid(); - var command = BulkCommand(BulkUpdateAssetType.Delete, id: id); + var command = BulkCommand(BulkUpdateAssetType.Delete, id: id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - private async Task<BulkUpdateResult> PublishAsync(ICommand command) - { - var context = new CommandContext(command, commandBus); + private async Task<BulkUpdateResult> PublishAsync(ICommand command) + { + var context = new CommandContext(command, commandBus); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - return (context.PlainResult as BulkUpdateResult)!; - } + return (context.PlainResult as BulkUpdateResult)!; + } - private BulkUpdateAssets BulkCommand(BulkUpdateAssetType type, DomainId id, string? fileName = null) + private BulkUpdateAssets BulkCommand(BulkUpdateAssetType type, DomainId id, string? fileName = null) + { + return new BulkUpdateAssets { - return new BulkUpdateAssets + AppId = appId, + Jobs = new[] { - AppId = appId, - Jobs = new[] + new BulkUpdateJob { - new BulkUpdateJob - { - Type = type, - Id = id, - FileName = fileName - } + Type = type, + Id = id, + FileName = fileName } - }; - } + } + }; + } - private Context SetupContext(string id) - { - var permission = PermissionIds.ForApp(id, appId.Name).Id; + private Context SetupContext(string id) + { + var permission = PermissionIds.ForApp(id, appId.Name).Id; - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); - var requestContext = new Context(claimsPrincipal, Mocks.App(appId)); + var requestContext = new Context(claimsPrincipal, Mocks.App(appId)); - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); - return requestContext; - } + return requestContext; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs index 82c3553bfc..3e41ebdf92 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs @@ -15,144 +15,143 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; + +public class GuardAssetFolderTests : IClassFixture<TranslationsFixture> { - public class GuardAssetFolderTests : IClassFixture<TranslationsFixture> + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RefToken actor = RefToken.User("123"); + + [Fact] + public void Should_throw_exception_if_folder_name_not_defined() { - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RefToken actor = RefToken.User("123"); + var operation = Operation(CreateAssetFolder()); - [Fact] - public void Should_throw_exception_if_folder_name_not_defined() - { - var operation = Operation(CreateAssetFolder()); + ValidationAssert.Throws(() => operation.MustHaveName(null!), + new ValidationError("Folder name is required.", "FolderName")); + } - ValidationAssert.Throws(() => operation.MustHaveName(null!), - new ValidationError("Folder name is required.", "FolderName")); - } + [Fact] + public void Should_not_throw_exception_if_folder_name_defined() + { + var operation = Operation(CreateAssetFolder()); - [Fact] - public void Should_not_throw_exception_if_folder_name_defined() - { - var operation = Operation(CreateAssetFolder()); + operation.MustHaveName("Folder"); + } - operation.MustHaveName("Folder"); - } + [Fact] + public async Task Should_throw_exception_if_moving_to_invalid_folder() + { + var parentId = DomainId.NewGuid(); - [Fact] - public async Task Should_throw_exception_if_moving_to_invalid_folder() - { - var parentId = DomainId.NewGuid(); + var operation = Operation(CreateAssetFolder()); - var operation = Operation(CreateAssetFolder()); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) + .Returns(new List<IAssetFolderEntity>()); - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) - .Returns(new List<IAssetFolderEntity>()); + await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId), + new ValidationError("Asset folder does not exist.", "ParentId")); + } - await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId), - new ValidationError("Asset folder does not exist.", "ParentId")); - } + [Fact] + public async Task Should_not_throw_exception_if_moving_to_valid_folder() + { + var parentId = DomainId.NewGuid(); - [Fact] - public async Task Should_not_throw_exception_if_moving_to_valid_folder() - { - var parentId = DomainId.NewGuid(); + var operation = Operation(CreateAssetFolder()); - var operation = Operation(CreateAssetFolder()); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) + .Returns(new List<IAssetFolderEntity> { CreateAssetFolder() }); - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) - .Returns(new List<IAssetFolderEntity> { CreateAssetFolder() }); + await operation.MustMoveToValidFolder(parentId); + } - await operation.MustMoveToValidFolder(parentId); - } + [Fact] + public async Task Should_not_throw_exception_if_moving_to_same_folder() + { + var parentId = DomainId.NewGuid(); - [Fact] - public async Task Should_not_throw_exception_if_moving_to_same_folder() - { - var parentId = DomainId.NewGuid(); + var operation = Operation(CreateAssetFolder(default, parentId)); - var operation = Operation(CreateAssetFolder(default, parentId)); + await operation.MustMoveToValidFolder(parentId); - await operation.MustMoveToValidFolder(parentId); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_not_throw_exception_if_moving_to_root() + { + var parentId = DomainId.Empty; - [Fact] - public async Task Should_not_throw_exception_if_moving_to_root() - { - var parentId = DomainId.Empty; + var operation = Operation(CreateAssetFolder()); - var operation = Operation(CreateAssetFolder()); + await operation.MustMoveToValidFolder(parentId); - await operation.MustMoveToValidFolder(parentId); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, A<DomainId>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, A<DomainId>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_throw_exception_if_moving_its_own_child() + { + var parentId = DomainId.NewGuid(); - [Fact] - public async Task Should_throw_exception_if_moving_its_own_child() - { - var parentId = DomainId.NewGuid(); + var operation = Operation(CreateAssetFolder()); - var operation = Operation(CreateAssetFolder()); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) + .Returns(new List<IAssetFolderEntity> + { + CreateAssetFolder(operation.CommandId), + CreateAssetFolder(parentId, operation.CommandId) + }); - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) - .Returns(new List<IAssetFolderEntity> - { - CreateAssetFolder(operation.CommandId), - CreateAssetFolder(parentId, operation.CommandId) - }); + await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId), + new ValidationError("Cannot add folder to its own child.", "ParentId")); + } - await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId), - new ValidationError("Cannot add folder to its own child.", "ParentId")); - } + private AssetFolderOperation Operation(IAssetFolderEntity assetFolder) + { + return Operation(assetFolder, Mocks.FrontendUser()); + } - private AssetFolderOperation Operation(IAssetFolderEntity assetFolder) - { - return Operation(assetFolder, Mocks.FrontendUser()); - } + private AssetFolderOperation Operation(IAssetFolderEntity assetFolder, ClaimsPrincipal? currentUser) + { + var serviceProvider = + new ServiceCollection() + .AddSingleton(assetQuery) + .BuildServiceProvider(); - private AssetFolderOperation Operation(IAssetFolderEntity assetFolder, ClaimsPrincipal? currentUser) + return new AssetFolderOperation(serviceProvider, () => assetFolder) { - var serviceProvider = - new ServiceCollection() - .AddSingleton(assetQuery) - .BuildServiceProvider(); - - return new AssetFolderOperation(serviceProvider, () => assetFolder) - { - App = Mocks.App(appId), - CommandId = assetFolder.Id, - Command = new CreateAssetFolder { User = currentUser, Actor = actor } - }; - } + App = Mocks.App(appId), + CommandId = assetFolder.Id, + Command = new CreateAssetFolder { User = currentUser, Actor = actor } + }; + } - private IAssetFolderEntity CreateAssetFolder(DomainId id = default, DomainId parentId = default) - { - var assetFolder = A.Fake<IAssetFolderEntity>(); + private IAssetFolderEntity CreateAssetFolder(DomainId id = default, DomainId parentId = default) + { + var assetFolder = A.Fake<IAssetFolderEntity>(); - A.CallTo(() => assetFolder.Id) - .Returns(OrNew(id)); - A.CallTo(() => assetFolder.AppId) - .Returns(appId); - A.CallTo(() => assetFolder.ParentId) - .Returns(OrNew(parentId)); + A.CallTo(() => assetFolder.Id) + .Returns(OrNew(id)); + A.CallTo(() => assetFolder.AppId) + .Returns(appId); + A.CallTo(() => assetFolder.ParentId) + .Returns(OrNew(parentId)); - return assetFolder; - } + return assetFolder; + } - private static DomainId OrNew(DomainId parentId) + private static DomainId OrNew(DomainId parentId) + { + if (parentId == default) { - if (parentId == default) - { - parentId = DomainId.NewGuid(); - } - - return parentId; + parentId = DomainId.NewGuid(); } + + return parentId; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs index f0105f1245..53e36d22f1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs @@ -17,145 +17,144 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; + +public class GuardAssetTests : IClassFixture<TranslationsFixture> { - public class GuardAssetTests : IClassFixture<TranslationsFixture> + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RefToken actor = RefToken.User("123"); + + [Fact] + public async Task Should_throw_exception_if_moving_to_invalid_folder() { - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RefToken actor = RefToken.User("123"); + var parentId = DomainId.NewGuid(); - [Fact] - public async Task Should_throw_exception_if_moving_to_invalid_folder() - { - var parentId = DomainId.NewGuid(); + var operation = Operation(CreateAsset()); - var operation = Operation(CreateAsset()); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default)) + .Returns(new List<IAssetFolderEntity>()); - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default)) - .Returns(new List<IAssetFolderEntity>()); + await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId), + new ValidationError("Asset folder does not exist.", "ParentId")); + } - await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId), - new ValidationError("Asset folder does not exist.", "ParentId")); - } + [Fact] + public async Task Should_not_throw_exception_if_moving_to_valid_folder() + { + var parentId = DomainId.NewGuid(); - [Fact] - public async Task Should_not_throw_exception_if_moving_to_valid_folder() - { - var parentId = DomainId.NewGuid(); + var operation = Operation(CreateAsset()); - var operation = Operation(CreateAsset()); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default)) + .Returns(new List<IAssetFolderEntity> { CreateAssetFolder() }); - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default)) - .Returns(new List<IAssetFolderEntity> { CreateAssetFolder() }); + await operation.MustMoveToValidFolder(parentId); + } - await operation.MustMoveToValidFolder(parentId); - } + [Fact] + public async Task Should_not_throw_exception_if_moving_to_same_folder() + { + var parentId = DomainId.NewGuid(); - [Fact] - public async Task Should_not_throw_exception_if_moving_to_same_folder() - { - var parentId = DomainId.NewGuid(); + var operation = Operation(CreateAsset(default, parentId)); - var operation = Operation(CreateAsset(default, parentId)); + await operation.MustMoveToValidFolder(parentId); - await operation.MustMoveToValidFolder(parentId); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_not_throw_exception_if_moving_to_root() + { + var parentId = DomainId.Empty; - [Fact] - public async Task Should_not_throw_exception_if_moving_to_root() - { - var parentId = DomainId.Empty; + var operation = Operation(CreateAsset(parentId)); - var operation = Operation(CreateAsset(parentId)); + await operation.MustMoveToValidFolder(parentId); - await operation.MustMoveToValidFolder(parentId); + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_throw_exception_if_referenced() + { + var operation = Operation(CreateAsset()); - [Fact] - public async Task Should_throw_exception_if_referenced() - { - var operation = Operation(CreateAsset()); + A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default)) + .Returns(true); - A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default)) - .Returns(true); + await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync()); + } - await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync()); - } + [Fact] + public async Task Should_not_throw_exception_if_not_referenced() + { + var operation = Operation(CreateAsset()); - [Fact] - public async Task Should_not_throw_exception_if_not_referenced() - { - var operation = Operation(CreateAsset()); + A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default)) + .Returns(true); - A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default)) - .Returns(true); + await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync()); + } - await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync()); - } + private AssetOperation Operation(AssetEntity asset) + { + return Operation(asset, Mocks.FrontendUser()); + } - private AssetOperation Operation(AssetEntity asset) - { - return Operation(asset, Mocks.FrontendUser()); - } + private AssetOperation Operation(AssetEntity asset, ClaimsPrincipal? currentUser) + { + var serviceProvider = + new ServiceCollection() + .AddSingleton(contentRepository) + .AddSingleton(assetQuery) + .BuildServiceProvider(); - private AssetOperation Operation(AssetEntity asset, ClaimsPrincipal? currentUser) + return new AssetOperation(serviceProvider, () => asset) { - var serviceProvider = - new ServiceCollection() - .AddSingleton(contentRepository) - .AddSingleton(assetQuery) - .BuildServiceProvider(); - - return new AssetOperation(serviceProvider, () => asset) - { - App = Mocks.App(appId), - CommandId = asset.Id, - Command = new CreateAsset { User = currentUser, Actor = actor } - }; - } + App = Mocks.App(appId), + CommandId = asset.Id, + Command = new CreateAsset { User = currentUser, Actor = actor } + }; + } - private AssetEntity CreateAsset(DomainId id = default, DomainId parentId = default) + private AssetEntity CreateAsset(DomainId id = default, DomainId parentId = default) + { + return new AssetEntity { - return new AssetEntity - { - Id = OrNew(id), - AppId = appId, - Created = default, - CreatedBy = actor, - ParentId = OrNew(parentId) - }; - } + Id = OrNew(id), + AppId = appId, + Created = default, + CreatedBy = actor, + ParentId = OrNew(parentId) + }; + } - private IAssetFolderEntity CreateAssetFolder(DomainId id = default, DomainId parentId = default) - { - var assetFolder = A.Fake<IAssetFolderEntity>(); + private IAssetFolderEntity CreateAssetFolder(DomainId id = default, DomainId parentId = default) + { + var assetFolder = A.Fake<IAssetFolderEntity>(); - A.CallTo(() => assetFolder.Id) - .Returns(OrNew(id)); - A.CallTo(() => assetFolder.AppId) - .Returns(appId); - A.CallTo(() => assetFolder.ParentId) - .Returns(OrNew(parentId)); + A.CallTo(() => assetFolder.Id) + .Returns(OrNew(id)); + A.CallTo(() => assetFolder.AppId) + .Returns(appId); + A.CallTo(() => assetFolder.ParentId) + .Returns(OrNew(parentId)); - return assetFolder; - } + return assetFolder; + } - private static DomainId OrNew(DomainId parentId) + private static DomainId OrNew(DomainId parentId) + { + if (parentId == default) { - if (parentId == default) - { - parentId = DomainId.NewGuid(); - } - - return parentId; + parentId = DomainId.NewGuid(); } + + return parentId; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs index 9b5ad5e048..f6d31e3f40 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs @@ -9,105 +9,104 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; + +public class ScriptMetadataWrapperTests { - public class ScriptMetadataWrapperTests + private readonly AssetMetadata metadata = new AssetMetadata(); + private readonly ScriptMetadataWrapper sut; + + public ScriptMetadataWrapperTests() { - private readonly AssetMetadata metadata = new AssetMetadata(); - private readonly ScriptMetadataWrapper sut; + sut = new ScriptMetadataWrapper(metadata); + } - public ScriptMetadataWrapperTests() - { - sut = new ScriptMetadataWrapper(metadata); - } + [Fact] + public void Should_add_value() + { + sut.Add("key", 1); - [Fact] - public void Should_add_value() - { - sut.Add("key", 1); + Assert.Equal(JsonValue.Create(1), metadata["key"]); + Assert.Equal(JsonValue.Create(1), sut["key"]); - Assert.Equal(JsonValue.Create(1), metadata["key"]); - Assert.Equal(JsonValue.Create(1), sut["key"]); + Assert.True(metadata.ContainsKey("key")); + Assert.True(sut.ContainsKey("key")); - Assert.True(metadata.ContainsKey("key")); - Assert.True(sut.ContainsKey("key")); + Assert.Single(metadata); + Assert.Single(sut); + } - Assert.Single(metadata); - Assert.Single(sut); - } + [Fact] + public void Should_set_value() + { + sut["key"] = 1; - [Fact] - public void Should_set_value() - { - sut["key"] = 1; + Assert.Equal(JsonValue.Create(1), metadata["key"]); + Assert.Equal(JsonValue.Create(1), sut["key"]); - Assert.Equal(JsonValue.Create(1), metadata["key"]); - Assert.Equal(JsonValue.Create(1), sut["key"]); + Assert.True(metadata.ContainsKey("key")); + Assert.True(sut.ContainsKey("key")); - Assert.True(metadata.ContainsKey("key")); - Assert.True(sut.ContainsKey("key")); + Assert.Single(metadata); + Assert.Single(sut); + } - Assert.Single(metadata); - Assert.Single(sut); - } + [Fact] + public void Should_provide_keys() + { + sut["key1"] = 1; + sut["key2"] = 2; - [Fact] - public void Should_provide_keys() - { - sut["key1"] = 1; - sut["key2"] = 2; - - Assert.Equal(new[] - { - "key1", - "key2" - }, sut.Keys.ToArray()); - } - - [Fact] - public void Should_provide_values() - { - sut["key1"] = 1; - sut["key2"] = 2; - - Assert.Equal(new object[] - { - JsonValue.Create(1), - JsonValue.Create(2) - }, sut.Values.ToArray()); - } - - [Fact] - public void Should_enumerate_values() + Assert.Equal(new[] { - sut["key1"] = 1; - sut["key2"] = 2; - - Assert.Equal(new[] - { - new KeyValuePair<string, object?>("key1", JsonValue.Create(1)), - new KeyValuePair<string, object?>("key2", JsonValue.Create(2)) - }, sut.ToArray()); - } - - [Fact] - public void Should_remove_value() + "key1", + "key2" + }, sut.Keys.ToArray()); + } + + [Fact] + public void Should_provide_values() + { + sut["key1"] = 1; + sut["key2"] = 2; + + Assert.Equal(new object[] { - sut["key"] = 1; - sut.Remove("key"); + JsonValue.Create(1), + JsonValue.Create(2) + }, sut.Values.ToArray()); + } - Assert.Empty(metadata); - Assert.Empty(sut); - } + [Fact] + public void Should_enumerate_values() + { + sut["key1"] = 1; + sut["key2"] = 2; - [Fact] - public void Should_clear_collection() + Assert.Equal(new[] { - sut["key"] = 1; - sut.Clear(); + new KeyValuePair<string, object?>("key1", JsonValue.Create(1)), + new KeyValuePair<string, object?>("key2", JsonValue.Create(2)) + }, sut.ToArray()); + } + + [Fact] + public void Should_remove_value() + { + sut["key"] = 1; + sut.Remove("key"); + + Assert.Empty(metadata); + Assert.Empty(sut); + } + + [Fact] + public void Should_clear_collection() + { + sut["key"] = 1; + sut.Clear(); - Assert.Empty(metadata); - Assert.Empty(sut); - } + Assert.Empty(metadata); + Assert.Empty(sut); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptingExtensionsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptingExtensionsTests.cs index 3d914be7d9..49ee733298 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptingExtensionsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptingExtensionsTests.cs @@ -17,107 +17,106 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; + +public sealed class ScriptingExtensionsTests { - public sealed class ScriptingExtensionsTests + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RefToken actor = RefToken.User("123"); + + [Fact] + public async Task Should_add_tag_in_script() { - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RefToken actor = RefToken.User("123"); + var script = "ctx.command.tags.add('tag');"; - [Fact] - public async Task Should_add_tag_in_script() - { - var script = "ctx.command.tags.add('tag');"; + var command = new AnnotateAsset { Tags = new HashSet<string>() }; - var command = new AnnotateAsset { Tags = new HashSet<string>() }; + var operation = Operation(script, CreateAsset(), command); - var operation = Operation(script, CreateAsset(), command); + await operation.ExecuteAnnotateScriptAsync(command); + + Assert.Contains("tag", command.Tags); + } - await operation.ExecuteAnnotateScriptAsync(command); + [Fact] + public async Task Should_add_metadata_in_script() + { + var script = "ctx.command.metadata['foo'] = 42;"; - Assert.Contains("tag", command.Tags); - } + var command = new AnnotateAsset { Metadata = new AssetMetadata() }; - [Fact] - public async Task Should_add_metadata_in_script() - { - var script = "ctx.command.metadata['foo'] = 42;"; + var operation = Operation(script, CreateAsset(), command); - var command = new AnnotateAsset { Metadata = new AssetMetadata() }; + await operation.ExecuteAnnotateScriptAsync(command); - var operation = Operation(script, CreateAsset(), command); + Assert.Equal(JsonValue.Create(42), command.Metadata["foo"]); + } - await operation.ExecuteAnnotateScriptAsync(command); + [Fact] + public async Task Should_not_allow_to_write_asset_tags() + { + var script = "ctx.asset.tags.add('tag');"; - Assert.Equal(JsonValue.Create(42), command.Metadata["foo"]); - } + var command = new AnnotateAsset(); - [Fact] - public async Task Should_not_allow_to_write_asset_tags() - { - var script = "ctx.asset.tags.add('tag');"; + var operation = Operation(script, CreateAsset(), command); - var command = new AnnotateAsset(); + await Assert.ThrowsAsync<ValidationException>(() => operation.ExecuteAnnotateScriptAsync(command)); + } + + [Fact] + public async Task Should_not_allow_to_write_asset_metadata() + { + var script = "ctx.asset.metadata['foo'] = 42;"; - var operation = Operation(script, CreateAsset(), command); + var command = new AnnotateAsset(); - await Assert.ThrowsAsync<ValidationException>(() => operation.ExecuteAnnotateScriptAsync(command)); - } + var operation = Operation(script, CreateAsset(), command); + + await Assert.ThrowsAsync<ValidationException>(() => operation.ExecuteAnnotateScriptAsync(command)); + } - [Fact] - public async Task Should_not_allow_to_write_asset_metadata() + private AssetOperation Operation(string script, AssetEntity asset, AnnotateAsset command) + { + var scripts = new AssetScripts { - var script = "ctx.asset.metadata['foo'] = 42;"; + Annotate = script + }; + + var app = Mocks.App(appId); - var command = new AnnotateAsset(); + A.CallTo(() => app.AssetScripts) + .Returns(scripts); - var operation = Operation(script, CreateAsset(), command); + var serviceProvider = + new ServiceCollection() + .AddMemoryCache() + .AddOptions() + .AddOptions<MemoryCacheOptions>().Services + .AddSingleton<IScriptEngine, JintScriptEngine>() + .BuildServiceProvider(); - await Assert.ThrowsAsync<ValidationException>(() => operation.ExecuteAnnotateScriptAsync(command)); - } + command.Actor = actor; + command.User = Mocks.FrontendUser(); - private AssetOperation Operation(string script, AssetEntity asset, AnnotateAsset command) + return new AssetOperation(serviceProvider, () => asset) { - var scripts = new AssetScripts - { - Annotate = script - }; - - var app = Mocks.App(appId); - - A.CallTo(() => app.AssetScripts) - .Returns(scripts); - - var serviceProvider = - new ServiceCollection() - .AddMemoryCache() - .AddOptions() - .AddOptions<MemoryCacheOptions>().Services - .AddSingleton<IScriptEngine, JintScriptEngine>() - .BuildServiceProvider(); - - command.Actor = actor; - command.User = Mocks.FrontendUser(); - - return new AssetOperation(serviceProvider, () => asset) - { - App = app, - CommandId = asset.Id, - Command = command - }; - } - - private AssetEntity CreateAsset() + App = app, + CommandId = asset.Id, + Command = command + }; + } + + private AssetEntity CreateAsset() + { + return new AssetEntity { - return new AssetEntity - { - Id = DomainId.NewGuid(), - AppId = appId, - Created = default, - CreatedBy = actor, - Metadata = new AssetMetadata(), - Tags = new HashSet<string>() - }; - } + Id = DomainId.NewGuid(), + AppId = appId, + Created = default, + CreatedBy = actor, + Metadata = new AssetMetadata(), + Tags = new HashSet<string>() + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs index b4232cbd27..a6f4162c5f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs @@ -11,142 +11,141 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class FileTagAssetMetadataSourceTests { - public class FileTagAssetMetadataSourceTests - { - private readonly FileTagAssetMetadataSource sut = new FileTagAssetMetadataSource(); + private readonly FileTagAssetMetadataSource sut = new FileTagAssetMetadataSource(); - [Fact] - public async Task Should_ignore_files_without_extension() - { - var command = FakeCommand("NoExtension"); + [Fact] + public async Task Should_ignore_files_without_extension() + { + var command = FakeCommand("NoExtension"); - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Equal(AssetType.Unknown, command.Type); - } + Assert.Equal(AssetType.Unknown, command.Type); + } - [Fact] - public async Task Should_provide_metadata_for_image() - { - var command = Command("SamplePNGImage_100kbmb.png"); + [Fact] + public async Task Should_provide_metadata_for_image() + { + var command = Command("SamplePNGImage_100kbmb.png"); - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Equal(AssetType.Image, command.Type); + Assert.Equal(AssetType.Image, command.Type); - Assert.Equal(272, command.Metadata.GetPixelWidth()); - Assert.Equal(170, command.Metadata.GetPixelHeight()); - } + Assert.Equal(272, command.Metadata.GetPixelWidth()); + Assert.Equal(170, command.Metadata.GetPixelHeight()); + } - [Fact] - public async Task Should_not_set_image_height_and_width_metadata_when_file_does_not_have_those_values() - { - var command = Command("SampleAudio_0.4mb.mp3"); + [Fact] + public async Task Should_not_set_image_height_and_width_metadata_when_file_does_not_have_those_values() + { + var command = Command("SampleAudio_0.4mb.mp3"); - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Null(command.Metadata.GetPixelWidth()); - Assert.Null(command.Metadata.GetPixelHeight()); - } + Assert.Null(command.Metadata.GetPixelWidth()); + Assert.Null(command.Metadata.GetPixelHeight()); + } - [Fact] - public async Task Should_provide_metadata_for_audio() - { - var command = Command("SampleAudio_0.4mb.mp3"); + [Fact] + public async Task Should_provide_metadata_for_audio() + { + var command = Command("SampleAudio_0.4mb.mp3"); - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Equal(AssetType.Audio, command.Type); + Assert.Equal(AssetType.Audio, command.Type); - Assert.Equal(JsonValue.Create("00:00:28.2708750"), command.Metadata["duration"]); - Assert.Equal(JsonValue.Create(128L), command.Metadata["audioBitrate"]); - Assert.Equal(JsonValue.Create(2L), command.Metadata["audioChannels"]); - Assert.Equal(JsonValue.Create(44100L), command.Metadata["audioSampleRate"]); - } + Assert.Equal(JsonValue.Create("00:00:28.2708750"), command.Metadata["duration"]); + Assert.Equal(JsonValue.Create(128L), command.Metadata["audioBitrate"]); + Assert.Equal(JsonValue.Create(2L), command.Metadata["audioChannels"]); + Assert.Equal(JsonValue.Create(44100L), command.Metadata["audioSampleRate"]); + } - [Fact] - public async Task Should_provide_metadata_for_video() - { - var command = Command("SampleVideo_1280x720_1mb.mp4"); + [Fact] + public async Task Should_provide_metadata_for_video() + { + var command = Command("SampleVideo_1280x720_1mb.mp4"); - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Equal(AssetType.Video, command.Type); + Assert.Equal(AssetType.Video, command.Type); - Assert.Equal(JsonValue.Create("00:00:05.3120000"), command.Metadata["duration"]); - Assert.Equal(JsonValue.Create(384L), command.Metadata["audioBitrate"]); - Assert.Equal(JsonValue.Create(2L), command.Metadata["audioChannels"]); - Assert.Equal(JsonValue.Create(48000L), command.Metadata["audioSampleRate"]); - Assert.Equal(JsonValue.Create(1280L), command.Metadata["videoWidth"]); - Assert.Equal(JsonValue.Create(720L), command.Metadata["videoHeight"]); - } + Assert.Equal(JsonValue.Create("00:00:05.3120000"), command.Metadata["duration"]); + Assert.Equal(JsonValue.Create(384L), command.Metadata["audioBitrate"]); + Assert.Equal(JsonValue.Create(2L), command.Metadata["audioChannels"]); + Assert.Equal(JsonValue.Create(48000L), command.Metadata["audioSampleRate"]); + Assert.Equal(JsonValue.Create(1280L), command.Metadata["videoWidth"]); + Assert.Equal(JsonValue.Create(720L), command.Metadata["videoHeight"]); + } - [Fact] - public void Should_format_video() + [Fact] + public void Should_format_video() + { + var source = new AssetEntity { - var source = new AssetEntity + Metadata = new AssetMetadata { - Metadata = new AssetMetadata - { - ["videoWidth"] = JsonValue.Create(128), - ["videoHeight"] = JsonValue.Create(55), - ["duration"] = JsonValue.Create("00:10:12") - }, - Type = AssetType.Video - }; - - var formatted = sut.Format(source); - - Assert.Equal(new[] { "128x55pt", "00:10:12" }, formatted); - } - - [Fact] - public void Should_format_audio() + ["videoWidth"] = JsonValue.Create(128), + ["videoHeight"] = JsonValue.Create(55), + ["duration"] = JsonValue.Create("00:10:12") + }, + Type = AssetType.Video + }; + + var formatted = sut.Format(source); + + Assert.Equal(new[] { "128x55pt", "00:10:12" }, formatted); + } + + [Fact] + public void Should_format_audio() + { + var source = new AssetEntity { - var source = new AssetEntity + Metadata = new AssetMetadata { - Metadata = new AssetMetadata - { - ["duration"] = JsonValue.Create("00:10:12") - }, - Type = AssetType.Audio - }; + ["duration"] = JsonValue.Create("00:10:12") + }, + Type = AssetType.Audio + }; - var formatted = sut.Format(source); + var formatted = sut.Format(source); - Assert.Equal(new[] { "00:10:12" }, formatted.ToArray()); - } + Assert.Equal(new[] { "00:10:12" }, formatted.ToArray()); + } - [Fact] - public void Should_not_format_image() - { - var source = new AssetEntity { Type = AssetType.Image }; + [Fact] + public void Should_not_format_image() + { + var source = new AssetEntity { Type = AssetType.Image }; - var formatted = sut.Format(source); + var formatted = sut.Format(source); - Assert.Empty(formatted); - } + Assert.Empty(formatted); + } + + private static UploadAssetCommand Command(string path) + { + var file = new FileInfo(Path.Combine("Assets", "TestFiles", path)); - private static UploadAssetCommand Command(string path) + return new CreateAsset { - var file = new FileInfo(Path.Combine("Assets", "TestFiles", path)); + File = new DelegateAssetFile(file.Name, "mime", file.Length, file.OpenRead) + }; + } - return new CreateAsset - { - File = new DelegateAssetFile(file.Name, "mime", file.Length, file.OpenRead) - }; - } + private static UploadAssetCommand FakeCommand(string name) + { + var stream = new MemoryStream(); - private static UploadAssetCommand FakeCommand(string name) + return new CreateAsset { - var stream = new MemoryStream(); - - return new CreateAsset - { - File = new DelegateAssetFile(name, "mime", stream.Length, () => stream) - }; - } + File = new DelegateAssetFile(name, "mime", stream.Length, () => stream) + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs index 3dd380d9f5..55171c2eed 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs @@ -9,56 +9,55 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class FileTypeAssetMetadataSourceTests { - public class FileTypeAssetMetadataSourceTests - { - private readonly FileTypeAssetMetadataSource sut = new FileTypeAssetMetadataSource(); + private readonly FileTypeAssetMetadataSource sut = new FileTypeAssetMetadataSource(); - [Fact] - public async Task Should_not_add_tag_if_no_file_info() - { - var command = new CreateAsset(); + [Fact] + public async Task Should_not_add_tag_if_no_file_info() + { + var command = new CreateAsset(); - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Empty(command.Tags); - } + Assert.Empty(command.Tags); + } - [Fact] - public async Task Should_add_file_type() + [Fact] + public async Task Should_add_file_type() + { + var command = new CreateAsset { - var command = new CreateAsset - { - File = new NoopAssetFile("File.DOCX") - }; + File = new NoopAssetFile("File.DOCX") + }; - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Contains("type/docx", command.Tags); - } + Assert.Contains("type/docx", command.Tags); + } - [Fact] - public async Task Should_add_blob_if_without_extension() + [Fact] + public async Task Should_add_blob_if_without_extension() + { + var command = new CreateAsset { - var command = new CreateAsset - { - File = new NoopAssetFile("File") - }; + File = new NoopAssetFile("File") + }; - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Contains("type/blob", command.Tags); - } + Assert.Contains("type/blob", command.Tags); + } - [Fact] - public void Should_always_format_to_empty() - { - var source = new AssetEntity(); + [Fact] + public void Should_always_format_to_empty() + { + var source = new AssetEntity(); - var formatted = sut.Format(source); + var formatted = sut.Format(source); - Assert.Empty(formatted); - } + Assert.Empty(formatted); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index a4221b22fe..33a94eb2dd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -12,164 +12,163 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class ImageAssetMetadataSourceTests { - public class ImageAssetMetadataSourceTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); + private readonly MemoryStream stream = new MemoryStream(); + private readonly AssetFile file; + private readonly ImageAssetMetadataSource sut; + + public ImageAssetMetadataSourceTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); - private readonly MemoryStream stream = new MemoryStream(); - private readonly AssetFile file; - private readonly ImageAssetMetadataSource sut; - - public ImageAssetMetadataSourceTests() - { - ct = cts.Token; + ct = cts.Token; - file = new DelegateAssetFile("MyImage.png", "image/png", 1024, () => stream); + file = new DelegateAssetFile("MyImage.png", "image/png", 1024, () => stream); - sut = new ImageAssetMetadataSource(assetThumbnailGenerator); - } + sut = new ImageAssetMetadataSource(assetThumbnailGenerator); + } - [Fact] - public async Task Should_also_enhance_if_type_already_found() - { - var command = new CreateAsset { File = file, Type = AssetType.Image }; + [Fact] + public async Task Should_also_enhance_if_type_already_found() + { + var command = new CreateAsset { File = file, Type = AssetType.Image }; - await sut.EnhanceAsync(command, ct); + await sut.EnhanceAsync(command, ct); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_add_tag_if_no_image() - { - var command = new CreateAsset { File = file }; + [Fact] + public async Task Should_not_add_tag_if_no_image() + { + var command = new CreateAsset { File = file }; - await sut.EnhanceAsync(command, ct); + await sut.EnhanceAsync(command, ct); - Assert.Empty(command.Tags); - } + Assert.Empty(command.Tags); + } - [Fact] - public async Task Should_get_dimensions_from_image_library() - { - var command = new CreateAsset { File = file }; + [Fact] + public async Task Should_get_dimensions_from_image_library() + { + var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, ct)) - .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, ct)) + .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); - await sut.EnhanceAsync(command, ct); + await sut.EnhanceAsync(command, ct); - Assert.Equal(800, command.Metadata.GetPixelWidth()); - Assert.Equal(600, command.Metadata.GetPixelHeight()); - Assert.Equal(AssetType.Image, command.Type); + Assert.Equal(800, command.Metadata.GetPixelWidth()); + Assert.Equal(600, command.Metadata.GetPixelHeight()); + Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A<Stream>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A<Stream>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_fix_image_if_oriented() - { - var command = new CreateAsset { File = file }; + [Fact] + public async Task Should_fix_image_if_oriented() + { + var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, ct)) - .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, ct)) + .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, ct)) - .Returns(new ImageInfo(600, 800, ImageOrientation.BottomRight, ImageFormat.PNG)).Once(); + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, ct)) + .Returns(new ImageInfo(600, 800, ImageOrientation.BottomRight, ImageFormat.PNG)).Once(); - await sut.EnhanceAsync(command, ct); + await sut.EnhanceAsync(command, ct); - Assert.Equal(800, command.Metadata.GetPixelWidth()); - Assert.Equal(600, command.Metadata.GetPixelHeight()); - Assert.Equal(AssetType.Image, command.Type); + Assert.Equal(800, command.Metadata.GetPixelWidth()); + Assert.Equal(600, command.Metadata.GetPixelHeight()); + Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A<Stream>._, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A<Stream>._, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_add_image_tag_if_small() - { - var command = new CreateAsset { File = file, Type = AssetType.Image }; + [Fact] + public async Task Should_add_image_tag_if_small() + { + var command = new CreateAsset { File = file, Type = AssetType.Image }; - command.Metadata.SetPixelWidth(100); - command.Metadata.SetPixelWidth(100); + command.Metadata.SetPixelWidth(100); + command.Metadata.SetPixelWidth(100); - await sut.EnhanceAsync(command, ct); + await sut.EnhanceAsync(command, ct); - Assert.Contains("image", command.Tags); - Assert.Contains("image/small", command.Tags); - } + Assert.Contains("image", command.Tags); + Assert.Contains("image/small", command.Tags); + } - [Fact] - public async Task Should_add_image_tag_if_medium() - { - var command = new CreateAsset { File = file, Type = AssetType.Image }; + [Fact] + public async Task Should_add_image_tag_if_medium() + { + var command = new CreateAsset { File = file, Type = AssetType.Image }; - command.Metadata.SetPixelWidth(800); - command.Metadata.SetPixelWidth(600); + command.Metadata.SetPixelWidth(800); + command.Metadata.SetPixelWidth(600); - await sut.EnhanceAsync(command, ct); + await sut.EnhanceAsync(command, ct); - Assert.Contains("image", command.Tags); - Assert.Contains("image/medium", command.Tags); - } + Assert.Contains("image", command.Tags); + Assert.Contains("image/medium", command.Tags); + } - [Fact] - public async Task Should_add_image_tag_if_large() - { - var command = new CreateAsset { File = file, Type = AssetType.Image }; + [Fact] + public async Task Should_add_image_tag_if_large() + { + var command = new CreateAsset { File = file, Type = AssetType.Image }; - command.Metadata.SetPixelWidth(1200); - command.Metadata.SetPixelWidth(1400); + command.Metadata.SetPixelWidth(1200); + command.Metadata.SetPixelWidth(1400); - await sut.EnhanceAsync(command, ct); + await sut.EnhanceAsync(command, ct); - Assert.Contains("image", command.Tags); - Assert.Contains("image/large", command.Tags); - } + Assert.Contains("image", command.Tags); + Assert.Contains("image/large", command.Tags); + } - [Fact] - public void Should_format_image() + [Fact] + public void Should_format_image() + { + var source = new AssetEntity { - var source = new AssetEntity + Metadata = new AssetMetadata { - Metadata = new AssetMetadata - { - ["pixelWidth"] = JsonValue.Create(128), - ["pixelHeight"] = JsonValue.Create(55) - }, - Type = AssetType.Image - }; + ["pixelWidth"] = JsonValue.Create(128), + ["pixelHeight"] = JsonValue.Create(55) + }, + Type = AssetType.Image + }; - var formatted = sut.Format(source); + var formatted = sut.Format(source); - Assert.Equal(new[] { "128x55px" }, formatted); - } + Assert.Equal(new[] { "128x55px" }, formatted); + } - [Fact] - public void Should_not_format_video() - { - var source = new AssetEntity { Type = AssetType.Video }; + [Fact] + public void Should_not_format_video() + { + var source = new AssetEntity { Type = AssetType.Video }; - var formatted = sut.Format(source); + var formatted = sut.Format(source); - Assert.Empty(formatted); - } + Assert.Empty(formatted); + } - [Fact] - public void Should_not_format_audio() - { - var source = new AssetEntity { Type = AssetType.Audio }; + [Fact] + public void Should_not_format_audio() + { + var source = new AssetEntity { Type = AssetType.Audio }; - var formatted = sut.Format(source); + var formatted = sut.Format(source); - Assert.Empty(formatted); - } + Assert.Empty(formatted); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetMappingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetMappingTests.cs index e7f008c61a..85f1d267fb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetMappingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetMappingTests.cs @@ -14,76 +14,75 @@ using Squidex.Infrastructure.States; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.MongoDb +namespace Squidex.Domain.Apps.Entities.Assets.MongoDb; + +public class AssetMappingTests { - public class AssetMappingTests + [Fact] + public void Should_map_asset() { - [Fact] - public void Should_map_asset() - { - var user = RefToken.User("1"); + var user = RefToken.User("1"); - var time = SystemClock.Instance.GetCurrentInstant(); + var time = SystemClock.Instance.GetCurrentInstant(); - var source = new AssetDomainObject.State - { - Id = DomainId.NewGuid(), - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), - Created = time, - CreatedBy = user, - FileHash = "my-hash", - FileName = "my-image.png", - FileSize = 1024, - FileVersion = 13, - IsDeleted = true, - IsProtected = true, - LastModified = time, - LastModifiedBy = user, - Metadata = new AssetMetadata().SetPixelHeight(600), - MimeType = "image/png", - ParentId = DomainId.NewGuid(), - Slug = "my-image", - Tags = new HashSet<string> { "image" }, - TotalSize = 1024 * 2, - Type = AssetType.Image, - Version = 42 - }; + var source = new AssetDomainObject.State + { + Id = DomainId.NewGuid(), + AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + Created = time, + CreatedBy = user, + FileHash = "my-hash", + FileName = "my-image.png", + FileSize = 1024, + FileVersion = 13, + IsDeleted = true, + IsProtected = true, + LastModified = time, + LastModifiedBy = user, + Metadata = new AssetMetadata().SetPixelHeight(600), + MimeType = "image/png", + ParentId = DomainId.NewGuid(), + Slug = "my-image", + Tags = new HashSet<string> { "image" }, + TotalSize = 1024 * 2, + Type = AssetType.Image, + Version = 42 + }; - var snapshotJob = new SnapshotWriteJob<AssetDomainObject.State>(source.UniqueId, source, source.Version); - var snapshot = MongoAssetEntity.Create(snapshotJob); + var snapshotJob = new SnapshotWriteJob<AssetDomainObject.State>(source.UniqueId, source, source.Version); + var snapshot = MongoAssetEntity.Create(snapshotJob); - var mapped = snapshot.ToState(); + var mapped = snapshot.ToState(); - mapped.Should().BeEquivalentTo(source); - } + mapped.Should().BeEquivalentTo(source); + } - [Fact] - public void Should_map_asset_folder() - { - var user = RefToken.User("1"); + [Fact] + public void Should_map_asset_folder() + { + var user = RefToken.User("1"); - var time = SystemClock.Instance.GetCurrentInstant(); + var time = SystemClock.Instance.GetCurrentInstant(); - var source = new AssetFolderDomainObject.State - { - Id = DomainId.NewGuid(), - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), - Created = time, - CreatedBy = user, - FolderName = "my-folder", - IsDeleted = true, - LastModified = time, - LastModifiedBy = user, - ParentId = DomainId.NewGuid(), - Version = 42 - }; + var source = new AssetFolderDomainObject.State + { + Id = DomainId.NewGuid(), + AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + Created = time, + CreatedBy = user, + FolderName = "my-folder", + IsDeleted = true, + LastModified = time, + LastModifiedBy = user, + ParentId = DomainId.NewGuid(), + Version = 42 + }; - var snapshotJob = new SnapshotWriteJob<AssetFolderDomainObject.State>(source.UniqueId, source, source.Version); - var snapshot = MongoAssetFolderEntity.Create(snapshotJob); + var snapshotJob = new SnapshotWriteJob<AssetFolderDomainObject.State>(source.UniqueId, source, source.Version); + var snapshot = MongoAssetFolderEntity.Create(snapshotJob); - var mapped = snapshot.ToState(); + var mapped = snapshot.ToState(); - mapped.Should().BeEquivalentTo(source); - } + mapped.Should().BeEquivalentTo(source); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs index d08bd7eb44..467795fce5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs @@ -20,222 +20,221 @@ using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; -namespace Squidex.Domain.Apps.Entities.Assets.MongoDb +namespace Squidex.Domain.Apps.Entities.Assets.MongoDb; + +public class AssetQueryTests { - public class AssetQueryTests + private readonly DomainId appId = DomainId.NewGuid(); + + static AssetQueryTests() { - private readonly DomainId appId = DomainId.NewGuid(); + TestUtils.SetupBson(); + } - static AssetQueryTests() - { - TestUtils.SetupBson(); - } + [Fact] + public void Should_throw_exception_for_full_text_search() + { + Assert.Throws<ValidationException>(() => AssertQuery(string.Empty, new ClrQuery { FullText = "Full Text" })); + } - [Fact] - public void Should_throw_exception_for_full_text_search() - { - Assert.Throws<ValidationException>(() => AssertQuery(string.Empty, new ClrQuery { FullText = "Full Text" })); - } + [Fact] + public void Should_make_query_with_id() + { + var id = Guid.NewGuid(); - [Fact] - public void Should_make_query_with_id() - { - var id = Guid.NewGuid(); + var filter = ClrFilter.Eq("id", id); - var filter = ClrFilter.Eq("id", id); + AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); + } - AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); - } + [Fact] + public void Should_make_query_with_id_string() + { + var id = DomainId.NewGuid().ToString(); - [Fact] - public void Should_make_query_with_id_string() - { - var id = DomainId.NewGuid().ToString(); + var filter = ClrFilter.Eq("id", id); - var filter = ClrFilter.Eq("id", id); + AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); + } - AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); - } + [Fact] + public void Should_make_query_with_id_list() + { + var id = Guid.NewGuid(); - [Fact] - public void Should_make_query_with_id_list() - { - var id = Guid.NewGuid(); + var filter = ClrFilter.In("id", new List<Guid> { id }); - var filter = ClrFilter.In("id", new List<Guid> { id }); + AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); + } - AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); - } + [Fact] + public void Should_make_query_with_id_string_list() + { + var id = DomainId.NewGuid().ToString(); - [Fact] - public void Should_make_query_with_id_string_list() - { - var id = DomainId.NewGuid().ToString(); + var filter = ClrFilter.In("id", new List<string> { id }); - var filter = ClrFilter.In("id", new List<string> { id }); + AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); + } - AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); - } + [Fact] + public void Should_make_query_with_lastModified() + { + var time = "1988-01-19T12:00:00Z"; - [Fact] - public void Should_make_query_with_lastModified() - { - var time = "1988-01-19T12:00:00Z"; + var filter = ClrFilter.Eq("lastModified", InstantPattern.ExtendedIso.Parse(time).Value); - var filter = ClrFilter.Eq("lastModified", InstantPattern.ExtendedIso.Parse(time).Value); + AssertQuery("{ 'mt' : ISODate('[value]') }", filter, time); + } - AssertQuery("{ 'mt' : ISODate('[value]') }", filter, time); - } + [Fact] + public void Should_make_query_with_lastModifiedBy() + { + var filter = ClrFilter.Eq("lastModifiedBy", "subject:me"); - [Fact] - public void Should_make_query_with_lastModifiedBy() - { - var filter = ClrFilter.Eq("lastModifiedBy", "subject:me"); + AssertQuery("{ 'mb' : 'subject:me' }", filter); + } - AssertQuery("{ 'mb' : 'subject:me' }", filter); - } + [Fact] + public void Should_make_query_with_created() + { + var time = "1988-01-19T12:00:00Z"; - [Fact] - public void Should_make_query_with_created() - { - var time = "1988-01-19T12:00:00Z"; + var filter = ClrFilter.Eq("created", InstantPattern.ExtendedIso.Parse(time).Value); - var filter = ClrFilter.Eq("created", InstantPattern.ExtendedIso.Parse(time).Value); + AssertQuery("{ 'ct' : ISODate('[value]') }", filter, time); + } - AssertQuery("{ 'ct' : ISODate('[value]') }", filter, time); - } + [Fact] + public void Should_make_query_with_createdBy() + { + var filter = ClrFilter.Eq("createdBy", "subject:me"); - [Fact] - public void Should_make_query_with_createdBy() - { - var filter = ClrFilter.Eq("createdBy", "subject:me"); + AssertQuery("{ 'cb' : 'subject:me' }", filter); + } - AssertQuery("{ 'cb' : 'subject:me' }", filter); - } + [Fact] + public void Should_make_query_with_version() + { + var filter = ClrFilter.Eq("version", 2L); - [Fact] - public void Should_make_query_with_version() - { - var filter = ClrFilter.Eq("version", 2L); + AssertQuery("{ 'vs' : NumberLong(2) }", filter); + } - AssertQuery("{ 'vs' : NumberLong(2) }", filter); - } + [Fact] + public void Should_make_query_with_fileVersion() + { + var filter = ClrFilter.Eq("fileVersion", 2L); - [Fact] - public void Should_make_query_with_fileVersion() - { - var filter = ClrFilter.Eq("fileVersion", 2L); + AssertQuery("{ 'fv' : NumberLong(2) }", filter); + } - AssertQuery("{ 'fv' : NumberLong(2) }", filter); - } + [Fact] + public void Should_make_query_with_tags() + { + var filter = ClrFilter.Eq("tags", "tag1"); - [Fact] - public void Should_make_query_with_tags() - { - var filter = ClrFilter.Eq("tags", "tag1"); + AssertQuery("{ 'td' : 'tag1' }", filter); + } - AssertQuery("{ 'td' : 'tag1' }", filter); - } + [Fact] + public void Should_make_query_with_fileName() + { + var filter = ClrFilter.Eq("fileName", "Logo.png"); - [Fact] - public void Should_make_query_with_fileName() - { - var filter = ClrFilter.Eq("fileName", "Logo.png"); + AssertQuery("{ 'fn' : 'Logo.png' }", filter); + } - AssertQuery("{ 'fn' : 'Logo.png' }", filter); - } + [Fact] + public void Should_make_query_with_mimeType() + { + var filter = ClrFilter.Eq("mimeType", "text/json"); + + AssertQuery("{ 'mm' : 'text/json' }", filter); + } + + [Fact] + public void Should_make_query_with_fileSize() + { + var filter = ClrFilter.Eq("fileSize", 1024); + + AssertQuery("{ 'fs' : NumberLong(1024) }", filter); + } + + [Fact] + public void Should_make_query_with_pixelHeight() + { + var filter = ClrFilter.Eq("metadata.pixelHeight", 600); + + AssertQuery("{ 'md.pixelHeight' : 600 }", filter); + } + + [Fact] + public void Should_make_query_with_pixelWidth() + { + var filter = ClrFilter.Eq("metadata.pixelWidth", 800); + + AssertQuery("{ 'md.pixelWidth' : 800 }", filter); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var sorting = SortBuilder.Descending("created"); - [Fact] - public void Should_make_query_with_mimeType() - { - var filter = ClrFilter.Eq("mimeType", "text/json"); + AssertSorting("{ 'ct' : -1 }", sorting); + } - AssertQuery("{ 'mm' : 'text/json' }", filter); - } + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var sorting1 = SortBuilder.Ascending("created"); + var sorting2 = SortBuilder.Descending("createdBy"); + + AssertSorting("{ 'ct' : 1, 'cb' : -1 }", sorting1, sorting2); + } - [Fact] - public void Should_make_query_with_fileSize() - { - var filter = ClrFilter.Eq("fileSize", 1024); + private void AssertQuery(string expected, FilterNode<ClrValue> filter, object? arg = null) + { + AssertQuery(expected, new ClrQuery { Filter = filter }, arg); + } - AssertQuery("{ 'fs' : NumberLong(1024) }", filter); - } + private void AssertQuery(string expected, ClrQuery query, object? arg = null) + { + var filter = query.AdjustToModel(appId).BuildFilter<MongoAssetEntity>(false).Filter!; - [Fact] - public void Should_make_query_with_pixelHeight() - { - var filter = ClrFilter.Eq("metadata.pixelHeight", 600); + var rendered = + filter.Render( + BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>(), + BsonSerializer.SerializerRegistry) + .ToString(); - AssertQuery("{ 'md.pixelHeight' : 600 }", filter); - } + Assert.Equal(Cleanup(expected, arg), rendered); + } - [Fact] - public void Should_make_query_with_pixelWidth() - { - var filter = ClrFilter.Eq("metadata.pixelWidth", 800); + private void AssertSorting(string expected, params SortNode[] sort) + { + var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); - AssertQuery("{ 'md.pixelWidth' : 800 }", filter); - } + var rendered = string.Empty; - [Fact] - public void Should_make_orderby_with_single_field() - { - var sorting = SortBuilder.Descending("created"); + A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>._)) + .Invokes((SortDefinition<MongoAssetEntity> sortDefinition) => + { + rendered = + sortDefinition.Render( + BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>(), + BsonSerializer.SerializerRegistry) + .ToString(); + }); - AssertSorting("{ 'ct' : -1 }", sorting); - } - - [Fact] - public void Should_make_orderby_with_multiple_fields() - { - var sorting1 = SortBuilder.Ascending("created"); - var sorting2 = SortBuilder.Descending("createdBy"); + cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }.AdjustToModel(appId)); - AssertSorting("{ 'ct' : 1, 'cb' : -1 }", sorting1, sorting2); - } - - private void AssertQuery(string expected, FilterNode<ClrValue> filter, object? arg = null) - { - AssertQuery(expected, new ClrQuery { Filter = filter }, arg); - } + Assert.Equal(Cleanup(expected), rendered); + } - private void AssertQuery(string expected, ClrQuery query, object? arg = null) - { - var filter = query.AdjustToModel(appId).BuildFilter<MongoAssetEntity>(false).Filter!; - - var rendered = - filter.Render( - BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>(), - BsonSerializer.SerializerRegistry) - .ToString(); - - Assert.Equal(Cleanup(expected, arg), rendered); - } - - private void AssertSorting(string expected, params SortNode[] sort) - { - var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); - - var rendered = string.Empty; - - A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>._)) - .Invokes((SortDefinition<MongoAssetEntity> sortDefinition) => - { - rendered = - sortDefinition.Render( - BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>(), - BsonSerializer.SerializerRegistry) - .ToString(); - }); - - cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }.AdjustToModel(appId)); - - Assert.Equal(Cleanup(expected), rendered); - } - - private static string Cleanup(string filter, object? arg = null) - { - return filter.Replace('\'', '"').Replace("[value]", arg?.ToString(), StringComparison.Ordinal); - } + private static string Cleanup(string filter, object? arg = null) + { + return filter.Replace('\'', '"').Replace("[value]", arg?.ToString(), StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs index 8478b4eedc..1b40bafd76 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs @@ -20,134 +20,133 @@ using Squidex.Infrastructure.States; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.MongoDb +namespace Squidex.Domain.Apps.Entities.Assets.MongoDb; + +public sealed class AssetsQueryFixture : IAsyncLifetime { - public sealed class AssetsQueryFixture : IAsyncLifetime + private readonly int numValues = 250; + private readonly IMongoClient mongoClient; + private readonly IMongoDatabase mongoDatabase; + + public MongoAssetRepository AssetRepository { get; } + + public NamedId<DomainId>[] AppIds { get; } = { - private readonly int numValues = 250; - private readonly IMongoClient mongoClient; - private readonly IMongoDatabase mongoDatabase; + NamedId.Of(DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d452"), "my-app1"), + NamedId.Of(DomainId.Create("4b3672c1-97c6-4e0b-a067-71e9e9a29db9"), "my-app1") + }; - public MongoAssetRepository AssetRepository { get; } + public AssetsQueryFixture() + { + BsonJsonConvention.Register(TestUtils.DefaultOptions()); - public NamedId<DomainId>[] AppIds { get; } = - { - NamedId.Of(DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d452"), "my-app1"), - NamedId.Of(DomainId.Create("4b3672c1-97c6-4e0b-a067-71e9e9a29db9"), "my-app1") - }; + mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); + mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); - public AssetsQueryFixture() - { - BsonJsonConvention.Register(TestUtils.DefaultOptions()); + AssetRepository = new MongoAssetRepository(mongoDatabase); + } - mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); - mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); + public Task DisposeAsync() + { + return Task.CompletedTask; + } - AssetRepository = new MongoAssetRepository(mongoDatabase); - } + public async Task InitializeAsync() + { + await AssetRepository.InitializeAsync(default); - public Task DisposeAsync() - { - return Task.CompletedTask; - } + await CreateDataAsync(default); + await ClearProfileAsync(default); + } - public async Task InitializeAsync() + private async Task CreateDataAsync( + CancellationToken ct) + { + if (await AssetRepository.StreamAll(AppIds[0].Id, ct).AnyAsync(ct)) { - await AssetRepository.InitializeAsync(default); - - await CreateDataAsync(default); - await ClearProfileAsync(default); + return; } - private async Task CreateDataAsync( - CancellationToken ct) + var batch = new List<SnapshotWriteJob<AssetDomainObject.State>>(); + + async Task ExecuteBatchAsync(AssetDomainObject.State? entity) { - if (await AssetRepository.StreamAll(AppIds[0].Id, ct).AnyAsync(ct)) + if (entity != null) { - return; + batch.Add(new SnapshotWriteJob<AssetDomainObject.State>(entity.UniqueId, entity, 0)); } - var batch = new List<SnapshotWriteJob<AssetDomainObject.State>>(); - - async Task ExecuteBatchAsync(AssetDomainObject.State? entity) + if ((entity == null || batch.Count >= 1000) && batch.Count > 0) { - if (entity != null) - { - batch.Add(new SnapshotWriteJob<AssetDomainObject.State>(entity.UniqueId, entity, 0)); - } - - if ((entity == null || batch.Count >= 1000) && batch.Count > 0) - { - var store = (ISnapshotStore<AssetDomainObject.State>)AssetRepository; + var store = (ISnapshotStore<AssetDomainObject.State>)AssetRepository; - await store.WriteManyAsync(batch, ct); + await store.WriteManyAsync(batch, ct); - batch.Clear(); - } + batch.Clear(); } + } - var now = SystemClock.Instance.GetCurrentInstant(); + var now = SystemClock.Instance.GetCurrentInstant(); - var user = RefToken.User("1"); + var user = RefToken.User("1"); - foreach (var appId in AppIds) + foreach (var appId in AppIds) + { + for (var i = 0; i < numValues; i++) { - for (var i = 0; i < numValues; i++) + var fileName = i.ToString(CultureInfo.InvariantCulture); + + for (var j = 0; j < numValues; j++) { - var fileName = i.ToString(CultureInfo.InvariantCulture); + var tag = j.ToString(CultureInfo.InvariantCulture); - for (var j = 0; j < numValues; j++) + var asset = new AssetDomainObject.State { - var tag = j.ToString(CultureInfo.InvariantCulture); - - var asset = new AssetDomainObject.State + Id = DomainId.NewGuid(), + AppId = appId, + Created = now, + CreatedBy = user, + FileHash = fileName, + FileName = fileName, + FileSize = 1024, + LastModified = now, + LastModifiedBy = user, + IsDeleted = false, + IsProtected = false, + Metadata = new AssetMetadata + { + ["value"] = JsonValue.Create(tag) + }, + Tags = new HashSet<string> { - Id = DomainId.NewGuid(), - AppId = appId, - Created = now, - CreatedBy = user, - FileHash = fileName, - FileName = fileName, - FileSize = 1024, - LastModified = now, - LastModifiedBy = user, - IsDeleted = false, - IsProtected = false, - Metadata = new AssetMetadata - { - ["value"] = JsonValue.Create(tag) - }, - Tags = new HashSet<string> - { - tag - }, - Slug = fileName - }; - - await ExecuteBatchAsync(asset); - } + tag + }, + Slug = fileName + }; + + await ExecuteBatchAsync(asset); } } - - await ExecuteBatchAsync(null); } - private async Task ClearProfileAsync( - CancellationToken ct) - { - await mongoDatabase.RunCommandAsync<BsonDocument>("{ profile : 0 }", cancellationToken: ct); - await mongoDatabase.DropCollectionAsync("system.profile", ct); - await mongoDatabase.RunCommandAsync<BsonDocument>("{ profile : 2 }", cancellationToken: ct); - } + await ExecuteBatchAsync(null); + } - public DomainId RandomAppId() - { - return AppIds[Random.Shared.Next(AppIds.Length)].Id; - } + private async Task ClearProfileAsync( + CancellationToken ct) + { + await mongoDatabase.RunCommandAsync<BsonDocument>("{ profile : 0 }", cancellationToken: ct); + await mongoDatabase.DropCollectionAsync("system.profile", ct); + await mongoDatabase.RunCommandAsync<BsonDocument>("{ profile : 2 }", cancellationToken: ct); + } - public string RandomValue() - { - return Random.Shared.Next(numValues).ToString(CultureInfo.InvariantCulture); - } + public DomainId RandomAppId() + { + return AppIds[Random.Shared.Next(AppIds.Length)].Id; + } + + public string RandomValue() + { + return Random.Shared.Next(numValues).ToString(CultureInfo.InvariantCulture); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs index 4918cf158a..88a884b430 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs @@ -12,174 +12,173 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Domain.Apps.Entities.Assets.MongoDb +namespace Squidex.Domain.Apps.Entities.Assets.MongoDb; + +[Trait("Category", "Dependencies")] +public class AssetsQueryIntegrationTests : IClassFixture<AssetsQueryFixture> { - [Trait("Category", "Dependencies")] - public class AssetsQueryIntegrationTests : IClassFixture<AssetsQueryFixture> - { - public AssetsQueryFixture _ { get; } + public AssetsQueryFixture _ { get; } - public AssetsQueryIntegrationTests(AssetsQueryFixture fixture) - { - _ = fixture; - } + public AssetsQueryIntegrationTests(AssetsQueryFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_find_asset_by_slug() - { - var random = _.RandomValue(); + [Fact] + public async Task Should_find_asset_by_slug() + { + var random = _.RandomValue(); - var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), random); + var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), random); - // The Slug is random here, as it does not really matter. - Assert.NotNull(asset); - } + // The Slug is random here, as it does not really matter. + Assert.NotNull(asset); + } - [Fact] - public async Task Should_query_asset_by_hash() - { - var random = _.RandomValue(); + [Fact] + public async Task Should_query_asset_by_hash() + { + var random = _.RandomValue(); - var assets = await _.AssetRepository.FindAssetByHashAsync(_.RandomAppId(), random, random, 1024); + var assets = await _.AssetRepository.FindAssetByHashAsync(_.RandomAppId(), random, random, 1024); - // The Hash is random here, as it does not really matter. - Assert.NotNull(assets); - } + // The Hash is random here, as it does not really matter. + Assert.NotNull(assets); + } - [Fact] - public async Task Should_verify_ids() - { - var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); + [Fact] + public async Task Should_verify_ids() + { + var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - var assets = await _.AssetRepository.QueryIdsAsync(_.RandomAppId(), ids); + var assets = await _.AssetRepository.QueryIdsAsync(_.RandomAppId(), ids); - // The IDs are random here, as it does not really matter. - Assert.NotNull(assets); - } + // The IDs are random here, as it does not really matter. + Assert.NotNull(assets); + } - [Theory] - [MemberData(nameof(ParentIds))] - public async Task Should_query_assets(DomainId? parentId) - { - var query = new ClrQuery(); + [Theory] + [MemberData(nameof(ParentIds))] + public async Task Should_query_assets(DomainId? parentId) + { + var query = new ClrQuery(); - var assets = await QueryAsync(parentId, query); + var assets = await QueryAsync(parentId, query); - // Default page size is 1000. - Assert.Equal(1000, assets.Count); - } + // Default page size is 1000. + Assert.Equal(1000, assets.Count); + } - [Theory] - [MemberData(nameof(ParentIds))] - public async Task Should_query_assets_with_random_count(DomainId? parentId) + [Theory] + [MemberData(nameof(ParentIds))] + public async Task Should_query_assets_with_random_count(DomainId? parentId) + { + var query = new ClrQuery { - var query = new ClrQuery - { - Random = 40 - }; + Random = 40 + }; - var assets = await QueryAsync(parentId, query); + var assets = await QueryAsync(parentId, query); - // Default page size is 1000, so we expect less elements. - Assert.Equal(40, assets.Count); - } + // Default page size is 1000, so we expect less elements. + Assert.Equal(40, assets.Count); + } + + [Theory] + [MemberData(nameof(ParentIds))] + public async Task Should_query_assets_by_tags(DomainId? parentId) + { + var random = _.RandomValue(); - [Theory] - [MemberData(nameof(ParentIds))] - public async Task Should_query_assets_by_tags(DomainId? parentId) + var query = new ClrQuery { - var random = _.RandomValue(); + Filter = F.Eq("Tags", random) + }; - var query = new ClrQuery - { - Filter = F.Eq("Tags", random) - }; + var assets = await QueryAsync(parentId, query); - var assets = await QueryAsync(parentId, query); + // The tag is random here, as it does not really matter. + Assert.NotNull(assets); + } - // The tag is random here, as it does not really matter. - Assert.NotNull(assets); - } + [Theory] + [MemberData(nameof(ParentIds))] + public async Task Should_query_assets_by_tags_and_fileName(DomainId? parentId) + { + var random = _.RandomValue(); - [Theory] - [MemberData(nameof(ParentIds))] - public async Task Should_query_assets_by_tags_and_fileName(DomainId? parentId) + var query = new ClrQuery { - var random = _.RandomValue(); + Filter = F.And(F.Eq("Tags", random), F.Contains("FileName", random)) + }; - var query = new ClrQuery - { - Filter = F.And(F.Eq("Tags", random), F.Contains("FileName", random)) - }; + var assets = await QueryAsync(parentId, query); - var assets = await QueryAsync(parentId, query); + // The filter is a random value from the expected result set. + Assert.NotEmpty(assets); + } - // The filter is a random value from the expected result set. - Assert.NotEmpty(assets); - } + [Theory] + [MemberData(nameof(ParentIds))] + public async Task Should_query_assets_by_fileName(DomainId? parentId) + { + var random = _.RandomValue(); - [Theory] - [MemberData(nameof(ParentIds))] - public async Task Should_query_assets_by_fileName(DomainId? parentId) + var query = new ClrQuery { - var random = _.RandomValue(); + Filter = F.Contains("FileName", random) + }; - var query = new ClrQuery - { - Filter = F.Contains("FileName", random) - }; + var assets = await QueryAsync(parentId, query); - var assets = await QueryAsync(parentId, query); + // The filter is a random value from the expected result set. + Assert.NotEmpty(assets); + } - // The filter is a random value from the expected result set. - Assert.NotEmpty(assets); - } + [Theory] + [MemberData(nameof(ParentIds))] + public async Task Should_query_assets_by_fileName_and_tags(DomainId? parentId) + { + var random = _.RandomValue(); - [Theory] - [MemberData(nameof(ParentIds))] - public async Task Should_query_assets_by_fileName_and_tags(DomainId? parentId) + var query = new ClrQuery { - var random = _.RandomValue(); + Filter = F.And(F.Contains("FileName", random), F.Eq("Tags", random)) + }; - var query = new ClrQuery - { - Filter = F.And(F.Contains("FileName", random), F.Eq("Tags", random)) - }; + var assets = await QueryAsync(parentId, query); - var assets = await QueryAsync(parentId, query); + // The filter is a random value from the expected result set. + Assert.NotEmpty(assets); + } - // The filter is a random value from the expected result set. - Assert.NotEmpty(assets); - } + public static IEnumerable<object?[]> ParentIds() + { + yield return new object?[] { null }; + yield return new object?[] { DomainId.Empty }; + } - public static IEnumerable<object?[]> ParentIds() - { - yield return new object?[] { null }; - yield return new object?[] { DomainId.Empty }; - } + private async Task<IResultList<IAssetEntity>> QueryAsync(DomainId? parentId, ClrQuery clrQuery) + { + clrQuery.Top = 1000; + clrQuery.Skip = 100; - private async Task<IResultList<IAssetEntity>> QueryAsync(DomainId? parentId, ClrQuery clrQuery) + if (clrQuery.Sort == null || clrQuery.Sort.Count == 0) { - clrQuery.Top = 1000; - clrQuery.Skip = 100; - - if (clrQuery.Sort == null || clrQuery.Sort.Count == 0) + clrQuery.Sort = new List<SortNode> { - clrQuery.Sort = new List<SortNode> - { - new SortNode("LastModified", SortOrder.Descending), - new SortNode("Id", SortOrder.Descending) - }; - } + new SortNode("LastModified", SortOrder.Descending), + new SortNode("Id", SortOrder.Descending) + }; + } - var q = - Q.Empty - .WithoutTotal() - .WithQuery(clrQuery); + var q = + Q.Empty + .WithoutTotal() + .WithQuery(clrQuery); - var assets = await _.AssetRepository.QueryAsync(_.RandomAppId(), parentId, q); + var assets = await _.AssetRepository.QueryAsync(_.RandomAppId(), parentId, q); - return assets; - } + return assets; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs index 5939ec8967..4bcb123dad 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs @@ -15,193 +15,192 @@ using Squidex.Infrastructure.Json; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +public class AssetEnricherTests { - public class AssetEnricherTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly ITagService tagService = A.Fake<ITagService>(); + private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly IAssetMetadataSource assetMetadataSource1 = A.Fake<IAssetMetadataSource>(); + private readonly IAssetMetadataSource assetMetadataSource2 = A.Fake<IAssetMetadataSource>(); + private readonly IJsonSerializer serializer = A.Fake<IJsonSerializer>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly AssetEnricher sut; + + public AssetEnricherTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly ITagService tagService = A.Fake<ITagService>(); - private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly IAssetMetadataSource assetMetadataSource1 = A.Fake<IAssetMetadataSource>(); - private readonly IAssetMetadataSource assetMetadataSource2 = A.Fake<IAssetMetadataSource>(); - private readonly IJsonSerializer serializer = A.Fake<IJsonSerializer>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext; - private readonly AssetEnricher sut; - - public AssetEnricherTests() - { - ct = cts.Token; + ct = cts.Token; - var assetMetadataSources = new[] - { - assetMetadataSource1, - assetMetadataSource2 - }; + var assetMetadataSources = new[] + { + assetMetadataSource1, + assetMetadataSource2 + }; - requestContext = Context.Anonymous(Mocks.App(appId)); + requestContext = Context.Anonymous(Mocks.App(appId)); - sut = new AssetEnricher(tagService, assetMetadataSources, requestCache, urlGenerator, serializer); - } + sut = new AssetEnricher(tagService, assetMetadataSources, requestCache, urlGenerator, serializer); + } - [Fact] - public async Task Should_not_enrich_if_asset_contains_null_tags() - { - var source = new AssetEntity { AppId = appId }; + [Fact] + public async Task Should_not_enrich_if_asset_contains_null_tags() + { + var source = new AssetEntity { AppId = appId }; - var actual = await sut.EnrichAsync(source, requestContext, ct); + var actual = await sut.EnrichAsync(source, requestContext, ct); - Assert.Empty(actual.TagNames); - } + Assert.Empty(actual.TagNames); + } - [Fact] - public async Task Should_enrich_with_cache_dependencies() - { - var source = new AssetEntity { AppId = appId, Id = DomainId.NewGuid(), Version = 13 }; + [Fact] + public async Task Should_enrich_with_cache_dependencies() + { + var source = new AssetEntity { AppId = appId, Id = DomainId.NewGuid(), Version = 13 }; - var actual = await sut.EnrichAsync(source, requestContext, ct); + var actual = await sut.EnrichAsync(source, requestContext, ct); - A.CallTo(() => requestCache.AddDependency(actual.UniqueId, actual.Version)) - .MustHaveHappened(); - } + A.CallTo(() => requestCache.AddDependency(actual.UniqueId, actual.Version)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_enrich_asset_with_tag_names() + [Fact] + public async Task Should_enrich_asset_with_tag_names() + { + var source = new AssetEntity { - var source = new AssetEntity + Tags = new HashSet<string> + { + "id1", + "id2" + }, + AppId = appId + }; + + A.CallTo(() => tagService.GetTagNamesAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Is("id1", "id2"), ct)) + .Returns(new Dictionary<string, string> { - Tags = new HashSet<string> - { - "id1", - "id2" - }, - AppId = appId - }; - - A.CallTo(() => tagService.GetTagNamesAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Is("id1", "id2"), ct)) - .Returns(new Dictionary<string, string> - { - ["id1"] = "name1", - ["id2"] = "name2" - }); - - var actual = await sut.EnrichAsync(source, requestContext, ct); - - Assert.Equal(new HashSet<string> { "name1", "name2" }, actual.TagNames); - } - - [Fact] - public async Task Should_not_enrich_asset_with_tag_names_if_disabled() + ["id1"] = "name1", + ["id2"] = "name2" + }); + + var actual = await sut.EnrichAsync(source, requestContext, ct); + + Assert.Equal(new HashSet<string> { "name1", "name2" }, actual.TagNames); + } + + [Fact] + public async Task Should_not_enrich_asset_with_tag_names_if_disabled() + { + var source = new AssetEntity { - var source = new AssetEntity + Tags = new HashSet<string> { - Tags = new HashSet<string> - { - "id1", - "id2" - }, - AppId = appId - }; + "id1", + "id2" + }, + AppId = appId + }; - var actual = await sut.EnrichAsync(source, requestContext.Clone(b => b.WithoutAssetEnrichment()), ct); + var actual = await sut.EnrichAsync(source, requestContext.Clone(b => b.WithoutAssetEnrichment()), ct); - Assert.Null(actual.TagNames); - } + Assert.Null(actual.TagNames); + } - [Fact] - public async Task Should_enrich_asset_with_metadata() + [Fact] + public async Task Should_enrich_asset_with_metadata() + { + var source = new AssetEntity { - var source = new AssetEntity + FileSize = 2 * 1024, + Tags = new HashSet<string> { - FileSize = 2 * 1024, - Tags = new HashSet<string> - { - "id1", - "id2" - }, - AppId = appId - }; + "id1", + "id2" + }, + AppId = appId + }; - A.CallTo(() => assetMetadataSource1.Format(A<IAssetEntity>._)) - .Returns(new[] { "metadata1" }); + A.CallTo(() => assetMetadataSource1.Format(A<IAssetEntity>._)) + .Returns(new[] { "metadata1" }); - A.CallTo(() => assetMetadataSource2.Format(A<IAssetEntity>._)) - .Returns(new[] { "metadata2", "metadata3" }); + A.CallTo(() => assetMetadataSource2.Format(A<IAssetEntity>._)) + .Returns(new[] { "metadata2", "metadata3" }); - var actual = await sut.EnrichAsync(source, requestContext, ct); + var actual = await sut.EnrichAsync(source, requestContext, ct); - Assert.Equal("metadata1, metadata2, metadata3, 2 kB", actual.MetadataText); - } + Assert.Equal("metadata1, metadata2, metadata3, 2 kB", actual.MetadataText); + } - [Fact] - public async Task Should_enrich_multiple_assets_with_tag_names() + [Fact] + public async Task Should_enrich_multiple_assets_with_tag_names() + { + var source1 = new AssetEntity { - var source1 = new AssetEntity + Tags = new HashSet<string> { - Tags = new HashSet<string> - { - "id1", - "id2" - }, - AppId = appId - }; - - var source2 = new AssetEntity - { - Tags = new HashSet<string> - { - "id2", - "id3" - }, - AppId = appId - }; - - A.CallTo(() => tagService.GetTagNamesAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Is("id1", "id2", "id3"), ct)) - .Returns(new Dictionary<string, string> - { - ["id1"] = "name1", - ["id2"] = "name2", - ["id3"] = "name3" - }); - - var actual = await sut.EnrichAsync(new[] { source1, source2 }, requestContext, ct); - - Assert.Equal(new HashSet<string> { "name1", "name2" }, actual[0].TagNames); - Assert.Equal(new HashSet<string> { "name2", "name3" }, actual[1].TagNames); - } - - [Fact] - public async Task Should_also_compute_ui_tokens_for_frontend() + "id1", + "id2" + }, + AppId = appId + }; + + var source2 = new AssetEntity { - var source = new AssetEntity + Tags = new HashSet<string> { - AppId = appId - }; + "id2", + "id3" + }, + AppId = appId + }; + + A.CallTo(() => tagService.GetTagNamesAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Is("id1", "id2", "id3"), ct)) + .Returns(new Dictionary<string, string> + { + ["id1"] = "name1", + ["id2"] = "name2", + ["id3"] = "name3" + }); - var actual = await sut.EnrichAsync(new[] { source }, new Context(Mocks.FrontendUser(), Mocks.App(appId)), ct); + var actual = await sut.EnrichAsync(new[] { source1, source2 }, requestContext, ct); - Assert.NotNull(actual[0].EditToken); + Assert.Equal(new HashSet<string> { "name1", "name2" }, actual[0].TagNames); + Assert.Equal(new HashSet<string> { "name2", "name3" }, actual[1].TagNames); + } - A.CallTo(() => urlGenerator.Root()) - .MustHaveHappened(); - } + [Fact] + public async Task Should_also_compute_ui_tokens_for_frontend() + { + var source = new AssetEntity + { + AppId = appId + }; + + var actual = await sut.EnrichAsync(new[] { source }, new Context(Mocks.FrontendUser(), Mocks.App(appId)), ct); + + Assert.NotNull(actual[0].EditToken); - [Fact] - public async Task Should_compute_ui_tokens() + A.CallTo(() => urlGenerator.Root()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_compute_ui_tokens() + { + var source = new AssetEntity { - var source = new AssetEntity - { - AppId = appId - }; + AppId = appId + }; - var actual = await sut.EnrichAsync(new[] { source }, requestContext, ct); + var actual = await sut.EnrichAsync(new[] { source }, requestContext, ct); - Assert.NotNull(actual[0].EditToken); + Assert.NotNull(actual[0].EditToken); - A.CallTo(() => urlGenerator.Root()) - .MustHaveHappened(); - } + A.CallTo(() => urlGenerator.Root()) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs index d5f95c037c..5e06ff46eb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs @@ -11,108 +11,107 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +public class AssetLoaderTests { - public class AssetLoaderTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly IDomainObjectCache domainObjectCache = A.Fake<IDomainObjectCache>(); + private readonly AssetDomainObject domainObject = A.Fake<AssetDomainObject>(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly DomainId id = DomainId.NewGuid(); + private readonly DomainId uniqueId; + private readonly AssetLoader sut; + + public AssetLoaderTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly IDomainObjectCache domainObjectCache = A.Fake<IDomainObjectCache>(); - private readonly AssetDomainObject domainObject = A.Fake<AssetDomainObject>(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly DomainId id = DomainId.NewGuid(); - private readonly DomainId uniqueId; - private readonly AssetLoader sut; - - public AssetLoaderTests() - { - ct = cts.Token; + ct = cts.Token; - uniqueId = DomainId.Combine(appId, id); + uniqueId = DomainId.Combine(appId, id); - A.CallTo(() => domainObjectCache.GetAsync<AssetDomainObject.State>(A<DomainId>._, A<long>._, ct)) - .Returns(Task.FromResult<AssetDomainObject.State>(null!)); + A.CallTo(() => domainObjectCache.GetAsync<AssetDomainObject.State>(A<DomainId>._, A<long>._, ct)) + .Returns(Task.FromResult<AssetDomainObject.State>(null!)); - A.CallTo(() => domainObjectFactory.Create<AssetDomainObject>(uniqueId)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<AssetDomainObject>(uniqueId)) + .Returns(domainObject); - sut = new AssetLoader(domainObjectFactory, domainObjectCache); - } + sut = new AssetLoader(domainObjectFactory, domainObjectCache); + } - [Fact] - public async Task Should_return_null_if_no_state_returned() - { - var asset = (AssetDomainObject.State)null!; + [Fact] + public async Task Should_return_null_if_no_state_returned() + { + var asset = (AssetDomainObject.State)null!; - A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) - .Returns(asset); + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(asset); - Assert.Null(await sut.GetAsync(appId, id, 10, ct)); - } + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); + } - [Fact] - public async Task Should_return_null_if_state_empty() - { - var asset = new AssetDomainObject.State { Version = EtagVersion.Empty }; + [Fact] + public async Task Should_return_null_if_state_empty() + { + var asset = new AssetDomainObject.State { Version = EtagVersion.Empty }; - A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) - .Returns(asset); + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(asset); - Assert.Null(await sut.GetAsync(appId, id, 10, ct)); - } + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); + } - [Fact] - public async Task Should_return_null_if_state_has_other_version() - { - var asset = new AssetDomainObject.State { Version = 5 }; + [Fact] + public async Task Should_return_null_if_state_has_other_version() + { + var asset = new AssetDomainObject.State { Version = 5 }; - A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) - .Returns(asset); + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(asset); - Assert.Null(await sut.GetAsync(appId, id, 10, ct)); - } + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); + } - [Fact] - public async Task Should_not_return_null_if_state_has_other_version_than_any() - { - var asset = new AssetDomainObject.State { Version = 5 }; + [Fact] + public async Task Should_not_return_null_if_state_has_other_version_than_any() + { + var asset = new AssetDomainObject.State { Version = 5 }; - A.CallTo(() => domainObject.GetSnapshotAsync(EtagVersion.Any, ct)) - .Returns(asset); + A.CallTo(() => domainObject.GetSnapshotAsync(EtagVersion.Any, ct)) + .Returns(asset); - var actual = await sut.GetAsync(appId, id, EtagVersion.Any, ct); + var actual = await sut.GetAsync(appId, id, EtagVersion.Any, ct); - Assert.Same(asset, actual); - } + Assert.Same(asset, actual); + } - [Fact] - public async Task Should_return_asset_from_state() - { - var asset = new AssetDomainObject.State { Version = 10 }; + [Fact] + public async Task Should_return_asset_from_state() + { + var asset = new AssetDomainObject.State { Version = 10 }; - A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) - .Returns(asset); + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(asset); - var actual = await sut.GetAsync(appId, id, 10, ct); + var actual = await sut.GetAsync(appId, id, 10, ct); - Assert.Same(asset, actual); - } + Assert.Same(asset, actual); + } - [Fact] - public async Task Should_return_content_from_cache() - { - var content = new AssetDomainObject.State { Version = 10 }; + [Fact] + public async Task Should_return_content_from_cache() + { + var content = new AssetDomainObject.State { Version = 10 }; - A.CallTo(() => domainObjectCache.GetAsync<AssetDomainObject.State>(DomainId.Combine(appId, id), 10, ct)) - .Returns(content); + A.CallTo(() => domainObjectCache.GetAsync<AssetDomainObject.State>(DomainId.Combine(appId, id), 10, ct)) + .Returns(content); - var actual = await sut.GetAsync(appId, id, 10, ct); + var actual = await sut.GetAsync(appId, id, 10, ct); - Assert.Same(content, actual); + Assert.Same(content, actual); - A.CallTo(() => domainObjectFactory.Create<AssetDomainObject>(uniqueId)) - .MustNotHaveHappened(); - } + A.CallTo(() => domainObjectFactory.Create<AssetDomainObject>(uniqueId)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs index 8007698043..6840d18d18 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs @@ -15,179 +15,178 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +public class AssetQueryParserTests { - public class AssetQueryParserTests - { - private readonly ITagService tagService = A.Fake<ITagService>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext; - private readonly AssetQueryParser sut; + private readonly ITagService tagService = A.Fake<ITagService>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly AssetQueryParser sut; - public AssetQueryParserTests() - { - requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + public AssetQueryParserTests() + { + requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - var options = Options.Create(new AssetOptions { DefaultPageSize = 30 }); + var options = Options.Create(new AssetOptions { DefaultPageSize = 30 }); - sut = new AssetQueryParser(TestUtils.DefaultSerializer, tagService, options); - } + sut = new AssetQueryParser(TestUtils.DefaultSerializer, tagService, options); + } - [Fact] - public async Task Should_skip_total_if_set_in_context() - { - var q = await sut.ParseAsync(requestContext.Clone(b => b.WithoutTotal()), Q.Empty); + [Fact] + public async Task Should_skip_total_if_set_in_context() + { + var q = await sut.ParseAsync(requestContext.Clone(b => b.WithoutTotal()), Q.Empty); - Assert.True(q.NoTotal); - } + Assert.True(q.NoTotal); + } - [Fact] - public async Task Should_throw_if_odata_query_is_invalid() - { - var query = Q.Empty.WithODataQuery("$filter=invalid"); + [Fact] + public async Task Should_throw_if_odata_query_is_invalid() + { + var query = Q.Empty.WithODataQuery("$filter=invalid"); - await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query)); + } - [Fact] - public async Task Should_throw_if_json_query_is_invalid() - { - var query = Q.Empty.WithJsonQuery("invalid"); + [Fact] + public async Task Should_throw_if_json_query_is_invalid() + { + var query = Q.Empty.WithJsonQuery("invalid"); - await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query)); + } - [Fact] - public async Task Should_parse_odata_query() - { - var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"); + [Fact] + public async Task Should_parse_odata_query() + { + var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending, id Ascending", q.Query.ToString()); - } + Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_parse_odata_query_and_enrich_with_defaults() - { - var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'"); + [Fact] + public async Task Should_parse_odata_query_and_enrich_with_defaults() + { + var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_parse_json_query_and_enrich_with_defaults() - { - var query = Q.Empty.WithJsonQuery("{ \"filter\": { \"path\": \"fileName\", \"op\": \"eq\", \"value\": \"ABC\" } }"); + [Fact] + public async Task Should_parse_json_query_and_enrich_with_defaults() + { + var query = Q.Empty.WithJsonQuery("{ \"filter\": { \"path\": \"fileName\", \"op\": \"eq\", \"value\": \"ABC\" } }"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_parse_json_full_text_query_and_enrich_with_defaults() - { - var query = Q.Empty.WithJsonQuery("{ \"fullText\": \"Hello\" }"); + [Fact] + public async Task Should_parse_json_full_text_query_and_enrich_with_defaults() + { + var query = Q.Empty.WithJsonQuery("{ \"fullText\": \"Hello\" }"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Theory] - [InlineData(0L)] - [InlineData(-1L)] - [InlineData(long.MaxValue)] - [InlineData(long.MinValue)] - public async Task Should_apply_default_take_size_if_not_defined(long take) - { - var query = Q.Empty.WithQuery(new ClrQuery { Take = take }); + [Theory] + [InlineData(0L)] + [InlineData(-1L)] + [InlineData(long.MaxValue)] + [InlineData(long.MinValue)] + public async Task Should_apply_default_take_size_if_not_defined(long take) + { + var query = Q.Empty.WithQuery(new ClrQuery { Take = take }); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_set_take_to_ids_count_if_take_not_defined() - { - var query = Q.Empty.WithIds("1, 2, 3"); + [Fact] + public async Task Should_set_take_to_ids_count_if_take_not_defined() + { + var query = Q.Empty.WithIds("1, 2, 3"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Take: 3; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Take: 3; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_not_set_take_to_ids_count_if_take_defined() - { - var query = Q.Empty.WithIds("1, 2, 3").WithQuery(new ClrQuery { Take = 20 }); + [Fact] + public async Task Should_not_set_take_to_ids_count_if_take_defined() + { + var query = Q.Empty.WithIds("1, 2, 3").WithQuery(new ClrQuery { Take = 20 }); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Take: 20; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Take: 20; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_apply_default_take_limit() - { - var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); + [Fact] + public async Task Should_apply_default_take_limit() + { + var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_not_apply_id_ordering_twice() - { - var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); + [Fact] + public async Task Should_not_apply_id_ordering_twice() + { + var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString()); - } + Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString()); + } - [Fact] - public async Task Should_denormalize_tags() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"), default)) - .Returns(new Dictionary<string, string> { ["name1"] = "id1" }); + [Fact] + public async Task Should_denormalize_tags() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"), default)) + .Returns(new Dictionary<string, string> { ["name1"] = "id1" }); - var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); + var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_not_fail_if_tags_not_found() - { - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"), default)) - .Returns(new Dictionary<string, string>()); + [Fact] + public async Task Should_not_fail_if_tags_not_found() + { + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"), default)) + .Returns(new Dictionary<string, string>()); - var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); + var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Filter: tags == 'name1'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: tags == 'name1'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_not_normalize_other_field() - { - var query = Q.Empty.WithODataQuery("$filter=fileSize eq 123"); + [Fact] + public async Task Should_not_normalize_other_field() + { + var query = Q.Empty.WithODataQuery("$filter=fileSize eq 123"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Filter: fileSize == 123; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + Assert.Equal("Filter: fileSize == 123; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A<string>._, A<HashSet<string>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A<string>._, A<HashSet<string>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index da4902958c..15e7f9cc40 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs @@ -13,347 +13,346 @@ using Squidex.Infrastructure.Reflection; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets.Queries +namespace Squidex.Domain.Apps.Entities.Assets.Queries; + +public class AssetQueryServiceTests { - public class AssetQueryServiceTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>(); + private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); + private readonly IAssetLoader assetLoader = A.Fake<IAssetLoader>(); + private readonly IAssetFolderRepository assetFolderRepository = A.Fake<IAssetFolderRepository>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly AssetQueryParser queryParser = A.Fake<AssetQueryParser>(); + private readonly AssetQueryService sut; + + public AssetQueryServiceTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>(); - private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); - private readonly IAssetLoader assetLoader = A.Fake<IAssetLoader>(); - private readonly IAssetFolderRepository assetFolderRepository = A.Fake<IAssetFolderRepository>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext; - private readonly AssetQueryParser queryParser = A.Fake<AssetQueryParser>(); - private readonly AssetQueryService sut; - - public AssetQueryServiceTests() - { - ct = cts.Token; + ct = cts.Token; - requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - SetupEnricher(); + SetupEnricher(); - A.CallTo(() => queryParser.ParseAsync(requestContext, A<Q>._, ct)) - .ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!)); + A.CallTo(() => queryParser.ParseAsync(requestContext, A<Q>._, ct)) + .ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!)); - var options = Options.Create(new AssetOptions()); + var options = Options.Create(new AssetOptions()); - sut = new AssetQueryService( - assetEnricher, - assetRepository, - assetLoader, - assetFolderRepository, - options, - queryParser); - } + sut = new AssetQueryService( + assetEnricher, + assetRepository, + assetLoader, + assetFolderRepository, + options, + queryParser); + } - [Fact] - public async Task Should_find_asset_by_slug_and_enrich_it() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_find_asset_by_slug_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug", A<CancellationToken>._)) - .Returns(asset); + A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug", A<CancellationToken>._)) + .Returns(asset); - var actual = await sut.FindBySlugAsync(requestContext, "slug", ct); + var actual = await sut.FindBySlugAsync(requestContext, "slug", ct); - AssertAsset(asset, actual); - } + AssertAsset(asset, actual); + } - [Fact] - public async Task Should_return_null_if_asset_by_slug_cannot_be_found() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_return_null_if_asset_by_slug_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug", A<CancellationToken>._)) - .Returns(Task.FromResult<IAssetEntity?>(null)); + A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug", A<CancellationToken>._)) + .Returns(Task.FromResult<IAssetEntity?>(null)); - var actual = await sut.FindBySlugAsync(requestContext, "slug", ct); + var actual = await sut.FindBySlugAsync(requestContext, "slug", ct); - Assert.Null(actual); - } + Assert.Null(actual); + } - [Fact] - public async Task Should_find_asset_by_id_and_enrich_it() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_find_asset_by_id_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id, A<CancellationToken>._)) - .Returns(asset); + A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id, A<CancellationToken>._)) + .Returns(asset); - var actual = await sut.FindAsync(requestContext, asset.Id, ct: ct); + var actual = await sut.FindAsync(requestContext, asset.Id, ct: ct); - AssertAsset(asset, actual); - } + AssertAsset(asset, actual); + } - [Fact] - public async Task Should_return_null_if_asset_by_id_cannot_be_found() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_return_null_if_asset_by_id_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id, A<CancellationToken>._)) - .Returns(Task.FromResult<IAssetEntity?>(null)); + A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id, A<CancellationToken>._)) + .Returns(Task.FromResult<IAssetEntity?>(null)); - var actual = await sut.FindAsync(requestContext, asset.Id, ct: ct); + var actual = await sut.FindAsync(requestContext, asset.Id, ct: ct); - Assert.Null(actual); - } + Assert.Null(actual); + } - [Fact] - public async Task Should_find_asset_by_id_and_version_and_enrich_it() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_find_asset_by_id_and_version_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2, A<CancellationToken>._)) - .Returns(asset); + A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2, A<CancellationToken>._)) + .Returns(asset); - var actual = await sut.FindAsync(requestContext, asset.Id, 2, ct); + var actual = await sut.FindAsync(requestContext, asset.Id, 2, ct); - AssertAsset(asset, actual); - } + AssertAsset(asset, actual); + } - [Fact] - public async Task Should_return_null_if_asset_by_id_and_version_cannot_be_found() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_return_null_if_asset_by_id_and_version_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2, A<CancellationToken>._)) - .Returns(Task.FromResult<IAssetEntity?>(null)); + A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2, A<CancellationToken>._)) + .Returns(Task.FromResult<IAssetEntity?>(null)); - var actual = await sut.FindAsync(requestContext, asset.Id, 2, ct); + var actual = await sut.FindAsync(requestContext, asset.Id, 2, ct); - Assert.Null(actual); - } + Assert.Null(actual); + } - [Fact] - public async Task Should_find_global_asset_by_id_and_enrich_it() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_find_global_asset_by_id_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetRepository.FindAssetAsync(asset.Id, A<CancellationToken>._)) - .Returns(asset); + A.CallTo(() => assetRepository.FindAssetAsync(asset.Id, A<CancellationToken>._)) + .Returns(asset); - var actual = await sut.FindGlobalAsync(requestContext, asset.Id, ct); + var actual = await sut.FindGlobalAsync(requestContext, asset.Id, ct); - AssertAsset(asset, actual); - } + AssertAsset(asset, actual); + } - [Fact] - public async Task Should_return_null_if_global_asset_by_id_cannot_be_found() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_return_null_if_global_asset_by_id_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetRepository.FindAssetAsync(asset.Id, A<CancellationToken>._)) - .Returns(Task.FromResult<IAssetEntity?>(null)); + A.CallTo(() => assetRepository.FindAssetAsync(asset.Id, A<CancellationToken>._)) + .Returns(Task.FromResult<IAssetEntity?>(null)); - var actual = await sut.FindGlobalAsync(requestContext, asset.Id, ct); + var actual = await sut.FindGlobalAsync(requestContext, asset.Id, ct); - Assert.Null(actual); - } + Assert.Null(actual); + } - [Fact] - public async Task Should_find_assets_by_hash_and_and_enrich_it() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_find_assets_by_hash_and_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123, A<CancellationToken>._)) - .Returns(asset); + A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123, A<CancellationToken>._)) + .Returns(asset); - var actual = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); + var actual = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); - AssertAsset(asset, actual); - } + AssertAsset(asset, actual); + } - [Fact] - public async Task Should_return_null_if_asset_by_hash_cannot_be_found() - { - var asset = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_return_null_if_asset_by_hash_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123, A<CancellationToken>._)) - .Returns(Task.FromResult<IAssetEntity?>(null)); + A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123, A<CancellationToken>._)) + .Returns(Task.FromResult<IAssetEntity?>(null)); - var actual = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); + var actual = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); - Assert.Null(actual); - } + Assert.Null(actual); + } - [Fact] - public async Task Should_query_assets_and_enrich_it() - { - var asset1 = CreateAsset(DomainId.NewGuid()); - var asset2 = CreateAsset(DomainId.NewGuid()); + [Fact] + public async Task Should_query_assets_and_enrich_it() + { + var asset1 = CreateAsset(DomainId.NewGuid()); + var asset2 = CreateAsset(DomainId.NewGuid()); - var parentId = DomainId.NewGuid(); + var parentId = DomainId.NewGuid(); - var q = Q.Empty.WithODataQuery("fileName eq 'Name'"); + var q = Q.Empty.WithODataQuery("fileName eq 'Name'"); - A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, q, A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(8, asset1, asset2)); + A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, q, A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(8, asset1, asset2)); - var actual = await sut.QueryAsync(requestContext, parentId, q, ct); + var actual = await sut.QueryAsync(requestContext, parentId, q, ct); - Assert.Equal(8, actual.Total); + Assert.Equal(8, actual.Total); - AssertAsset(asset1, actual[0]); - AssertAsset(asset2, actual[1]); - } + AssertAsset(asset1, actual[0]); + AssertAsset(asset2, actual[1]); + } - [Fact] - public async Task Should_query_asset_folders() - { - var parentId = DomainId.NewGuid(); + [Fact] + public async Task Should_query_asset_folders() + { + var parentId = DomainId.NewGuid(); - var assetFolders = ResultList.CreateFrom<IAssetFolderEntity>(10); + var assetFolders = ResultList.CreateFrom<IAssetFolderEntity>(10); - A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId, A<CancellationToken>._)) - .Returns(assetFolders); + A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId, A<CancellationToken>._)) + .Returns(assetFolders); - var actual = await sut.QueryAssetFoldersAsync(requestContext, parentId, ct); + var actual = await sut.QueryAssetFoldersAsync(requestContext, parentId, ct); - Assert.Same(assetFolders, actual); - } + Assert.Same(assetFolders, actual); + } - [Fact] - public async Task Should_query_asset_folders_with_appId() - { - var parentId = DomainId.NewGuid(); + [Fact] + public async Task Should_query_asset_folders_with_appId() + { + var parentId = DomainId.NewGuid(); - var assetFolders = ResultList.CreateFrom<IAssetFolderEntity>(10); + var assetFolders = ResultList.CreateFrom<IAssetFolderEntity>(10); - A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId, A<CancellationToken>._)) - .Returns(assetFolders); + A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId, A<CancellationToken>._)) + .Returns(assetFolders); - var actual = await sut.QueryAssetFoldersAsync(appId.Id, parentId, ct); + var actual = await sut.QueryAssetFoldersAsync(appId.Id, parentId, ct); - Assert.Same(assetFolders, actual); - } + Assert.Same(assetFolders, actual); + } - [Fact] - public async Task Should_find_asset_folder_with_path() - { - var folderId1 = DomainId.NewGuid(); - var folder1 = CreateFolder(folderId1); + [Fact] + public async Task Should_find_asset_folder_with_path() + { + var folderId1 = DomainId.NewGuid(); + var folder1 = CreateFolder(folderId1); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) - .Returns(folder1); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) + .Returns(folder1); - var actual = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); - Assert.Equal(actual, new[] { folder1 }); - } + Assert.Equal(actual, new[] { folder1 }); + } - [Fact] - public async Task Should_resolve_folder_path_from_child() - { - var folderId1 = DomainId.NewGuid(); - var folderId2 = DomainId.NewGuid(); - var folderId3 = DomainId.NewGuid(); + [Fact] + public async Task Should_resolve_folder_path_from_child() + { + var folderId1 = DomainId.NewGuid(); + var folderId2 = DomainId.NewGuid(); + var folderId3 = DomainId.NewGuid(); - var folder1 = CreateFolder(folderId1); - var folder2 = CreateFolder(folderId2, folderId1); - var folder3 = CreateFolder(folderId3, folderId2); + var folder1 = CreateFolder(folderId1); + var folder2 = CreateFolder(folderId2, folderId1); + var folder3 = CreateFolder(folderId3, folderId2); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) - .Returns(folder1); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) + .Returns(folder1); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A<CancellationToken>._)) - .Returns(folder2); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A<CancellationToken>._)) + .Returns(folder2); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId3, A<CancellationToken>._)) - .Returns(folder3); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId3, A<CancellationToken>._)) + .Returns(folder3); - var actual = await sut.FindAssetFolderAsync(appId.Id, folderId3, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId3, ct); - Assert.Equal(actual, new[] { folder1, folder2, folder3 }); - } + Assert.Equal(actual, new[] { folder1, folder2, folder3 }); + } - [Fact] - public async Task Should_not_resolve_folder_path_if_root_not_found() - { - var folderId1 = DomainId.NewGuid(); + [Fact] + public async Task Should_not_resolve_folder_path_if_root_not_found() + { + var folderId1 = DomainId.NewGuid(); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) - .Returns(Task.FromResult<IAssetFolderEntity?>(null)); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) + .Returns(Task.FromResult<IAssetFolderEntity?>(null)); - var actual = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_not_resolve_folder_path_if_parent_of_child_not_found() - { - var folderId1 = DomainId.NewGuid(); - var folderId2 = DomainId.NewGuid(); + [Fact] + public async Task Should_not_resolve_folder_path_if_parent_of_child_not_found() + { + var folderId1 = DomainId.NewGuid(); + var folderId2 = DomainId.NewGuid(); - var folder1 = CreateFolder(folderId1); - var folder2 = CreateFolder(folderId2, folderId1); + var folder1 = CreateFolder(folderId1); + var folder2 = CreateFolder(folderId2, folderId1); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) - .Returns(Task.FromResult<IAssetFolderEntity?>(null)); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) + .Returns(Task.FromResult<IAssetFolderEntity?>(null)); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A<CancellationToken>._)) - .Returns(folder2); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A<CancellationToken>._)) + .Returns(folder2); - var actual = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_not_resolve_folder_path_if_recursion_detected() - { - var folderId1 = DomainId.NewGuid(); - var folderId2 = DomainId.NewGuid(); + [Fact] + public async Task Should_not_resolve_folder_path_if_recursion_detected() + { + var folderId1 = DomainId.NewGuid(); + var folderId2 = DomainId.NewGuid(); - var folder1 = CreateFolder(folderId1, folderId2); - var folder2 = CreateFolder(folderId2, folderId1); + var folder1 = CreateFolder(folderId1, folderId2); + var folder2 = CreateFolder(folderId2, folderId1); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) - .Returns(Task.FromResult<IAssetFolderEntity?>(null)); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A<CancellationToken>._)) + .Returns(Task.FromResult<IAssetFolderEntity?>(null)); - A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A<CancellationToken>._)) - .Returns(folder2); + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A<CancellationToken>._)) + .Returns(folder2); - var actual = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); + var actual = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - private static void AssertAsset(IAssetEntity source, IEnrichedAssetEntity? actual) - { - Assert.NotNull(actual); - Assert.NotSame(source, actual); - Assert.Equal(source.AssetId, actual?.AssetId); - } + private static void AssertAsset(IAssetEntity source, IEnrichedAssetEntity? actual) + { + Assert.NotNull(actual); + Assert.NotSame(source, actual); + Assert.Equal(source.AssetId, actual?.AssetId); + } - private static IAssetFolderEntity CreateFolder(DomainId id, DomainId parentId = default) - { - var assetFolder = A.Fake<IAssetFolderEntity>(); + private static IAssetFolderEntity CreateFolder(DomainId id, DomainId parentId = default) + { + var assetFolder = A.Fake<IAssetFolderEntity>(); - A.CallTo(() => assetFolder.Id).Returns(id); - A.CallTo(() => assetFolder.ParentId).Returns(parentId); + A.CallTo(() => assetFolder.Id).Returns(id); + A.CallTo(() => assetFolder.ParentId).Returns(parentId); - return assetFolder; - } + return assetFolder; + } - private static AssetEntity CreateAsset(DomainId id) - { - return new AssetEntity { Id = id }; - } + private static AssetEntity CreateAsset(DomainId id) + { + return new AssetEntity { Id = id }; + } - private void SetupEnricher() - { - A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>._, A<Context>._, ct)) - .ReturnsLazily(x => - { - var input = x.GetArgument<IEnumerable<IAssetEntity>>(0)!; + private void SetupEnricher() + { + A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>._, A<Context>._, ct)) + .ReturnsLazily(x => + { + var input = x.GetArgument<IEnumerable<IAssetEntity>>(0)!; - return Task.FromResult<IReadOnlyList<IEnrichedAssetEntity>>(input.Select(c => SimpleMapper.Map(c, new AssetEntity())).ToList()); - }); - } + return Task.FromResult<IReadOnlyList<IEnrichedAssetEntity>>(input.Select(c => SimpleMapper.Map(c, new AssetEntity())).ToList()); + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs index f56e4d01d2..3c144720f9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs @@ -16,120 +16,119 @@ using Squidex.Infrastructure.Reflection; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class RecursiveDeleterTests { - public class RecursiveDeleterTests + private readonly ILogger<RecursiveDeleter> log = A.Fake<ILogger<RecursiveDeleter>>(); + private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); + private readonly IAssetFolderRepository assetFolderRepository = A.Fake<IAssetFolderRepository>(); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RecursiveDeleter sut; + + public RecursiveDeleterTests() { - private readonly ILogger<RecursiveDeleter> log = A.Fake<ILogger<RecursiveDeleter>>(); - private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); - private readonly IAssetFolderRepository assetFolderRepository = A.Fake<IAssetFolderRepository>(); - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RecursiveDeleter sut; - - public RecursiveDeleterTests() - { - var typeNameRegistry = new TypeNameRegistry().Map(typeof(AssetFolderDeleted)); + var typeNameRegistry = new TypeNameRegistry().Map(typeof(AssetFolderDeleted)); - sut = new RecursiveDeleter(commandBus, assetRepository, assetFolderRepository, typeNameRegistry, log); - } + sut = new RecursiveDeleter(commandBus, assetRepository, assetFolderRepository, typeNameRegistry, log); + } - [Fact] - public void Should_return_assets_filter_for_events_filter() - { - IEventConsumer consumer = sut; + [Fact] + public void Should_return_assets_filter_for_events_filter() + { + IEventConsumer consumer = sut; - Assert.Equal("^assetFolder-", consumer.EventsFilter); - } + Assert.Equal("^assetFolder-", consumer.EventsFilter); + } - [Fact] - public async Task Should_do_nothing_on_clear() - { - IEventConsumer consumer = sut; + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; - await consumer.ClearAsync(); - } + await consumer.ClearAsync(); + } - [Fact] - public void Should_return_type_name_for_name() - { - IEventConsumer consumer = sut; + [Fact] + public void Should_return_type_name_for_name() + { + IEventConsumer consumer = sut; - Assert.Equal(nameof(RecursiveDeleter), consumer.Name); - } + Assert.Equal(nameof(RecursiveDeleter), consumer.Name); + } - [Fact] - public async Task Should_Not_invoke_delete_commands_if_event_restored() - { - var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = DomainId.NewGuid() }; + [Fact] + public async Task Should_Not_invoke_delete_commands_if_event_restored() + { + var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = DomainId.NewGuid() }; - await sut.On(Envelope.Create(@event).SetRestored()); + await sut.On(Envelope.Create(@event).SetRestored()); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_invoke_delete_commands_for_all_subfolders() - { - var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = DomainId.NewGuid() }; + [Fact] + public async Task Should_invoke_delete_commands_for_all_subfolders() + { + var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = DomainId.NewGuid() }; - var childFolderId1 = DomainId.NewGuid(); - var childFolderId2 = DomainId.NewGuid(); + var childFolderId1 = DomainId.NewGuid(); + var childFolderId2 = DomainId.NewGuid(); - A.CallTo(() => assetFolderRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) - .Returns(new List<DomainId> { childFolderId1, childFolderId2 }); + A.CallTo(() => assetFolderRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) + .Returns(new List<DomainId> { childFolderId1, childFolderId2 }); - await sut.On(Envelope.Create(@event)); + await sut.On(Envelope.Create(@event)); - A.CallTo(() => commandBus.PublishAsync(A<DeleteAssetFolder>.That.Matches(x => x.AssetFolderId == childFolderId1), default)) - .MustHaveHappened(); + A.CallTo(() => commandBus.PublishAsync(A<DeleteAssetFolder>.That.Matches(x => x.AssetFolderId == childFolderId1), default)) + .MustHaveHappened(); - A.CallTo(() => commandBus.PublishAsync(A<DeleteAssetFolder>.That.Matches(x => x.AssetFolderId == childFolderId2), default)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<DeleteAssetFolder>.That.Matches(x => x.AssetFolderId == childFolderId2), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_invoke_delete_commands_for_all_assets() - { - var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = DomainId.NewGuid() }; + [Fact] + public async Task Should_invoke_delete_commands_for_all_assets() + { + var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = DomainId.NewGuid() }; - var childId1 = DomainId.NewGuid(); - var childId2 = DomainId.NewGuid(); + var childId1 = DomainId.NewGuid(); + var childId2 = DomainId.NewGuid(); - A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) - .Returns(new List<DomainId> { childId1, childId2 }); + A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) + .Returns(new List<DomainId> { childId1, childId2 }); - await sut.On(Envelope.Create(@event)); + await sut.On(Envelope.Create(@event)); - A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId1), default)) - .MustHaveHappened(); + A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId1), default)) + .MustHaveHappened(); - A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId2), default)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId2), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_ignore_exceptions() - { - var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = DomainId.NewGuid() }; + [Fact] + public async Task Should_ignore_exceptions() + { + var @event = new AssetFolderDeleted { AppId = appId, AssetFolderId = DomainId.NewGuid() }; - var childId1 = DomainId.NewGuid(); - var childId2 = DomainId.NewGuid(); + var childId1 = DomainId.NewGuid(); + var childId2 = DomainId.NewGuid(); - A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId1), default)) - .Throws(new InvalidOperationException()); + A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId1), default)) + .Throws(new InvalidOperationException()); - A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) - .Returns(new List<DomainId> { childId1, childId2 }); + A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) + .Returns(new List<DomainId> { childId1, childId2 }); - await sut.On(Envelope.Create(@event)); + await sut.On(Envelope.Create(@event)); - A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId2), default)) - .MustHaveHappened(); + A.CallTo(() => commandBus.PublishAsync(A<DeleteAsset>.That.Matches(x => x.AssetId == childId2), default)) + .MustHaveHappened(); - A.CallTo(log).Where(x => x.Method.Name == "Log") - .MustHaveHappened(); - } + A.CallTo(log).Where(x => x.Method.Name == "Log") + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs index 4329bf6dd6..9bb5d57740 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs @@ -12,120 +12,119 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class RepairFilesTests { - public class RepairFilesTests + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); + private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RebuildFiles sut; + + public RepairFilesTests() { - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); - private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RebuildFiles sut; + sut = new RebuildFiles(assetFileStore, eventFormatter, eventStore); + } - public RepairFilesTests() - { - sut = new RebuildFiles(assetFileStore, eventFormatter, eventStore); - } + [Fact] + public async Task Should_repair_created_asset_if_not_found() + { + var @event = new AssetCreated { AppId = appId, AssetId = DomainId.NewGuid() }; - [Fact] - public async Task Should_repair_created_asset_if_not_found() - { - var @event = new AssetCreated { AppId = appId, AssetId = DomainId.NewGuid() }; + SetupEvent(@event); - SetupEvent(@event); + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, null, default)) + .Throws(new AssetNotFoundException("file")); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, null, default)) - .Throws(new AssetNotFoundException("file")); + await sut.RepairAsync(); - await sut.RepairAsync(); + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A<Stream>._, true, default)) + .MustHaveHappened(); + } - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A<Stream>._, true, default)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_not_repair_created_asset_if_found() + { + var @event = new AssetCreated { AppId = appId, AssetId = DomainId.NewGuid() }; - [Fact] - public async Task Should_not_repair_created_asset_if_found() - { - var @event = new AssetCreated { AppId = appId, AssetId = DomainId.NewGuid() }; + SetupEvent(@event); - SetupEvent(@event); + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, null, default)) + .Returns(100); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, null, default)) - .Returns(100); + await sut.RepairAsync(); - await sut.RepairAsync(); + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A<Stream>._, true, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A<Stream>._, true, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_repair_updated_asset_if_not_found() + { + var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; - [Fact] - public async Task Should_repair_updated_asset_if_not_found() - { - var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; + SetupEvent(@event); - SetupEvent(@event); + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, A<CancellationToken>._)) + .Throws(new AssetNotFoundException("file")); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, A<CancellationToken>._)) - .Throws(new AssetNotFoundException("file")); + await sut.RepairAsync(); - await sut.RepairAsync(); + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A<Stream>._, true, default)) + .MustHaveHappened(); + } - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A<Stream>._, true, default)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_not_repair_updated_asset_if_found() + { + var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; - [Fact] - public async Task Should_not_repair_updated_asset_if_found() - { - var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; + SetupEvent(@event); - SetupEvent(@event); + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, default)) + .Returns(100); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, default)) - .Returns(100); + await sut.RepairAsync(); - await sut.RepairAsync(); + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A<Stream>._, true, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A<Stream>._, true, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_ignore_old_events() + { + SetupEvent(null); - [Fact] - public async Task Should_ignore_old_events() - { - SetupEvent(null); + await sut.RepairAsync(); - await sut.RepairAsync(); + A.CallTo(() => assetFileStore.GetFileSizeAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => assetFileStore.GetFileSizeAsync(A<DomainId>._, A<DomainId>._, A<long>._, null, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + private void SetupEvent(IEvent? @event) + { + var storedEvent = + new StoredEvent("stream", "0", -1, + new EventData("type", new EnvelopeHeaders(), "payload")); + + var storedEvents = new List<StoredEvent> + { + storedEvent + }; - private void SetupEvent(IEvent? @event) + if (@event != null) { - var storedEvent = - new StoredEvent("stream", "0", -1, - new EventData("type", new EnvelopeHeaders(), "payload")); - - var storedEvents = new List<StoredEvent> - { - storedEvent - }; - - if (@event != null) - { - A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) - .Returns(Envelope.Create(@event)); - } - else - { - A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) - .Returns(null); - } - - A.CallTo(() => eventStore.QueryAllAsync("^asset\\-", null, int.MaxValue, default)) - .Returns(storedEvents.ToAsyncEnumerable()); + A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) + .Returns(Envelope.Create(@event)); } + else + { + A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) + .Returns(null); + } + + A.CallTo(() => eventStore.QueryAllAsync("^asset\\-", null, int.MaxValue, default)) + .Returns(storedEvents.ToAsyncEnumerable()); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/SvgAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/SvgAssetMetadataSourceTests.cs index c7db852db6..f231b68b23 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/SvgAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/SvgAssetMetadataSourceTests.cs @@ -12,70 +12,69 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Assets; + +public class SvgAssetMetadataSourceTests { - public class SvgAssetMetadataSourceTests - { - private readonly MemoryStream stream = new MemoryStream(); - private readonly SvgAssetMetadataSource sut = new SvgAssetMetadataSource(); + private readonly MemoryStream stream = new MemoryStream(); + private readonly SvgAssetMetadataSource sut = new SvgAssetMetadataSource(); - public SvgAssetMetadataSourceTests() - { - sut = new SvgAssetMetadataSource(); - } + public SvgAssetMetadataSourceTests() + { + sut = new SvgAssetMetadataSource(); + } - [Fact] - public async Task Should_add_image_tag_if_svg_mime() - { - var svg = new DelegateAssetFile("MyImage.png", "image/svg+xml", 1024, () => stream); + [Fact] + public async Task Should_add_image_tag_if_svg_mime() + { + var svg = new DelegateAssetFile("MyImage.png", "image/svg+xml", 1024, () => stream); - var command = new CreateAsset { File = svg }; + var command = new CreateAsset { File = svg }; - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Contains("image", command.Tags); - } + Assert.Contains("image", command.Tags); + } - [Fact] - public async Task Should_add_image_tag_if_svg_extension() - { - var svg = new DelegateAssetFile("MyImage.svg", "other", 1024, () => stream); + [Fact] + public async Task Should_add_image_tag_if_svg_extension() + { + var svg = new DelegateAssetFile("MyImage.svg", "other", 1024, () => stream); - var command = new CreateAsset { File = svg }; + var command = new CreateAsset { File = svg }; - await sut.EnhanceAsync(command, default); + await sut.EnhanceAsync(command, default); - Assert.Contains("image", command.Tags); - } + Assert.Contains("image", command.Tags); + } - [Fact] - public async Task Should_throw_exception_if_svg_is_malicious() - { - var bytes = Encoding.UTF8.GetBytes(Resources.SvgInvalid); + [Fact] + public async Task Should_throw_exception_if_svg_is_malicious() + { + var bytes = Encoding.UTF8.GetBytes(Resources.SvgInvalid); - stream.Write(bytes); - stream.Seek(0, SeekOrigin.Begin); + stream.Write(bytes); + stream.Seek(0, SeekOrigin.Begin); - var svg = new DelegateAssetFile("MyImage.svg", "other", 1024, () => stream); + var svg = new DelegateAssetFile("MyImage.svg", "other", 1024, () => stream); - var command = new CreateAsset { File = svg }; + var command = new CreateAsset { File = svg }; - await Assert.ThrowsAsync<ValidationException>(() => sut.EnhanceAsync(command, default)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.EnhanceAsync(command, default)); + } - [Fact] - public async Task Should_not_throw_exception_if_svg_is_not_malicious() - { - var bytes = Encoding.UTF8.GetBytes(Resources.SvgValid); + [Fact] + public async Task Should_not_throw_exception_if_svg_is_not_malicious() + { + var bytes = Encoding.UTF8.GetBytes(Resources.SvgValid); - stream.Write(bytes); - stream.Seek(0, SeekOrigin.Begin); + stream.Write(bytes); + stream.Seek(0, SeekOrigin.Begin); - var svg = new DelegateAssetFile("MyImage.svg", "other", 1024, () => stream); + var svg = new DelegateAssetFile("MyImage.svg", "other", 1024, () => stream); - var command = new CreateAsset { File = svg }; + var command = new CreateAsset { File = svg }; - await sut.EnhanceAsync(command, default); - } + await sut.EnhanceAsync(command, default); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs index d04a26abdf..c25555d9d6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs @@ -8,52 +8,51 @@ using FakeItEasy; using Xunit; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public class BackupCompatibilityTests { - public class BackupCompatibilityTests + [Fact] + public async Task Should_writer_version() { - [Fact] - public async Task Should_writer_version() - { - var writer = A.Fake<IBackupWriter>(); + var writer = A.Fake<IBackupWriter>(); - await writer.WriteVersionAsync(); + await writer.WriteVersionAsync(); - A.CallTo(() => writer.WriteJsonAsync(A<string>._, A<CompatibilityExtensions.FileVersion>.That.Matches(x => x.Major == 5), default)) - .MustHaveHappened(); - } + A.CallTo(() => writer.WriteJsonAsync(A<string>._, A<CompatibilityExtensions.FileVersion>.That.Matches(x => x.Major == 5), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_throw_exception_if_backup_has_correct_version() - { - var reader = A.Fake<IBackupReader>(); + [Fact] + public async Task Should_not_throw_exception_if_backup_has_correct_version() + { + var reader = A.Fake<IBackupReader>(); - A.CallTo(() => reader.ReadJsonAsync<CompatibilityExtensions.FileVersion>(A<string>._, default)) - .Returns(new CompatibilityExtensions.FileVersion { Major = 5 }); + A.CallTo(() => reader.ReadJsonAsync<CompatibilityExtensions.FileVersion>(A<string>._, default)) + .Returns(new CompatibilityExtensions.FileVersion { Major = 5 }); - await reader.CheckCompatibilityAsync(); - } + await reader.CheckCompatibilityAsync(); + } - [Fact] - public async Task Should_throw_exception_if_backup_has_wrong_version() - { - var reader = A.Fake<IBackupReader>(); + [Fact] + public async Task Should_throw_exception_if_backup_has_wrong_version() + { + var reader = A.Fake<IBackupReader>(); - A.CallTo(() => reader.ReadJsonAsync<CompatibilityExtensions.FileVersion>(A<string>._, default)) - .Returns(new CompatibilityExtensions.FileVersion { Major = 3 }); + A.CallTo(() => reader.ReadJsonAsync<CompatibilityExtensions.FileVersion>(A<string>._, default)) + .Returns(new CompatibilityExtensions.FileVersion { Major = 3 }); - await Assert.ThrowsAsync<BackupRestoreException>(() => reader.CheckCompatibilityAsync()); - } + await Assert.ThrowsAsync<BackupRestoreException>(() => reader.CheckCompatibilityAsync()); + } - [Fact] - public async Task Should_not_throw_exception_if_backup_has_no_version() - { - var reader = A.Fake<IBackupReader>(); + [Fact] + public async Task Should_not_throw_exception_if_backup_has_no_version() + { + var reader = A.Fake<IBackupReader>(); - A.CallTo(() => reader.ReadJsonAsync<CompatibilityExtensions.FileVersion>(A<string>._, default)) - .Throws(new FileNotFoundException()); + A.CallTo(() => reader.ReadJsonAsync<CompatibilityExtensions.FileVersion>(A<string>._, default)) + .Throws(new FileNotFoundException()); - await reader.CheckCompatibilityAsync(); - } + await reader.CheckCompatibilityAsync(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs index 2958e5d0a3..f2b0590db5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs @@ -16,288 +16,287 @@ using Squidex.Infrastructure.States; using Xunit; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public class BackupReaderWriterTests { - public class BackupReaderWriterTests + private readonly IEventFormatter eventFormatter; + private readonly IEventStreamNames eventStreamNames = A.Fake<IEventStreamNames>(); + private readonly IJsonSerializer serializer = TestUtils.DefaultSerializer; + private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + + [TypeName(nameof(MyEvent))] + public sealed class MyEvent : IEvent { - private readonly IEventFormatter eventFormatter; - private readonly IEventStreamNames eventStreamNames = A.Fake<IEventStreamNames>(); - private readonly IJsonSerializer serializer = TestUtils.DefaultSerializer; - private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + public Guid Id { get; set; } = Guid.NewGuid(); + } - [TypeName(nameof(MyEvent))] - public sealed class MyEvent : IEvent - { - public Guid Id { get; set; } = Guid.NewGuid(); - } + public BackupReaderWriterTests() + { + typeNameRegistry.Map(typeof(MyEvent)); - public BackupReaderWriterTests() - { - typeNameRegistry.Map(typeof(MyEvent)); + eventFormatter = new DefaultEventFormatter(typeNameRegistry, serializer); + } - eventFormatter = new DefaultEventFormatter(typeNameRegistry, serializer); - } + [Fact] + public async Task Should_not_write_blob_if_handler_failed() + { + var file = "File.json"; - [Fact] - public async Task Should_not_write_blob_if_handler_failed() + await TestReaderWriterAsync(BackupVersion.V1, async writer => { - var file = "File.json"; - - await TestReaderWriterAsync(BackupVersion.V1, async writer => + try { - try + await using (var stream = await writer.OpenBlobAsync(file)) { - await using (var stream = await writer.OpenBlobAsync(file)) - { - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); } - catch - { - return; - } - }, async reader => + } + catch { - await Assert.ThrowsAsync<FileNotFoundException>(() => ReadGuidAsync(reader, file)); - }); - } + return; + } + }, async reader => + { + await Assert.ThrowsAsync<FileNotFoundException>(() => ReadGuidAsync(reader, file)); + }); + } + + [Fact] + public async Task Should_return_true_if_file_exists() + { + var file = "File.json"; + + var value = Guid.NewGuid(); - [Fact] - public async Task Should_return_true_if_file_exists() + await TestReaderWriterAsync(BackupVersion.V1, async writer => { - var file = "File.json"; + await WriteJsonGuidAsync(writer, file, value); + }, async reader => + { + var hasFile = await reader.HasFileAsync(file); - var value = Guid.NewGuid(); + Assert.True(hasFile); + }); + } - await TestReaderWriterAsync(BackupVersion.V1, async writer => - { - await WriteJsonGuidAsync(writer, file, value); - }, async reader => - { - var hasFile = await reader.HasFileAsync(file); + [Fact] + public async Task Should_return_file_if_file_does_not_exist() + { + var file = "File.json"; - Assert.True(hasFile); - }); - } + var value = Guid.NewGuid(); - [Fact] - public async Task Should_return_file_if_file_does_not_exist() + await TestReaderWriterAsync(BackupVersion.V1, async writer => + { + await Task.Yield(); + }, async reader => { - var file = "File.json"; + var hasFile = await reader.HasFileAsync(file); - var value = Guid.NewGuid(); + Assert.False(hasFile); + }); + } - await TestReaderWriterAsync(BackupVersion.V1, async writer => - { - await Task.Yield(); - }, async reader => - { - var hasFile = await reader.HasFileAsync(file); + [Fact] + public async Task Should_read_and_write_json_async() + { + var file = "File.json"; - Assert.False(hasFile); - }); - } + var value = Guid.NewGuid(); - [Fact] - public async Task Should_read_and_write_json_async() + await TestReaderWriterAsync(BackupVersion.V1, async writer => + { + await WriteJsonGuidAsync(writer, file, value); + }, async reader => { - var file = "File.json"; + var read = await ReadJsonGuidAsync(reader, file); - var value = Guid.NewGuid(); + Assert.Equal(value, read); + }); + } - await TestReaderWriterAsync(BackupVersion.V1, async writer => - { - await WriteJsonGuidAsync(writer, file, value); - }, async reader => - { - var read = await ReadJsonGuidAsync(reader, file); + [Fact] + public async Task Should_read_and_write_blob_async() + { + var file = "File.json"; - Assert.Equal(value, read); - }); - } + var value = Guid.NewGuid(); - [Fact] - public async Task Should_read_and_write_blob_async() + await TestReaderWriterAsync(BackupVersion.V1, async writer => { - var file = "File.json"; + await WriteGuidAsync(writer, file, value); + }, async reader => + { + var read = await ReadGuidAsync(reader, file); - var value = Guid.NewGuid(); + Assert.Equal(value, read); + }); + } - await TestReaderWriterAsync(BackupVersion.V1, async writer => - { - await WriteGuidAsync(writer, file, value); - }, async reader => - { - var read = await ReadGuidAsync(reader, file); + [Fact] + public async Task Should_throw_exception_if_json_not_found() + { + await TestReaderWriterAsync(BackupVersion.V1, writer => + { + return Task.CompletedTask; + }, async reader => + { + await Assert.ThrowsAsync<FileNotFoundException>(() => reader.ReadJsonAsync<int>("404")); + }); + } - Assert.Equal(value, read); - }); - } + [Fact] + public async Task Should_throw_exception_if_blob_not_found() + { + await TestReaderWriterAsync(BackupVersion.V1, writer => + { + return Task.CompletedTask; + }, async reader => + { + await Assert.ThrowsAsync<FileNotFoundException>(() => reader.OpenBlobAsync("404")); + }); + } - [Fact] - public async Task Should_throw_exception_if_json_not_found() + [Theory] + [InlineData(BackupVersion.V1)] + [InlineData(BackupVersion.V2)] + public async Task Should_write_and_read_events_to_backup(BackupVersion version) + { + var randomDomainIds = new List<DomainId>(); + + for (var i = 0; i < 100; i++) { - await TestReaderWriterAsync(BackupVersion.V1, writer => - { - return Task.CompletedTask; - }, async reader => - { - await Assert.ThrowsAsync<FileNotFoundException>(() => reader.ReadJsonAsync<int>("404")); - }); + randomDomainIds.Add(DomainId.NewGuid()); } - [Fact] - public async Task Should_throw_exception_if_blob_not_found() + DomainId RandomDomainId() { - await TestReaderWriterAsync(BackupVersion.V1, writer => - { - return Task.CompletedTask; - }, async reader => - { - await Assert.ThrowsAsync<FileNotFoundException>(() => reader.OpenBlobAsync("404")); - }); + return randomDomainIds[Random.Shared.Next(randomDomainIds.Count)]; } - [Theory] - [InlineData(BackupVersion.V1)] - [InlineData(BackupVersion.V2)] - public async Task Should_write_and_read_events_to_backup(BackupVersion version) + var sourceEvents = new List<(string Stream, Envelope<MyEvent> Event)>(); + + for (var i = 0; i < 200; i++) { - var randomDomainIds = new List<DomainId>(); + var @event = new MyEvent(); - for (var i = 0; i < 100; i++) - { - randomDomainIds.Add(DomainId.NewGuid()); - } + var envelope = Envelope.Create(@event); - DomainId RandomDomainId() - { - return randomDomainIds[Random.Shared.Next(randomDomainIds.Count)]; - } + envelope.Headers.Add("Id", JsonValue.Create(@event.Id)); + envelope.Headers.Add("Index", JsonValue.Create(i)); - var sourceEvents = new List<(string Stream, Envelope<MyEvent> Event)>(); + sourceEvents.Add(($"My-{RandomDomainId()}", envelope)); + } - for (var i = 0; i < 200; i++) + await TestReaderWriterAsync(version, async writer => + { + foreach (var (stream, envelope) in sourceEvents) { - var @event = new MyEvent(); + var eventData = eventFormatter.ToEventData(envelope, Guid.NewGuid(), true); + var eventStored = new StoredEvent(stream, "1", 2, eventData); - var envelope = Envelope.Create(@event); + var index = int.Parse(envelope.Headers["Index"].ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture); - envelope.Headers.Add("Id", JsonValue.Create(@event.Id)); - envelope.Headers.Add("Index", JsonValue.Create(i)); + if (index % 17 == 0) + { + await WriteGuidAsync(writer, index.ToString(CultureInfo.InvariantCulture), envelope.Payload.Id); + } + else if (index % 37 == 0) + { + await WriteJsonGuidAsync(writer, index.ToString(CultureInfo.InvariantCulture), envelope.Payload.Id); + } - sourceEvents.Add(($"My-{RandomDomainId()}", envelope)); + writer.WriteEvent(eventStored); } + }, async reader => + { + var targetEvents = new List<(string Stream, Envelope<IEvent> Event)>(); - await TestReaderWriterAsync(version, async writer => + await foreach (var @event in reader.ReadEventsAsync(eventStreamNames, eventFormatter)) { - foreach (var (stream, envelope) in sourceEvents) - { - var eventData = eventFormatter.ToEventData(envelope, Guid.NewGuid(), true); - var eventStored = new StoredEvent(stream, "1", 2, eventData); + var index = int.Parse(@event.Event.Headers["Index"].ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture); - var index = int.Parse(envelope.Headers["Index"].ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture); + var id = Guid.Parse(@event.Event.Headers["Id"].ToString()); - if (index % 17 == 0) - { - await WriteGuidAsync(writer, index.ToString(CultureInfo.InvariantCulture), envelope.Payload.Id); - } - else if (index % 37 == 0) - { - await WriteJsonGuidAsync(writer, index.ToString(CultureInfo.InvariantCulture), envelope.Payload.Id); - } + if (index % 17 == 0) + { + var guid = await ReadGuidAsync(reader, index.ToString(CultureInfo.InvariantCulture)); - writer.WriteEvent(eventStored); + Assert.Equal(id, guid); } - }, async reader => - { - var targetEvents = new List<(string Stream, Envelope<IEvent> Event)>(); - - await foreach (var @event in reader.ReadEventsAsync(eventStreamNames, eventFormatter)) + else if (index % 37 == 0) { - var index = int.Parse(@event.Event.Headers["Index"].ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture); + var guid = await ReadJsonGuidAsync(reader, index.ToString(CultureInfo.InvariantCulture)); - var id = Guid.Parse(@event.Event.Headers["Id"].ToString()); - - if (index % 17 == 0) - { - var guid = await ReadGuidAsync(reader, index.ToString(CultureInfo.InvariantCulture)); - - Assert.Equal(id, guid); - } - else if (index % 37 == 0) - { - var guid = await ReadJsonGuidAsync(reader, index.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(id, guid); + } - Assert.Equal(id, guid); - } + targetEvents.Add(@event); + } - targetEvents.Add(@event); - } + for (var i = 0; i < targetEvents.Count; i++) + { + var targetEvent = targetEvents[i].Event.To<MyEvent>(); + var targetStream = targetEvents[i].Stream; - for (var i = 0; i < targetEvents.Count; i++) - { - var targetEvent = targetEvents[i].Event.To<MyEvent>(); - var targetStream = targetEvents[i].Stream; + var sourceEvent = sourceEvents[i].Event.To<MyEvent>(); + var sourceStream = sourceEvents[i].Stream; - var sourceEvent = sourceEvents[i].Event.To<MyEvent>(); - var sourceStream = sourceEvents[i].Stream; + Assert.Equal(sourceEvent.Payload.Id, targetEvent.Payload.Id); + Assert.Equal(sourceStream, targetStream); + } + }); + } - Assert.Equal(sourceEvent.Payload.Id, targetEvent.Payload.Id); - Assert.Equal(sourceStream, targetStream); - } - }); - } + private static Task<Guid> ReadJsonGuidAsync(IBackupReader reader, string file) + { + return reader.ReadJsonAsync<Guid>(file); + } - private static Task<Guid> ReadJsonGuidAsync(IBackupReader reader, string file) - { - return reader.ReadJsonAsync<Guid>(file); - } + private static Task WriteJsonGuidAsync(IBackupWriter writer, string file, Guid value) + { + return writer.WriteJsonAsync(file, value); + } - private static Task WriteJsonGuidAsync(IBackupWriter writer, string file, Guid value) + private static async Task WriteGuidAsync(IBackupWriter writer, string file, Guid value) + { + await using (var stream = await writer.OpenBlobAsync(file)) { - return writer.WriteJsonAsync(file, value); + await stream.WriteAsync(value.ToByteArray()); } + } - private static async Task WriteGuidAsync(IBackupWriter writer, string file, Guid value) - { - await using (var stream = await writer.OpenBlobAsync(file)) - { - await stream.WriteAsync(value.ToByteArray()); - } - } + private static async Task<Guid> ReadGuidAsync(IBackupReader reader, string file) + { + var read = Guid.Empty; - private static async Task<Guid> ReadGuidAsync(IBackupReader reader, string file) + await using (var stream = await reader.OpenBlobAsync(file)) { - var read = Guid.Empty; + var buffer = new byte[16]; - await using (var stream = await reader.OpenBlobAsync(file)) - { - var buffer = new byte[16]; - - _ = await stream.ReadAsync(buffer); - - read = new Guid(buffer); - } + _ = await stream.ReadAsync(buffer); - return read; + read = new Guid(buffer); } - private async Task TestReaderWriterAsync(BackupVersion version, Func<IBackupWriter, Task> write, Func<IBackupReader, Task> read) + return read; + } + + private async Task TestReaderWriterAsync(BackupVersion version, Func<IBackupWriter, Task> write, Func<IBackupReader, Task> read) + { + using (var stream = new MemoryStream()) { - using (var stream = new MemoryStream()) + using (var writer = new BackupWriter(serializer, stream, true, version)) { - using (var writer = new BackupWriter(serializer, stream, true, version)) - { - await write(writer); - } + await write(writer); + } - stream.Position = 0; + stream.Position = 0; - using (var reader = new BackupReader(serializer, stream)) - { - await read(reader); - } + using (var reader = new BackupReader(serializer, stream)) + { + await read(reader); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs index 402583b4bb..3d0fed6b8b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs @@ -15,153 +15,152 @@ using Squidex.Messaging; using Xunit; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public class BackupServiceTests { - public class BackupServiceTests + private readonly TestState<BackupState> stateBackup; + private readonly TestState<BackupRestoreState> stateRestore; + private readonly IMessageBus messaging = A.Fake<IMessageBus>(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly DomainId backupId = DomainId.NewGuid(); + private readonly RefToken actor = RefToken.User("me"); + private readonly BackupService sut; + + public BackupServiceTests() { - private readonly TestState<BackupState> stateBackup; - private readonly TestState<BackupRestoreState> stateRestore; - private readonly IMessageBus messaging = A.Fake<IMessageBus>(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly DomainId backupId = DomainId.NewGuid(); - private readonly RefToken actor = RefToken.User("me"); - private readonly BackupService sut; - - public BackupServiceTests() - { - stateRestore = new TestState<BackupRestoreState>("Default"); - stateBackup = new TestState<BackupState>(appId); + stateRestore = new TestState<BackupRestoreState>("Default"); + stateBackup = new TestState<BackupState>(appId); - sut = new BackupService( - stateRestore.PersistenceFactory, - stateBackup.PersistenceFactory, messaging); - } + sut = new BackupService( + stateRestore.PersistenceFactory, + stateBackup.PersistenceFactory, messaging); + } - [Fact] - public async Task Should_send_message_to_restore_backup() - { - var restoreUrl = new Uri("http://squidex.io"); - var restoreAppName = "New App"; + [Fact] + public async Task Should_send_message_to_restore_backup() + { + var restoreUrl = new Uri("http://squidex.io"); + var restoreAppName = "New App"; - await sut.StartRestoreAsync(actor, restoreUrl, restoreAppName); + await sut.StartRestoreAsync(actor, restoreUrl, restoreAppName); - A.CallTo(() => messaging.PublishAsync(new BackupRestore(actor, restoreUrl, restoreAppName), null, default)) - .MustHaveHappened(); - } + A.CallTo(() => messaging.PublishAsync(new BackupRestore(actor, restoreUrl, restoreAppName), null, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_send_message_to_start_backup() - { - await sut.StartBackupAsync(appId, actor); + [Fact] + public async Task Should_send_message_to_start_backup() + { + await sut.StartBackupAsync(appId, actor); - A.CallTo(() => messaging.PublishAsync(new BackupStart(appId, actor), null, default)) - .MustHaveHappened(); - } + A.CallTo(() => messaging.PublishAsync(new BackupStart(appId, actor), null, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_send_message_to_delete_backup() - { - await sut.DeleteBackupAsync(appId, backupId); + [Fact] + public async Task Should_send_message_to_delete_backup() + { + await sut.DeleteBackupAsync(appId, backupId); - A.CallTo(() => messaging.PublishAsync(new BackupDelete(appId, backupId), null, default)) - .MustHaveHappened(); - } + A.CallTo(() => messaging.PublishAsync(new BackupDelete(appId, backupId), null, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_send_message_to_clear_backups() - { - await ((IDeleter)sut).DeleteAppAsync(Mocks.App(NamedId.Of(appId, "my-app")), default); + [Fact] + public async Task Should_send_message_to_clear_backups() + { + await ((IDeleter)sut).DeleteAppAsync(Mocks.App(NamedId.Of(appId, "my-app")), default); - A.CallTo(() => messaging.PublishAsync(new BackupClear(appId), null, default)) - .MustHaveHappened(); - } + A.CallTo(() => messaging.PublishAsync(new BackupClear(appId), null, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_exception_when_restore_already_running() + [Fact] + public async Task Should_throw_exception_when_restore_already_running() + { + stateRestore.Snapshot = new BackupRestoreState { - stateRestore.Snapshot = new BackupRestoreState + Job = new RestoreJob { - Job = new RestoreJob - { - Status = JobStatus.Started - } - }; + Status = JobStatus.Started + } + }; - var restoreUrl = new Uri("http://squidex.io"); + var restoreUrl = new Uri("http://squidex.io"); - await Assert.ThrowsAnyAsync<DomainException>(() => sut.StartRestoreAsync(actor, restoreUrl, null)); - } + await Assert.ThrowsAnyAsync<DomainException>(() => sut.StartRestoreAsync(actor, restoreUrl, null)); + } - [Fact] - public async Task Should_throw_exception_when_backup_has_too_many_jobs() + [Fact] + public async Task Should_throw_exception_when_backup_has_too_many_jobs() + { + for (var i = 0; i < 10; i++) { - for (var i = 0; i < 10; i++) - { - stateBackup.Snapshot.Jobs.Add(new BackupJob()); - } - - await Assert.ThrowsAnyAsync<DomainException>(() => sut.StartBackupAsync(appId, actor)); + stateBackup.Snapshot.Jobs.Add(new BackupJob()); } - [Fact] - public async Task Should_throw_exception_when_backup_has_one_running_job() - { - for (var i = 0; i < 2; i++) - { - stateBackup.Snapshot.Jobs.Add(new BackupJob { Status = JobStatus.Started }); - } + await Assert.ThrowsAnyAsync<DomainException>(() => sut.StartBackupAsync(appId, actor)); + } - await Assert.ThrowsAnyAsync<DomainException>(() => sut.StartBackupAsync(appId, actor)); + [Fact] + public async Task Should_throw_exception_when_backup_has_one_running_job() + { + for (var i = 0; i < 2; i++) + { + stateBackup.Snapshot.Jobs.Add(new BackupJob { Status = JobStatus.Started }); } - [Fact] - public async Task Should_get_restore_state_from_store() + await Assert.ThrowsAnyAsync<DomainException>(() => sut.StartBackupAsync(appId, actor)); + } + + [Fact] + public async Task Should_get_restore_state_from_store() + { + stateRestore.Snapshot = new BackupRestoreState { - stateRestore.Snapshot = new BackupRestoreState + Job = new RestoreJob { - Job = new RestoreJob - { - Stopped = SystemClock.Instance.GetCurrentInstant() - } - }; + Stopped = SystemClock.Instance.GetCurrentInstant() + } + }; - var actual = await sut.GetRestoreAsync(); + var actual = await sut.GetRestoreAsync(); - actual.Should().BeEquivalentTo(stateRestore.Snapshot.Job); - } + actual.Should().BeEquivalentTo(stateRestore.Snapshot.Job); + } - [Fact] - public async Task Should_get_backups_state_from_store() + [Fact] + public async Task Should_get_backups_state_from_store() + { + var job = new BackupJob { - var job = new BackupJob - { - Id = backupId, - Started = SystemClock.Instance.GetCurrentInstant(), - Stopped = SystemClock.Instance.GetCurrentInstant() - }; + Id = backupId, + Started = SystemClock.Instance.GetCurrentInstant(), + Stopped = SystemClock.Instance.GetCurrentInstant() + }; - stateBackup.Snapshot.Jobs.Add(job); + stateBackup.Snapshot.Jobs.Add(job); - var actual = await sut.GetBackupsAsync(appId); + var actual = await sut.GetBackupsAsync(appId); - actual.Should().BeEquivalentTo(stateBackup.Snapshot.Jobs); - } + actual.Should().BeEquivalentTo(stateBackup.Snapshot.Jobs); + } - [Fact] - public async Task Should_get_backup_state_from_store() + [Fact] + public async Task Should_get_backup_state_from_store() + { + var job = new BackupJob { - var job = new BackupJob - { - Id = backupId, - Started = SystemClock.Instance.GetCurrentInstant(), - Stopped = SystemClock.Instance.GetCurrentInstant() - }; + Id = backupId, + Started = SystemClock.Instance.GetCurrentInstant(), + Stopped = SystemClock.Instance.GetCurrentInstant() + }; - stateBackup.Snapshot.Jobs.Add(job); + stateBackup.Snapshot.Jobs.Add(job); - var actual = await sut.GetBackupAsync(appId, backupId); + var actual = await sut.GetBackupAsync(appId, backupId); - actual.Should().BeEquivalentTo(job); - } + actual.Should().BeEquivalentTo(job); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs index 7f9049c764..06474edad3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs @@ -10,55 +10,54 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public class DefaultBackupArchiveStoreTests { - public class DefaultBackupArchiveStoreTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); + private readonly DomainId backupId = DomainId.NewGuid(); + private readonly string fileName; + private readonly DefaultBackupArchiveStore sut; + + public DefaultBackupArchiveStoreTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); - private readonly DomainId backupId = DomainId.NewGuid(); - private readonly string fileName; - private readonly DefaultBackupArchiveStore sut; - - public DefaultBackupArchiveStoreTests() - { - ct = cts.Token; + ct = cts.Token; - fileName = $"{backupId}_0"; + fileName = $"{backupId}_0"; - sut = new DefaultBackupArchiveStore(assetStore); - } + sut = new DefaultBackupArchiveStore(assetStore); + } - [Fact] - public async Task Should_invoke_asset_store_to_upload_archive_using_suffix_for_compatibility() - { - var stream = new MemoryStream(); + [Fact] + public async Task Should_invoke_asset_store_to_upload_archive_using_suffix_for_compatibility() + { + var stream = new MemoryStream(); - await sut.UploadAsync(backupId, stream, ct); + await sut.UploadAsync(backupId, stream, ct); - A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_invoke_asset_store_to_download_archive_using_suffix_for_compatibility() - { - var stream = new MemoryStream(); + [Fact] + public async Task Should_invoke_asset_store_to_download_archive_using_suffix_for_compatibility() + { + var stream = new MemoryStream(); - await sut.DownloadAsync(backupId, stream, ct); + await sut.DownloadAsync(backupId, stream, ct); - A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_invoke_asset_store_to_delete_archive_using_suffix_for_compatibility() - { - await sut.DeleteAsync(backupId, ct); + [Fact] + public async Task Should_invoke_asset_store_to_delete_archive_using_suffix_for_compatibility() + { + await sut.DeleteAsync(backupId, ct); - A.CallTo(() => assetStore.DeleteAsync(fileName, ct)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DeleteAsync(fileName, ct)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/StreamMapperTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/StreamMapperTests.cs index f37f1d8bc2..0b289dae38 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/StreamMapperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/StreamMapperTests.cs @@ -9,62 +9,61 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Domain.Apps.Entities.Backup; + +public class StreamMapperTests { - public class StreamMapperTests - { - private readonly DomainId appIdOld = DomainId.NewGuid(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly StreamMapper sut; + private readonly DomainId appIdOld = DomainId.NewGuid(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly StreamMapper sut; - public StreamMapperTests() - { - sut = new StreamMapper(new RestoreContext(appId, - A.Fake<IUserMapping>(), - A.Fake<IBackupReader>(), - appIdOld)); - } + public StreamMapperTests() + { + sut = new StreamMapper(new RestoreContext(appId, + A.Fake<IUserMapping>(), + A.Fake<IBackupReader>(), + appIdOld)); + } - [Fact] - public void Should_map_old_app_id() - { - var actual = sut.Map($"app-{appIdOld}"); + [Fact] + public void Should_map_old_app_id() + { + var actual = sut.Map($"app-{appIdOld}"); - Assert.Equal(($"app-{appId}", appId), actual); - } + Assert.Equal(($"app-{appId}", appId), actual); + } - [Fact] - public void Should_map_old_app_broken_id() - { - var actual = sut.Map($"app-{appIdOld}--{appIdOld}"); + [Fact] + public void Should_map_old_app_broken_id() + { + var actual = sut.Map($"app-{appIdOld}--{appIdOld}"); - Assert.Equal(($"app-{appId}", appId), actual); - } + Assert.Equal(($"app-{appId}", appId), actual); + } - [Fact] - public void Should_map_non_app_id() - { - var actual = sut.Map($"content-{appIdOld}--123"); + [Fact] + public void Should_map_non_app_id() + { + var actual = sut.Map($"content-{appIdOld}--123"); - Assert.Equal(($"content-{appId}--123", DomainId.Create($"{appId}--123")), actual); - } + Assert.Equal(($"content-{appId}--123", DomainId.Create($"{appId}--123")), actual); + } - [Fact] - public void Should_map_non_app_id_with_double_slash() - { - var actual = sut.Map($"content-{appIdOld}--other--id"); + [Fact] + public void Should_map_non_app_id_with_double_slash() + { + var actual = sut.Map($"content-{appIdOld}--other--id"); - Assert.Equal(($"content-{appId}--other--id", DomainId.Create($"{appId}--other--id")), actual); - } + Assert.Equal(($"content-{appId}--other--id", DomainId.Create($"{appId}--other--id")), actual); + } - [Fact] - public void Should_map_non_combined_id() - { - var id = DomainId.NewGuid(); + [Fact] + public void Should_map_non_combined_id() + { + var id = DomainId.NewGuid(); - var actual = sut.Map($"content-{id}"); + var actual = sut.Map($"content-{id}"); - Assert.Equal(($"content-{appId}--{id}", DomainId.Create($"{appId}--{id}")), actual); - } + Assert.Equal(($"content-{appId}--{id}", DomainId.Create($"{appId}--{id}")), actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs index 799042681b..96dd23e7e6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs @@ -11,123 +11,122 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Backup -{ - public class UserMappingTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly RefToken initiator = Subject("me"); - private readonly UserMapping sut; +namespace Squidex.Domain.Apps.Entities.Backup; - public UserMappingTests() - { - ct = cts.Token; +public class UserMappingTests +{ + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly RefToken initiator = Subject("me"); + private readonly UserMapping sut; - sut = new UserMapping(initiator); - } + public UserMappingTests() + { + ct = cts.Token; - [Fact] - public async Task Should_backup_users_but_no_clients() - { - sut.Backup("1"); - sut.Backup(Subject("2")); + sut = new UserMapping(initiator); + } - sut.Backup(Client("client")); + [Fact] + public async Task Should_backup_users_but_no_clients() + { + sut.Backup("1"); + sut.Backup(Subject("2")); - var user1 = UserMocks.User("1", "1@email.com"); - var user2 = UserMocks.User("2", "1@email.com"); + sut.Backup(Client("client")); - var users = new Dictionary<string, IUser> - { - [user1.Id] = user1, - [user2.Id] = user2 - }; + var user1 = UserMocks.User("1", "1@email.com"); + var user2 = UserMocks.User("2", "1@email.com"); - var userResolver = A.Fake<IUserResolver>(); + var users = new Dictionary<string, IUser> + { + [user1.Id] = user1, + [user2.Id] = user2 + }; - A.CallTo(() => userResolver.QueryManyAsync(A<string[]>.That.Is(user1.Id, user2.Id), ct)) - .Returns(users); + var userResolver = A.Fake<IUserResolver>(); - var writer = A.Fake<IBackupWriter>(); + A.CallTo(() => userResolver.QueryManyAsync(A<string[]>.That.Is(user1.Id, user2.Id), ct)) + .Returns(users); - Dictionary<string, string>? storedUsers = null; + var writer = A.Fake<IBackupWriter>(); - A.CallTo(() => writer.WriteJsonAsync(A<string>._, A<object>._, ct)) - .Invokes(x => storedUsers = x.GetArgument<Dictionary<string, string>>(1)); + Dictionary<string, string>? storedUsers = null; - await sut.StoreAsync(writer, userResolver, ct); + A.CallTo(() => writer.WriteJsonAsync(A<string>._, A<object>._, ct)) + .Invokes(x => storedUsers = x.GetArgument<Dictionary<string, string>>(1)); - Assert.Equal(new Dictionary<string, string> - { - [user1.Id] = user1.Email, - [user2.Id] = user2.Email - }, storedUsers); - } + await sut.StoreAsync(writer, userResolver, ct); - [Fact] - public async Task Should_restore_users() + Assert.Equal(new Dictionary<string, string> { - var user1 = UserMocks.User("1", "1@email.com"); - var user2 = UserMocks.User("2", "2@email.com"); + [user1.Id] = user1.Email, + [user2.Id] = user2.Email + }, storedUsers); + } - var reader = SetupReader(user1, user2); + [Fact] + public async Task Should_restore_users() + { + var user1 = UserMocks.User("1", "1@email.com"); + var user2 = UserMocks.User("2", "2@email.com"); - var userResolver = A.Fake<IUserResolver>(); + var reader = SetupReader(user1, user2); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user1.Email, false, ct)) - .Returns((user1, false)); + var userResolver = A.Fake<IUserResolver>(); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user2.Email, false, ct)) - .Returns((user2, true)); + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user1.Email, false, ct)) + .Returns((user1, false)); - await sut.RestoreAsync(reader, userResolver, ct); + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user2.Email, false, ct)) + .Returns((user2, true)); - Assert.True(sut.TryMap("1_old", out var mapped1)); - Assert.True(sut.TryMap(Subject("2_old"), out var mapped2)); + await sut.RestoreAsync(reader, userResolver, ct); - Assert.Equal(Subject("1"), mapped1); - Assert.Equal(Subject("2"), mapped2); - } + Assert.True(sut.TryMap("1_old", out var mapped1)); + Assert.True(sut.TryMap(Subject("2_old"), out var mapped2)); - [Fact] - public void Should_return_initiator_if_user_not_found() - { - var user = Subject("user1"); + Assert.Equal(Subject("1"), mapped1); + Assert.Equal(Subject("2"), mapped2); + } - Assert.False(sut.TryMap(user, out var mapped)); - Assert.Same(initiator, mapped); - } + [Fact] + public void Should_return_initiator_if_user_not_found() + { + var user = Subject("user1"); - [Fact] - public void Should_create_same_token_if_mapping_client() - { - var client = Client("client1"); + Assert.False(sut.TryMap(user, out var mapped)); + Assert.Same(initiator, mapped); + } - Assert.True(sut.TryMap(client, out var mapped)); - Assert.Same(client, mapped); - } + [Fact] + public void Should_create_same_token_if_mapping_client() + { + var client = Client("client1"); - private IBackupReader SetupReader(params IUser[] users) - { - var storedUsers = users.ToDictionary(x => $"{x.Id}_old", x => x.Email); + Assert.True(sut.TryMap(client, out var mapped)); + Assert.Same(client, mapped); + } - var reader = A.Fake<IBackupReader>(); + private IBackupReader SetupReader(params IUser[] users) + { + var storedUsers = users.ToDictionary(x => $"{x.Id}_old", x => x.Email); - A.CallTo(() => reader.ReadJsonAsync<Dictionary<string, string>>(A<string>._, ct)) - .Returns(storedUsers); + var reader = A.Fake<IBackupReader>(); - return reader; - } + A.CallTo(() => reader.ReadJsonAsync<Dictionary<string, string>>(A<string>._, ct)) + .Returns(storedUsers); - private static RefToken Client(string identifier) - { - return RefToken.Client(identifier); - } + return reader; + } - private static RefToken Subject(string identifier) - { - return RefToken.User(identifier); - } + private static RefToken Client(string identifier) + { + return RefToken.Client(identifier); + } + + private static RefToken Subject(string identifier) + { + return RefToken.User(identifier); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs index 4c8b589699..eb5a8e05b9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs @@ -8,127 +8,126 @@ using FluentAssertions; using Xunit; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public class ConfigPlansProviderTests { - public class ConfigPlansProviderTests + private static readonly Plan InfinitePlan = new Plan + { + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1, + BlockingApiCalls = -1 + }; + + private static readonly Plan FreePlan = new Plan + { + Id = "free", + Name = "Free", + MaxApiCalls = 50000, + MaxAssetSize = 1024 * 1024 * 10, + MaxContributors = 2, + BlockingApiCalls = 50000, + IsFree = true + }; + + private static readonly Plan BasicPlan = new Plan + { + Id = "basic", + Name = "Basic", + MaxApiCalls = 150000, + MaxAssetSize = 1024 * 1024 * 2, + MaxContributors = 5, + YearlyCosts = "100€", + YearlyId = "basic_yearly", + BlockingApiCalls = 150000, + IsFree = false + }; + + private static readonly Plan[] Plans = { BasicPlan, FreePlan }; + + [Fact] + public void Should_return_plans() + { + var sut = new ConfigPlansProvider(Plans); + + sut.GetAvailablePlans().Should().BeEquivalentTo(Plans.OrderBy(x => x.MaxApiCalls)); + } + + [Theory] + [InlineData(null)] + [InlineData("my-plan")] + public void Should_return_infinite_if_nothing_configured(string planId) + { + var sut = new ConfigPlansProvider(Enumerable.Empty<Plan>()); + + var actual = sut.GetActualPlan(planId); + + actual.Should().BeEquivalentTo((InfinitePlan, "infinite")); + } + + [Fact] + public void Should_return_free_plan() + { + var sut = new ConfigPlansProvider(Plans); + + var plan = sut.GetFreePlan(); + + plan.Should().BeEquivalentTo(FreePlan); + } + + [Fact] + public void Should_return_infinite_plan_for_free_plan_if_not_found() + { + var sut = new ConfigPlansProvider(Enumerable.Empty<Plan>()); + + var plan = sut.GetFreePlan(); + + plan.Should().NotBeNull(); + } + + [Fact] + public void Should_return_fitting_app_plan() + { + var sut = new ConfigPlansProvider(Plans); + + var actual = sut.GetActualPlan("basic"); + + actual.Should().BeEquivalentTo((BasicPlan, "basic")); + } + + [Fact] + public void Should_return_fitting_yearly_app_plan() + { + var sut = new ConfigPlansProvider(Plans); + + var actual = sut.GetActualPlan("basic_yearly"); + + actual.Should().BeEquivalentTo((BasicPlan, "basic_yearly")); + } + + [Fact] + public void Should_smallest_plan_if_none_fits() + { + var sut = new ConfigPlansProvider(Plans); + + var actual = sut.GetActualPlan("enterprise"); + + actual.Should().BeEquivalentTo((FreePlan, "free")); + } + + [Fact] + public void Should_check_plan_exists() { - private static readonly Plan InfinitePlan = new Plan - { - Id = "infinite", - Name = "Infinite", - MaxApiCalls = -1, - MaxAssetSize = -1, - MaxContributors = -1, - BlockingApiCalls = -1 - }; - - private static readonly Plan FreePlan = new Plan - { - Id = "free", - Name = "Free", - MaxApiCalls = 50000, - MaxAssetSize = 1024 * 1024 * 10, - MaxContributors = 2, - BlockingApiCalls = 50000, - IsFree = true - }; - - private static readonly Plan BasicPlan = new Plan - { - Id = "basic", - Name = "Basic", - MaxApiCalls = 150000, - MaxAssetSize = 1024 * 1024 * 2, - MaxContributors = 5, - YearlyCosts = "100€", - YearlyId = "basic_yearly", - BlockingApiCalls = 150000, - IsFree = false - }; - - private static readonly Plan[] Plans = { BasicPlan, FreePlan }; - - [Fact] - public void Should_return_plans() - { - var sut = new ConfigPlansProvider(Plans); - - sut.GetAvailablePlans().Should().BeEquivalentTo(Plans.OrderBy(x => x.MaxApiCalls)); - } - - [Theory] - [InlineData(null)] - [InlineData("my-plan")] - public void Should_return_infinite_if_nothing_configured(string planId) - { - var sut = new ConfigPlansProvider(Enumerable.Empty<Plan>()); - - var actual = sut.GetActualPlan(planId); - - actual.Should().BeEquivalentTo((InfinitePlan, "infinite")); - } - - [Fact] - public void Should_return_free_plan() - { - var sut = new ConfigPlansProvider(Plans); - - var plan = sut.GetFreePlan(); - - plan.Should().BeEquivalentTo(FreePlan); - } - - [Fact] - public void Should_return_infinite_plan_for_free_plan_if_not_found() - { - var sut = new ConfigPlansProvider(Enumerable.Empty<Plan>()); - - var plan = sut.GetFreePlan(); - - plan.Should().NotBeNull(); - } - - [Fact] - public void Should_return_fitting_app_plan() - { - var sut = new ConfigPlansProvider(Plans); - - var actual = sut.GetActualPlan("basic"); - - actual.Should().BeEquivalentTo((BasicPlan, "basic")); - } - - [Fact] - public void Should_return_fitting_yearly_app_plan() - { - var sut = new ConfigPlansProvider(Plans); - - var actual = sut.GetActualPlan("basic_yearly"); - - actual.Should().BeEquivalentTo((BasicPlan, "basic_yearly")); - } - - [Fact] - public void Should_smallest_plan_if_none_fits() - { - var sut = new ConfigPlansProvider(Plans); - - var actual = sut.GetActualPlan("enterprise"); - - actual.Should().BeEquivalentTo((FreePlan, "free")); - } - - [Fact] - public void Should_check_plan_exists() - { - var sut = new ConfigPlansProvider(Plans); - - Assert.True(sut.IsConfiguredPlan("basic")); - Assert.True(sut.IsConfiguredPlan("free")); - - Assert.False(sut.IsConfiguredPlan("infinite")); - Assert.False(sut.IsConfiguredPlan("invalid")); - Assert.False(sut.IsConfiguredPlan(null)); - } + var sut = new ConfigPlansProvider(Plans); + + Assert.True(sut.IsConfiguredPlan("basic")); + Assert.True(sut.IsConfiguredPlan("free")); + + Assert.False(sut.IsConfiguredPlan("infinite")); + Assert.False(sut.IsConfiguredPlan("invalid")); + Assert.False(sut.IsConfiguredPlan(null)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/NoopBillingManagerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/NoopBillingManagerTests.cs index 043a30073b..0176a5d727 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/NoopBillingManagerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/NoopBillingManagerTests.cs @@ -9,82 +9,81 @@ using Squidex.Domain.Apps.Entities.Teams; using Xunit; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public class NoopBillingManagerTests { - public class NoopBillingManagerTests + private readonly NoopBillingManager sut = new NoopBillingManager(); + + [Fact] + public async Task Should_do_nothing_if_subscribing_to_app() + { + await sut.SubscribeAsync(null!, (IAppEntity)null!, null!); + } + + [Fact] + public async Task Should_do_nothing_if_subscribing_to_team() + { + await sut.SubscribeAsync(null!, (ITeamEntity)null!, null!); + } + + [Fact] + public async Task Should_do_nothing_if_unsubscribing_from_app() + { + await sut.UnsubscribeAsync(null!, (IAppEntity)null!); + } + + [Fact] + public async Task Should_do_nothing_if_unsubscribing_from_team() + { + await sut.UnsubscribeAsync(null!, (ITeamEntity)null!); + } + + [Fact] + public async Task Should_not_return_portal_link_for_app() + { + var actual = await sut.GetPortalLinkAsync(null!, (IAppEntity)null!); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_not_return_portal_link_for_team() { - private readonly NoopBillingManager sut = new NoopBillingManager(); - - [Fact] - public async Task Should_do_nothing_if_subscribing_to_app() - { - await sut.SubscribeAsync(null!, (IAppEntity)null!, null!); - } - - [Fact] - public async Task Should_do_nothing_if_subscribing_to_team() - { - await sut.SubscribeAsync(null!, (ITeamEntity)null!, null!); - } - - [Fact] - public async Task Should_do_nothing_if_unsubscribing_from_app() - { - await sut.UnsubscribeAsync(null!, (IAppEntity)null!); - } - - [Fact] - public async Task Should_do_nothing_if_unsubscribing_from_team() - { - await sut.UnsubscribeAsync(null!, (ITeamEntity)null!); - } - - [Fact] - public async Task Should_not_return_portal_link_for_app() - { - var actual = await sut.GetPortalLinkAsync(null!, (IAppEntity)null!); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_not_return_portal_link_for_team() - { - var actual = await sut.GetPortalLinkAsync(null!, (ITeamEntity)null!); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_not_return_referral_code_for_app() - { - var actual = await sut.GetReferralInfoAsync(null!, (IAppEntity)null!); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_not_return_referral_code_for_team() - { - var actual = await sut.GetReferralInfoAsync(null!, (ITeamEntity)null!); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_do_nothing_if_checking_for_redirect_for_app() - { - var actual = await sut.MustRedirectToPortalAsync(null!, (IAppEntity)null!, null); - - Assert.Null(actual); - } - - [Fact] - public async Task Should_do_nothing_if_checking_for_redirect_for_team() - { - var actual = await sut.MustRedirectToPortalAsync(null!, (ITeamEntity)null!, null); - - Assert.Null(actual); - } + var actual = await sut.GetPortalLinkAsync(null!, (ITeamEntity)null!); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_not_return_referral_code_for_app() + { + var actual = await sut.GetReferralInfoAsync(null!, (IAppEntity)null!); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_not_return_referral_code_for_team() + { + var actual = await sut.GetReferralInfoAsync(null!, (ITeamEntity)null!); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_do_nothing_if_checking_for_redirect_for_app() + { + var actual = await sut.MustRedirectToPortalAsync(null!, (IAppEntity)null!, null); + + Assert.Null(actual); + } + + [Fact] + public async Task Should_do_nothing_if_checking_for_redirect_for_team() + { + var actual = await sut.MustRedirectToPortalAsync(null!, (ITeamEntity)null!, null); + + Assert.Null(actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs index 280c003715..02dfaa6ed3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs @@ -18,424 +18,423 @@ using Squidex.Messaging; using Xunit; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public class UsageGateTests { - public class UsageGateTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IMessageBus messaging = A.Fake<IMessageBus>(); + private readonly IApiUsageTracker apiUsageTracker = A.Fake<IApiUsageTracker>(); + private readonly IAppEntity appWithoutTeam; + private readonly IAppEntity appWithTeam; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); + private readonly IUsageTracker usageTracker = A.Fake<IUsageTracker>(); + private readonly string clientId = Guid.NewGuid().ToString(); + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly DateTime today = new DateTime(2020, 10, 3); + private readonly Plan planFree = new Plan { Id = "free" }; + private readonly Plan planPaid = new Plan { Id = "paid" }; + private readonly UsageGate sut; + + public UsageGateTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IMessageBus messaging = A.Fake<IMessageBus>(); - private readonly IApiUsageTracker apiUsageTracker = A.Fake<IApiUsageTracker>(); - private readonly IAppEntity appWithoutTeam; - private readonly IAppEntity appWithTeam; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); - private readonly IUsageTracker usageTracker = A.Fake<IUsageTracker>(); - private readonly string clientId = Guid.NewGuid().ToString(); - private readonly DomainId teamId = DomainId.NewGuid(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly DateTime today = new DateTime(2020, 10, 3); - private readonly Plan planFree = new Plan { Id = "free" }; - private readonly Plan planPaid = new Plan { Id = "paid" }; - private readonly UsageGate sut; - - public UsageGateTests() - { - appWithoutTeam = Mocks.App(appId); - appWithTeam = Mocks.App(appId); - - ct = cts.Token; + appWithoutTeam = Mocks.App(appId); + appWithTeam = Mocks.App(appId); - A.CallTo(() => appWithTeam.TeamId) - .Returns(teamId); + ct = cts.Token; - A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) - .ReturnsLazily(x => (planFree, planFree.Id)); + A.CallTo(() => appWithTeam.TeamId) + .Returns(teamId); - A.CallTo(() => billingPlans.GetActualPlan(planPaid.Id)) - .ReturnsLazily(x => (planPaid, planPaid.Id)); + A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) + .ReturnsLazily(x => (planFree, planFree.Id)); - A.CallTo(() => usageTracker.FallbackCategory) - .Returns("*"); + A.CallTo(() => billingPlans.GetActualPlan(planPaid.Id)) + .ReturnsLazily(x => (planPaid, planPaid.Id)); - sut = new UsageGate(appProvider, apiUsageTracker, billingPlans, messaging, usageTracker); - } + A.CallTo(() => usageTracker.FallbackCategory) + .Returns("*"); - [Fact] - public async Task Should_delete_app_asset_usage() - { - await sut.DeleteAssetUsageAsync(appId.Id, ct); + sut = new UsageGate(appProvider, apiUsageTracker, billingPlans, messaging, usageTracker); + } - A.CallTo(() => usageTracker.DeleteAsync($"{appId.Id}_Assets", ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_delete_app_asset_usage() + { + await sut.DeleteAssetUsageAsync(appId.Id, ct); - [Fact] - public async Task Should_delete_assets_usage() - { - await sut.DeleteAssetsUsageAsync(ct); + A.CallTo(() => usageTracker.DeleteAsync($"{appId.Id}_Assets", ct)) + .MustHaveHappened(); + } - A.CallTo(() => usageTracker.DeleteByKeyPatternAsync("^([a-zA-Z0-9]+)_Assets", ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_delete_assets_usage() + { + await sut.DeleteAssetsUsageAsync(ct); - [Fact] - public async Task Should_get_free_plan_for_app() - { - var plan = await sut.GetPlanForAppAsync(appWithoutTeam, ct); + A.CallTo(() => usageTracker.DeleteByKeyPatternAsync("^([a-zA-Z0-9]+)_Assets", ct)) + .MustHaveHappened(); + } - Assert.Equal((planFree, planFree.Id, null), plan); - } + [Fact] + public async Task Should_get_free_plan_for_app() + { + var plan = await sut.GetPlanForAppAsync(appWithoutTeam, ct); - [Fact] - public async Task Should_get_free_plan_for_app_with_team() - { - var team = A.Fake<ITeamEntity>(); + Assert.Equal((planFree, planFree.Id, null), plan); + } - A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) - .Returns(team); + [Fact] + public async Task Should_get_free_plan_for_app_with_team() + { + var team = A.Fake<ITeamEntity>(); - A.CallTo(() => team.Id) - .Returns(teamId); + A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) + .Returns(team); - var plan = await sut.GetPlanForAppAsync(appWithTeam, ct); + A.CallTo(() => team.Id) + .Returns(teamId); - Assert.Equal((planFree, planFree.Id, teamId), plan); - } + var plan = await sut.GetPlanForAppAsync(appWithTeam, ct); - [Fact] - public async Task Should_get_paid_plan_for_app() - { - A.CallTo(() => appWithoutTeam.Plan) - .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); + Assert.Equal((planFree, planFree.Id, teamId), plan); + } - var plan = await sut.GetPlanForAppAsync(appWithoutTeam, ct); + [Fact] + public async Task Should_get_paid_plan_for_app() + { + A.CallTo(() => appWithoutTeam.Plan) + .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); - Assert.Equal((planPaid, planPaid.Id, null), plan); - } + var plan = await sut.GetPlanForAppAsync(appWithoutTeam, ct); - [Fact] - public async Task Should_get_paid_plan_for_app_id() - { - A.CallTo(() => appProvider.GetAppAsync(appWithoutTeam.Id, true, ct)) - .Returns(appWithoutTeam); + Assert.Equal((planPaid, planPaid.Id, null), plan); + } - A.CallTo(() => appWithoutTeam.Plan) - .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); + [Fact] + public async Task Should_get_paid_plan_for_app_id() + { + A.CallTo(() => appProvider.GetAppAsync(appWithoutTeam.Id, true, ct)) + .Returns(appWithoutTeam); - var plan = await sut.GetPlanForAppAsync(appWithoutTeam.Id, ct); + A.CallTo(() => appWithoutTeam.Plan) + .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); - Assert.Equal((planPaid, planPaid.Id, null), plan); - } + var plan = await sut.GetPlanForAppAsync(appWithoutTeam.Id, ct); - [Fact] - public async Task Should_get_paid_plan_for_app_with_team() - { - var team = A.Fake<ITeamEntity>(); + Assert.Equal((planPaid, planPaid.Id, null), plan); + } - A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) - .Returns(team); + [Fact] + public async Task Should_get_paid_plan_for_app_with_team() + { + var team = A.Fake<ITeamEntity>(); - A.CallTo(() => team.Id) - .Returns(teamId); + A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) + .Returns(team); - A.CallTo(() => team.Plan) - .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); + A.CallTo(() => team.Id) + .Returns(teamId); - var plan = await sut.GetPlanForAppAsync(appWithTeam, ct); + A.CallTo(() => team.Plan) + .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); - Assert.Equal((planPaid, planPaid.Id, teamId), plan); - } + var plan = await sut.GetPlanForAppAsync(appWithTeam, ct); - [Fact] - public async Task Should_block_with_true_if_over_client_limit() - { - var plan = new Plan { Id = "custom", BlockingApiCalls = 1600, MaxApiCalls = 1600 }; + Assert.Equal((planPaid, planPaid.Id, teamId), plan); + } - A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) - .ReturnsLazily(x => (plan, plan.Id)); + [Fact] + public async Task Should_block_with_true_if_over_client_limit() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 1600, MaxApiCalls = 1600 }; - A.CallTo(() => appWithoutTeam.Clients) - .Returns(AppClients.Empty.Add(clientId, clientId).Update(clientId, apiCallsLimit: 1000)); + A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) + .ReturnsLazily(x => (plan, plan.Id)); - A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) - .Returns(1000); + A.CallTo(() => appWithoutTeam.Clients) + .Returns(AppClients.Empty.Add(clientId, clientId).Update(clientId, apiCallsLimit: 1000)); - var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) + .Returns(1000); - Assert.True(isBlocked); + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); - A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) - .MustHaveHappened(); - } + Assert.True(isBlocked); - [Fact] - public async Task Should_return_true_if_over_blocking_limit() - { - var plan = new Plan { Id = "custom", BlockingApiCalls = 600, MaxApiCalls = 600 }; + A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) + .MustHaveHappened(); + } - A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) - .ReturnsLazily(x => (plan, plan.Id)); + [Fact] + public async Task Should_return_true_if_over_blocking_limit() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 600, MaxApiCalls = 600 }; - A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) - .Returns(1000); + A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) + .ReturnsLazily(x => (plan, plan.Id)); - var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) + .Returns(1000); - Assert.True(isBlocked); + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); - A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) - .MustHaveHappened(); - } + Assert.True(isBlocked); - [Fact] - public async Task Should_return_false_if_below_blocking_limit() - { - var plan = new Plan { Id = "custom", BlockingApiCalls = 1600, MaxApiCalls = 1600 }; + A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) + .MustHaveHappened(); + } - A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) - .ReturnsLazily(x => (plan, plan.Id)); + [Fact] + public async Task Should_return_false_if_below_blocking_limit() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 1600, MaxApiCalls = 1600 }; - A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) - .Returns(100); + A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) + .ReturnsLazily(x => (plan, plan.Id)); - var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) + .Returns(100); - Assert.False(isBlocked); + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); - A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + Assert.False(isBlocked); - [Fact] - public async Task Should_return_false_and_notify_if_about_to_over_included_contingent() - { - var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; + A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) - .ReturnsLazily(x => (plan, plan.Id)); + [Fact] + public async Task Should_return_false_and_notify_if_about_to_over_included_contingent() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; - A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) - .Returns(1200); // in 10 days = 4000 / month + A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) + .ReturnsLazily(x => (plan, plan.Id)); - var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) + .Returns(1200); // in 10 days = 4000 / month - Assert.False(isBlocked); + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); - A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) - .MustHaveHappened(); - } + Assert.False(isBlocked); - [Fact] - public async Task Should_return_false_and_notify_if_about_to_over_included_contingent_but_no_max_given() - { - var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 0 }; + A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) + .MustHaveHappened(); + } - A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) - .ReturnsLazily(x => (plan, plan.Id)); + [Fact] + public async Task Should_return_false_and_notify_if_about_to_over_included_contingent_but_no_max_given() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 0 }; - A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) - .Returns(1200); // in 10 days = 4000 / month + A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) + .ReturnsLazily(x => (plan, plan.Id)); - var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) + .Returns(1200); // in 10 days = 4000 / month - Assert.False(isBlocked); + var isBlocked = await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); - A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) - .MustHaveHappened(); - } + Assert.False(isBlocked); - [Fact] - public async Task Should_only_notify_once_if_about_to_be_over_included_contingent() - { - var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; + A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) + .MustHaveHappened(); + } - A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) - .ReturnsLazily(x => (plan, plan.Id)); + [Fact] + public async Task Should_only_notify_once_if_about_to_be_over_included_contingent() + { + var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; - A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) - .Returns(1200); // in 10 days = 4000 / month + A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) + .ReturnsLazily(x => (plan, plan.Id)); - await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); - await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A<string>._, ct)) + .Returns(1200); // in 10 days = 4000 / month - A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) - .MustHaveHappenedOnceExactly(); - } + await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); + await sut.IsBlockedAsync(appWithoutTeam, clientId, today, ct); - [Fact] - public async Task Should_not_notify_if_lower_than_10_percent() - { - var now = new DateTime(2020, 10, 2); + A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, ct)) + .MustHaveHappenedOnceExactly(); + } - var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; + [Fact] + public async Task Should_not_notify_if_lower_than_10_percent() + { + var now = new DateTime(2020, 10, 2); - A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) - .ReturnsLazily(x => (plan, plan.Id)); + var plan = new Plan { Id = "custom", BlockingApiCalls = 5000, MaxApiCalls = 3000 }; - A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), now, A<string>._, ct)) - .Returns(220); // in 3 days = 3300 / month + A.CallTo(() => billingPlans.GetActualPlan(A<string>._)) + .ReturnsLazily(x => (plan, plan.Id)); - await sut.IsBlockedAsync(appWithoutTeam, clientId, now, ct); - await sut.IsBlockedAsync(appWithoutTeam, clientId, now, ct); + A.CallTo(() => apiUsageTracker.GetMonthCallsAsync(appId.Id.ToString(), now, A<string>._, ct)) + .Returns(220); // in 3 days = 3300 / month - A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + await sut.IsBlockedAsync(appWithoutTeam, clientId, now, ct); + await sut.IsBlockedAsync(appWithoutTeam, clientId, now, ct); - [Fact] - public async Task Should_get_app_asset_total_size_from_summary_date() - { - A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_Assets", default, default, null, ct)) - .Returns(new Counters { ["TotalSize"] = 2048 }); + A.CallTo(() => messaging.PublishAsync(A<UsageTrackingCheck>._, null, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - var size = await sut.GetTotalSizeByAppAsync(appId.Id, ct); + [Fact] + public async Task Should_get_app_asset_total_size_from_summary_date() + { + A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_Assets", default, default, null, ct)) + .Returns(new Counters { ["TotalSize"] = 2048 }); - Assert.Equal(2048, size); - } + var size = await sut.GetTotalSizeByAppAsync(appId.Id, ct); - [Fact] - public async Task Should_get_team_asset_total_size_from_summary_date() - { - A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_TeamAssets", default, default, null, ct)) - .Returns(new Counters { ["TotalSize"] = 2048 }); + Assert.Equal(2048, size); + } - var size = await sut.GetTotalSizeByTeamAsync(appId.Id, ct); + [Fact] + public async Task Should_get_team_asset_total_size_from_summary_date() + { + A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_TeamAssets", default, default, null, ct)) + .Returns(new Counters { ["TotalSize"] = 2048 }); - Assert.Equal(2048, size); - } + var size = await sut.GetTotalSizeByTeamAsync(appId.Id, ct); - [Fact] - public async Task Should_track_request_async() - { - await sut.TrackRequestAsync(appWithoutTeam, "client", today, 42, 50, 512, ct); + Assert.Equal(2048, size); + } - A.CallTo(() => apiUsageTracker.TrackAsync(today, appWithoutTeam.Id.ToString(), "client", 42, 50, 512, ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_track_request_async() + { + await sut.TrackRequestAsync(appWithoutTeam, "client", today, 42, 50, 512, ct); - [Fact] - public async Task Should_track_request_for_team_async() - { - await sut.TrackRequestAsync(appWithTeam, "client", today, 42, 50, 512, ct); + A.CallTo(() => apiUsageTracker.TrackAsync(today, appWithoutTeam.Id.ToString(), "client", 42, 50, 512, ct)) + .MustHaveHappened(); + } - A.CallTo(() => apiUsageTracker.TrackAsync(today, appWithTeam.TeamId!.ToString()!, appWithTeam.Name, 42, 50, 512, ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_track_request_for_team_async() + { + await sut.TrackRequestAsync(appWithTeam, "client", today, 42, 50, 512, ct); - [Fact] - public async Task Should_get_app_asset_counters_from_categories() - { - SetupAssetQuery($"{appId.Id}_Assets"); + A.CallTo(() => apiUsageTracker.TrackAsync(today, appWithTeam.TeamId!.ToString()!, appWithTeam.Name, 42, 50, 512, ct)) + .MustHaveHappened(); + } - var actual = await sut.QueryByAppAsync(appId.Id, today, today.AddDays(3), ct); + [Fact] + public async Task Should_get_app_asset_counters_from_categories() + { + SetupAssetQuery($"{appId.Id}_Assets"); - actual.Should().BeEquivalentTo(new List<AssetStats> - { - new AssetStats(today.AddDays(0), 2, 128), - new AssetStats(today.AddDays(1), 3, 256), - new AssetStats(today.AddDays(2), 4, 512) - }); - } + var actual = await sut.QueryByAppAsync(appId.Id, today, today.AddDays(3), ct); - [Fact] - public async Task Should_get_team_asset_counters_from_categories() + actual.Should().BeEquivalentTo(new List<AssetStats> { - SetupAssetQuery($"{appId.Id}_TeamAssets"); + new AssetStats(today.AddDays(0), 2, 128), + new AssetStats(today.AddDays(1), 3, 256), + new AssetStats(today.AddDays(2), 4, 512) + }); + } - var actual = await sut.QueryByTeamAsync(appId.Id, today, today.AddDays(3), ct); + [Fact] + public async Task Should_get_team_asset_counters_from_categories() + { + SetupAssetQuery($"{appId.Id}_TeamAssets"); - actual.Should().BeEquivalentTo(new List<AssetStats> - { - new AssetStats(today.AddDays(0), 2, 128), - new AssetStats(today.AddDays(1), 3, 256), - new AssetStats(today.AddDays(2), 4, 512) - }); - } + var actual = await sut.QueryByTeamAsync(appId.Id, today, today.AddDays(3), ct); - private void SetupAssetQuery(string key) + actual.Should().BeEquivalentTo(new List<AssetStats> { - A.CallTo(() => usageTracker.QueryAsync(key, today, today.AddDays(3), ct)) - .Returns(new Dictionary<string, List<(DateTime, Counters)>> + new AssetStats(today.AddDays(0), 2, 128), + new AssetStats(today.AddDays(1), 3, 256), + new AssetStats(today.AddDays(2), 4, 512) + }); + } + + private void SetupAssetQuery(string key) + { + A.CallTo(() => usageTracker.QueryAsync(key, today, today.AddDays(3), ct)) + .Returns(new Dictionary<string, List<(DateTime, Counters)>> + { + [usageTracker.FallbackCategory] = new List<(DateTime, Counters)> { - [usageTracker.FallbackCategory] = new List<(DateTime, Counters)> + (today.AddDays(0), new Counters { - (today.AddDays(0), new Counters - { - ["TotalSize"] = 128, - ["TotalAssets"] = 2 - }), - (today.AddDays(1), new Counters - { - ["TotalSize"] = 256, - ["TotalAssets"] = 3 - }), - (today.AddDays(2), new Counters - { - ["TotalSize"] = 512, - ["TotalAssets"] = 4 - }) - } - }); - } - - [Fact] - public async Task Should_increase_usage_for_asset_event() - { - Counters? countersSummary = null; - Counters? countersDate = null; - - A.CallTo(() => usageTracker.TrackAsync(default, $"{appId.Id}_Assets", null, A<Counters>._, ct)) - .Invokes(x => countersSummary = x.GetArgument<Counters>(3)); + ["TotalSize"] = 128, + ["TotalAssets"] = 2 + }), + (today.AddDays(1), new Counters + { + ["TotalSize"] = 256, + ["TotalAssets"] = 3 + }), + (today.AddDays(2), new Counters + { + ["TotalSize"] = 512, + ["TotalAssets"] = 4 + }) + } + }); + } - A.CallTo(() => usageTracker.TrackAsync(today, $"{appId.Id}_Assets", null, A<Counters>._, ct)) - .Invokes(x => countersDate = x.GetArgument<Counters>(3)); + [Fact] + public async Task Should_increase_usage_for_asset_event() + { + Counters? countersSummary = null; + Counters? countersDate = null; - await sut.TrackAssetAsync(appWithoutTeam.Id, today, 512, 3, ct); + A.CallTo(() => usageTracker.TrackAsync(default, $"{appId.Id}_Assets", null, A<Counters>._, ct)) + .Invokes(x => countersSummary = x.GetArgument<Counters>(3)); - var expected = new Counters - { - ["TotalSize"] = 512, - ["TotalAssets"] = 3 - }; + A.CallTo(() => usageTracker.TrackAsync(today, $"{appId.Id}_Assets", null, A<Counters>._, ct)) + .Invokes(x => countersDate = x.GetArgument<Counters>(3)); - countersSummary.Should().BeEquivalentTo(expected); - countersDate.Should().BeEquivalentTo(expected); - } + await sut.TrackAssetAsync(appWithoutTeam.Id, today, 512, 3, ct); - [Fact] - public async Task Should_increase_team_usage_for_asset_event_and_team_app() + var expected = new Counters { - Counters? countersSummary = null; - Counters? countersDate = null; + ["TotalSize"] = 512, + ["TotalAssets"] = 3 + }; + + countersSummary.Should().BeEquivalentTo(expected); + countersDate.Should().BeEquivalentTo(expected); + } - var team = A.Fake<ITeamEntity>(); + [Fact] + public async Task Should_increase_team_usage_for_asset_event_and_team_app() + { + Counters? countersSummary = null; + Counters? countersDate = null; - A.CallTo(() => team.Id) - .Returns(teamId); + var team = A.Fake<ITeamEntity>(); - A.CallTo(() => appProvider.GetAppAsync(appWithTeam.Id, true, ct)) - .Returns(appWithTeam); + A.CallTo(() => team.Id) + .Returns(teamId); - A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) - .Returns(team); + A.CallTo(() => appProvider.GetAppAsync(appWithTeam.Id, true, ct)) + .Returns(appWithTeam); - A.CallTo(() => usageTracker.TrackAsync(default, $"{teamId}_TeamAssets", null, A<Counters>._, ct)) - .Invokes(x => countersSummary = x.GetArgument<Counters>(3)); + A.CallTo(() => appProvider.GetTeamAsync(teamId, ct)) + .Returns(team); - A.CallTo(() => usageTracker.TrackAsync(today, $"{teamId}_TeamAssets", null, A<Counters>._, ct)) - .Invokes(x => countersDate = x.GetArgument<Counters>(3)); + A.CallTo(() => usageTracker.TrackAsync(default, $"{teamId}_TeamAssets", null, A<Counters>._, ct)) + .Invokes(x => countersSummary = x.GetArgument<Counters>(3)); - await sut.TrackAssetAsync(appWithTeam.Id, today, 512, 3, ct); + A.CallTo(() => usageTracker.TrackAsync(today, $"{teamId}_TeamAssets", null, A<Counters>._, ct)) + .Invokes(x => countersDate = x.GetArgument<Counters>(3)); - var expected = new Counters - { - ["TotalSize"] = 512, - ["TotalAssets"] = 3 - }; + await sut.TrackAssetAsync(appWithTeam.Id, today, 512, 3, ct); + + var expected = new Counters + { + ["TotalSize"] = 512, + ["TotalAssets"] = 3 + }; - countersSummary.Should().BeEquivalentTo(expected); - countersDate.Should().BeEquivalentTo(expected); - } + countersSummary.Should().BeEquivalentTo(expected); + countersDate.Should().BeEquivalentTo(expected); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs index 9a6f9ab297..58c5923e05 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs @@ -16,181 +16,180 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Billing +namespace Squidex.Domain.Apps.Entities.Billing; + +public class UsageNotifierWorkerTest { - public class UsageNotifierWorkerTest + private readonly TestState<UsageNotifierWorker.State> state = new TestState<UsageNotifierWorker.State>("Default"); + private readonly IClock clock = A.Fake<IClock>(); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IUserNotifications notificationSender = A.Fake<IUserNotifications>(); + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + private readonly UsageNotifierWorker sut; + private Instant time = SystemClock.Instance.GetCurrentInstant(); + + public UsageNotifierWorkerTest() { - private readonly TestState<UsageNotifierWorker.State> state = new TestState<UsageNotifierWorker.State>("Default"); - private readonly IClock clock = A.Fake<IClock>(); - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IUserNotifications notificationSender = A.Fake<IUserNotifications>(); - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - private readonly UsageNotifierWorker sut; - private Instant time = SystemClock.Instance.GetCurrentInstant(); - - public UsageNotifierWorkerTest() - { - A.CallTo(() => appProvider.GetAppAsync(app.Id, true, default)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(app.Id, true, default)) + .Returns(app); - A.CallTo(() => clock.GetCurrentInstant()) - .ReturnsLazily(() => time); + A.CallTo(() => clock.GetCurrentInstant()) + .ReturnsLazily(() => time); - A.CallTo(() => notificationSender.IsActive) - .Returns(true); - - sut = new UsageNotifierWorker(state.PersistenceFactory, appProvider, notificationSender, userResolver) - { - Clock = clock - }; - } + A.CallTo(() => notificationSender.IsActive) + .Returns(true); - [Fact] - public async Task Should_load_on_initialize() + sut = new UsageNotifierWorker(state.PersistenceFactory, appProvider, notificationSender, userResolver) { - await sut.InitializeAsync(default); + Clock = clock + }; + } - A.CallTo(() => state.Persistence.ReadAsync(EtagVersion.Any, default)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_load_on_initialize() + { + await sut.InitializeAsync(default); - [Fact] - public async Task Should_not_send_notification_if_not_active() + A.CallTo(() => state.Persistence.ReadAsync(EtagVersion.Any, default)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_send_notification_if_not_active() + { + SetupUser("1", null); + SetupUser("2", null); + + var message = new UsageTrackingCheck { - SetupUser("1", null); - SetupUser("2", null); + AppId = app.Id, + Usage = 1000, + UsageLimit = 3000, + Users = new[] { "1", "2" } + }; - var message = new UsageTrackingCheck - { - AppId = app.Id, - Usage = 1000, - UsageLimit = 3000, - Users = new[] { "1", "2" } - }; + await sut.HandleAsync(message, default); - await sut.HandleAsync(message, default); + A.CallTo(() => notificationSender.SendUsageAsync(A<IUser>._, A<IAppEntity>._, A<long>._, A<long>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => notificationSender.SendUsageAsync(A<IUser>._, A<IAppEntity>._, A<long>._, A<long>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_notify_found_users() + { + var user1 = SetupUser("1", "user1@email.com"); + var user2 = SetupUser("2", "user2@email.com"); + var user3 = SetupUser("3", null); - [Fact] - public async Task Should_notify_found_users() + var message = new UsageTrackingCheck { - var user1 = SetupUser("1", "user1@email.com"); - var user2 = SetupUser("2", "user2@email.com"); - var user3 = SetupUser("3", null); + AppId = app.Id, + Usage = 1000, + UsageLimit = 3000, + Users = new[] { "1", "2", "3" } + }; - var message = new UsageTrackingCheck - { - AppId = app.Id, - Usage = 1000, - UsageLimit = 3000, - Users = new[] { "1", "2", "3" } - }; + await sut.HandleAsync(message, default); - await sut.HandleAsync(message, default); + A.CallTo(() => notificationSender.SendUsageAsync(user1!, app, 1000, 3000, default)) + .MustHaveHappened(); - A.CallTo(() => notificationSender.SendUsageAsync(user1!, app, 1000, 3000, default)) - .MustHaveHappened(); + A.CallTo(() => notificationSender.SendUsageAsync(user2!, app, 1000, 3000, default)) + .MustHaveHappened(); - A.CallTo(() => notificationSender.SendUsageAsync(user2!, app, 1000, 3000, default)) - .MustHaveHappened(); + A.CallTo(() => notificationSender.SendUsageAsync(user3!, app, 1000, 3000, default)) + .MustNotHaveHappened(); + } - A.CallTo(() => notificationSender.SendUsageAsync(user3!, app, 1000, 3000, default)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_not_notify_again() + { + var user = SetupUser("1", "user1@email.com"); - [Fact] - public async Task Should_not_notify_again() + var message = new UsageTrackingCheck { - var user = SetupUser("1", "user1@email.com"); + AppId = app.Id, + Usage = 1000, + UsageLimit = 3000, + Users = new[] { "1" } + }; - var message = new UsageTrackingCheck - { - AppId = app.Id, - Usage = 1000, - UsageLimit = 3000, - Users = new[] { "1" } - }; + await sut.HandleAsync(message, default); + await sut.HandleAsync(message, default); - await sut.HandleAsync(message, default); - await sut.HandleAsync(message, default); + A.CallTo(() => notificationSender.SendUsageAsync(user!, app, 1000, 3000, default)) + .MustHaveHappenedOnceExactly(); + } - A.CallTo(() => notificationSender.SendUsageAsync(user!, app, 1000, 3000, default)) - .MustHaveHappenedOnceExactly(); - } + [Fact] + public async Task Should_send_again_after_3_days() + { + var user = SetupUser("1", "user1@email.com"); - [Fact] - public async Task Should_send_again_after_3_days() + var message = new UsageTrackingCheck { - var user = SetupUser("1", "user1@email.com"); + AppId = app.Id, + Usage = 1000, + UsageLimit = 3000, + Users = new[] { "1" } + }; - var message = new UsageTrackingCheck - { - AppId = app.Id, - Usage = 1000, - UsageLimit = 3000, - Users = new[] { "1" } - }; + await sut.HandleAsync(message, default); - await sut.HandleAsync(message, default); + time = time.Plus(Duration.FromDays(3)); - time = time.Plus(Duration.FromDays(3)); + await sut.HandleAsync(message, default); - await sut.HandleAsync(message, default); + A.CallTo(() => notificationSender.SendUsageAsync(user!, app, 1000, 3000, default)) + .MustHaveHappenedTwiceExactly(); + } - A.CallTo(() => notificationSender.SendUsageAsync(user!, app, 1000, 3000, default)) - .MustHaveHappenedTwiceExactly(); - } + [Theory] + [InlineData(1)] + [InlineData(6)] + [InlineData(12)] + [InlineData(24)] + [InlineData(48)] + public async Task Should_not_notify_again_after_few_hours(int hours) + { + var user = SetupUser("1", "user1@email.com"); - [Theory] - [InlineData(1)] - [InlineData(6)] - [InlineData(12)] - [InlineData(24)] - [InlineData(48)] - public async Task Should_not_notify_again_after_few_hours(int hours) + var message = new UsageTrackingCheck { - var user = SetupUser("1", "user1@email.com"); + AppId = app.Id, + Usage = 1000, + UsageLimit = 3000, + Users = new[] { "1" } + }; - var message = new UsageTrackingCheck - { - AppId = app.Id, - Usage = 1000, - UsageLimit = 3000, - Users = new[] { "1" } - }; + await sut.HandleAsync(message, default); - await sut.HandleAsync(message, default); + time = time.Plus(Duration.FromHours(hours)); - time = time.Plus(Duration.FromHours(hours)); + await sut.HandleAsync(message, default); - await sut.HandleAsync(message, default); + A.CallTo(() => notificationSender.SendUsageAsync(user!, app, 1000, 3000, default)) + .MustHaveHappenedOnceExactly(); + } - A.CallTo(() => notificationSender.SendUsageAsync(user!, app, 1000, 3000, default)) - .MustHaveHappenedOnceExactly(); - } + private IUser? SetupUser(string id, string? email) + { + if (email != null) + { + var user = UserMocks.User(id, email); - private IUser? SetupUser(string id, string? email) + A.CallTo(() => userResolver.FindByIdOrEmailAsync(id, default)) + .Returns(user); + + return user; + } + else { - if (email != null) - { - var user = UserMocks.User(id, email); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(id, default)) - .Returns(user); - - return user; - } - else - { - A.CallTo(() => userResolver.FindByIdOrEmailAsync(id, default)) - .Returns(Task.FromResult<IUser?>(null)); - - return null; - } + A.CallTo(() => userResolver.FindByIdOrEmailAsync(id, default)) + .Returns(Task.FromResult<IUser?>(null)); + + return null; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs index 1fe44808b3..181118872c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs @@ -22,297 +22,296 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public class CommentTriggerHandlerTests { - public class CommentTriggerHandlerTests - { - private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly IRuleTriggerHandler sut; + private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly IRuleTriggerHandler sut; - public CommentTriggerHandlerTests() - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true", default)) - .Returns(true); + public CommentTriggerHandlerTests() + { + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true", default)) + .Returns(true); - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false", default)) - .Returns(false); + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false", default)) + .Returns(false); - sut = new CommentTriggerHandler(scriptEngine, userResolver); - } + sut = new CommentTriggerHandler(scriptEngine, userResolver); + } - [Fact] - public void Should_return_false_if_asking_for_snapshot_support() - { - Assert.False(sut.CanCreateSnapshotEvents); - } + [Fact] + public void Should_return_false_if_asking_for_snapshot_support() + { + Assert.False(sut.CanCreateSnapshotEvents); + } - [Fact] - public void Should_handle_comment_event() - { - Assert.True(sut.Handles(new CommentCreated())); - } + [Fact] + public void Should_handle_comment_event() + { + Assert.True(sut.Handles(new CommentCreated())); + } - [Fact] - public void Should_not_handle_comment_update_event() - { - Assert.False(sut.Handles(new CommentUpdated())); - } + [Fact] + public void Should_not_handle_comment_update_event() + { + Assert.False(sut.Handles(new CommentUpdated())); + } - [Fact] - public void Should_not_handle_other_event() - { - Assert.False(sut.Handles(new ContentCreated())); - } + [Fact] + public void Should_not_handle_other_event() + { + Assert.False(sut.Handles(new ContentCreated())); + } - [Fact] - public async Task Should_create_enriched_events() - { - var ctx = Context(); + [Fact] + public async Task Should_create_enriched_events() + { + var ctx = Context(); - var user1 = UserMocks.User("1"); - var user2 = UserMocks.User("2"); + var user1 = UserMocks.User("1"); + var user2 = UserMocks.User("2"); - var users = new List<IUser> { user1, user2 }; - var userIds = users.Select(x => x.Id).ToArray(); + var users = new List<IUser> { user1, user2 }; + var userIds = users.Select(x => x.Id).ToArray(); - var @event = new CommentCreated { Mentions = userIds }; - var envelope = Envelope.Create<AppEvent>(@event); + var @event = new CommentCreated { Mentions = userIds }; + var envelope = Envelope.Create<AppEvent>(@event); - A.CallTo(() => userResolver.QueryManyAsync(userIds, default)) - .Returns(users.ToDictionary(x => x.Id)); + A.CallTo(() => userResolver.QueryManyAsync(userIds, default)) + .Returns(users.ToDictionary(x => x.Id)); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - Assert.Equal(2, actual.Count); + Assert.Equal(2, actual.Count); - var enrichedEvent1 = actual[0] as EnrichedCommentEvent; - var enrichedEvent2 = actual[1] as EnrichedCommentEvent; + var enrichedEvent1 = actual[0] as EnrichedCommentEvent; + var enrichedEvent2 = actual[1] as EnrichedCommentEvent; - Assert.Equal(user1, enrichedEvent1!.MentionedUser); - Assert.Equal(user2, enrichedEvent2!.MentionedUser); - Assert.Equal("UserMentioned", enrichedEvent1.Name); - Assert.Equal("UserMentioned", enrichedEvent2.Name); - } + Assert.Equal(user1, enrichedEvent1!.MentionedUser); + Assert.Equal(user2, enrichedEvent2!.MentionedUser); + Assert.Equal("UserMentioned", enrichedEvent1.Name); + Assert.Equal("UserMentioned", enrichedEvent2.Name); + } - [Fact] - public async Task Should_not_create_enriched_events_if_users_cannot_be_resolved() - { - var ctx = Context(); + [Fact] + public async Task Should_not_create_enriched_events_if_users_cannot_be_resolved() + { + var ctx = Context(); - var user1 = UserMocks.User("1"); - var user2 = UserMocks.User("2"); + var user1 = UserMocks.User("1"); + var user2 = UserMocks.User("2"); - var users = new List<IUser> { user1, user2 }; - var userIds = users.Select(x => x.Id).ToArray(); + var users = new List<IUser> { user1, user2 }; + var userIds = users.Select(x => x.Id).ToArray(); - var @event = new CommentCreated { Mentions = userIds }; - var envelope = Envelope.Create<AppEvent>(@event); + var @event = new CommentCreated { Mentions = userIds }; + var envelope = Envelope.Create<AppEvent>(@event); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_not_create_enriched_events_if_mentions_is_null() - { - var ctx = Context(); + [Fact] + public async Task Should_not_create_enriched_events_if_mentions_is_null() + { + var ctx = Context(); - var @event = new CommentCreated { Mentions = null }; - var envelope = Envelope.Create<AppEvent>(@event); + var @event = new CommentCreated { Mentions = null }; + var envelope = Envelope.Create<AppEvent>(@event); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - Assert.Empty(actual); + Assert.Empty(actual); - A.CallTo(() => userResolver.QueryManyAsync(A<string[]>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.QueryManyAsync(A<string[]>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_create_enriched_events_if_mentions_is_empty() - { - var ctx = Context(); + [Fact] + public async Task Should_not_create_enriched_events_if_mentions_is_empty() + { + var ctx = Context(); - var @event = new CommentCreated { Mentions = Array.Empty<string>() }; - var envelope = Envelope.Create<AppEvent>(@event); + var @event = new CommentCreated { Mentions = Array.Empty<string>() }; + var envelope = Envelope.Create<AppEvent>(@event); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - Assert.Empty(actual); + Assert.Empty(actual); - A.CallTo(() => userResolver.QueryManyAsync(A<string[]>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.QueryManyAsync(A<string[]>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public void Should_trigger_precheck_if_event_type_correct() + [Fact] + public void Should_trigger_precheck_if_event_type_correct() + { + TestForCondition(string.Empty, ctx => { - TestForCondition(string.Empty, ctx => - { - var @event = new CommentCreated(); + var @event = new CommentCreated(); - var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_check_if_condition_is_empty() + [Fact] + public void Should_trigger_check_if_condition_is_empty() + { + TestForCondition(string.Empty, ctx => { - TestForCondition(string.Empty, ctx => - { - var @event = new EnrichedCommentEvent(); + var @event = new EnrichedCommentEvent(); - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_check_if_condition_matchs() + [Fact] + public void Should_trigger_check_if_condition_matchs() + { + TestForCondition("true", ctx => { - TestForCondition("true", ctx => - { - var @event = new EnrichedCommentEvent(); + var @event = new EnrichedCommentEvent(); - var actual = sut.Trigger(new EnrichedCommentEvent(), ctx); + var actual = sut.Trigger(new EnrichedCommentEvent(), ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_not_trigger_check_if_condition_does_not_match() + [Fact] + public void Should_not_trigger_check_if_condition_does_not_match() + { + TestForCondition("false", ctx => { - TestForCondition("false", ctx => - { - var @event = new EnrichedCommentEvent(); + var @event = new EnrichedCommentEvent(); - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - [Fact] - public void Should_trigger_check_if_email_is_correct() + [Fact] + public void Should_trigger_check_if_email_is_correct() + { + TestForRealCondition("event.mentionedUser.email == '1@email.com'", (handler, ctx) => { - TestForRealCondition("event.mentionedUser.email == '1@email.com'", (handler, ctx) => + var @event = new EnrichedCommentEvent { - var @event = new EnrichedCommentEvent - { - MentionedUser = UserMocks.User("1", "1@email.com") - }; + MentionedUser = UserMocks.User("1", "1@email.com") + }; - var actual = handler.Trigger(@event, ctx); + var actual = handler.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_not_trigger_check_if_email_is_not_correct() + [Fact] + public void Should_not_trigger_check_if_email_is_not_correct() + { + TestForRealCondition("event.mentionedUser.email == 'other@squidex.io'", (handler, ctx) => { - TestForRealCondition("event.mentionedUser.email == 'other@squidex.io'", (handler, ctx) => + var @event = new EnrichedCommentEvent { - var @event = new EnrichedCommentEvent - { - MentionedUser = UserMocks.User("1", "1@email.com") - }; + MentionedUser = UserMocks.User("1", "1@email.com") + }; - var actual = handler.Trigger(@event, ctx); + var actual = handler.Trigger(@event, ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - [Fact] - public void Should_trigger_check_if_text_is_urgent() + [Fact] + public void Should_trigger_check_if_text_is_urgent() + { + TestForRealCondition("event.text.indexOf('urgent') >= 0", (handler, ctx) => { - TestForRealCondition("event.text.indexOf('urgent') >= 0", (handler, ctx) => + var @event = new EnrichedCommentEvent { - var @event = new EnrichedCommentEvent - { - Text = "very_urgent_text" - }; + Text = "very_urgent_text" + }; - var actual = handler.Trigger(@event, ctx); + var actual = handler.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_not_trigger_check_if_text_is_not_urgent() + [Fact] + public void Should_not_trigger_check_if_text_is_not_urgent() + { + TestForRealCondition("event.text.indexOf('urgent') >= 0", (handler, ctx) => { - TestForRealCondition("event.text.indexOf('urgent') >= 0", (handler, ctx) => + var @event = new EnrichedCommentEvent { - var @event = new EnrichedCommentEvent - { - Text = "just_gossip" - }; + Text = "just_gossip" + }; - var actual = handler.Trigger(@event, ctx); + var actual = handler.Trigger(@event, ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - private void TestForRealCondition(string condition, Action<IRuleTriggerHandler, RuleContext> action) + private void TestForRealCondition(string condition, Action<IRuleTriggerHandler, RuleContext> action) + { + var trigger = new CommentTrigger { - var trigger = new CommentTrigger - { - Condition = condition - }; + Condition = condition + }; - var realScriptEngine = - new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - })); + var realScriptEngine = + new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + })); - var handler = new CommentTriggerHandler(realScriptEngine, userResolver); + var handler = new CommentTriggerHandler(realScriptEngine, userResolver); - action(handler, Context(trigger)); - } + action(handler, Context(trigger)); + } - private void TestForCondition(string condition, Action<RuleContext> action) + private void TestForCondition(string condition, Action<RuleContext> action) + { + var trigger = new CommentTrigger { - var trigger = new CommentTrigger - { - Condition = condition - }; + Condition = condition + }; - action(Context(trigger)); + action(Context(trigger)); - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) - .MustHaveHappened(); - } + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) + .MustNotHaveHappened(); } - - private static RuleContext Context(RuleTrigger? trigger = null) + else { - trigger ??= new CommentTrigger(); - - return new RuleContext - { - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), - Rule = new Rule(trigger, A.Fake<RuleAction>()), - RuleId = DomainId.NewGuid() - }; + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) + .MustHaveHappened(); } } + + private static RuleContext Context(RuleTrigger? trigger = null) + { + trigger ??= new CommentTrigger(); + + return new RuleContext + { + AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + Rule = new Rule(trigger, A.Fake<RuleAction>()), + RuleId = DomainId.NewGuid() + }; + } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs index abb66f6fe7..3fd97edef9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs @@ -11,42 +11,41 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public sealed class CommentsLoaderTests { - public sealed class CommentsLoaderTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly CommentsLoader sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly CommentsLoader sut; - public CommentsLoaderTests() - { - ct = cts.Token; + public CommentsLoaderTests() + { + ct = cts.Token; - sut = new CommentsLoader(domainObjectFactory); - } + sut = new CommentsLoader(domainObjectFactory); + } - [Fact] - public async Task Should_get_comments_from_domain_object() - { - var commentsId = DomainId.NewGuid(); - var comments = new CommentsResult(); + [Fact] + public async Task Should_get_comments_from_domain_object() + { + var commentsId = DomainId.NewGuid(); + var comments = new CommentsResult(); - var domainObject = A.Fake<CommentsStream>(); + var domainObject = A.Fake<CommentsStream>(); - A.CallTo(() => domainObjectFactory.Create<CommentsStream>(commentsId)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<CommentsStream>(commentsId)) + .Returns(domainObject); - A.CallTo(() => domainObject.GetComments(11)) - .Returns(comments); + A.CallTo(() => domainObject.GetComments(11)) + .Returns(comments); - var actual = await sut.GetCommentsAsync(commentsId, 11, ct); + var actual = await sut.GetCommentsAsync(commentsId, 11, ct); - Assert.Same(comments, actual); + Assert.Same(comments, actual); - A.CallTo(() => domainObject.LoadAsync(ct)) - .MustHaveHappened(); - } + A.CallTo(() => domainObject.LoadAsync(ct)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs index ae2657b2e5..9741db9520 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs @@ -13,151 +13,150 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject +namespace Squidex.Domain.Apps.Entities.Comments.DomainObject; + +public class CommentsCommandMiddlewareTests { - public class CommentsCommandMiddlewareTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly RefToken actor = RefToken.User("me"); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly DomainId commentsId = DomainId.NewGuid(); + private readonly DomainId commentId = DomainId.NewGuid(); + private readonly CommentsCommandMiddleware sut; + + public CommentsCommandMiddlewareTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - private readonly RefToken actor = RefToken.User("me"); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly DomainId commentsId = DomainId.NewGuid(); - private readonly DomainId commentId = DomainId.NewGuid(); - private readonly CommentsCommandMiddleware sut; - - public CommentsCommandMiddlewareTests() - { - ct = cts.Token; + ct = cts.Token; - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>._, default)) - .Returns(Task.FromResult<IUser?>(null)); + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>._, default)) + .Returns(Task.FromResult<IUser?>(null)); - sut = new CommentsCommandMiddleware(domainObjectFactory, userResolver); - } + sut = new CommentsCommandMiddleware(domainObjectFactory, userResolver); + } - [Fact] - public async Task Should_invoke_domain_object_for_comments_command() - { - var command = CreateCommentsCommand(new CreateComment()); - var context = CrateCommandContext(command); + [Fact] + public async Task Should_invoke_domain_object_for_comments_command() + { + var command = CreateCommentsCommand(new CreateComment()); + var context = CrateCommandContext(command); - var domainObject = A.Fake<CommentsStream>(); + var domainObject = A.Fake<CommentsStream>(); - A.CallTo(() => domainObject.ExecuteAsync(command, ct)) - .Returns(CommandResult.Empty(commentsId, 0, 0)); + A.CallTo(() => domainObject.ExecuteAsync(command, ct)) + .Returns(CommandResult.Empty(commentsId, 0, 0)); - A.CallTo(() => domainObjectFactory.Create<CommentsStream>(commentsId)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<CommentsStream>(commentsId)) + .Returns(domainObject); - var isNextCalled = false; + var isNextCalled = false; - await sut.HandleAsync(context, (c, ct) => - { - isNextCalled = true; + await sut.HandleAsync(context, (c, ct) => + { + isNextCalled = true; - return Task.CompletedTask; - }, ct); + return Task.CompletedTask; + }, ct); - Assert.True(isNextCalled); - } + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_enrich_with_mentioned_user_ids_if_found() + [Fact] + public async Task Should_enrich_with_mentioned_user_ids_if_found() + { + SetupUser("id1", "mail1@squidex.io"); + SetupUser("id2", "mail2@squidex.io"); + + var command = CreateCommentsCommand(new CreateComment { - SetupUser("id1", "mail1@squidex.io"); - SetupUser("id2", "mail2@squidex.io"); + Text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io", + IsMention = false + }); - var command = CreateCommentsCommand(new CreateComment - { - Text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io", - IsMention = false - }); + var context = CrateCommandContext(command); - var context = CrateCommandContext(command); + await sut.HandleAsync(context, ct); - await sut.HandleAsync(context, ct); + Assert.Equal(command.Mentions, new[] { "id1", "id2" }); + } - Assert.Equal(command.Mentions, new[] { "id1", "id2" }); - } + [Fact] + public async Task Should_not_invoke_commands_for_mentioned_users() + { + SetupUser("id1", "mail1@squidex.io"); + SetupUser("id2", "mail2@squidex.io"); - [Fact] - public async Task Should_not_invoke_commands_for_mentioned_users() + var command = CreateCommentsCommand(new CreateComment { - SetupUser("id1", "mail1@squidex.io"); - SetupUser("id2", "mail2@squidex.io"); - - var command = CreateCommentsCommand(new CreateComment - { - Text = "Hi @mail1@squidex.io and @mail2@squidex.io", - IsMention = false - }); + Text = "Hi @mail1@squidex.io and @mail2@squidex.io", + IsMention = false + }); - var context = CrateCommandContext(command); + var context = CrateCommandContext(command); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_enrich_with_mentioned_user_ids_if_invalid_mentioned_tags_used() + [Fact] + public async Task Should_not_enrich_with_mentioned_user_ids_if_invalid_mentioned_tags_used() + { + var command = CreateCommentsCommand(new CreateComment { - var command = CreateCommentsCommand(new CreateComment - { - Text = "Hi invalid@squidex.io", - IsMention = false - }); + Text = "Hi invalid@squidex.io", + IsMention = false + }); - var context = CrateCommandContext(command); + var context = CrateCommandContext(command); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_enrich_with_mentioned_user_ids_for_notification() + [Fact] + public async Task Should_not_enrich_with_mentioned_user_ids_for_notification() + { + var command = CreateCommentsCommand(new CreateComment { - var command = CreateCommentsCommand(new CreateComment - { - Text = "Hi @invalid@squidex.io", - IsMention = true - }); + Text = "Hi @invalid@squidex.io", + IsMention = true + }); - var context = CrateCommandContext(command); + var context = CrateCommandContext(command); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - private CommandContext CrateCommandContext(ICommand command) - { - return new CommandContext(command, commandBus); - } + private CommandContext CrateCommandContext(ICommand command) + { + return new CommandContext(command, commandBus); + } - private void SetupUser(string id, string email) - { - var user = UserMocks.User(id, email); + private void SetupUser(string id, string email) + { + var user = UserMocks.User(id, email); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(email, default)) - .Returns(user); - } + A.CallTo(() => userResolver.FindByIdOrEmailAsync(email, default)) + .Returns(user); + } - private T CreateCommentsCommand<T>(T command) where T : CommentCommand - { - command.Actor = actor; - command.AppId = appId; - command.CommentsId = commentsId; - command.CommentId = commentId; + private T CreateCommentsCommand<T>(T command) where T : CommentCommand + { + command.Actor = actor; + command.AppId = appId; + command.CommentsId = commentsId; + command.CommentId = commentId; - return command; - } + return command; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs index 0d5f4bbbb6..2c51c02f2e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs @@ -17,163 +17,162 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject +namespace Squidex.Domain.Apps.Entities.Comments.DomainObject; + +public class CommentsStreamTests { - public class CommentsStreamTests - { - private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly DomainId commentsId = DomainId.NewGuid(); - private readonly DomainId commentId = DomainId.NewGuid(); - private readonly RefToken actor = RefToken.User("me"); - private readonly CommentsStream sut; + private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly DomainId commentsId = DomainId.NewGuid(); + private readonly DomainId commentId = DomainId.NewGuid(); + private readonly RefToken actor = RefToken.User("me"); + private readonly CommentsStream sut; - public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = Enumerable.Empty<Envelope<IEvent>>(); + public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = Enumerable.Empty<Envelope<IEvent>>(); - public CommentsStreamTests() - { - A.CallTo(() => eventStore.AppendAsync(A<Guid>._, A<string>._, A<long>._, A<ICollection<EventData>>._, default)) - .Invokes(x => LastEvents = sut!.GetUncommittedEvents().Select(x => x.To<IEvent>()).ToList()); + public CommentsStreamTests() + { + A.CallTo(() => eventStore.AppendAsync(A<Guid>._, A<string>._, A<long>._, A<ICollection<EventData>>._, default)) + .Invokes(x => LastEvents = sut!.GetUncommittedEvents().Select(x => x.To<IEvent>()).ToList()); - sut = new CommentsStream(commentsId, eventFormatter, eventStore); - } + sut = new CommentsStream(commentsId, eventFormatter, eventStore); + } - [Fact] - public async Task Create_should_create_events() - { - var command = new CreateComment { Text = "text1", Url = new Uri("http://uri") }; + [Fact] + public async Task Create_should_create_events() + { + var command = new CreateComment { Text = "text1", Url = new Uri("http://uri") }; - var actual = await sut.ExecuteAsync(CreateCommentsCommand(command), default); + var actual = await sut.ExecuteAsync(CreateCommentsCommand(command), default); - actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 0, EtagVersion.Empty)); + actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 0, EtagVersion.Empty)); - sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult - { - Version = 0 - }); + sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult + { + Version = 0 + }); - sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult - { - CreatedComments = new List<Comment> - { - new Comment(command.CommentId, GetTime(), command.Actor, "text1", command.Url) - }, - Version = 0 - }); - - LastEvents - .ShouldHaveSameEvents( - CreateCommentsEvent(new CommentCreated { Text = command.Text, Url = command.Url }) - ); - } - - [Fact] - public async Task Update_should_create_events() + sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult { - await ExecuteCreateAsync(); + CreatedComments = new List<Comment> + { + new Comment(command.CommentId, GetTime(), command.Actor, "text1", command.Url) + }, + Version = 0 + }); + + LastEvents + .ShouldHaveSameEvents( + CreateCommentsEvent(new CommentCreated { Text = command.Text, Url = command.Url }) + ); + } - var updateCommand = new UpdateComment { Text = "text2" }; + [Fact] + public async Task Update_should_create_events() + { + await ExecuteCreateAsync(); - var actual = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand), default); + var updateCommand = new UpdateComment { Text = "text2" }; - actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 1, 0)); + var actual = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand), default); - sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult - { - CreatedComments = new List<Comment> - { - new Comment(commentId, GetTime(), updateCommand.Actor, "text2") - }, - Version = 1 - }); - - sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult + actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 1, 0)); + + sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult + { + CreatedComments = new List<Comment> { - UpdatedComments = new List<Comment> - { - new Comment(commentId, GetTime(), updateCommand.Actor, "text2") - }, - Version = 1 - }); - - LastEvents - .ShouldHaveSameEvents( - CreateCommentsEvent(new CommentUpdated { Text = updateCommand.Text }) - ); - } - - [Fact] - public async Task Delete_should_create_events() + new Comment(commentId, GetTime(), updateCommand.Actor, "text2") + }, + Version = 1 + }); + + sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult { - await ExecuteCreateAsync(); - await ExecuteUpdateAsync(); + UpdatedComments = new List<Comment> + { + new Comment(commentId, GetTime(), updateCommand.Actor, "text2") + }, + Version = 1 + }); + + LastEvents + .ShouldHaveSameEvents( + CreateCommentsEvent(new CommentUpdated { Text = updateCommand.Text }) + ); + } - var deleteCommand = new DeleteComment(); + [Fact] + public async Task Delete_should_create_events() + { + await ExecuteCreateAsync(); + await ExecuteUpdateAsync(); - var actual = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand), default); + var deleteCommand = new DeleteComment(); - actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 2, 1)); + var actual = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand), default); - sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult - { - Version = 2 - }); + actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 2, 1)); - sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult - { - DeletedComments = new List<DomainId> - { - commentId - }, - Version = 2 - }); - - sut.GetComments(1).Should().BeEquivalentTo(new CommentsResult - { - DeletedComments = new List<DomainId> - { - commentId - }, - Version = 2 - }); - - LastEvents - .ShouldHaveSameEvents( - CreateCommentsEvent(new CommentDeleted()) - ); - } - - private Task ExecuteCreateAsync() + sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult { - return sut.ExecuteAsync(CreateCommentsCommand(new CreateComment { Text = "text1" }), default); - } + Version = 2 + }); - private Task ExecuteUpdateAsync() + sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult { - return sut.ExecuteAsync(CreateCommentsCommand(new UpdateComment { Text = "text2" }), default); - } + DeletedComments = new List<DomainId> + { + commentId + }, + Version = 2 + }); - private T CreateCommentsEvent<T>(T @event) where T : CommentsEvent + sut.GetComments(1).Should().BeEquivalentTo(new CommentsResult { - @event.Actor = actor; - @event.CommentsId = commentsId; - @event.CommentId = commentId; + DeletedComments = new List<DomainId> + { + commentId + }, + Version = 2 + }); + + LastEvents + .ShouldHaveSameEvents( + CreateCommentsEvent(new CommentDeleted()) + ); + } - return @event; - } + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateCommentsCommand(new CreateComment { Text = "text1" }), default); + } - private T CreateCommentsCommand<T>(T command) where T : CommentCommand - { - command.Actor = actor; - command.CommentsId = commentsId; - command.CommentId = commentId; + private Task ExecuteUpdateAsync() + { + return sut.ExecuteAsync(CreateCommentsCommand(new UpdateComment { Text = "text2" }), default); + } - return command; - } + private T CreateCommentsEvent<T>(T @event) where T : CommentsEvent + { + @event.Actor = actor; + @event.CommentsId = commentsId; + @event.CommentId = commentId; - private Instant GetTime() - { - return LastEvents.ElementAt(0).Headers.Timestamp(); - } + return @event; + } + + private T CreateCommentsCommand<T>(T command) where T : CommentCommand + { + command.Actor = actor; + command.CommentsId = commentsId; + command.CommentId = commentId; + + return command; + } + + private Instant GetTime() + { + return LastEvents.ElementAt(0).Headers.Timestamp(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/Guards/GuardCommentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/Guards/GuardCommentsTests.cs index 840ea86483..318ea08656 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/Guards/GuardCommentsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/Guards/GuardCommentsTests.cs @@ -14,180 +14,179 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards; + +public class GuardCommentsTests : IClassFixture<TranslationsFixture> { - public class GuardCommentsTests : IClassFixture<TranslationsFixture> + private readonly string commentsId = DomainId.NewGuid().ToString(); + private readonly RefToken user1 = RefToken.User("1"); + private readonly RefToken user2 = RefToken.User("2"); + + [Fact] + public void CanCreate_should_throw_exception_if_text_not_defined() { - private readonly string commentsId = DomainId.NewGuid().ToString(); - private readonly RefToken user1 = RefToken.User("1"); - private readonly RefToken user2 = RefToken.User("2"); + var command = new CreateComment(); - [Fact] - public void CanCreate_should_throw_exception_if_text_not_defined() - { - var command = new CreateComment(); + ValidationAssert.Throws(() => GuardComments.CanCreate(command), + new ValidationError("Text is required.", "Text")); + } - ValidationAssert.Throws(() => GuardComments.CanCreate(command), - new ValidationError("Text is required.", "Text")); - } + [Fact] + public void CanCreate_should_not_throw_exception_if_text_defined() + { + var command = new CreateComment { Text = "text" }; - [Fact] - public void CanCreate_should_not_throw_exception_if_text_defined() - { - var command = new CreateComment { Text = "text" }; + GuardComments.CanCreate(command); + } - GuardComments.CanCreate(command); - } + [Fact] + public void CanUpdate_should_throw_exception_if_text_not_defined() + { + var commentId = DomainId.NewGuid(); + var command = new UpdateComment { CommentId = commentId, Actor = user1 }; - [Fact] - public void CanUpdate_should_throw_exception_if_text_not_defined() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1 }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() + }; - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() - }; + ValidationAssert.Throws(() => GuardComments.CanUpdate(command, commentsId, events), + new ValidationError("Text is required.", "Text")); + } - ValidationAssert.Throws(() => GuardComments.CanUpdate(command, commentsId, events), - new ValidationError("Text is required.", "Text")); - } + [Fact] + public void CanUpdate_should_throw_exception_if_comment_from_another_user() + { + var commentId = DomainId.NewGuid(); + var command = new UpdateComment { CommentId = commentId, Actor = user2, Text = "text2" }; - [Fact] - public void CanUpdate_should_throw_exception_if_comment_from_another_user() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user2, Text = "text2" }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() + }; - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() - }; + Assert.Throws<DomainException>(() => GuardComments.CanUpdate(command, commentsId, events)); + } - Assert.Throws<DomainException>(() => GuardComments.CanUpdate(command, commentsId, events)); - } + [Fact] + public void CanUpdate_should_throw_exception_if_comment_not_found() + { + var commentId = DomainId.NewGuid(); + var command = new UpdateComment { CommentId = commentId, Actor = user1 }; - [Fact] - public void CanUpdate_should_throw_exception_if_comment_not_found() - { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1 }; + var events = new List<Envelope<CommentsEvent>>(); - var events = new List<Envelope<CommentsEvent>>(); + Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(command, commentsId, events)); + } - Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(command, commentsId, events)); - } + [Fact] + public void CanUpdate_should_throw_exception_if_comment_deleted_found() + { + var commentId = DomainId.NewGuid(); + var command = new UpdateComment { CommentId = commentId, Actor = user1 }; - [Fact] - public void CanUpdate_should_throw_exception_if_comment_deleted_found() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1 }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(), + Envelope.Create<CommentsEvent>(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>() + }; - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(), - Envelope.Create<CommentsEvent>(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>() - }; + Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(command, commentsId, events)); + } - Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(command, commentsId, events)); - } + [Fact] + public void CanUpdate_should_not_throw_exception_if_comment_is_own_notification() + { + var commentId = DomainId.NewGuid(); + var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" }; - [Fact] - public void CanUpdate_should_not_throw_exception_if_comment_is_own_notification() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() + }; - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() - }; + GuardComments.CanUpdate(command, user1.Identifier, events); + } - GuardComments.CanUpdate(command, user1.Identifier, events); - } + [Fact] + public void CanUpdate_should_not_throw_exception_if_comment_from_same_user() + { + var commentId = DomainId.NewGuid(); + var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" }; - [Fact] - public void CanUpdate_should_not_throw_exception_if_comment_from_same_user() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() + }; - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() - }; + GuardComments.CanUpdate(command, commentsId, events); + } - GuardComments.CanUpdate(command, commentsId, events); - } + [Fact] + public void CanDelete_should_throw_exception_if_comment_from_another_user() + { + var commentId = DomainId.NewGuid(); + var command = new DeleteComment { CommentId = commentId, Actor = user2 }; - [Fact] - public void CanDelete_should_throw_exception_if_comment_from_another_user() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user2 }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() + }; - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() - }; + Assert.Throws<DomainException>(() => GuardComments.CanDelete(command, commentsId, events)); + } - Assert.Throws<DomainException>(() => GuardComments.CanDelete(command, commentsId, events)); - } + [Fact] + public void CanDelete_should_throw_exception_if_comment_not_found() + { + var commentId = DomainId.NewGuid(); + var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - [Fact] - public void CanDelete_should_throw_exception_if_comment_not_found() - { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user1 }; + var events = new List<Envelope<CommentsEvent>>(); - var events = new List<Envelope<CommentsEvent>>(); + Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(command, commentsId, events)); + } - Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(command, commentsId, events)); - } + [Fact] + public void CanDelete_should_throw_exception_if_comment_deleted() + { + var commentId = DomainId.NewGuid(); + var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - [Fact] - public void CanDelete_should_throw_exception_if_comment_deleted() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user1 }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }), + Envelope.Create<CommentsEvent>(new CommentDeleted { CommentId = commentId }) + }; - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }), - Envelope.Create<CommentsEvent>(new CommentDeleted { CommentId = commentId }) - }; + Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(command, commentsId, events)); + } - Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(command, commentsId, events)); - } + [Fact] + public void CanDelete_should_not_throw_exception_if_comment_is_own_notification() + { + var commentId = DomainId.NewGuid(); + var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - [Fact] - public void CanDelete_should_not_throw_exception_if_comment_is_own_notification() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user1 }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() + }; - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() - }; + GuardComments.CanDelete(command, user1.Identifier, events); + } - GuardComments.CanDelete(command, user1.Identifier, events); - } + [Fact] + public void CanDelete_should_not_throw_exception_if_comment_from_same_user() + { + var commentId = DomainId.NewGuid(); + var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - [Fact] - public void CanDelete_should_not_throw_exception_if_comment_from_same_user() + var events = new List<Envelope<CommentsEvent>> { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - - var events = new List<Envelope<CommentsEvent>> - { - Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() - }; + Envelope.Create<CommentsEvent>(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>() + }; - GuardComments.CanDelete(command, commentsId, events); - } + GuardComments.CanDelete(command, commentsId, events); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs index 06580f7e79..ce562d0551 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs @@ -11,80 +11,79 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Comments +namespace Squidex.Domain.Apps.Entities.Comments; + +public class WatchingServiceTests { - public class WatchingServiceTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState<WatchingService.State> state1; + private readonly TestState<WatchingService.State> state2; + private readonly IClock clock = A.Fake<IClock>(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly string resource1 = "resource1"; + private readonly string resource2 = "resource2"; + private readonly WatchingService sut; + private Instant now = SystemClock.Instance.GetCurrentInstant(); + + public WatchingServiceTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly TestState<WatchingService.State> state1; - private readonly TestState<WatchingService.State> state2; - private readonly IClock clock = A.Fake<IClock>(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly string resource1 = "resource1"; - private readonly string resource2 = "resource2"; - private readonly WatchingService sut; - private Instant now = SystemClock.Instance.GetCurrentInstant(); - - public WatchingServiceTests() - { - ct = cts.Token; - - A.CallTo(() => clock.GetCurrentInstant()) - .ReturnsLazily(() => now); + ct = cts.Token; - state1 = new TestState<WatchingService.State>($"{appId}_{resource1}"); - state2 = new TestState<WatchingService.State>($"{appId}_{resource2}", state1.PersistenceFactory); + A.CallTo(() => clock.GetCurrentInstant()) + .ReturnsLazily(() => now); - sut = new WatchingService(state1.PersistenceFactory) - { - Clock = clock - }; - } + state1 = new TestState<WatchingService.State>($"{appId}_{resource1}"); + state2 = new TestState<WatchingService.State>($"{appId}_{resource2}", state1.PersistenceFactory); - [Fact] - public async Task Should_only_return_self_if_no_one_watching() + sut = new WatchingService(state1.PersistenceFactory) { - var watching = await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); + Clock = clock + }; + } - Assert.Equal(new[] { "user1" }, watching); - } + [Fact] + public async Task Should_only_return_self_if_no_one_watching() + { + var watching = await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); - [Fact] - public async Task Should_return_users_watching_on_same_resource() - { - await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); - await sut.GetWatchingUsersAsync(appId, resource2, "user2", ct); + Assert.Equal(new[] { "user1" }, watching); + } - var watching1 = await sut.GetWatchingUsersAsync(appId, resource1, "user3", ct); - var watching2 = await sut.GetWatchingUsersAsync(appId, resource2, "user4", ct); + [Fact] + public async Task Should_return_users_watching_on_same_resource() + { + await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); + await sut.GetWatchingUsersAsync(appId, resource2, "user2", ct); - Assert.Equal(new[] { "user1", "user3" }, watching1); - Assert.Equal(new[] { "user2", "user4" }, watching2); - } + var watching1 = await sut.GetWatchingUsersAsync(appId, resource1, "user3", ct); + var watching2 = await sut.GetWatchingUsersAsync(appId, resource2, "user4", ct); - [Fact] - public async Task Should_cleanup_old_users() - { - await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); - await sut.GetWatchingUsersAsync(appId, resource2, "user2", ct); + Assert.Equal(new[] { "user1", "user3" }, watching1); + Assert.Equal(new[] { "user2", "user4" }, watching2); + } + + [Fact] + public async Task Should_cleanup_old_users() + { + await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); + await sut.GetWatchingUsersAsync(appId, resource2, "user2", ct); - now = now.Plus(Duration.FromMinutes(2)); + now = now.Plus(Duration.FromMinutes(2)); - await sut.GetWatchingUsersAsync(appId, resource1, "user3", ct); - await sut.GetWatchingUsersAsync(appId, resource2, "user4", ct); + await sut.GetWatchingUsersAsync(appId, resource1, "user3", ct); + await sut.GetWatchingUsersAsync(appId, resource2, "user4", ct); - var watching1 = await sut.GetWatchingUsersAsync(appId, resource1, "user5", ct); - var watching2 = await sut.GetWatchingUsersAsync(appId, resource2, "user6", ct); + var watching1 = await sut.GetWatchingUsersAsync(appId, resource1, "user5", ct); + var watching2 = await sut.GetWatchingUsersAsync(appId, resource2, "user6", ct); - Assert.Equal(new[] { "user3", "user5" }, watching1); - Assert.Equal(new[] { "user4", "user6" }, watching2); + Assert.Equal(new[] { "user3", "user5" }, watching1); + Assert.Equal(new[] { "user4", "user6" }, watching2); - A.CallTo(() => state1.Persistence.WriteSnapshotAsync(A<WatchingService.State>._, ct)) - .MustHaveHappened(); + A.CallTo(() => state1.Persistence.WriteSnapshotAsync(A<WatchingService.State>._, ct)) + .MustHaveHappened(); - A.CallTo(() => state2.Persistence.WriteSnapshotAsync(A<WatchingService.State>._, ct)) - .MustHaveHappened(); - } + A.CallTo(() => state2.Persistence.WriteSnapshotAsync(A<WatchingService.State>._, ct)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs index a2f966421d..b861f391af 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs @@ -19,197 +19,196 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class BackupContentsTests { - public class BackupContentsTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); + private readonly BackupContents sut; + + public BackupContentsTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); - private readonly BackupContents sut; - - public BackupContentsTests() - { - ct = cts.Token; + ct = cts.Token; - sut = new BackupContents(rebuilder, urlGenerator); - } + sut = new BackupContents(rebuilder, urlGenerator); + } - [Fact] - public void Should_provide_name() - { - Assert.Equal("Contents", sut.Name); - } + [Fact] + public void Should_provide_name() + { + Assert.Equal("Contents", sut.Name); + } - [Fact] - public async Task Should_write_asset_urls() - { - var me = RefToken.User("123"); + [Fact] + public async Task Should_write_asset_urls() + { + var me = RefToken.User("123"); - var assetsUrl = "https://old.squidex.com/api/assets/"; - var assetsUrlApp = "https://old.squidex.com/api/assets/my-app"; + var assetsUrl = "https://old.squidex.com/api/assets/"; + var assetsUrlApp = "https://old.squidex.com/api/assets/my-app"; - A.CallTo(() => urlGenerator.AssetContentBase()) - .Returns(assetsUrl); + A.CallTo(() => urlGenerator.AssetContentBase()) + .Returns(assetsUrl); - A.CallTo(() => urlGenerator.AssetContentBase(appId.Name)) - .Returns(assetsUrlApp); + A.CallTo(() => urlGenerator.AssetContentBase(appId.Name)) + .Returns(assetsUrlApp); - var writer = A.Fake<IBackupWriter>(); + var writer = A.Fake<IBackupWriter>(); - var context = new BackupContext(appId.Id, new UserMapping(me), writer); + var context = new BackupContext(appId.Id, new UserMapping(me), writer); - await sut.BackupEventAsync(Envelope.Create(new AppCreated - { - Name = appId.Name - }), context, ct); - - A.CallTo(() => writer.WriteJsonAsync(A<string>._, - A<BackupContents.Urls>.That.Matches(x => - x.Assets == assetsUrl && - x.AssetsApp == assetsUrlApp), ct)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_replace_asset_url_in_content() + await sut.BackupEventAsync(Envelope.Create(new AppCreated { - var me = RefToken.User("123"); - - var newAssetsUrl = "https://new.squidex.com/api/assets"; - var newAssetsUrlApp = "https://old.squidex.com/api/assets/my-new-app"; - - var oldAssetsUrl = "https://old.squidex.com/api/assets"; - var oldAssetsUrlApp = "https://old.squidex.com/api/assets/my-old-app"; - - var reader = A.Fake<IBackupReader>(); - - A.CallTo(() => urlGenerator.AssetContentBase()) - .Returns(newAssetsUrl); - - A.CallTo(() => urlGenerator.AssetContentBase(appId.Name)) - .Returns(newAssetsUrlApp); - - A.CallTo(() => reader.ReadJsonAsync<BackupContents.Urls>(A<string>._, ct)) - .Returns(new BackupContents.Urls - { - Assets = oldAssetsUrl, - AssetsApp = oldAssetsUrlApp - }); - - var data = - new ContentData() - .AddField("asset", - new ContentFieldData() - .AddLocalized("en", $"Asset: {oldAssetsUrlApp}/my-asset.jpg.") - .AddLocalized("it", $"Asset: {oldAssetsUrl}/my-asset.jpg.")) - .AddField("assetsInArray", - new ContentFieldData() - .AddLocalized("iv", - JsonValue.Array( - $"Asset: {oldAssetsUrlApp}/my-asset.jpg."))) - .AddField("assetsInObj", - new ContentFieldData() - .AddLocalized("iv", - new JsonObject() - .Add("asset", $"Asset: {oldAssetsUrlApp}/my-asset.jpg."))); - - var updateData = - new ContentData() - .AddField("asset", - new ContentFieldData() - .AddLocalized("en", $"Asset: {newAssetsUrlApp}/my-asset.jpg.") - .AddLocalized("it", $"Asset: {newAssetsUrl}/my-asset.jpg.")) - .AddField("assetsInArray", - new ContentFieldData() - .AddLocalized("iv", - JsonValue.Array( - $"Asset: {newAssetsUrlApp}/my-asset.jpg."))) - .AddField("assetsInObj", - new ContentFieldData() - .AddLocalized("iv", - new JsonObject() - .Add("asset", $"Asset: {newAssetsUrlApp}/my-asset.jpg."))); - - var context = new RestoreContext(appId.Id, new UserMapping(me), reader, DomainId.NewGuid()); - - await sut.RestoreEventAsync(Envelope.Create(new AppCreated - { - Name = appId.Name - }), context, ct); + Name = appId.Name + }), context, ct); + + A.CallTo(() => writer.WriteJsonAsync(A<string>._, + A<BackupContents.Urls>.That.Matches(x => + x.Assets == assetsUrl && + x.AssetsApp == assetsUrlApp), ct)) + .MustHaveHappened(); + } - await sut.RestoreEventAsync(Envelope.Create(new ContentUpdated - { - Data = data - }), context, ct); + [Fact] + public async Task Should_replace_asset_url_in_content() + { + var me = RefToken.User("123"); - Assert.Equal(updateData, data); - } + var newAssetsUrl = "https://new.squidex.com/api/assets"; + var newAssetsUrlApp = "https://old.squidex.com/api/assets/my-new-app"; - [Fact] - public async Task Should_restore_states_for_all_contents() - { - var me = RefToken.User("123"); + var oldAssetsUrl = "https://old.squidex.com/api/assets"; + var oldAssetsUrlApp = "https://old.squidex.com/api/assets/my-old-app"; - var schemaId1 = NamedId.Of(DomainId.NewGuid(), "my-schema1"); - var schemaId2 = NamedId.Of(DomainId.NewGuid(), "my-schema2"); + var reader = A.Fake<IBackupReader>(); - var contentId1 = DomainId.NewGuid(); - var contentId2 = DomainId.NewGuid(); - var contentId3 = DomainId.NewGuid(); + A.CallTo(() => urlGenerator.AssetContentBase()) + .Returns(newAssetsUrl); - var context = new RestoreContext(appId.Id, new UserMapping(me), A.Fake<IBackupReader>(), DomainId.NewGuid()); + A.CallTo(() => urlGenerator.AssetContentBase(appId.Name)) + .Returns(newAssetsUrlApp); - await sut.RestoreEventAsync(ContentEvent(new ContentCreated + A.CallTo(() => reader.ReadJsonAsync<BackupContents.Urls>(A<string>._, ct)) + .Returns(new BackupContents.Urls { - ContentId = contentId1, - SchemaId = schemaId1 - }), context, ct); + Assets = oldAssetsUrl, + AssetsApp = oldAssetsUrlApp + }); + + var data = + new ContentData() + .AddField("asset", + new ContentFieldData() + .AddLocalized("en", $"Asset: {oldAssetsUrlApp}/my-asset.jpg.") + .AddLocalized("it", $"Asset: {oldAssetsUrl}/my-asset.jpg.")) + .AddField("assetsInArray", + new ContentFieldData() + .AddLocalized("iv", + JsonValue.Array( + $"Asset: {oldAssetsUrlApp}/my-asset.jpg."))) + .AddField("assetsInObj", + new ContentFieldData() + .AddLocalized("iv", + new JsonObject() + .Add("asset", $"Asset: {oldAssetsUrlApp}/my-asset.jpg."))); + + var updateData = + new ContentData() + .AddField("asset", + new ContentFieldData() + .AddLocalized("en", $"Asset: {newAssetsUrlApp}/my-asset.jpg.") + .AddLocalized("it", $"Asset: {newAssetsUrl}/my-asset.jpg.")) + .AddField("assetsInArray", + new ContentFieldData() + .AddLocalized("iv", + JsonValue.Array( + $"Asset: {newAssetsUrlApp}/my-asset.jpg."))) + .AddField("assetsInObj", + new ContentFieldData() + .AddLocalized("iv", + new JsonObject() + .Add("asset", $"Asset: {newAssetsUrlApp}/my-asset.jpg."))); + + var context = new RestoreContext(appId.Id, new UserMapping(me), reader, DomainId.NewGuid()); + + await sut.RestoreEventAsync(Envelope.Create(new AppCreated + { + Name = appId.Name + }), context, ct); - await sut.RestoreEventAsync(ContentEvent(new ContentCreated - { - ContentId = contentId2, - SchemaId = schemaId1 - }), context, ct); + await sut.RestoreEventAsync(Envelope.Create(new ContentUpdated + { + Data = data + }), context, ct); - await sut.RestoreEventAsync(ContentEvent(new ContentCreated - { - ContentId = contentId3, - SchemaId = schemaId2 - }), context, ct); + Assert.Equal(updateData, data); + } - await sut.RestoreEventAsync(ContentEvent(new ContentDeleted - { - ContentId = contentId2, - SchemaId = schemaId1 - }), context, ct); + [Fact] + public async Task Should_restore_states_for_all_contents() + { + var me = RefToken.User("123"); - await sut.RestoreEventAsync(Envelope.Create(new SchemaDeleted - { - SchemaId = schemaId2 - }), context, ct); + var schemaId1 = NamedId.Of(DomainId.NewGuid(), "my-schema1"); + var schemaId2 = NamedId.Of(DomainId.NewGuid(), "my-schema2"); - var rebuildContents = new HashSet<DomainId>(); + var contentId1 = DomainId.NewGuid(); + var contentId2 = DomainId.NewGuid(); + var contentId3 = DomainId.NewGuid(); - A.CallTo(() => rebuilder.InsertManyAsync<ContentDomainObject, ContentDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) - .Invokes(x => rebuildContents.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); + var context = new RestoreContext(appId.Id, new UserMapping(me), A.Fake<IBackupReader>(), DomainId.NewGuid()); - await sut.RestoreAsync(context, ct); + await sut.RestoreEventAsync(ContentEvent(new ContentCreated + { + ContentId = contentId1, + SchemaId = schemaId1 + }), context, ct); - Assert.Equal(new HashSet<DomainId> - { - DomainId.Combine(appId, contentId1), - DomainId.Combine(appId, contentId2) - }, rebuildContents); - } + await sut.RestoreEventAsync(ContentEvent(new ContentCreated + { + ContentId = contentId2, + SchemaId = schemaId1 + }), context, ct); + + await sut.RestoreEventAsync(ContentEvent(new ContentCreated + { + ContentId = contentId3, + SchemaId = schemaId2 + }), context, ct); + + await sut.RestoreEventAsync(ContentEvent(new ContentDeleted + { + ContentId = contentId2, + SchemaId = schemaId1 + }), context, ct); - private Envelope<ContentEvent> ContentEvent(ContentEvent @event) + await sut.RestoreEventAsync(Envelope.Create(new SchemaDeleted { - @event.AppId = appId; + SchemaId = schemaId2 + }), context, ct); + + var rebuildContents = new HashSet<DomainId>(); + + A.CallTo(() => rebuilder.InsertManyAsync<ContentDomainObject, ContentDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) + .Invokes(x => rebuildContents.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); + + await sut.RestoreAsync(context, ct); + + Assert.Equal(new HashSet<DomainId> + { + DomainId.Combine(appId, contentId1), + DomainId.Combine(appId, contentId2) + }, rebuildContents); + } + + private Envelope<ContentEvent> ContentEvent(ContentEvent @event) + { + @event.AppId = appId; - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.ContentId)); - } + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.ContentId)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index 52e9f7071e..c71ff8b174 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -23,383 +23,382 @@ using Squidex.Infrastructure.Reflection; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class ContentChangedTriggerHandlerTests { - public class ContentChangedTriggerHandlerTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); + private readonly IContentLoader contentLoader = A.Fake<IContentLoader>(); + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly NamedId<DomainId> schemaMatch = NamedId.Of(DomainId.NewGuid(), "my-schema1"); + private readonly NamedId<DomainId> schemaNonMatch = NamedId.Of(DomainId.NewGuid(), "my-schema2"); + private readonly IRuleTriggerHandler sut; + + public ContentChangedTriggerHandlerTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); - private readonly IContentLoader contentLoader = A.Fake<IContentLoader>(); - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly NamedId<DomainId> schemaMatch = NamedId.Of(DomainId.NewGuid(), "my-schema1"); - private readonly NamedId<DomainId> schemaNonMatch = NamedId.Of(DomainId.NewGuid(), "my-schema2"); - private readonly IRuleTriggerHandler sut; - - public ContentChangedTriggerHandlerTests() - { - ct = cts.Token; + ct = cts.Token; - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true", default)) - .Returns(true); + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true", default)) + .Returns(true); - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false", default)) - .Returns(false); + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false", default)) + .Returns(false); - sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader, contentRepository); - } + sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader, contentRepository); + } - public static IEnumerable<object[]> TestEvents() - { - yield return new object[] { TestUtils.CreateEvent<ContentCreated>(), EnrichedContentEventType.Created }; - yield return new object[] { TestUtils.CreateEvent<ContentUpdated>(), EnrichedContentEventType.Updated }; - yield return new object[] { TestUtils.CreateEvent<ContentDeleted>(), EnrichedContentEventType.Deleted }; - yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Change), EnrichedContentEventType.StatusChanged }; - yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Published), EnrichedContentEventType.Published }; - yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Unpublished), EnrichedContentEventType.Unpublished }; - } + public static IEnumerable<object[]> TestEvents() + { + yield return new object[] { TestUtils.CreateEvent<ContentCreated>(), EnrichedContentEventType.Created }; + yield return new object[] { TestUtils.CreateEvent<ContentUpdated>(), EnrichedContentEventType.Updated }; + yield return new object[] { TestUtils.CreateEvent<ContentDeleted>(), EnrichedContentEventType.Deleted }; + yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Change), EnrichedContentEventType.StatusChanged }; + yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Published), EnrichedContentEventType.Published }; + yield return new object[] { TestUtils.CreateEvent<ContentStatusChanged>(x => x.Change = StatusChange.Unpublished), EnrichedContentEventType.Unpublished }; + } - [Fact] - public void Should_return_true_if_asking_for_snapshot_support() - { - Assert.True(sut.CanCreateSnapshotEvents); - } + [Fact] + public void Should_return_true_if_asking_for_snapshot_support() + { + Assert.True(sut.CanCreateSnapshotEvents); + } - [Fact] - public void Should_handle_content_event() - { - Assert.True(sut.Handles(new ContentCreated())); - } + [Fact] + public void Should_handle_content_event() + { + Assert.True(sut.Handles(new ContentCreated())); + } - [Fact] - public void Should_not_handle_other_event() - { - Assert.False(sut.Handles(new AssetMoved())); - } + [Fact] + public void Should_not_handle_other_event() + { + Assert.False(sut.Handles(new AssetMoved())); + } - [Fact] - public void Should_calculate_name_for_created() - { - var @event = new ContentCreated { SchemaId = schemaMatch }; + [Fact] + public void Should_calculate_name_for_created() + { + var @event = new ContentCreated { SchemaId = schemaMatch }; - Assert.Equal("MySchema1Created", sut.GetName(@event)); - } + Assert.Equal("MySchema1Created", sut.GetName(@event)); + } - [Fact] - public void Should_calculate_name_for_deleted() - { - var @event = new ContentDeleted { SchemaId = schemaMatch }; + [Fact] + public void Should_calculate_name_for_deleted() + { + var @event = new ContentDeleted { SchemaId = schemaMatch }; - Assert.Equal("MySchema1Deleted", sut.GetName(@event)); - } + Assert.Equal("MySchema1Deleted", sut.GetName(@event)); + } - [Fact] - public void Should_calculate_name_for_updated() - { - var @event = new ContentUpdated { SchemaId = schemaMatch }; + [Fact] + public void Should_calculate_name_for_updated() + { + var @event = new ContentUpdated { SchemaId = schemaMatch }; - Assert.Equal("MySchema1Updated", sut.GetName(@event)); - } + Assert.Equal("MySchema1Updated", sut.GetName(@event)); + } - [Fact] - public void Should_calculate_name_for_published() - { - var @event = new ContentStatusChanged { SchemaId = schemaMatch, Change = StatusChange.Published }; + [Fact] + public void Should_calculate_name_for_published() + { + var @event = new ContentStatusChanged { SchemaId = schemaMatch, Change = StatusChange.Published }; - Assert.Equal("MySchema1Published", sut.GetName(@event)); - } + Assert.Equal("MySchema1Published", sut.GetName(@event)); + } - [Fact] - public void Should_calculate_name_for_unpublished() - { - var @event = new ContentStatusChanged { SchemaId = schemaMatch, Change = StatusChange.Unpublished }; + [Fact] + public void Should_calculate_name_for_unpublished() + { + var @event = new ContentStatusChanged { SchemaId = schemaMatch, Change = StatusChange.Unpublished }; - Assert.Equal("MySchema1Unpublished", sut.GetName(@event)); - } + Assert.Equal("MySchema1Unpublished", sut.GetName(@event)); + } - [Fact] - public void Should_calculate_name_for_status_change() - { - var @event = new ContentStatusChanged { SchemaId = schemaMatch, Change = StatusChange.Change }; + [Fact] + public void Should_calculate_name_for_status_change() + { + var @event = new ContentStatusChanged { SchemaId = schemaMatch, Change = StatusChange.Change }; - Assert.Equal("MySchema1StatusChanged", sut.GetName(@event)); - } + Assert.Equal("MySchema1StatusChanged", sut.GetName(@event)); + } - [Fact] - public async Task Should_create_events_from_snapshots() - { - var ctx = Context(); + [Fact] + public async Task Should_create_events_from_snapshots() + { + var ctx = Context(); - A.CallTo(() => contentRepository.StreamAll(ctx.AppId.Id, null, ct)) - .Returns(new List<ContentEntity> - { - new ContentEntity { SchemaId = schemaMatch }, - new ContentEntity { SchemaId = schemaNonMatch } - }.ToAsyncEnumerable()); + A.CallTo(() => contentRepository.StreamAll(ctx.AppId.Id, null, ct)) + .Returns(new List<ContentEntity> + { + new ContentEntity { SchemaId = schemaMatch }, + new ContentEntity { SchemaId = schemaNonMatch } + }.ToAsyncEnumerable()); - var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); + var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); - var typed = actual.OfType<EnrichedContentEvent>().ToList(); + var typed = actual.OfType<EnrichedContentEvent>().ToList(); - Assert.Equal(2, typed.Count); - Assert.Equal(2, typed.Count(x => x.Type == EnrichedContentEventType.Created)); + Assert.Equal(2, typed.Count); + Assert.Equal(2, typed.Count(x => x.Type == EnrichedContentEventType.Created)); - Assert.Equal("ContentQueried(MySchema1)", typed[0].Name); - Assert.Equal("ContentQueried(MySchema2)", typed[1].Name); - } + Assert.Equal("ContentQueried(MySchema1)", typed[0].Name); + Assert.Equal("ContentQueried(MySchema2)", typed[1].Name); + } - [Fact] - public async Task Should_create_events_from_snapshots_with_schema_ids() + [Fact] + public async Task Should_create_events_from_snapshots_with_schema_ids() + { + var trigger = new ContentChangedTriggerV2 { - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadonlyList.Create( - new ContentChangedTriggerSchemaV2 - { - SchemaId = schemaMatch.Id - }) - }; + Schemas = ReadonlyList.Create( + new ContentChangedTriggerSchemaV2 + { + SchemaId = schemaMatch.Id + }) + }; - var ctx = Context(trigger); + var ctx = Context(trigger); - A.CallTo(() => contentRepository.StreamAll(ctx.AppId.Id, A<HashSet<DomainId>>.That.Is(schemaMatch.Id), ct)) - .Returns(new List<ContentEntity> - { - new ContentEntity { SchemaId = schemaMatch }, - new ContentEntity { SchemaId = schemaMatch } - }.ToAsyncEnumerable()); + A.CallTo(() => contentRepository.StreamAll(ctx.AppId.Id, A<HashSet<DomainId>>.That.Is(schemaMatch.Id), ct)) + .Returns(new List<ContentEntity> + { + new ContentEntity { SchemaId = schemaMatch }, + new ContentEntity { SchemaId = schemaMatch } + }.ToAsyncEnumerable()); - var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); + var actual = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); - var typed = actual.OfType<EnrichedContentEvent>().ToList(); + var typed = actual.OfType<EnrichedContentEvent>().ToList(); - Assert.Equal(2, typed.Count); - Assert.Equal(2, typed.Count(x => x.Type == EnrichedContentEventType.Created)); - } + Assert.Equal(2, typed.Count); + Assert.Equal(2, typed.Count(x => x.Type == EnrichedContentEventType.Created)); + } - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_create_enriched_events(ContentEvent @event, EnrichedContentEventType type) - { - var ctx = Context(); + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_create_enriched_events(ContentEvent @event, EnrichedContentEventType type) + { + var ctx = Context(); - @event.AppId = ctx.AppId; - @event.SchemaId = schemaMatch; + @event.AppId = ctx.AppId; + @event.SchemaId = schemaMatch; - var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); + var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); - A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) - .Returns(SimpleMapper.Map(@event, new ContentEntity())); + A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) + .Returns(SimpleMapper.Map(@event, new ContentEntity())); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); - var enrichedEvent = (EnrichedContentEvent)actual.Single(); + var enrichedEvent = (EnrichedContentEvent)actual.Single(); - Assert.Equal(type, enrichedEvent!.Type); - Assert.Equal(@event.Actor, enrichedEvent.Actor); - Assert.Equal(@event.AppId, enrichedEvent.AppId); - Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); - Assert.Equal(@event.SchemaId, enrichedEvent.SchemaId); - Assert.Equal(@event.SchemaId.Id, enrichedEvent.SchemaId.Id); - } + Assert.Equal(type, enrichedEvent!.Type); + Assert.Equal(@event.Actor, enrichedEvent.Actor); + Assert.Equal(@event.AppId, enrichedEvent.AppId); + Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); + Assert.Equal(@event.SchemaId, enrichedEvent.SchemaId); + Assert.Equal(@event.SchemaId.Id, enrichedEvent.SchemaId.Id); + } - [Fact] - public async Task Should_enrich_with_old_data_if_updated() - { - var ctx = Context(); + [Fact] + public async Task Should_enrich_with_old_data_if_updated() + { + var ctx = Context(); - var @event = new ContentUpdated { AppId = ctx.AppId, ContentId = DomainId.NewGuid(), SchemaId = schemaMatch }; + var @event = new ContentUpdated { AppId = ctx.AppId, ContentId = DomainId.NewGuid(), SchemaId = schemaMatch }; - var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); + var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); - var dataNow = new ContentData(); - var dataOld = new ContentData(); + var dataNow = new ContentData(); + var dataOld = new ContentData(); - A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) - .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch, Version = 12, Data = dataNow, Id = @event.ContentId }); + A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) + .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch, Version = 12, Data = dataNow, Id = @event.ContentId }); - A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 11, ct)) - .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch, Version = 11, Data = dataOld }); + A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 11, ct)) + .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch, Version = 11, Data = dataOld }); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); - var enrichedEvent = actual.Single() as EnrichedContentEvent; + var enrichedEvent = actual.Single() as EnrichedContentEvent; - Assert.Same(dataNow, enrichedEvent!.Data); - Assert.Same(dataOld, enrichedEvent!.DataOld); - } + Assert.Same(dataNow, enrichedEvent!.Data); + Assert.Same(dataOld, enrichedEvent!.DataOld); + } - [Fact] - public void Should_not_trigger_precheck_if_trigger_contains_no_schemas() + [Fact] + public void Should_not_trigger_precheck_if_trigger_contains_no_schemas() + { + TestForTrigger(handleAll: false, schemaId: null, condition: null, action: ctx => { - TestForTrigger(handleAll: false, schemaId: null, condition: null, action: ctx => - { - var @event = new ContentCreated { SchemaId = schemaMatch }; + var @event = new ContentCreated { SchemaId = schemaMatch }; - var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - [Fact] - public void Should_trigger_precheck_if_handling_all_events() + [Fact] + public void Should_trigger_precheck_if_handling_all_events() + { + TestForTrigger(handleAll: true, schemaId: schemaMatch, condition: null, action: ctx => { - TestForTrigger(handleAll: true, schemaId: schemaMatch, condition: null, action: ctx => - { - var @event = new ContentCreated { SchemaId = schemaMatch }; + var @event = new ContentCreated { SchemaId = schemaMatch }; - var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_precheck_if_condition_is_empty() + [Fact] + public void Should_trigger_precheck_if_condition_is_empty() + { + TestForTrigger(handleAll: false, schemaId: schemaMatch, condition: string.Empty, action: ctx => { - TestForTrigger(handleAll: false, schemaId: schemaMatch, condition: string.Empty, action: ctx => - { - var @event = new ContentCreated { SchemaId = schemaMatch }; + var @event = new ContentCreated { SchemaId = schemaMatch }; - var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_not_trigger_precheck_if_schema_id_does_not_match() + [Fact] + public void Should_not_trigger_precheck_if_schema_id_does_not_match() + { + TestForTrigger(handleAll: false, schemaId: schemaNonMatch, condition: null, action: ctx => { - TestForTrigger(handleAll: false, schemaId: schemaNonMatch, condition: null, action: ctx => - { - var @event = new ContentCreated { SchemaId = schemaMatch }; + var @event = new ContentCreated { SchemaId = schemaMatch }; - var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - [Fact] - public void Should_not_trigger_check_if_trigger_contains_no_schemas() + [Fact] + public void Should_not_trigger_check_if_trigger_contains_no_schemas() + { + TestForTrigger(handleAll: false, schemaId: null, condition: null, action: ctx => { - TestForTrigger(handleAll: false, schemaId: null, condition: null, action: ctx => - { - var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; + var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - [Fact] - public void Should_trigger_check_if_handling_all_events() + [Fact] + public void Should_trigger_check_if_handling_all_events() + { + TestForTrigger(handleAll: true, schemaId: schemaMatch, condition: null, action: ctx => { - TestForTrigger(handleAll: true, schemaId: schemaMatch, condition: null, action: ctx => - { - var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; + var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_check_if_condition_is_empty() + [Fact] + public void Should_trigger_check_if_condition_is_empty() + { + TestForTrigger(handleAll: false, schemaId: schemaMatch, condition: string.Empty, action: ctx => { - TestForTrigger(handleAll: false, schemaId: schemaMatch, condition: string.Empty, action: ctx => - { - var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; + var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_check_if_condition_matchs() + [Fact] + public void Should_trigger_check_if_condition_matchs() + { + TestForTrigger(handleAll: false, schemaId: schemaMatch, condition: "true", action: ctx => { - TestForTrigger(handleAll: false, schemaId: schemaMatch, condition: "true", action: ctx => - { - var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; + var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_not_trigger_check_if_schema_id_does_not_match() + [Fact] + public void Should_not_trigger_check_if_schema_id_does_not_match() + { + TestForTrigger(handleAll: false, schemaId: schemaNonMatch, condition: null, action: ctx => { - TestForTrigger(handleAll: false, schemaId: schemaNonMatch, condition: null, action: ctx => - { - var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; + var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - [Fact] - public void Should_not_trigger_check_if_condition_does_not_match() + [Fact] + public void Should_not_trigger_check_if_condition_does_not_match() + { + TestForTrigger(handleAll: false, schemaId: schemaMatch, condition: "false", action: ctx => { - TestForTrigger(handleAll: false, schemaId: schemaMatch, condition: "false", action: ctx => - { - var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; + var @event = new EnrichedContentEvent { SchemaId = schemaMatch }; - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.False(actual); - }); - } + Assert.False(actual); + }); + } - private void TestForTrigger(bool handleAll, NamedId<DomainId>? schemaId, string? condition, Action<RuleContext> action) - { - var trigger = new ContentChangedTriggerV2 { HandleAll = handleAll }; + private void TestForTrigger(bool handleAll, NamedId<DomainId>? schemaId, string? condition, Action<RuleContext> action) + { + var trigger = new ContentChangedTriggerV2 { HandleAll = handleAll }; - if (schemaId != null) - { - trigger = trigger with - { - Schemas = ReadonlyList.Create( - new ContentChangedTriggerSchemaV2 - { - SchemaId = schemaId.Id, - Condition = condition - }) - }; - } - - action(Context(trigger)); - - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, A<string>._, default)) - .MustNotHaveHappened(); - } - else + if (schemaId != null) + { + trigger = trigger with { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) - .MustHaveHappened(); - } + Schemas = ReadonlyList.Create( + new ContentChangedTriggerSchemaV2 + { + SchemaId = schemaId.Id, + Condition = condition + }) + }; } - private static RuleContext Context(RuleTrigger? trigger = null) - { - trigger ??= new ContentChangedTriggerV2(); + action(Context(trigger)); - return new RuleContext - { - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), - Rule = new Rule(trigger, A.Fake<RuleAction>()), - RuleId = DomainId.NewGuid() - }; + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, A<string>._, default)) + .MustNotHaveHappened(); + } + else + { + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) + .MustHaveHappened(); } } + + private static RuleContext Context(RuleTrigger? trigger = null) + { + trigger ??= new ContentChangedTriggerV2(); + + return new RuleContext + { + AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + Rule = new Rule(trigger, A.Fake<RuleAction>()), + RuleId = DomainId.NewGuid() + }; + } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerProcessTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerProcessTests.cs index 5e41fa82e3..99d7bb54a7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerProcessTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerProcessTests.cs @@ -15,117 +15,116 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class ContentSchedulerProcessTests { - public class ContentSchedulerProcessTests + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly IClock clock = A.Fake<IClock>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly ContentSchedulerProcess sut; + + public ContentSchedulerProcessTests() { - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - private readonly IClock clock = A.Fake<IClock>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly ContentSchedulerProcess sut; + sut = new ContentSchedulerProcess(contentRepository, commandBus, A.Fake<ILogger<ContentSchedulerProcess>>()) + { + Clock = clock + }; + } - public ContentSchedulerProcessTests() + [Fact] + public async Task Should_change_scheduled_items() + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var content1 = new ContentEntity { - sut = new ContentSchedulerProcess(contentRepository, commandBus, A.Fake<ILogger<ContentSchedulerProcess>>()) - { - Clock = clock - }; - } - - [Fact] - public async Task Should_change_scheduled_items() + AppId = appId, + Id = DomainId.NewGuid(), + ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Archived, null!, now) + }; + + var content2 = new ContentEntity { - var now = SystemClock.Instance.GetCurrentInstant(); - - var content1 = new ContentEntity - { - AppId = appId, - Id = DomainId.NewGuid(), - ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Archived, null!, now) - }; - - var content2 = new ContentEntity - { - AppId = appId, - Id = DomainId.NewGuid(), - ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Draft, null!, now) - }; - - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(now); - - A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) - .Returns(new[] { content1, content2 }.ToAsyncEnumerable()); - - await sut.PublishAsync(); - - A.CallTo(() => commandBus.PublishAsync( - A<ChangeContentStatus>.That.Matches(x => - x.ContentId == content1.Id && - x.Status == content1.ScheduleJob.Status && - x.StatusJobId == content1.ScheduleJob.Id), - default)) - .MustHaveHappened(); - - A.CallTo(() => commandBus.PublishAsync( - A<ChangeContentStatus>.That.Matches(x => - x.ContentId == content2.Id && - x.Status == content2.ScheduleJob.Status && - x.StatusJobId == content2.ScheduleJob.Id), - default)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_change_status_if_content_has_no_schedule_job() + AppId = appId, + Id = DomainId.NewGuid(), + ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Draft, null!, now) + }; + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); + + A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) + .Returns(new[] { content1, content2 }.ToAsyncEnumerable()); + + await sut.PublishAsync(); + + A.CallTo(() => commandBus.PublishAsync( + A<ChangeContentStatus>.That.Matches(x => + x.ContentId == content1.Id && + x.Status == content1.ScheduleJob.Status && + x.StatusJobId == content1.ScheduleJob.Id), + default)) + .MustHaveHappened(); + + A.CallTo(() => commandBus.PublishAsync( + A<ChangeContentStatus>.That.Matches(x => + x.ContentId == content2.Id && + x.Status == content2.ScheduleJob.Status && + x.StatusJobId == content2.ScheduleJob.Id), + default)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_change_status_if_content_has_no_schedule_job() + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var content1 = new ContentEntity { - var now = SystemClock.Instance.GetCurrentInstant(); + AppId = appId, + Id = DomainId.NewGuid(), + ScheduleJob = null + }; - var content1 = new ContentEntity - { - AppId = appId, - Id = DomainId.NewGuid(), - ScheduleJob = null - }; + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(now); + A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) + .Returns(new[] { content1 }.ToAsyncEnumerable()); - A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) - .Returns(new[] { content1 }.ToAsyncEnumerable()); + await sut.PublishAsync(); - await sut.PublishAsync(); + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_reset_job_if_content_not_found_anymore() + { + var now = SystemClock.Instance.GetCurrentInstant(); - [Fact] - public async Task Should_reset_job_if_content_not_found_anymore() + var content1 = new ContentEntity { - var now = SystemClock.Instance.GetCurrentInstant(); - - var content1 = new ContentEntity - { - AppId = appId, - Id = DomainId.NewGuid(), - ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Archived, null!, now) - }; + AppId = appId, + Id = DomainId.NewGuid(), + ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Archived, null!, now) + }; - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(now); + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); - A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) - .Returns(new[] { content1 }.ToAsyncEnumerable()); + A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) + .Returns(new[] { content1 }.ToAsyncEnumerable()); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, default)) - .Throws(new DomainObjectNotFoundException(content1.Id.ToString())); + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, default)) + .Throws(new DomainObjectNotFoundException(content1.Id.ToString())); - await sut.PublishAsync(); + await sut.PublishAsync(); - A.CallTo(() => contentRepository.ResetScheduledAsync(content1.UniqueId, default)) - .MustHaveHappened(); - } + A.CallTo(() => contentRepository.ResetScheduledAsync(content1.UniqueId, default)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs index b5e3b1c87b..776e768a3c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs @@ -20,204 +20,203 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class ContentsSearchSourceTests { - public class ContentsSearchSourceTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly ITextIndex contentIndex = A.Fake<ITextIndex>(); + private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId1 = NamedId.Of(DomainId.NewGuid(), "my-schema1"); + private readonly NamedId<DomainId> schemaId2 = NamedId.Of(DomainId.NewGuid(), "my-schema2"); + private readonly NamedId<DomainId> schemaId3 = NamedId.Of(DomainId.NewGuid(), "my-schema3"); + private readonly ContentsSearchSource sut; + + public ContentsSearchSourceTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly ITextIndex contentIndex = A.Fake<ITextIndex>(); - private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId1 = NamedId.Of(DomainId.NewGuid(), "my-schema1"); - private readonly NamedId<DomainId> schemaId2 = NamedId.Of(DomainId.NewGuid(), "my-schema2"); - private readonly NamedId<DomainId> schemaId3 = NamedId.Of(DomainId.NewGuid(), "my-schema3"); - private readonly ContentsSearchSource sut; - - public ContentsSearchSourceTests() - { - ct = cts.Token; + ct = cts.Token; - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, ct)) - .Returns(new List<ISchemaEntity> - { - Mocks.Schema(appId, schemaId1), - Mocks.Schema(appId, schemaId2), - Mocks.Schema(appId, schemaId3) - }); + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, ct)) + .Returns(new List<ISchemaEntity> + { + Mocks.Schema(appId, schemaId1), + Mocks.Schema(appId, schemaId2), + Mocks.Schema(appId, schemaId3) + }); - sut = new ContentsSearchSource(appProvider, contentQuery, contentIndex, urlGenerator); - } + sut = new ContentsSearchSource(appProvider, contentQuery, contentIndex, urlGenerator); + } - [Fact] - public async Task Should_return_content_with_default_name() - { - var content = new ContentEntity { Id = DomainId.NewGuid(), SchemaId = schemaId1 }; + [Fact] + public async Task Should_return_content_with_default_name() + { + var content = new ContentEntity { Id = DomainId.NewGuid(), SchemaId = schemaId1 }; - await TestContentAsync(content, "Content"); - } + await TestContentAsync(content, "Content"); + } - [Fact] - public async Task Should_return_content_with_multiple_invariant_reference_fields() + [Fact] + public async Task Should_return_content_with_multiple_invariant_reference_fields() + { + var content = new ContentEntity { - var content = new ContentEntity + Id = DomainId.NewGuid(), + Data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant("hello")) + .AddField("field2", + new ContentFieldData() + .AddInvariant("world")), + ReferenceFields = new[] { - Id = DomainId.NewGuid(), - Data = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant("hello")) - .AddField("field2", - new ContentFieldData() - .AddInvariant("world")), - ReferenceFields = new[] - { - Fields.String(1, "field1", Partitioning.Invariant), - Fields.String(2, "field2", Partitioning.Invariant) - }, - SchemaId = schemaId1 - }; - - await TestContentAsync(content, "hello, world"); - } + Fields.String(1, "field1", Partitioning.Invariant), + Fields.String(2, "field2", Partitioning.Invariant) + }, + SchemaId = schemaId1 + }; - [Fact] - public async Task Should_return_content_with_invariant_reference_field() + await TestContentAsync(content, "hello, world"); + } + + [Fact] + public async Task Should_return_content_with_invariant_reference_field() + { + var content = new ContentEntity { - var content = new ContentEntity + Id = DomainId.NewGuid(), + Data = + new ContentData() + .AddField("field", + new ContentFieldData() + .AddInvariant("hello")), + ReferenceFields = new[] { - Id = DomainId.NewGuid(), - Data = - new ContentData() - .AddField("field", - new ContentFieldData() - .AddInvariant("hello")), - ReferenceFields = new[] - { - Fields.String(1, "field", Partitioning.Invariant) - }, - SchemaId = schemaId1 - }; - - await TestContentAsync(content, "hello"); - } + Fields.String(1, "field", Partitioning.Invariant) + }, + SchemaId = schemaId1 + }; + + await TestContentAsync(content, "hello"); + } - [Fact] - public async Task Should_return_content_with_localized_reference_field() + [Fact] + public async Task Should_return_content_with_localized_reference_field() + { + var content = new ContentEntity { - var content = new ContentEntity + Id = DomainId.NewGuid(), + Data = + new ContentData() + .AddField("field", + new ContentFieldData() + .AddLocalized("en", "hello")), + ReferenceFields = new[] { - Id = DomainId.NewGuid(), - Data = - new ContentData() - .AddField("field", - new ContentFieldData() - .AddLocalized("en", "hello")), - ReferenceFields = new[] - { - Fields.String(1, "field", Partitioning.Language) - }, - SchemaId = schemaId1 - }; - - await TestContentAsync(content, "hello"); - } + Fields.String(1, "field", Partitioning.Language) + }, + SchemaId = schemaId1 + }; + + await TestContentAsync(content, "hello"); + } - [Fact] - public async Task Should_return_content_with_invariant_field_and_reference_data() + [Fact] + public async Task Should_return_content_with_invariant_field_and_reference_data() + { + var content = new ContentEntity { - var content = new ContentEntity + Id = DomainId.NewGuid(), + Data = + new ContentData() + .AddField("field", + new ContentFieldData() + .AddInvariant("raw")), + ReferenceData = + new ContentData() + .AddField("field", + new ContentFieldData() + .AddLocalized("en", "resolved")), + ReferenceFields = new[] { - Id = DomainId.NewGuid(), - Data = - new ContentData() - .AddField("field", - new ContentFieldData() - .AddInvariant("raw")), - ReferenceData = - new ContentData() - .AddField("field", - new ContentFieldData() - .AddLocalized("en", "resolved")), - ReferenceFields = new[] - { - Fields.String(1, "field", Partitioning.Language) - }, - SchemaId = schemaId1 - }; - - await TestContentAsync(content, "resolved"); - } + Fields.String(1, "field", Partitioning.Language) + }, + SchemaId = schemaId1 + }; - [Fact] - public async Task Should_not_invoke_content_index_if_user_has_no_permission() - { - var ctx = ContextWithPermissions(); + await TestContentAsync(content, "resolved"); + } - var actual = await sut.SearchAsync("query", ctx, ct); + [Fact] + public async Task Should_not_invoke_content_index_if_user_has_no_permission() + { + var ctx = ContextWithPermissions(); - Assert.Empty(actual); + var actual = await sut.SearchAsync("query", ctx, ct); - A.CallTo(() => contentIndex.SearchAsync(ctx.App, A<TextQuery>._, A<SearchScope>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + Assert.Empty(actual); - [Fact] - public async Task Should_not_invoke_context_query_if_no_id_found() - { - var ctx = ContextWithPermissions(schemaId1, schemaId2); + A.CallTo(() => contentIndex.SearchAsync(ctx.App, A<TextQuery>._, A<SearchScope>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => contentIndex.SearchAsync(ctx.App, A<TextQuery>.That.Matches(x => x.Text == "query~"), ctx.Scope(), ct)) - .Returns(new List<DomainId>()); + [Fact] + public async Task Should_not_invoke_context_query_if_no_id_found() + { + var ctx = ContextWithPermissions(schemaId1, schemaId2); - var actual = await sut.SearchAsync("query", ctx, ct); + A.CallTo(() => contentIndex.SearchAsync(ctx.App, A<TextQuery>.That.Matches(x => x.Text == "query~"), ctx.Scope(), ct)) + .Returns(new List<DomainId>()); - Assert.Empty(actual); + var actual = await sut.SearchAsync("query", ctx, ct); - A.CallTo(() => contentQuery.QueryAsync(ctx, A<Q>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + Assert.Empty(actual); - private async Task TestContentAsync(ContentEntity content, string expectedName) - { - content.AppId = appId; + A.CallTo(() => contentQuery.QueryAsync(ctx, A<Q>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - var ctx = ContextWithPermissions(schemaId1, schemaId2); + private async Task TestContentAsync(ContentEntity content, string expectedName) + { + content.AppId = appId; - var ids = new List<DomainId> { content.Id }; + var ctx = ContextWithPermissions(schemaId1, schemaId2); - A.CallTo(() => contentIndex.SearchAsync(ctx.App, A<TextQuery>.That.Matches(x => x.Text == "query~"), ctx.Scope(), ct)) - .Returns(ids); + var ids = new List<DomainId> { content.Id }; - A.CallTo(() => contentQuery.QueryAsync(ctx, A<Q>.That.HasIds(ids), ct)) - .Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1, content)); + A.CallTo(() => contentIndex.SearchAsync(ctx.App, A<TextQuery>.That.Matches(x => x.Text == "query~"), ctx.Scope(), ct)) + .Returns(ids); - A.CallTo(() => urlGenerator.ContentUI(appId, schemaId1, content.Id)) - .Returns("content-url"); + A.CallTo(() => contentQuery.QueryAsync(ctx, A<Q>.That.HasIds(ids), ct)) + .Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1, content)); - var actual = await sut.SearchAsync("query", ctx, ct); + A.CallTo(() => urlGenerator.ContentUI(appId, schemaId1, content.Id)) + .Returns("content-url"); - actual.Should().BeEquivalentTo( - new SearchResults() - .Add(expectedName, SearchResultType.Content, "content-url")); - } + var actual = await sut.SearchAsync("query", ctx, ct); - private Context ContextWithPermissions(params NamedId<DomainId>[] allowedSchemas) - { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add(expectedName, SearchResultType.Content, "content-url")); + } - foreach (var schemaId in allowedSchemas) - { - var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, schemaId.Name).Id; + private Context ContextWithPermissions(params NamedId<DomainId>[] allowedSchemas) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); - } + foreach (var schemaId in allowedSchemas) + { + var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, schemaId.Name).Id; - return new Context(claimsPrincipal, Mocks.App(appId)); + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); } + + return new Context(claimsPrincipal, Mocks.App(appId)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs index 5aa018b593..0c87b14d58 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs @@ -12,118 +12,117 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Counter +namespace Squidex.Domain.Apps.Entities.Contents.Counter; + +public class CounterJintExtensionTests { - public class CounterJintExtensionTests - { - private readonly ICounterService counterService = A.Fake<ICounterService>(); - private readonly JintScriptEngine sut; + private readonly ICounterService counterService = A.Fake<ICounterService>(); + private readonly JintScriptEngine sut; - public CounterJintExtensionTests() + public CounterJintExtensionTests() + { + var extensions = new IJintExtension[] { - var extensions = new IJintExtension[] + new CounterJintExtension(counterService) + }; + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions { - new CounterJintExtension(counterService) - }; - - sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - }), extensions); - } - - [Fact] - public void Should_reset_counter() - { - var appId = DomainId.NewGuid(); + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + }), extensions); + } - A.CallTo(() => counterService.ResetAsync(appId, "my", 4, default)) - .Returns(3); + [Fact] + public void Should_reset_counter() + { + var appId = DomainId.NewGuid(); - const string script = @" + A.CallTo(() => counterService.ResetAsync(appId, "my", 4, default)) + .Returns(3); + + const string script = @" return resetCounter('my', 4); "; - var vars = new ScriptVars - { - ["appId"] = appId - }; + var vars = new ScriptVars + { + ["appId"] = appId + }; - var actual = sut.Execute(vars, script).ToString(); + var actual = sut.Execute(vars, script).ToString(); - Assert.Equal("3", actual); - } + Assert.Equal("3", actual); + } - [Fact] - public async Task Should_reset_counter_with_callback() - { - var appId = DomainId.NewGuid(); + [Fact] + public async Task Should_reset_counter_with_callback() + { + var appId = DomainId.NewGuid(); - A.CallTo(() => counterService.ResetAsync(appId, "my", 4, A<CancellationToken>._)) - .Returns(3); + A.CallTo(() => counterService.ResetAsync(appId, "my", 4, A<CancellationToken>._)) + .Returns(3); - const string script = @" + const string script = @" resetCounterV2('my', function(actual) { complete(actual); }, 4); "; - var vars = new ScriptVars - { - ["appId"] = appId - }; + var vars = new ScriptVars + { + ["appId"] = appId + }; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal("3", actual); - } + Assert.Equal("3", actual); + } - [Fact] - public void Should_increment_counter() - { - var appId = DomainId.NewGuid(); + [Fact] + public void Should_increment_counter() + { + var appId = DomainId.NewGuid(); - A.CallTo(() => counterService.IncrementAsync(appId, "my", A<CancellationToken>._)) - .Returns(3); + A.CallTo(() => counterService.IncrementAsync(appId, "my", A<CancellationToken>._)) + .Returns(3); - const string script = @" + const string script = @" return incrementCounter('my'); "; - var vars = new ScriptVars - { - ["appId"] = appId - }; + var vars = new ScriptVars + { + ["appId"] = appId + }; - var actual = sut.Execute(vars, script).ToString(); + var actual = sut.Execute(vars, script).ToString(); - Assert.Equal("3", actual); - } + Assert.Equal("3", actual); + } - [Fact] - public async Task Should_increment_counter_with_callback() - { - var appId = DomainId.NewGuid(); + [Fact] + public async Task Should_increment_counter_with_callback() + { + var appId = DomainId.NewGuid(); - A.CallTo(() => counterService.IncrementAsync(appId, "my", A<CancellationToken>._)) - .Returns(3); + A.CallTo(() => counterService.IncrementAsync(appId, "my", A<CancellationToken>._)) + .Returns(3); - const string script = @" + const string script = @" incrementCounter('my', function (actual) { complete(actual); }); "; - var vars = new ScriptVars - { - ["appId"] = appId - }; + var vars = new ScriptVars + { + ["appId"] = appId + }; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal("3", actual); - } + Assert.Equal("3", actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterServiceTests.cs index af3b55bed3..d3fdcda92c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterServiceTests.cs @@ -11,67 +11,66 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Counter +namespace Squidex.Domain.Apps.Entities.Contents.Counter; + +public class CounterServiceTests { - public class CounterServiceTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly TestState<CounterService.State> state; - private readonly DomainId appId = DomainId.NewGuid(); - private readonly CounterService sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState<CounterService.State> state; + private readonly DomainId appId = DomainId.NewGuid(); + private readonly CounterService sut; - public CounterServiceTests() - { - ct = cts.Token; + public CounterServiceTests() + { + ct = cts.Token; - state = new TestState<CounterService.State>(appId); + state = new TestState<CounterService.State>(appId); - sut = new CounterService(state.PersistenceFactory); - } + sut = new CounterService(state.PersistenceFactory); + } - [Fact] - public void Should_run_delete_with_default_order() - { - var order = ((IDeleter)sut).Order; + [Fact] + public void Should_run_delete_with_default_order() + { + var order = ((IDeleter)sut).Order; - Assert.Equal(0, order); - } + Assert.Equal(0, order); + } - [Fact] - public async Task Should_delete_state_when_app_deleted() - { - await ((IDeleter)sut).DeleteAppAsync(Mocks.App(NamedId.Of(appId, "my-app")), ct); + [Fact] + public async Task Should_delete_state_when_app_deleted() + { + await ((IDeleter)sut).DeleteAppAsync(Mocks.App(NamedId.Of(appId, "my-app")), ct); - A.CallTo(() => state.Persistence.DeleteAsync(ct)) - .MustHaveHappened(); - } + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_increment_counters() - { - Assert.Equal(1, await sut.IncrementAsync(appId, "Counter1", ct)); - Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); + [Fact] + public async Task Should_increment_counters() + { + Assert.Equal(1, await sut.IncrementAsync(appId, "Counter1", ct)); + Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); - Assert.Equal(1, await sut.IncrementAsync(appId, "Counter2", ct)); - Assert.Equal(2, await sut.IncrementAsync(appId, "Counter2", ct)); + Assert.Equal(1, await sut.IncrementAsync(appId, "Counter2", ct)); + Assert.Equal(2, await sut.IncrementAsync(appId, "Counter2", ct)); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<CounterService.State>._, ct)) - .MustHaveHappened(4, Times.Exactly); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<CounterService.State>._, ct)) + .MustHaveHappened(4, Times.Exactly); + } - [Fact] - public async Task Should_reset_counter() - { - Assert.Equal(1, await sut.IncrementAsync(appId, "Counter1", ct)); - Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); + [Fact] + public async Task Should_reset_counter() + { + Assert.Equal(1, await sut.IncrementAsync(appId, "Counter1", ct)); + Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); - Assert.Equal(1, await sut.ResetAsync(appId, "Counter1", 1, ct)); + Assert.Equal(1, await sut.ResetAsync(appId, "Counter1", 1, ct)); - Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); + Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<CounterService.State>._, ct)) - .MustHaveHappened(4, Times.Exactly); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<CounterService.State>._, ct)) + .MustHaveHappened(4, Times.Exactly); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index 674750be09..2f56a32bbc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -13,188 +13,187 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class DefaultContentWorkflowTests { - public class DefaultContentWorkflowTests + private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); + + [Fact] + public async Task Should_return_info_for_valid_status() { - private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); + var info = await sut.GetInfoAsync(null!, Status.Draft); - [Fact] - public async Task Should_return_info_for_valid_status() - { - var info = await sut.GetInfoAsync(null!, Status.Draft); + Assert.Equal(new StatusInfo(Status.Draft, StatusColors.Draft), info); + } - Assert.Equal(new StatusInfo(Status.Draft, StatusColors.Draft), info); - } + [Fact] + public async Task Should_return_info_as_null_for_invalid_status() + { + var info = await sut.GetInfoAsync(null!, new Status("Invalid")); - [Fact] - public async Task Should_return_info_as_null_for_invalid_status() - { - var info = await sut.GetInfoAsync(null!, new Status("Invalid")); + Assert.Null(info); + } - Assert.Null(info); - } + [Fact] + public async Task Should_return_draft_as_initial_status() + { + var actual = await sut.GetInitialStatusAsync(null!); - [Fact] - public async Task Should_return_draft_as_initial_status() - { - var actual = await sut.GetInitialStatusAsync(null!); + Assert.Equal(Status.Draft, actual); + } - Assert.Equal(Status.Draft, actual); - } + [Fact] + public async Task Should_allow_publish_on_create() + { + var actual = await sut.CanPublishInitialAsync(null!, null); - [Fact] - public async Task Should_allow_publish_on_create() - { - var actual = await sut.CanPublishInitialAsync(null!, null); + Assert.True(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_allow_if_transition_is_valid() + { + var content = new ContentEntity { Status = Status.Published }; - [Fact] - public async Task Should_allow_if_transition_is_valid() - { - var content = new ContentEntity { Status = Status.Published }; + var actual = await sut.CanMoveToAsync(null!, content.Status, Status.Draft, null!, null!); - var actual = await sut.CanMoveToAsync(null!, content.Status, Status.Draft, null!, null!); + Assert.True(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_allow_if_transition_is_valid_for_content() + { + var content = new ContentEntity { Status = Status.Published }; - [Fact] - public async Task Should_allow_if_transition_is_valid_for_content() - { - var content = new ContentEntity { Status = Status.Published }; + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Draft, null!); - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Draft, null!); + Assert.True(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_be_able_to_update_published() + { + var content = new ContentEntity { Status = Status.Published }; - [Fact] - public async Task Should_be_able_to_update_published() - { - var content = new ContentEntity { Status = Status.Published }; + var actual = await sut.CanUpdateAsync(content, content.Status, null!); - var actual = await sut.CanUpdateAsync(content, content.Status, null!); + Assert.True(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_be_able_to_update_draft() + { + var content = new ContentEntity { Status = Status.Published }; - [Fact] - public async Task Should_be_able_to_update_draft() - { - var content = new ContentEntity { Status = Status.Published }; + var actual = await sut.CanUpdateAsync(content, content.Status, null!); - var actual = await sut.CanUpdateAsync(content, content.Status, null!); + Assert.True(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_not_be_able_to_update_archived() + { + var content = new ContentEntity { Status = Status.Archived }; - [Fact] - public async Task Should_not_be_able_to_update_archived() - { - var content = new ContentEntity { Status = Status.Archived }; + var actual = await sut.CanUpdateAsync(content, content.Status, null!); - var actual = await sut.CanUpdateAsync(content, content.Status, null!); + Assert.False(actual); + } - Assert.False(actual); - } + [Fact] + public async Task Should_get_next_statuses_for_draft() + { + var content = new ContentEntity { Status = Status.Draft }; - [Fact] - public async Task Should_get_next_statuses_for_draft() + var expected = new[] { - var content = new ContentEntity { Status = Status.Draft }; + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Published, StatusColors.Published) + }; - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Published, StatusColors.Published) - }; + var actual = await sut.GetNextAsync(content, content.Status, null!); - var actual = await sut.GetNextAsync(content, content.Status, null!); + actual.Should().BeEquivalentTo(expected); + } - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task Should_get_next_statuses_for_archived() + { + var content = new ContentEntity { Status = Status.Archived }; - [Fact] - public async Task Should_get_next_statuses_for_archived() + var expected = new[] { - var content = new ContentEntity { Status = Status.Archived }; + new StatusInfo(Status.Draft, StatusColors.Draft) + }; - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft) - }; + var actual = await sut.GetNextAsync(content, content.Status, null!); - var actual = await sut.GetNextAsync(content, content.Status, null!); + actual.Should().BeEquivalentTo(expected); + } - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task Should_get_next_statuses_for_published() + { + var content = new ContentEntity { Status = Status.Published }; - [Fact] - public async Task Should_get_next_statuses_for_published() + var expected = new[] { - var content = new ContentEntity { Status = Status.Published }; + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft) + }; - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft) - }; + var actual = await sut.GetNextAsync(content, content.Status, null!); - var actual = await sut.GetNextAsync(content, content.Status, null!); - - actual.Should().BeEquivalentTo(expected); - } + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task Should_return_all_statuses() + [Fact] + public async Task Should_return_all_statuses() + { + var expected = new[] { - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; - var actual = await sut.GetAllAsync(null!); + var actual = await sut.GetAllAsync(null!); - actual.Should().BeEquivalentTo(expected); - } + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task Should_not_validate_when_not_publishing() - { - var actual = await sut.ShouldValidateAsync(null!, Status.Draft); + [Fact] + public async Task Should_not_validate_when_not_publishing() + { + var actual = await sut.ShouldValidateAsync(null!, Status.Draft); - Assert.False(actual); - } + Assert.False(actual); + } - [Fact] - public async Task Should_not_validate_when_publishing_but_not_enabled() - { - var actual = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published); + [Fact] + public async Task Should_not_validate_when_publishing_but_not_enabled() + { + var actual = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published); - Assert.False(actual); - } + Assert.False(actual); + } - [Fact] - public async Task Should_validate_when_publishing_and_enabled() - { - var actual = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published); + [Fact] + public async Task Should_validate_when_publishing_and_enabled() + { + var actual = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published); - Assert.True(actual); - } + Assert.True(actual); + } - private static ISchemaEntity CreateSchema(bool validateOnPublish) + private static ISchemaEntity CreateSchema(bool validateOnPublish) + { + var schema = new Schema("my-schema", new SchemaProperties { - var schema = new Schema("my-schema", new SchemaProperties - { - ValidateOnPublish = validateOnPublish - }); + ValidateOnPublish = validateOnPublish + }); - return Mocks.Schema(NamedId.Of(DomainId.NewGuid(), "my-app"), NamedId.Of(DomainId.NewGuid(), schema.Name), schema); - } + return Mocks.Schema(NamedId.Of(DomainId.NewGuid(), "my-app"), NamedId.Of(DomainId.NewGuid(), schema.Name), schema); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs index b0b7e59c6a..33ad68ac87 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs @@ -15,98 +15,97 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class DefaultWorkflowsValidatorTests : IClassFixture<TranslationsFixture> { - public class DefaultWorkflowsValidatorTests : IClassFixture<TranslationsFixture> - { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly DefaultWorkflowsValidator sut; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly DefaultWorkflowsValidator sut; - public DefaultWorkflowsValidatorTests() - { - var schema = Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); + public DefaultWorkflowsValidatorTests() + { + var schema = Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, false, default)) - .Returns(Task.FromResult<ISchemaEntity?>(null)); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, false, default)) + .Returns(Task.FromResult<ISchemaEntity?>(null)); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) + .Returns(schema); - sut = new DefaultWorkflowsValidator(appProvider); - } + sut = new DefaultWorkflowsValidator(appProvider); + } - [Fact] - public async Task Should_generate_error_if_multiple_workflows_cover_all_schemas() - { - var workflows = Workflows.Empty - .Add(DomainId.NewGuid(), "workflow1") - .Add(DomainId.NewGuid(), "workflow2"); + [Fact] + public async Task Should_generate_error_if_multiple_workflows_cover_all_schemas() + { + var workflows = Workflows.Empty + .Add(DomainId.NewGuid(), "workflow1") + .Add(DomainId.NewGuid(), "workflow2"); - var errors = await sut.ValidateAsync(appId.Id, workflows); + var errors = await sut.ValidateAsync(appId.Id, workflows); - Assert.Equal(new[] { "Multiple workflows cover all schemas." }, errors.ToArray()); - } + Assert.Equal(new[] { "Multiple workflows cover all schemas." }, errors.ToArray()); + } - [Fact] - public async Task Should_generate_error_if_multiple_workflows_cover_specific_schema() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + [Fact] + public async Task Should_generate_error_if_multiple_workflows_cover_specific_schema() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, null, ReadonlyList.Create(schemaId.Id))) - .Update(id2, new Workflow(default, null, ReadonlyList.Create(schemaId.Id))); + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, null, ReadonlyList.Create(schemaId.Id))) + .Update(id2, new Workflow(default, null, ReadonlyList.Create(schemaId.Id))); - var errors = await sut.ValidateAsync(appId.Id, workflows); + var errors = await sut.ValidateAsync(appId.Id, workflows); - Assert.Equal(new[] { "The schema 'my-schema' is covered by multiple workflows." }, errors.ToArray()); - } + Assert.Equal(new[] { "The schema 'my-schema' is covered by multiple workflows." }, errors.ToArray()); + } - [Fact] - public async Task Should_not_generate_error_if_schema_deleted() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + [Fact] + public async Task Should_not_generate_error_if_schema_deleted() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - var oldSchemaId = DomainId.NewGuid(); + var oldSchemaId = DomainId.NewGuid(); - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, null, ReadonlyList.Create(oldSchemaId))) - .Update(id2, new Workflow(default, null, ReadonlyList.Create(oldSchemaId))); + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, null, ReadonlyList.Create(oldSchemaId))) + .Update(id2, new Workflow(default, null, ReadonlyList.Create(oldSchemaId))); - var errors = await sut.ValidateAsync(appId.Id, workflows); + var errors = await sut.ValidateAsync(appId.Id, workflows); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_generate_errors_for_no_overlaps() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + [Fact] + public async Task Should_not_generate_errors_for_no_overlaps() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - var workflows = Workflows.Empty - .Add(id1, "workflow1") - .Add(id2, "workflow2") - .Update(id1, new Workflow(default, null, ReadonlyList.Create(schemaId.Id))); + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, null, ReadonlyList.Create(schemaId.Id))); - var errors = await sut.ValidateAsync(appId.Id, workflows); + var errors = await sut.ValidateAsync(appId.Id, workflows); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_generate_errors_for_empty_workflows() - { - var errors = await sut.ValidateAsync(appId.Id, Workflows.Empty); + [Fact] + public async Task Should_not_generate_errors_for_empty_workflows() + { + var errors = await sut.ValidateAsync(appId.Id, Workflows.Empty); - Assert.Empty(errors); - } + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs index 989bf6389f..dad0c0921a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs @@ -13,97 +13,96 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public sealed class ContentCommandMiddlewareTests : HandlerTestBase<ContentDomainObject.State> { - public sealed class ContentCommandMiddlewareTests : HandlerTestBase<ContentDomainObject.State> + private readonly IDomainObjectCache domainObjectCache = A.Fake<IDomainObjectCache>(); + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>(); + private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); + private readonly DomainId contentId = DomainId.NewGuid(); + private readonly Context requestContext; + private readonly ContentCommandMiddleware sut; + + public sealed class MyCommand : SquidexCommand { - private readonly IDomainObjectCache domainObjectCache = A.Fake<IDomainObjectCache>(); - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>(); - private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); - private readonly DomainId contentId = DomainId.NewGuid(); - private readonly Context requestContext; - private readonly ContentCommandMiddleware sut; - - public sealed class MyCommand : SquidexCommand - { - } + } - protected override DomainId Id - { - get => contentId; - } + protected override DomainId Id + { + get => contentId; + } - public ContentCommandMiddlewareTests() - { - requestContext = Context.Anonymous(Mocks.App(AppNamedId)); + public ContentCommandMiddlewareTests() + { + requestContext = Context.Anonymous(Mocks.App(AppNamedId)); - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); - sut = new ContentCommandMiddleware( - domainObjectFactory, - domainObjectCache, - contentEnricher, - contextProvider); - } + sut = new ContentCommandMiddleware( + domainObjectFactory, + domainObjectCache, + contentEnricher, + contextProvider); + } - [Fact] - public async Task Should_not_invoke_enricher_for_other_actual() - { - await HandleAsync(new CreateContent(), 12); + [Fact] + public async Task Should_not_invoke_enricher_for_other_actual() + { + await HandleAsync(new CreateContent(), 12); - A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>._, A<bool>._, requestContext, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>._, A<bool>._, requestContext, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_invoke_enricher_if_already_enriched() - { - var actual = new ContentEntity(); + [Fact] + public async Task Should_not_invoke_enricher_if_already_enriched() + { + var actual = new ContentEntity(); - var context = - await HandleAsync(new CreateContent(), - actual); + var context = + await HandleAsync(new CreateContent(), + actual); - Assert.Same(actual, context.Result<IEnrichedContentEntity>()); + Assert.Same(actual, context.Result<IEnrichedContentEntity>()); - A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>._, A<bool>._, requestContext, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>._, A<bool>._, requestContext, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_enrich_content_actual() - { - var actual = A.Fake<IContentEntity>(); + [Fact] + public async Task Should_enrich_content_actual() + { + var actual = A.Fake<IContentEntity>(); - var enriched = new ContentEntity(); + var enriched = new ContentEntity(); - A.CallTo(() => contentEnricher.EnrichAsync(actual, true, requestContext, A<CancellationToken>._)) - .Returns(enriched); + A.CallTo(() => contentEnricher.EnrichAsync(actual, true, requestContext, A<CancellationToken>._)) + .Returns(enriched); - var context = - await HandleAsync(new CreateContent(), - actual); + var context = + await HandleAsync(new CreateContent(), + actual); - Assert.Same(enriched, context.Result<IEnrichedContentEntity>()); - } + Assert.Same(enriched, context.Result<IEnrichedContentEntity>()); + } - private Task<CommandContext> HandleAsync(ContentCommand command, object actual) - { - command.ContentId = contentId; + private Task<CommandContext> HandleAsync(ContentCommand command, object actual) + { + command.ContentId = contentId; - CreateCommand(command); + CreateCommand(command); - var domainObject = A.Fake<ContentDomainObject>(); + var domainObject = A.Fake<ContentDomainObject>(); - A.CallTo(() => domainObject.ExecuteAsync(A<IAggregateCommand>._, A<CancellationToken>._)) - .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); + A.CallTo(() => domainObject.ExecuteAsync(A<IAggregateCommand>._, A<CancellationToken>._)) + .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); - A.CallTo(() => domainObjectFactory.Create<ContentDomainObject>(command.AggregateId)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<ContentDomainObject>(command.AggregateId)) + .Returns(domainObject); - return HandleAsync(sut, command); - } + return HandleAsync(sut, command); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs index 3304dac4be..d15f5eeb47 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs @@ -26,1011 +26,1010 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public class ContentDomainObjectTests : HandlerTestBase<ContentDomainObject.State> { - public class ContentDomainObjectTests : HandlerTestBase<ContentDomainObject.State> - { - private readonly DomainId contentId = DomainId.NewGuid(); - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(x => x.Wrapping(new DefaultContentWorkflow())); - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly ISchemaEntity schema; - private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); - - private readonly ContentData invalidData = - new ContentData() - .AddField("my-field1", + private readonly DomainId contentId = DomainId.NewGuid(); + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(x => x.Wrapping(new DefaultContentWorkflow())); + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly ISchemaEntity schema; + private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); + + private readonly ContentData invalidData = + new ContentData() + .AddField("my-field1", + new ContentFieldData() + .AddInvariant(JsonValue.Null)) + .AddField("my-field2", new ContentFieldData() - .AddInvariant(JsonValue.Null)) - .AddField("my-field2", - new ContentFieldData() - .AddInvariant(1)); - private readonly ContentData data = - new ContentData() - .AddField("my-field1", - new ContentFieldData() - .AddInvariant(1)); - private readonly ContentData patch = - new ContentData() - .AddField("my-field2", - new ContentFieldData() - .AddInvariant(2)); - private readonly ContentData otherData = - new ContentData() - .AddField("my-field1", - new ContentFieldData() - .AddInvariant(2)) - .AddField("my-field2", - new ContentFieldData() - .AddInvariant(2)); - private readonly ContentData patched; - private readonly ContentDomainObject sut; - - protected override DomainId Id - { - get => DomainId.Combine(AppId, contentId); - } + .AddInvariant(1)); + private readonly ContentData data = + new ContentData() + .AddField("my-field1", + new ContentFieldData() + .AddInvariant(1)); + private readonly ContentData patch = + new ContentData() + .AddField("my-field2", + new ContentFieldData() + .AddInvariant(2)); + private readonly ContentData otherData = + new ContentData() + .AddField("my-field1", + new ContentFieldData() + .AddInvariant(2)) + .AddField("my-field2", + new ContentFieldData() + .AddInvariant(2)); + private readonly ContentData patched; + private readonly ContentDomainObject sut; + + protected override DomainId Id + { + get => DomainId.Combine(AppId, contentId); + } + + public ContentDomainObjectTests() + { + app = Mocks.App(AppNamedId, Language.DE); - public ContentDomainObjectTests() + var scripts = new SchemaScripts { - app = Mocks.App(AppNamedId, Language.DE); - - var scripts = new SchemaScripts - { - Change = "<change-script>", - Create = "<create-script>", - Delete = "<delete-script>", - Update = "<update-script>" - }; - - var schemaDef = - new Schema("my-schema").Publish() - .AddNumber(1, "my-field1", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = true }) - .AddNumber(2, "my-field2", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = false }) - .SetScripts(scripts); - - schema = Mocks.Schema(AppNamedId, SchemaNamedId, schemaDef); - - A.CallTo(() => appProvider.GetAppAsync(AppName, false, default)) - .Returns(app); - - A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId, false, default)) - .Returns((app, schema)); - - A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), default)) - .ReturnsLazily(x => Task.FromResult(x.GetArgument<DataScriptVars>(0)!.Data!)); - - patched = patch.MergeInto(data); - - var log = A.Fake<ILogger<ContentDomainObject>>(); - - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(A.Fake<ILogger<ContentValidator>>()) - .AddSingleton(log) - .AddSingleton(contentWorkflow) - .AddSingleton(contentRepository) - .AddSingleton(scriptEngine) - .AddSingleton(TestUtils.DefaultSerializer) - .AddSingleton<IValidatorsFactory>(new DefaultValidatorsFactory()) - .BuildServiceProvider(); + Change = "<change-script>", + Create = "<create-script>", + Delete = "<delete-script>", + Update = "<update-script>" + }; + + var schemaDef = + new Schema("my-schema").Publish() + .AddNumber(1, "my-field1", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true }) + .AddNumber(2, "my-field2", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = false }) + .SetScripts(scripts); + + schema = Mocks.Schema(AppNamedId, SchemaNamedId, schemaDef); + + A.CallTo(() => appProvider.GetAppAsync(AppName, false, default)) + .Returns(app); + + A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId, false, default)) + .Returns((app, schema)); + + A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), default)) + .ReturnsLazily(x => Task.FromResult(x.GetArgument<DataScriptVars>(0)!.Data!)); + + patched = patch.MergeInto(data); + + var log = A.Fake<ILogger<ContentDomainObject>>(); + + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(A.Fake<ILogger<ContentValidator>>()) + .AddSingleton(log) + .AddSingleton(contentWorkflow) + .AddSingleton(contentRepository) + .AddSingleton(scriptEngine) + .AddSingleton(TestUtils.DefaultSerializer) + .AddSingleton<IValidatorsFactory>(new DefaultValidatorsFactory()) + .BuildServiceProvider(); #pragma warning disable MA0056 // Do not call overridable members in constructor - sut = new ContentDomainObject(Id, PersistenceFactory, log, serviceProvider); + sut = new ContentDomainObject(Id, PersistenceFactory, log, serviceProvider); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - [Fact] - public async Task Command_should_throw_exception_if_content_is_deleted() - { - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); + [Fact] + public async Task Command_should_throw_exception_if_content_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); - await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteUpdateAsync); - } + await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteUpdateAsync); + } - [Fact] - public async Task Create_should_create_events_and_update_data_and_status() - { - var command = new CreateContent { Data = data }; + [Fact] + public async Task Create_should_create_events_and_update_data_and_status() + { + var command = new CreateContent { Data = data }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); - Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Create_should_not_change_status_if_set_to_initial() - { - var command = new CreateContent { Data = data, Status = Status.Draft }; + [Fact] + public async Task Create_should_not_change_status_if_set_to_initial() + { + var command = new CreateContent { Data = data, Status = Status.Draft }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); - Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Create_should_change_status_if_set() - { - var command = new CreateContent { Data = data, Status = Status.Archived }; + [Fact] + public async Task Create_should_change_status_if_set() + { + var command = new CreateContent { Data = data, Status = Status.Archived }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); - Assert.Equal(Status.Archived, sut.Snapshot.Status); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Archived, sut.Snapshot.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Create_should_recreate_deleted_content() - { - var command = new CreateContent { Data = data }; + [Fact] + public async Task Create_should_recreate_deleted_content() + { + var command = new CreateContent { Data = data }; - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); - await PublishAsync(command); - } + await PublishAsync(command); + } - [Fact] - public async Task Create_should_recreate_permanently_deleted_content() - { - var command = new CreateContent { Data = data }; + [Fact] + public async Task Create_should_recreate_permanently_deleted_content() + { + var command = new CreateContent { Data = data }; - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(true); + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(true); - await PublishAsync(command); - } + await PublishAsync(command); + } - [Fact] - public async Task Create_should_throw_exception_if_invalid_data_is_passed() - { - var command = new CreateContent { Data = invalidData }; + [Fact] + public async Task Create_should_throw_exception_if_invalid_data_is_passed() + { + var command = new CreateContent { Data = invalidData }; - await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(command)); - } + await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(command)); + } - [Fact] - public async Task Upsert_should_create_content_if_not_found() - { - var command = new UpsertContent { Data = data }; + [Fact] + public async Task Upsert_should_create_content_if_not_found() + { + var command = new UpsertContent { Data = data }; - var actual = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); - Assert.Equal(Status.Draft, sut.Snapshot.Status); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Draft, sut.Snapshot.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Upsert_should_not_change_status_on_create_if_status_set_to_initial() - { - var command = new UpsertContent { Data = data }; + [Fact] + public async Task Upsert_should_not_change_status_on_create_if_status_set_to_initial() + { + var command = new UpsertContent { Data = data }; - var actual = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); - Assert.Equal(Status.Draft, sut.Snapshot.Status); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Draft, sut.Snapshot.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Upsert_should_change_status_on_create_if_status_set() - { - var command = new UpsertContent { Data = data, Status = Status.Archived }; + [Fact] + public async Task Upsert_should_change_status_on_create_if_status_set() + { + var command = new UpsertContent { Data = data, Status = Status.Archived }; - var actual = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); - Assert.Equal(Status.Archived, sut.Snapshot.Status); + Assert.Equal(data, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Archived, sut.Snapshot.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default)) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Upsert_should_update_content_if_found() - { - var command = new UpsertContent { Data = otherData }; + [Fact] + public async Task Upsert_should_update_content_if_found() + { + var command = new UpsertContent { Data = otherData }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Upsert_should_patch_content_if_found() - { - var command = new UpsertContent { Data = patch, Patch = true }; + [Fact] + public async Task Upsert_should_patch_content_if_found() + { + var command = new UpsertContent { Data = patch, Patch = true }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(patched, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(patched, sut.Snapshot.CurrentVersion.Data); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = patched }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = patched }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Upsert_should_not_change_status_on_update_if_status_set_to_initial() - { - var command = new UpsertContent { Data = otherData, Status = Status.Draft }; + [Fact] + public async Task Upsert_should_not_change_status_on_update_if_status_set_to_initial() + { + var command = new UpsertContent { Data = otherData, Status = Status.Draft }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Upsert_should_change_status_on_update_if_status_set() - { - var command = new UpsertContent { Data = otherData, Status = Status.Archived }; + [Fact] + public async Task Upsert_should_change_status_on_update_if_status_set() + { + var command = new UpsertContent { Data = otherData, Status = Status.Archived }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(CreateContentCommand(command)); + var actual = await PublishAsync(CreateContentCommand(command)); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); - Assert.Equal(Status.Archived, sut.Snapshot.Status); + Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Archived, sut.Snapshot.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Upsert_should_recreate_deleted_content() - { - var command = new UpsertContent { Data = data }; + [Fact] + public async Task Upsert_should_recreate_deleted_content() + { + var command = new UpsertContent { Data = data }; - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); - await PublishAsync(command); + await PublishAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); + } - [Fact] - public async Task Upsert_should_recreate_permanently_deleted_content() - { - var command = new UpsertContent { Data = data }; + [Fact] + public async Task Upsert_should_recreate_permanently_deleted_content() + { + var command = new UpsertContent { Data = data }; - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(true); + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(true); - await PublishAsync(command); + await PublishAsync(command); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) + ); + } - [Fact] - public async Task Update_should_create_events_and_update_data() - { - var command = new UpdateContent { Data = otherData }; + [Fact] + public async Task Update_should_create_events_and_update_data() + { + var command = new UpdateContent { Data = otherData }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Update_should_create_events_and_update_new_version_if_draft_available() - { - var command = new UpdateContent { Data = otherData }; + [Fact] + public async Task Update_should_create_events_and_update_new_version_if_draft_available() + { + var command = new UpdateContent { Data = otherData }; - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteCreateDraftAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteCreateDraftAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(otherData, sut.Snapshot.NewVersion?.Data); + Assert.Equal(otherData, sut.Snapshot.NewVersion?.Data); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Update_should_not_create_event_for_same_data() - { - var command = new UpdateContent { Data = data }; + [Fact] + public async Task Update_should_not_create_event_for_same_data() + { + var command = new UpdateContent { Data = data }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Single(LastEvents); + Assert.Single(LastEvents); - A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<update-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<update-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Update_should_throw_exception_if_invalid_data_is_passed() - { - var command = new UpdateContent { Data = invalidData }; + [Fact] + public async Task Update_should_throw_exception_if_invalid_data_is_passed() + { + var command = new UpdateContent { Data = invalidData }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(command)); - } + await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(command)); + } - [Fact] - public async Task Patch_should_create_events_and_update_data() - { - var command = new PatchContent { Data = patch }; + [Fact] + public async Task Patch_should_create_events_and_update_data() + { + var command = new PatchContent { Data = patch }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.NotEqual(data, sut.Snapshot.CurrentVersion.Data); + Assert.NotEqual(data, sut.Snapshot.CurrentVersion.Data); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = patched }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = patched }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Patch_should_create_events_and_update_new_version_if_draft_available() - { - var command = new PatchContent { Data = patch }; + [Fact] + public async Task Patch_should_create_events_and_update_new_version_if_draft_available() + { + var command = new PatchContent { Data = patch }; - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteCreateDraftAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteCreateDraftAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(patched, sut.Snapshot.NewVersion?.Data); + Assert.Equal(patched, sut.Snapshot.NewVersion?.Data); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = patched }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = patched }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Patch_should_not_create_event_for_same_data() - { - var command = new PatchContent { Data = data }; + [Fact] + public async Task Patch_should_not_create_event_for_same_data() + { + var command = new PatchContent { Data = data }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Single(LastEvents); + Assert.Single(LastEvents); - A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<update-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<update-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_create_events_and_update_status_if_published() - { - var command = new ChangeContentStatus { Status = Status.Archived }; + [Fact] + public async Task ChangeStatus_should_create_events_and_update_status_if_published() + { + var command = new ChangeContentStatus { Status = Status.Archived }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Archived, sut.Snapshot.CurrentVersion.Status); + Assert.Equal(Status.Archived, sut.Snapshot.CurrentVersion.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_create_events_and_update_status_if_changed() - { - var command = new ChangeContentStatus { Status = Status.Archived }; + [Fact] + public async Task ChangeStatus_should_create_events_and_update_status_if_changed() + { + var command = new ChangeContentStatus { Status = Status.Archived }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Archived, sut.Snapshot.CurrentVersion.Status); + Assert.Equal(Status.Archived, sut.Snapshot.CurrentVersion.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_create_events_and_update_status_if_unpublished() - { - var command = new ChangeContentStatus { Status = Status.Draft }; + [Fact] + public async Task ChangeStatus_should_create_events_and_update_status_if_unpublished() + { + var command = new ChangeContentStatus { Status = Status.Draft }; - await ExecuteCreateAsync(); - await ExecutePublishAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); + Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_also_update_if_script_changes_data() - { - var command = new ChangeContentStatus { Status = Status.Draft }; + [Fact] + public async Task ChangeStatus_should_also_update_if_script_changes_data() + { + var command = new ChangeContentStatus { Status = Status.Draft }; - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default)) - .Returns(otherData); + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default)) + .Returns(otherData); - await ExecuteCreateAsync(); - await ExecutePublishAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); - Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); + Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); + Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_create_events_and_update_new_version_if_draft_available() - { - var command = new ChangeContentStatus { Status = Status.Archived }; + [Fact] + public async Task ChangeStatus_should_create_events_and_update_new_version_if_draft_available() + { + var command = new ChangeContentStatus { Status = Status.Archived }; - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteCreateDraftAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteCreateDraftAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Archived, sut.Snapshot.NewVersion?.Status); + Assert.Equal(Status.Archived, sut.Snapshot.NewVersion?.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Change, Status = Status.Archived }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Change, Status = Status.Archived }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_create_events_and_delete_new_version_if_available() - { - var command = new ChangeContentStatus { Status = Status.Published }; + [Fact] + public async Task ChangeStatus_should_create_events_and_delete_new_version_if_available() + { + var command = new ChangeContentStatus { Status = Status.Published }; - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteCreateDraftAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteCreateDraftAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(sut.Snapshot.NewVersion?.Status); + Assert.Null(sut.Snapshot.NewVersion?.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Published, Status = Status.Published }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Published, Status = Status.Published }) + ); - A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Published, Status.Draft), "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Published, Status.Draft), "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangeStatus_create_events_and_set_schedule_if_duetime_set() - { - var dueTime = Instant.MaxValue; + [Fact] + public async Task ChangeStatus_create_events_and_set_schedule_if_duetime_set() + { + var dueTime = Instant.MaxValue; - var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTime }; + var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTime }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); - Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob?.Status); + Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status); + Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob?.Status); - Assert.Equal(dueTime, sut.Snapshot.ScheduleJob?.DueTime); + Assert.Equal(dueTime, sut.Snapshot.ScheduleJob?.DueTime); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) + ); - A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_refresh_properties_and_unset_schedule_if_completed() - { - var dueTime = Instant.MaxValue; + [Fact] + public async Task ChangeStatus_should_refresh_properties_and_unset_schedule_if_completed() + { + var dueTime = Instant.MaxValue; - await ExecuteCreateAsync(); - await ExecuteChangeStatusAsync(Status.Archived, dueTime); + await ExecuteCreateAsync(); + await ExecuteChangeStatusAsync(Status.Archived, dueTime); - var command = new ChangeContentStatus { Status = Status.Archived, StatusJobId = sut.Snapshot.ScheduleJob!.Id }; + var command = new ChangeContentStatus { Status = Status.Archived, StatusJobId = sut.Snapshot.ScheduleJob!.Id }; - A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Archived, User)) - .Returns(true); + A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Archived, User)) + .Returns(true); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(sut.Snapshot.ScheduleJob); + Assert.Null(sut.Snapshot.ScheduleJob); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) + ); - A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_create_events_and_unset_schedule_if_failed() - { - var dueTime = Instant.MaxValue; + [Fact] + public async Task ChangeStatus_should_create_events_and_unset_schedule_if_failed() + { + var dueTime = Instant.MaxValue; - await ExecuteCreateAsync(); - await ExecuteChangeStatusAsync(Status.Published, dueTime); + await ExecuteCreateAsync(); + await ExecuteChangeStatusAsync(Status.Published, dueTime); - var command = new ChangeContentStatus { Status = Status.Published, StatusJobId = sut.Snapshot.ScheduleJob!.Id }; + var command = new ChangeContentStatus { Status = Status.Published, StatusJobId = sut.Snapshot.ScheduleJob!.Id }; - A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Published, User)) - .Returns(false); + A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Published, User)) + .Returns(false); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(sut.Snapshot.ScheduleJob); + Assert.Null(sut.Snapshot.ScheduleJob); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentSchedulingCancelled()) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentSchedulingCancelled()) + ); - A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task ChangeStatus_should_throw_exception_if_referenced_by_other_item() - { - var command = new ChangeContentStatus { Status = Status.Draft, CheckReferrers = true }; + [Fact] + public async Task ChangeStatus_should_throw_exception_if_referenced_by_other_item() + { + var command = new ChangeContentStatus { Status = Status.Draft, CheckReferrers = true }; - await ExecuteCreateAsync(); - await ExecuteChangeStatusAsync(Status.Published); + await ExecuteCreateAsync(); + await ExecuteChangeStatusAsync(Status.Published); - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId, SearchScope.All, A<CancellationToken>._)) - .Returns(true); + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId, SearchScope.All, A<CancellationToken>._)) + .Returns(true); - await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command)); - } + await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command)); + } - [Fact] - public async Task ChangeStatus_should_not_throw_exception_if_referenced_by_other_item_but_forced() - { - var command = new ChangeContentStatus { Status = Status.Draft, CheckReferrers = false }; + [Fact] + public async Task ChangeStatus_should_not_throw_exception_if_referenced_by_other_item_but_forced() + { + var command = new ChangeContentStatus { Status = Status.Draft, CheckReferrers = false }; - await ExecuteCreateAsync(); - await ExecuteChangeStatusAsync(Status.Published); + await ExecuteCreateAsync(); + await ExecuteChangeStatusAsync(Status.Published); - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId, SearchScope.Published, A<CancellationToken>._)) - .Returns(true); + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId, SearchScope.Published, A<CancellationToken>._)) + .Returns(true); - await PublishAsync(command); - } + await PublishAsync(command); + } - [Fact] - public async Task CancelContentSchedule_create_events_and_unset_schedule() - { - var command = new CancelContentSchedule(); + [Fact] + public async Task CancelContentSchedule_create_events_and_unset_schedule() + { + var command = new CancelContentSchedule(); - await ExecuteCreateAsync(); - await ExecuteChangeStatusAsync(Status.Published, SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(1))); + await ExecuteCreateAsync(); + await ExecuteChangeStatusAsync(Status.Published, SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(1))); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(sut.Snapshot.ScheduleJob); + Assert.Null(sut.Snapshot.ScheduleJob); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentSchedulingCancelled()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentSchedulingCancelled()) + ); + } - [Fact] - public async Task Validate_should_not_update_state() - { - await ExecuteCreateAsync(); + [Fact] + public async Task Validate_should_not_update_state() + { + await ExecuteCreateAsync(); - var command = new ValidateContent(); + var command = new ValidateContent(); - await PublishAsync(command); + await PublishAsync(command); - Assert.Equal(0, sut.Version); - } + Assert.Equal(0, sut.Version); + } - [Fact] - public async Task Delete_should_create_events_and_update_deleted_flag() - { - await ExecuteCreateAsync(); + [Fact] + public async Task Delete_should_create_events_and_update_deleted_flag() + { + await ExecuteCreateAsync(); - var command = new DeleteContent(); + var command = new DeleteContent(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); - Assert.True(sut.Snapshot.IsDeleted); + Assert.True(sut.Snapshot.IsDeleted); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentDeleted()) - ); + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDeleted()) + ); - A.CallTo(() => scriptEngine.ExecuteAsync(DataScriptVars(data, null, Status.Draft), "<delete-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.ExecuteAsync(DataScriptVars(data, null, Status.Draft), "<delete-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Delete_should_not_create_events_if_permanent() - { - await ExecuteCreateAsync(); + [Fact] + public async Task Delete_should_not_create_events_if_permanent() + { + await ExecuteCreateAsync(); - var command = new DeleteContent { Permanent = true }; + var command = new DeleteContent { Permanent = true }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); - Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); - Assert.Empty(LastEvents); + Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); + Assert.Empty(LastEvents); - A.CallTo(() => scriptEngine.ExecuteAsync(DataScriptVars(data, null, Status.Draft), "<delete-script>", ScriptOptions(), default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.ExecuteAsync(DataScriptVars(data, null, Status.Draft), "<delete-script>", ScriptOptions(), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Delete_should_throw_exception_if_referenced_by_other_item() - { - await ExecuteCreateAsync(); + [Fact] + public async Task Delete_should_throw_exception_if_referenced_by_other_item() + { + await ExecuteCreateAsync(); - var command = new DeleteContent { CheckReferrers = true }; + var command = new DeleteContent { CheckReferrers = true }; - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId, SearchScope.All, A<CancellationToken>._)) - .Returns(true); + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId, SearchScope.All, A<CancellationToken>._)) + .Returns(true); - await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command)); - } + await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command)); + } - [Fact] - public async Task Delete_should_not_throw_exception_if_referenced_by_other_item_but_forced() - { - var command = new DeleteContent(); + [Fact] + public async Task Delete_should_not_throw_exception_if_referenced_by_other_item_but_forced() + { + var command = new DeleteContent(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId, SearchScope.All, A<CancellationToken>._)) - .Returns(true); + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId, SearchScope.All, A<CancellationToken>._)) + .Returns(true); - await PublishAsync(command); - } + await PublishAsync(command); + } - [Fact] - public async Task CreateDraft_should_create_events_and_update_new_state() - { - var command = new CreateContentDraft(); + [Fact] + public async Task CreateDraft_should_create_events_and_update_new_state() + { + var command = new CreateContentDraft(); - await ExecuteCreateAsync(); - await ExecutePublishAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(Status.Draft, sut.Snapshot.NewVersion?.Status); + Assert.Equal(Status.Draft, sut.Snapshot.NewVersion?.Status); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentDraftCreated { Status = Status.Draft }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDraftCreated { Status = Status.Draft }) + ); + } - [Fact] - public async Task DeleteDraft_should_create_events_and_delete_new_version() - { - var command = new DeleteContentDraft(); + [Fact] + public async Task DeleteDraft_should_create_events_and_delete_new_version() + { + var command = new DeleteContentDraft(); - await ExecuteCreateAsync(); - await ExecutePublishAsync(); - await ExecuteCreateDraftAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + await ExecuteCreateDraftAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(sut.Snapshot.NewVersion); + Assert.Null(sut.Snapshot.NewVersion); - LastEvents - .ShouldHaveSameEvents( - CreateContentEvent(new ContentDraftDeleted()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDraftDeleted()) + ); + } - private Task ExecuteCreateAsync() - { - return PublishAsync(new CreateContent { Data = data }); - } + private Task ExecuteCreateAsync() + { + return PublishAsync(new CreateContent { Data = data }); + } - private Task ExecuteUpdateAsync() - { - return PublishAsync(new UpdateContent { Data = otherData }); - } + private Task ExecuteUpdateAsync() + { + return PublishAsync(new UpdateContent { Data = otherData }); + } - private Task ExecuteCreateDraftAsync() - { - return PublishAsync(new CreateContentDraft()); - } + private Task ExecuteCreateDraftAsync() + { + return PublishAsync(new CreateContentDraft()); + } - private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null) - { - return PublishAsync(new ChangeContentStatus { Status = status, DueTime = dueTime }); - } + private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null) + { + return PublishAsync(new ChangeContentStatus { Status = status, DueTime = dueTime }); + } - private Task ExecuteDeleteAsync(bool permanent = false) - { - return PublishAsync(CreateContentCommand(new DeleteContent { Permanent = permanent })); - } + private Task ExecuteDeleteAsync(bool permanent = false) + { + return PublishAsync(CreateContentCommand(new DeleteContent { Permanent = permanent })); + } - private Task ExecutePublishAsync() - { - return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); - } + private Task ExecutePublishAsync() + { + return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); + } - private static ScriptOptions ScriptOptions() - { - return A<ScriptOptions>.That.Matches(x => x.CanDisallow && x.CanReject && x.AsContext); - } + private static ScriptOptions ScriptOptions() + { + return A<ScriptOptions>.That.Matches(x => x.CanDisallow && x.CanReject && x.AsContext); + } - private DataScriptVars DataScriptVars(ContentData? newData, ContentData? oldData, Status newStatus) - { - return A<DataScriptVars>.That.Matches(x => Matches(x, newData, oldData, newStatus, default)); - } + private DataScriptVars DataScriptVars(ContentData? newData, ContentData? oldData, Status newStatus) + { + return A<DataScriptVars>.That.Matches(x => Matches(x, newData, oldData, newStatus, default)); + } - private DataScriptVars DataScriptVars(ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus) - { - return A<DataScriptVars>.That.Matches(x => Matches(x, newData, oldData, newStatus, oldStatus)); - } + private DataScriptVars DataScriptVars(ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus) + { + return A<DataScriptVars>.That.Matches(x => Matches(x, newData, oldData, newStatus, oldStatus)); + } - private bool Matches(DataScriptVars x, ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus) - { - return - Equals(x["contentId"], contentId) && - Equals(x["data"], newData) && - Equals(x["dataOld"], oldData) && - Equals(x["status"], newStatus) && - Equals(x["statusOld"], oldStatus) && - Equals(x["user"], User); - } - - private T CreateContentEvent<T>(T @event) where T : ContentEvent - { - @event.ContentId = contentId; + private bool Matches(DataScriptVars x, ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus) + { + return + Equals(x["contentId"], contentId) && + Equals(x["data"], newData) && + Equals(x["dataOld"], oldData) && + Equals(x["status"], newStatus) && + Equals(x["statusOld"], oldStatus) && + Equals(x["user"], User); + } - return CreateEvent(@event); - } + private T CreateContentEvent<T>(T @event) where T : ContentEvent + { + @event.ContentId = contentId; - private T CreateContentCommand<T>(T command) where T : ContentCommand - { - command.ContentId = contentId; + return CreateEvent(@event); + } - return CreateCommand(command); - } + private T CreateContentCommand<T>(T command) where T : ContentCommand + { + command.ContentId = contentId; - private async Task<object> PublishAsync(ContentCommand command) - { - var actual = await sut.ExecuteAsync(CreateContentCommand(command), default); + return CreateCommand(command); + } + + private async Task<object> PublishAsync(ContentCommand command) + { + var actual = await sut.ExecuteAsync(CreateContentCommand(command), default); - return actual.Payload; - } + return actual.Payload; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs index 7bf6f54382..4df2f8b95b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs @@ -20,584 +20,583 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject; + +public class ContentsBulkUpdateCommandMiddlewareTests { - public class ContentsBulkUpdateCommandMiddlewareTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); + private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); + private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly NamedId<DomainId> schemaCustomId = NamedId.Of(DomainId.NewGuid(), "my-schema2"); + private readonly Instant time = Instant.FromDateTimeUtc(DateTime.UtcNow); + private readonly ContentsBulkUpdateCommandMiddleware sut; + + public ContentsBulkUpdateCommandMiddlewareTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); - private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); - private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly NamedId<DomainId> schemaCustomId = NamedId.Of(DomainId.NewGuid(), "my-schema2"); - private readonly Instant time = Instant.FromDateTimeUtc(DateTime.UtcNow); - private readonly ContentsBulkUpdateCommandMiddleware sut; - - public ContentsBulkUpdateCommandMiddlewareTests() - { - ct = cts.Token; + ct = cts.Token; - var log = A.Fake<ILogger<ContentsBulkUpdateCommandMiddleware>>(); + var log = A.Fake<ILogger<ContentsBulkUpdateCommandMiddleware>>(); - sut = new ContentsBulkUpdateCommandMiddleware(contentQuery, contextProvider, log); - } + sut = new ContentsBulkUpdateCommandMiddleware(contentQuery, contextProvider, log); + } - [Fact] - public async Task Should_do_nothing_if_jobs_is_null() - { - var command = new BulkUpdateContents(); + [Fact] + public async Task Should_do_nothing_if_jobs_is_null() + { + var command = new BulkUpdateContents(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_do_nothing_if_jobs_is_empty() - { - var command = new BulkUpdateContents { Jobs = Array.Empty<BulkUpdateJob>() }; + [Fact] + public async Task Should_do_nothing_if_jobs_is_empty() + { + var command = new BulkUpdateContents { Jobs = Array.Empty<BulkUpdateJob>() }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_throw_exception_if_content_cannot_be_resolved() - { - SetupContext(PermissionIds.AppContentsUpdateOwn); + [Fact] + public async Task Should_throw_exception_if_content_cannot_be_resolved() + { + SetupContext(PermissionIds.AppContentsUpdateOwn); - CreateTestData(true); + CreateTestData(true); - var command = BulkCommand(BulkUpdateContentType.ChangeStatus); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainObjectNotFoundException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainObjectNotFoundException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_throw_exception_if_query_resolves_multiple_contents() - { - var requestContext = SetupContext(PermissionIds.AppContentsUpdateOwn); + [Fact] + public async Task Should_throw_exception_if_query_resolves_multiple_contents() + { + var requestContext = SetupContext(PermissionIds.AppContentsUpdateOwn); - var (id, _, query) = CreateTestData(true); + var (id, _, query) = CreateTestData(true); - A.CallTo(() => contentQuery.QueryAsync( - A<Context>.That.Matches(x => - x.ShouldSkipCleanup() && - x.ShouldSkipContentEnrichment() && - x.ShouldSkipTotal()), - schemaId.Name, A<Q>.That.Matches(x => x.JsonQuery == query), ct)) - .Returns(ResultList.CreateFrom(2, CreateContent(id), CreateContent(id))); + A.CallTo(() => contentQuery.QueryAsync( + A<Context>.That.Matches(x => + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.ShouldSkipTotal()), + schemaId.Name, A<Q>.That.Matches(x => x.JsonQuery == query), ct)) + .Returns(ResultList.CreateFrom(2, CreateContent(id), CreateContent(id))); - var command = BulkCommand(BulkUpdateContentType.ChangeStatus, new BulkUpdateJob { Query = query }); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus, new BulkUpdateJob { Query = query }); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_upsert_content_with_resolved_id() - { - var requestContext = SetupContext(PermissionIds.AppContentsUpsert); + [Fact] + public async Task Should_upsert_content_with_resolved_id() + { + var requestContext = SetupContext(PermissionIds.AppContentsUpsert); - var (id, data, query) = CreateTestData(true); + var (id, data, query) = CreateTestData(true); - A.CallTo(() => contentQuery.QueryAsync( - A<Context>.That.Matches(x => - x.ShouldSkipCleanup() && - x.ShouldSkipContentEnrichment() && - x.ShouldSkipTotal()), - schemaId.Name, A<Q>.That.Matches(x => x.JsonQuery == query), ct)) - .Returns(ResultList.CreateFrom(1, CreateContent(id))); + A.CallTo(() => contentQuery.QueryAsync( + A<Context>.That.Matches(x => + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.ShouldSkipTotal()), + schemaId.Name, A<Q>.That.Matches(x => x.JsonQuery == query), ct)) + .Returns(ResultList.CreateFrom(1, CreateContent(id))); - var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Query = query, Data = data }); + var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Query = query, Data = data }); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id), ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id), ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_upsert_content_with_resolved_ids() - { - var requestContext = SetupContext(PermissionIds.AppContentsUpsert); + [Fact] + public async Task Should_upsert_content_with_resolved_ids() + { + var requestContext = SetupContext(PermissionIds.AppContentsUpsert); - var (_, data, query) = CreateTestData(true); + var (_, data, query) = CreateTestData(true); - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - A.CallTo(() => contentQuery.QueryAsync( - A<Context>.That.Matches(x => - x.ShouldSkipCleanup() && - x.ShouldSkipContentEnrichment() && - x.ShouldSkipTotal()), - schemaId.Name, A<Q>.That.Matches(x => x.JsonQuery == query), ct)) - .Returns(ResultList.CreateFrom(2, - CreateContent(id1), - CreateContent(id2))); + A.CallTo(() => contentQuery.QueryAsync( + A<Context>.That.Matches(x => + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.ShouldSkipTotal()), + schemaId.Name, A<Q>.That.Matches(x => x.JsonQuery == query), ct)) + .Returns(ResultList.CreateFrom(2, + CreateContent(id1), + CreateContent(id2))); - var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Query = query, Data = data }); + var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Query = query, Data = data }); - command.Jobs![0].ExpectedCount = 2; + command.Jobs![0].ExpectedCount = 2; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Equal(2, actual.Count); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id1 && x.Exception == null); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id2 && x.Exception == null); + Assert.Equal(2, actual.Count); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id1 && x.Exception == null); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id2 && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id1), ct)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id1), ct)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id2), ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id2), ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_upsert_content_with_random_id_if_no_query_and_id_defined() - { - SetupContext(PermissionIds.AppContentsUpsert); + [Fact] + public async Task Should_upsert_content_with_random_id_if_no_query_and_id_defined() + { + SetupContext(PermissionIds.AppContentsUpsert); - var (_, data, _) = CreateTestData(false); + var (_, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }); + var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId != default), ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId != default), ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_upsert_content_with_random_id_if_query_returns_no_actual() - { - SetupContext(PermissionIds.AppContentsUpsert); + [Fact] + public async Task Should_upsert_content_with_random_id_if_query_returns_no_actual() + { + SetupContext(PermissionIds.AppContentsUpsert); - var (_, data, query) = CreateTestData(true); + var (_, data, query) = CreateTestData(true); - var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Query = query, Data = data }); + var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Query = query, Data = data }); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId != default), ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId != default), ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_upsert_content_if_id_defined() - { - SetupContext(PermissionIds.AppContentsUpsert); + [Fact] + public async Task Should_upsert_content_if_id_defined() + { + SetupContext(PermissionIds.AppContentsUpsert); - var (id, data, _) = CreateTestData(false); + var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }, id); + var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id), ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id), ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_upsert_content_with_custom_id() - { - SetupContext(PermissionIds.AppContentsUpsert); + [Fact] + public async Task Should_upsert_content_with_custom_id() + { + SetupContext(PermissionIds.AppContentsUpsert); - var (id, data, _) = CreateTestData(true); + var (id, data, _) = CreateTestData(true); - var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }, id); + var command = BulkCommand(BulkUpdateContentType.Upsert, new BulkUpdateJob { Data = data }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id), ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id), ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_create_content() - { - SetupContext(PermissionIds.AppContentsCreate); + [Fact] + public async Task Should_create_content() + { + SetupContext(PermissionIds.AppContentsCreate); - var (id, data, _) = CreateTestData(false); + var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Create, new BulkUpdateJob { Data = data }, id); + var command = BulkCommand(BulkUpdateContentType.Create, new BulkUpdateJob { Data = data }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<CreateContent>.That.Matches(x => x.ContentId == id && x.Data == data), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<CreateContent>.That.Matches(x => x.ContentId == id && x.Data == data), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_creating() - { - SetupContext(PermissionIds.AppContentsReadOwn); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_creating() + { + SetupContext(PermissionIds.AppContentsReadOwn); - var (id, data, _) = CreateTestData(false); + var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Create, new BulkUpdateJob { Data = data }, id); + var command = BulkCommand(BulkUpdateContentType.Create, new BulkUpdateJob { Data = data }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_update_content() - { - SetupContext(PermissionIds.AppContentsUpdateOwn); + [Fact] + public async Task Should_update_content() + { + SetupContext(PermissionIds.AppContentsUpdateOwn); - var (id, data, _) = CreateTestData(false); + var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Update, new BulkUpdateJob { Data = data }, id); + var command = BulkCommand(BulkUpdateContentType.Update, new BulkUpdateJob { Data = data }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<UpdateContent>.That.Matches(x => x.ContentId == id && x.Data == data), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<UpdateContent>.That.Matches(x => x.ContentId == id && x.Data == data), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_updating() - { - SetupContext(PermissionIds.AppContentsReadOwn); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_updating() + { + SetupContext(PermissionIds.AppContentsReadOwn); - var (id, data, _) = CreateTestData(false); + var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Update, new BulkUpdateJob { Data = data }, id); + var command = BulkCommand(BulkUpdateContentType.Update, new BulkUpdateJob { Data = data }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_patch_content() - { - SetupContext(PermissionIds.AppContentsUpdateOwn); + [Fact] + public async Task Should_patch_content() + { + SetupContext(PermissionIds.AppContentsUpdateOwn); - var (id, data, _) = CreateTestData(false); + var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Patch, new BulkUpdateJob { Data = data }, id); + var command = BulkCommand(BulkUpdateContentType.Patch, new BulkUpdateJob { Data = data }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<PatchContent>.That.Matches(x => x.ContentId == id && x.Data == data), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<PatchContent>.That.Matches(x => x.ContentId == id && x.Data == data), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_patching() - { - SetupContext(PermissionIds.AppContentsReadOwn); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_patching() + { + SetupContext(PermissionIds.AppContentsReadOwn); - var (id, data, _) = CreateTestData(false); + var (id, data, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob { Data = data }, id); + var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob { Data = data }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_change_content_status() - { - SetupContext(PermissionIds.AppContentsChangeStatusOwn); + [Fact] + public async Task Should_change_content_status() + { + SetupContext(PermissionIds.AppContentsChangeStatusOwn); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<ChangeContentStatus>.That.Matches(x => x.ContentId == id && x.DueTime == null), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<ChangeContentStatus>.That.Matches(x => x.ContentId == id && x.DueTime == null), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_change_content_status_with_due_time() - { - SetupContext(PermissionIds.AppContentsChangeStatusOwn); + [Fact] + public async Task Should_change_content_status_with_due_time() + { + SetupContext(PermissionIds.AppContentsChangeStatusOwn); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.ChangeStatus, new BulkUpdateJob { DueTime = time }, id); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus, new BulkUpdateJob { DueTime = time }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<ChangeContentStatus>.That.Matches(x => x.ContentId == id && x.DueTime == time), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<ChangeContentStatus>.That.Matches(x => x.ContentId == id && x.DueTime == time), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_changing_status() - { - SetupContext(PermissionIds.AppContentsReadOwn); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_changing_status() + { + SetupContext(PermissionIds.AppContentsReadOwn); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id); + var command = BulkCommand(BulkUpdateContentType.ChangeStatus, id: id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_validate_content() - { - SetupContext(PermissionIds.AppContentsReadOwn); + [Fact] + public async Task Should_validate_content() + { + SetupContext(PermissionIds.AppContentsReadOwn); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Validate, id: id); + var command = BulkCommand(BulkUpdateContentType.Validate, id: id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<ValidateContent>.That.Matches(x => x.ContentId == id), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<ValidateContent>.That.Matches(x => x.ContentId == id), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_validation() - { - SetupContext(PermissionIds.AppContentsDeleteOwn); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_validation() + { + SetupContext(PermissionIds.AppContentsDeleteOwn); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Validate, id: id); + var command = BulkCommand(BulkUpdateContentType.Validate, id: id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_delete_content() - { - SetupContext(PermissionIds.AppContentsDeleteOwn); + [Fact] + public async Task Should_delete_content() + { + SetupContext(PermissionIds.AppContentsDeleteOwn); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Delete, id: id); + var command = BulkCommand(BulkUpdateContentType.Delete, id: id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<DeleteContent>.That.Matches(x => x.ContentId == id), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<DeleteContent>.That.Matches(x => x.ContentId == id), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_security_exception_if_user_has_no_permission_for_deletion() - { - SetupContext(PermissionIds.AppContentsReadOwn); + [Fact] + public async Task Should_throw_security_exception_if_user_has_no_permission_for_deletion() + { + SetupContext(PermissionIds.AppContentsReadOwn); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Delete, id: id); + var command = BulkCommand(BulkUpdateContentType.Delete, id: id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_override_schema_name() - { - SetupContext(PermissionIds.AppContentsDeleteOwn); + [Fact] + public async Task Should_override_schema_name() + { + SetupContext(PermissionIds.AppContentsDeleteOwn); - A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A<Context>._, schemaCustomId.Name, ct)) - .Returns(Mocks.Schema(appId, schemaCustomId)); + A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A<Context>._, schemaCustomId.Name, ct)) + .Returns(Mocks.Schema(appId, schemaCustomId)); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob { Schema = schemaCustomId.Name }, id); + var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob { Schema = schemaCustomId.Name }, id); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync( - A<DeleteContent>.That.Matches(x => x.SchemaId == schemaCustomId), ct)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<DeleteContent>.That.Matches(x => x.SchemaId == schemaCustomId), ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_exception_if_schema_name_not_defined() - { - SetupContext(PermissionIds.AppContentsDeleteOwn); + [Fact] + public async Task Should_throw_exception_if_schema_name_not_defined() + { + SetupContext(PermissionIds.AppContentsDeleteOwn); - var (id, _, _) = CreateTestData(false); + var (id, _, _) = CreateTestData(false); - var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob(), id); + var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob(), id); - // Unset schema id, so that no schema id is set for the command. - command.SchemaId = null!; + // Unset schema id, so that no schema id is set for the command. + command.SchemaId = null!; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - Assert.Single(actual); - Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainObjectNotFoundException); + Assert.Single(actual); + Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainObjectNotFoundException); - A.CallTo(() => commandBus.PublishAsync( - A<DeleteContent>.That.Matches(x => x.SchemaId == schemaCustomId), ct)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<DeleteContent>.That.Matches(x => x.SchemaId == schemaCustomId), ct)) + .MustNotHaveHappened(); + } - private async Task<BulkUpdateResult> PublishAsync(ICommand command) - { - var context = new CommandContext(command, commandBus); + private async Task<BulkUpdateResult> PublishAsync(ICommand command) + { + var context = new CommandContext(command, commandBus); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - return (context.PlainResult as BulkUpdateResult)!; - } + return (context.PlainResult as BulkUpdateResult)!; + } - private BulkUpdateContents BulkCommand(BulkUpdateContentType type, BulkUpdateJob? job = null, DomainId? id = null) - { - job ??= new BulkUpdateJob(); - job.Id = id; - job.Type = type; + private BulkUpdateContents BulkCommand(BulkUpdateContentType type, BulkUpdateJob? job = null, DomainId? id = null) + { + job ??= new BulkUpdateJob(); + job.Id = id; + job.Type = type; - return new BulkUpdateContents - { - AppId = appId, - Jobs = new[] - { - job - }, - SchemaId = schemaId - }; - } - - private Context SetupContext(string id) + return new BulkUpdateContents { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + AppId = appId, + Jobs = new[] + { + job + }, + SchemaId = schemaId + }; + } - claimsIdentity.AddClaim( - new Claim(SquidexClaimTypes.Permissions, - PermissionIds.ForApp(id, appId.Name, schemaId.Name).Id)); + private Context SetupContext(string id) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - claimsIdentity.AddClaim( - new Claim(SquidexClaimTypes.Permissions, - PermissionIds.ForApp(id, appId.Name, schemaCustomId.Name).Id)); + claimsIdentity.AddClaim( + new Claim(SquidexClaimTypes.Permissions, + PermissionIds.ForApp(id, appId.Name, schemaId.Name).Id)); - var requestContext = new Context(claimsPrincipal, Mocks.App(appId)); + claimsIdentity.AddClaim( + new Claim(SquidexClaimTypes.Permissions, + PermissionIds.ForApp(id, appId.Name, schemaCustomId.Name).Id)); - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); + var requestContext = new Context(claimsPrincipal, Mocks.App(appId)); - return requestContext; - } + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); - private static (DomainId Id, ContentData Data, Query<JsonValue>? Query) CreateTestData(bool withQuery) - { - Query<JsonValue>? query = withQuery ? new Query<JsonValue>() : null; + return requestContext; + } - var data = - new ContentData() - .AddField("value", - new ContentFieldData() - .AddInvariant(1)); + private static (DomainId Id, ContentData Data, Query<JsonValue>? Query) CreateTestData(bool withQuery) + { + Query<JsonValue>? query = withQuery ? new Query<JsonValue>() : null; - return (DomainId.NewGuid(), data, query); - } + var data = + new ContentData() + .AddField("value", + new ContentFieldData() + .AddInvariant(1)); - private static IEnrichedContentEntity CreateContent(DomainId id) - { - return new ContentEntity { Id = id }; - } + return (DomainId.NewGuid(), data, query); + } + + private static IEnrichedContentEntity CreateContent(DomainId id) + { + return new ContentEntity { Id = id }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs index 008fe64606..9b66240cc1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs @@ -22,404 +22,403 @@ #pragma warning disable CA2012 // Use ValueTasks correctly -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards; + +public class GuardContentTests : IClassFixture<TranslationsFixture> { - public class GuardContentTests : IClassFixture<TranslationsFixture> - { - private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(); - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly ISchemaEntity normalSchema; - private readonly ISchemaEntity normalUnpublishedSchema; - private readonly ISchemaEntity singletonSchema; - private readonly ISchemaEntity singletonUnpublishedSchema; - private readonly ISchemaEntity componentSchema; - private readonly RefToken actor = RefToken.User("123"); - - public GuardContentTests() - { - normalUnpublishedSchema = - Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); + private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(); + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly ISchemaEntity normalSchema; + private readonly ISchemaEntity normalUnpublishedSchema; + private readonly ISchemaEntity singletonSchema; + private readonly ISchemaEntity singletonUnpublishedSchema; + private readonly ISchemaEntity componentSchema; + private readonly RefToken actor = RefToken.User("123"); + + public GuardContentTests() + { + normalUnpublishedSchema = + Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); - normalSchema = - Mocks.Schema(appId, schemaId, new Schema(schemaId.Name).Publish()); + normalSchema = + Mocks.Schema(appId, schemaId, new Schema(schemaId.Name).Publish()); - singletonUnpublishedSchema = - Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton)); + singletonUnpublishedSchema = + Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton)); - singletonSchema = - Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton).Publish()); + singletonSchema = + Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Singleton).Publish()); - componentSchema = - Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Component).Publish()); - } + componentSchema = + Mocks.Schema(appId, schemaId, new Schema(schemaId.Name, type: SchemaType.Component).Publish()); + } - [Fact] - public void Should_throw_exception_if_creating_content_for_unpublished_schema() - { - var operation = Operation(CreateContent(Status.Draft), normalUnpublishedSchema); + [Fact] + public void Should_throw_exception_if_creating_content_for_unpublished_schema() + { + var operation = Operation(CreateContent(Status.Draft), normalUnpublishedSchema); - Assert.Throws<DomainException>(() => operation.MustNotCreateForUnpublishedSchema()); - } + Assert.Throws<DomainException>(() => operation.MustNotCreateForUnpublishedSchema()); + } - [Fact] - public void Should_not_throw_exception_if_creating_content_for_unpublished_singleton() - { - var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), singletonUnpublishedSchema); + [Fact] + public void Should_not_throw_exception_if_creating_content_for_unpublished_singleton() + { + var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), singletonUnpublishedSchema); - operation.MustNotCreateSingleton(); - } + operation.MustNotCreateSingleton(); + } - [Fact] - public void Should_not_throw_exception_if_creating_content_for_published_schema() - { - var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), normalSchema); + [Fact] + public void Should_not_throw_exception_if_creating_content_for_published_schema() + { + var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), normalSchema); - operation.MustNotCreateSingleton(); - } + operation.MustNotCreateSingleton(); + } - [Fact] - public void Should_not_throw_exception_if_creating_content_for_published_singleton_schema() - { - var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), singletonSchema); + [Fact] + public void Should_not_throw_exception_if_creating_content_for_published_singleton_schema() + { + var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), singletonSchema); - operation.MustNotCreateSingleton(); - } + operation.MustNotCreateSingleton(); + } - [Fact] - public void Should_throw_exception_if_creating_singleton_content() - { - var operation = Operation(CreateContent(Status.Draft), singletonSchema); + [Fact] + public void Should_throw_exception_if_creating_singleton_content() + { + var operation = Operation(CreateContent(Status.Draft), singletonSchema); - Assert.Throws<DomainException>(() => operation.MustNotCreateSingleton()); - } + Assert.Throws<DomainException>(() => operation.MustNotCreateSingleton()); + } - [Fact] - public void Should_throw_exception_if_creating_component_content() - { - var operation = Operation(CreateContent(Status.Draft), componentSchema); + [Fact] + public void Should_throw_exception_if_creating_component_content() + { + var operation = Operation(CreateContent(Status.Draft), componentSchema); - Assert.Throws<DomainException>(() => operation.MustNotCreateComponent()); - } + Assert.Throws<DomainException>(() => operation.MustNotCreateComponent()); + } - [Fact] - public void Should_not_throw_exception_if_creating_singleton_content_with_schema_id() - { - var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), singletonSchema); + [Fact] + public void Should_not_throw_exception_if_creating_singleton_content_with_schema_id() + { + var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), singletonSchema); - operation.MustNotCreateSingleton(); - } + operation.MustNotCreateSingleton(); + } - [Fact] - public void Should_not_throw_exception_if_creating_non_singleton_content() - { - var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), normalSchema); + [Fact] + public void Should_not_throw_exception_if_creating_non_singleton_content() + { + var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), normalSchema); - operation.MustNotCreateSingleton(); - } + operation.MustNotCreateSingleton(); + } - [Fact] - public void Should_throw_exception_if_changing_singleton_content() - { - var operation = Operation(CreateContent(Status.Draft), singletonSchema); + [Fact] + public void Should_throw_exception_if_changing_singleton_content() + { + var operation = Operation(CreateContent(Status.Draft), singletonSchema); - Assert.Throws<DomainException>(() => operation.MustNotChangeSingleton(Status.Archived)); - } + Assert.Throws<DomainException>(() => operation.MustNotChangeSingleton(Status.Archived)); + } - [Fact] - public void Should_not_throw_exception_if_changing_singleton_to_published() - { - var operation = Operation(CreateDraftContent(Status.Published, singletonSchema.Id), singletonSchema); + [Fact] + public void Should_not_throw_exception_if_changing_singleton_to_published() + { + var operation = Operation(CreateDraftContent(Status.Published, singletonSchema.Id), singletonSchema); - operation.MustNotChangeSingleton(Status.Published); - } + operation.MustNotChangeSingleton(Status.Published); + } - [Fact] - public void Should_not_throw_exception_if_changing_non_singleton_content() - { - var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), normalSchema); + [Fact] + public void Should_not_throw_exception_if_changing_non_singleton_content() + { + var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), normalSchema); - operation.MustNotChangeSingleton(Status.Archived); - } + operation.MustNotChangeSingleton(Status.Archived); + } - [Fact] - public void Should_throw_exception_if_deleting_singleton_content() - { - var operation = Operation(CreateContent(Status.Draft), singletonSchema); + [Fact] + public void Should_throw_exception_if_deleting_singleton_content() + { + var operation = Operation(CreateContent(Status.Draft), singletonSchema); - Assert.Throws<DomainException>(() => operation.MustNotDeleteSingleton()); - } + Assert.Throws<DomainException>(() => operation.MustNotDeleteSingleton()); + } - [Fact] - public void Should_not_throw_exception_if_deleting_non_singleton_content() - { - var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), normalSchema); + [Fact] + public void Should_not_throw_exception_if_deleting_non_singleton_content() + { + var operation = Operation(CreateContent(Status.Draft, singletonSchema.Id), normalSchema); - operation.MustNotDeleteSingleton(); - } + operation.MustNotDeleteSingleton(); + } - [Fact] - public void Should_throw_exception_if_draft_already_created() - { - var operation = Operation(CreateDraftContent(Status.Draft), normalSchema); + [Fact] + public void Should_throw_exception_if_draft_already_created() + { + var operation = Operation(CreateDraftContent(Status.Draft), normalSchema); - Assert.Throws<DomainException>(() => operation.MustCreateDraft()); - } + Assert.Throws<DomainException>(() => operation.MustCreateDraft()); + } - [Fact] - public void Should_throw_exception_if_draft_cannot_be_created() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public void Should_throw_exception_if_draft_cannot_be_created() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - Assert.Throws<DomainException>(() => operation.MustCreateDraft()); - } + Assert.Throws<DomainException>(() => operation.MustCreateDraft()); + } - [Fact] - public void Should_not_throw_exception_if_draft_can_be_created() - { - var operation = Operation(CreateContent(Status.Published), normalSchema); + [Fact] + public void Should_not_throw_exception_if_draft_can_be_created() + { + var operation = Operation(CreateContent(Status.Published), normalSchema); - operation.MustCreateDraft(); - } + operation.MustCreateDraft(); + } - [Fact] - public void Should_throw_exception_if_draft_cannot_be_deleted() - { - var operation = Operation(CreateContent(Status.Published), normalSchema); + [Fact] + public void Should_throw_exception_if_draft_cannot_be_deleted() + { + var operation = Operation(CreateContent(Status.Published), normalSchema); - Assert.Throws<DomainException>(() => operation.MustDeleteDraft()); - } + Assert.Throws<DomainException>(() => operation.MustDeleteDraft()); + } - [Fact] - public void Should_not_throw_exception_if_draft_can_be_deleted() - { - var operation = Operation(CreateDraftContent(Status.Draft), normalSchema); + [Fact] + public void Should_not_throw_exception_if_draft_can_be_deleted() + { + var operation = Operation(CreateDraftContent(Status.Draft), normalSchema); - operation.MustDeleteDraft(); - } + operation.MustDeleteDraft(); + } - [Fact] - public void Should_throw_exception_if_data_is_not_defined() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public void Should_throw_exception_if_data_is_not_defined() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - Assert.Throws<ValidationException>(() => operation.MustHaveData(null)); - } + Assert.Throws<ValidationException>(() => operation.MustHaveData(null)); + } - [Fact] - public void Should_not_throw_exception_if_data_is_defined() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public void Should_not_throw_exception_if_data_is_defined() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - operation.MustHaveData(new ContentData()); - } + operation.MustHaveData(new ContentData()); + } - [Fact] - public async Task Should_provide_initial_status() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public async Task Should_provide_initial_status() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - A.CallTo(() => contentWorkflow.GetInitialStatusAsync(operation.Schema)) - .Returns(Status.Archived); + A.CallTo(() => contentWorkflow.GetInitialStatusAsync(operation.Schema)) + .Returns(Status.Archived); - Assert.Equal(Status.Archived, await operation.GetInitialStatusAsync()); - } + Assert.Equal(Status.Archived, await operation.GetInitialStatusAsync()); + } - [Fact] - public async Task Should_throw_exception_if_workflow_permits_update() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public async Task Should_throw_exception_if_workflow_permits_update() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - A.CallTo(() => contentWorkflow.CanUpdateAsync(operation.Snapshot, operation.Snapshot.EditingStatus(), operation.User)) - .Returns(false); + A.CallTo(() => contentWorkflow.CanUpdateAsync(operation.Snapshot, operation.Snapshot.EditingStatus(), operation.User)) + .Returns(false); - await Assert.ThrowsAsync<DomainException>(() => operation.CheckUpdateAsync()); - } + await Assert.ThrowsAsync<DomainException>(() => operation.CheckUpdateAsync()); + } - [Fact] - public async Task Should_not_throw_exception_if_workflow_allows_update() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public async Task Should_not_throw_exception_if_workflow_allows_update() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - A.CallTo(() => contentWorkflow.CanUpdateAsync(operation.Snapshot, operation.Snapshot.EditingStatus(), operation.User)) - .Returns(true); + A.CallTo(() => contentWorkflow.CanUpdateAsync(operation.Snapshot, operation.Snapshot.EditingStatus(), operation.User)) + .Returns(true); - await operation.CheckUpdateAsync(); - } + await operation.CheckUpdateAsync(); + } - [Fact] - public async Task Should_throw_exception_if_workflow_status_not_valid() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public async Task Should_throw_exception_if_workflow_status_not_valid() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived)) - .Returns(ValueTask.FromResult<StatusInfo?>(null)); + A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived)) + .Returns(ValueTask.FromResult<StatusInfo?>(null)); - await Assert.ThrowsAsync<ValidationException>(() => operation.CheckStatusAsync(Status.Archived)); - } + await Assert.ThrowsAsync<ValidationException>(() => operation.CheckStatusAsync(Status.Archived)); + } - [Fact] - public async Task Should_not_throw_exception_if_workflow_status_is_valid() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public async Task Should_not_throw_exception_if_workflow_status_is_valid() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived)) - .Returns(new StatusInfo(Status.Archived, StatusColors.Archived)); + A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived)) + .Returns(new StatusInfo(Status.Archived, StatusColors.Archived)); - await operation.CheckStatusAsync(Status.Archived); - } + await operation.CheckStatusAsync(Status.Archived); + } - [Fact] - public async Task Should_not_throw_exception_if_workflow_status_is_checked_for_singleton() - { - var operation = Operation(CreateContent(Status.Draft), singletonSchema); + [Fact] + public async Task Should_not_throw_exception_if_workflow_status_is_checked_for_singleton() + { + var operation = Operation(CreateContent(Status.Draft), singletonSchema); - await operation.CheckStatusAsync(Status.Archived); + await operation.CheckStatusAsync(Status.Archived); - A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived)) - .MustNotHaveHappened(); - } + A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_throw_exception_if_workflow_transition_not_valid() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public async Task Should_throw_exception_if_workflow_transition_not_valid() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - A.CallTo(() => contentWorkflow.CanMoveToAsync((ContentEntity)operation.Snapshot, Status.Draft, Status.Archived, operation.User)) - .Returns(false); + A.CallTo(() => contentWorkflow.CanMoveToAsync((ContentEntity)operation.Snapshot, Status.Draft, Status.Archived, operation.User)) + .Returns(false); - await Assert.ThrowsAsync<ValidationException>(() => operation.CheckTransitionAsync(Status.Archived)); - } + await Assert.ThrowsAsync<ValidationException>(() => operation.CheckTransitionAsync(Status.Archived)); + } - [Fact] - public async Task Should_not_throw_exception_if_workflow_transition_is_valid() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + [Fact] + public async Task Should_not_throw_exception_if_workflow_transition_is_valid() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - A.CallTo(() => contentWorkflow.CanMoveToAsync((ContentEntity)operation.Snapshot, Status.Draft, Status.Archived, operation.User)) - .Returns(true); + A.CallTo(() => contentWorkflow.CanMoveToAsync((ContentEntity)operation.Snapshot, Status.Draft, Status.Archived, operation.User)) + .Returns(true); - await operation.CheckTransitionAsync(Status.Archived); - } + await operation.CheckTransitionAsync(Status.Archived); + } - [Fact] - public async Task Should_not_throw_exception_if_workflow_transition_is_checked_for_singleton() - { - var operation = Operation(CreateContent(Status.Draft), singletonSchema); + [Fact] + public async Task Should_not_throw_exception_if_workflow_transition_is_checked_for_singleton() + { + var operation = Operation(CreateContent(Status.Draft), singletonSchema); - await operation.CheckTransitionAsync(Status.Archived); + await operation.CheckTransitionAsync(Status.Archived); - A.CallTo(() => contentWorkflow.CanMoveToAsync((ContentEntity)operation.Snapshot, A<Status>._, A<Status>._, A<ClaimsPrincipal>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => contentWorkflow.CanMoveToAsync((ContentEntity)operation.Snapshot, A<Status>._, A<Status>._, A<ClaimsPrincipal>._)) + .MustNotHaveHappened(); + } - [Fact] - public void Should_not_throw_exception_if_content_is_from_another_user_but_user_has_permission() - { - var userPermission = PermissionIds.ForApp(PermissionIds.AppContentsDelete, appId.Name, schemaId.Name).Id; - var userObject = Mocks.FrontendUser(permission: userPermission); + [Fact] + public void Should_not_throw_exception_if_content_is_from_another_user_but_user_has_permission() + { + var userPermission = PermissionIds.ForApp(PermissionIds.AppContentsDelete, appId.Name, schemaId.Name).Id; + var userObject = Mocks.FrontendUser(permission: userPermission); + + var operation = Operation(CreateContent(Status.Draft), normalSchema, userObject); - var operation = Operation(CreateContent(Status.Draft), normalSchema, userObject); + ((ContentEntity)operation.Snapshot).CreatedBy = RefToken.User("456"); - ((ContentEntity)operation.Snapshot).CreatedBy = RefToken.User("456"); + operation.MustHavePermission(PermissionIds.AppContentsDelete); + } - operation.MustHavePermission(PermissionIds.AppContentsDelete); - } + [Fact] + public void Should_not_throw_exception_if_content_is_from_current_user() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - [Fact] - public void Should_not_throw_exception_if_content_is_from_current_user() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + ((ContentEntity)operation.Snapshot).CreatedBy = actor; - ((ContentEntity)operation.Snapshot).CreatedBy = actor; + operation.MustHavePermission(PermissionIds.AppContentsDelete); + } - operation.MustHavePermission(PermissionIds.AppContentsDelete); - } + [Fact] + public void Should_not_throw_exception_if_user_is_null() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema, null); - [Fact] - public void Should_not_throw_exception_if_user_is_null() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema, null); + ((ContentEntity)operation.Snapshot).CreatedBy = RefToken.User("456"); - ((ContentEntity)operation.Snapshot).CreatedBy = RefToken.User("456"); + operation.MustHavePermission(PermissionIds.AppContentsDelete); + } - operation.MustHavePermission(PermissionIds.AppContentsDelete); - } + [Fact] + public void Should_throw_exception_if_content_is_from_another_user_and_user_has_no_permission() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - [Fact] - public void Should_throw_exception_if_content_is_from_another_user_and_user_has_no_permission() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + ((ContentEntity)operation.Snapshot).CreatedBy = RefToken.User("456"); - ((ContentEntity)operation.Snapshot).CreatedBy = RefToken.User("456"); + Assert.Throws<DomainForbiddenException>(() => operation.MustHavePermission(PermissionIds.AppContentsDelete)); + } - Assert.Throws<DomainForbiddenException>(() => operation.MustHavePermission(PermissionIds.AppContentsDelete)); - } + [Fact] + public async Task Should_throw_exception_if_referenced() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - [Fact] - public async Task Should_throw_exception_if_referenced() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default)) + .Returns(true); - A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default)) - .Returns(true); + await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync()); + } - await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync()); - } + [Fact] + public async Task Should_not_throw_exception_if_not_referenced() + { + var operation = Operation(CreateContent(Status.Draft), normalSchema); - [Fact] - public async Task Should_not_throw_exception_if_not_referenced() - { - var operation = Operation(CreateContent(Status.Draft), normalSchema); + A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default)) + .Returns(true); - A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default)) - .Returns(true); + await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync()); + } - await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync()); - } + private ContentOperation Operation(ContentEntity content, ISchemaEntity operationSchema) + { + return Operation(content, operationSchema, Mocks.FrontendUser()); + } - private ContentOperation Operation(ContentEntity content, ISchemaEntity operationSchema) - { - return Operation(content, operationSchema, Mocks.FrontendUser()); - } + private ContentOperation Operation(ContentEntity content, ISchemaEntity operationSchema, ClaimsPrincipal? currentUser) + { + var serviceProvider = + new ServiceCollection() + .AddSingleton(contentRepository) + .AddSingleton(contentWorkflow) + .BuildServiceProvider(); + + return new ContentOperation(serviceProvider, () => content) + { + App = Mocks.App(appId), + Command = new CreateContent { User = currentUser, Actor = actor }, + CommandId = content.Id, + Schema = operationSchema + }; + } - private ContentOperation Operation(ContentEntity content, ISchemaEntity operationSchema, ClaimsPrincipal? currentUser) - { - var serviceProvider = - new ServiceCollection() - .AddSingleton(contentRepository) - .AddSingleton(contentWorkflow) - .BuildServiceProvider(); - - return new ContentOperation(serviceProvider, () => content) - { - App = Mocks.App(appId), - Command = new CreateContent { User = currentUser, Actor = actor }, - CommandId = content.Id, - Schema = operationSchema - }; - } - - private ContentEntity CreateDraftContent(Status status, DomainId? id = null) - { - return CreateContentCore(new ContentEntity { NewStatus = status }, id); - } + private ContentEntity CreateDraftContent(Status status, DomainId? id = null) + { + return CreateContentCore(new ContentEntity { NewStatus = status }, id); + } - private ContentEntity CreateContent(Status status, DomainId? id = null) - { - return CreateContentCore(new ContentEntity { Status = status }, id); - } + private ContentEntity CreateContent(Status status, DomainId? id = null) + { + return CreateContentCore(new ContentEntity { Status = status }, id); + } - private ContentEntity CreateContentCore(ContentEntity content, DomainId? id = null) - { - content.Id = id ?? DomainId.NewGuid(); - content.AppId = appId; - content.Created = default; - content.CreatedBy = actor; - content.SchemaId = schemaId; - - return content; - } + private ContentEntity CreateContentCore(ContentEntity content, DomainId? id = null) + { + content.Id = id ?? DomainId.NewGuid(); + content.AppId = appId; + content.Created = default; + content.CreatedBy = actor; + content.SchemaId = schemaId; + + return content; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index a90c5fb74d..68eccdae05 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -19,438 +19,437 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class DynamicContentWorkflowTests { - public class DynamicContentWorkflowTests + private readonly IAppEntity app; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly NamedId<DomainId> simpleSchemaId = NamedId.Of(DomainId.NewGuid(), "my-simple-schema"); + private readonly DynamicContentWorkflow sut; + + private readonly Workflow workflow = new Workflow( + Status.Draft, + new Dictionary<Status, WorkflowStep> + { + [Status.Archived] = + new WorkflowStep( + new Dictionary<Status, WorkflowTransition> + { + [Status.Draft] = WorkflowTransition.Always + }.ToReadonlyDictionary(), + StatusColors.Archived, NoUpdate.Always, Validate: true), + [Status.Draft] = + new WorkflowStep( + new Dictionary<Status, WorkflowTransition> + { + [Status.Archived] = WorkflowTransition.Always, + [Status.Published] = WorkflowTransition.When("data.field.iv === 2", "Editor") + }.ToReadonlyDictionary(), + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary<Status, WorkflowTransition> + { + [Status.Archived] = WorkflowTransition.Always, + [Status.Draft] = WorkflowTransition.Always + }.ToReadonlyDictionary(), + StatusColors.Published, NoUpdate.When("data.field.iv === 2", "Owner", "Editor")) + }.ToReadonlyDictionary()); + + public DynamicContentWorkflowTests() { - private readonly IAppEntity app; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly NamedId<DomainId> simpleSchemaId = NamedId.Of(DomainId.NewGuid(), "my-simple-schema"); - private readonly DynamicContentWorkflow sut; + app = Mocks.App(appId); - private readonly Workflow workflow = new Workflow( + var simpleWorkflow = new Workflow( Status.Draft, new Dictionary<Status, WorkflowStep> { - [Status.Archived] = - new WorkflowStep( - new Dictionary<Status, WorkflowTransition> - { - [Status.Draft] = WorkflowTransition.Always - }.ToReadonlyDictionary(), - StatusColors.Archived, NoUpdate.Always, Validate: true), [Status.Draft] = new WorkflowStep( new Dictionary<Status, WorkflowTransition> { - [Status.Archived] = WorkflowTransition.Always, - [Status.Published] = WorkflowTransition.When("data.field.iv === 2", "Editor") + [Status.Published] = WorkflowTransition.Always }.ToReadonlyDictionary(), StatusColors.Draft), [Status.Published] = new WorkflowStep( new Dictionary<Status, WorkflowTransition> { - [Status.Archived] = WorkflowTransition.Always, [Status.Draft] = WorkflowTransition.Always }.ToReadonlyDictionary(), - StatusColors.Published, NoUpdate.When("data.field.iv === 2", "Owner", "Editor")) - }.ToReadonlyDictionary()); + StatusColors.Published) + }.ToReadonlyDictionary(), + ReadonlyList.Create(simpleSchemaId.Id)); - public DynamicContentWorkflowTests() - { - app = Mocks.App(appId); - - var simpleWorkflow = new Workflow( - Status.Draft, - new Dictionary<Status, WorkflowStep> - { - [Status.Draft] = - new WorkflowStep( - new Dictionary<Status, WorkflowTransition> - { - [Status.Published] = WorkflowTransition.Always - }.ToReadonlyDictionary(), - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary<Status, WorkflowTransition> - { - [Status.Draft] = WorkflowTransition.Always - }.ToReadonlyDictionary(), - StatusColors.Published) - }.ToReadonlyDictionary(), - ReadonlyList.Create(simpleSchemaId.Id)); - - var workflows = Workflows.Empty.Set(workflow).Set(DomainId.NewGuid(), simpleWorkflow); - - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) - .Returns(app); - - A.CallTo(() => app.Workflows) - .Returns(workflows); - - var scriptEngine = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - })); - - sut = new DynamicContentWorkflow(scriptEngine, appProvider); - } + var workflows = Workflows.Empty.Set(workflow).Set(DomainId.NewGuid(), simpleWorkflow); - [Fact] - public async Task Should_return_info_for_valid_status() - { - var content = CreateContent(Status.Draft, 2); + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) + .Returns(app); - var info = await sut.GetInfoAsync(content, Status.Draft); + A.CallTo(() => app.Workflows) + .Returns(workflows); - Assert.Equal(new StatusInfo(Status.Draft, StatusColors.Draft), info); - } + var scriptEngine = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + })); - [Fact] - public async Task Should_return_info_as_null_for_invalid_status() - { - var content = CreateContent(Status.Draft, 2); + sut = new DynamicContentWorkflow(scriptEngine, appProvider); + } - var info = await sut.GetInfoAsync(content, new Status("Invalid")); + [Fact] + public async Task Should_return_info_for_valid_status() + { + var content = CreateContent(Status.Draft, 2); - Assert.Null(info); - } + var info = await sut.GetInfoAsync(content, Status.Draft); - [Fact] - public async Task Should_return_draft_as_initial_status() - { - var actual = await sut.GetInitialStatusAsync(Mocks.Schema(appId, schemaId)); + Assert.Equal(new StatusInfo(Status.Draft, StatusColors.Draft), info); + } - Assert.Equal(Status.Draft, actual); - } + [Fact] + public async Task Should_return_info_as_null_for_invalid_status() + { + var content = CreateContent(Status.Draft, 2); - [Fact] - public async Task Should_allow_publish_on_create() - { - var actual = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Editor")); + var info = await sut.GetInfoAsync(content, new Status("Invalid")); - Assert.True(actual); - } + Assert.Null(info); + } - [Fact] - public async Task Should_not_allow_publish_on_create_if_role_not_allowed() - { - var actual = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Developer")); + [Fact] + public async Task Should_return_draft_as_initial_status() + { + var actual = await sut.GetInitialStatusAsync(Mocks.Schema(appId, schemaId)); - Assert.False(actual); - } + Assert.Equal(Status.Draft, actual); + } - [Fact] - public async Task Should_allow_if_transition_is_valid() - { - var content = CreateContent(Status.Draft, 2); + [Fact] + public async Task Should_allow_publish_on_create() + { + var actual = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Editor")); - var actual = await sut.CanMoveToAsync(Mocks.Schema(appId, schemaId), content.Status, Status.Published, content.Data, Mocks.FrontendUser("Editor")); + Assert.True(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_not_allow_publish_on_create_if_role_not_allowed() + { + var actual = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Developer")); - [Fact] - public async Task Should_allow_if_transition_is_valid_for_content() - { - var content = CreateContent(Status.Draft, 2); + Assert.False(actual); + } - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + [Fact] + public async Task Should_allow_if_transition_is_valid() + { + var content = CreateContent(Status.Draft, 2); - Assert.True(actual); - } + var actual = await sut.CanMoveToAsync(Mocks.Schema(appId, schemaId), content.Status, Status.Published, content.Data, Mocks.FrontendUser("Editor")); - [Fact] - public async Task Should_not_allow_transition_if_role_is_not_allowed() - { - var content = CreateContent(Status.Draft, 2); + Assert.True(actual); + } - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Developer")); + [Fact] + public async Task Should_allow_if_transition_is_valid_for_content() + { + var content = CreateContent(Status.Draft, 2); - Assert.False(actual); - } + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); - [Fact] - public async Task Should_allow_transition_if_role_is_allowed() - { - var content = CreateContent(Status.Draft, 2); + Assert.True(actual); + } - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + [Fact] + public async Task Should_not_allow_transition_if_role_is_not_allowed() + { + var content = CreateContent(Status.Draft, 2); - Assert.True(actual); - } + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Developer")); - [Fact] - public async Task Should_not_allow_transition_if_data_not_valid() - { - var content = CreateContent(Status.Draft, 4); + Assert.False(actual); + } - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + [Fact] + public async Task Should_allow_transition_if_role_is_allowed() + { + var content = CreateContent(Status.Draft, 2); - Assert.False(actual); - } + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); - [Fact] - public async Task Should_be_able_to_update_published() - { - var content = CreateContent(Status.Published, 2); + Assert.True(actual); + } - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + [Fact] + public async Task Should_not_allow_transition_if_data_not_valid() + { + var content = CreateContent(Status.Draft, 4); - Assert.True(actual); - } + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); - [Fact] - public async Task Should_be_able_to_update_draft() - { - var content = CreateContent(Status.Published, 2); + Assert.False(actual); + } - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + [Fact] + public async Task Should_be_able_to_update_published() + { + var content = CreateContent(Status.Published, 2); - Assert.True(actual); - } + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); - [Fact] - public async Task Should_not_be_able_to_update_archived() - { - var content = CreateContent(Status.Archived, 2); + Assert.True(actual); + } - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + [Fact] + public async Task Should_be_able_to_update_draft() + { + var content = CreateContent(Status.Published, 2); - Assert.False(actual); - } + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); - [Fact] - public async Task Should_not_be_able_to_update_published_with_true_expression() - { - var content = CreateContent(Status.Published, 2); + Assert.True(actual); + } - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + [Fact] + public async Task Should_not_be_able_to_update_archived() + { + var content = CreateContent(Status.Archived, 2); - Assert.False(actual); - } + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); - [Fact] - public async Task Should_be_able_to_update_published_with_false_expression() - { - var content = CreateContent(Status.Published, 1); + Assert.False(actual); + } - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + [Fact] + public async Task Should_not_be_able_to_update_published_with_true_expression() + { + var content = CreateContent(Status.Published, 2); - Assert.True(actual); - } + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); - [Fact] - public async Task Should_not_be_able_to_update_published_with_correct_roles() - { - var content = CreateContent(Status.Published, 2); + Assert.False(actual); + } - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Editor")); + [Fact] + public async Task Should_be_able_to_update_published_with_false_expression() + { + var content = CreateContent(Status.Published, 1); - Assert.False(actual); - } + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); - [Fact] - public async Task Should_be_able_to_update_published_with_incorrect_roles() - { - var content = CreateContent(Status.Published, 1); + Assert.True(actual); + } - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + [Fact] + public async Task Should_not_be_able_to_update_published_with_correct_roles() + { + var content = CreateContent(Status.Published, 2); - Assert.True(actual); - } + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Editor")); - [Fact] - public async Task Should_get_next_statuses_for_draft() - { - var content = CreateContent(Status.Draft, 2); + Assert.False(actual); + } - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived) - }; + [Fact] + public async Task Should_be_able_to_update_published_with_incorrect_roles() + { + var content = CreateContent(Status.Published, 1); - var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); - actual.Should().BeEquivalentTo(expected); - } + Assert.True(actual); + } + + [Fact] + public async Task Should_get_next_statuses_for_draft() + { + var content = CreateContent(Status.Draft, 2); - [Fact] - public async Task Should_limit_next_statuses_if_expression_does_not_evauate_to_true() + var expected = new[] { - var content = CreateContent(Status.Draft, 4); + new StatusInfo(Status.Archived, StatusColors.Archived) + }; - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived) - }; + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Developer")); - var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); + actual.Should().BeEquivalentTo(expected); + } - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task Should_limit_next_statuses_if_expression_does_not_evauate_to_true() + { + var content = CreateContent(Status.Draft, 4); - [Fact] - public async Task Should_limit_next_statuses_if_role_is_not_allowed() + var expected = new[] { - var content = CreateContent(Status.Draft, 2); + new StatusInfo(Status.Archived, StatusColors.Archived) + }; - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Published, StatusColors.Published) - }; + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); - var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); + actual.Should().BeEquivalentTo(expected); + } - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task Should_limit_next_statuses_if_role_is_not_allowed() + { + var content = CreateContent(Status.Draft, 2); - [Fact] - public async Task Should_get_next_statuses_for_archived() + var expected = new[] { - var content = CreateContent(Status.Archived, 2); + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Published, StatusColors.Published) + }; - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft) - }; + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); - var actual = await sut.GetNextAsync(content, content.Status, null!); + actual.Should().BeEquivalentTo(expected); + } - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task Should_get_next_statuses_for_archived() + { + var content = CreateContent(Status.Archived, 2); - [Fact] - public async Task Should_get_next_statuses_for_published() + var expected = new[] { - var content = CreateContent(Status.Published, 2); + new StatusInfo(Status.Draft, StatusColors.Draft) + }; - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft) - }; + var actual = await sut.GetNextAsync(content, content.Status, null!); - var actual = await sut.GetNextAsync(content, content.Status, null!); + actual.Should().BeEquivalentTo(expected); + } - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task Should_get_next_statuses_for_published() + { + var content = CreateContent(Status.Published, 2); - [Fact] - public async Task Should_return_all_statuses() + var expected = new[] { - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft) + }; - var actual = await sut.GetAllAsync(Mocks.Schema(appId, schemaId)); + var actual = await sut.GetNextAsync(content, content.Status, null!); - actual.Should().BeEquivalentTo(expected); - } + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task Should_return_all_statuses_for_simple_schema_workflow() + [Fact] + public async Task Should_return_all_statuses() + { + var expected = new[] { - var expected = new[] - { - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; - var actual = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); + var actual = await sut.GetAllAsync(Mocks.Schema(appId, schemaId)); - actual.Should().BeEquivalentTo(expected); - } + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task Should_return_all_statuses_for_default_workflow_if_no_workflow_configured() + [Fact] + public async Task Should_return_all_statuses_for_simple_schema_workflow() + { + var expected = new[] { - A.CallTo(() => app.Workflows).Returns(Workflows.Empty); + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; - var expected = new[] - { - new StatusInfo(Status.Archived, StatusColors.Archived), - new StatusInfo(Status.Draft, StatusColors.Draft), - new StatusInfo(Status.Published, StatusColors.Published) - }; + var actual = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); - var actual = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); + actual.Should().BeEquivalentTo(expected); + } - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task Should_return_all_statuses_for_default_workflow_if_no_workflow_configured() + { + A.CallTo(() => app.Workflows).Returns(Workflows.Empty); - [Fact] - public async Task Should_not_validate_when_not_publishing() + var expected = new[] { - var actual = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Draft); + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; - Assert.False(actual); - } + var actual = await sut.GetAllAsync(Mocks.Schema(appId, simpleSchemaId)); - [Fact] - public async Task Should_not_validate_when_publishing_but_not_enabled() - { - var actual = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published); + actual.Should().BeEquivalentTo(expected); + } - Assert.False(actual); - } + [Fact] + public async Task Should_not_validate_when_not_publishing() + { + var actual = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Draft); - [Fact] - public async Task Should_validate_when_publishing_and_enabled() - { - var actual = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published); + Assert.False(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_not_validate_when_publishing_but_not_enabled() + { + var actual = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published); - [Fact] - public async Task Should_validate_when_enabled_in_step() - { - var actual = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Archived); + Assert.False(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_validate_when_publishing_and_enabled() + { + var actual = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published); - private ISchemaEntity CreateSchema(bool validateOnPublish) - { - var schema = new Schema("my-schema", new SchemaProperties - { - ValidateOnPublish = validateOnPublish - }); + Assert.True(actual); + } - return Mocks.Schema(appId, simpleSchemaId, schema); - } + [Fact] + public async Task Should_validate_when_enabled_in_step() + { + var actual = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Archived); - private ContentEntity CreateContent(Status status, int value, bool simple = false) + Assert.True(actual); + } + + private ISchemaEntity CreateSchema(bool validateOnPublish) + { + var schema = new Schema("my-schema", new SchemaProperties { - var content = new ContentEntity { AppId = appId, Status = status }; + ValidateOnPublish = validateOnPublish + }); - if (simple) - { - content.SchemaId = simpleSchemaId; - } - else - { - content.SchemaId = schemaId; - } + return Mocks.Schema(appId, simpleSchemaId, schema); + } - content.Data = - new ContentData() - .AddField("field", - new ContentFieldData() - .AddInvariant(value)); + private ContentEntity CreateContent(Status status, int value, bool simple = false) + { + var content = new ContentEntity { AppId = appId, Status = status }; - return content; + if (simple) + { + content.SchemaId = simpleSchemaId; } + else + { + content.SchemaId = schemaId; + } + + content.Data = + new ContentData() + .AddField("field", + new ContentFieldData() + .AddInvariant(value)); + + return content; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs index 697571e0df..87d0a84b95 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs @@ -16,14 +16,14 @@ using GraphQLSchema = GraphQL.Types.Schema; using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public class GraphQLIntrospectionTests : GraphQLTestBase { - public class GraphQLIntrospectionTests : GraphQLTestBase + [Fact] + public async Task Should_introspect() { - [Fact] - public async Task Should_introspect() - { - const string query = @" + const string query = @" query IntrospectionQuery { __schema { queryType { @@ -110,199 +110,198 @@ fragment TypeRef on __Type { } }"; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query, OperationName = "IntrospectionQuery" }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, OperationName = "IntrospectionQuery" }); + + var json = serializer.Serialize(actual); + + Assert.NotEmpty(json); + } + + [Fact] + public async Task Should_create_empty_schema() + { + var model = await CreateSut().GetSchemaAsync(TestApp.Default); + + Assert.NotNull(model); + } + + [Fact] + public async Task Should_create_empty_schema_with_empty_schema() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "content"), + new Schema("content").Publish()); + + var model = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + Assert.NotNull(model); + } + + [Fact] + public async Task Should_create_empty_schema_with_empty_schema_because_ui_field() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "content"), + new Schema("content").Publish() + .AddUI(1, "ui", Partitioning.Invariant)); + + var model = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + Assert.NotNull(model); + } + + [Fact] + public async Task Should_create_empty_schema_with_empty_schema_because_invalid_field() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "content"), + new Schema("content").Publish() + .AddComponent(1, "component", Partitioning.Invariant)); + + var model = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + Assert.NotNull(model); + } + + [Fact] + public async Task Should_create_empty_schema_with_unpublished_schema() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "content"), + new Schema("content") + .AddString(1, "myField", Partitioning.Invariant)); + + var model = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + Assert.NotNull(model); + } + + [Fact] + public async Task Should_create_schema_with_reserved_schema_name() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "content"), + new Schema("content").Publish() + .AddString(1, "myField", Partitioning.Invariant)); + + var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + Assert.Contains(graphQLSchema.AllTypes, x => x.Name == "Content2"); + } + + [Fact] + public async Task Should_create_schema_with_reserved_field_name() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "my-schema"), + new Schema("my-schema").Publish() + .AddString(1, "content", Partitioning.Invariant)); + + var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + var type = FindDataType(graphQLSchema, "MySchema"); + + Assert.Contains(type?.Fields, x => x.Name == "content"); + } + + [Fact] + public async Task Should_create_schema_with_invalid_field_name() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "my-schema"), + new Schema("my-schema").Publish() + .AddString(1, "2-field", Partitioning.Invariant)); + + var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + var type = FindDataType(graphQLSchema, "MySchema"); + + Assert.Contains(type?.Fields, x => x.Name == "gql_2Field"); + } + + [Fact] + public async Task Should_create_schema_with_duplicate_field_names() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "my-schema"), + new Schema("my-schema").Publish() + .AddString(1, "my-field", Partitioning.Invariant) + .AddString(2, "my_field", Partitioning.Invariant)); + + var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + var type = FindDataType(graphQLSchema, "MySchema"); + + Assert.Contains(type?.Fields, x => x.Name == "myField"); + Assert.Contains(type?.Fields, x => x.Name == "myField2"); + } + + [Fact] + public async Task Should_not_create_schema_With_invalid_component() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "my-schema"), + new Schema("my-schema").Publish() + .AddComponent(1, "my-component", Partitioning.Invariant, + new ComponentFieldProperties()) + .AddString(2, "my-string", Partitioning.Invariant)); - var json = serializer.Serialize(actual); - - Assert.NotEmpty(json); - } - - [Fact] - public async Task Should_create_empty_schema() - { - var model = await CreateSut().GetSchemaAsync(TestApp.Default); - - Assert.NotNull(model); - } - - [Fact] - public async Task Should_create_empty_schema_with_empty_schema() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "content"), - new Schema("content").Publish()); - - var model = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - Assert.NotNull(model); - } - - [Fact] - public async Task Should_create_empty_schema_with_empty_schema_because_ui_field() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "content"), - new Schema("content").Publish() - .AddUI(1, "ui", Partitioning.Invariant)); - - var model = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - Assert.NotNull(model); - } - - [Fact] - public async Task Should_create_empty_schema_with_empty_schema_because_invalid_field() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "content"), - new Schema("content").Publish() - .AddComponent(1, "component", Partitioning.Invariant)); - - var model = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - Assert.NotNull(model); - } - - [Fact] - public async Task Should_create_empty_schema_with_unpublished_schema() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "content"), - new Schema("content") - .AddString(1, "myField", Partitioning.Invariant)); - - var model = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - Assert.NotNull(model); - } - - [Fact] - public async Task Should_create_schema_with_reserved_schema_name() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "content"), - new Schema("content").Publish() - .AddString(1, "myField", Partitioning.Invariant)); - - var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - Assert.Contains(graphQLSchema.AllTypes, x => x.Name == "Content2"); - } - - [Fact] - public async Task Should_create_schema_with_reserved_field_name() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "my-schema"), - new Schema("my-schema").Publish() - .AddString(1, "content", Partitioning.Invariant)); - - var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - var type = FindDataType(graphQLSchema, "MySchema"); - - Assert.Contains(type?.Fields, x => x.Name == "content"); - } - - [Fact] - public async Task Should_create_schema_with_invalid_field_name() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "my-schema"), - new Schema("my-schema").Publish() - .AddString(1, "2-field", Partitioning.Invariant)); - - var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - var type = FindDataType(graphQLSchema, "MySchema"); - - Assert.Contains(type?.Fields, x => x.Name == "gql_2Field"); - } - - [Fact] - public async Task Should_create_schema_with_duplicate_field_names() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "my-schema"), - new Schema("my-schema").Publish() - .AddString(1, "my-field", Partitioning.Invariant) - .AddString(2, "my_field", Partitioning.Invariant)); - - var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - var type = FindDataType(graphQLSchema, "MySchema"); - - Assert.Contains(type?.Fields, x => x.Name == "myField"); - Assert.Contains(type?.Fields, x => x.Name == "myField2"); - } - - [Fact] - public async Task Should_not_create_schema_With_invalid_component() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "my-schema"), - new Schema("my-schema").Publish() - .AddComponent(1, "my-component", Partitioning.Invariant, - new ComponentFieldProperties()) - .AddString(2, "my-string", Partitioning.Invariant)); - - var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - var type = FindDataType(graphQLSchema, "MySchema"); - - Assert.DoesNotContain(type?.Fields, x => x.Name == "myComponent"); - } - - [Fact] - public async Task Should_not_create_schema_With_invalid_components() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "my-schema"), - new Schema("my-schema").Publish() - .AddComponents(1, "my-components", Partitioning.Invariant, - new ComponentsFieldProperties()) - .AddString(2, "my-string", Partitioning.Invariant)); - - var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - var type = FindDataType(graphQLSchema, "MySchema"); - - Assert.DoesNotContain(type?.Fields, x => x.Name == "myComponents"); - } - - [Fact] - public async Task Should_not_create_schema_With_invalid_references() - { - var schema = - Mocks.Schema(TestApp.DefaultId, - NamedId.Of(DomainId.NewGuid(), "my-schema"), - new Schema("my-schema").Publish() - .AddReferences(1, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = DomainId.NewGuid() }) - .AddString(2, "my-string", Partitioning.Invariant)); - - var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); - - var type = FindDataType(graphQLSchema, "MySchema"); - - Assert.DoesNotContain(type?.Fields, x => x.Name == "myReferences"); - } - - private static IObjectGraphType? FindDataType(GraphQLSchema graphQLSchema, string schema) - { - var type = (IObjectGraphType)graphQLSchema.AllTypes.Single(x => x.Name == schema); + var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + var type = FindDataType(graphQLSchema, "MySchema"); + + Assert.DoesNotContain(type?.Fields, x => x.Name == "myComponent"); + } + + [Fact] + public async Task Should_not_create_schema_With_invalid_components() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "my-schema"), + new Schema("my-schema").Publish() + .AddComponents(1, "my-components", Partitioning.Invariant, + new ComponentsFieldProperties()) + .AddString(2, "my-string", Partitioning.Invariant)); + + var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + var type = FindDataType(graphQLSchema, "MySchema"); + + Assert.DoesNotContain(type?.Fields, x => x.Name == "myComponents"); + } + + [Fact] + public async Task Should_not_create_schema_With_invalid_references() + { + var schema = + Mocks.Schema(TestApp.DefaultId, + NamedId.Of(DomainId.NewGuid(), "my-schema"), + new Schema("my-schema").Publish() + .AddReferences(1, "my-references", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = DomainId.NewGuid() }) + .AddString(2, "my-string", Partitioning.Invariant)); + + var graphQLSchema = await CreateSut(schema).GetSchemaAsync(TestApp.Default); + + var type = FindDataType(graphQLSchema, "MySchema"); + + Assert.DoesNotContain(type?.Fields, x => x.Name == "myReferences"); + } + + private static IObjectGraphType? FindDataType(GraphQLSchema graphQLSchema, string schema) + { + var type = (IObjectGraphType)graphQLSchema.AllTypes.Single(x => x.Name == schema); - return (IObjectGraphType?)type.GetField("flatData")?.ResolvedType?.Flatten(); - } + return (IObjectGraphType?)type.GetField("flatData")?.ResolvedType?.Flatten(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index a77ad1b736..33afbc305e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -17,762 +17,761 @@ using Squidex.Shared; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public class GraphQLMutationTests : GraphQLTestBase { - public class GraphQLMutationTests : GraphQLTestBase - { - private readonly DomainId contentId = DomainId.NewGuid(); - private readonly IEnrichedContentEntity content; - private readonly CommandContext commandContext = new CommandContext(new PatchContent(), A.Dummy<ICommandBus>()); + private readonly DomainId contentId = DomainId.NewGuid(); + private readonly IEnrichedContentEntity content; + private readonly CommandContext commandContext = new CommandContext(new PatchContent(), A.Dummy<ICommandBus>()); - public GraphQLMutationTests() - { - content = TestContent.Create(contentId, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id, null); + public GraphQLMutationTests() + { + content = TestContent.Create(contentId, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id, null); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>.Ignored, A<CancellationToken>._)) - .Returns(commandContext); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>.Ignored, A<CancellationToken>._)) + .Returns(commandContext); + } - [Fact] - public async Task Should_return_error_if_user_has_no_permission_to_create() - { - var query = @" + [Fact] + public async Task Should_return_error_if_user_has_no_permission_to_create() + { + var query = @" mutation { createMySchemaContent(data: { myNumber: { iv: 42 } }) { id } }"; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new[] { - errors = new[] + new { - new + message = "You do not have the necessary permission.", + locations = new[] { - message = "You do not have the necessary permission.", - locations = new[] - { - new - { - line = 3, - column = 19 - } - }, - path = new[] + new { - "createMySchemaContent" + line = 3, + column = 19 } + }, + path = new[] + { + "createMySchemaContent" } - }, - data = (object?)null - }; + } + }, + data = (object?)null + }; - AssertResult(expected, actual); + AssertResult(expected, actual); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_single_content_if_creating_content() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_single_content_if_creating_content() + { + var query = CreateQuery(@" mutation { createMySchemaContent(data: <DATA>, publish: true) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsCreate; + var permission = PermissionIds.AppContentsCreate; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new - { - data = new - { - createMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<CreateContent>.That.Matches(x => - x.ExpectedVersion == EtagVersion.Any && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_single_content_if_creating_content_with_custom_id() + var expected = new { - var query = CreateQuery(@" + data = new + { + createMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<CreateContent>.That.Matches(x => + x.ExpectedVersion == EtagVersion.Any && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_if_creating_content_with_custom_id() + { + var query = CreateQuery(@" mutation { createMySchemaContent(data: <DATA>, id: '123', publish: true) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsCreate; + var permission = PermissionIds.AppContentsCreate; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new - { - data = new - { - createMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<CreateContent>.That.Matches(x => - x.ExpectedVersion == EtagVersion.Any && - x.ContentId == DomainId.Create("123") && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_single_content_if_creating_content_with_variable() + var expected = new { - var query = CreateQuery(@" + data = new + { + createMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<CreateContent>.That.Matches(x => + x.ExpectedVersion == EtagVersion.Any && + x.ContentId == DomainId.Create("123") && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_if_creating_content_with_variable() + { + var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { createMySchemaContent(data: $data, publish: true) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsCreate; + var permission = PermissionIds.AppContentsCreate; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); - var expected = new - { - data = new - { - createMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<CreateContent>.That.Matches(x => - x.ExpectedVersion == EtagVersion.Any && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_error_if_user_has_no_permission_to_update() + var expected = new { - var query = CreateQuery(@" + data = new + { + createMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<CreateContent>.That.Matches(x => + x.ExpectedVersion == EtagVersion.Any && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_error_if_user_has_no_permission_to_update() + { + var query = CreateQuery(@" mutation { updateMySchemaContent(id: '<ID>', data: { myNumber: { iv: 42 } }) { id } }", contentId, content); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new[] { - errors = new[] + new { - new + message = "You do not have the necessary permission.", + locations = new[] { - message = "You do not have the necessary permission.", - locations = new[] + new { - new - { - line = 3, - column = 19 - } - }, - path = new[] - { - "updateMySchemaContent" + line = 3, + column = 19 } + }, + path = new[] + { + "updateMySchemaContent" } - }, - data = (object?)null - }; + } + }, + data = (object?)null + }; - AssertResult(expected, actual); + AssertResult(expected, actual); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_single_content_if_updating_content() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_single_content_if_updating_content() + { + var query = CreateQuery(@" mutation { updateMySchemaContent(id: '<ID>', data: <DATA>, expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsUpdateOwn; + var permission = PermissionIds.AppContentsUpdateOwn; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new - { - data = new - { - updateMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<UpdateContent>.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_single_content_if_updating_content_with_variable() + var expected = new { - var query = CreateQuery(@" + data = new + { + updateMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<UpdateContent>.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_if_updating_content_with_variable() + { + var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { updateMySchemaContent(id: '<ID>', data: $data, expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsUpdateOwn; + var permission = PermissionIds.AppContentsUpdateOwn; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); - var expected = new - { - data = new - { - updateMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<UpdateContent>.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_error_if_user_has_no_permission_to_upsert() + var expected = new { - var query = CreateQuery(@" + data = new + { + updateMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<UpdateContent>.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_error_if_user_has_no_permission_to_upsert() + { + var query = CreateQuery(@" mutation { upsertMySchemaContent(id: '<ID>', data: { myNumber: { iv: 42 } }) { id } }", contentId, content); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new[] { - errors = new[] + new { - new + message = "You do not have the necessary permission.", + locations = new[] { - message = "You do not have the necessary permission.", - locations = new[] + new { - new - { - line = 3, - column = 19 - } - }, - path = new[] - { - "upsertMySchemaContent" + line = 3, + column = 19 } + }, + path = new[] + { + "upsertMySchemaContent" } - }, - data = (object?)null - }; + } + }, + data = (object?)null + }; - AssertResult(expected, actual); + AssertResult(expected, actual); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_single_content_if_upserting_content() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_single_content_if_upserting_content() + { + var query = CreateQuery(@" mutation { upsertMySchemaContent(id: '<ID>', data: <DATA>, publish: true, expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsUpsert; + var permission = PermissionIds.AppContentsUpsert; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new - { - data = new - { - upsertMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_single_content_if_upserting_content_with_variable() + var expected = new { - var query = CreateQuery(@" + data = new + { + upsertMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_if_upserting_content_with_variable() + { + var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { upsertMySchemaContent(id: '<ID>', data: $data, publish: true, expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsUpsert; + var permission = PermissionIds.AppContentsUpsert; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); - var expected = new - { - data = new - { - upsertMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<UpsertContent>.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_error_if_user_has_no_permission_to_patch() + var expected = new { - var query = CreateQuery(@" + data = new + { + upsertMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<UpsertContent>.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_error_if_user_has_no_permission_to_patch() + { + var query = CreateQuery(@" mutation { patchMySchemaContent(id: '<ID>', data: { myNumber: { iv: 42 } }) { id } }", contentId, content); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new[] { - errors = new[] + new { - new + message = "You do not have the necessary permission.", + locations = new[] { - message = "You do not have the necessary permission.", - locations = new[] + new { - new - { - line = 3, - column = 19 - } - }, - path = new[] - { - "patchMySchemaContent" + line = 3, + column = 19 } + }, + path = new[] + { + "patchMySchemaContent" } - }, - data = (object?)null - }; + } + }, + data = (object?)null + }; - AssertResult(expected, actual); + AssertResult(expected, actual); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_single_content_if_patching_content() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_single_content_if_patching_content() + { + var query = CreateQuery(@" mutation { patchMySchemaContent(id: '<ID>', data: <DATA>, expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsUpdateOwn; + var permission = PermissionIds.AppContentsUpdateOwn; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new - { - data = new - { - patchMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<PatchContent>.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_single_content_if_patching_content_with_variable() + var expected = new { - var query = CreateQuery(@" + data = new + { + patchMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<PatchContent>.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_if_patching_content_with_variable() + { + var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { patchMySchemaContent(id: '<ID>', data: $data, expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsUpdateOwn; + var permission = PermissionIds.AppContentsUpdateOwn; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission); - var expected = new - { - data = new - { - patchMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<PatchContent>.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Data.Equals(content.Data)), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_error_if_user_has_no_permission_to_change_status() + var expected = new { - var query = CreateQuery(@" + data = new + { + patchMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<PatchContent>.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Data.Equals(content.Data)), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_error_if_user_has_no_permission_to_change_status() + { + var query = CreateQuery(@" mutation { changeMySchemaContent(id: '<ID>', status: 'Published') { id } }", contentId, content); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new[] { - errors = new[] + new { - new + message = "You do not have the necessary permission.", + locations = new[] { - message = "You do not have the necessary permission.", - locations = new[] - { - new - { - line = 3, - column = 19 - } - }, - path = new[] + new { - "changeMySchemaContent" + line = 3, + column = 19 } + }, + path = new[] + { + "changeMySchemaContent" } - }, - data = (object?)null - }; + } + }, + data = (object?)null + }; - AssertResult(expected, actual); + AssertResult(expected, actual); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_single_content_if_changing_status() - { - var dueTime = InstantPattern.General.Parse("2021-12-12T11:10:09Z").Value; + [Fact] + public async Task Should_return_single_content_if_changing_status() + { + var dueTime = InstantPattern.General.Parse("2021-12-12T11:10:09Z").Value; - var query = CreateQuery(@" + var query = CreateQuery(@" mutation { changeMySchemaContent(id: '<ID>', status: 'Published', dueTime: '2021-12-12T11:10:09Z', expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsChangeStatusOwn; + var permission = PermissionIds.AppContentsChangeStatusOwn; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new - { - data = new - { - changeMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<ChangeContentStatus>.That.Matches(x => - x.ContentId == contentId && - x.DueTime == dueTime && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_single_content_if_changing_status_without_due_time() + var expected = new { - var query = CreateQuery(@" + data = new + { + changeMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<ChangeContentStatus>.That.Matches(x => + x.ContentId == contentId && + x.DueTime == dueTime && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_if_changing_status_without_due_time() + { + var query = CreateQuery(@" mutation { changeMySchemaContent(id: '<ID>', status: 'Published', expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsChangeStatusOwn; + var permission = PermissionIds.AppContentsChangeStatusOwn; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new - { - data = new - { - changeMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<ChangeContentStatus>.That.Matches(x => - x.ContentId == contentId && - x.DueTime == null && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_single_content_if_changing_status_with_null_due_time() + var expected = new { - var query = CreateQuery(@" + data = new + { + changeMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<ChangeContentStatus>.That.Matches(x => + x.ContentId == contentId && + x.DueTime == null && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_if_changing_status_with_null_due_time() + { + var query = CreateQuery(@" mutation { changeMySchemaContent(id: '<ID>', status: 'Published', dueTime: null, expectedVersion: 10) { <FIELDS_CONTENT> } }", contentId, content); - commandContext.Complete(content); + commandContext.Complete(content); - var permission = PermissionIds.AppContentsChangeStatusOwn; + var permission = PermissionIds.AppContentsChangeStatusOwn; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new - { - data = new - { - changeMySchemaContent = TestContent.Response(content) - } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<ChangeContentStatus>.That.Matches(x => - x.ContentId == contentId && - x.DueTime == null && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published), - A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_return_error_if_user_has_no_permission_to_delete() + var expected = new { - var query = CreateQuery(@" + data = new + { + changeMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<ChangeContentStatus>.That.Matches(x => + x.ContentId == contentId && + x.DueTime == null && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published), + A<CancellationToken>._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_error_if_user_has_no_permission_to_delete() + { + var query = CreateQuery(@" mutation { deleteMySchemaContent(id: '<ID>') { version } }", contentId, content); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new[] { - errors = new[] + new { - new + message = "You do not have the necessary permission.", + locations = new[] { - message = "You do not have the necessary permission.", - locations = new[] + new { - new - { - line = 3, - column = 19 - } - }, - path = new[] - { - "deleteMySchemaContent" + line = 3, + column = 19 } + }, + path = new[] + { + "deleteMySchemaContent" } - }, - data = (object?)null - }; + } + }, + data = (object?)null + }; - AssertResult(expected, actual); + AssertResult(expected, actual); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_new_version_if_deleting_content() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_new_version_if_deleting_content() + { + var query = CreateQuery(@" mutation { deleteMySchemaContent(id: '<ID>', expectedVersion: 10) { version } }", contentId, content); - commandContext.Complete(CommandResult.Empty(contentId, 13, 12)); + commandContext.Complete(CommandResult.Empty(contentId, 13, 12)); - var permission = PermissionIds.AppContentsDeleteOwn; + var permission = PermissionIds.AppContentsDeleteOwn; - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission); - var expected = new + var expected = new + { + data = new { - data = new + deleteMySchemaContent = new { - deleteMySchemaContent = new - { - version = 13 - } + version = 13 } - }; - - AssertResult(expected, actual); - - A.CallTo(() => commandBus.PublishAsync( - A<DeleteContent>.That.Matches(x => - x.ContentId == contentId && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId)), - A<CancellationToken>._)) - .MustHaveHappened(); - } + } + }; + + AssertResult(expected, actual); + + A.CallTo(() => commandBus.PublishAsync( + A<DeleteContent>.That.Matches(x => + x.ContentId == contentId && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId)), + A<CancellationToken>._)) + .MustHaveHappened(); + } - private Inputs GetInput() + private Inputs GetInput() + { + var input = new { - var input = new - { - data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id) - }; + data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id) + }; - var element = JsonSerializer.SerializeToElement(input, TestUtils.DefaultOptions()); + var element = JsonSerializer.SerializeToElement(input, TestUtils.DefaultOptions()); - return serializer.ReadNode<Inputs>(element)!; - } + return serializer.ReadNode<Inputs>(element)!; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 34ec8efe3c..905930ded8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -13,108 +13,108 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public class GraphQLQueriesTests : GraphQLTestBase { - public class GraphQLQueriesTests : GraphQLTestBase + [Theory] + [InlineData("")] + [InlineData(" ")] + public async Task Should_return_error_empty_query(string query) { - [Theory] - [InlineData("")] - [InlineData(" ")] - public async Task Should_return_error_empty_query(string query) - { - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new object[] { - errors = new object[] + new { - new + message = "Document does not contain any operations.", + extensions = new { - message = "Document does not contain any operations.", - extensions = new + code = "NO_OPERATION", + codes = new[] { - code = "NO_OPERATION", - codes = new[] - { - "NO_OPERATION" - } + "NO_OPERATION" } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_query_contents_with_full_text() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_query_contents_with_full_text() + { + var query = CreateQuery(@" query { queryMySchemaContents(search: ""Hello"") { <FIELDS_CONTENT_FLAT> } }"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A<Q>.That.Matches(x => x.QueryAsOdata == "?$skip=0&$search=\"Hello\"" && x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A<Q>.That.Matches(x => x.QueryAsOdata == "?$skip=0&$search=\"Hello\"" && x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + queryMySchemaContents = new[] { - queryMySchemaContents = new[] - { - TestContent.FlatResponse(content) - } + TestContent.FlatResponse(content) } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_multiple_assets_if_querying_assets() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_multiple_assets_if_querying_assets() + { + var query = CreateQuery(@" query { queryAssets(filter: 'my-query', top: 30, skip: 5) { <FIELDS_ASSET> } }"); - var asset = TestAsset.Create(DomainId.NewGuid()); + var asset = TestAsset.Create(DomainId.NewGuid()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, asset)); + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, asset)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + queryAssets = new[] { - queryAssets = new[] - { - TestAsset.Response(asset) - } + TestAsset.Response(asset) } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_multiple_assets_with_total_if_querying_assets_with_total() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_multiple_assets_with_total_if_querying_assets_with_total() + { + var query = CreateQuery(@" query { queryAssetsWithTotal(filter: 'my-query', top: 30, skip: 5) { total @@ -124,161 +124,161 @@ public async Task Should_return_multiple_assets_with_total_if_querying_assets_wi } }"); - var asset = TestAsset.Create(DomainId.NewGuid()); + var asset = TestAsset.Create(DomainId.NewGuid()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && !x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(10, asset)); + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && !x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(10, asset)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + queryAssetsWithTotal = new { - queryAssetsWithTotal = new + total = 10, + items = new[] { - total = 10, - items = new[] - { - TestAsset.Response(asset) - } + TestAsset.Response(asset) } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_null_if_single_asset_not_found() - { - var assetId = DomainId.NewGuid(); + [Fact] + public async Task Should_return_null_if_single_asset_not_found() + { + var assetId = DomainId.NewGuid(); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findAsset(id: '<ID>') { id } }", assetId); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A<Q>.That.HasIdsWithoutTotal(assetId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom<IEnrichedAssetEntity>(1)); + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, + A<Q>.That.HasIdsWithoutTotal(assetId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom<IEnrichedAssetEntity>(1)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new - { - findAsset = (object?)null - } - }; + findAsset = (object?)null + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_single_asset_if_finding_asset() - { - var assetId = DomainId.NewGuid(); - var asset = TestAsset.Create(assetId); + [Fact] + public async Task Should_return_single_asset_if_finding_asset() + { + var assetId = DomainId.NewGuid(); + var asset = TestAsset.Create(assetId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findAsset(id: '<ID>') { <FIELDS_ASSET> } }", assetId); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A<Q>.That.HasIdsWithoutTotal(assetId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, asset)); + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, + A<Q>.That.HasIdsWithoutTotal(assetId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, asset)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new - { - findAsset = TestAsset.Response(asset) - } - }; + findAsset = TestAsset.Response(asset) + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_multiple_flat_contents_if_querying_contents() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_multiple_flat_contents_if_querying_contents() + { + var query = CreateQuery(@" query { queryMySchemaContents(top: 30, skip: 5) { <FIELDS_CONTENT_FLAT> } }"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + queryMySchemaContents = new[] { - queryMySchemaContents = new[] - { - TestContent.FlatResponse(content) - } + TestContent.FlatResponse(content) } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_multiple_contents_if_querying_contents() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_multiple_contents_if_querying_contents() + { + var query = CreateQuery(@" query { queryMySchemaContents(top: 30, skip: 5) { <FIELDS_CONTENT> } }"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + queryMySchemaContents = new[] { - queryMySchemaContents = new[] - { - TestContent.Response(content) - } + TestContent.Response(content) } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_multiple_contents_with_total_if_querying_contents_with_total() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_multiple_contents_with_total_if_querying_contents_with_total() + { + var query = CreateQuery(@" query { queryMySchemaContentsWithTotal(top: 30, skip: 5) { total @@ -288,161 +288,161 @@ public async Task Should_return_multiple_contents_with_total_if_querying_content } }"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && !x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(10, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && !x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(10, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + queryMySchemaContentsWithTotal = new { - queryMySchemaContentsWithTotal = new + total = 10, + items = new[] { - total = 10, - items = new[] - { - TestContent.Response(content) - } + TestContent.Response(content) } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_null_if_single_content_not_found() - { - var contentId = DomainId.NewGuid(); + [Fact] + public async Task Should_return_null_if_single_content_not_found() + { + var contentId = DomainId.NewGuid(); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new - { - findMySchemaContent = (object?)null - } - }; + findMySchemaContent = (object?)null + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_null_if_single_content_from_another_schema() - { - var contentId = DomainId.NewGuid(); - var content = TestContent.CreateRef(TestSchemas.Ref1Id, contentId, "ref1-field", "ref1"); + [Fact] + public async Task Should_return_null_if_single_content_from_another_schema() + { + var contentId = DomainId.NewGuid(); + var content = TestContent.CreateRef(TestSchemas.Ref1Id, contentId, "ref1-field", "ref1"); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(10, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(10, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new - { - findMySchemaContent = (object?)null - } - }; + findMySchemaContent = (object?)null + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_single_content_if_finding_content() - { - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); + [Fact] + public async Task Should_return_single_content_if_finding_content() + { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { <FIELDS_CONTENT> } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new - { - findMySchemaContent = TestContent.Response(content) - } - }; + findMySchemaContent = TestContent.Response(content) + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_single_content_if_finding_content_with_version() - { - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); + [Fact] + public async Task Should_return_single_content_if_finding_content_with_version() + { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>', version: 3) { <FIELDS_CONTENT> } }", contentId); - A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), contentId, 3, A<CancellationToken>._)) - .Returns(content); + A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), contentId, 3, A<CancellationToken>._)) + .Returns(content); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new - { - findMySchemaContent = TestContent.Response(content) - } - }; + findMySchemaContent = TestContent.Response(content) + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_embedded_contents_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); + [Fact] + public async Task Should_also_fetch_embedded_contents_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -468,41 +468,40 @@ ... on MyRefSchema1 { } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = content.Id, + data = new { - id = content.Id, - data = new + myEmbeds = new { - myEmbeds = new + iv = new { - iv = new + text = $"assets:{DomainId.Empty}, contents:{contentRefId}", + contents = new[] { - text = $"assets:{DomainId.Empty}, contents:{contentRefId}", - contents = new[] + new { - new + id = contentRefId, + data = new { - id = contentRefId, - data = new + schemaRef1Field = new { - schemaRef1Field = new - { - iv = "ref1" - } + iv = "ref1" } } } @@ -511,21 +510,22 @@ ... on MyRefSchema1 { } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_referenced_contents_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); + [Fact] + public async Task Should_also_fetch_referenced_contents_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -544,38 +544,37 @@ public async Task Should_also_fetch_referenced_contents_if_field_is_included_in_ } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = content.Id, + data = new { - id = content.Id, - data = new + myReferences = new { - myReferences = new + iv = new[] { - iv = new[] + new { - new + id = contentRefId, + data = new { - id = contentRefId, - data = new + schemaRef1Field = new { - schemaRef1Field = new - { - iv = "ref1" - } + iv = "ref1" } } } @@ -583,21 +582,22 @@ public async Task Should_also_fetch_referenced_contents_if_field_is_included_in_ } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_referenced_contents_from_flat_data_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); + [Fact] + public async Task Should_also_fetch_referenced_contents_from_flat_data_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -609,50 +609,50 @@ public async Task Should_also_fetch_referenced_contents_from_flat_data_if_field_ } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = content.Id, + flatData = new { - id = content.Id, - flatData = new + myReferences = new[] { - myReferences = new[] + new { - new - { - id = contentRefId - } + id = contentRefId } } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_cache_referenced_contents_from_flat_data_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); + [Fact] + public async Task Should_cache_referenced_contents_from_flat_data_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -664,56 +664,56 @@ myReferences @cache(duration: 1000) { } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - var actual1 = await ExecuteAsync(new ExecutionOptions { Query = query }); - var actual2 = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual1 = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual2 = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = content.Id, + flatData = new { - id = content.Id, - flatData = new + myReferences = new[] { - myReferences = new[] + new { - new - { - id = contentRefId - } + id = contentRefId } } } } - }; + } + }; - AssertResult(expected, actual1); - AssertResult(expected, actual2); + AssertResult(expected, actual1); + AssertResult(expected, actual2); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_also_fetch_referencing_contents_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "ref1-field", "ref1"); + [Fact] + public async Task Should_also_fetch_referencing_contents_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "ref1-field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMyRefSchema1Content(id: '<ID>') { id @@ -728,54 +728,54 @@ public async Task Should_also_fetch_referencing_contents_if_field_is_included_in } }", contentRefId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMyRefSchema1Content = new { - findMyRefSchema1Content = new + id = contentRefId, + referencingMySchemaContents = new[] { - id = contentRefId, - referencingMySchemaContents = new[] + new { - new + id = contentId, + data = new { - id = contentId, - data = new + myLocalizedString = new { - myLocalizedString = new - { - de_DE = "de-DE" - } + de_DE = "de-DE" } } } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_referencing_contents_with_total_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "ref1-field", "ref1"); + [Fact] + public async Task Should_also_fetch_referencing_contents_with_total_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "ref1-field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMyRefSchema1Content(id: '<ID>') { id @@ -793,58 +793,58 @@ public async Task Should_also_fetch_referencing_contents_with_total_if_field_is_ } }", contentRefId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && !x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(10, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && !x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(10, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMyRefSchema1Content = new { - findMyRefSchema1Content = new + id = contentRefId, + referencingMySchemaContentsWithTotal = new { - id = contentRefId, - referencingMySchemaContentsWithTotal = new + total = 10, + items = new[] { - total = 10, - items = new[] + new { - new + id = contentId, + data = new { - id = contentId, - data = new + myLocalizedString = new { - myLocalizedString = new - { - de_DE = "de-DE" - } + de_DE = "de-DE" } } } } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_references_contents_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "ref1-field", "ref1"); + [Fact] + public async Task Should_also_fetch_references_contents_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "ref1-field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -854,47 +854,47 @@ public async Task Should_also_fetch_references_contents_if_field_is_included_in_ } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), contentRef.SchemaId.Id.ToString(), - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId && x.NoTotal), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), contentRef.SchemaId.Id.ToString(), + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId && x.NoTotal), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, contentRef)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = contentId, + referencesMyRefSchema1Contents = new[] { - id = contentId, - referencesMyRefSchema1Contents = new[] + new { - new - { - id = contentRefId - } + id = contentRefId } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_references_contents_with_total_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "ref1-field", "ref1"); + [Fact] + public async Task Should_also_fetch_references_contents_with_total_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "ref1-field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -907,51 +907,51 @@ public async Task Should_also_fetch_references_contents_with_total_if_field_is_i } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), contentRef.SchemaId.Id.ToString(), - A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(10, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), contentRef.SchemaId.Id.ToString(), + A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(10, contentRef)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = contentId, + referencesMyRefSchema1ContentsWithTotal = new { - id = contentId, - referencesMyRefSchema1ContentsWithTotal = new + total = 10, + items = new[] { - total = 10, - items = new[] + new { - new - { - id = contentRefId - } + id = contentRefId } } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_union_contents_if_field_is_included_in_query() - { - var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); + [Fact] + public async Task Should_also_fetch_union_contents_if_field_is_included_in_query() + { + var contentRefId = DomainId.NewGuid(); + var contentRef = TestContent.CreateRef(TestSchemas.Ref1Id, contentRefId, "schemaRef1Field", "ref1"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, contentRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, contentRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -975,61 +975,61 @@ ... on MyRefSchema1 { } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = content.Id, + data = new { - id = content.Id, - data = new + myUnion = new { - myUnion = new + iv = new[] { - iv = new[] + new { - new + id = contentRefId, + data = new { - id = contentRefId, - data = new + schemaRef1Field = new { - schemaRef1Field = new - { - iv = "ref1" - } - }, - __typename = "MyRefSchema1" - } + iv = "ref1" + } + }, + __typename = "MyRefSchema1" } } } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_embedded_assets_if_field_is_included_in_query() - { - var assetRefId = DomainId.NewGuid(); - var assetRef = TestAsset.Create(assetRefId); + [Fact] + public async Task Should_also_fetch_embedded_assets_if_field_is_included_in_query() + { + var assetRefId = DomainId.NewGuid(); + var assetRef = TestAsset.Create(assetRefId); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, assetId: assetRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, assetId: assetRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -1046,57 +1046,57 @@ public async Task Should_also_fetch_embedded_assets_if_field_is_included_in_quer } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A<Q>.That.HasIdsWithoutTotal(assetRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, assetRef)); + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, + A<Q>.That.HasIdsWithoutTotal(assetRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, assetRef)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = content.Id, + data = new { - id = content.Id, - data = new + myEmbeds = new { - myEmbeds = new + iv = new { - iv = new + text = $"assets:{assetRefId}, contents:{DomainId.Empty}", + assets = new[] { - text = $"assets:{assetRefId}, contents:{DomainId.Empty}", - assets = new[] + new { - new - { - id = assetRefId - } + id = assetRefId } } } } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_also_fetch_referenced_assets_if_field_is_included_in_query() - { - var assetRefId = DomainId.NewGuid(); - var assetRef = TestAsset.Create(assetRefId); + [Fact] + public async Task Should_also_fetch_referenced_assets_if_field_is_included_in_query() + { + var assetRefId = DomainId.NewGuid(); + var assetRef = TestAsset.Create(assetRefId); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, assetId: assetRefId); + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, assetId: assetRefId); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -1110,50 +1110,50 @@ public async Task Should_also_fetch_referenced_assets_if_field_is_included_in_qu } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A<Q>.That.HasIdsWithoutTotal(assetRefId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(0, assetRef)); + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, + A<Q>.That.HasIdsWithoutTotal(assetRefId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(0, assetRef)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + data = new { - data = new + findMySchemaContent = new { - findMySchemaContent = new + id = content.Id, + data = new { - id = content.Id, - data = new + myAssets = new { - myAssets = new + iv = new[] { - iv = new[] + new { - new - { - id = assetRefId - } + id = assetRefId } } } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_not_return_data_if_field_not_part_of_content() - { - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId, data: new ContentData()); + [Fact] + public async Task Should_not_return_data_if_field_not_part_of_content() + { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId, data: new ContentData()); - var query = CreateQuery(@" + var query = CreateQuery(@" query { findMySchemaContent(id: '<ID>') { id @@ -1171,15 +1171,14 @@ public async Task Should_not_return_data_if_field_not_part_of_content() } }", contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, content)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, content)); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var json = serializer.Serialize(actual); + var json = serializer.Serialize(actual); - Assert.Contains("\"errors\"", json, StringComparison.Ordinal); - } + Assert.Contains("\"errors\"", json, StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs index ce96db219a..6924eb15e5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs @@ -15,16 +15,16 @@ using Squidex.Shared; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public class GraphQLSubscriptionTests : GraphQLTestBase { - public class GraphQLSubscriptionTests : GraphQLTestBase + [Fact] + public async Task Should_subscribe_to_assets() { - [Fact] - public async Task Should_subscribe_to_assets() - { - var id = DomainId.NewGuid(); + var id = DomainId.NewGuid(); - var query = CreateQuery(@" + var query = CreateQuery(@" subscription { assetChanges { id, @@ -33,42 +33,42 @@ public async Task Should_subscribe_to_assets() } }"); - var stream = - Observable.Return<object>( - new EnrichedAssetEvent - { - Id = id, - FileName = "image.png", - FileSize = 1024 - }); + var stream = + Observable.Return<object>( + new EnrichedAssetEvent + { + Id = id, + FileName = "image.png", + FileSize = 1024 + }); - A.CallTo(() => subscriptionService.Subscribe<object>(A<AssetSubscription>._)) - .Returns(stream); + A.CallTo(() => subscriptionService.Subscribe<object>(A<AssetSubscription>._)) + .Returns(stream); - var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, TestApp.Default.Name); + var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, TestApp.Default.Name); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); - var expected = new + var expected = new + { + data = new { - data = new + assetChanges = new { - assetChanges = new - { - id, - fileName = "image.png", - fileSize = 1024 - } + id, + fileName = "image.png", + fileSize = 1024 } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_error_if_user_has_no_permissions_for_assets() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_error_if_user_has_no_permissions_for_assets() + { + var query = CreateQuery(@" subscription { assetChanges { id, @@ -77,41 +77,41 @@ public async Task Should_return_error_if_user_has_no_permissions_for_assets() } }"); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new[] { - errors = new[] + new { - new + message = "You do not have the necessary permission.", + locations = new[] { - message = "You do not have the necessary permission.", - locations = new[] + new { - new - { - line = 3, - column = 19 - } - }, - path = new[] - { - "assetChanges" + line = 3, + column = 19 } + }, + path = new[] + { + "assetChanges" } - }, - data = (object?)null - }; + } + }, + data = (object?)null + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_subscribe_to_contents() - { - var id = DomainId.NewGuid(); + [Fact] + public async Task Should_subscribe_to_contents() + { + var id = DomainId.NewGuid(); - var query = CreateQuery(@" + var query = CreateQuery(@" subscription { contentChanges { id, @@ -119,49 +119,49 @@ public async Task Should_subscribe_to_contents() } }"); - var stream = - Observable.Return<object>( - new EnrichedContentEvent - { - Id = id, - Data = new ContentData() - .AddField("field", - new ContentFieldData() - .AddInvariant(42)) - }); + var stream = + Observable.Return<object>( + new EnrichedContentEvent + { + Id = id, + Data = new ContentData() + .AddField("field", + new ContentFieldData() + .AddInvariant(42)) + }); - A.CallTo(() => subscriptionService.Subscribe<object>(A<ContentSubscription>._)) - .Returns(stream); + A.CallTo(() => subscriptionService.Subscribe<object>(A<ContentSubscription>._)) + .Returns(stream); - var permission = PermissionIds.ForApp(PermissionIds.AppContentsRead, TestApp.Default.Name, "random-schema"); + var permission = PermissionIds.ForApp(PermissionIds.AppContentsRead, TestApp.Default.Name, "random-schema"); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id); - var expected = new + var expected = new + { + data = new { - data = new + contentChanges = new { - contentChanges = new + id, + data = new { - id, - data = new + field = new { - field = new - { - iv = 42 - } + iv = 42 } } } - }; + } + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); + } - [Fact] - public async Task Should_return_error_if_user_has_no_permissions_for_contents() - { - var query = CreateQuery(@" + [Fact] + public async Task Should_return_error_if_user_has_no_permissions_for_contents() + { + var query = CreateQuery(@" subscription { contentChanges { id, @@ -169,33 +169,32 @@ public async Task Should_return_error_if_user_has_no_permissions_for_contents() } }"); - var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); - var expected = new + var expected = new + { + errors = new[] { - errors = new[] + new { - new + message = "You do not have the necessary permission.", + locations = new[] { - message = "You do not have the necessary permission.", - locations = new[] + new { - new - { - line = 3, - column = 19 - } - }, - path = new[] - { - "contentChanges" + line = 3, + column = 19 } + }, + path = new[] + { + "contentChanges" } - }, - data = (object?)null - }; + } + }, + data = (object?)null + }; - AssertResult(expected, actual); - } + AssertResult(expected, actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index e773e57d3b..90ce8f1f36 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -32,189 +32,188 @@ #pragma warning disable SA1401 // Fields must be private -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture> { - public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture> + protected readonly GraphQLSerializer serializer = new GraphQLSerializer(TestUtils.DefaultOptions()); + protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + protected readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + protected readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); + protected readonly ISubscriptionService subscriptionService = A.Fake<ISubscriptionService>(); + protected readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + protected readonly Context requestContext; + private CachingGraphQLResolver? sut; + + protected GraphQLTestBase() { - protected readonly GraphQLSerializer serializer = new GraphQLSerializer(TestUtils.DefaultOptions()); - protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - protected readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - protected readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); - protected readonly ISubscriptionService subscriptionService = A.Fake<ISubscriptionService>(); - protected readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - protected readonly Context requestContext; - private CachingGraphQLResolver? sut; - - protected GraphQLTestBase() - { - A.CallTo(() => userResolver.QueryManyAsync(A<string[]>._, default)) - .ReturnsLazily(x => - { - var ids = x.GetArgument<string[]>(0)!; + A.CallTo(() => userResolver.QueryManyAsync(A<string[]>._, default)) + .ReturnsLazily(x => + { + var ids = x.GetArgument<string[]>(0)!; - var users = ids.Select(id => UserMocks.User(id, $"{id}@email.com", $"name_{id}")); + var users = ids.Select(id => UserMocks.User(id, $"{id}@email.com", $"name_{id}")); - return Task.FromResult(users.ToDictionary(x => x.Id)); - }); + return Task.FromResult(users.ToDictionary(x => x.Id)); + }); - requestContext = new Context(Mocks.FrontendUser(), TestApp.Default); - } + requestContext = new Context(Mocks.FrontendUser(), TestApp.Default); + } - protected void AssertResult(object expected, ExecutionResult actual) - { - var jsonOutputResult = serializer.Serialize(actual); - var isonOutputExpected = serializer.Serialize(expected); + protected void AssertResult(object expected, ExecutionResult actual) + { + var jsonOutputResult = serializer.Serialize(actual); + var isonOutputExpected = serializer.Serialize(expected); - Assert.Equal(isonOutputExpected, jsonOutputResult); - } + Assert.Equal(isonOutputExpected, jsonOutputResult); + } - protected Task<ExecutionResult> ExecuteAsync(ExecutionOptions options) - { - return ExecuteCoreAsync(options, requestContext); - } + protected Task<ExecutionResult> ExecuteAsync(ExecutionOptions options) + { + return ExecuteCoreAsync(options, requestContext); + } - protected Task<ExecutionResult> ExecuteAsync(ExecutionOptions options, string permissionId) + protected Task<ExecutionResult> ExecuteAsync(ExecutionOptions options, string permissionId) + { + return ExecuteCoreAsync(options, BuildContext(permissionId)); + } + + protected async Task<ExecutionResult> ExecuteCoreAsync(ExecutionOptions options, Context context) + { + // Use a shared instance to test caching. + sut ??= CreateSut(TestSchemas.Default, TestSchemas.Ref1, TestSchemas.Ref2); + + // Provide the context to the test if services need to be resolved. + var graphQLContext = ActivatorUtilities.CreateInstance<GraphQLExecutionContext>(sut.Services, context)!; + + options.UserContext = graphQLContext; + + // Register data loader and other listeners. + foreach (var listener in sut.Services.GetRequiredService<IEnumerable<IDocumentExecutionListener>>()) { - return ExecuteCoreAsync(options, BuildContext(permissionId)); + options.Listeners.Add(listener); } - protected async Task<ExecutionResult> ExecuteCoreAsync(ExecutionOptions options, Context context) - { - // Use a shared instance to test caching. - sut ??= CreateSut(TestSchemas.Default, TestSchemas.Ref1, TestSchemas.Ref2); + // Enrich the context with the schema. + await sut.ExecuteAsync(options, x => Task.FromResult<ExecutionResult>(null!)); - // Provide the context to the test if services need to be resolved. - var graphQLContext = ActivatorUtilities.CreateInstance<GraphQLExecutionContext>(sut.Services, context)!; + var actual = await new DocumentExecuter().ExecuteAsync(options); - options.UserContext = graphQLContext; + if (actual.Streams?.Count > 0 && actual.Errors?.Any() != true) + { + // Resolve the first stream actual with a timeout. + var stream = actual.Streams.First(); - // Register data loader and other listeners. - foreach (var listener in sut.Services.GetRequiredService<IEnumerable<IDocumentExecutionListener>>()) + using (var cts = new CancellationTokenSource(5000)) { - options.Listeners.Add(listener); + actual = await stream.Value.FirstAsync().ToTask().WithCancellation(cts.Token); } + } - // Enrich the context with the schema. - await sut.ExecuteAsync(options, x => Task.FromResult<ExecutionResult>(null!)); - - var actual = await new DocumentExecuter().ExecuteAsync(options); + return actual; + } - if (actual.Streams?.Count > 0 && actual.Errors?.Any() != true) - { - // Resolve the first stream actual with a timeout. - var stream = actual.Streams.First(); + private static Context BuildContext(string permissionId) + { + var permission = PermissionIds.ForApp(permissionId, TestApp.Default.Name, TestSchemas.DefaultId.Name).Id; - using (var cts = new CancellationTokenSource(5000)) - { - actual = await stream.Value.FirstAsync().ToTask().WithCancellation(cts.Token); - } - } + return new Context(Mocks.FrontendUser(permission: permission), TestApp.Default); + } - return actual; - } + protected CachingGraphQLResolver CreateSut(params ISchemaEntity[] schemas) + { + var appProvider = A.Fake<IAppProvider>(); - private static Context BuildContext(string permissionId) - { - var permission = PermissionIds.ForApp(permissionId, TestApp.Default.Name, TestSchemas.DefaultId.Name).Id; + A.CallTo(() => appProvider.GetSchemasAsync(TestApp.Default.Id, default)) + .Returns(schemas.ToList()); - return new Context(Mocks.FrontendUser(permission: permission), TestApp.Default); - } + var serviceProvider = + new ServiceCollection() + .AddLogging() + .AddMemoryCache() + .AddBackgroundCache() + .Configure<AssetOptions>(x => + { + x.CanCache = true; + }) + .Configure<ContentOptions>(x => + { + x.CanCache = true; + }) + .AddSingleton<StringReferenceExtractor>() + .AddSingleton<IDocumentExecutionListener, + DataLoaderDocumentListener>() + .AddSingleton<IDataLoaderContextAccessor, + DataLoaderContextAccessor>() + .AddTransient<IAssetCache, + AssetCache>() + .AddTransient<IContentCache, + ContentCache>() + .AddSingleton<IUrlGenerator, + FakeUrlGenerator>() + .AddSingleton( + A.Fake<ILoggerFactory>()) + .AddSingleton( + A.Fake<ISchemasHash>()) + .AddSingleton(appProvider) + .AddSingleton(assetQuery) + .AddSingleton(commandBus) + .AddSingleton(contentQuery) + .AddSingleton(subscriptionService) + .AddSingleton(userResolver) + .BuildServiceProvider(); + + return ActivatorUtilities.CreateInstance<CachingGraphQLResolver>(serviceProvider); + } - protected CachingGraphQLResolver CreateSut(params ISchemaEntity[] schemas) + protected static string CreateQuery(string query, DomainId id = default, IEnrichedContentEntity? content = null) + { + query = query + .Replace("'", "\"", StringComparison.Ordinal) + .Replace("`", "\"", StringComparison.Ordinal) + .Replace("<FIELDS_ASSET>", TestAsset.AllFields, StringComparison.Ordinal) + .Replace("<FIELDS_CONTENT>", TestContent.AllFields, StringComparison.Ordinal) + .Replace("<FIELDS_CONTENT_FLAT>", TestContent.AllFlatFields, StringComparison.Ordinal); + + if (id != default) { - var appProvider = A.Fake<IAppProvider>(); - - A.CallTo(() => appProvider.GetSchemasAsync(TestApp.Default.Id, default)) - .Returns(schemas.ToList()); - - var serviceProvider = - new ServiceCollection() - .AddLogging() - .AddMemoryCache() - .AddBackgroundCache() - .Configure<AssetOptions>(x => - { - x.CanCache = true; - }) - .Configure<ContentOptions>(x => - { - x.CanCache = true; - }) - .AddSingleton<StringReferenceExtractor>() - .AddSingleton<IDocumentExecutionListener, - DataLoaderDocumentListener>() - .AddSingleton<IDataLoaderContextAccessor, - DataLoaderContextAccessor>() - .AddTransient<IAssetCache, - AssetCache>() - .AddTransient<IContentCache, - ContentCache>() - .AddSingleton<IUrlGenerator, - FakeUrlGenerator>() - .AddSingleton( - A.Fake<ILoggerFactory>()) - .AddSingleton( - A.Fake<ISchemasHash>()) - .AddSingleton(appProvider) - .AddSingleton(assetQuery) - .AddSingleton(commandBus) - .AddSingleton(contentQuery) - .AddSingleton(subscriptionService) - .AddSingleton(userResolver) - .BuildServiceProvider(); - - return ActivatorUtilities.CreateInstance<CachingGraphQLResolver>(serviceProvider); + query = query.Replace("<ID>", id.ToString(), StringComparison.Ordinal); } - protected static string CreateQuery(string query, DomainId id = default, IEnrichedContentEntity? content = null) + if (query.Contains("<DATA>", StringComparison.Ordinal) && content != null) { - query = query - .Replace("'", "\"", StringComparison.Ordinal) - .Replace("`", "\"", StringComparison.Ordinal) - .Replace("<FIELDS_ASSET>", TestAsset.AllFields, StringComparison.Ordinal) - .Replace("<FIELDS_CONTENT>", TestContent.AllFields, StringComparison.Ordinal) - .Replace("<FIELDS_CONTENT_FLAT>", TestContent.AllFlatFields, StringComparison.Ordinal); - - if (id != default) - { - query = query.Replace("<ID>", id.ToString(), StringComparison.Ordinal); - } + var data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id); - if (query.Contains("<DATA>", StringComparison.Ordinal) && content != null) - { - var data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id); - - // Json is not the same as the input format of graphql, therefore we need to convert it. - var dataJson = TestUtils.DefaultSerializer.Serialize(data, true); + // Json is not the same as the input format of graphql, therefore we need to convert it. + var dataJson = TestUtils.DefaultSerializer.Serialize(data, true); - // Use properties without quotes. - dataJson = Regex.Replace(dataJson, "\"([^\"]+)\":", x => $"{x.Groups[1].Value}:"); + // Use properties without quotes. + dataJson = Regex.Replace(dataJson, "\"([^\"]+)\":", x => $"{x.Groups[1].Value}:"); - // Use enum values whithout quotes. - dataJson = Regex.Replace(dataJson, "\"Enum([A-Za-z]+)\"", x => $"Enum{x.Groups[1].Value}"); - - query = query.Replace("<DATA>", dataJson, StringComparison.Ordinal); - } + // Use enum values whithout quotes. + dataJson = Regex.Replace(dataJson, "\"Enum([A-Za-z]+)\"", x => $"Enum{x.Groups[1].Value}"); - return query; + query = query.Replace("<DATA>", dataJson, StringComparison.Ordinal); } - protected Context MatchsAssetContext() - { - return A<Context>.That.Matches(x => - x.App == TestApp.Default && - x.ShouldSkipCleanup() && - x.ShouldSkipContentEnrichment() && - x.UserPrincipal == requestContext.UserPrincipal); - } + return query; + } - protected Context MatchsContentContext() - { - return A<Context>.That.Matches(x => - x.App == TestApp.Default && - x.ShouldSkipCleanup() && - x.ShouldSkipContentEnrichment() && - x.UserPrincipal == requestContext.UserPrincipal); - } + protected Context MatchsAssetContext() + { + return A<Context>.That.Matches(x => + x.App == TestApp.Default && + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.UserPrincipal == requestContext.UserPrincipal); + } + + protected Context MatchsContentContext() + { + return A<Context>.That.Matches(x => + x.App == TestApp.Default && + x.ShouldSkipCleanup() && + x.ShouldSkipContentEnrichment() && + x.UserPrincipal == requestContext.UserPrincipal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/NamesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/NamesTests.cs index 4c5b9b8220..0cdf6ee132 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/NamesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/NamesTests.cs @@ -8,66 +8,65 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public sealed class NamesTests { - public sealed class NamesTests + [Fact] + public void Should_return_name_if_not_taken() { - [Fact] - public void Should_return_name_if_not_taken() - { - var sut = ReservedNames.ForFields(); + var sut = ReservedNames.ForFields(); - var result = sut["myName"]; + var result = sut["myName"]; - Assert.Equal("myName", result); - } + Assert.Equal("myName", result); + } - [Fact] - public void Should_return_corrected_name_if_not_taken() - { - var sut = ReservedNames.ForFields(); + [Fact] + public void Should_return_corrected_name_if_not_taken() + { + var sut = ReservedNames.ForFields(); - var result = sut["2myName"]; + var result = sut["2myName"]; - Assert.Equal("gql_2myName", result); - } + Assert.Equal("gql_2myName", result); + } - [Fact] - public void Should_return_name_with_offset_if_taken() - { - var sut = ReservedNames.ForFields(); + [Fact] + public void Should_return_name_with_offset_if_taken() + { + var sut = ReservedNames.ForFields(); - var result1 = sut["myName"]; - var result2 = sut["myName"]; - var result3 = sut["myName"]; + var result1 = sut["myName"]; + var result2 = sut["myName"]; + var result3 = sut["myName"]; - Assert.Equal("myName", result1); - Assert.Equal("myName2", result2); - Assert.Equal("myName3", result3); - } + Assert.Equal("myName", result1); + Assert.Equal("myName2", result2); + Assert.Equal("myName3", result3); + } - [Fact] - public void Should_return_corrected_name_with_offset_if_taken() - { - var sut = ReservedNames.ForFields(); + [Fact] + public void Should_return_corrected_name_with_offset_if_taken() + { + var sut = ReservedNames.ForFields(); - var result1 = sut["2myName"]; - var result2 = sut["2myName"]; - var result3 = sut["2myName"]; + var result1 = sut["2myName"]; + var result2 = sut["2myName"]; + var result3 = sut["2myName"]; - Assert.Equal("gql_2myName", result1); - Assert.Equal("gql_2myName2", result2); - Assert.Equal("gql_2myName3", result3); - } + Assert.Equal("gql_2myName", result1); + Assert.Equal("gql_2myName2", result2); + Assert.Equal("gql_2myName3", result3); + } - [Fact] - public void Should_return_name_with_offset_if_reserved() - { - var sut = ReservedNames.ForTypes(); + [Fact] + public void Should_return_name_with_offset_if_reserved() + { + var sut = ReservedNames.ForTypes(); - var result = sut["Content"]; + var result = sut["Content"]; - Assert.Equal("Content2", result); - } + Assert.Equal("Content2", result); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestApp.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestApp.cs index 561e9ce4e5..472ead8e7d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestApp.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestApp.cs @@ -9,17 +9,16 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public static class TestApp { - public static class TestApp - { - public static readonly NamedId<DomainId> DefaultId = NamedId.Of(DomainId.NewGuid(), "my-app"); + public static readonly NamedId<DomainId> DefaultId = NamedId.Of(DomainId.NewGuid(), "my-app"); - public static readonly IAppEntity Default; + public static readonly IAppEntity Default; - static TestApp() - { - Default = Mocks.App(DefaultId, Language.DE, Language.GermanGermany); - } + static TestApp() + { + Default = Mocks.App(DefaultId, Language.DE, Language.GermanGermany); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs index 308c57c951..6f32b9eee2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs @@ -10,11 +10,11 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public static class TestAsset { - public static class TestAsset - { - public const string AllFields = @" + public const string AllFields = @" id version created @@ -52,89 +52,88 @@ public static class TestAsset metadata slug"; - public static IEnrichedAssetEntity Create(DomainId id) - { - var now = SystemClock.Instance.GetCurrentInstant(); + public static IEnrichedAssetEntity Create(DomainId id) + { + var now = SystemClock.Instance.GetCurrentInstant(); - var asset = new AssetEntity + var asset = new AssetEntity + { + Id = id, + AppId = TestApp.DefaultId, + Version = 1, + Created = now, + CreatedBy = RefToken.User("user1"), + EditToken = $"token_{id}", + LastModified = now, + LastModifiedBy = RefToken.Client("client1"), + FileName = "MyFile.png", + Slug = "myfile.png", + FileSize = 1024, + FileHash = "ABC123", + FileVersion = 123, + MimeType = "image/png", + Type = AssetType.Image, + MetadataText = "metadata-text", + Metadata = + new AssetMetadata() + .SetPixelWidth(800) + .SetPixelHeight(600), + TagNames = new[] { - Id = id, - AppId = TestApp.DefaultId, - Version = 1, - Created = now, - CreatedBy = RefToken.User("user1"), - EditToken = $"token_{id}", - LastModified = now, - LastModifiedBy = RefToken.Client("client1"), - FileName = "MyFile.png", - Slug = "myfile.png", - FileSize = 1024, - FileHash = "ABC123", - FileVersion = 123, - MimeType = "image/png", - Type = AssetType.Image, - MetadataText = "metadata-text", - Metadata = - new AssetMetadata() - .SetPixelWidth(800) - .SetPixelHeight(600), - TagNames = new[] - { - "tag1", - "tag2" - }.ToHashSet() - }; + "tag1", + "tag2" + }.ToHashSet() + }; - return asset; - } + return asset; + } - public static object Response(IEnrichedAssetEntity asset) + public static object Response(IEnrichedAssetEntity asset) + { + return new { - return new + id = asset.Id, + version = asset.Version, + created = asset.Created, + createdBy = asset.CreatedBy.ToString(), + createdByUser = new + { + id = asset.CreatedBy.Identifier, + email = $"{asset.CreatedBy.Identifier}@email.com", + displayName = $"name_{asset.CreatedBy.Identifier}" + }, + editToken = $"token_{asset.Id}", + lastModified = asset.LastModified, + lastModifiedBy = asset.LastModifiedBy.ToString(), + lastModifiedByUser = new + { + id = asset.LastModifiedBy.Identifier, + email = $"{asset.LastModifiedBy}", + displayName = asset.LastModifiedBy.Identifier + }, + url = $"assets/{asset.AppId.Name}/{asset.Id}", + thumbnailUrl = $"assets/{asset.AppId.Name}/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = asset.MimeType, + fileName = asset.FileName, + fileHash = asset.FileHash, + fileSize = asset.FileSize, + fileVersion = asset.FileVersion, + isImage = true, + isProtected = asset.IsProtected, + pixelWidth = asset.Metadata.GetPixelWidth(), + pixelHeight = asset.Metadata.GetPixelHeight(), + tags = asset.TagNames, + type = "IMAGE", + metadataText = asset.MetadataText, + metadataPixelWidth = 800, + metadataUnknown = (string?)null, + metadata = new { - id = asset.Id, - version = asset.Version, - created = asset.Created, - createdBy = asset.CreatedBy.ToString(), - createdByUser = new - { - id = asset.CreatedBy.Identifier, - email = $"{asset.CreatedBy.Identifier}@email.com", - displayName = $"name_{asset.CreatedBy.Identifier}" - }, - editToken = $"token_{asset.Id}", - lastModified = asset.LastModified, - lastModifiedBy = asset.LastModifiedBy.ToString(), - lastModifiedByUser = new - { - id = asset.LastModifiedBy.Identifier, - email = $"{asset.LastModifiedBy}", - displayName = asset.LastModifiedBy.Identifier - }, - url = $"assets/{asset.AppId.Name}/{asset.Id}", - thumbnailUrl = $"assets/{asset.AppId.Name}/{asset.Id}?width=100", - sourceUrl = $"assets/source/{asset.Id}", - mimeType = asset.MimeType, - fileName = asset.FileName, - fileHash = asset.FileHash, - fileSize = asset.FileSize, - fileVersion = asset.FileVersion, - isImage = true, - isProtected = asset.IsProtected, pixelWidth = asset.Metadata.GetPixelWidth(), - pixelHeight = asset.Metadata.GetPixelHeight(), - tags = asset.TagNames, - type = "IMAGE", - metadataText = asset.MetadataText, - metadataPixelWidth = 800, - metadataUnknown = (string?)null, - metadata = new - { - pixelWidth = asset.Metadata.GetPixelWidth(), - pixelHeight = asset.Metadata.GetPixelHeight() - }, - slug = asset.Slug - }; - } + pixelHeight = asset.Metadata.GetPixelHeight() + }, + slug = asset.Slug + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs index 226383f70e..853e57ff8b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs @@ -10,11 +10,11 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public static class TestContent { - public static class TestContent - { - public const string AllFields = @" + public const string AllFields = @" id version created @@ -120,7 +120,7 @@ ... on MyRefSchema2Component { } }"; - public const string AllFlatFields = @" + public const string AllFlatFields = @" id version created @@ -194,552 +194,405 @@ ... on MyRefSchema2Component { } }"; - public static IEnrichedContentEntity Create(DomainId id, DomainId refId = default, DomainId assetId = default, ContentData? data = null) - { - var now = SystemClock.Instance.GetCurrentInstant(); + public static IEnrichedContentEntity Create(DomainId id, DomainId refId = default, DomainId assetId = default, ContentData? data = null) + { + var now = SystemClock.Instance.GetCurrentInstant(); - data ??= - new ContentData() - .AddField("my-localized-string", - new ContentFieldData() - .AddLocalized("de-DE", "de-DE")) - .AddField("my-string", - new ContentFieldData() - .AddInvariant(JsonValue.Null)) - .AddField("my-string-enum", - new ContentFieldData() - .AddInvariant("EnumA")) - .AddField("my-assets", - new ContentFieldData() - .AddInvariant(JsonValue.Array(assetId.ToString()))) - .AddField("my-number", - new ContentFieldData() - .AddInvariant(1.0)) - .AddField("my-boolean", - new ContentFieldData() - .AddInvariant(true)) - .AddField("my-datetime", - new ContentFieldData() - .AddInvariant(now)) - .AddField("my-tags", - new ContentFieldData() - .AddInvariant(JsonValue.Array("tag1", "tag2"))) - .AddField("my-tags-enum", - new ContentFieldData() - .AddInvariant(JsonValue.Array("EnumA", "EnumB"))) - .AddField("my-references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(refId.ToString()))) - .AddField("my-union", - new ContentFieldData() - .AddInvariant(JsonValue.Array(refId.ToString()))) - .AddField("my-geolocation", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("latitude", 10) - .Add("longitude", 20))) - .AddField("my-component", - new ContentFieldData() - .AddInvariant( + data ??= + new ContentData() + .AddField("my-localized-string", + new ContentFieldData() + .AddLocalized("de-DE", "de-DE")) + .AddField("my-string", + new ContentFieldData() + .AddInvariant(JsonValue.Null)) + .AddField("my-string-enum", + new ContentFieldData() + .AddInvariant("EnumA")) + .AddField("my-assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(assetId.ToString()))) + .AddField("my-number", + new ContentFieldData() + .AddInvariant(1.0)) + .AddField("my-boolean", + new ContentFieldData() + .AddInvariant(true)) + .AddField("my-datetime", + new ContentFieldData() + .AddInvariant(now)) + .AddField("my-tags", + new ContentFieldData() + .AddInvariant(JsonValue.Array("tag1", "tag2"))) + .AddField("my-tags-enum", + new ContentFieldData() + .AddInvariant(JsonValue.Array("EnumA", "EnumB"))) + .AddField("my-references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(refId.ToString()))) + .AddField("my-union", + new ContentFieldData() + .AddInvariant(JsonValue.Array(refId.ToString()))) + .AddField("my-geolocation", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("latitude", 10) + .Add("longitude", 20))) + .AddField("my-component", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add(Component.Discriminator, TestSchemas.Ref1.Id) + .Add("schemaRef1Field", "Component1"))) + .AddField("my-components", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( new JsonObject() .Add(Component.Discriminator, TestSchemas.Ref1.Id) - .Add("schemaRef1Field", "Component1"))) - .AddField("my-components", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add(Component.Discriminator, TestSchemas.Ref1.Id) - .Add("schemaRef1Field", "Component1"), - new JsonObject() - .Add(Component.Discriminator, TestSchemas.Ref2.Id) - .Add("schemaRef2Field", "Component2")))) - .AddField("my-json", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("value", 1))) - .AddField("my-json2", - new ContentFieldData() - .AddInvariant( - JsonValue.Object() - .Add("rootString", "Root String") - .Add("rootInt", 42) - .Add("rootFloat", 3.14) - .Add("rootBoolean", true) - .Add("rootArray", - JsonValue.Array() - .Add("1") - .Add("2") - .Add("3")) - .Add("rootObject", - JsonValue.Object() - .Add("nestedString", "Nested String") - .Add("nestedInt", 42) - .Add("nestedFloat", 3.14) - .Add("nestedBoolean", true) - .Add("nestedArray", - JsonValue.Array() - .Add("1") - .Add("2") - .Add("3"))))) - .AddField("my-array", - new ContentFieldData() - .AddInvariant(JsonValue.Array( - new JsonObject() - .Add("nested-number", 42) - .Add("nested-boolean", true), + .Add("schemaRef1Field", "Component1"), new JsonObject() - .Add("nested-number", 3.14) - .Add("nested-boolean", false)))); - - if (assetId != default || refId != default) - { - data.AddField("my-embeds", + .Add(Component.Discriminator, TestSchemas.Ref2.Id) + .Add("schemaRef2Field", "Component2")))) + .AddField("my-json", new ContentFieldData() - .AddInvariant(JsonValue.Create($"assets:{assetId}, contents:{refId}"))); - } - - var content = new ContentEntity - { - Id = id, - AppId = TestApp.DefaultId, - Version = 1, - Created = now, - CreatedBy = RefToken.User("user1"), - EditToken = $"token_{id}", - LastModified = now, - LastModifiedBy = RefToken.Client("client1"), - Data = data, - SchemaId = TestSchemas.DefaultId, - Status = Status.Draft, - StatusColor = "red", - NewStatus = Status.Published, - NewStatusColor = "blue" - }; + .AddInvariant( + new JsonObject() + .Add("value", 1))) + .AddField("my-json2", + new ContentFieldData() + .AddInvariant( + JsonValue.Object() + .Add("rootString", "Root String") + .Add("rootInt", 42) + .Add("rootFloat", 3.14) + .Add("rootBoolean", true) + .Add("rootArray", + JsonValue.Array() + .Add("1") + .Add("2") + .Add("3")) + .Add("rootObject", + JsonValue.Object() + .Add("nestedString", "Nested String") + .Add("nestedInt", 42) + .Add("nestedFloat", 3.14) + .Add("nestedBoolean", true) + .Add("nestedArray", + JsonValue.Array() + .Add("1") + .Add("2") + .Add("3"))))) + .AddField("my-array", + new ContentFieldData() + .AddInvariant(JsonValue.Array( + new JsonObject() + .Add("nested-number", 42) + .Add("nested-boolean", true), + new JsonObject() + .Add("nested-number", 3.14) + .Add("nested-boolean", false)))); - return content; + if (assetId != default || refId != default) + { + data.AddField("my-embeds", + new ContentFieldData() + .AddInvariant(JsonValue.Create($"assets:{assetId}, contents:{refId}"))); } - public static IEnrichedContentEntity CreateRef(NamedId<DomainId> schemaId, DomainId id, string field, string value) + var content = new ContentEntity { - var now = SystemClock.Instance.GetCurrentInstant(); + Id = id, + AppId = TestApp.DefaultId, + Version = 1, + Created = now, + CreatedBy = RefToken.User("user1"), + EditToken = $"token_{id}", + LastModified = now, + LastModifiedBy = RefToken.Client("client1"), + Data = data, + SchemaId = TestSchemas.DefaultId, + Status = Status.Draft, + StatusColor = "red", + NewStatus = Status.Published, + NewStatusColor = "blue" + }; - var data = - new ContentData() - .AddField(field, - new ContentFieldData() - .AddInvariant(value)); + return content; + } - var content = new ContentEntity - { - Id = id, - AppId = TestApp.DefaultId, - Version = 1, - Created = now, - CreatedBy = RefToken.User("user1"), - EditToken = $"token_{id}", - LastModified = now, - LastModifiedBy = RefToken.User("user2"), - Data = data, - SchemaId = schemaId, - Status = Status.Draft, - StatusColor = "red", - NewStatus = Status.Published, - NewStatusColor = "blue" - }; + public static IEnrichedContentEntity CreateRef(NamedId<DomainId> schemaId, DomainId id, string field, string value) + { + var now = SystemClock.Instance.GetCurrentInstant(); - return content; - } + var data = + new ContentData() + .AddField(field, + new ContentFieldData() + .AddInvariant(value)); - public static object Response(IEnrichedContentEntity content) + var content = new ContentEntity { - return new + Id = id, + AppId = TestApp.DefaultId, + Version = 1, + Created = now, + CreatedBy = RefToken.User("user1"), + EditToken = $"token_{id}", + LastModified = now, + LastModifiedBy = RefToken.User("user2"), + Data = data, + SchemaId = schemaId, + Status = Status.Draft, + StatusColor = "red", + NewStatus = Status.Published, + NewStatusColor = "blue" + }; + + return content; + } + + public static object Response(IEnrichedContentEntity content) + { + return new + { + id = content.Id, + version = 1, + created = content.Created, + createdBy = content.CreatedBy.ToString(), + createdByUser = new { - id = content.Id, - version = 1, - created = content.Created, - createdBy = content.CreatedBy.ToString(), - createdByUser = new - { - id = content.CreatedBy.Identifier, - email = $"{content.CreatedBy.Identifier}@email.com", - displayName = $"name_{content.CreatedBy.Identifier}" - }, - editToken = $"token_{content.Id}", - lastModified = content.LastModified, - lastModifiedBy = content.LastModifiedBy.ToString(), - lastModifiedByUser = new - { - id = content.LastModifiedBy.Identifier, - email = $"{content.LastModifiedBy}", - displayName = content.LastModifiedBy.Identifier - }, - status = "DRAFT", - statusColor = "red", - newStatus = "PUBLISHED", - newStatusColor = "blue", - url = $"contents/my-schema/{content.Id}", - data = Data(content) - }; - } + id = content.CreatedBy.Identifier, + email = $"{content.CreatedBy.Identifier}@email.com", + displayName = $"name_{content.CreatedBy.Identifier}" + }, + editToken = $"token_{content.Id}", + lastModified = content.LastModified, + lastModifiedBy = content.LastModifiedBy.ToString(), + lastModifiedByUser = new + { + id = content.LastModifiedBy.Identifier, + email = $"{content.LastModifiedBy}", + displayName = content.LastModifiedBy.Identifier + }, + status = "DRAFT", + statusColor = "red", + newStatus = "PUBLISHED", + newStatusColor = "blue", + url = $"contents/my-schema/{content.Id}", + data = Data(content) + }; + } - public static object FlatResponse(IEnrichedContentEntity content) + public static object FlatResponse(IEnrichedContentEntity content) + { + return new { - return new + id = content.Id, + version = 1, + created = content.Created, + createdBy = content.CreatedBy.ToString(), + createdByUser = new { - id = content.Id, - version = 1, - created = content.Created, - createdBy = content.CreatedBy.ToString(), - createdByUser = new - { - id = content.CreatedBy.Identifier, - email = $"{content.CreatedBy.Identifier}@email.com", - displayName = $"name_{content.CreatedBy.Identifier}" - }, - editToken = $"token_{content.Id}", - lastModified = content.LastModified, - lastModifiedBy = content.LastModifiedBy.ToString(), - lastModifiedByUser = new - { - id = content.LastModifiedBy.Identifier, - email = $"{content.LastModifiedBy}", - displayName = content.LastModifiedBy.Identifier - }, - status = "DRAFT", - statusColor = "red", - newStatus = "PUBLISHED", - newStatusColor = "blue", - url = $"contents/my-schema/{content.Id}", - flatData = FlatData(content) - }; - } + id = content.CreatedBy.Identifier, + email = $"{content.CreatedBy.Identifier}@email.com", + displayName = $"name_{content.CreatedBy.Identifier}" + }, + editToken = $"token_{content.Id}", + lastModified = content.LastModified, + lastModifiedBy = content.LastModifiedBy.ToString(), + lastModifiedByUser = new + { + id = content.LastModifiedBy.Identifier, + email = $"{content.LastModifiedBy}", + displayName = content.LastModifiedBy.Identifier + }, + status = "DRAFT", + statusColor = "red", + newStatus = "PUBLISHED", + newStatusColor = "blue", + url = $"contents/my-schema/{content.Id}", + flatData = FlatData(content) + }; + } - public static object Input(IContentEntity content, DomainId refId = default, DomainId assetId = default) + public static object Input(IContentEntity content, DomainId refId = default, DomainId assetId = default) + { + var actual = new Dictionary<string, object> { - var actual = new Dictionary<string, object> + ["myJson"] = new { - ["myJson"] = new + iv = new { - iv = new - { - value = 1 - } - }, - ["myJson2"] = new + value = 1 + } + }, + ["myJson2"] = new + { + iv = new Dictionary<string, object> { - iv = new Dictionary<string, object> + ["rootString"] = "Root String", + ["rootInt"] = 42, + ["rootFloat"] = 3.14, + ["rootBoolean"] = true, + ["rootArray"] = new[] { "1", "2", "3" }, + ["rootObject"] = new Dictionary<string, object> { - ["rootString"] = "Root String", - ["rootInt"] = 42, - ["rootFloat"] = 3.14, - ["rootBoolean"] = true, - ["rootArray"] = new[] { "1", "2", "3" }, - ["rootObject"] = new Dictionary<string, object> - { - ["nestedString"] = "Nested String", - ["nestedInt"] = 42, - ["nestedFloat"] = 3.14, - ["nestedBoolean"] = true, - ["nestedArray"] = new[] { "1", "2", "3" }, - } + ["nestedString"] = "Nested String", + ["nestedInt"] = 42, + ["nestedFloat"] = 3.14, + ["nestedBoolean"] = true, + ["nestedArray"] = new[] { "1", "2", "3" }, } - }, - ["myString"] = new - { - iv = (string?)null - }, - ["myStringEnum"] = new - { - iv = "EnumA" - }, - ["myLocalizedString"] = new - { - de_DE = "de-DE" - }, - ["myNumber"] = new - { - iv = 1.0 - }, - ["myBoolean"] = new - { - iv = true - }, - ["myDatetime"] = new + } + }, + ["myString"] = new + { + iv = (string?)null + }, + ["myStringEnum"] = new + { + iv = "EnumA" + }, + ["myLocalizedString"] = new + { + de_DE = "de-DE" + }, + ["myNumber"] = new + { + iv = 1.0 + }, + ["myBoolean"] = new + { + iv = true + }, + ["myDatetime"] = new + { + iv = content.LastModified.ToString() + }, + ["myGeolocation"] = new + { + iv = new { - iv = content.LastModified.ToString() - }, - ["myGeolocation"] = new + latitude = 10, + longitude = 20 + } + }, + ["myComponent"] = new + { + iv = new Dictionary<string, object> { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - ["myComponent"] = new + ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaRef1Field"] = "Component1" + } + }, + ["myComponents"] = new + { + iv = new[] { - iv = new Dictionary<string, object> + new Dictionary<string, object> { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), ["schemaRef1Field"] = "Component1" - } - }, - ["myComponents"] = new - { - iv = new[] - { - new Dictionary<string, object> - { - ["schemaId"] = TestSchemas.Ref1.Id.ToString(), - ["schemaRef1Field"] = "Component1" - }, - new Dictionary<string, object> - { - ["schemaId"] = TestSchemas.Ref2.Id.ToString(), - ["schemaRef2Field"] = "Component2" - } - } - }, - ["myTags"] = new - { - iv = new[] + }, + new Dictionary<string, object> { - "tag1", - "tag2" + ["schemaId"] = TestSchemas.Ref2.Id.ToString(), + ["schemaRef2Field"] = "Component2" } - }, - ["myTagsEnum"] = new + } + }, + ["myTags"] = new + { + iv = new[] { - iv = new[] - { - "EnumA", - "EnumB" - } - }, - ["myArray"] = new + "tag1", + "tag2" + } + }, + ["myTagsEnum"] = new + { + iv = new[] { - iv = new[] - { - new - { - nestedNumber = 42.0, - nestedBoolean = true - }, - new - { - nestedNumber = 3.14, - nestedBoolean = false - } - } + "EnumA", + "EnumB" } - }; - - if (refId != default) + }, + ["myArray"] = new { - actual["myReferences"] = new + iv = new[] { - iv = new[] + new { - refId - } - }; - - actual["myUnion"] = new - { - iv = new[] + nestedNumber = 42.0, + nestedBoolean = true + }, + new { - refId + nestedNumber = 3.14, + nestedBoolean = false } - }; + } } + }; - if (assetId != default) + if (refId != default) + { + actual["myReferences"] = new { - actual["myAssets"] = new + iv = new[] { - iv = new[] - { - assetId - } - }; - } + refId + } + }; - if (assetId != default || refId != default) + actual["myUnion"] = new { - actual["myEmbeds"] = new + iv = new[] { - iv = $"assets:{assetId}, contents:{refId}" - }; - } - - return actual; + refId + } + }; } - private static object Data(IContentEntity content) + if (assetId != default) { - var actual = new Dictionary<string, object> + actual["myAssets"] = new { - ["myJson"] = new - { - iv = new - { - value = 1 - }, - ivValue = 1 - }, - ["myJson2"] = new - { - iv = new Dictionary<string, object> - { - ["__typename"] = "JsonObject2", - ["rootString"] = "Root String", - ["rootInt"] = 42, - ["rootFloat"] = 3.14, - ["rootBoolean"] = true, - ["rootArray"] = new[] { "1", "2", "3" }, - ["rootObject"] = new Dictionary<string, object> - { - ["__typename"] = "JsonNested", - ["nestedString"] = "Nested String", - ["nestedInt"] = 42, - ["nestedFloat"] = 3.14, - ["nestedBoolean"] = true, - ["nestedArray"] = new[] { "1", "2", "3" }, - } - } - }, - ["myString"] = new - { - iv = (string?)null - }, - ["myStringEnum"] = new - { - iv = "EnumA" - }, - ["myLocalizedString"] = new - { - de_DE = "de-DE" - }, - ["myNumber"] = new + iv = new[] { - iv = 1.0 - }, - ["myBoolean"] = new - { - iv = true - }, - ["myDatetime"] = new - { - iv = content.LastModified.ToString() - }, - ["myGeolocation"] = new - { - iv = new - { - latitude = 10, - longitude = 20 - } - }, - ["myComponent__Dynamic"] = new - { - iv = new Dictionary<string, object> - { - ["schemaId"] = TestSchemas.Ref1.Id.ToString(), - ["schemaRef1Field"] = "Component1" - } - }, - ["myComponent"] = new - { - iv = new Dictionary<string, object> - { - ["schemaId"] = TestSchemas.Ref1.Id.ToString(), - ["schemaRef1Field"] = "Component1" - } - }, - ["myComponents__Dynamic"] = new - { - iv = new[] - { - new Dictionary<string, object> - { - ["schemaId"] = TestSchemas.Ref1.Id.ToString(), - ["schemaRef1Field"] = "Component1" - }, - new Dictionary<string, object> - { - ["schemaId"] = TestSchemas.Ref2.Id.ToString(), - ["schemaRef2Field"] = "Component2" - } - } - }, - ["myComponents"] = new - { - iv = new object[] - { - new Dictionary<string, object> - { - ["__typename"] = "MyRefSchema1Component", - ["schemaId"] = TestSchemas.Ref1.Id.ToString(), - ["schemaRef1Field"] = "Component1" - }, - new Dictionary<string, object> - { - ["__typename"] = "MyRefSchema2Component", - ["schemaId"] = TestSchemas.Ref2.Id.ToString(), - ["schemaRef2Field"] = "Component2" - } - } - }, - ["myTags"] = new - { - iv = new[] - { - "tag1", - "tag2" - } - }, - ["myTagsEnum"] = new - { - iv = new[] - { - "EnumA", - "EnumB" - } - }, - ["myArray"] = new - { - iv = new[] - { - new - { - nestedNumber = 42.0, - nestedBoolean = true - }, - new - { - nestedNumber = 3.14, - nestedBoolean = false - } - } + assetId } }; + } - return actual; + if (assetId != default || refId != default) + { + actual["myEmbeds"] = new + { + iv = $"assets:{assetId}, contents:{refId}" + }; } - private static object FlatData(IContentEntity content) + return actual; + } + + private static object Data(IContentEntity content) + { + var actual = new Dictionary<string, object> { - var actual = new Dictionary<string, object?> + ["myJson"] = new { - ["myJson"] = new + iv = new { value = 1 }, - ["myJsonValue"] = 1, - ["myJson2"] = new Dictionary<string, object> + ivValue = 1 + }, + ["myJson2"] = new + { + iv = new Dictionary<string, object> { ["__typename"] = "JsonObject2", ["rootString"] = "Root String", @@ -756,29 +609,59 @@ private static object FlatData(IContentEntity content) ["nestedBoolean"] = true, ["nestedArray"] = new[] { "1", "2", "3" }, } - }, - ["myString"] = null, - ["myStringEnum"] = "EnumA", - ["myLocalizedString"] = "de-DE", - ["myNumber"] = 1.0, - ["myBoolean"] = true, - ["myDatetime"] = content.LastModified.ToString(), - ["myGeolocation"] = new + } + }, + ["myString"] = new + { + iv = (string?)null + }, + ["myStringEnum"] = new + { + iv = "EnumA" + }, + ["myLocalizedString"] = new + { + de_DE = "de-DE" + }, + ["myNumber"] = new + { + iv = 1.0 + }, + ["myBoolean"] = new + { + iv = true + }, + ["myDatetime"] = new + { + iv = content.LastModified.ToString() + }, + ["myGeolocation"] = new + { + iv = new { latitude = 10, longitude = 20 - }, - ["myComponent__Dynamic"] = new Dictionary<string, object> + } + }, + ["myComponent__Dynamic"] = new + { + iv = new Dictionary<string, object> { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), ["schemaRef1Field"] = "Component1" - }, - ["myComponent"] = new Dictionary<string, object> + } + }, + ["myComponent"] = new + { + iv = new Dictionary<string, object> { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), ["schemaRef1Field"] = "Component1" - }, - ["myComponents__Dynamic"] = new[] + } + }, + ["myComponents__Dynamic"] = new + { + iv = new[] { new Dictionary<string, object> { @@ -790,8 +673,11 @@ private static object FlatData(IContentEntity content) ["schemaId"] = TestSchemas.Ref2.Id.ToString(), ["schemaRef2Field"] = "Component2" } - }, - ["myComponents"] = new object[] + } + }, + ["myComponents"] = new + { + iv = new object[] { new Dictionary<string, object> { @@ -805,18 +691,27 @@ private static object FlatData(IContentEntity content) ["schemaId"] = TestSchemas.Ref2.Id.ToString(), ["schemaRef2Field"] = "Component2" } - }, - ["myTags"] = new[] + } + }, + ["myTags"] = new + { + iv = new[] { "tag1", "tag2" - }, - ["myTagsEnum"] = new[] + } + }, + ["myTagsEnum"] = new + { + iv = new[] { "EnumA", "EnumB" - }, - ["myArray"] = new[] + } + }, + ["myArray"] = new + { + iv = new[] { new { @@ -829,9 +724,113 @@ private static object FlatData(IContentEntity content) nestedBoolean = false } } - }; + } + }; - return actual; - } + return actual; + } + + private static object FlatData(IContentEntity content) + { + var actual = new Dictionary<string, object?> + { + ["myJson"] = new + { + value = 1 + }, + ["myJsonValue"] = 1, + ["myJson2"] = new Dictionary<string, object> + { + ["__typename"] = "JsonObject2", + ["rootString"] = "Root String", + ["rootInt"] = 42, + ["rootFloat"] = 3.14, + ["rootBoolean"] = true, + ["rootArray"] = new[] { "1", "2", "3" }, + ["rootObject"] = new Dictionary<string, object> + { + ["__typename"] = "JsonNested", + ["nestedString"] = "Nested String", + ["nestedInt"] = 42, + ["nestedFloat"] = 3.14, + ["nestedBoolean"] = true, + ["nestedArray"] = new[] { "1", "2", "3" }, + } + }, + ["myString"] = null, + ["myStringEnum"] = "EnumA", + ["myLocalizedString"] = "de-DE", + ["myNumber"] = 1.0, + ["myBoolean"] = true, + ["myDatetime"] = content.LastModified.ToString(), + ["myGeolocation"] = new + { + latitude = 10, + longitude = 20 + }, + ["myComponent__Dynamic"] = new Dictionary<string, object> + { + ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaRef1Field"] = "Component1" + }, + ["myComponent"] = new Dictionary<string, object> + { + ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaRef1Field"] = "Component1" + }, + ["myComponents__Dynamic"] = new[] + { + new Dictionary<string, object> + { + ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaRef1Field"] = "Component1" + }, + new Dictionary<string, object> + { + ["schemaId"] = TestSchemas.Ref2.Id.ToString(), + ["schemaRef2Field"] = "Component2" + } + }, + ["myComponents"] = new object[] + { + new Dictionary<string, object> + { + ["__typename"] = "MyRefSchema1Component", + ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaRef1Field"] = "Component1" + }, + new Dictionary<string, object> + { + ["__typename"] = "MyRefSchema2Component", + ["schemaId"] = TestSchemas.Ref2.Id.ToString(), + ["schemaRef2Field"] = "Component2" + } + }, + ["myTags"] = new[] + { + "tag1", + "tag2" + }, + ["myTagsEnum"] = new[] + { + "EnumA", + "EnumB" + }, + ["myArray"] = new[] + { + new + { + nestedNumber = 42.0, + nestedBoolean = true + }, + new + { + nestedNumber = 3.14, + nestedBoolean = false + } + } + }; + + return actual; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs index 077f93a706..f8340ef340 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs @@ -12,33 +12,33 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; + +public static class TestSchemas { - public static class TestSchemas - { - public static readonly NamedId<DomainId> DefaultId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - public static readonly NamedId<DomainId> Ref1Id = NamedId.Of(DomainId.NewGuid(), "my-ref-schema1"); - public static readonly NamedId<DomainId> Ref2Id = NamedId.Of(DomainId.NewGuid(), "my-ref-schema2"); + public static readonly NamedId<DomainId> DefaultId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + public static readonly NamedId<DomainId> Ref1Id = NamedId.Of(DomainId.NewGuid(), "my-ref-schema1"); + public static readonly NamedId<DomainId> Ref2Id = NamedId.Of(DomainId.NewGuid(), "my-ref-schema2"); - public static readonly ISchemaEntity Default; - public static readonly ISchemaEntity Ref1; - public static readonly ISchemaEntity Ref2; + public static readonly ISchemaEntity Default; + public static readonly ISchemaEntity Ref1; + public static readonly ISchemaEntity Ref2; - static TestSchemas() - { - Ref1 = Mocks.Schema(TestApp.DefaultId, Ref1Id, - new Schema(Ref1Id.Name) - .Publish() - .AddString(1, "schemaRef1Field", Partitioning.Invariant)); + static TestSchemas() + { + Ref1 = Mocks.Schema(TestApp.DefaultId, Ref1Id, + new Schema(Ref1Id.Name) + .Publish() + .AddString(1, "schemaRef1Field", Partitioning.Invariant)); - Ref2 = Mocks.Schema(TestApp.DefaultId, Ref2Id, - new Schema(Ref2Id.Name) - .Publish() - .AddString(1, "schemaRef2Field", Partitioning.Invariant)); + Ref2 = Mocks.Schema(TestApp.DefaultId, Ref2Id, + new Schema(Ref2Id.Name) + .Publish() + .AddString(1, "schemaRef2Field", Partitioning.Invariant)); - var enums = ReadonlyList.Create("EnumA", "EnumB", "EnumC"); + var enums = ReadonlyList.Create("EnumA", "EnumB", "EnumC"); - var jsonSchema = @" + var jsonSchema = @" type JsonObject { rootString: String rootInt: Int @@ -56,49 +56,48 @@ type JsonNested { nestedArray: [String] }"; - Default = Mocks.Schema(TestApp.DefaultId, DefaultId, - new Schema(DefaultId.Name) - .Publish() - .AddJson(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties()) - .AddJson(2, "my-json2", Partitioning.Invariant, - new JsonFieldProperties { GraphQLSchema = jsonSchema }) - .AddString(3, "my-string", Partitioning.Invariant, - new StringFieldProperties()) - .AddString(4, "my-string-enum", Partitioning.Invariant, - new StringFieldProperties { AllowedValues = enums, CreateEnum = true }) - .AddString(5, "my-localized-string", Partitioning.Language, - new StringFieldProperties()) - .AddNumber(6, "my-number", Partitioning.Invariant, - new NumberFieldProperties()) - .AddAssets(7, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddBoolean(8, "my-boolean", Partitioning.Invariant, + Default = Mocks.Schema(TestApp.DefaultId, DefaultId, + new Schema(DefaultId.Name) + .Publish() + .AddJson(1, "my-json", Partitioning.Invariant, + new JsonFieldProperties()) + .AddJson(2, "my-json2", Partitioning.Invariant, + new JsonFieldProperties { GraphQLSchema = jsonSchema }) + .AddString(3, "my-string", Partitioning.Invariant, + new StringFieldProperties()) + .AddString(4, "my-string-enum", Partitioning.Invariant, + new StringFieldProperties { AllowedValues = enums, CreateEnum = true }) + .AddString(5, "my-localized-string", Partitioning.Language, + new StringFieldProperties()) + .AddNumber(6, "my-number", Partitioning.Invariant, + new NumberFieldProperties()) + .AddAssets(7, "my-assets", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddBoolean(8, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddDateTime(9, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties()) + .AddReferences(10, "my-references", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = Ref1Id.Id }) + .AddReferences(11, "my-union", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddGeolocation(12, "my-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties()) + .AddComponent(13, "my-component", Partitioning.Invariant, + new ComponentFieldProperties { SchemaId = Ref1Id.Id }) + .AddComponents(14, "my-components", Partitioning.Invariant, + new ComponentsFieldProperties { SchemaIds = ReadonlyList.Create(Ref1.Id, Ref2.Id) }) + .AddTags(15, "my-tags", Partitioning.Invariant, + new TagsFieldProperties()) + .AddTags(16, "my-tags-enum", Partitioning.Invariant, + new TagsFieldProperties { AllowedValues = enums, CreateEnum = true }) + .AddArray(100, "my-array", Partitioning.Invariant, f => f + .AddBoolean(121, "nested-boolean", new BooleanFieldProperties()) - .AddDateTime(9, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties()) - .AddReferences(10, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Ref1Id.Id }) - .AddReferences(11, "my-union", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddGeolocation(12, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties()) - .AddComponent(13, "my-component", Partitioning.Invariant, - new ComponentFieldProperties { SchemaId = Ref1Id.Id }) - .AddComponents(14, "my-components", Partitioning.Invariant, - new ComponentsFieldProperties { SchemaIds = ReadonlyList.Create(Ref1.Id, Ref2.Id) }) - .AddTags(15, "my-tags", Partitioning.Invariant, - new TagsFieldProperties()) - .AddTags(16, "my-tags-enum", Partitioning.Invariant, - new TagsFieldProperties { AllowedValues = enums, CreateEnum = true }) - .AddArray(100, "my-array", Partitioning.Invariant, f => f - .AddBoolean(121, "nested-boolean", - new BooleanFieldProperties()) - .AddNumber(122, "nested-number", - new NumberFieldProperties())) - .AddString(17, "my-embeds", Partitioning.Invariant, - new StringFieldProperties { IsEmbeddable = true, SchemaIds = ReadonlyList.Create(Ref1.Id, Ref2.Id) }) - .SetScripts(new SchemaScripts { Query = "<query-script>" })); - } + .AddNumber(122, "nested-number", + new NumberFieldProperties())) + .AddString(17, "my-embeds", Partitioning.Invariant, + new StringFieldProperties { IsEmbeddable = true, SchemaIds = ReadonlyList.Create(Ref1.Id, Ref2.Id) }) + .SetScripts(new SchemaScripts { Query = "<query-script>" })); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs index 7160583baf..a5a02d0dd3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs @@ -15,145 +15,144 @@ using Squidex.Infrastructure.States; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; + +public class ContentMappingTests { - public class ContentMappingTests + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + + [Fact] + public async Task Should_map_content_without_new_version_to_draft() { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + var source = CreateContentWithoutNewVersion(); - [Fact] - public async Task Should_map_content_without_new_version_to_draft() - { - var source = CreateContentWithoutNewVersion(); + var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version); + var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, appProvider); - var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version); - var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, appProvider); + Assert.Equal(source.CurrentVersion.Data, snapshot.Data); + Assert.Null(snapshot.DraftData); + Assert.Null(snapshot.NewStatus); + Assert.NotNull(snapshot.ScheduleJob); + Assert.True(snapshot.IsSnapshot); - Assert.Equal(source.CurrentVersion.Data, snapshot.Data); - Assert.Null(snapshot.DraftData); - Assert.Null(snapshot.NewStatus); - Assert.NotNull(snapshot.ScheduleJob); - Assert.True(snapshot.IsSnapshot); + var mapped = snapshot.ToState(); - var mapped = snapshot.ToState(); + mapped.Should().BeEquivalentTo(source); + } - mapped.Should().BeEquivalentTo(source); - } + [Fact] + public async Task Should_map_content_without_new_version_to_published() + { + var source = CreateContentWithoutNewVersion(); - [Fact] - public async Task Should_map_content_without_new_version_to_published() - { - var source = CreateContentWithoutNewVersion(); + var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version); + var snapshot = await MongoContentEntity.CreatePublishedAsync(snapshotJob, appProvider); - var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version); - var snapshot = await MongoContentEntity.CreatePublishedAsync(snapshotJob, appProvider); + Assert.Equal(source.CurrentVersion.Data, snapshot.Data); + Assert.Null(snapshot.DraftData); + Assert.Null(snapshot.NewStatus); + Assert.Null(snapshot.ScheduleJob); + Assert.False(snapshot.IsSnapshot); + } - Assert.Equal(source.CurrentVersion.Data, snapshot.Data); - Assert.Null(snapshot.DraftData); - Assert.Null(snapshot.NewStatus); - Assert.Null(snapshot.ScheduleJob); - Assert.False(snapshot.IsSnapshot); - } + [Fact] + public async Task Should_map_content_with_new_version_to_draft() + { + var source = CreateContentWithNewVersion(); - [Fact] - public async Task Should_map_content_with_new_version_to_draft() - { - var source = CreateContentWithNewVersion(); + var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version); + var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, appProvider); - var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version); - var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, appProvider); + Assert.Equal(source.NewVersion?.Data, snapshot.Data); + Assert.Equal(source.CurrentVersion.Data, snapshot.DraftData); + Assert.NotNull(snapshot.NewStatus); + Assert.NotNull(snapshot.ScheduleJob); + Assert.True(snapshot.IsSnapshot); - Assert.Equal(source.NewVersion?.Data, snapshot.Data); - Assert.Equal(source.CurrentVersion.Data, snapshot.DraftData); - Assert.NotNull(snapshot.NewStatus); - Assert.NotNull(snapshot.ScheduleJob); - Assert.True(snapshot.IsSnapshot); + var mapped = snapshot.ToState(); - var mapped = snapshot.ToState(); + mapped.Should().BeEquivalentTo(source); + } - mapped.Should().BeEquivalentTo(source); - } + [Fact] + public async Task Should_map_content_with_new_version_to_published() + { + var source = CreateContentWithNewVersion(); - [Fact] - public async Task Should_map_content_with_new_version_to_published() - { - var source = CreateContentWithNewVersion(); + var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version); + var snapshot = await MongoContentEntity.CreatePublishedAsync(snapshotJob, appProvider); - var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version); - var snapshot = await MongoContentEntity.CreatePublishedAsync(snapshotJob, appProvider); + Assert.Equal(source.CurrentVersion?.Data, snapshot.Data); + Assert.Null(snapshot.DraftData); + Assert.Null(snapshot.NewStatus); + Assert.Null(snapshot.ScheduleJob); + Assert.False(snapshot.IsSnapshot); + } + + private static ContentDomainObject.State CreateContentWithoutNewVersion() + { + var user = RefToken.User("1"); - Assert.Equal(source.CurrentVersion?.Data, snapshot.Data); - Assert.Null(snapshot.DraftData); - Assert.Null(snapshot.NewStatus); - Assert.Null(snapshot.ScheduleJob); - Assert.False(snapshot.IsSnapshot); - } + var data = + new ContentData() + .AddField("my-field", + new ContentFieldData() + .AddInvariant(42)); - private static ContentDomainObject.State CreateContentWithoutNewVersion() + var time = SystemClock.Instance.GetCurrentInstant(); + + var state = new ContentDomainObject.State { - var user = RefToken.User("1"); - - var data = - new ContentData() - .AddField("my-field", - new ContentFieldData() - .AddInvariant(42)); - - var time = SystemClock.Instance.GetCurrentInstant(); - - var state = new ContentDomainObject.State - { - Id = DomainId.NewGuid(), - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), - Created = time, - CreatedBy = user, - CurrentVersion = new ContentVersion(Status.Archived, data), - IsDeleted = true, - LastModified = time, - LastModifiedBy = user, - ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Published, user, time), - SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"), - Version = 42 - }; - - return state; - } - - private static ContentDomainObject.State CreateContentWithNewVersion() + Id = DomainId.NewGuid(), + AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + Created = time, + CreatedBy = user, + CurrentVersion = new ContentVersion(Status.Archived, data), + IsDeleted = true, + LastModified = time, + LastModifiedBy = user, + ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Published, user, time), + SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"), + Version = 42 + }; + + return state; + } + + private static ContentDomainObject.State CreateContentWithNewVersion() + { + var user = RefToken.User("1"); + + var data = + new ContentData() + .AddField("my-field", + new ContentFieldData() + .AddInvariant(42)); + + var newData = + new ContentData() + .AddField("my-field", + new ContentFieldData() + .AddInvariant(13)); + + var time = SystemClock.Instance.GetCurrentInstant(); + + var state = new ContentDomainObject.State { - var user = RefToken.User("1"); - - var data = - new ContentData() - .AddField("my-field", - new ContentFieldData() - .AddInvariant(42)); - - var newData = - new ContentData() - .AddField("my-field", - new ContentFieldData() - .AddInvariant(13)); - - var time = SystemClock.Instance.GetCurrentInstant(); - - var state = new ContentDomainObject.State - { - Id = DomainId.NewGuid(), - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), - Created = time, - CreatedBy = user, - CurrentVersion = new ContentVersion(Status.Archived, data), - IsDeleted = true, - LastModified = time, - LastModifiedBy = user, - NewVersion = new ContentVersion(Status.Published, newData), - ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Published, user, time), - SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"), - Version = 42 - }; - - return state; - } + Id = DomainId.NewGuid(), + AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + Created = time, + CreatedBy = user, + CurrentVersion = new ContentVersion(Status.Archived, data), + IsDeleted = true, + LastModified = time, + LastModifiedBy = user, + NewVersion = new ContentVersion(Status.Published, newData), + ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Published, user, time), + SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"), + Version = 42 + }; + + return state; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs index e31b51ad5c..47948577b4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs @@ -24,257 +24,256 @@ using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; + +public class ContentQueryTests { - public class ContentQueryTests + private readonly DomainId appId = DomainId.NewGuid(); + private readonly Schema schemaDef; + private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); + + static ContentQueryTests() + { + TestUtils.SetupBson(); + } + + public ContentQueryTests() + { + schemaDef = + new Schema("user") + .AddString(1, "firstName", Partitioning.Language, + new StringFieldProperties()) + .AddString(2, "lastName", Partitioning.Language, + new StringFieldProperties()) + .AddBoolean(3, "isAdmin", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddNumber(4, "age", Partitioning.Invariant, + new NumberFieldProperties()) + .AddDateTime(5, "birthday", Partitioning.Invariant, + new DateTimeFieldProperties()) + .AddAssets(6, "pictures", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddReferences(7, "friends", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddString(8, "dashed-field", Partitioning.Invariant, + new StringFieldProperties()) + .AddJson(9, "json", Partitioning.Invariant, + new JsonFieldProperties()) + .AddArray(10, "hobbies", Partitioning.Invariant, a => a + .AddString(101, "name")) + .Update(new SchemaProperties()); + + var schema = A.Dummy<ISchemaEntity>(); + A.CallTo(() => schema.Id).Returns(DomainId.NewGuid()); + A.CallTo(() => schema.Version).Returns(3); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + + var app = A.Dummy<IAppEntity>(); + A.CallTo(() => app.Id).Returns(DomainId.NewGuid()); + A.CallTo(() => app.Version).Returns(3); + A.CallTo(() => app.Languages).Returns(languages); + } + + [Fact] + public void Should_make_query_with_id() + { + var id = Guid.NewGuid(); + + var filter = ClrFilter.Eq("id", id); + + AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); + } + + [Fact] + public void Should_make_query_with_id_string() + { + var id = DomainId.NewGuid().ToString(); + + var filter = ClrFilter.Eq("id", id); + + AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); + } + + [Fact] + public void Should_make_query_with_id_list() { - private readonly DomainId appId = DomainId.NewGuid(); - private readonly Schema schemaDef; - private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); - - static ContentQueryTests() - { - TestUtils.SetupBson(); - } - - public ContentQueryTests() - { - schemaDef = - new Schema("user") - .AddString(1, "firstName", Partitioning.Language, - new StringFieldProperties()) - .AddString(2, "lastName", Partitioning.Language, - new StringFieldProperties()) - .AddBoolean(3, "isAdmin", Partitioning.Invariant, - new BooleanFieldProperties()) - .AddNumber(4, "age", Partitioning.Invariant, - new NumberFieldProperties()) - .AddDateTime(5, "birthday", Partitioning.Invariant, - new DateTimeFieldProperties()) - .AddAssets(6, "pictures", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddReferences(7, "friends", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddString(8, "dashed-field", Partitioning.Invariant, - new StringFieldProperties()) - .AddJson(9, "json", Partitioning.Invariant, - new JsonFieldProperties()) - .AddArray(10, "hobbies", Partitioning.Invariant, a => a - .AddString(101, "name")) - .Update(new SchemaProperties()); - - var schema = A.Dummy<ISchemaEntity>(); - A.CallTo(() => schema.Id).Returns(DomainId.NewGuid()); - A.CallTo(() => schema.Version).Returns(3); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - - var app = A.Dummy<IAppEntity>(); - A.CallTo(() => app.Id).Returns(DomainId.NewGuid()); - A.CallTo(() => app.Version).Returns(3); - A.CallTo(() => app.Languages).Returns(languages); - } - - [Fact] - public void Should_make_query_with_id() - { - var id = Guid.NewGuid(); - - var filter = ClrFilter.Eq("id", id); - - AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); - } - - [Fact] - public void Should_make_query_with_id_string() - { - var id = DomainId.NewGuid().ToString(); - - var filter = ClrFilter.Eq("id", id); - - AssertQuery($"{{ '_id' : '{appId}--{id}' }}", filter); - } - - [Fact] - public void Should_make_query_with_id_list() - { - var id = Guid.NewGuid(); - - var filter = ClrFilter.In("id", new List<Guid> { id }); - - AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); - } - - [Fact] - public void Should_make_query_with_id_string_list() - { - var id = DomainId.NewGuid().ToString(); - - var filter = ClrFilter.In("id", new List<string> { id }); - - AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); - } - - [Fact] - public void Should_make_query_with_lastModified() - { - var time = "1988-01-19T12:00:00Z"; - - var filter = ClrFilter.Eq("lastModified", InstantPattern.ExtendedIso.Parse(time).Value); - - AssertQuery("{ 'mt' : ISODate('[value]') }", filter, time); - } - - [Fact] - public void Should_make_query_with_lastModifiedBy() - { - var filter = ClrFilter.Eq("lastModifiedBy", "me"); - - AssertQuery("{ 'mb' : 'me' }", filter); - } - - [Fact] - public void Should_make_query_with_created() - { - var time = "1988-01-19T12:00:00Z"; - - var filter = ClrFilter.Eq("created", InstantPattern.ExtendedIso.Parse(time).Value); - - AssertQuery("{ 'ct' : ISODate('[value]') }", filter, time); - } - - [Fact] - public void Should_make_query_with_createdBy() - { - var filter = ClrFilter.Eq("createdBy", "subject:me"); - - AssertQuery("{ 'cb' : 'subject:me' }", filter); - } - - [Fact] - public void Should_make_query_with_version() - { - var filter = ClrFilter.Eq("version", 2L); + var id = Guid.NewGuid(); + + var filter = ClrFilter.In("id", new List<Guid> { id }); + + AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); + } - AssertQuery("{ 'vs' : NumberLong(2) }", filter); - } + [Fact] + public void Should_make_query_with_id_string_list() + { + var id = DomainId.NewGuid().ToString(); - [Fact] - public void Should_make_query_with_datetime_data() - { - var time = "1988-01-19T12:00:00Z"; + var filter = ClrFilter.In("id", new List<string> { id }); - var filter = ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse(time).Value); + AssertQuery($"{{ '_id' : {{ '$in' : ['{appId}--{id}'] }} }}", filter); + } - AssertQuery("{ 'do.birthday.iv' : '[value]' }", filter, time); - } + [Fact] + public void Should_make_query_with_lastModified() + { + var time = "1988-01-19T12:00:00Z"; - [Fact] - public void Should_make_query_with_underscore_field() - { - var filter = ClrFilter.Eq("data/dashed_field/iv", "Value"); + var filter = ClrFilter.Eq("lastModified", InstantPattern.ExtendedIso.Parse(time).Value); - AssertQuery("{ 'do.dashed-field.iv' : 'Value' }", filter); - } + AssertQuery("{ 'mt' : ISODate('[value]') }", filter, time); + } - [Fact] - public void Should_make_query_with_json_dot_field() - { - var filter = ClrFilter.Eq("data/json/iv/with\\.dot", "Value"); + [Fact] + public void Should_make_query_with_lastModifiedBy() + { + var filter = ClrFilter.Eq("lastModifiedBy", "me"); - AssertQuery("{ 'do.json.iv.with_§§_dot' : 'Value' }", filter); - } + AssertQuery("{ 'mb' : 'me' }", filter); + } - [Fact] - public void Should_make_query_with_json_slash_field() - { - var filter = ClrFilter.Eq("data/json/iv/with\\/slash", "Value"); + [Fact] + public void Should_make_query_with_created() + { + var time = "1988-01-19T12:00:00Z"; - AssertQuery("{ 'do.json.iv.with/slash' : 'Value' }", filter); - } + var filter = ClrFilter.Eq("created", InstantPattern.ExtendedIso.Parse(time).Value); - [Fact] - public void Should_make_query_with_references_equals() - { - var filter = ClrFilter.Eq("data/friends/iv", "guid"); + AssertQuery("{ 'ct' : ISODate('[value]') }", filter, time); + } - AssertQuery("{ 'do.friends.iv' : 'guid' }", filter); - } + [Fact] + public void Should_make_query_with_createdBy() + { + var filter = ClrFilter.Eq("createdBy", "subject:me"); - [Fact] - public void Should_make_query_with_array_field() - { - var filter = ClrFilter.Eq("data/hobbies/iv/name", "PC"); + AssertQuery("{ 'cb' : 'subject:me' }", filter); + } - AssertQuery("{ 'do.hobbies.iv.name' : 'PC' }", filter); - } + [Fact] + public void Should_make_query_with_version() + { + var filter = ClrFilter.Eq("version", 2L); - [Fact] - public void Should_make_query_with_assets_equals() - { - var filter = ClrFilter.Eq("data/pictures/iv", "guid"); + AssertQuery("{ 'vs' : NumberLong(2) }", filter); + } - AssertQuery("{ 'do.pictures.iv' : 'guid' }", filter); - } + [Fact] + public void Should_make_query_with_datetime_data() + { + var time = "1988-01-19T12:00:00Z"; - [Fact] - public void Should_make_orderby_with_single_field() - { - var sorting = SortBuilder.Descending("data/age/iv"); + var filter = ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse(time).Value); - AssertSorting("{ 'do.age.iv' : -1 }", sorting); - } + AssertQuery("{ 'do.birthday.iv' : '[value]' }", filter, time); + } - [Fact] - public void Should_make_orderby_with_multiple_fields() - { - var sorting1 = SortBuilder.Ascending("data/age/iv"); - var sorting2 = SortBuilder.Descending("data/firstName/en"); + [Fact] + public void Should_make_query_with_underscore_field() + { + var filter = ClrFilter.Eq("data/dashed_field/iv", "Value"); - AssertSorting("{ 'do.age.iv' : 1, 'do.firstName.en' : -1 }", sorting1, sorting2); - } - - private void AssertQuery(string expected, FilterNode<ClrValue> filter, object? arg = null) - { - AssertQuery(new ClrQuery { Filter = filter }, expected, arg); - } + AssertQuery("{ 'do.dashed-field.iv' : 'Value' }", filter); + } - private void AssertQuery(ClrQuery query, string expected, object? arg = null) - { - var filter = query.AdjustToModel(appId).BuildFilter<MongoContentEntity>(false).Filter!; + [Fact] + public void Should_make_query_with_json_dot_field() + { + var filter = ClrFilter.Eq("data/json/iv/with\\.dot", "Value"); - var rendered = - filter.Render( - BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(), - BsonSerializer.SerializerRegistry) - .ToString(); - - Assert.Equal(Cleanup(expected, arg), rendered); - } - - private void AssertSorting(string expected, params SortNode[] sort) - { - var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); + AssertQuery("{ 'do.json.iv.with_§§_dot' : 'Value' }", filter); + } - var rendered = string.Empty; - - A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>._)) - .Invokes((SortDefinition<MongoContentEntity> sortDefinition) => - { - rendered = - sortDefinition.Render( - BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(), - BsonSerializer.SerializerRegistry) - .ToString(); - }); - - cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }.AdjustToModel(appId)); - - Assert.Equal(Cleanup(expected), rendered); - } - - private static string Cleanup(string filter, object? arg = null) - { - return filter.Replace('\'', '"').Replace("[value]", arg?.ToString(), StringComparison.Ordinal); - } + [Fact] + public void Should_make_query_with_json_slash_field() + { + var filter = ClrFilter.Eq("data/json/iv/with\\/slash", "Value"); + + AssertQuery("{ 'do.json.iv.with/slash' : 'Value' }", filter); + } + + [Fact] + public void Should_make_query_with_references_equals() + { + var filter = ClrFilter.Eq("data/friends/iv", "guid"); + + AssertQuery("{ 'do.friends.iv' : 'guid' }", filter); + } + + [Fact] + public void Should_make_query_with_array_field() + { + var filter = ClrFilter.Eq("data/hobbies/iv/name", "PC"); + + AssertQuery("{ 'do.hobbies.iv.name' : 'PC' }", filter); + } + + [Fact] + public void Should_make_query_with_assets_equals() + { + var filter = ClrFilter.Eq("data/pictures/iv", "guid"); + + AssertQuery("{ 'do.pictures.iv' : 'guid' }", filter); + } + + [Fact] + public void Should_make_orderby_with_single_field() + { + var sorting = SortBuilder.Descending("data/age/iv"); + + AssertSorting("{ 'do.age.iv' : -1 }", sorting); + } + + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var sorting1 = SortBuilder.Ascending("data/age/iv"); + var sorting2 = SortBuilder.Descending("data/firstName/en"); + + AssertSorting("{ 'do.age.iv' : 1, 'do.firstName.en' : -1 }", sorting1, sorting2); + } + + private void AssertQuery(string expected, FilterNode<ClrValue> filter, object? arg = null) + { + AssertQuery(new ClrQuery { Filter = filter }, expected, arg); + } + + private void AssertQuery(ClrQuery query, string expected, object? arg = null) + { + var filter = query.AdjustToModel(appId).BuildFilter<MongoContentEntity>(false).Filter!; + + var rendered = + filter.Render( + BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(), + BsonSerializer.SerializerRegistry) + .ToString(); + + Assert.Equal(Cleanup(expected, arg), rendered); + } + + private void AssertSorting(string expected, params SortNode[] sort) + { + var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); + + var rendered = string.Empty; + + A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>._)) + .Invokes((SortDefinition<MongoContentEntity> sortDefinition) => + { + rendered = + sortDefinition.Render( + BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(), + BsonSerializer.SerializerRegistry) + .ToString(); + }); + + cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }.AdjustToModel(appId)); + + Assert.Equal(Cleanup(expected), rendered); + } + + private static string Cleanup(string filter, object? arg = null) + { + return filter.Replace('\'', '"').Replace("[value]", arg?.ToString(), StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryDedicatedIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryDedicatedIntegrationTests.cs index 1074defeca..5a04f1f41d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryDedicatedIntegrationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryDedicatedIntegrationTests.cs @@ -10,14 +10,13 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; + +[Trait("Category", "Dependencies")] +public class ContentsQueryDedicatedIntegrationTests : ContentsQueryTestsBase, IClassFixture<ContentsQueryDedicatedFixture> { - [Trait("Category", "Dependencies")] - public class ContentsQueryDedicatedIntegrationTests : ContentsQueryTestsBase, IClassFixture<ContentsQueryDedicatedFixture> + public ContentsQueryDedicatedIntegrationTests(ContentsQueryDedicatedFixture fixture) + : base(fixture) { - public ContentsQueryDedicatedIntegrationTests(ContentsQueryDedicatedFixture fixture) - : base(fixture) - { - } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs index d3ec8f6414..71ec7bb8e1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs @@ -29,228 +29,227 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; + +public sealed class ContentsQueryFixture : ContentsQueryFixtureBase { - public sealed class ContentsQueryFixture : ContentsQueryFixtureBase + public ContentsQueryFixture() + : base(false) { - public ContentsQueryFixture() - : base(false) - { - } } +} - public sealed class ContentsQueryDedicatedFixture : ContentsQueryFixtureBase +public sealed class ContentsQueryDedicatedFixture : ContentsQueryFixtureBase +{ + public ContentsQueryDedicatedFixture() + : base(true) { - public ContentsQueryDedicatedFixture() - : base(true) - { - } } +} + +public abstract class ContentsQueryFixtureBase : IAsyncLifetime +{ + private readonly int numValues = 10000; + private readonly IMongoClient mongoClient; + private readonly IMongoDatabase mongoDatabase; + + public MongoContentRepository ContentRepository { get; } - public abstract class ContentsQueryFixtureBase : IAsyncLifetime + public NamedId<DomainId>[] AppIds { get; } = { - private readonly int numValues = 10000; - private readonly IMongoClient mongoClient; - private readonly IMongoDatabase mongoDatabase; + NamedId.Of(DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d452"), "my-app1"), + NamedId.Of(DomainId.Create("4b3672c1-97c6-4e0b-a067-71e9e9a29db9"), "my-app1") + }; - public MongoContentRepository ContentRepository { get; } + public NamedId<DomainId>[] SchemaIds { get; } = + { + NamedId.Of(DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d452"), "my-schema1"), + NamedId.Of(DomainId.Create("4b3672c1-97c6-4e0b-a067-71e9e9a29db9"), "my-schema2"), + NamedId.Of(DomainId.Create("76357c9b-0514-4377-9fcc-a632e7ef960d"), "my-schema3"), + NamedId.Of(DomainId.Create("164c451e-e5a8-41f8-8aaf-e4b56603d7e7"), "my-schema4"), + NamedId.Of(DomainId.Create("741e902c-fdfa-41ad-8e5a-b7cb9d6e3d94"), "my-schema5") + }; + + protected ContentsQueryFixtureBase(bool dedicatedCollections) + { + BsonJsonConvention.Register(TestUtils.DefaultOptions()); - public NamedId<DomainId>[] AppIds { get; } = - { - NamedId.Of(DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d452"), "my-app1"), - NamedId.Of(DomainId.Create("4b3672c1-97c6-4e0b-a067-71e9e9a29db9"), "my-app1") - }; + mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); + mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); - public NamedId<DomainId>[] SchemaIds { get; } = - { - NamedId.Of(DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d452"), "my-schema1"), - NamedId.Of(DomainId.Create("4b3672c1-97c6-4e0b-a067-71e9e9a29db9"), "my-schema2"), - NamedId.Of(DomainId.Create("76357c9b-0514-4377-9fcc-a632e7ef960d"), "my-schema3"), - NamedId.Of(DomainId.Create("164c451e-e5a8-41f8-8aaf-e4b56603d7e7"), "my-schema4"), - NamedId.Of(DomainId.Create("741e902c-fdfa-41ad-8e5a-b7cb9d6e3d94"), "my-schema5") - }; - - protected ContentsQueryFixtureBase(bool dedicatedCollections) + var appProvider = CreateAppProvider(); + + var options = Options.Create(new ContentOptions { - BsonJsonConvention.Register(TestUtils.DefaultOptions()); + OptimizeForSelfHosting = dedicatedCollections + }); - mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); - mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); + ContentRepository = + new MongoContentRepository( + mongoDatabase, + appProvider, + options); + } - var appProvider = CreateAppProvider(); + public Task DisposeAsync() + { + return Task.CompletedTask; + } - var options = Options.Create(new ContentOptions - { - OptimizeForSelfHosting = dedicatedCollections - }); + public async Task InitializeAsync() + { + await ContentRepository.InitializeAsync(default); - ContentRepository = - new MongoContentRepository( - mongoDatabase, - appProvider, - options); - } + await CreateDataAsync(default); + await ClearProfilerAsync(default); + } - public Task DisposeAsync() + private async Task CreateDataAsync( + CancellationToken ct) + { + if (await ContentRepository.StreamAll(AppIds[0].Id, null, ct).AnyAsync(ct)) { - return Task.CompletedTask; + return; } - public async Task InitializeAsync() - { - await ContentRepository.InitializeAsync(default); - - await CreateDataAsync(default); - await ClearProfilerAsync(default); - } + var batch = new List<SnapshotWriteJob<ContentDomainObject.State>>(); - private async Task CreateDataAsync( - CancellationToken ct) + async Task ExecuteBatchAsync(ContentDomainObject.State? state) { - if (await ContentRepository.StreamAll(AppIds[0].Id, null, ct).AnyAsync(ct)) + if (state != null) { - return; + batch.Add(new SnapshotWriteJob<ContentDomainObject.State>(state.UniqueId, state, 0)); } - var batch = new List<SnapshotWriteJob<ContentDomainObject.State>>(); - - async Task ExecuteBatchAsync(ContentDomainObject.State? state) + if ((state == null || batch.Count >= 1000) && batch.Count > 0) { - if (state != null) - { - batch.Add(new SnapshotWriteJob<ContentDomainObject.State>(state.UniqueId, state, 0)); - } + var store = (ISnapshotStore<ContentDomainObject.State>)ContentRepository; - if ((state == null || batch.Count >= 1000) && batch.Count > 0) - { - var store = (ISnapshotStore<ContentDomainObject.State>)ContentRepository; - - await store.WriteManyAsync(batch, ct); + await store.WriteManyAsync(batch, ct); - batch.Clear(); - } + batch.Clear(); } + } - var now = SystemClock.Instance.GetCurrentInstant(); + var now = SystemClock.Instance.GetCurrentInstant(); - var user = RefToken.User("1"); + var user = RefToken.User("1"); - foreach (var appId in AppIds) + foreach (var appId in AppIds) + { + foreach (var schemaId in SchemaIds) { - foreach (var schemaId in SchemaIds) + for (var i = 0; i < numValues; i++) { - for (var i = 0; i < numValues; i++) + var data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(JsonValue.Create(i))) + .AddField("field2", + new ContentFieldData() + .AddInvariant(JsonValue.Create(Lorem.Paragraph(200, 20)))); + + var content = new ContentDomainObject.State { - var data = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(JsonValue.Create(i))) - .AddField("field2", - new ContentFieldData() - .AddInvariant(JsonValue.Create(Lorem.Paragraph(200, 20)))); - - var content = new ContentDomainObject.State - { - Id = DomainId.NewGuid(), - AppId = appId, - Created = now, - CreatedBy = user, - CurrentVersion = new ContentVersion(Status.Published, data), - IsDeleted = false, - LastModified = now, - LastModifiedBy = user, - SchemaId = schemaId - }; - - await ExecuteBatchAsync(content); - } + Id = DomainId.NewGuid(), + AppId = appId, + Created = now, + CreatedBy = user, + CurrentVersion = new ContentVersion(Status.Published, data), + IsDeleted = false, + LastModified = now, + LastModifiedBy = user, + SchemaId = schemaId + }; + + await ExecuteBatchAsync(content); } } - - await ExecuteBatchAsync(null); } - private async Task ClearProfilerAsync( - CancellationToken ct) - { - var prefix = mongoDatabase.DatabaseNamespace.DatabaseName; + await ExecuteBatchAsync(null); + } + + private async Task ClearProfilerAsync( + CancellationToken ct) + { + var prefix = mongoDatabase.DatabaseNamespace.DatabaseName; - foreach (var databaseName in await (await mongoClient.ListDatabaseNamesAsync(ct)).ToListAsync(ct)) + foreach (var databaseName in await (await mongoClient.ListDatabaseNamesAsync(ct)).ToListAsync(ct)) + { + if (!databaseName.StartsWith(prefix, StringComparison.Ordinal)) { - if (!databaseName.StartsWith(prefix, StringComparison.Ordinal)) - { - continue; - } + continue; + } - var database = mongoClient.GetDatabase(databaseName); + var database = mongoClient.GetDatabase(databaseName); - await database.RunCommandAsync<BsonDocument>("{ profile : 0 }", cancellationToken: ct); - await database.DropCollectionAsync("system.profile", ct); - await database.RunCommandAsync<BsonDocument>("{ profile : 2 }", cancellationToken: ct); - } + await database.RunCommandAsync<BsonDocument>("{ profile : 0 }", cancellationToken: ct); + await database.DropCollectionAsync("system.profile", ct); + await database.RunCommandAsync<BsonDocument>("{ profile : 2 }", cancellationToken: ct); } + } - private static IAppProvider CreateAppProvider() - { - var appProvider = A.Fake<IAppProvider>(); + private static IAppProvider CreateAppProvider() + { + var appProvider = A.Fake<IAppProvider>(); - A.CallTo(() => appProvider.GetAppWithSchemaAsync(A<DomainId>._, A<DomainId>._, false, A<CancellationToken>._)) - .ReturnsLazily(x => - { - var appId = x.GetArgument<DomainId>(0)!; + A.CallTo(() => appProvider.GetAppWithSchemaAsync(A<DomainId>._, A<DomainId>._, false, A<CancellationToken>._)) + .ReturnsLazily(x => + { + var appId = x.GetArgument<DomainId>(0)!; - return Task.FromResult<(IAppEntity?, ISchemaEntity?)>(( - CreateApp(appId), - CreateSchema(appId, x.GetArgument<DomainId>(1)!))); - }); + return Task.FromResult<(IAppEntity?, ISchemaEntity?)>(( + CreateApp(appId), + CreateSchema(appId, x.GetArgument<DomainId>(1)!))); + }); - return appProvider; - } + return appProvider; + } - public DomainId RandomAppId() - { - return AppIds[Random.Shared.Next(AppIds.Length)].Id; - } + public DomainId RandomAppId() + { + return AppIds[Random.Shared.Next(AppIds.Length)].Id; + } - public IAppEntity RandomApp() - { - return CreateApp(RandomAppId()); - } + public IAppEntity RandomApp() + { + return CreateApp(RandomAppId()); + } - public DomainId RandomSchemaId() - { - return SchemaIds[Random.Shared.Next(SchemaIds.Length)].Id; - } + public DomainId RandomSchemaId() + { + return SchemaIds[Random.Shared.Next(SchemaIds.Length)].Id; + } - public ISchemaEntity RandomSchema() - { - return CreateSchema(RandomAppId(), RandomSchemaId()); - } + public ISchemaEntity RandomSchema() + { + return CreateSchema(RandomAppId(), RandomSchemaId()); + } - public string RandomValue() - { - return Random.Shared.Next(numValues).ToString(CultureInfo.InvariantCulture); - } + public string RandomValue() + { + return Random.Shared.Next(numValues).ToString(CultureInfo.InvariantCulture); + } - private static IAppEntity CreateApp(DomainId appId) - { - return Mocks.App(NamedId.Of(appId, "my-app")); - } + private static IAppEntity CreateApp(DomainId appId) + { + return Mocks.App(NamedId.Of(appId, "my-app")); + } - private static ISchemaEntity CreateSchema(DomainId appId, DomainId schemaId) - { - var schemaDef = - new Schema("my-schema") - .AddField(Fields.Number(1, "value", Partitioning.Invariant)); + private static ISchemaEntity CreateSchema(DomainId appId, DomainId schemaId) + { + var schemaDef = + new Schema("my-schema") + .AddField(Fields.Number(1, "value", Partitioning.Invariant)); - var schema = - Mocks.Schema( - NamedId.Of(appId, "my-app"), - NamedId.Of(schemaId, "my-schema"), - schemaDef); + var schema = + Mocks.Schema( + NamedId.Of(appId, "my-app"), + NamedId.Of(schemaId, "my-schema"), + schemaDef); - return schema; - } + return schema; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs index ed60548138..15f680a191 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs @@ -7,14 +7,13 @@ using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; + +[Trait("Category", "Dependencies")] +public class ContentsQueryIntegrationTests : ContentsQueryTestsBase, IClassFixture<ContentsQueryFixture> { - [Trait("Category", "Dependencies")] - public class ContentsQueryIntegrationTests : ContentsQueryTestsBase, IClassFixture<ContentsQueryFixture> + public ContentsQueryIntegrationTests(ContentsQueryFixture fixture) + : base(fixture) { - public ContentsQueryIntegrationTests(ContentsQueryFixture fixture) - : base(fixture) - { - } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTestsBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTestsBase.cs index cecc08e3fd..6ef5300f05 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTestsBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTestsBase.cs @@ -16,220 +16,219 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; + +public abstract class ContentsQueryTestsBase { - public abstract class ContentsQueryTestsBase + public ContentsQueryFixtureBase _ { get; } + + protected ContentsQueryTestsBase(ContentsQueryFixtureBase fixture) { - public ContentsQueryFixtureBase _ { get; } + _ = fixture; + } - protected ContentsQueryTestsBase(ContentsQueryFixtureBase fixture) - { - _ = fixture; - } + [Fact] + public async Task Should_verify_ids() + { + var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - [Fact] - public async Task Should_verify_ids() - { - var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); + var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), ids, SearchScope.Published); - var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), ids, SearchScope.Published); + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } + [Fact] + public async Task Should_query_contents_by_ids() + { + var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - [Fact] - public async Task Should_query_contents_by_ids() + var schemas = new List<ISchemaEntity> { - var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); + _.RandomSchema() + }; - var schemas = new List<ISchemaEntity> - { - _.RandomSchema() - }; - - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), schemas, Q.Empty.WithIds(ids), SearchScope.All); + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), schemas, Q.Empty.WithIds(ids), SearchScope.All); - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } - [Fact] - public async Task Should_query_contents_by_ids_and_schema() - { - var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); + [Fact] + public async Task Should_query_contents_by_ids_and_schema() + { + var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithIds(ids), SearchScope.All); + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithIds(ids), SearchScope.All); - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } - [Fact] - public async Task Should_query_contents_ids_by_filter() - { - var filter = F.Eq("data.field1.iv", 12); + [Fact] + public async Task Should_query_contents_ids_by_filter() + { + var filter = F.Eq("data.field1.iv", 12); - var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), _.RandomSchemaId(), filter); + var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), _.RandomSchemaId(), filter); - // We have a concrete query, so we expect an actual. - Assert.NotEmpty(contents); - } + // We have a concrete query, so we expect an actual. + Assert.NotEmpty(contents); + } - [Fact] - public async Task Should_query_contents_by_filter() + [Fact] + public async Task Should_query_contents_by_filter() + { + var query = new ClrQuery { - var query = new ClrQuery - { - Filter = F.Eq("data.field1.iv", 12) - }; + Filter = F.Eq("data.field1.iv", 12) + }; - var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); + var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); - // We have a concrete query, so we expect an actual. - Assert.NotEmpty(contents); - } + // We have a concrete query, so we expect an actual. + Assert.NotEmpty(contents); + } - [Fact] - public async Task Should_query_contents_scheduled() - { - var time = SystemClock.Instance.GetCurrentInstant(); + [Fact] + public async Task Should_query_contents_scheduled() + { + var time = SystemClock.Instance.GetCurrentInstant(); - var contents = await _.ContentRepository.QueryScheduledWithoutDataAsync(time).ToListAsync(); + var contents = await _.ContentRepository.QueryScheduledWithoutDataAsync(time).ToListAsync(); - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } - [Fact] - public async Task Should_query_contents_with_default_query() - { - var query = new ClrQuery(); + [Fact] + public async Task Should_query_contents_with_default_query() + { + var query = new ClrQuery(); - var contents = await QueryAsync(_.ContentRepository, query); + var contents = await QueryAsync(_.ContentRepository, query); - // We have a concrete query, so we expect an actual result. - Assert.NotEmpty(contents); - } + // We have a concrete query, so we expect an actual result. + Assert.NotEmpty(contents); + } - [Fact] - public async Task Should_query_contents_with_default_query_and_id() - { - var query = new ClrQuery(); + [Fact] + public async Task Should_query_contents_with_default_query_and_id() + { + var query = new ClrQuery(); - var contents = await QueryAsync(_.ContentRepository, query, reference: DomainId.NewGuid()); + var contents = await QueryAsync(_.ContentRepository, query, reference: DomainId.NewGuid()); - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } - [Fact] - public async Task Should_query_contents_with_large_skip() + [Fact] + public async Task Should_query_contents_with_large_skip() + { + var query = new ClrQuery { - var query = new ClrQuery + Sort = new List<SortNode> { - Sort = new List<SortNode> - { - new SortNode("data.value.iv", SortOrder.Ascending) - } - }; + new SortNode("data.value.iv", SortOrder.Ascending) + } + }; - var contents = await QueryAsync(_.ContentRepository, query, 1000, 9000); + var contents = await QueryAsync(_.ContentRepository, query, 1000, 9000); - // We have a concrete query, so we expect an actual result. - Assert.NotEmpty(contents); - } + // We have a concrete query, so we expect an actual result. + Assert.NotEmpty(contents); + } - [Fact] - public async Task Should_query_contents_with_query_fulltext() + [Fact] + public async Task Should_query_contents_with_query_fulltext() + { + var query = new ClrQuery { - var query = new ClrQuery - { - FullText = "hello" - }; + FullText = "hello" + }; - var contents = await QueryAsync(_.ContentRepository, query); + var contents = await QueryAsync(_.ContentRepository, query); - // The full text is resolved by another system, so we cannot verify the actual result. - Assert.NotNull(contents); - } + // The full text is resolved by another system, so we cannot verify the actual result. + Assert.NotNull(contents); + } - [Fact] - public async Task Should_query_contents_with_query_filter() + [Fact] + public async Task Should_query_contents_with_query_filter() + { + var query = new ClrQuery { - var query = new ClrQuery - { - Filter = F.Eq("data.field1.iv", 200) - }; + Filter = F.Eq("data.field1.iv", 200) + }; - var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); + var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); - // We have a concrete query, so we expect an actual result. - Assert.NotEmpty(contents); - } + // We have a concrete query, so we expect an actual result. + Assert.NotEmpty(contents); + } - [Fact] - public async Task Should_query_contents_with_query_filter_and_id() + [Fact] + public async Task Should_query_contents_with_query_filter_and_id() + { + var query = new ClrQuery { - var query = new ClrQuery - { - Filter = F.Eq("data.value.iv", 12) - }; + Filter = F.Eq("data.value.iv", 12) + }; - var contents = await QueryAsync(_.ContentRepository, query, 1000, 0, reference: DomainId.NewGuid()); + var contents = await QueryAsync(_.ContentRepository, query, 1000, 0, reference: DomainId.NewGuid()); - // We do not insert test entities with references, so we cannot verify the actual result. - Assert.Empty(contents); - } + // We do not insert test entities with references, so we cannot verify the actual result. + Assert.Empty(contents); + } - [Fact] - public async Task Should_query_contents_with_random_count() + [Fact] + public async Task Should_query_contents_with_random_count() + { + var query = new ClrQuery { - var query = new ClrQuery - { - Random = 40 - }; + Random = 40 + }; - var contents = await QueryAsync(_.ContentRepository, query); + var contents = await QueryAsync(_.ContentRepository, query); - // We do not insert test entities with references, so we cannot verify the actual. - Assert.Equal(40, contents.Count); - } + // We do not insert test entities with references, so we cannot verify the actual. + Assert.Equal(40, contents.Count); + } - private async Task<IResultList<IContentEntity>> QueryAsync(IContentRepository contentRepository, - ClrQuery clrQuery, - int take = 1000, - int skip = 100, - DomainId reference = default) + private async Task<IResultList<IContentEntity>> QueryAsync(IContentRepository contentRepository, + ClrQuery clrQuery, + int take = 1000, + int skip = 100, + DomainId reference = default) + { + if (clrQuery.Take == long.MaxValue) { - if (clrQuery.Take == long.MaxValue) - { - clrQuery.Take = take; - } + clrQuery.Take = take; + } - if (clrQuery.Skip == 0) - { - clrQuery.Skip = skip; - } + if (clrQuery.Skip == 0) + { + clrQuery.Skip = skip; + } - if (clrQuery.Sort == null || clrQuery.Sort.Count == 0) + if (clrQuery.Sort == null || clrQuery.Sort.Count == 0) + { + clrQuery.Sort = new List<SortNode> { - clrQuery.Sort = new List<SortNode> - { - new SortNode("LastModified", SortOrder.Descending), - new SortNode("Id", SortOrder.Ascending) - }; - } + new SortNode("LastModified", SortOrder.Descending), + new SortNode("Id", SortOrder.Ascending) + }; + } - var q = - Q.Empty - .WithoutTotal() - .WithQuery(clrQuery) - .WithReference(reference); + var q = + Q.Empty + .WithoutTotal() + .WithQuery(clrQuery) + .WithReference(reference); - var contents = await contentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), q, SearchScope.All); + var contents = await contentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), q, SearchScope.All); - return contents; - } + return contents; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs index e7fabe5e0c..4c754464ef 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs @@ -11,65 +11,64 @@ using Squidex.Domain.Apps.Core.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; + +public sealed class StatusSerializerTests { - public sealed class StatusSerializerTests + private sealed record ValueHolder<T> { - private sealed record ValueHolder<T> - { - public T Value { get; set; } - } + public T Value { get; set; } + } - public StatusSerializerTests() - { - TestUtils.SetupBson(); - } + public StatusSerializerTests() + { + TestUtils.SetupBson(); + } - [Fact] - public void Should_serialize_and_deserialize_status() + [Fact] + public void Should_serialize_and_deserialize_status() + { + var source = new ValueHolder<Status> { - var source = new ValueHolder<Status> - { - Value = Status.Published - }; + Value = Status.Published + }; - var deserialized = SerializeAndDeserializeBson(source); + var deserialized = SerializeAndDeserializeBson(source); - Assert.Equal(source, deserialized); - } + Assert.Equal(source, deserialized); + } - [Fact] - public void Should_serialize_and_deserialize_default_status() + [Fact] + public void Should_serialize_and_deserialize_default_status() + { + var source = new ValueHolder<Status> { - var source = new ValueHolder<Status> - { - Value = default - }; + Value = default + }; - var deserialized = SerializeAndDeserializeBson(source); + var deserialized = SerializeAndDeserializeBson(source); - Assert.Equal(source, deserialized); - } + Assert.Equal(source, deserialized); + } - private static T SerializeAndDeserializeBson<T>(T value) - { - var stream = new MemoryStream(); + private static T SerializeAndDeserializeBson<T>(T value) + { + var stream = new MemoryStream(); - using (var writer = new BsonBinaryWriter(stream)) - { - BsonSerializer.Serialize(writer, value); + using (var writer = new BsonBinaryWriter(stream)) + { + BsonSerializer.Serialize(writer, value); - writer.Flush(); - } + writer.Flush(); + } - stream.Position = 0; + stream.Position = 0; - using (var reader = new BsonBinaryReader(stream)) - { - var actual = BsonSerializer.Deserialize<T>(reader); + using (var reader = new BsonBinaryReader(stream)) + { + var actual = BsonSerializer.Deserialize<T>(reader); - return actual; - } + return actual; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculateTokensTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculateTokensTests.cs index d30243db9a..61470d673d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculateTokensTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculateTokensTests.cs @@ -15,58 +15,57 @@ using Squidex.Infrastructure.Json; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class CalculateTokensTests { - public class CalculateTokensTests - { - private readonly ISchemaEntity schema; - private readonly IJsonSerializer serializer = A.Fake<IJsonSerializer>(); - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly Context requestContext; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly ProvideSchema schemaProvider; - private readonly CalculateTokens sut; + private readonly ISchemaEntity schema; + private readonly IJsonSerializer serializer = A.Fake<IJsonSerializer>(); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly Context requestContext; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly CalculateTokens sut; - public CalculateTokensTests() - { - requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + public CalculateTokensTests() + { + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); - schema = Mocks.Schema(appId, schemaId); - schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); + schema = Mocks.Schema(appId, schemaId); + schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); - sut = new CalculateTokens(urlGenerator, serializer); - } + sut = new CalculateTokens(urlGenerator, serializer); + } - [Fact] - public async Task Should_compute_ui_tokens() - { - var source = CreateContent(); + [Fact] + public async Task Should_compute_ui_tokens() + { + var source = CreateContent(); - await sut.EnrichAsync(requestContext, new[] { source }, schemaProvider, default); + await sut.EnrichAsync(requestContext, new[] { source }, schemaProvider, default); - Assert.NotNull(source.EditToken); + Assert.NotNull(source.EditToken); - A.CallTo(() => urlGenerator.Root()) - .MustHaveHappened(); - } + A.CallTo(() => urlGenerator.Root()) + .MustHaveHappened(); + } - [Fact] - public async Task Should_also_compute_ui_tokens_for_frontend() - { - var source = CreateContent(); + [Fact] + public async Task Should_also_compute_ui_tokens_for_frontend() + { + var source = CreateContent(); - await sut.EnrichAsync(new Context(Mocks.FrontendUser(), Mocks.App(appId)), new[] { source }, schemaProvider, default); + await sut.EnrichAsync(new Context(Mocks.FrontendUser(), Mocks.App(appId)), new[] { source }, schemaProvider, default); - Assert.NotNull(source.EditToken); + Assert.NotNull(source.EditToken); - A.CallTo(() => urlGenerator.Root()) - .MustHaveHappened(); - } + A.CallTo(() => urlGenerator.Root()) + .MustHaveHappened(); + } - private ContentEntity CreateContent() - { - return new ContentEntity { AppId = appId, SchemaId = schemaId }; - } + private ContentEntity CreateContent() + { + return new ContentEntity { AppId = appId, SchemaId = schemaId }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs index 3f6fe90b25..f61235e954 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs @@ -12,140 +12,139 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ContentEnricherTests { - public class ContentEnricherTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly ISchemaEntity schema; + private readonly Context requestContext; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + + private sealed class ResolveSchema : IContentEnricherStep { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly ISchemaEntity schema; - private readonly Context requestContext; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - - private sealed class ResolveSchema : IContentEnricherStep - { - public ISchemaEntity Schema { get; private set; } + public ISchemaEntity Schema { get; private set; } - public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, - CancellationToken ct) + public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, + CancellationToken ct) + { + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) { - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - Schema = (await schemas(group.Key)).Schema; - } + Schema = (await schemas(group.Key)).Schema; } } + } - public ContentEnricherTests() - { - ct = cts.Token; + public ContentEnricherTests() + { + ct = cts.Token; - requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); - schema = Mocks.Schema(appId, schemaId); + schema = Mocks.Schema(appId, schemaId); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, ct)) - .Returns(schema); - } + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, ct)) + .Returns(schema); + } - [Fact] - public async Task Should_only_invoke_pre_enrich_for_empty_actuals() - { - var source = Array.Empty<IContentEntity>(); + [Fact] + public async Task Should_only_invoke_pre_enrich_for_empty_actuals() + { + var source = Array.Empty<IContentEntity>(); - var step1 = A.Fake<IContentEnricherStep>(); - var step2 = A.Fake<IContentEnricherStep>(); + var step1 = A.Fake<IContentEnricherStep>(); + var step2 = A.Fake<IContentEnricherStep>(); - var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); + var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); - await sut.EnrichAsync(source, requestContext, ct); + await sut.EnrichAsync(source, requestContext, ct); - A.CallTo(() => step1.EnrichAsync(requestContext, ct)) - .MustHaveHappened(); + A.CallTo(() => step1.EnrichAsync(requestContext, ct)) + .MustHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, ct)) - .MustHaveHappened(); + A.CallTo(() => step2.EnrichAsync(requestContext, ct)) + .MustHaveHappened(); - A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => step2.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_invoke_steps() - { - var source = CreateContent(); + [Fact] + public async Task Should_invoke_steps() + { + var source = CreateContent(); - var step1 = A.Fake<IContentEnricherStep>(); - var step2 = A.Fake<IContentEnricherStep>(); + var step1 = A.Fake<IContentEnricherStep>(); + var step2 = A.Fake<IContentEnricherStep>(); - var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); + var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); - await sut.EnrichAsync(source, false, requestContext, ct); + await sut.EnrichAsync(source, false, requestContext, ct); - A.CallTo(() => step1.EnrichAsync(requestContext, ct)) - .MustHaveHappened(); + A.CallTo(() => step1.EnrichAsync(requestContext, ct)) + .MustHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, ct)) - .MustHaveHappened(); + A.CallTo(() => step2.EnrichAsync(requestContext, ct)) + .MustHaveHappened(); - A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._, ct)) - .MustHaveHappened(); + A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._, ct)) + .MustHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._, ct)) - .MustHaveHappened(); - } + A.CallTo(() => step2.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_provide_and_cache_schema() - { - var source = CreateContent(); + [Fact] + public async Task Should_provide_and_cache_schema() + { + var source = CreateContent(); - var step1 = new ResolveSchema(); - var step2 = new ResolveSchema(); + var step1 = new ResolveSchema(); + var step2 = new ResolveSchema(); - var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); + var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); - await sut.EnrichAsync(source, false, requestContext, ct); + await sut.EnrichAsync(source, false, requestContext, ct); - Assert.Same(schema, step1.Schema); - Assert.Same(schema, step1.Schema); + Assert.Same(schema, step1.Schema); + Assert.Same(schema, step1.Schema); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_clone_data_if_requested() - { - var source = CreateContent(new ContentData()); + [Fact] + public async Task Should_clone_data_if_requested() + { + var source = CreateContent(new ContentData()); - var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), appProvider); + var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), appProvider); - var actual = await sut.EnrichAsync(source, true, requestContext, ct); + var actual = await sut.EnrichAsync(source, true, requestContext, ct); - Assert.NotSame(source.Data, actual.Data); - } + Assert.NotSame(source.Data, actual.Data); + } - [Fact] - public async Task Should_not_clone_data_if_not_requested() - { - var source = CreateContent(new ContentData()); + [Fact] + public async Task Should_not_clone_data_if_not_requested() + { + var source = CreateContent(new ContentData()); - var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), appProvider); + var sut = new ContentEnricher(Enumerable.Empty<IContentEnricherStep>(), appProvider); - var actual = await sut.EnrichAsync(source, false, requestContext, ct); + var actual = await sut.EnrichAsync(source, false, requestContext, ct); - Assert.Same(source.Data, actual.Data); - } + Assert.Same(source.Data, actual.Data); + } - private ContentEntity CreateContent(ContentData? data = null) - { - return new ContentEntity { SchemaId = schemaId, Data = data! }; - } + private ContentEntity CreateContent(ContentData? data = null) + { + return new ContentEntity { SchemaId = schemaId, Data = data! }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs index 8c88cbe5ef..60288ab90e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs @@ -11,108 +11,107 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ContentLoaderTests { - public class ContentLoaderTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly IDomainObjectCache domainObjectCache = A.Fake<IDomainObjectCache>(); + private readonly ContentDomainObject domainObject = A.Fake<ContentDomainObject>(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly DomainId id = DomainId.NewGuid(); + private readonly DomainId unqiueId; + private readonly ContentLoader sut; + + public ContentLoaderTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly IDomainObjectCache domainObjectCache = A.Fake<IDomainObjectCache>(); - private readonly ContentDomainObject domainObject = A.Fake<ContentDomainObject>(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly DomainId id = DomainId.NewGuid(); - private readonly DomainId unqiueId; - private readonly ContentLoader sut; - - public ContentLoaderTests() - { - ct = cts.Token; + ct = cts.Token; - unqiueId = DomainId.Combine(appId, id); + unqiueId = DomainId.Combine(appId, id); - A.CallTo(() => domainObjectCache.GetAsync<ContentDomainObject.State>(A<DomainId>._, A<long>._, ct)) - .Returns(Task.FromResult<ContentDomainObject.State>(null!)); + A.CallTo(() => domainObjectCache.GetAsync<ContentDomainObject.State>(A<DomainId>._, A<long>._, ct)) + .Returns(Task.FromResult<ContentDomainObject.State>(null!)); - A.CallTo(() => domainObjectFactory.Create<ContentDomainObject>(unqiueId)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<ContentDomainObject>(unqiueId)) + .Returns(domainObject); - sut = new ContentLoader(domainObjectFactory, domainObjectCache); - } + sut = new ContentLoader(domainObjectFactory, domainObjectCache); + } - [Fact] - public async Task Should_return_null_if_no_state_returned() - { - var content = (ContentDomainObject.State)null!; + [Fact] + public async Task Should_return_null_if_no_state_returned() + { + var content = (ContentDomainObject.State)null!; - A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) - .Returns(content); + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(content); - Assert.Null(await sut.GetAsync(appId, id, 10, ct)); - } + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); + } - [Fact] - public async Task Should_return_null_if_state_empty() - { - var content = new ContentDomainObject.State { Version = EtagVersion.Empty }; + [Fact] + public async Task Should_return_null_if_state_empty() + { + var content = new ContentDomainObject.State { Version = EtagVersion.Empty }; - A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) - .Returns(content); + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(content); - Assert.Null(await sut.GetAsync(appId, id, 10, ct)); - } + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); + } - [Fact] - public async Task Should_return_null_if_state_has_other_version() - { - var content = new ContentDomainObject.State { Version = 5 }; + [Fact] + public async Task Should_return_null_if_state_has_other_version() + { + var content = new ContentDomainObject.State { Version = 5 }; - A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) - .Returns(content); + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(content); - Assert.Null(await sut.GetAsync(appId, id, 10, ct)); - } + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); + } - [Fact] - public async Task Should_not_return_null_if_state_has_other_version_than_any() - { - var content = new ContentDomainObject.State { Version = 5 }; + [Fact] + public async Task Should_not_return_null_if_state_has_other_version_than_any() + { + var content = new ContentDomainObject.State { Version = 5 }; - A.CallTo(() => domainObject.GetSnapshotAsync(EtagVersion.Any, ct)) - .Returns(content); + A.CallTo(() => domainObject.GetSnapshotAsync(EtagVersion.Any, ct)) + .Returns(content); - var actual = await sut.GetAsync(appId, id, EtagVersion.Any, ct); + var actual = await sut.GetAsync(appId, id, EtagVersion.Any, ct); - Assert.Same(content, actual); - } + Assert.Same(content, actual); + } - [Fact] - public async Task Should_return_content_from_state() - { - var content = new ContentDomainObject.State { Version = 10 }; + [Fact] + public async Task Should_return_content_from_state() + { + var content = new ContentDomainObject.State { Version = 10 }; - A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) - .Returns(content); + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(content); - var actual = await sut.GetAsync(appId, id, 10, ct); + var actual = await sut.GetAsync(appId, id, 10, ct); - Assert.Same(content, actual); - } + Assert.Same(content, actual); + } - [Fact] - public async Task Should_return_content_from_cache() - { - var content = new ContentDomainObject.State { Version = 10 }; + [Fact] + public async Task Should_return_content_from_cache() + { + var content = new ContentDomainObject.State { Version = 10 }; - A.CallTo(() => domainObjectCache.GetAsync<ContentDomainObject.State>(DomainId.Combine(appId, id), 10, ct)) - .Returns(content); + A.CallTo(() => domainObjectCache.GetAsync<ContentDomainObject.State>(DomainId.Combine(appId, id), 10, ct)) + .Returns(content); - var actual = await sut.GetAsync(appId, id, 10, ct); + var actual = await sut.GetAsync(appId, id, 10, ct); - Assert.Same(content, actual); + Assert.Same(content, actual); - A.CallTo(() => domainObjectFactory.Create<ContentDomainObject>(unqiueId)) - .MustNotHaveHappened(); - } + A.CallTo(() => domainObjectFactory.Create<ContentDomainObject>(unqiueId)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs index 8a7ef75adb..24d7f3b0bb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs @@ -20,293 +20,292 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ContentQueryParserTests { - public class ContentQueryParserTests + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly ITextIndex textIndex = A.Fake<ITextIndex>(); + private readonly ISchemaEntity schema; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly ContentQueryParser sut; + + public ContentQueryParserTests() { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly ITextIndex textIndex = A.Fake<ITextIndex>(); - private readonly ISchemaEntity schema; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext; - private readonly ContentQueryParser sut; - - public ContentQueryParserTests() - { - var options = Options.Create(new ContentOptions { DefaultPageSize = 30 }); + var options = Options.Create(new ContentOptions { DefaultPageSize = 30 }); - requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - var schemaDef = - new Schema(schemaId.Name) - .AddString(1, "firstName", Partitioning.Invariant) - .AddGeolocation(2, "geo", Partitioning.Invariant); + var schemaDef = + new Schema(schemaId.Name) + .AddString(1, "firstName", Partitioning.Invariant) + .AddGeolocation(2, "geo", Partitioning.Invariant); - schema = Mocks.Schema(appId, schemaId, schemaDef); + schema = Mocks.Schema(appId, schemaId, schemaDef); - var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - sut = new ContentQueryParser(appProvider, textIndex, options, cache, TestUtils.DefaultSerializer); - } + sut = new ContentQueryParser(appProvider, textIndex, options, cache, TestUtils.DefaultSerializer); + } - [Fact] - public async Task Should_skip_total_if_set_in_context() - { - var q = await sut.ParseAsync(requestContext.Clone(b => b.WithoutTotal()), Q.Empty); + [Fact] + public async Task Should_skip_total_if_set_in_context() + { + var q = await sut.ParseAsync(requestContext.Clone(b => b.WithoutTotal()), Q.Empty); - Assert.True(q.NoTotal); - } + Assert.True(q.NoTotal); + } - [Fact] - public async Task Should_throw_if_odata_query_is_invalid() - { - var query = Q.Empty.WithODataQuery("$filter=invalid"); + [Fact] + public async Task Should_throw_if_odata_query_is_invalid() + { + var query = Q.Empty.WithODataQuery("$filter=invalid"); - await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query, schema)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query, schema)); + } - [Fact] - public async Task Should_throw_if_json_query_is_invalid() - { - var query = Q.Empty.WithJsonQuery("invalid"); + [Fact] + public async Task Should_throw_if_json_query_is_invalid() + { + var query = Q.Empty.WithJsonQuery("invalid"); - await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query, schema)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.ParseAsync(requestContext, query, schema)); + } - [Fact] - public async Task Should_parse_odata_query_without_schema() - { - var query = Q.Empty.WithODataQuery("$filter=status eq 'Draft'"); + [Fact] + public async Task Should_parse_odata_query_without_schema() + { + var query = Q.Empty.WithODataQuery("$filter=status eq 'Draft'"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_parse_json_query_without_schema() - { - var query = Q.Empty.WithJsonQuery("{ \"filter\": { \"path\": \"status\", \"op\": \"eq\", \"value\": \"Draft\" } }"); + [Fact] + public async Task Should_parse_json_query_without_schema() + { + var query = Q.Empty.WithJsonQuery("{ \"filter\": { \"path\": \"status\", \"op\": \"eq\", \"value\": \"Draft\" } }"); - var q = await sut.ParseAsync(requestContext, query); + var q = await sut.ParseAsync(requestContext, query); - Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_parse_odata_query() - { - var query = Q.Empty.WithODataQuery("$top=100&$orderby=data/firstName/iv asc&$filter=status eq 'Draft'"); + [Fact] + public async Task Should_parse_odata_query() + { + var query = Q.Empty.WithODataQuery("$top=100&$orderby=data/firstName/iv asc&$filter=status eq 'Draft'"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: status == 'Draft'; Take: 100; Sort: data.firstName.iv Ascending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: status == 'Draft'; Take: 100; Sort: data.firstName.iv Ascending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_parse_odata_query_and_enrich_with_defaults() - { - var query = Q.Empty.WithODataQuery("$top=200&$filter=data/firstName/iv eq 'ABC'"); + [Fact] + public async Task Should_parse_odata_query_and_enrich_with_defaults() + { + var query = Q.Empty.WithODataQuery("$top=200&$filter=data/firstName/iv eq 'ABC'"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_parse_json_query_and_enrich_with_defaults() - { - var query = Q.Empty.WithJsonQuery("{ \"filter\": { \"path\": \"data.firstName.iv\", \"op\": \"eq\", \"value\": \"ABC\" } }"); + [Fact] + public async Task Should_parse_json_query_and_enrich_with_defaults() + { + var query = Q.Empty.WithJsonQuery("{ \"filter\": { \"path\": \"data.firstName.iv\", \"op\": \"eq\", \"value\": \"ABC\" } }"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_full_text_query_to_filter_with_other_filter() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) - .Returns(new List<DomainId> { DomainId.Create("1"), DomainId.Create("2") }); + [Fact] + public async Task Should_convert_full_text_query_to_filter_with_other_filter() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) + .Returns(new List<DomainId> { DomainId.Create("1"), DomainId.Create("2") }); - var query = Q.Empty.WithODataQuery("$search=Hello&$filter=data/firstName/iv eq 'ABC'"); + var query = Q.Empty.WithODataQuery("$search=Hello&$filter=data/firstName/iv eq 'ABC'"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: (data.firstName.iv == 'ABC' && id in ['1', '2']); Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: (data.firstName.iv == 'ABC' && id in ['1', '2']); Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_full_text_query_to_filter() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) - .Returns(new List<DomainId> { DomainId.Create("1"), DomainId.Create("2") }); + [Fact] + public async Task Should_convert_full_text_query_to_filter() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) + .Returns(new List<DomainId> { DomainId.Create("1"), DomainId.Create("2") }); - var query = Q.Empty.WithODataQuery("$search=Hello"); + var query = Q.Empty.WithODataQuery("$search=Hello"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: id in ['1', '2']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: id in ['1', '2']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_full_text_query_to_filter_if_single_id_found() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) - .Returns(new List<DomainId> { DomainId.Create("1") }); + [Fact] + public async Task Should_convert_full_text_query_to_filter_if_single_id_found() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) + .Returns(new List<DomainId> { DomainId.Create("1") }); - var query = Q.Empty.WithODataQuery("$search=Hello"); + var query = Q.Empty.WithODataQuery("$search=Hello"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: id in ['1']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: id in ['1']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_full_text_query_to_filter_if_index_returns_null() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) - .Returns(Task.FromResult<List<DomainId>?>(null)); + [Fact] + public async Task Should_convert_full_text_query_to_filter_if_index_returns_null() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) + .Returns(Task.FromResult<List<DomainId>?>(null)); - var query = Q.Empty.WithODataQuery("$search=Hello"); + var query = Q.Empty.WithODataQuery("$search=Hello"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_full_text_query_to_filter_if_index_returns_empty() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) - .Returns(new List<DomainId>()); + [Fact] + public async Task Should_convert_full_text_query_to_filter_if_index_returns_empty() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) + .Returns(new List<DomainId>()); - var query = Q.Empty.WithODataQuery("$search=Hello"); + var query = Q.Empty.WithODataQuery("$search=Hello"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_geo_query_to_filter() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30, 1000), requestContext.Scope(), default)) - .Returns(new List<DomainId> { DomainId.Create("1"), DomainId.Create("2") }); + [Fact] + public async Task Should_convert_geo_query_to_filter() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30, 1000), requestContext.Scope(), default)) + .Returns(new List<DomainId> { DomainId.Create("1"), DomainId.Create("2") }); - var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); + var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: id in ['1', '2']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: id in ['1', '2']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_geo_query_to_filter_if_single_id_found() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30, 1000), requestContext.Scope(), default)) - .Returns(new List<DomainId> { DomainId.Create("1") }); + [Fact] + public async Task Should_convert_geo_query_to_filter_if_single_id_found() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30, 1000), requestContext.Scope(), default)) + .Returns(new List<DomainId> { DomainId.Create("1") }); - var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); + var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: id in ['1']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: id in ['1']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_geo_query_to_filter_if_index_returns_null() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30, 1000), requestContext.Scope(), default)) - .Returns(Task.FromResult<List<DomainId>?>(null)); + [Fact] + public async Task Should_convert_geo_query_to_filter_if_index_returns_null() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30, 1000), requestContext.Scope(), default)) + .Returns(Task.FromResult<List<DomainId>?>(null)); - var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); + var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_geo_query_to_filter_if_index_returns_empty() - { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30, 1000), requestContext.Scope(), default)) - .Returns(new List<DomainId>()); + [Fact] + public async Task Should_convert_geo_query_to_filter_if_index_returns_empty() + { + A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30, 1000), requestContext.Scope(), default)) + .Returns(new List<DomainId>()); - var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); + var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Theory] - [InlineData(0L)] - [InlineData(-1L)] - [InlineData(long.MaxValue)] - [InlineData(long.MinValue)] - public async Task Should_apply_default_take_size_if_not_defined(long take) - { - var query = Q.Empty.WithQuery(new ClrQuery { Take = take }); + [Theory] + [InlineData(0L)] + [InlineData(-1L)] + [InlineData(long.MaxValue)] + [InlineData(long.MinValue)] + public async Task Should_apply_default_take_size_if_not_defined(long take) + { + var query = Q.Empty.WithQuery(new ClrQuery { Take = take }); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_set_take_to_ids_count_if_take_not_defined() - { - var query = Q.Empty.WithIds("1, 2, 3"); + [Fact] + public async Task Should_set_take_to_ids_count_if_take_not_defined() + { + var query = Q.Empty.WithIds("1, 2, 3"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Take: 3; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Take: 3; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_not_set_take_to_ids_count_if_take_defined() - { - var query = Q.Empty.WithIds("1, 2, 3").WithQuery(new ClrQuery { Take = 20 }); + [Fact] + public async Task Should_not_set_take_to_ids_count_if_take_defined() + { + var query = Q.Empty.WithIds("1, 2, 3").WithQuery(new ClrQuery { Take = 20 }); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Take: 20; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Take: 20; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_apply_default_take_limit() - { - var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); + [Fact] + public async Task Should_apply_default_take_limit() + { + var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); + } - [Fact] - public async Task Should_not_apply_id_ordering_twice() - { - var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); + [Fact] + public async Task Should_not_apply_id_ordering_twice() + { + var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString()); - } + Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString()); + } - [Fact] - public async Task Should_convert_json_query_and_enrich_with_defaults() - { - var query = Q.Empty.WithJsonQuery( - new Query<JsonValue> - { - Filter = new CompareFilter<JsonValue>("data.firstName.iv", CompareOperator.Equals, JsonValue.Create("ABC")) - }); + [Fact] + public async Task Should_convert_json_query_and_enrich_with_defaults() + { + var query = Q.Empty.WithJsonQuery( + new Query<JsonValue> + { + Filter = new CompareFilter<JsonValue>("data.firstName.iv", CompareOperator.Equals, JsonValue.Create("ABC")) + }); - var q = await sut.ParseAsync(requestContext, query, schema); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - } + Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index 36fc5208b2..6f31e410f9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -20,338 +20,337 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ContentQueryServiceTests { - public class ContentQueryServiceTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>(); + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly IContentLoader contentVersionLoader = A.Fake<IContentLoader>(); + private readonly ISchemaEntity schema; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly ContentData contentData = new ContentData(); + private readonly ContentQueryParser queryParser = A.Fake<ContentQueryParser>(); + private readonly ContentQueryService sut; + + public ContentQueryServiceTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>(); - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly IContentLoader contentVersionLoader = A.Fake<IContentLoader>(); - private readonly ISchemaEntity schema; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly ContentData contentData = new ContentData(); - private readonly ContentQueryParser queryParser = A.Fake<ContentQueryParser>(); - private readonly ContentQueryService sut; - - public ContentQueryServiceTests() - { - ct = cts.Token; + ct = cts.Token; - var schemaDef = - new Schema(schemaId.Name) - .Publish() - .SetScripts(new SchemaScripts { Query = "<query-script>" }); + var schemaDef = + new Schema(schemaId.Name) + .Publish() + .SetScripts(new SchemaScripts { Query = "<query-script>" }); - schema = Mocks.Schema(appId, schemaId, schemaDef); + schema = Mocks.Schema(appId, schemaId, schemaDef); - SetupEnricher(); + SetupEnricher(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, A<bool>._, ct)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, A<bool>._, ct)) + .Returns(schema); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, ct)) - .Returns(new List<ISchemaEntity> { schema }); + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, ct)) + .Returns(new List<ISchemaEntity> { schema }); - A.CallTo(() => queryParser.ParseAsync(A<Context>._, A<Q>._, A<ISchemaEntity?>._)) - .ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!)); + A.CallTo(() => queryParser.ParseAsync(A<Context>._, A<Q>._, A<ISchemaEntity?>._)) + .ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!)); - var options = Options.Create(new ContentOptions()); + var options = Options.Create(new ContentOptions()); - sut = new ContentQueryService( - appProvider, - contentEnricher, - contentRepository, - contentVersionLoader, - options, - queryParser); - } + sut = new ContentQueryService( + appProvider, + contentEnricher, + contentRepository, + contentVersionLoader, + options, + queryParser); + } - [Fact] - public async Task Should_get_schema_from_guid_string() - { - var input = schemaId.Id.ToString(); + [Fact] + public async Task Should_get_schema_from_guid_string() + { + var input = schemaId.Id.ToString(); - var requestContext = CreateContext(); + var requestContext = CreateContext(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true, ct)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true, ct)) + .Returns(schema); - var actual = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); + var actual = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); - Assert.Equal(schema, actual); - } + Assert.Equal(schema, actual); + } - [Fact] - public async Task Should_get_schema_from_name() - { - var input = schemaId.Name; + [Fact] + public async Task Should_get_schema_from_name() + { + var input = schemaId.Name; - var requestContext = CreateContext(); + var requestContext = CreateContext(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true, ct)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true, ct)) + .Returns(schema); - var actual = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); + var actual = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); - Assert.Equal(schema, actual); - } - - [Fact] - public async Task Should_throw_notfound_exception_if_schema_to_get_not_found() - { - var requestContext = CreateContext(); + Assert.Equal(schema, actual); + } - A.CallTo(() => appProvider.GetSchemaAsync(A<DomainId>._, A<string>._, true, ct)) - .Returns((ISchemaEntity?)null); + [Fact] + public async Task Should_throw_notfound_exception_if_schema_to_get_not_found() + { + var requestContext = CreateContext(); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name, ct)); - } + A.CallTo(() => appProvider.GetSchemaAsync(A<DomainId>._, A<string>._, true, ct)) + .Returns((ISchemaEntity?)null); - [Fact] - public async Task Should_throw_permission_exception_if_content_to_find_is_restricted() - { - var requestContext = CreateContext(allowSchema: false); + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name, ct)); + } - var content = CreateContent(DomainId.NewGuid()); + [Fact] + public async Task Should_throw_permission_exception_if_content_to_find_is_restricted() + { + var requestContext = CreateContext(allowSchema: false); - A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A<SearchScope>._, A<CancellationToken>._)) - .Returns(CreateContent(DomainId.NewGuid())); + var content = CreateContent(DomainId.NewGuid()); - await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct)); - } + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A<SearchScope>._, A<CancellationToken>._)) + .Returns(CreateContent(DomainId.NewGuid())); - [Fact] - public async Task Should_return_null_if_content_by_id_dannot_be_found() - { - var requestContext = CreateContext(); + await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct)); + } - var content = CreateContent(DomainId.NewGuid()); + [Fact] + public async Task Should_return_null_if_content_by_id_dannot_be_found() + { + var requestContext = CreateContext(); - A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A<SearchScope>._, A<CancellationToken>._)) - .Returns<IContentEntity?>(null); + var content = CreateContent(DomainId.NewGuid()); - var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A<SearchScope>._, A<CancellationToken>._)) + .Returns<IContentEntity?>(null); - Assert.Null(actual); - } + var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); - [Fact] - public async Task Should_return_content_by_special_id() - { - var requestContext = CreateContext(); + Assert.Null(actual); + } - var content = CreateContent(DomainId.NewGuid()); + [Fact] + public async Task Should_return_content_by_special_id() + { + var requestContext = CreateContext(); - A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, schema.Id, SearchScope.Published, A<CancellationToken>._)) - .Returns(content); + var content = CreateContent(DomainId.NewGuid()); - var actual = await sut.FindAsync(requestContext, schemaId.Name, DomainId.Create("_schemaId_"), ct: ct); + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, schema.Id, SearchScope.Published, A<CancellationToken>._)) + .Returns(content); - AssertContent(content, actual); - } + var actual = await sut.FindAsync(requestContext, schemaId.Name, DomainId.Create("_schemaId_"), ct: ct); - [Theory] - [InlineData(1, 0, SearchScope.All)] - [InlineData(1, 1, SearchScope.All)] - [InlineData(0, 1, SearchScope.All)] - [InlineData(0, 0, SearchScope.Published)] - public async Task Should_return_content_by_id(int isFrontend, int unpublished, SearchScope scope) - { - var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); + AssertContent(content, actual); + } - var content = CreateContent(DomainId.NewGuid()); + [Theory] + [InlineData(1, 0, SearchScope.All)] + [InlineData(1, 1, SearchScope.All)] + [InlineData(0, 1, SearchScope.All)] + [InlineData(0, 0, SearchScope.Published)] + public async Task Should_return_content_by_id(int isFrontend, int unpublished, SearchScope scope) + { + var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); - A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, scope, A<CancellationToken>._)) - .Returns(content); + var content = CreateContent(DomainId.NewGuid()); - var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, scope, A<CancellationToken>._)) + .Returns(content); - AssertContent(content, actual); - } + var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); - [Fact] - public async Task Should_return_content_by_id_and_version() - { - var requestContext = CreateContext(); + AssertContent(content, actual); + } - var content = CreateContent(DomainId.NewGuid()); + [Fact] + public async Task Should_return_content_by_id_and_version() + { + var requestContext = CreateContext(); - A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, content.Id, 13, A<CancellationToken>._)) - .Returns(content); + var content = CreateContent(DomainId.NewGuid()); - var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13, ct); + A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, content.Id, 13, A<CancellationToken>._)) + .Returns(content); - AssertContent(content, actual); - } + var actual = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13, ct); - [Fact] - public async Task Should_throw_exception_if_user_has_no_permission_to_query_content() - { - var requestContext = CreateContext(allowSchema: false); + AssertContent(content, actual); + } - await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct)); - } + [Fact] + public async Task Should_throw_exception_if_user_has_no_permission_to_query_content() + { + var requestContext = CreateContext(allowSchema: false); - [Theory] - [InlineData(1, 0, SearchScope.All)] - [InlineData(1, 1, SearchScope.All)] - [InlineData(0, 1, SearchScope.All)] - [InlineData(0, 0, SearchScope.Published)] - public async Task Should_query_contents(int isFrontend, int unpublished, SearchScope scope) - { - var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); + await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct)); + } - var content1 = CreateContent(DomainId.NewGuid()); - var content2 = CreateContent(DomainId.NewGuid()); + [Theory] + [InlineData(1, 0, SearchScope.All)] + [InlineData(1, 1, SearchScope.All)] + [InlineData(0, 1, SearchScope.All)] + [InlineData(0, 0, SearchScope.Published)] + public async Task Should_query_contents(int isFrontend, int unpublished, SearchScope scope) + { + var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); - var q = Q.Empty.WithReference(DomainId.NewGuid()); + var content1 = CreateContent(DomainId.NewGuid()); + var content2 = CreateContent(DomainId.NewGuid()); - A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, q, scope, A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(5, content1, content2)); + var q = Q.Empty.WithReference(DomainId.NewGuid()); - var actual = await sut.QueryAsync(requestContext, schemaId.Name, q, ct); + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, q, scope, A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(5, content1, content2)); - Assert.Equal(5, actual.Total); + var actual = await sut.QueryAsync(requestContext, schemaId.Name, q, ct); - AssertContent(content1, actual[0]); - AssertContent(content2, actual[1]); - } + Assert.Equal(5, actual.Total); - [Theory] - [InlineData(1, 0, SearchScope.All)] - [InlineData(1, 1, SearchScope.All)] - [InlineData(0, 1, SearchScope.All)] - [InlineData(0, 0, SearchScope.Published)] - public async Task Should_query_contents_by_ids(int isFrontend, int unpublished, SearchScope scope) - { - var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); + AssertContent(content1, actual[0]); + AssertContent(content2, actual[1]); + } - var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); + [Theory] + [InlineData(1, 0, SearchScope.All)] + [InlineData(1, 1, SearchScope.All)] + [InlineData(0, 1, SearchScope.All)] + [InlineData(0, 0, SearchScope.Published)] + public async Task Should_query_contents_by_ids(int isFrontend, int unpublished, SearchScope scope) + { + var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); - var contents = ids.Select(CreateContent).ToList(); + var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); - var q = Q.Empty.WithIds(ids); + var contents = ids.Select(CreateContent).ToList(); - A.CallTo(() => contentRepository.QueryAsync(requestContext.App, - A<List<ISchemaEntity>>.That.Matches(x => x.Count == 1), q, scope, - A<CancellationToken>._)) - .Returns(ResultList.Create(5, contents)); + var q = Q.Empty.WithIds(ids); - var actual = await sut.QueryAsync(requestContext, q, ct); + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, + A<List<ISchemaEntity>>.That.Matches(x => x.Count == 1), q, scope, + A<CancellationToken>._)) + .Returns(ResultList.Create(5, contents)); - Assert.Equal(5, actual.Total); + var actual = await sut.QueryAsync(requestContext, q, ct); - for (var i = 0; i < contents.Count; i++) - { - AssertContent(contents[i], actual[i]); - } - } + Assert.Equal(5, actual.Total); - [Fact] - public async Task Should_query_contents_with_matching_permissions() + for (var i = 0; i < contents.Count; i++) { - var requestContext = CreateContext(allowSchema: false); - - var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); - - var q = Q.Empty.WithIds(ids); - - A.CallTo(() => contentRepository.QueryAsync(requestContext.App, - A<List<ISchemaEntity>>.That.Matches(x => x.Count == 0), q, SearchScope.All, - A<CancellationToken>._)) - .Returns(ResultList.Create(0, ids.Select(CreateContent))); + AssertContent(contents[i], actual[i]); + } + } - var actual = await sut.QueryAsync(requestContext, q, ct); + [Fact] + public async Task Should_query_contents_with_matching_permissions() + { + var requestContext = CreateContext(allowSchema: false); - Assert.Empty(actual); - } + var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); - [Fact] - public async Task Should_query_contents_from_user_if_user_has_only_own_permission() - { - var requestContext = CreateContext(permissionId: PermissionIds.AppContentsReadOwn); + var q = Q.Empty.WithIds(ids); - await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct); + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, + A<List<ISchemaEntity>>.That.Matches(x => x.Count == 0), q, SearchScope.All, + A<CancellationToken>._)) + .Returns(ResultList.Create(0, ids.Select(CreateContent))); - A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, - A<Q>.That.Matches(x => Equals(x.CreatedBy, requestContext.UserPrincipal.Token())), SearchScope.Published, A - <CancellationToken>._)) - .MustHaveHappened(); - } + var actual = await sut.QueryAsync(requestContext, q, ct); - [Fact] - public async Task Should_query_all_contents_if_user_has_read_permission() - { - var requestContext = CreateContext(permissionId: PermissionIds.AppContentsRead); + Assert.Empty(actual); + } - await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct); + [Fact] + public async Task Should_query_contents_from_user_if_user_has_only_own_permission() + { + var requestContext = CreateContext(permissionId: PermissionIds.AppContentsReadOwn); - A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, - A<Q>.That.Matches(x => x.CreatedBy == null), SearchScope.Published, - A<CancellationToken>._)) - .MustHaveHappened(); - } + await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct); - private void SetupEnricher() - { - A.CallTo(() => contentEnricher.EnrichAsync(A<IEnumerable<IContentEntity>>._, A<Context>._, ct)) - .ReturnsLazily(x => - { - var input = x.GetArgument<IEnumerable<IContentEntity>>(0)!; + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, + A<Q>.That.Matches(x => Equals(x.CreatedBy, requestContext.UserPrincipal.Token())), SearchScope.Published, A + <CancellationToken>._)) + .MustHaveHappened(); + } - return Task.FromResult<IReadOnlyList<IEnrichedContentEntity>>(input.Select(c => SimpleMapper.Map(c, new ContentEntity())).ToList()); - }); - } + [Fact] + public async Task Should_query_all_contents_if_user_has_read_permission() + { + var requestContext = CreateContext(permissionId: PermissionIds.AppContentsRead); - private Context CreateContext( - int isFrontend = 0, - int isUnpublished = 0, - bool allowSchema = true, - string permissionId = PermissionIds.AppContentsRead) - { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct); - claimsIdentity.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, + A<Q>.That.Matches(x => x.CreatedBy == null), SearchScope.Published, + A<CancellationToken>._)) + .MustHaveHappened(); + } - if (isFrontend == 1) + private void SetupEnricher() + { + A.CallTo(() => contentEnricher.EnrichAsync(A<IEnumerable<IContentEntity>>._, A<Context>._, ct)) + .ReturnsLazily(x => { - claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); - } + var input = x.GetArgument<IEnumerable<IContentEntity>>(0)!; - if (allowSchema) - { - var concretePermission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name).Id; + return Task.FromResult<IReadOnlyList<IEnrichedContentEntity>>(input.Select(c => SimpleMapper.Map(c, new ContentEntity())).ToList()); + }); + } - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, concretePermission)); - } + private Context CreateContext( + int isFrontend = 0, + int isUnpublished = 0, + bool allowSchema = true, + string permissionId = PermissionIds.AppContentsRead) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - return new Context(claimsPrincipal, Mocks.App(appId)).Clone(b => b.WithUnpublished(isUnpublished == 1)); - } + claimsIdentity.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - private static void AssertContent(IContentEntity source, IEnrichedContentEntity? actual) + if (isFrontend == 1) { - Assert.NotNull(actual); - Assert.NotSame(source, actual); - Assert.Same(source.Data, actual?.Data); - Assert.Equal(source.Id, actual?.Id); + claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); } - private IContentEntity CreateContent(DomainId id) + if (allowSchema) { - var content = new ContentEntity - { - Id = id, - Data = contentData, - SchemaId = schemaId, - Status = Status.Published - }; + var concretePermission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name).Id; - return content; + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, concretePermission)); } + + return new Context(claimsPrincipal, Mocks.App(appId)).Clone(b => b.WithUnpublished(isUnpublished == 1)); + } + + private static void AssertContent(IContentEntity source, IEnrichedContentEntity? actual) + { + Assert.NotNull(actual); + Assert.NotSame(source, actual); + Assert.Same(source.Data, actual?.Data); + Assert.Equal(source.Id, actual?.Id); + } + + private IContentEntity CreateContent(DomainId id) + { + var content = new ContentEntity + { + Id = id, + Data = contentData, + SchemaId = schemaId, + Status = Status.Published + }; + + return content; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs index 146a8fb2cb..6760f88058 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs @@ -20,147 +20,146 @@ using Xunit; using TestUtils = Squidex.Domain.Apps.Core.TestHelpers.TestUtils; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ConvertDataTests { - public class ConvertDataTests + private readonly ISchemaEntity schema; + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); + private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly ConvertData sut; + + public ConvertDataTests() { - private readonly ISchemaEntity schema; - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); - private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly ProvideSchema schemaProvider; - private readonly ConvertData sut; - - public ConvertDataTests() - { - var schemaDef = - new Schema("my-schema") - .AddReferences(1, "references", Partitioning.Invariant) - .AddAssets(2, "assets", Partitioning.Invariant) - .AddArray(3, "array", Partitioning.Invariant, a => a - .AddAssets(31, "nested")); - - schema = Mocks.Schema(appId, schemaId, schemaDef); - schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); - - sut = new ConvertData(urlGenerator, TestUtils.DefaultSerializer, assetRepository, contentRepository); - } - - [Fact] - public async Task Should_convert_data_and_data_draft_if_frontend_user() - { - var content = CreateContent(new ContentData()); - - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - - await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); + var schemaDef = + new Schema("my-schema") + .AddReferences(1, "references", Partitioning.Invariant) + .AddAssets(2, "assets", Partitioning.Invariant) + .AddArray(3, "array", Partitioning.Invariant, a => a + .AddAssets(31, "nested")); - Assert.NotNull(content.Data); - } - - [Fact] - public async Task Should_cleanup_references() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + schema = Mocks.Schema(appId, schemaId, schemaDef); + schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); - var source = BuildTestData(id1, id2); + sut = new ConvertData(urlGenerator, TestUtils.DefaultSerializer, assetRepository, contentRepository); + } - var content = CreateContent(source); + [Fact] + public async Task Should_convert_data_and_data_draft_if_frontend_user() + { + var content = CreateContent(new ContentData()); - var expected = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(id2))) - .AddField("assets", - new ContentFieldData() - .AddInvariant(JsonValue.Array())) - .AddField("array", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("nested", JsonValue.Array(id2))))); + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - A.CallTo(() => assetRepository.QueryIdsAsync(appId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), A<CancellationToken>._)) - .Returns(new List<DomainId> { id2 }); + await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); - A.CallTo(() => contentRepository.QueryIdsAsync(appId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), SearchScope.All, A<CancellationToken>._)) - .Returns(new List<ContentIdStatus> { new ContentIdStatus(id2, id2, Status.Published) }); + Assert.NotNull(content.Data); + } - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + [Fact] + public async Task Should_cleanup_references() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); + var source = BuildTestData(id1, id2); - Assert.Equal(expected, content.Data); - } + var content = CreateContent(source); - [Fact] - public async Task Should_cleanup_references_if_everything_deleted() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + var expected = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(id2))) + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array())) + .AddField("array", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject() + .Add("nested", JsonValue.Array(id2))))); - var source = BuildTestData(id1, id2); + A.CallTo(() => assetRepository.QueryIdsAsync(appId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), A<CancellationToken>._)) + .Returns(new List<DomainId> { id2 }); - var content = CreateContent(source); + A.CallTo(() => contentRepository.QueryIdsAsync(appId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), SearchScope.All, A<CancellationToken>._)) + .Returns(new List<ContentIdStatus> { new ContentIdStatus(id2, id2, Status.Published) }); - var expected = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array())) - .AddField("assets", - new ContentFieldData() - .AddInvariant(JsonValue.Array())) - .AddField("array", - new ContentFieldData() - .AddInvariant( - JsonValue.Array( - new JsonObject() - .Add("nested", JsonValue.Array())))); + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - A.CallTo(() => assetRepository.QueryIdsAsync(appId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), A<CancellationToken>._)) - .Returns(new List<DomainId>()); + await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); - A.CallTo(() => contentRepository.QueryIdsAsync(appId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), SearchScope.All, A<CancellationToken>._)) - .Returns(new List<ContentIdStatus>()); + Assert.Equal(expected, content.Data); + } - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + [Fact] + public async Task Should_cleanup_references_if_everything_deleted() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); + var source = BuildTestData(id1, id2); - Assert.Equal(expected, content.Data); - } + var content = CreateContent(source); - private static ContentData BuildTestData(DomainId id1, DomainId id2) - { - return new ContentData() + var expected = + new ContentData() .AddField("references", new ContentFieldData() - .AddInvariant(JsonValue.Array(id1, id2))) + .AddInvariant(JsonValue.Array())) .AddField("assets", new ContentFieldData() - .AddInvariant(JsonValue.Array(id1))) + .AddInvariant(JsonValue.Array())) .AddField("array", new ContentFieldData() .AddInvariant( JsonValue.Array( new JsonObject() - .Add("nested", JsonValue.Array(id1, id2))))); - } + .Add("nested", JsonValue.Array())))); + + A.CallTo(() => assetRepository.QueryIdsAsync(appId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), A<CancellationToken>._)) + .Returns(new List<DomainId>()); - private ContentEntity CreateContent(ContentData data) + A.CallTo(() => contentRepository.QueryIdsAsync(appId.Id, A<HashSet<DomainId>>.That.Is(id1, id2), SearchScope.All, A<CancellationToken>._)) + .Returns(new List<ContentIdStatus>()); + + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + + await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); + + Assert.Equal(expected, content.Data); + } + + private static ContentData BuildTestData(DomainId id1, DomainId id2) + { + return new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(id1, id2))) + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(id1))) + .AddField("array", + new ContentFieldData() + .AddInvariant( + JsonValue.Array( + new JsonObject() + .Add("nested", JsonValue.Array(id1, id2))))); + } + + private ContentEntity CreateContent(ContentData data) + { + return new ContentEntity { - return new ContentEntity - { - Data = data, - SchemaId = schemaId, - Status = Status.Published - }; - } + Data = data, + SchemaId = schemaId, + Status = Status.Published + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs index 14a3f1f7ca..d7638a6b9c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs @@ -14,71 +14,70 @@ using Squidex.Infrastructure.Caching; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class EnrichForCachingTests { - public class EnrichForCachingTests + private readonly ISchemaEntity schema; + private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); + private readonly Context requestContext; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly EnrichForCaching sut; + + public EnrichForCachingTests() { - private readonly ISchemaEntity schema; - private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); - private readonly Context requestContext; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly ProvideSchema schemaProvider; - private readonly EnrichForCaching sut; - - public EnrichForCachingTests() - { - requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + schema = Mocks.Schema(appId, schemaId); + schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); - schema = Mocks.Schema(appId, schemaId); - schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); + sut = new EnrichForCaching(requestCache); + } + + [Fact] + public async Task Should_add_cache_headers() + { + var headers = new List<string>(); - sut = new EnrichForCaching(requestCache); - } + A.CallTo(() => requestCache.AddHeader(A<string>._)) + .Invokes(new Action<string>(header => headers.Add(header))); - [Fact] - public async Task Should_add_cache_headers() - { - var headers = new List<string>(); - - A.CallTo(() => requestCache.AddHeader(A<string>._)) - .Invokes(new Action<string>(header => headers.Add(header))); - - await sut.EnrichAsync(requestContext, default); - - Assert.Equal(new List<string> - { - "X-Flatten", - "X-Languages", - "X-NoCleanup", - "X-NoEnrichment", - "X-NoResolveLanguages", - "X-ResolveFlow", - "X-Resolve-Urls", - "X-Unpublished" - }, headers); - } - - [Fact] - public async Task Should_add_app_version_and_schema_as_dependency() + await sut.EnrichAsync(requestContext, default); + + Assert.Equal(new List<string> { - var content = CreateContent(); + "X-Flatten", + "X-Languages", + "X-NoCleanup", + "X-NoEnrichment", + "X-NoResolveLanguages", + "X-ResolveFlow", + "X-Resolve-Urls", + "X-Unpublished" + }, headers); + } - await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider, default); + [Fact] + public async Task Should_add_app_version_and_schema_as_dependency() + { + var content = CreateContent(); - A.CallTo(() => requestCache.AddDependency(content.UniqueId, content.Version)) - .MustHaveHappened(); + await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider, default); - A.CallTo(() => requestCache.AddDependency(schema.UniqueId, schema.Version)) - .MustHaveHappened(); + A.CallTo(() => requestCache.AddDependency(content.UniqueId, content.Version)) + .MustHaveHappened(); - A.CallTo(() => requestCache.AddDependency(requestContext.App.UniqueId, requestContext.App.Version)) - .MustHaveHappened(); - } + A.CallTo(() => requestCache.AddDependency(schema.UniqueId, schema.Version)) + .MustHaveHappened(); - private ContentEntity CreateContent() - { - return new ContentEntity { AppId = appId, Id = DomainId.NewGuid(), SchemaId = schemaId, Version = 13 }; - } + A.CallTo(() => requestCache.AddDependency(requestContext.App.UniqueId, requestContext.App.Version)) + .MustHaveHappened(); + } + + private ContentEntity CreateContent() + { + return new ContentEntity { AppId = appId, Id = DomainId.NewGuid(), SchemaId = schemaId, Version = 13 }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs index 0707d2b323..2402ee39e7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs @@ -12,63 +12,62 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class EnrichWithSchemaTests { - public class EnrichWithSchemaTests - { - private readonly ISchemaEntity schema; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly ProvideSchema schemaProvider; - private readonly EnrichWithSchema sut; + private readonly ISchemaEntity schema; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly EnrichWithSchema sut; - public EnrichWithSchemaTests() - { - schema = Mocks.Schema(appId, schemaId); - schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); + public EnrichWithSchemaTests() + { + schema = Mocks.Schema(appId, schemaId); + schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); - sut = new EnrichWithSchema(); - } + sut = new EnrichWithSchema(); + } - [Fact] - public async Task Should_enrich_with_reference_fields() - { - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + [Fact] + public async Task Should_enrich_with_reference_fields() + { + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - var content = CreateContent(); + var content = CreateContent(); - await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); + await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); - Assert.NotNull(content.ReferenceFields); - } + Assert.NotNull(content.ReferenceFields); + } - [Fact] - public async Task Should_not_enrich_with_reference_fields_if_not_frontend() - { - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + [Fact] + public async Task Should_not_enrich_with_reference_fields_if_not_frontend() + { + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); - var source = CreateContent(); + var source = CreateContent(); - await sut.EnrichAsync(ctx, Enumerable.Repeat(source, 1), schemaProvider, default); + await sut.EnrichAsync(ctx, Enumerable.Repeat(source, 1), schemaProvider, default); - Assert.Null(source.ReferenceFields); - } + Assert.Null(source.ReferenceFields); + } - [Fact] - public async Task Should_enrich_with_schema_names() - { - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + [Fact] + public async Task Should_enrich_with_schema_names() + { + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); - var content = CreateContent(); + var content = CreateContent(); - await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); + await sut.EnrichAsync(ctx, Enumerable.Repeat(content, 1), schemaProvider, default); - Assert.Equal("my-schema", content.SchemaDisplayName); - } + Assert.Equal("my-schema", content.SchemaDisplayName); + } - private ContentEntity CreateContent() - { - return new ContentEntity { SchemaId = schemaId }; - } + private ContentEntity CreateContent() + { + return new ContentEntity { SchemaId = schemaId }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs index 1d91ed6c98..118165a8d5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs @@ -14,150 +14,149 @@ #pragma warning disable CA2012 // Use ValueTasks correctly -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class EnrichWithWorkflowsTests { - public class EnrichWithWorkflowsTests + private readonly IContentWorkflow workflow = A.Fake<IContentWorkflow>(); + private readonly Context requestContext; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly RefToken user = RefToken.User("me"); + private readonly EnrichWithWorkflows sut; + + public EnrichWithWorkflowsTests() { - private readonly IContentWorkflow workflow = A.Fake<IContentWorkflow>(); - private readonly Context requestContext; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly RefToken user = RefToken.User("me"); - private readonly EnrichWithWorkflows sut; - - public EnrichWithWorkflowsTests() - { - requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - sut = new EnrichWithWorkflows(workflow); - } + sut = new EnrichWithWorkflows(workflow); + } - [Fact] - public async Task Should_enrich_content_with_next_statuses() - { - var content = new ContentEntity { SchemaId = schemaId }; + [Fact] + public async Task Should_enrich_content_with_next_statuses() + { + var content = new ContentEntity { SchemaId = schemaId }; - var nexts = new[] - { - new StatusInfo(Status.Published, StatusColors.Published) - }; + var nexts = new[] + { + new StatusInfo(Status.Published, StatusColors.Published) + }; - A.CallTo(() => workflow.GetNextAsync(content, content.Status, requestContext.UserPrincipal)) - .Returns(nexts); + A.CallTo(() => workflow.GetNextAsync(content, content.Status, requestContext.UserPrincipal)) + .Returns(nexts); - await sut.EnrichAsync(requestContext, new[] { content }, null!, default); + await sut.EnrichAsync(requestContext, new[] { content }, null!, default); - Assert.Equal(nexts, content.NextStatuses); - } + Assert.Equal(nexts, content.NextStatuses); + } - [Fact] - public async Task Should_enrich_content_with_next_statuses_if_draft_singleton() - { - var content = new ContentEntity { SchemaId = schemaId, IsSingleton = true, Status = Status.Draft }; + [Fact] + public async Task Should_enrich_content_with_next_statuses_if_draft_singleton() + { + var content = new ContentEntity { SchemaId = schemaId, IsSingleton = true, Status = Status.Draft }; - await sut.EnrichAsync(requestContext, new[] { content }, null!, default); + await sut.EnrichAsync(requestContext, new[] { content }, null!, default); - Assert.Equal(Status.Published, content.NextStatuses?.Single().Status); + Assert.Equal(Status.Published, content.NextStatuses?.Single().Status); - A.CallTo(() => workflow.GetNextAsync(content, A<Status>._, requestContext.UserPrincipal)) - .MustNotHaveHappened(); - } + A.CallTo(() => workflow.GetNextAsync(content, A<Status>._, requestContext.UserPrincipal)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_enrich_content_with_next_statuses_if_published_singleton() - { - var content = new ContentEntity { SchemaId = schemaId, IsSingleton = true, Status = Status.Published }; + [Fact] + public async Task Should_enrich_content_with_next_statuses_if_published_singleton() + { + var content = new ContentEntity { SchemaId = schemaId, IsSingleton = true, Status = Status.Published }; - await sut.EnrichAsync(requestContext, new[] { content }, null!, default); + await sut.EnrichAsync(requestContext, new[] { content }, null!, default); - Assert.Empty(content.NextStatuses); + Assert.Empty(content.NextStatuses); - A.CallTo(() => workflow.GetNextAsync(content, A<Status>._, requestContext.UserPrincipal)) - .MustNotHaveHappened(); - } + A.CallTo(() => workflow.GetNextAsync(content, A<Status>._, requestContext.UserPrincipal)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_enrich_content_with_status_color() - { - var content = new ContentEntity { SchemaId = schemaId }; + [Fact] + public async Task Should_enrich_content_with_status_color() + { + var content = new ContentEntity { SchemaId = schemaId }; - A.CallTo(() => workflow.GetInfoAsync(content, content.Status)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + A.CallTo(() => workflow.GetInfoAsync(content, content.Status)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - await sut.EnrichAsync(requestContext, new[] { content }, null!, default); + await sut.EnrichAsync(requestContext, new[] { content }, null!, default); - Assert.Equal(StatusColors.Published, content.StatusColor); - } + Assert.Equal(StatusColors.Published, content.StatusColor); + } - [Fact] - public async Task Should_enrich_content_with_new_status_color() - { - var content = new ContentEntity { SchemaId = schemaId, NewStatus = Status.Archived }; + [Fact] + public async Task Should_enrich_content_with_new_status_color() + { + var content = new ContentEntity { SchemaId = schemaId, NewStatus = Status.Archived }; - A.CallTo(() => workflow.GetInfoAsync(content, content.NewStatus.Value)) - .Returns(new StatusInfo(Status.Published, StatusColors.Archived)); + A.CallTo(() => workflow.GetInfoAsync(content, content.NewStatus.Value)) + .Returns(new StatusInfo(Status.Published, StatusColors.Archived)); - await sut.EnrichAsync(requestContext, new[] { content }, null!, default); + await sut.EnrichAsync(requestContext, new[] { content }, null!, default); - Assert.Equal(StatusColors.Archived, content.NewStatusColor); - } + Assert.Equal(StatusColors.Archived, content.NewStatusColor); + } - [Fact] - public async Task Should_enrich_content_with_scheduled_status_color() - { - var content = new ContentEntity { SchemaId = schemaId, ScheduleJob = ScheduleJob.Build(Status.Archived, user, default) }; + [Fact] + public async Task Should_enrich_content_with_scheduled_status_color() + { + var content = new ContentEntity { SchemaId = schemaId, ScheduleJob = ScheduleJob.Build(Status.Archived, user, default) }; - A.CallTo(() => workflow.GetInfoAsync(content, content.ScheduleJob.Status)) - .Returns(new StatusInfo(Status.Published, StatusColors.Archived)); + A.CallTo(() => workflow.GetInfoAsync(content, content.ScheduleJob.Status)) + .Returns(new StatusInfo(Status.Published, StatusColors.Archived)); - await sut.EnrichAsync(requestContext, new[] { content }, null!, default); + await sut.EnrichAsync(requestContext, new[] { content }, null!, default); - Assert.Equal(StatusColors.Archived, content.ScheduledStatusColor); - } + Assert.Equal(StatusColors.Archived, content.ScheduledStatusColor); + } - [Fact] - public async Task Should_enrich_content_with_default_color_if_not_found() - { - var content = new ContentEntity { SchemaId = schemaId }; + [Fact] + public async Task Should_enrich_content_with_default_color_if_not_found() + { + var content = new ContentEntity { SchemaId = schemaId }; - A.CallTo(() => workflow.GetInfoAsync(content, content.Status)) - .Returns(ValueTask.FromResult<StatusInfo?>(null!)); + A.CallTo(() => workflow.GetInfoAsync(content, content.Status)) + .Returns(ValueTask.FromResult<StatusInfo?>(null!)); - var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); + var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); - await sut.EnrichAsync(ctx, new[] { content }, null!, default); + await sut.EnrichAsync(ctx, new[] { content }, null!, default); - Assert.Equal(StatusColors.Draft, content.StatusColor); - } + Assert.Equal(StatusColors.Draft, content.StatusColor); + } - [Fact] - public async Task Should_enrich_content_with_can_update() - { - var content = new ContentEntity { SchemaId = schemaId }; + [Fact] + public async Task Should_enrich_content_with_can_update() + { + var content = new ContentEntity { SchemaId = schemaId }; - A.CallTo(() => workflow.CanUpdateAsync(content, content.Status, requestContext.UserPrincipal)) - .Returns(true); + A.CallTo(() => workflow.CanUpdateAsync(content, content.Status, requestContext.UserPrincipal)) + .Returns(true); - var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); + var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); - await sut.EnrichAsync(ctx, new[] { content }, null!, default); + await sut.EnrichAsync(ctx, new[] { content }, null!, default); - Assert.True(content.CanUpdate); - } + Assert.True(content.CanUpdate); + } - [Fact] - public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() - { - var content = new ContentEntity { SchemaId = schemaId }; + [Fact] + public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() + { + var content = new ContentEntity { SchemaId = schemaId }; - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)).Clone(b => b.WithResolveFlow(false)); + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)).Clone(b => b.WithResolveFlow(false)); - await sut.EnrichAsync(ctx, new[] { content }, null!, default); + await sut.EnrichAsync(ctx, new[] { content }, null!, default); - Assert.False(content.CanUpdate); + Assert.False(content.CanUpdate); - A.CallTo(() => workflow.CanUpdateAsync(content, A<Status>._, requestContext.UserPrincipal)) - .MustNotHaveHappened(); - } + A.CallTo(() => workflow.CanUpdateAsync(content, A<Status>._, requestContext.UserPrincipal)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs index 9c8989bc90..1443a876c8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs @@ -18,232 +18,231 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ResolveAssetsTests { - public class ResolveAssetsTests + private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly Context requestContext; + private readonly ResolveAssets sut; + + public ResolveAssetsTests() { - private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly ProvideSchema schemaProvider; - private readonly Context requestContext; - private readonly ResolveAssets sut; - - public ResolveAssetsTests() - { - requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE)); - - var schemaDef = - new Schema(schemaId.Name) - .AddAssets(1, "asset1", Partitioning.Invariant, new AssetsFieldProperties - { - ResolveFirst = true, - MinItems = 2, - MaxItems = 3 - }) - .AddAssets(2, "asset2", Partitioning.Language, new AssetsFieldProperties - { - ResolveFirst = true, - MinItems = 1, - MaxItems = 1 - }) - .SetFieldsInLists("asset1", "asset2"); - - A.CallTo(() => urlGenerator.AssetContent(appId, A<string>._)) - .ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<string>(1)}"); - - schemaProvider = x => - { - if (x == schemaId.Id) + requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE)); + + var schemaDef = + new Schema(schemaId.Name) + .AddAssets(1, "asset1", Partitioning.Invariant, new AssetsFieldProperties { - return Task.FromResult((Mocks.Schema(appId, schemaId, schemaDef), ResolvedComponents.Empty)); - } - else + ResolveFirst = true, + MinItems = 2, + MaxItems = 3 + }) + .AddAssets(2, "asset2", Partitioning.Language, new AssetsFieldProperties { - throw new DomainObjectNotFoundException(x.ToString()); - } - }; + ResolveFirst = true, + MinItems = 1, + MaxItems = 1 + }) + .SetFieldsInLists("asset1", "asset2"); - sut = new ResolveAssets(urlGenerator, assetQuery, requestCache); - } + A.CallTo(() => urlGenerator.AssetContent(appId, A<string>._)) + .ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<string>(1)}"); - [Fact] - public async Task Should_add_assets_id_and_versions_as_dependency() + schemaProvider = x => { - var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.docx"); - var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.docx"); - - var contents = new[] + if (x == schemaId.Id) { - CreateContent( - new[] { doc1.Id }, - new[] { doc1.Id }), - CreateContent( - new[] { doc2.Id }, - new[] { doc2.Id }) - }; - - A.CallTo(() => assetQuery.QueryAsync( - A<Context>.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A<Q>.That.HasIds(doc1.Id, doc2.Id), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(4, doc1, doc2)); - - await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + return Task.FromResult((Mocks.Schema(appId, schemaId, schemaDef), ResolvedComponents.Empty)); + } + else + { + throw new DomainObjectNotFoundException(x.ToString()); + } + }; - A.CallTo(() => requestCache.AddDependency(doc1.UniqueId, doc1.Version)) - .MustHaveHappened(); + sut = new ResolveAssets(urlGenerator, assetQuery, requestCache); + } - A.CallTo(() => requestCache.AddDependency(doc2.UniqueId, doc2.Version)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_add_assets_id_and_versions_as_dependency() + { + var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.docx"); + var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.docx"); - [Fact] - public async Task Should_enrich_with_asset_urls() + var contents = new[] { - var img1 = CreateAsset(DomainId.NewGuid(), 1, AssetType.Image, "Image1.png"); - var img2 = CreateAsset(DomainId.NewGuid(), 2, AssetType.Unknown, "Image2.png", "image/svg+xml"); + CreateContent( + new[] { doc1.Id }, + new[] { doc1.Id }), + CreateContent( + new[] { doc2.Id }, + new[] { doc2.Id }) + }; - var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.png"); - var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.png", "image/svg+xml", 20_000); + A.CallTo(() => assetQuery.QueryAsync( + A<Context>.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A<Q>.That.HasIds(doc1.Id, doc2.Id), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(4, doc1, doc2)); - var contents = new[] - { - CreateContent( - new[] { img1.Id }, - new[] { img2.Id, img1.Id }), - CreateContent( - new[] { doc1.Id }, - new[] { doc2.Id, doc1.Id }) - }; + await sut.EnrichAsync(requestContext, contents, schemaProvider, default); - A.CallTo(() => assetQuery.QueryAsync( - A<Context>.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A<Q>.That.HasIds(doc1.Id, doc2.Id, img1.Id, img2.Id), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(4, img1, img2, doc1, doc2)); + A.CallTo(() => requestCache.AddDependency(doc1.UniqueId, doc1.Version)) + .MustHaveHappened(); - await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + A.CallTo(() => requestCache.AddDependency(doc2.UniqueId, doc2.Version)) + .MustHaveHappened(); + } - Assert.Equal( - new ContentData() - .AddField("asset1", - new ContentFieldData() - .AddLocalized("iv", JsonValue.Array($"url/to/{img1.Id}", img1.FileName))) - .AddField("asset2", - new ContentFieldData() - .AddLocalized("en", JsonValue.Array($"url/to/{img2.Id}", img2.FileName))), - contents[0].ReferenceData); + [Fact] + public async Task Should_enrich_with_asset_urls() + { + var img1 = CreateAsset(DomainId.NewGuid(), 1, AssetType.Image, "Image1.png"); + var img2 = CreateAsset(DomainId.NewGuid(), 2, AssetType.Unknown, "Image2.png", "image/svg+xml"); - Assert.Equal( - new ContentData() - .AddField("asset1", - new ContentFieldData() - .AddLocalized("iv", JsonValue.Array(doc1.FileName))) - .AddField("asset2", - new ContentFieldData() - .AddLocalized("en", JsonValue.Array(doc2.FileName))), - contents[1].ReferenceData); - } + var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.png"); + var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.png", "image/svg+xml", 20_000); - [Fact] - public async Task Should_not_enrich_references_if_not_api_user() + var contents = new[] { - var contents = new[] - { - CreateContent(new[] { DomainId.NewGuid() }, Array.Empty<DomainId>()) - }; + CreateContent( + new[] { img1.Id }, + new[] { img2.Id, img1.Id }), + CreateContent( + new[] { doc1.Id }, + new[] { doc2.Id, doc1.Id }) + }; + + A.CallTo(() => assetQuery.QueryAsync( + A<Context>.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A<Q>.That.HasIds(doc1.Id, doc2.Id, img1.Id, img2.Id), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(4, img1, img2, doc1, doc2)); + + await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + + Assert.Equal( + new ContentData() + .AddField("asset1", + new ContentFieldData() + .AddLocalized("iv", JsonValue.Array($"url/to/{img1.Id}", img1.FileName))) + .AddField("asset2", + new ContentFieldData() + .AddLocalized("en", JsonValue.Array($"url/to/{img2.Id}", img2.FileName))), + contents[0].ReferenceData); + + Assert.Equal( + new ContentData() + .AddField("asset1", + new ContentFieldData() + .AddLocalized("iv", JsonValue.Array(doc1.FileName))) + .AddField("asset2", + new ContentFieldData() + .AddLocalized("en", JsonValue.Array(doc2.FileName))), + contents[1].ReferenceData); + } - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + [Fact] + public async Task Should_not_enrich_references_if_not_api_user() + { + var contents = new[] + { + CreateContent(new[] { DomainId.NewGuid() }, Array.Empty<DomainId>()) + }; + + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); - await sut.EnrichAsync(ctx, contents, schemaProvider, default); + await sut.EnrichAsync(ctx, contents, schemaProvider, default); - Assert.Null(contents[0].ReferenceData); + Assert.Null(contents[0].ReferenceData); - A.CallTo(() => assetQuery.QueryAsync(A<Context>._, null, A<Q>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetQuery.QueryAsync(A<Context>._, null, A<Q>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_enrich_references_if_disabled() + [Fact] + public async Task Should_not_enrich_references_if_disabled() + { + var contents = new[] { - var contents = new[] - { - CreateContent(new[] { DomainId.NewGuid() }, Array.Empty<DomainId>()) - }; + CreateContent(new[] { DomainId.NewGuid() }, Array.Empty<DomainId>()) + }; - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).Clone(b => b.WithoutContentEnrichment(true)); + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).Clone(b => b.WithoutContentEnrichment(true)); - await sut.EnrichAsync(ctx, contents, schemaProvider, default); + await sut.EnrichAsync(ctx, contents, schemaProvider, default); - Assert.Null(contents[0].ReferenceData); + Assert.Null(contents[0].ReferenceData); - A.CallTo(() => assetQuery.QueryAsync(A<Context>._, null, A<Q>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetQuery.QueryAsync(A<Context>._, null, A<Q>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_invoke_query_service_if_no_assets_found() + [Fact] + public async Task Should_not_invoke_query_service_if_no_assets_found() + { + var contents = new[] { - var contents = new[] - { - CreateContent(Array.Empty<DomainId>(), Array.Empty<DomainId>()) - }; + CreateContent(Array.Empty<DomainId>(), Array.Empty<DomainId>()) + }; - await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + await sut.EnrichAsync(requestContext, contents, schemaProvider, default); - Assert.NotNull(contents[0].ReferenceData); + Assert.NotNull(contents[0].ReferenceData); - A.CallTo(() => assetQuery.QueryAsync(A<Context>._, null, A<Q>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => assetQuery.QueryAsync(A<Context>._, null, A<Q>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_only_query_first_assets() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + [Fact] + public async Task Should_only_query_first_assets() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - var contents = new[] - { - CreateContent(new[] { id1, id2 }, Array.Empty<DomainId>()) - }; + var contents = new[] + { + CreateContent(new[] { id1, id2 }, Array.Empty<DomainId>()) + }; - await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + await sut.EnrichAsync(requestContext, contents, schemaProvider, default); - Assert.NotNull(contents[0].ReferenceData); + Assert.NotNull(contents[0].ReferenceData); - A.CallTo(() => assetQuery.QueryAsync( - A<Context>.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A<Q>.That.HasIds(id1), A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => assetQuery.QueryAsync( + A<Context>.That.Matches(x => x.ShouldSkipAssetEnrichment() && x.ShouldSkipTotal()), null, A<Q>.That.HasIds(id1), A<CancellationToken>._)) + .MustHaveHappened(); + } - private ContentEntity CreateContent(DomainId[] assets1, DomainId[] assets2) + private ContentEntity CreateContent(DomainId[] assets1, DomainId[] assets2) + { + return new ContentEntity { - return new ContentEntity - { - Data = - new ContentData() - .AddField("asset1", - new ContentFieldData() - .AddLocalized("iv", JsonValue.Array(assets1.Select(x => x.ToString())))) - .AddField("asset2", - new ContentFieldData() - .AddLocalized("en", JsonValue.Array(assets2.Select(x => x.ToString())))), - SchemaId = schemaId - }; - } - - private IEnrichedAssetEntity CreateAsset(DomainId id, int version, AssetType type, string fileName, string? fileType = null, int fileSize = 100) + Data = + new ContentData() + .AddField("asset1", + new ContentFieldData() + .AddLocalized("iv", JsonValue.Array(assets1.Select(x => x.ToString())))) + .AddField("asset2", + new ContentFieldData() + .AddLocalized("en", JsonValue.Array(assets2.Select(x => x.ToString())))), + SchemaId = schemaId + }; + } + + private IEnrichedAssetEntity CreateAsset(DomainId id, int version, AssetType type, string fileName, string? fileType = null, int fileSize = 100) + { + return new AssetEntity { - return new AssetEntity - { - AppId = appId, - Id = id, - Type = type, - FileName = fileName, - FileSize = fileSize, - MimeType = fileType!, - Version = version - }; - } + AppId = appId, + Id = id, + Type = type, + FileName = fileName, + FileSize = fileSize, + MimeType = fileType!, + Version = version + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs index 179e4b2da2..7d0d382b10 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs @@ -17,306 +17,305 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ResolveReferencesTests : IClassFixture<TranslationsFixture> { - public class ResolveReferencesTests : IClassFixture<TranslationsFixture> + private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); + private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> refSchemaId1 = NamedId.Of(DomainId.NewGuid(), "my-ref1"); + private readonly NamedId<DomainId> refSchemaId2 = NamedId.Of(DomainId.NewGuid(), "my-ref2"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly Context requestContext; + private readonly ResolveReferences sut; + + public ResolveReferencesTests() { - private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); - private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> refSchemaId1 = NamedId.Of(DomainId.NewGuid(), "my-ref1"); - private readonly NamedId<DomainId> refSchemaId2 = NamedId.Of(DomainId.NewGuid(), "my-ref2"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly ProvideSchema schemaProvider; - private readonly Context requestContext; - private readonly ResolveReferences sut; - - public ResolveReferencesTests() - { - requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE)); - - var refSchemaDef = - new Schema("my-ref") - .AddString(1, "name", Partitioning.Invariant, - new StringFieldProperties()) - .AddNumber(2, "number", Partitioning.Invariant, - new NumberFieldProperties()) - .SetFieldsInReferences("name", "number"); - - var schemaDef = - new Schema(schemaId.Name) - .AddReferences(1, "ref1", Partitioning.Invariant, new ReferencesFieldProperties - { - ResolveReference = true, - MinItems = 1, - MaxItems = 1, - SchemaId = refSchemaId1.Id - }) - .AddReferences(2, "ref2", Partitioning.Invariant, new ReferencesFieldProperties - { - ResolveReference = true, - MinItems = 1, - MaxItems = 1, - SchemaId = refSchemaId2.Id - }) - .SetFieldsInLists("ref1", "ref2"); - - schemaProvider = x => - { - if (x == schemaId.Id) - { - return Task.FromResult((Mocks.Schema(appId, schemaId, schemaDef), ResolvedComponents.Empty)); - } - else if (x == refSchemaId1.Id) - { - return Task.FromResult((Mocks.Schema(appId, refSchemaId1, refSchemaDef), ResolvedComponents.Empty)); - } - else if (x == refSchemaId2.Id) + requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE)); + + var refSchemaDef = + new Schema("my-ref") + .AddString(1, "name", Partitioning.Invariant, + new StringFieldProperties()) + .AddNumber(2, "number", Partitioning.Invariant, + new NumberFieldProperties()) + .SetFieldsInReferences("name", "number"); + + var schemaDef = + new Schema(schemaId.Name) + .AddReferences(1, "ref1", Partitioning.Invariant, new ReferencesFieldProperties { - return Task.FromResult((Mocks.Schema(appId, refSchemaId2, refSchemaDef), ResolvedComponents.Empty)); - } - else + ResolveReference = true, + MinItems = 1, + MaxItems = 1, + SchemaId = refSchemaId1.Id + }) + .AddReferences(2, "ref2", Partitioning.Invariant, new ReferencesFieldProperties { - throw new DomainObjectNotFoundException(x.ToString()); - } - }; - - sut = new ResolveReferences(new Lazy<IContentQueryService>(() => contentQuery), requestCache); - } - - [Fact] - public async Task Should_add_referenced_id_and_as_dependency() + ResolveReference = true, + MinItems = 1, + MaxItems = 1, + SchemaId = refSchemaId2.Id + }) + .SetFieldsInLists("ref1", "ref2"); + + schemaProvider = x => { - var ref1_1 = CreateRefContent(DomainId.NewGuid(), 1, "ref1_1", 13, refSchemaId1); - var ref1_2 = CreateRefContent(DomainId.NewGuid(), 2, "ref1_2", 17, refSchemaId1); - var ref2_1 = CreateRefContent(DomainId.NewGuid(), 3, "ref2_1", 23, refSchemaId2); - var ref2_2 = CreateRefContent(DomainId.NewGuid(), 4, "ref2_2", 29, refSchemaId2); - - var contents = new[] + if (x == schemaId.Id) { - CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id }), - CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) - }; - - A.CallTo(() => contentQuery.QueryAsync( - A<Context>.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); + return Task.FromResult((Mocks.Schema(appId, schemaId, schemaDef), ResolvedComponents.Empty)); + } + else if (x == refSchemaId1.Id) + { + return Task.FromResult((Mocks.Schema(appId, refSchemaId1, refSchemaDef), ResolvedComponents.Empty)); + } + else if (x == refSchemaId2.Id) + { + return Task.FromResult((Mocks.Schema(appId, refSchemaId2, refSchemaDef), ResolvedComponents.Empty)); + } + else + { + throw new DomainObjectNotFoundException(x.ToString()); + } + }; - await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + sut = new ResolveReferences(new Lazy<IContentQueryService>(() => contentQuery), requestCache); + } - A.CallTo(() => requestCache.AddDependency(DomainId.Combine(appId, refSchemaId1.Id), 0)) - .MustHaveHappened(); + [Fact] + public async Task Should_add_referenced_id_and_as_dependency() + { + var ref1_1 = CreateRefContent(DomainId.NewGuid(), 1, "ref1_1", 13, refSchemaId1); + var ref1_2 = CreateRefContent(DomainId.NewGuid(), 2, "ref1_2", 17, refSchemaId1); + var ref2_1 = CreateRefContent(DomainId.NewGuid(), 3, "ref2_1", 23, refSchemaId2); + var ref2_2 = CreateRefContent(DomainId.NewGuid(), 4, "ref2_2", 29, refSchemaId2); - A.CallTo(() => requestCache.AddDependency(DomainId.Combine(appId, refSchemaId2.Id), 0)) - .MustHaveHappened(); + var contents = new[] + { + CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id }), + CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) + }; - A.CallTo(() => requestCache.AddDependency(ref1_1.UniqueId, ref1_1.Version)) - .MustHaveHappened(); + A.CallTo(() => contentQuery.QueryAsync( + A<Context>.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); - A.CallTo(() => requestCache.AddDependency(ref2_1.UniqueId, ref2_1.Version)) - .MustHaveHappened(); + await sut.EnrichAsync(requestContext, contents, schemaProvider, default); - A.CallTo(() => requestCache.AddDependency(ref1_2.UniqueId, ref1_2.Version)) - .MustHaveHappened(); + A.CallTo(() => requestCache.AddDependency(DomainId.Combine(appId, refSchemaId1.Id), 0)) + .MustHaveHappened(); - A.CallTo(() => requestCache.AddDependency(ref2_2.UniqueId, ref2_2.Version)) - .MustHaveHappened(); - } + A.CallTo(() => requestCache.AddDependency(DomainId.Combine(appId, refSchemaId2.Id), 0)) + .MustHaveHappened(); - [Fact] - public async Task Should_enrich_with_reference_data() - { - var ref1_1 = CreateRefContent(DomainId.NewGuid(), 1, "ref1_1", 13, refSchemaId1); - var ref1_2 = CreateRefContent(DomainId.NewGuid(), 2, "ref1_2", 17, refSchemaId1); - var ref2_1 = CreateRefContent(DomainId.NewGuid(), 3, "ref2_1", 23, refSchemaId2); - var ref2_2 = CreateRefContent(DomainId.NewGuid(), 3, "ref2_2", 29, refSchemaId2); + A.CallTo(() => requestCache.AddDependency(ref1_1.UniqueId, ref1_1.Version)) + .MustHaveHappened(); - var contents = new[] - { - CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id }), - CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) - }; + A.CallTo(() => requestCache.AddDependency(ref2_1.UniqueId, ref2_1.Version)) + .MustHaveHappened(); - A.CallTo(() => contentQuery.QueryAsync( - A<Context>.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); + A.CallTo(() => requestCache.AddDependency(ref1_2.UniqueId, ref1_2.Version)) + .MustHaveHappened(); - await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + A.CallTo(() => requestCache.AddDependency(ref2_2.UniqueId, ref2_2.Version)) + .MustHaveHappened(); + } - Assert.Equal( - new ContentData() - .AddField("ref1", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("en", "ref1_1, 13") - .Add("de", "ref1_1, 13"))) - .AddField("ref2", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("en", "ref2_1, 23") - .Add("de", "ref2_1, 23"))), - contents[0].ReferenceData); + [Fact] + public async Task Should_enrich_with_reference_data() + { + var ref1_1 = CreateRefContent(DomainId.NewGuid(), 1, "ref1_1", 13, refSchemaId1); + var ref1_2 = CreateRefContent(DomainId.NewGuid(), 2, "ref1_2", 17, refSchemaId1); + var ref2_1 = CreateRefContent(DomainId.NewGuid(), 3, "ref2_1", 23, refSchemaId2); + var ref2_2 = CreateRefContent(DomainId.NewGuid(), 3, "ref2_2", 29, refSchemaId2); - Assert.Equal( - new ContentData() - .AddField("ref1", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("en", "ref1_2, 17") - .Add("de", "ref1_2, 17"))) - .AddField("ref2", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("en", "ref2_2, 29") - .Add("de", "ref2_2, 29"))), - contents[1].ReferenceData); - } - - [Fact] - public async Task Should_not_enrich_if_content_has_more_items() + var contents = new[] { - var ref1_1 = CreateRefContent(DomainId.NewGuid(), 1, "ref1_1", 13, refSchemaId1); - var ref1_2 = CreateRefContent(DomainId.NewGuid(), 2, "ref1_2", 17, refSchemaId1); - var ref2_1 = CreateRefContent(DomainId.NewGuid(), 3, "ref2_1", 23, refSchemaId2); - var ref2_2 = CreateRefContent(DomainId.NewGuid(), 4, "ref2_2", 29, refSchemaId2); - - var contents = new[] - { - CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id, ref2_2.Id }), - CreateContent(new[] { ref1_2.Id }, new[] { ref2_1.Id, ref2_2.Id }) - }; - - A.CallTo(() => contentQuery.QueryAsync( - A<Context>.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); + CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id }), + CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) + }; + + A.CallTo(() => contentQuery.QueryAsync( + A<Context>.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); + + await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + + Assert.Equal( + new ContentData() + .AddField("ref1", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("en", "ref1_1, 13") + .Add("de", "ref1_1, 13"))) + .AddField("ref2", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("en", "ref2_1, 23") + .Add("de", "ref2_1, 23"))), + contents[0].ReferenceData); + + Assert.Equal( + new ContentData() + .AddField("ref1", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("en", "ref1_2, 17") + .Add("de", "ref1_2, 17"))) + .AddField("ref2", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("en", "ref2_2, 29") + .Add("de", "ref2_2, 29"))), + contents[1].ReferenceData); + } - await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + [Fact] + public async Task Should_not_enrich_if_content_has_more_items() + { + var ref1_1 = CreateRefContent(DomainId.NewGuid(), 1, "ref1_1", 13, refSchemaId1); + var ref1_2 = CreateRefContent(DomainId.NewGuid(), 2, "ref1_2", 17, refSchemaId1); + var ref2_1 = CreateRefContent(DomainId.NewGuid(), 3, "ref2_1", 23, refSchemaId2); + var ref2_2 = CreateRefContent(DomainId.NewGuid(), 4, "ref2_2", 29, refSchemaId2); - Assert.Equal( - new ContentData() - .AddField("ref1", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("en", "ref1_1, 13") - .Add("de", "ref1_1, 13"))) - .AddField("ref2", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("en", "2 Reference(s)") - .Add("de", "2 Reference(s)"))), - contents[0].ReferenceData); + var contents = new[] + { + CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id, ref2_2.Id }), + CreateContent(new[] { ref1_2.Id }, new[] { ref2_1.Id, ref2_2.Id }) + }; + + A.CallTo(() => contentQuery.QueryAsync( + A<Context>.That.Matches(x => x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), A<Q>.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); + + await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + + Assert.Equal( + new ContentData() + .AddField("ref1", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("en", "ref1_1, 13") + .Add("de", "ref1_1, 13"))) + .AddField("ref2", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("en", "2 Reference(s)") + .Add("de", "2 Reference(s)"))), + contents[0].ReferenceData); + + Assert.Equal( + new ContentData() + .AddField("ref1", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("en", "ref1_2, 17") + .Add("de", "ref1_2, 17"))) + .AddField("ref2", + new ContentFieldData() + .AddInvariant( + new JsonObject() + .Add("en", "2 Reference(s)") + .Add("de", "2 Reference(s)"))), + contents[1].ReferenceData); + } - Assert.Equal( - new ContentData() - .AddField("ref1", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("en", "ref1_2, 17") - .Add("de", "ref1_2, 17"))) - .AddField("ref2", - new ContentFieldData() - .AddInvariant( - new JsonObject() - .Add("en", "2 Reference(s)") - .Add("de", "2 Reference(s)"))), - contents[1].ReferenceData); - } - - [Fact] - public async Task Should_not_enrich_references_if_not_api_user() + [Fact] + public async Task Should_not_enrich_references_if_not_api_user() + { + var contents = new[] { - var contents = new[] - { - CreateContent(new[] { DomainId.NewGuid() }, Array.Empty<DomainId>()) - }; + CreateContent(new[] { DomainId.NewGuid() }, Array.Empty<DomainId>()) + }; - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); - await sut.EnrichAsync(ctx, contents, schemaProvider, default); + await sut.EnrichAsync(ctx, contents, schemaProvider, default); - Assert.Null(contents[0].ReferenceData); + Assert.Null(contents[0].ReferenceData); - A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_enrich_references_if_disabled() + [Fact] + public async Task Should_not_enrich_references_if_disabled() + { + var contents = new[] { - var contents = new[] - { - CreateContent(new[] { DomainId.NewGuid() }, Array.Empty<DomainId>()) - }; + CreateContent(new[] { DomainId.NewGuid() }, Array.Empty<DomainId>()) + }; - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).Clone(b => b.WithoutContentEnrichment(true)); + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).Clone(b => b.WithoutContentEnrichment(true)); - await sut.EnrichAsync(ctx, contents, schemaProvider, default); + await sut.EnrichAsync(ctx, contents, schemaProvider, default); - Assert.Null(contents[0].ReferenceData); + Assert.Null(contents[0].ReferenceData); - A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_invoke_query_service_if_no_references_found() + [Fact] + public async Task Should_not_invoke_query_service_if_no_references_found() + { + var contents = new[] { - var contents = new[] - { - CreateContent(Array.Empty<DomainId>(), Array.Empty<DomainId>()) - }; + CreateContent(Array.Empty<DomainId>(), Array.Empty<DomainId>()) + }; - await sut.EnrichAsync(requestContext, contents, schemaProvider, default); + await sut.EnrichAsync(requestContext, contents, schemaProvider, default); - Assert.NotNull(contents[0].ReferenceData); + Assert.NotNull(contents[0].ReferenceData); - A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - private ContentEntity CreateContent(DomainId[] ref1, DomainId[] ref2) + private ContentEntity CreateContent(DomainId[] ref1, DomainId[] ref2) + { + return new ContentEntity { - return new ContentEntity - { - Id = DomainId.NewGuid(), - Data = - new ContentData() - .AddField("ref1", - new ContentFieldData() - .AddInvariant(JsonValue.Array(ref1.Select(x => x.ToString())))) - .AddField("ref2", - new ContentFieldData() - .AddInvariant(JsonValue.Array(ref2.Select(x => x.ToString())))), - SchemaId = schemaId, - AppId = appId, - Version = 0 - }; - } - - private IEnrichedContentEntity CreateRefContent(DomainId id, int version, string name, int number, NamedId<DomainId> refSchemaId) + Id = DomainId.NewGuid(), + Data = + new ContentData() + .AddField("ref1", + new ContentFieldData() + .AddInvariant(JsonValue.Array(ref1.Select(x => x.ToString())))) + .AddField("ref2", + new ContentFieldData() + .AddInvariant(JsonValue.Array(ref2.Select(x => x.ToString())))), + SchemaId = schemaId, + AppId = appId, + Version = 0 + }; + } + + private IEnrichedContentEntity CreateRefContent(DomainId id, int version, string name, int number, NamedId<DomainId> refSchemaId) + { + return new ContentEntity { - return new ContentEntity - { - Id = id, - Data = - new ContentData() - .AddField("name", - new ContentFieldData() - .AddInvariant(name)) - .AddField("number", - new ContentFieldData() - .AddInvariant(number)), - SchemaId = refSchemaId, - AppId = appId, - Version = version - }; - } + Id = id, + Data = + new ContentData() + .AddField("name", + new ContentFieldData() + .AddInvariant(name)) + .AddField("number", + new ContentFieldData() + .AddInvariant(number)), + SchemaId = refSchemaId, + AppId = appId, + Version = version + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs index 0efdbb7bac..af9a622319 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs @@ -17,129 +17,128 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Queries +namespace Squidex.Domain.Apps.Entities.Contents.Queries; + +public class ScriptContentTests { - public class ScriptContentTests - { - private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly ScriptContent sut; + private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly ScriptContent sut; - public ScriptContentTests() - { - sut = new ScriptContent(scriptEngine); - } + public ScriptContentTests() + { + sut = new ScriptContent(scriptEngine); + } - [Fact] - public async Task Should_not_call_script_engine_if_no_script_configured() - { - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + [Fact] + public async Task Should_not_call_script_engine_if_no_script_configured() + { + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); - var (provider, schemaId) = CreateSchema( - queryPre: "my-pre-query"); + var (provider, schemaId) = CreateSchema( + queryPre: "my-pre-query"); - var content = new ContentEntity { Data = new ContentData(), SchemaId = schemaId }; + var content = new ContentEntity { Data = new ContentData(), SchemaId = schemaId }; - await sut.EnrichAsync(ctx, new[] { content }, provider, default); + await sut.EnrichAsync(ctx, new[] { content }, provider, default); - A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_call_script_engine_for_frontend_user() - { - var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + [Fact] + public async Task Should_not_call_script_engine_for_frontend_user() + { + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); - var (provider, schemaId) = CreateSchema( - query: "my-query"); + var (provider, schemaId) = CreateSchema( + query: "my-query"); - var content = new ContentEntity { Data = new ContentData(), SchemaId = schemaId }; + var content = new ContentEntity { Data = new ContentData(), SchemaId = schemaId }; - await sut.EnrichAsync(ctx, new[] { content }, provider, default); + await sut.EnrichAsync(ctx, new[] { content }, provider, default); - A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_call_script_engine_with_data() - { - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + [Fact] + public async Task Should_call_script_engine_with_data() + { + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); - var oldData = new ContentData(); + var oldData = new ContentData(); - var (provider, schemaId) = CreateSchema( - query: "my-query"); + var (provider, schemaId) = CreateSchema( + query: "my-query"); - var content = new ContentEntity { Data = oldData, SchemaId = schemaId }; + var content = new ContentEntity { Data = oldData, SchemaId = schemaId }; - A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "my-query", ScriptOptions(), A<CancellationToken>._)) - .Returns(new ContentData()); + A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "my-query", ScriptOptions(), A<CancellationToken>._)) + .Returns(new ContentData()); - await sut.EnrichAsync(ctx, new[] { content }, provider, default); + await sut.EnrichAsync(ctx, new[] { content }, provider, default); - Assert.NotSame(oldData, content.Data); + Assert.NotSame(oldData, content.Data); - A.CallTo(() => scriptEngine.TransformAsync( - A<DataScriptVars>.That.Matches(x => - Equals(x["user"], ctx.UserPrincipal) && - Equals(x["data"], oldData) && - Equals(x["contentId"], content.Id)), - "my-query", - ScriptOptions(), A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.TransformAsync( + A<DataScriptVars>.That.Matches(x => + Equals(x["user"], ctx.UserPrincipal) && + Equals(x["data"], oldData) && + Equals(x["contentId"], content.Id)), + "my-query", + ScriptOptions(), A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_make_test_with_pre_query_script() - { - var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + [Fact] + public async Task Should_make_test_with_pre_query_script() + { + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); - var (provider, id) = CreateSchema( - query: @" + var (provider, id) = CreateSchema( + query: @" ctx.data.test = { iv: ctx.custom }; replace()", - queryPre: "ctx.custom = 123;"); + queryPre: "ctx.custom = 123;"); - var content = new ContentEntity { Data = new ContentData(), SchemaId = id }; + var content = new ContentEntity { Data = new ContentData(), SchemaId = id }; - var realScriptEngine = - new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(20), - TimeoutExecution = TimeSpan.FromSeconds(100) - })); + var realScriptEngine = + new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions + { + TimeoutScript = TimeSpan.FromSeconds(20), + TimeoutExecution = TimeSpan.FromSeconds(100) + })); - var sut2 = new ScriptContent(realScriptEngine); + var sut2 = new ScriptContent(realScriptEngine); - await sut2.EnrichAsync(ctx, new[] { content }, provider, default); + await sut2.EnrichAsync(ctx, new[] { content }, provider, default); - Assert.Equal(JsonValue.Create(123), content.Data["test"]!["iv"]); - } + Assert.Equal(JsonValue.Create(123), content.Data["test"]!["iv"]); + } - private (ProvideSchema, NamedId<DomainId>) CreateSchema(string? query = null, string? queryPre = null) - { - var id = NamedId.Of(DomainId.NewGuid(), "my-schema"); - - return (_ => - { - var schemaDef = - new Schema(id.Name) - .SetScripts(new SchemaScripts - { - Query = query, - QueryPre = queryPre - }); - - return Task.FromResult((Mocks.Schema(appId, id, schemaDef), ResolvedComponents.Empty)); - }, id); - } - - private static ScriptOptions ScriptOptions() + private (ProvideSchema, NamedId<DomainId>) CreateSchema(string? query = null, string? queryPre = null) + { + var id = NamedId.Of(DomainId.NewGuid(), "my-schema"); + + return (_ => { - return A<ScriptOptions>.That.Matches(x => x.AsContext); - } + var schemaDef = + new Schema(id.Name) + .SetScripts(new SchemaScripts + { + Query = query, + QueryPre = queryPre + }); + + return Task.FromResult((Mocks.Schema(appId, id, schemaDef), ResolvedComponents.Empty)); + }, id); + } + + private static ScriptOptions ScriptOptions() + { + return A<ScriptOptions>.That.Matches(x => x.AsContext); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs index 4ae113e2ba..ee14fb171b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs @@ -16,149 +16,148 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class ReferencesFluidExtensionTests { - public class ReferencesFluidExtensionTests + private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly FluidTemplateEngine sut; + + public ReferencesFluidExtensionTests() + { + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(contentQuery) + .BuildServiceProvider(); + + var extensions = new IFluidExtension[] + { + new ContentFluidExtension(), + new ReferencesFluidExtension(serviceProvider) + }; + + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) + .Returns(Mocks.App(appId)); + + sut = new FluidTemplateEngine(extensions); + } + + [Fact] + public async Task Should_resolve_references_in_loop() { - private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly FluidTemplateEngine sut; + var referenceId1 = DomainId.NewGuid(); + var reference1 = CreateReference(referenceId1, 1); + var referenceId2 = DomainId.NewGuid(); + var reference2 = CreateReference(referenceId2, 2); - public ReferencesFluidExtensionTests() + var @event = new EnrichedContentEvent { - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(contentQuery) - .BuildServiceProvider(); - - var extensions = new IFluidExtension[] - { - new ContentFluidExtension(), - new ReferencesFluidExtension(serviceProvider) - }; - - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) - .Returns(Mocks.App(appId)); - - sut = new FluidTemplateEngine(extensions); - } - - [Fact] - public async Task Should_resolve_references_in_loop() + Data = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(referenceId1, referenceId2))), + AppId = appId + }; + + A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId1), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, reference1)); + + A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId2), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, reference2)); + + var vars = new TemplateVars { - var referenceId1 = DomainId.NewGuid(); - var reference1 = CreateReference(referenceId1, 1); - var referenceId2 = DomainId.NewGuid(); - var reference2 = CreateReference(referenceId2, 2); - - var @event = new EnrichedContentEvent - { - Data = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(referenceId1, referenceId2))), - AppId = appId - }; - - A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId1), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, reference1)); - - A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId2), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, reference2)); - - var vars = new TemplateVars - { - ["event"] = @event - }; - - var template = @" + ["event"] = @event + }; + + var template = @" {% for id in event.data.references.iv %} {% reference 'ref', id %} Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} {{ ref.id }} {% endfor %} "; - var expected = $@" + var expected = $@" Text: Hello 1 World 1 {referenceId1} Text: Hello 2 World 2 {referenceId2} "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_resolve_references_in_loop_with_filter() + [Fact] + public async Task Should_resolve_references_in_loop_with_filter() + { + var referenceId1 = DomainId.NewGuid(); + var reference1 = CreateReference(referenceId1, 1); + var referenceId2 = DomainId.NewGuid(); + var reference2 = CreateReference(referenceId2, 2); + + var @event = new EnrichedContentEvent { - var referenceId1 = DomainId.NewGuid(); - var reference1 = CreateReference(referenceId1, 1); - var referenceId2 = DomainId.NewGuid(); - var reference2 = CreateReference(referenceId2, 2); - - var @event = new EnrichedContentEvent - { - Data = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(referenceId1, referenceId2))), - AppId = appId - }; - - A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId1), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, reference1)); - - A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId2), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(1, reference2)); - - var vars = new TemplateVars - { - ["event"] = @event - }; - - var template = @" + Data = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(referenceId1, referenceId2))), + AppId = appId + }; + + A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId1), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, reference1)); + + A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<Q>.That.HasIds(referenceId2), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(1, reference2)); + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" {% for id in event.data.references.iv %} {% assign ref = id | reference %} Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} {{ ref.id }} {% endfor %} "; - var expected = $@" + var expected = $@" Text: Hello 1 World 1 {referenceId1} Text: Hello 2 World 2 {referenceId2} "; - var actual = await sut.RenderAsync(template, vars); + var actual = await sut.RenderAsync(template, vars); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index) - { - return new ContentEntity - { - Data = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(JsonValue.Create($"Hello {index}"))) - .AddField("field2", - new ContentFieldData() - .AddInvariant(JsonValue.Create($"World {index}"))), - Id = referenceId - }; - } - - private static string Cleanup(string text) + private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index) + { + return new ContentEntity { - return text - .Replace("\r", string.Empty, StringComparison.Ordinal) - .Replace("\n", string.Empty, StringComparison.Ordinal) - .Replace(" ", string.Empty, StringComparison.Ordinal); - } + Data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(JsonValue.Create($"Hello {index}"))) + .AddField("field2", + new ContentFieldData() + .AddInvariant(JsonValue.Create($"World {index}"))), + Id = referenceId + }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty, StringComparison.Ordinal) + .Replace("\n", string.Empty, StringComparison.Ordinal) + .Replace(" ", string.Empty, StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs index bf9d22c8ff..76504eb0ec 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs @@ -18,72 +18,72 @@ using Squidex.Infrastructure.Json.Objects; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class ReferencesJintExtensionTests : IClassFixture<TranslationsFixture> { - public class ReferencesJintExtensionTests : IClassFixture<TranslationsFixture> + private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly JintScriptEngine sut; + + public ReferencesJintExtensionTests() { - private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly JintScriptEngine sut; + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(contentQuery) + .BuildServiceProvider(); - public ReferencesJintExtensionTests() + var extensions = new IJintExtension[] { - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(contentQuery) - .BuildServiceProvider(); + new ReferencesJintExtension(serviceProvider) + }; - var extensions = new IJintExtension[] + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) + .Returns(Mocks.App(appId)); + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), + Options.Create(new JintScriptOptions { - new ReferencesJintExtension(serviceProvider) - }; - - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) - .Returns(Mocks.App(appId)); - - sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), - Options.Create(new JintScriptOptions - { - TimeoutScript = TimeSpan.FromSeconds(2), - TimeoutExecution = TimeSpan.FromSeconds(10) - }), - extensions); - } - - [Fact] - public async Task Should_resolve_reference() - { - var (vars, _) = SetupReferenceVars(1); + TimeoutScript = TimeSpan.FromSeconds(2), + TimeoutExecution = TimeSpan.FromSeconds(10) + }), + extensions); + } + + [Fact] + public async Task Should_resolve_reference() + { + var (vars, _) = SetupReferenceVars(1); - var expected = @" + var expected = @" Text: Hello 1 World 1 "; - var script = @" + var script = @" getReference(data.references.iv[0], function (references) { var actual1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; complete(`${actual1}`); })"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - [Fact] - public async Task Should_resolve_references() - { - var (vars, _) = SetupReferenceVars(2); + [Fact] + public async Task Should_resolve_references() + { + var (vars, _) = SetupReferenceVars(2); - var expected = @" + var expected = @" Text: Hello 1 World 1 Text: Hello 2 World 2 "; - var script = @" + var script = @" getReferences(data.references.iv, function (references) { var actual1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; var actual2 = `Text: ${references[1].data.field1.iv} ${references[1].data.field2.iv}`; @@ -91,61 +91,60 @@ public async Task Should_resolve_references() complete(`${actual1}\n${actual2}`); })"; - var actual = (await sut.ExecuteAsync(vars, script)).ToString(); + var actual = (await sut.ExecuteAsync(vars, script)).ToString(); - Assert.Equal(Cleanup(expected), Cleanup(actual)); - } + Assert.Equal(Cleanup(expected), Cleanup(actual)); + } - private (ScriptVars, IContentEntity[]) SetupReferenceVars(int count) - { - var references = Enumerable.Range(0, count).Select((x, i) => CreateReference(i + 1)).ToArray(); - var referenceIds = references.Select(x => x.Id); + private (ScriptVars, IContentEntity[]) SetupReferenceVars(int count) + { + var references = Enumerable.Range(0, count).Select((x, i) => CreateReference(i + 1)).ToArray(); + var referenceIds = references.Select(x => x.Id); - var user = new ClaimsPrincipal(); + var user = new ClaimsPrincipal(); - var data = - new ContentData() - .AddField("references", - new ContentFieldData() - .AddInvariant(JsonValue.Array(referenceIds))); + var data = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(referenceIds))); - A.CallTo(() => contentQuery.QueryAsync( - A<Context>.That.Matches(x => x.App.Id == appId.Id && x.UserPrincipal == user), A<Q>.That.HasIds(referenceIds), A<CancellationToken>._)) - .Returns(ResultList.CreateFrom(2, references)); + A.CallTo(() => contentQuery.QueryAsync( + A<Context>.That.Matches(x => x.App.Id == appId.Id && x.UserPrincipal == user), A<Q>.That.HasIds(referenceIds), A<CancellationToken>._)) + .Returns(ResultList.CreateFrom(2, references)); - var vars = new ScriptVars - { - ["appId"] = appId.Id, - ["data"] = data, - ["dataOld"] = null, - ["user"] = user - }; + var vars = new ScriptVars + { + ["appId"] = appId.Id, + ["data"] = data, + ["dataOld"] = null, + ["user"] = user + }; - return (vars, references); - } + return (vars, references); + } - private static IEnrichedContentEntity CreateReference(int index) - { - return new ContentEntity - { - Data = - new ContentData() - .AddField("field1", - new ContentFieldData() - .AddInvariant(JsonValue.Create($"Hello {index}"))) - .AddField("field2", - new ContentFieldData() - .AddInvariant(JsonValue.Create($"World {index}"))), - Id = DomainId.NewGuid() - }; - } - - private static string Cleanup(string text) + private static IEnrichedContentEntity CreateReference(int index) + { + return new ContentEntity { - return text - .Replace("\r", string.Empty, StringComparison.Ordinal) - .Replace("\n", string.Empty, StringComparison.Ordinal) - .Replace(" ", string.Empty, StringComparison.Ordinal); - } + Data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(JsonValue.Create($"Hello {index}"))) + .AddField("field2", + new ContentFieldData() + .AddInvariant(JsonValue.Create($"World {index}"))), + Id = DomainId.NewGuid() + }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty, StringComparison.Ordinal) + .Replace("\n", string.Empty, StringComparison.Ordinal) + .Replace(" ", string.Empty, StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs index 5c2ea1234c..30e3b81137 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs @@ -13,56 +13,55 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents; + +public class SingletonCommandMiddlewareTests { - public class SingletonCommandMiddlewareTests - { - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - private readonly SingletonCommandMiddleware sut = new SingletonCommandMiddleware(); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly SingletonCommandMiddleware sut = new SingletonCommandMiddleware(); - [Fact] - public async Task Should_create_content_if_singleton_schema_is_created() - { - var command = new CreateSchema { Type = SchemaType.Singleton, Name = "my-schema" }; + [Fact] + public async Task Should_create_content_if_singleton_schema_is_created() + { + var command = new CreateSchema { Type = SchemaType.Singleton, Name = "my-schema" }; - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - await sut.HandleAsync(context, default); + await sut.HandleAsync(context, default); - A.CallTo(() => commandBus.PublishAsync( - A<CreateContent>.That.Matches(x => x.Status == Status.Published), default)) - .MustHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync( + A<CreateContent>.That.Matches(x => x.Status == Status.Published), default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_create_content_if_non_singleton_schema_is_created() - { - var command = new CreateSchema(); + [Fact] + public async Task Should_not_create_content_if_non_singleton_schema_is_created() + { + var command = new CreateSchema(); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - await sut.HandleAsync(context, default); + await sut.HandleAsync(context, default); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_create_content_if_singleton_schema_not_created() - { - var command = new CreateSchema { Type = SchemaType.Singleton }; + [Fact] + public async Task Should_not_create_content_if_singleton_schema_not_created() + { + var command = new CreateSchema { Type = SchemaType.Singleton }; - var context = - new CommandContext(command, commandBus); + var context = + new CommandContext(command, commandBus); - await sut.HandleAsync(context, default); + await sut.HandleAsync(context, default); - A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => commandBus.PublishAsync(A<ICommand>._, A<CancellationToken>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs index 37856ef6f5..ce6ba5dbc1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs @@ -9,135 +9,134 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.TestData +namespace Squidex.Domain.Apps.Entities.Contents.TestData; + +public sealed class FakeUrlGenerator : IUrlGenerator { - public sealed class FakeUrlGenerator : IUrlGenerator - { - public bool CanGenerateAssetSourceUrl { get; } = true; - - public string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType) - { - return $"assets/{appId.Name}/{idOrSlug}?width=100"; - } - - public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion) - { - return $"assets/source/{assetId}"; - } - - public string AssetContent(NamedId<DomainId> appId, string idOrSlug) - { - return $"assets/{appId.Name}/{idOrSlug}"; - } - - public string ContentUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId, DomainId contentId) - { - return $"contents/{schemaId.Name}/{contentId}"; - } - - public string AssetContentBase() - { - return "$assets/"; - } - - public string AssetContentCDNBase() - { - return $"cdn/assets/"; - } - - public string ContentBase() - { - return $"contents/"; - } - - public string ContentCDNBase() - { - return $"cdn/contents/"; - } - - public string AssetsUI(NamedId<DomainId> appId, string? @ref = null) - { - throw new NotSupportedException(); - } - - public string AssetContentBase(string appName) - { - throw new NotSupportedException(); - } - - public string BackupsUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string ClientsUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string ContentsUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) - { - throw new NotSupportedException(); - } - - public string ContributorsUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string DashboardUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string LanguagesUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string PatternsUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string PlansUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string RolesUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string Root() - { - throw new NotSupportedException(); - } - - public string RulesUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string SchemasUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string SchemaUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) - { - throw new NotSupportedException(); - } - - public string WorkflowsUI(NamedId<DomainId> appId) - { - throw new NotSupportedException(); - } - - public string UI() - { - throw new NotSupportedException(); - } + public bool CanGenerateAssetSourceUrl { get; } = true; + + public string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType) + { + return $"assets/{appId.Name}/{idOrSlug}?width=100"; + } + + public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion) + { + return $"assets/source/{assetId}"; + } + + public string AssetContent(NamedId<DomainId> appId, string idOrSlug) + { + return $"assets/{appId.Name}/{idOrSlug}"; + } + + public string ContentUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId, DomainId contentId) + { + return $"contents/{schemaId.Name}/{contentId}"; + } + + public string AssetContentBase() + { + return "$assets/"; + } + + public string AssetContentCDNBase() + { + return $"cdn/assets/"; + } + + public string ContentBase() + { + return $"contents/"; + } + + public string ContentCDNBase() + { + return $"cdn/contents/"; + } + + public string AssetsUI(NamedId<DomainId> appId, string? @ref = null) + { + throw new NotSupportedException(); + } + + public string AssetContentBase(string appName) + { + throw new NotSupportedException(); + } + + public string BackupsUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string ClientsUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string ContentsUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) + { + throw new NotSupportedException(); + } + + public string ContributorsUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string DashboardUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string LanguagesUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string PatternsUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string PlansUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string RolesUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string Root() + { + throw new NotSupportedException(); + } + + public string RulesUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string SchemasUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string SchemaUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) + { + throw new NotSupportedException(); + } + + public string WorkflowsUI(NamedId<DomainId> appId) + { + throw new NotSupportedException(); + } + + public string UI() + { + throw new NotSupportedException(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasParsingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasParsingTests.cs index 598f0949e1..6ca5c366f1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasParsingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasParsingTests.cs @@ -15,408 +15,407 @@ using Xunit; using LuceneQueryAnalyzer = Lucene.Net.QueryParsers.Classic.QueryParser; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public class AtlasParsingTests { - public class AtlasParsingTests + private static readonly LuceneQueryVisitor QueryVisitor = new LuceneQueryVisitor(); + private static readonly LuceneQueryAnalyzer QueryParser = + new LuceneQueryAnalyzer(LuceneVersion.LUCENE_48, "*", + new StandardAnalyzer(LuceneVersion.LUCENE_48, CharArraySet.EMPTY_SET)); + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions { - private static readonly LuceneQueryVisitor QueryVisitor = new LuceneQueryVisitor(); - private static readonly LuceneQueryAnalyzer QueryParser = - new LuceneQueryAnalyzer(LuceneVersion.LUCENE_48, "*", - new StandardAnalyzer(LuceneVersion.LUCENE_48, CharArraySet.EMPTY_SET)); - private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions - { - WriteIndented = true - }; + WriteIndented = true + }; - [Fact] - public void Should_parse_term_query() - { - var actual = ParseQuery("hello"); + [Fact] + public void Should_parse_term_query() + { + var actual = ParseQuery("hello"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + text = new { - text = new + path = new { - path = new - { - wildcard = "*" - }, - query = "hello" - } - }); + wildcard = "*" + }, + query = "hello" + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_phrase_query() - { - var actual = ParseQuery("\"hello dolly\""); + [Fact] + public void Should_parse_phrase_query() + { + var actual = ParseQuery("\"hello dolly\""); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + phrase = new { - phrase = new + path = new { - path = new - { - wildcard = "*" - }, - query = new[] { "hello", "dolly" } - } - }); + wildcard = "*" + }, + query = new[] { "hello", "dolly" } + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_compound_phrase_query() - { - var actual = ParseQuery("title:\"The Right Way\" AND text:go"); + [Fact] + public void Should_parse_compound_phrase_query() + { + var actual = ParseQuery("title:\"The Right Way\" AND text:go"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + compound = new { - compound = new + must = new object[] { - must = new object[] + new { - new + phrase = new { - phrase = new + path = "title", + query = new[] { - path = "title", - query = new[] - { - "the", - "right", - "way" - } + "the", + "right", + "way" } - }, - new + } + }, + new + { + text = new { - text = new - { - path = "text", - query = "go" - } + path = "text", + query = "go" } } } - }); + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_compound_phrase_query_with_widldcard() - { - var actual = ParseQuery("title:\"Do it right\" AND right"); + [Fact] + public void Should_parse_compound_phrase_query_with_widldcard() + { + var actual = ParseQuery("title:\"Do it right\" AND right"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + compound = new { - compound = new + must = new object[] { - must = new object[] + new { - new + phrase = new { - phrase = new + path = "title", + query = new[] { - path = "title", - query = new[] - { - "do", - "it", - "right" - } + "do", + "it", + "right" } - }, - new + } + }, + new + { + text = new { - text = new + path = new { - path = new - { - wildcard = "*" - }, - query = "right" - } + wildcard = "*" + }, + query = "right" } } } - }); + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_wildcard_query() - { - var actual = ParseQuery("te?t"); + [Fact] + public void Should_parse_wildcard_query() + { + var actual = ParseQuery("te?t"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + wildcard = new { - wildcard = new + path = new { - path = new - { - wildcard = "*" - }, - query = "te?t" - } - }); + wildcard = "*" + }, + query = "te?t" + } + }); + + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Fact] + public void Should_parse_prefix_query() + { + var actual = ParseQuery("test*"); - [Fact] - public void Should_parse_prefix_query() + var expected = CreateQuery(new { - var actual = ParseQuery("test*"); - - var expected = CreateQuery(new + wildcard = new { - wildcard = new + path = new { - path = new - { - wildcard = "*" - }, - query = "test*" - } - }); + wildcard = "*" + }, + query = "test*" + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_fuzzy_query() - { - var actual = ParseQuery("roam~"); + [Fact] + public void Should_parse_fuzzy_query() + { + var actual = ParseQuery("roam~"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + text = new { - text = new + path = new { - path = new - { - wildcard = "*" - }, - query = "roam", - fuzzy = new - { - maxEdits = 2 - } + wildcard = "*" + }, + query = "roam", + fuzzy = new + { + maxEdits = 2 } - }); + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_fuzzy_query_with_max_edits() - { - var actual = ParseQuery("roam~1"); + [Fact] + public void Should_parse_fuzzy_query_with_max_edits() + { + var actual = ParseQuery("roam~1"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + text = new { - text = new + path = new { - path = new - { - wildcard = "*" - }, - query = "roam", - fuzzy = new - { - maxEdits = 1 - } + wildcard = "*" + }, + query = "roam", + fuzzy = new + { + maxEdits = 1 } - }); + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_fuzzy_phrase_query_with_slop() - { - var actual = ParseQuery("\"jakarta apache\"~10"); + [Fact] + public void Should_parse_fuzzy_phrase_query_with_slop() + { + var actual = ParseQuery("\"jakarta apache\"~10"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + phrase = new { - phrase = new + path = new { - path = new - { - wildcard = "*" - }, - query = new[] - { - "jakarta", - "apache" - }, - slop = 10 - } - }); + wildcard = "*" + }, + query = new[] + { + "jakarta", + "apache" + }, + slop = 10 + } + }); + + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Fact] + public void Should_parse_compound_query_with_brackets() + { + var actual = ParseQuery("(jakarta OR apache) AND website"); - [Fact] - public void Should_parse_compound_query_with_brackets() + var expected = CreateQuery(new { - var actual = ParseQuery("(jakarta OR apache) AND website"); - - var expected = CreateQuery(new + compound = new { - compound = new + must = new object[] { - must = new object[] + new { - new + compound = new { - compound = new + should = new object[] { - should = new object[] + new { - new + text = new { - text = new + path = new { - path = new - { - wildcard = "*" - }, - query = "jakarta" - } - }, - new + wildcard = "*" + }, + query = "jakarta" + } + }, + new + { + text = new { - text = new + path = new { - path = new - { - wildcard = "*" - }, - query = "apache" - } + wildcard = "*" + }, + query = "apache" } } } - }, - new + } + }, + new + { + text = new { - text = new + path = new { - path = new - { - wildcard = "*" - }, - query = "website" - } + wildcard = "*" + }, + query = "website" } } } - }); + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_compound_query_and_optimize() - { - var actual = ParseQuery("title:(+return +\"pink panther\")"); + [Fact] + public void Should_parse_compound_query_and_optimize() + { + var actual = ParseQuery("title:(+return +\"pink panther\")"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + compound = new { - compound = new + must = new object[] { - must = new object[] + new { - new + text = new { - text = new - { - path = "title", - query = "return" - } - }, - new + path = "title", + query = "return" + } + }, + new + { + phrase = new { - phrase = new + path = "title", + query = new[] { - path = "title", - query = new[] - { - "pink", - "panther" - } + "pink", + "panther" } } } } - }); + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_parse_range_query() - { - var actual = ParseQuery("mod_date:[20020101 TO 20030101]"); + [Fact] + public void Should_parse_range_query() + { + var actual = ParseQuery("mod_date:[20020101 TO 20030101]"); - var expected = CreateQuery(new + var expected = CreateQuery(new + { + range = new { - range = new - { - path = "mod_date", - gte = 20020101, - lte = 20030101 - } - }); + path = "mod_date", + gte = 20020101, + lte = 20030101 + } + }); + + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Fact] + public void Should_parse_open_range_query() + { + var actual = ParseQuery("mod_date:{20020101 TO 20030101}"); - [Fact] - public void Should_parse_open_range_query() + var expected = CreateQuery(new { - var actual = ParseQuery("mod_date:{20020101 TO 20030101}"); - - var expected = CreateQuery(new + range = new { - range = new - { - path = "mod_date", - gt = 20020101, - lt = 20030101 - } - }); + path = "mod_date", + gt = 20020101, + lt = 20030101 + } + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - private static object CreateQuery(object query) - { - return JsonSerializer.Serialize(query, JsonSerializerOptions); - } + private static object CreateQuery(object query) + { + return JsonSerializer.Serialize(query, JsonSerializerOptions); + } - private static object ParseQuery(string query) - { - var luceneQuery = QueryParser.Parse(query); + private static object ParseQuery(string query) + { + var luceneQuery = QueryParser.Parse(query); - var rendered = QueryVisitor.Visit(luceneQuery); + var rendered = QueryVisitor.Visit(luceneQuery); - var jsonStream = new MemoryStream(); - var jsonDocument = JsonDocument.Parse(rendered.ToJson()); + var jsonStream = new MemoryStream(); + var jsonDocument = JsonDocument.Parse(rendered.ToJson()); - var jsonWriter = new Utf8JsonWriter(jsonStream, new JsonWriterOptions { Indented = true }); + var jsonWriter = new Utf8JsonWriter(jsonStream, new JsonWriterOptions { Indented = true }); - jsonDocument.WriteTo(jsonWriter); + jsonDocument.WriteTo(jsonWriter); - jsonWriter.Flush(); + jsonWriter.Flush(); - return Encoding.UTF8.GetString(jsonStream.ToArray()); - } + return Encoding.UTF8.GetString(jsonStream.ToArray()); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs index ad75084cf2..b903259133 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs @@ -13,32 +13,31 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class AtlasTextIndexFixture : IAsyncLifetime { - public sealed class AtlasTextIndexFixture : IAsyncLifetime - { - public AtlasTextIndex Index { get; } + public AtlasTextIndex Index { get; } - public AtlasTextIndexFixture() - { - TestUtils.SetupBson(); + public AtlasTextIndexFixture() + { + TestUtils.SetupBson(); - var mongoClient = new MongoClient(TestConfig.Configuration["atlas:configuration"]); - var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["atlas:database"]); + var mongoClient = new MongoClient(TestConfig.Configuration["atlas:configuration"]); + var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["atlas:database"]); - var options = TestConfig.Configuration.GetSection("atlas").Get<AtlasOptions>(); + var options = TestConfig.Configuration.GetSection("atlas").Get<AtlasOptions>(); - Index = new AtlasTextIndex(mongoDatabase, Options.Create(options)); - } + Index = new AtlasTextIndex(mongoDatabase, Options.Create(options)); + } - public Task InitializeAsync() - { - return Index.InitializeAsync(default); - } + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } - public Task DisposeAsync() - { - return Task.CompletedTask; - } + public Task DisposeAsync() + { + return Task.CompletedTask; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexTests.cs index 53663c173e..7ba13f2608 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexTests.cs @@ -9,53 +9,52 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +[Trait("Category", "Dependencies")] +public class AtlasTextIndexTests : TextIndexerTestsBase, IClassFixture<AtlasTextIndexFixture> { - [Trait("Category", "Dependencies")] - public class AtlasTextIndexTests : TextIndexerTestsBase, IClassFixture<AtlasTextIndexFixture> - { - public override bool SupportsQuerySyntax => true; + public override bool SupportsQuerySyntax => true; - public override bool SupportsGeo => true; + public override bool SupportsGeo => true; - public override int WaitAfterUpdate => 2000; + public override int WaitAfterUpdate => 2000; - public AtlasTextIndexFixture _ { get; } + public AtlasTextIndexFixture _ { get; } - public AtlasTextIndexTests(AtlasTextIndexFixture fixture) - { - _ = fixture; - } + public AtlasTextIndexTests(AtlasTextIndexFixture fixture) + { + _ = fixture; + } - public override ITextIndex CreateIndex() - { - return _.Index; - } + public override ITextIndex CreateIndex() + { + return _.Index; + } - [Fact] - public async Task Should_retrieve_english_stopword_only_for_german_query() - { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + [Fact] + public async Task Should_retrieve_english_stopword_only_for_german_query() + { + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: ids2, text: "und"); - } + await SearchText(expected: ids2, text: "und"); + } - [Fact] - public async Task Should_retrieve_german_stopword_only_for_english_query() - { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + [Fact] + public async Task Should_retrieve_german_stopword_only_for_english_query() + { + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "and"); - } + await SearchText(expected: ids1, text: "and"); + } - [Fact] - public async Task Should_index_cjk_content_and_retrieve() - { - await CreateTextAsync(ids1[0], "zh", "東京大学"); + [Fact] + public async Task Should_index_cjk_content_and_retrieve() + { + await CreateTextAsync(ids1[0], "zh", "東京大学"); - await SearchText(expected: ids1, text: "東京"); - } + await SearchText(expected: ids1, text: "東京"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexFixture.cs index a024f104e2..205acaaf6a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexFixture.cs @@ -9,28 +9,27 @@ using Squidex.Extensions.Text.Azure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class AzureTextIndexFixture : IAsyncLifetime { - public sealed class AzureTextIndexFixture : IAsyncLifetime - { - public AzureTextIndex Index { get; } + public AzureTextIndex Index { get; } - public AzureTextIndexFixture() - { - Index = new AzureTextIndex( - TestConfig.Configuration["azureText:serviceEndpoint"], - TestConfig.Configuration["azureText:apiKey"], - TestConfig.Configuration["azureText:indexName"]); - } + public AzureTextIndexFixture() + { + Index = new AzureTextIndex( + TestConfig.Configuration["azureText:serviceEndpoint"], + TestConfig.Configuration["azureText:apiKey"], + TestConfig.Configuration["azureText:indexName"]); + } - public Task InitializeAsync() - { - return Index.InitializeAsync(default); - } + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } - public Task DisposeAsync() - { - return Task.CompletedTask; - } + public Task DisposeAsync() + { + return Task.CompletedTask; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs index a54567e36e..308e24703f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs @@ -9,51 +9,50 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +[Trait("Category", "Dependencies")] +public class AzureTextIndexTests : TextIndexerTestsBase, IClassFixture<AzureTextIndexFixture> { - [Trait("Category", "Dependencies")] - public class AzureTextIndexTests : TextIndexerTestsBase, IClassFixture<AzureTextIndexFixture> - { - public override bool SupportsGeo => true; + public override bool SupportsGeo => true; - public override int WaitAfterUpdate => 2000; + public override int WaitAfterUpdate => 2000; - public AzureTextIndexFixture _ { get; } + public AzureTextIndexFixture _ { get; } - public AzureTextIndexTests(AzureTextIndexFixture fixture) - { - _ = fixture; - } + public AzureTextIndexTests(AzureTextIndexFixture fixture) + { + _ = fixture; + } - public override ITextIndex CreateIndex() - { - return _.Index; - } + public override ITextIndex CreateIndex() + { + return _.Index; + } - [Fact] - public async Task Should_retrieve_english_stopword_only_for_german_query() - { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + [Fact] + public async Task Should_retrieve_english_stopword_only_for_german_query() + { + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: ids2, text: "und"); - } + await SearchText(expected: ids2, text: "und"); + } - [Fact] - public async Task Should_retrieve_german_stopword_only_for_english_query() - { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + [Fact] + public async Task Should_retrieve_german_stopword_only_for_english_query() + { + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "and"); - } + await SearchText(expected: ids1, text: "and"); + } - [Fact] - public async Task Should_index_cjk_content_and_retrieve() - { - await CreateTextAsync(ids1[0], "zh", "東京大学"); + [Fact] + public async Task Should_index_cjk_content_and_retrieve() + { + await CreateTextAsync(ids1[0], "zh", "東京大学"); - await SearchText(expected: ids1, text: "東京"); - } + await SearchText(expected: ids1, text: "東京"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs index 9f3382d478..6f199222a2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs @@ -11,116 +11,115 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public class CachingTextIndexerStateTests { - public class CachingTextIndexerStateTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly ITextIndexerState inner = A.Fake<ITextIndexerState>(); + private readonly DomainId contentId = DomainId.NewGuid(); + private readonly CachingTextIndexerState sut; + + public CachingTextIndexerStateTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly ITextIndexerState inner = A.Fake<ITextIndexerState>(); - private readonly DomainId contentId = DomainId.NewGuid(); - private readonly CachingTextIndexerState sut; + ct = cts.Token; - public CachingTextIndexerStateTests() - { - ct = cts.Token; + sut = new CachingTextIndexerState(inner); + } - sut = new CachingTextIndexerState(inner); - } + [Fact] + public async Task Should_retrieve_from_inner_if_not_cached() + { + var contentIds = HashSet.Of(contentId); - [Fact] - public async Task Should_retrieve_from_inner_if_not_cached() + var state = new TextContentState { UniqueContentId = contentId }; + + var states = new Dictionary<DomainId, TextContentState> { - var contentIds = HashSet.Of(contentId); + [contentId] = state + }; - var state = new TextContentState { UniqueContentId = contentId }; + A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>.That.Is(contentIds), ct)) + .Returns(states); - var states = new Dictionary<DomainId, TextContentState> - { - [contentId] = state - }; + var found1 = await sut.GetAsync(HashSet.Of(contentId), ct); + var found2 = await sut.GetAsync(HashSet.Of(contentId), ct); - A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>.That.Is(contentIds), ct)) - .Returns(states); + Assert.Same(state, found1[contentId]); + Assert.Same(state, found2[contentId]); - var found1 = await sut.GetAsync(HashSet.Of(contentId), ct); - var found2 = await sut.GetAsync(HashSet.Of(contentId), ct); + A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>.That.Is(contentIds), ct)) + .MustHaveHappenedOnceExactly(); + } - Assert.Same(state, found1[contentId]); - Assert.Same(state, found2[contentId]); + [Fact] + public async Task Should_retrieve_from_inner_if_not_cached_and_not_found() + { + var contentIds = HashSet.Of(contentId); - A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>.That.Is(contentIds), ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>.That.Is(contentIds), ct)) + .Returns(new Dictionary<DomainId, TextContentState>()); - [Fact] - public async Task Should_retrieve_from_inner_if_not_cached_and_not_found() - { - var contentIds = HashSet.Of(contentId); + var found1 = await sut.GetAsync(HashSet.Of(contentId), ct); + var found2 = await sut.GetAsync(HashSet.Of(contentId), ct); - A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>.That.Is(contentIds), ct)) - .Returns(new Dictionary<DomainId, TextContentState>()); + Assert.Empty(found1); + Assert.Empty(found2); - var found1 = await sut.GetAsync(HashSet.Of(contentId), ct); - var found2 = await sut.GetAsync(HashSet.Of(contentId), ct); + A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>.That.Is(contentIds), ct)) + .MustHaveHappenedOnceExactly(); + } - Assert.Empty(found1); - Assert.Empty(found2); + [Fact] + public async Task Should_not_retrieve_from_inner_if_cached() + { + var contentIds = HashSet.Of(contentId); - A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>.That.Is(contentIds), ct)) - .MustHaveHappenedOnceExactly(); - } + var state = new TextContentState { UniqueContentId = contentId }; - [Fact] - public async Task Should_not_retrieve_from_inner_if_cached() - { - var contentIds = HashSet.Of(contentId); + await sut.SetAsync(new List<TextContentState> { state }, ct); - var state = new TextContentState { UniqueContentId = contentId }; + var found1 = await sut.GetAsync(contentIds, ct); + var found2 = await sut.GetAsync(contentIds, ct); - await sut.SetAsync(new List<TextContentState> { state }, ct); + Assert.Same(state, found1[contentId]); + Assert.Same(state, found2[contentId]); - var found1 = await sut.GetAsync(contentIds, ct); - var found2 = await sut.GetAsync(contentIds, ct); + A.CallTo(() => inner.SetAsync(A<List<TextContentState>>.That.IsSameSequenceAs(state), ct)) + .MustHaveHappenedOnceExactly(); - Assert.Same(state, found1[contentId]); - Assert.Same(state, found2[contentId]); + A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => inner.SetAsync(A<List<TextContentState>>.That.IsSameSequenceAs(state), ct)) - .MustHaveHappenedOnceExactly(); + [Fact] + public async Task Should_not_retrieve_from_inner_if_removed() + { + var contentIds = HashSet.Of(contentId); - A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + var state = new TextContentState { UniqueContentId = contentId }; - [Fact] - public async Task Should_not_retrieve_from_inner_if_removed() + await sut.SetAsync(new List<TextContentState> { - var contentIds = HashSet.Of(contentId); - - var state = new TextContentState { UniqueContentId = contentId }; + state + }, ct); - await sut.SetAsync(new List<TextContentState> - { - state - }, ct); - - await sut.SetAsync(new List<TextContentState> - { - new TextContentState { UniqueContentId = contentId, IsDeleted = true } - }, ct); + await sut.SetAsync(new List<TextContentState> + { + new TextContentState { UniqueContentId = contentId, IsDeleted = true } + }, ct); - var found1 = await sut.GetAsync(contentIds, ct); - var found2 = await sut.GetAsync(contentIds, ct); + var found1 = await sut.GetAsync(contentIds, ct); + var found2 = await sut.GetAsync(contentIds, ct); - Assert.Empty(found1); - Assert.Empty(found2); + Assert.Empty(found1); + Assert.Empty(found2); - A.CallTo(() => inner.SetAsync(A<List<TextContentState>>.That.Matches(x => x.Count == 1 && x[0].IsDeleted), ct)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => inner.SetAsync(A<List<TextContentState>>.That.Matches(x => x.Count == 1 && x[0].IsDeleted), ct)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => inner.GetAsync(A<HashSet<DomainId>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexFixture.cs index 780c5cd0d1..31c1d4c8ec 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexFixture.cs @@ -10,28 +10,27 @@ using Squidex.Extensions.Text.ElasticSearch; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class ElasticSearchTextIndexFixture : IAsyncLifetime { - public sealed class ElasticSearchTextIndexFixture : IAsyncLifetime - { - public ElasticSearchTextIndex Index { get; } + public ElasticSearchTextIndex Index { get; } - public ElasticSearchTextIndexFixture() - { - Index = new ElasticSearchTextIndex( - new ElasticSearchClient(TestConfig.Configuration["elastic:configuration"]), - TestConfig.Configuration["elastic:indexName"], - TestUtils.DefaultSerializer); - } + public ElasticSearchTextIndexFixture() + { + Index = new ElasticSearchTextIndex( + new ElasticSearchClient(TestConfig.Configuration["elastic:configuration"]), + TestConfig.Configuration["elastic:indexName"], + TestUtils.DefaultSerializer); + } - public Task InitializeAsync() - { - return Index.InitializeAsync(default); - } + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } - public Task DisposeAsync() - { - return Task.CompletedTask; - } + public Task DisposeAsync() + { + return Task.CompletedTask; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs index 6aae342d14..8f872c7ef5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs @@ -9,51 +9,50 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +[Trait("Category", "Dependencies")] +public class ElasticSearchTextIndexTests : TextIndexerTestsBase, IClassFixture<ElasticSearchTextIndexFixture> { - [Trait("Category", "Dependencies")] - public class ElasticSearchTextIndexTests : TextIndexerTestsBase, IClassFixture<ElasticSearchTextIndexFixture> - { - public override bool SupportsGeo => true; + public override bool SupportsGeo => true; - public override int WaitAfterUpdate => 2000; + public override int WaitAfterUpdate => 2000; - public ElasticSearchTextIndexFixture _ { get; } + public ElasticSearchTextIndexFixture _ { get; } - public ElasticSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) - { - _ = fixture; - } + public ElasticSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) + { + _ = fixture; + } - public override ITextIndex CreateIndex() - { - return _.Index; - } + public override ITextIndex CreateIndex() + { + return _.Index; + } - [Fact] - public async Task Should_retrieve_english_stopword_only_for_german_query() - { - await CreateTextAsync(ids1[0], "de", "and y"); - await CreateTextAsync(ids2[0], "en", "and y"); + [Fact] + public async Task Should_retrieve_english_stopword_only_for_german_query() + { + await CreateTextAsync(ids1[0], "de", "and y"); + await CreateTextAsync(ids2[0], "en", "and y"); - await SearchText(expected: ids2, text: "und"); - } + await SearchText(expected: ids2, text: "und"); + } - [Fact] - public async Task Should_retrieve_german_stopword_only_for_english_query() - { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + [Fact] + public async Task Should_retrieve_german_stopword_only_for_english_query() + { + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "and"); - } + await SearchText(expected: ids1, text: "and"); + } - [Fact] - public async Task Should_index_cjk_content_and_retrieve() - { - await CreateTextAsync(ids1[0], "zh", "東京大学"); + [Fact] + public async Task Should_index_cjk_content_and_retrieve() + { + await CreateTextAsync(ids1[0], "zh", "東京大学"); - await SearchText(expected: ids1, text: "東京"); - } + await SearchText(expected: ids1, text: "東京"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexFixture.cs index 36297ab757..18ae8c3af5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexFixture.cs @@ -11,30 +11,29 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class MongoTextIndexFixture : IAsyncLifetime { - public sealed class MongoTextIndexFixture : IAsyncLifetime - { - public MongoTextIndex Index { get; } + public MongoTextIndex Index { get; } - public MongoTextIndexFixture() - { - TestUtils.SetupBson(); + public MongoTextIndexFixture() + { + TestUtils.SetupBson(); - var mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); - var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); + var mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); + var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); - Index = new MongoTextIndex(mongoDatabase); - } + Index = new MongoTextIndex(mongoDatabase); + } - public Task InitializeAsync() - { - return Index.InitializeAsync(default); - } + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } - public Task DisposeAsync() - { - return Task.CompletedTask; - } + public Task DisposeAsync() + { + return Task.CompletedTask; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexTests.cs index 2318997990..273499cc0a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexTests.cs @@ -9,47 +9,46 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +[Trait("Category", "Dependencies")] +public class MongoTextIndexTests : TextIndexerTestsBase, IClassFixture<MongoTextIndexFixture> { - [Trait("Category", "Dependencies")] - public class MongoTextIndexTests : TextIndexerTestsBase, IClassFixture<MongoTextIndexFixture> - { - public override bool SupportsQuerySyntax => false; + public override bool SupportsQuerySyntax => false; - public override bool SupportsGeo => true; + public override bool SupportsGeo => true; - public MongoTextIndexFixture _ { get; } + public MongoTextIndexFixture _ { get; } - public MongoTextIndexTests(MongoTextIndexFixture fixture) - { - _ = fixture; - } + public MongoTextIndexTests(MongoTextIndexFixture fixture) + { + _ = fixture; + } - public override ITextIndex CreateIndex() - { - return _.Index; - } + public override ITextIndex CreateIndex() + { + return _.Index; + } - [Fact] - public async Task Should_retrieve_all_stopwords_for_english_query() - { - var both = ids2.Union(ids1).ToList(); + [Fact] + public async Task Should_retrieve_all_stopwords_for_english_query() + { + var both = ids2.Union(ids1).ToList(); - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: both, text: "and"); - } + await SearchText(expected: both, text: "and"); + } - [Fact] - public async Task Should_retrieve_all_stopwords_for_german_query() - { - var both = ids2.Union(ids1).ToList(); + [Fact] + public async Task Should_retrieve_all_stopwords_for_german_query() + { + var both = ids2.Union(ids1).ToList(); - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: both, text: "und"); - } + await SearchText(expected: both, text: "und"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexFixture.cs index fc39410746..821a9e3dae 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexFixture.cs @@ -10,28 +10,27 @@ using Squidex.Extensions.Text.ElasticSearch; using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class OpenSearchTextIndexFixture : IAsyncLifetime { - public sealed class OpenSearchTextIndexFixture : IAsyncLifetime - { - public ElasticSearchTextIndex Index { get; } + public ElasticSearchTextIndex Index { get; } - public OpenSearchTextIndexFixture() - { - Index = new ElasticSearchTextIndex( - new OpenSearchClient(TestConfig.Configuration["elastic:configuration"]), - TestConfig.Configuration["elastic:indexName"], - TestUtils.DefaultSerializer); - } + public OpenSearchTextIndexFixture() + { + Index = new ElasticSearchTextIndex( + new OpenSearchClient(TestConfig.Configuration["elastic:configuration"]), + TestConfig.Configuration["elastic:indexName"], + TestUtils.DefaultSerializer); + } - public Task InitializeAsync() - { - return Index.InitializeAsync(default); - } + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } - public Task DisposeAsync() - { - return Task.CompletedTask; - } + public Task DisposeAsync() + { + return Task.CompletedTask; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs index cdc693ac22..8714f4fb38 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs @@ -9,51 +9,50 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +[Trait("Category", "Dependencies")] +public class OpenSearchTextIndexTests : TextIndexerTestsBase, IClassFixture<OpenSearchTextIndexFixture> { - [Trait("Category", "Dependencies")] - public class OpenSearchTextIndexTests : TextIndexerTestsBase, IClassFixture<OpenSearchTextIndexFixture> - { - public override bool SupportsGeo => true; + public override bool SupportsGeo => true; - public override int WaitAfterUpdate => 2000; + public override int WaitAfterUpdate => 2000; - public ElasticSearchTextIndexFixture _ { get; } + public ElasticSearchTextIndexFixture _ { get; } - public OpenSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) - { - _ = fixture; - } + public OpenSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) + { + _ = fixture; + } - public override ITextIndex CreateIndex() - { - return _.Index; - } + public override ITextIndex CreateIndex() + { + return _.Index; + } - [Fact] - public async Task Should_retrieve_english_stopword_only_for_german_query() - { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + [Fact] + public async Task Should_retrieve_english_stopword_only_for_german_query() + { + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: ids2, text: "und"); - } + await SearchText(expected: ids2, text: "und"); + } - [Fact] - public async Task Should_retrieve_german_stopword_only_for_english_query() - { - await CreateTextAsync(ids1[0], "de", "and und"); - await CreateTextAsync(ids2[0], "en", "and und"); + [Fact] + public async Task Should_retrieve_german_stopword_only_for_english_query() + { + await CreateTextAsync(ids1[0], "de", "and und"); + await CreateTextAsync(ids2[0], "en", "and und"); - await SearchText(expected: ids1, text: "and"); - } + await SearchText(expected: ids1, text: "and"); + } - [Fact] - public async Task Should_index_cjk_content_and_retrieve() - { - await CreateTextAsync(ids1[0], "zh", "東京大学"); + [Fact] + public async Task Should_index_cjk_content_and_retrieve() + { + await CreateTextAsync(ids1[0], "zh", "東京大学"); - await SearchText(expected: ids1, text: "東京"); - } + await SearchText(expected: ids1, text: "東京"); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/QueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/QueryParserTests.cs index 0ca27ecbd9..87d39d26cf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/QueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/QueryParserTests.cs @@ -7,42 +7,41 @@ using Xunit; -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public class QueryParserTests { - public class QueryParserTests - { - private readonly QueryParser sut = new QueryParser(x => $"texts.{x}"); + private readonly QueryParser sut = new QueryParser(x => $"texts.{x}"); - [Fact] - public void Should_prefix_field_query() - { - var source = "en:Hello"; + [Fact] + public void Should_prefix_field_query() + { + var source = "en:Hello"; - Assert.Equal("texts.en:Hello", sut.Parse(source)?.Text); - } + Assert.Equal("texts.en:Hello", sut.Parse(source)?.Text); + } - [Fact] - public void Should_prefix_field_with_complex_language() - { - var source = "en-EN:Hello"; + [Fact] + public void Should_prefix_field_with_complex_language() + { + var source = "en-EN:Hello"; - Assert.Equal("texts.en-EN:Hello", sut.Parse(source)?.Text); - } + Assert.Equal("texts.en-EN:Hello", sut.Parse(source)?.Text); + } - [Fact] - public void Should_prefix_field_query_within_query() - { - var source = "Hello en:World"; + [Fact] + public void Should_prefix_field_query_within_query() + { + var source = "Hello en:World"; - Assert.Equal("Hello texts.en:World", sut.Parse(source)?.Text); - } + Assert.Equal("Hello texts.en:World", sut.Parse(source)?.Text); + } - [Fact] - public void Should_prefix_field_query_within_complex_query() - { - var source = "Hallo OR (Hello en:World)"; + [Fact] + public void Should_prefix_field_query_within_complex_query() + { + var source = "Hallo OR (Hello en:World)"; - Assert.Equal("Hallo OR (Hello texts.en:World)", sut.Parse(source)?.Text); - } + Assert.Equal("Hallo OR (Hello texts.en:World)", sut.Parse(source)?.Text); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs index d8111f36e3..138b2d9eb8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTestsBase.cs @@ -19,459 +19,458 @@ #pragma warning disable SA1401 // Fields should be private -namespace Squidex.Domain.Apps.Entities.Contents.Text +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public abstract class TextIndexerTestsBase { - public abstract class TextIndexerTestsBase - { - protected readonly List<DomainId> ids1 = new List<DomainId> { DomainId.NewGuid() }; - protected readonly List<DomainId> ids2 = new List<DomainId> { DomainId.NewGuid() }; + protected readonly List<DomainId> ids1 = new List<DomainId> { DomainId.NewGuid() }; + protected readonly List<DomainId> ids2 = new List<DomainId> { DomainId.NewGuid() }; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly IAppEntity app; - private readonly Lazy<TextIndexingProcess> sut; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly IAppEntity app; + private readonly Lazy<TextIndexingProcess> sut; - protected TextIndexingProcess Sut - { - get { return sut.Value; } - } + protected TextIndexingProcess Sut + { + get { return sut.Value; } + } - public virtual bool SupportsQuerySyntax => true; + public virtual bool SupportsQuerySyntax => true; - public virtual bool SupportsGeo => false; + public virtual bool SupportsGeo => false; - public virtual int WaitAfterUpdate => 0; + public virtual int WaitAfterUpdate => 0; - protected TextIndexerTestsBase() - { - app = - Mocks.App(appId, - Language.DE, - Language.EN); + protected TextIndexerTestsBase() + { + app = + Mocks.App(appId, + Language.DE, + Language.EN); - sut = new Lazy<TextIndexingProcess>(CreateSut); - } + sut = new Lazy<TextIndexingProcess>(CreateSut); + } - private TextIndexingProcess CreateSut() - { - var index = CreateIndex(); + private TextIndexingProcess CreateSut() + { + var index = CreateIndex(); - return new TextIndexingProcess(TestUtils.DefaultSerializer, index, new InMemoryTextIndexerState()); - } + return new TextIndexingProcess(TestUtils.DefaultSerializer, index, new InMemoryTextIndexerState()); + } - public abstract ITextIndex CreateIndex(); + public abstract ITextIndex CreateIndex(); - [Fact] - public async Task Should_search_with_fuzzy() + [Fact] + public async Task Should_search_with_fuzzy() + { + if (!SupportsQuerySyntax) { - if (!SupportsQuerySyntax) - { - return; - } + return; + } - await CreateTextAsync(ids1[0], "iv", "Hello"); + await CreateTextAsync(ids1[0], "iv", "Hello"); - await SearchText(expected: ids1, text: "helo~"); - } + await SearchText(expected: ids1, text: "helo~"); + } - [Fact] - public async Task Should_search_by_field() + [Fact] + public async Task Should_search_by_field() + { + if (!SupportsQuerySyntax) { - if (!SupportsQuerySyntax) - { - return; - } + return; + } - await CreateTextAsync(ids1[0], "en", "City"); + await CreateTextAsync(ids1[0], "en", "City"); - await SearchText(expected: ids1, text: "en:city"); - } + await SearchText(expected: ids1, text: "en:city"); + } - [Fact] - public async Task Should_search_by_geo() + [Fact] + public async Task Should_search_by_geo() + { + if (!SupportsGeo) { - if (!SupportsGeo) - { - return; - } + return; + } - var field = Guid.NewGuid().ToString(); + var field = Guid.NewGuid().ToString(); - // Within search radius - await CreateGeoAsync(ids1[0], field, 51.343391192211506, 12.401476788622826); + // Within search radius + await CreateGeoAsync(ids1[0], field, 51.343391192211506, 12.401476788622826); - // Outside of search radius - await CreateGeoAsync(ids2[0], field, 51.30765141427311, 12.379631713912486); + // Outside of search radius + await CreateGeoAsync(ids2[0], field, 51.30765141427311, 12.379631713912486); - // Within search radius and correct field. - await SearchGeo(expected: ids1, $"{field}.iv", 51.34641682574934, 12.401965298137707); + // Within search radius and correct field. + await SearchGeo(expected: ids1, $"{field}.iv", 51.34641682574934, 12.401965298137707); - // Within search radius but incorrect field. - await SearchGeo(expected: null, "other.iv", 51.48596429889613, 12.102629469505713); - } + // Within search radius but incorrect field. + await SearchGeo(expected: null, "other.iv", 51.48596429889613, 12.102629469505713); + } - [Fact] - public async Task Should_search_by_geojson() + [Fact] + public async Task Should_search_by_geojson() + { + if (!SupportsGeo) { - if (!SupportsGeo) - { - return; - } + return; + } - var field = Guid.NewGuid().ToString(); + var field = Guid.NewGuid().ToString(); - // Within search radius - await CreateGeoJsonAsync(ids1[0], field, 51.343391192211506, 12.401476788622826); + // Within search radius + await CreateGeoJsonAsync(ids1[0], field, 51.343391192211506, 12.401476788622826); - // Outside of search radius - await CreateGeoJsonAsync(ids2[0], field, 51.30765141427311, 12.379631713912486); + // Outside of search radius + await CreateGeoJsonAsync(ids2[0], field, 51.30765141427311, 12.379631713912486); - // Within search radius and correct field. - await SearchGeo(expected: ids1, $"{field}.iv", 51.34641682574934, 12.401965298137707); + // Within search radius and correct field. + await SearchGeo(expected: ids1, $"{field}.iv", 51.34641682574934, 12.401965298137707); - // Within search radius but incorrect field. - await SearchGeo(expected: null, "other.iv", 51.48596429889613, 12.102629469505713); - } + // Within search radius but incorrect field. + await SearchGeo(expected: null, "other.iv", 51.48596429889613, 12.102629469505713); + } - [Fact] - public async Task Should_index_invariant_content_and_retrieve() - { - await CreateTextAsync(ids1[0], "iv", "Hello"); - await CreateTextAsync(ids2[0], "iv", "World"); + [Fact] + public async Task Should_index_invariant_content_and_retrieve() + { + await CreateTextAsync(ids1[0], "iv", "Hello"); + await CreateTextAsync(ids2[0], "iv", "World"); - await SearchText(expected: ids1, text: "Hello"); - await SearchText(expected: ids2, text: "World"); + await SearchText(expected: ids1, text: "Hello"); + await SearchText(expected: ids2, text: "World"); - await SearchText(expected: null, text: "Hello", SearchScope.Published); - await SearchText(expected: null, text: "World", SearchScope.Published); - } + await SearchText(expected: null, text: "Hello", SearchScope.Published); + await SearchText(expected: null, text: "World", SearchScope.Published); + } - [Fact] - public async Task Should_update_draft_only() - { - await CreateTextAsync(ids1[0], "iv", "V1"); + [Fact] + public async Task Should_update_draft_only() + { + await CreateTextAsync(ids1[0], "iv", "V1"); - await UpdateTextAsync(ids1[0], "iv", "V2"); + await UpdateTextAsync(ids1[0], "iv", "V2"); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: null, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "V1", target: SearchScope.All); + await SearchText(expected: null, text: "V1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); - } + await SearchText(expected: ids1, text: "V2", target: SearchScope.All); + await SearchText(expected: null, text: "V2", target: SearchScope.Published); + } - [Fact] - public async Task Should_update_draft_only_multiple_times() - { - await CreateTextAsync(ids1[0], "iv", "V1"); + [Fact] + public async Task Should_update_draft_only_multiple_times() + { + await CreateTextAsync(ids1[0], "iv", "V1"); - await UpdateTextAsync(ids1[0], "iv", "V2"); - await UpdateTextAsync(ids1[0], "iv", "V3"); + await UpdateTextAsync(ids1[0], "iv", "V2"); + await UpdateTextAsync(ids1[0], "iv", "V3"); - await SearchText(expected: null, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: null, text: "V2", target: SearchScope.All); + await SearchText(expected: null, text: "V2", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V3", target: SearchScope.All); - await SearchText(expected: null, text: "V3", target: SearchScope.Published); - } + await SearchText(expected: ids1, text: "V3", target: SearchScope.All); + await SearchText(expected: null, text: "V3", target: SearchScope.Published); + } - [Fact] - public async Task Should_also_serve_published_after_publish() - { - await CreateTextAsync(ids1[0], "iv", "V1"); + [Fact] + public async Task Should_also_serve_published_after_publish() + { + await CreateTextAsync(ids1[0], "iv", "V1"); - await PublishAsync(ids1[0]); + await PublishAsync(ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); - } + await SearchText(expected: ids1, text: "V1", target: SearchScope.All); + await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + } - [Fact] - public async Task Should_also_update_published_content() - { - await CreateTextAsync(ids1[0], "iv", "V1"); + [Fact] + public async Task Should_also_update_published_content() + { + await CreateTextAsync(ids1[0], "iv", "V1"); - await PublishAsync(ids1[0]); + await PublishAsync(ids1[0]); - await UpdateTextAsync(ids1[0], "iv", "V2"); + await UpdateTextAsync(ids1[0], "iv", "V2"); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: null, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "V1", target: SearchScope.All); + await SearchText(expected: null, text: "V1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: ids1, text: "V2", target: SearchScope.Published); - } + await SearchText(expected: ids1, text: "V2", target: SearchScope.All); + await SearchText(expected: ids1, text: "V2", target: SearchScope.Published); + } - [Fact] - public async Task Should_also_update_published_content_multiple_times() - { - await CreateTextAsync(ids1[0], "iv", "V1"); + [Fact] + public async Task Should_also_update_published_content_multiple_times() + { + await CreateTextAsync(ids1[0], "iv", "V1"); - await PublishAsync(ids1[0]); + await PublishAsync(ids1[0]); - await UpdateTextAsync(ids1[0], "iv", "V2"); - await UpdateTextAsync(ids1[0], "iv", "V3"); + await UpdateTextAsync(ids1[0], "iv", "V2"); + await UpdateTextAsync(ids1[0], "iv", "V3"); - await SearchText(expected: null, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: null, text: "V2", target: SearchScope.All); + await SearchText(expected: null, text: "V2", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V3", target: SearchScope.All); - await SearchText(expected: ids1, text: "V3", target: SearchScope.Published); - } + await SearchText(expected: ids1, text: "V3", target: SearchScope.All); + await SearchText(expected: ids1, text: "V3", target: SearchScope.Published); + } - [Fact] - public async Task Should_simulate_new_version() - { - await CreateTextAsync(ids1[0], "iv", "V1"); + [Fact] + public async Task Should_simulate_new_version() + { + await CreateTextAsync(ids1[0], "iv", "V1"); - // Publish the content. - await PublishAsync(ids1[0]); + // Publish the content. + await PublishAsync(ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: ids1, text: "V1", target: SearchScope.All); + await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); - // Create a new version, the value is still the same as old version. - await CreateDraftAsync(ids1[0]); + // Create a new version, the value is still the same as old version. + await CreateDraftAsync(ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: ids1, text: "V1", target: SearchScope.All); + await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); - // Make an update, this updates the new version only. - await UpdateTextAsync(ids1[0], "iv", "V2"); + // Make an update, this updates the new version only. + await UpdateTextAsync(ids1[0], "iv", "V2"); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "V1", target: SearchScope.All); + await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: ids1, text: "V2", target: SearchScope.All); + await SearchText(expected: null, text: "V2", target: SearchScope.Published); - // Publish the new version to get rid of the "V1" version. - await PublishAsync(ids1[0]); + // Publish the new version to get rid of the "V1" version. + await PublishAsync(ids1[0]); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: null, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "V1", target: SearchScope.All); + await SearchText(expected: null, text: "V1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: ids1, text: "V2", target: SearchScope.Published); + await SearchText(expected: ids1, text: "V2", target: SearchScope.All); + await SearchText(expected: ids1, text: "V2", target: SearchScope.Published); - // Unpublish the version - await UnpublishAsync(ids1[0]); + // Unpublish the version + await UnpublishAsync(ids1[0]); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); - } + await SearchText(expected: ids1, text: "V2", target: SearchScope.All); + await SearchText(expected: null, text: "V2", target: SearchScope.Published); + } - [Fact] - public async Task Should_simulate_new_version_with_migration() - { - await CreateTextAsync(ids1[0], "iv", "V1"); + [Fact] + public async Task Should_simulate_new_version_with_migration() + { + await CreateTextAsync(ids1[0], "iv", "V1"); - // Publish the content. - await PublishAsync(ids1[0]); + // Publish the content. + await PublishAsync(ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: ids1, text: "V1", target: SearchScope.All); + await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); - // Create a new version, his updates the new version also. - await CreateDraftWithTextAsync(ids1[0], "iv", "V2"); + // Create a new version, his updates the new version also. + await CreateDraftWithTextAsync(ids1[0], "iv", "V2"); - await SearchText(expected: null, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: null, text: "V1", target: SearchScope.All); + await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); - await SearchText(expected: ids1, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); - } + await SearchText(expected: ids1, text: "V2", target: SearchScope.All); + await SearchText(expected: null, text: "V2", target: SearchScope.Published); + } - [Fact] - public async Task Should_simulate_content_reversion() - { - await CreateTextAsync(ids1[0], "iv", "V1"); + [Fact] + public async Task Should_simulate_content_reversion() + { + await CreateTextAsync(ids1[0], "iv", "V1"); - // Publish the content. - await PublishAsync(ids1[0]); + // Publish the content. + await PublishAsync(ids1[0]); - // Create a new version, the value is still the same as old version. - await CreateDraftAsync(ids1[0]); + // Create a new version, the value is still the same as old version. + await CreateDraftAsync(ids1[0]); - // Make an update, this updates the new version only. - await UpdateTextAsync(ids1[0], "iv", "V2"); + // Make an update, this updates the new version only. + await UpdateTextAsync(ids1[0], "iv", "V2"); - // Make an update, this updates the new version only. - await DeleteDraftAsync(ids1[0]); + // Make an update, this updates the new version only. + await DeleteDraftAsync(ids1[0]); - await SearchText(expected: ids1, text: "V1", target: SearchScope.All); - await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); + await SearchText(expected: ids1, text: "V1", target: SearchScope.All); + await SearchText(expected: ids1, text: "V1", target: SearchScope.Published); - await SearchText(expected: null, text: "V2", target: SearchScope.All); - await SearchText(expected: null, text: "V2", target: SearchScope.Published); + await SearchText(expected: null, text: "V2", target: SearchScope.All); + await SearchText(expected: null, text: "V2", target: SearchScope.Published); - // Make an update, this updates the current version only. - await UpdateTextAsync(ids1[0], "iv", "V3"); + // Make an update, this updates the current version only. + await UpdateTextAsync(ids1[0], "iv", "V3"); - await SearchText(expected: ids1, text: "V3", target: SearchScope.All); - await SearchText(expected: ids1, text: "V3", target: SearchScope.Published); - } + await SearchText(expected: ids1, text: "V3", target: SearchScope.All); + await SearchText(expected: ids1, text: "V3", target: SearchScope.Published); + } - [Fact] - public async Task Should_delete_documents_from_index() - { - await CreateTextAsync(ids1[0], "iv", "V1_1"); - await CreateTextAsync(ids2[0], "iv", "V2_1"); + [Fact] + public async Task Should_delete_documents_from_index() + { + await CreateTextAsync(ids1[0], "iv", "V1_1"); + await CreateTextAsync(ids2[0], "iv", "V2_1"); - await SearchText(expected: ids1, text: "V1_1"); - await SearchText(expected: ids2, text: "V2_1"); + await SearchText(expected: ids1, text: "V1_1"); + await SearchText(expected: ids2, text: "V2_1"); - await DeleteAsync(ids1[0]); + await DeleteAsync(ids1[0]); - await SearchText(expected: null, text: "V1_1"); - await SearchText(expected: ids2, text: "V2_1"); - } + await SearchText(expected: null, text: "V1_1"); + await SearchText(expected: ids2, text: "V2_1"); + } - [Fact] - public async Task Should_index_invalid_geodata() - { - await CreateGeoAsync(ids1[0], "field", 144.34, -200); - } + [Fact] + public async Task Should_index_invalid_geodata() + { + await CreateGeoAsync(ids1[0], "field", 144.34, -200); + } - [Fact] - public async Task Should_index_invalid_geojsondata() - { - await CreateGeoJsonAsync(ids1[0], "field", 144.34, -200); - } + [Fact] + public async Task Should_index_invalid_geojsondata() + { + await CreateGeoJsonAsync(ids1[0], "field", 144.34, -200); + } - protected Task CreateTextAsync(DomainId id, string language, string text) - { - var data = TextData(language, text); + protected Task CreateTextAsync(DomainId id, string language, string text) + { + var data = TextData(language, text); - return UpdateAsync(id, new ContentCreated { Data = data }); - } + return UpdateAsync(id, new ContentCreated { Data = data }); + } - protected Task CreateGeoAsync(DomainId id, string field, double latitude, double longitude) - { - var data = GeoData(field, latitude, longitude); + protected Task CreateGeoAsync(DomainId id, string field, double latitude, double longitude) + { + var data = GeoData(field, latitude, longitude); - return UpdateAsync(id, new ContentCreated { Data = data }); - } + return UpdateAsync(id, new ContentCreated { Data = data }); + } - protected Task CreateGeoJsonAsync(DomainId id, string field, double latitude, double longitude) - { - var data = GeoJsonData(field, latitude, longitude); + protected Task CreateGeoJsonAsync(DomainId id, string field, double latitude, double longitude) + { + var data = GeoJsonData(field, latitude, longitude); - return UpdateAsync(id, new ContentCreated { Data = data }); - } + return UpdateAsync(id, new ContentCreated { Data = data }); + } - protected Task UpdateTextAsync(DomainId id, string language, string text) - { - var data = TextData(language, text); + protected Task UpdateTextAsync(DomainId id, string language, string text) + { + var data = TextData(language, text); - return UpdateAsync(id, new ContentUpdated { Data = data }); - } + return UpdateAsync(id, new ContentUpdated { Data = data }); + } - protected Task CreateDraftWithTextAsync(DomainId id, string language, string text) - { - var data = TextData(language, text); + protected Task CreateDraftWithTextAsync(DomainId id, string language, string text) + { + var data = TextData(language, text); - return UpdateAsync(id, new ContentDraftCreated { MigratedData = data }); - } + return UpdateAsync(id, new ContentDraftCreated { MigratedData = data }); + } - protected Task CreateDraftAsync(DomainId id) - { - return UpdateAsync(id, new ContentDraftCreated()); - } + protected Task CreateDraftAsync(DomainId id) + { + return UpdateAsync(id, new ContentDraftCreated()); + } - protected Task PublishAsync(DomainId id) - { - return UpdateAsync(id, new ContentStatusChanged { Status = Status.Published }); - } + protected Task PublishAsync(DomainId id) + { + return UpdateAsync(id, new ContentStatusChanged { Status = Status.Published }); + } - protected Task UnpublishAsync(DomainId id) - { - return UpdateAsync(id, new ContentStatusChanged { Status = Status.Draft }); - } + protected Task UnpublishAsync(DomainId id) + { + return UpdateAsync(id, new ContentStatusChanged { Status = Status.Draft }); + } - protected Task DeleteDraftAsync(DomainId id) - { - return UpdateAsync(id, new ContentDraftDeleted()); - } + protected Task DeleteDraftAsync(DomainId id) + { + return UpdateAsync(id, new ContentDraftDeleted()); + } - protected Task DeleteAsync(DomainId id) - { - return UpdateAsync(id, new ContentDeleted()); - } + protected Task DeleteAsync(DomainId id) + { + return UpdateAsync(id, new ContentDeleted()); + } - private async Task UpdateAsync(DomainId id, ContentEvent contentEvent) - { - contentEvent.ContentId = id; - contentEvent.AppId = appId; - contentEvent.SchemaId = schemaId; + private async Task UpdateAsync(DomainId id, ContentEvent contentEvent) + { + contentEvent.ContentId = id; + contentEvent.AppId = appId; + contentEvent.SchemaId = schemaId; - await Sut.On(Enumerable.Repeat(Envelope.Create<IEvent>(contentEvent), 1)); + await Sut.On(Enumerable.Repeat(Envelope.Create<IEvent>(contentEvent), 1)); - await Task.Delay(WaitAfterUpdate); - } + await Task.Delay(WaitAfterUpdate); + } - private static ContentData TextData(string language, string text) + private static ContentData TextData(string language, string text) + { + return new ContentData() + .AddField("text", + new ContentFieldData() + .AddLocalized(language, text)); + } + + private static ContentData GeoData(string field, double latitude, double longitude) + { + return new ContentData() + .AddField(field, + new ContentFieldData() + .AddInvariant(new JsonObject().Add("latitude", latitude).Add("longitude", longitude))); + } + + private static ContentData GeoJsonData(string field, double latitude, double longitude) + { + return new ContentData() + .AddField(field, + new ContentFieldData() + .AddInvariant(new JsonObject() + .Add("type", "Point") + .Add("coordinates", + new JsonArray() + .Add(longitude) + .Add(latitude)))); + } + + protected async Task SearchGeo(List<DomainId>? expected, string field, double latitude, double longitude, SearchScope target = SearchScope.All) + { + var query = new GeoQuery(schemaId.Id, field, latitude, longitude, 1000, 1000); + + var actual = await Sut.TextIndex.SearchAsync(app, query, target); + + if (expected != null) { - return new ContentData() - .AddField("text", - new ContentFieldData() - .AddLocalized(language, text)); + actual.Should().BeEquivalentTo(expected.ToHashSet()); } - - private static ContentData GeoData(string field, double latitude, double longitude) + else { - return new ContentData() - .AddField(field, - new ContentFieldData() - .AddInvariant(new JsonObject().Add("latitude", latitude).Add("longitude", longitude))); + actual.Should().BeEmpty(); } + } - private static ContentData GeoJsonData(string field, double latitude, double longitude) + protected async Task SearchText(List<DomainId>? expected, string text, SearchScope target = SearchScope.All) + { + var query = new TextQuery(text, 1000) { - return new ContentData() - .AddField(field, - new ContentFieldData() - .AddInvariant(new JsonObject() - .Add("type", "Point") - .Add("coordinates", - new JsonArray() - .Add(longitude) - .Add(latitude)))); - } + RequiredSchemaIds = new List<DomainId> { schemaId.Id } + }; - protected async Task SearchGeo(List<DomainId>? expected, string field, double latitude, double longitude, SearchScope target = SearchScope.All) + var actual = await Sut.TextIndex.SearchAsync(app, query, target); + + if (expected != null) { - var query = new GeoQuery(schemaId.Id, field, latitude, longitude, 1000, 1000); - - var actual = await Sut.TextIndex.SearchAsync(app, query, target); - - if (expected != null) - { - actual.Should().BeEquivalentTo(expected.ToHashSet()); - } - else - { - actual.Should().BeEmpty(); - } + actual.Should().BeEquivalentTo(expected.ToHashSet()); } - - protected async Task SearchText(List<DomainId>? expected, string text, SearchScope target = SearchScope.All) + else { - var query = new TextQuery(text, 1000) - { - RequiredSchemaIds = new List<DomainId> { schemaId.Id } - }; - - var actual = await Sut.TextIndex.SearchAsync(app, query, target); - - if (expected != null) - { - actual.Should().BeEquivalentTo(expected.ToHashSet()); - } - else - { - actual.Should().BeEmpty(); - } + actual.Should().BeEmpty(); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs index e0f9e49451..67e1f098e4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs @@ -20,319 +20,318 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Invitation +namespace Squidex.Domain.Apps.Entities.Invitation; + +public class InvitationEventConsumerTests { - public class InvitationEventConsumerTests + private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly ILogger<InvitationEventConsumer> log = A.Fake<ILogger<InvitationEventConsumer>>(); + private readonly ITeamEntity team = Mocks.Team(DomainId.NewGuid()); + private readonly IUser assignee = UserMocks.User("2"); + private readonly IUser assigner = UserMocks.User("1"); + private readonly IUserNotifications userNotifications = A.Fake<IUserNotifications>(); + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly string assignerId = DomainId.NewGuid().ToString(); + private readonly string assigneeId = DomainId.NewGuid().ToString(); + private readonly InvitationEventConsumer sut; + + public InvitationEventConsumerTests() { - private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly ILogger<InvitationEventConsumer> log = A.Fake<ILogger<InvitationEventConsumer>>(); - private readonly ITeamEntity team = Mocks.Team(DomainId.NewGuid()); - private readonly IUser assignee = UserMocks.User("2"); - private readonly IUser assigner = UserMocks.User("1"); - private readonly IUserNotifications userNotifications = A.Fake<IUserNotifications>(); - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly string assignerId = DomainId.NewGuid().ToString(); - private readonly string assigneeId = DomainId.NewGuid().ToString(); - private readonly InvitationEventConsumer sut; - - public InvitationEventConsumerTests() - { - A.CallTo(() => userNotifications.IsActive) - .Returns(true); + A.CallTo(() => userNotifications.IsActive) + .Returns(true); - A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) - .Returns(assigner); + A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) + .Returns(assigner); - A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) - .Returns(assignee); + A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) + .Returns(assignee); - A.CallTo(() => appProvider.GetAppAsync(app.Id, true, default)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(app.Id, true, default)) + .Returns(app); - A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(team.Id, default)) + .Returns(team); - sut = new InvitationEventConsumer(appProvider, userNotifications, userResolver, log); - } + sut = new InvitationEventConsumer(appProvider, userNotifications, userResolver, log); + } - [Fact] - public async Task Should_not_send_app_email_if_contributors_assigned_by_clients() - { - var @event = CreateAppEvent(RefTokenType.Client, true); + [Fact] + public async Task Should_not_send_app_email_if_contributors_assigned_by_clients() + { + var @event = CreateAppEvent(RefTokenType.Client, true); - await sut.On(@event); + await sut.On(@event); - MustNotResolveUser(); - MustNotSendEmail(); - } + MustNotResolveUser(); + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_app_email_for_initial_owner() - { - var @event = CreateAppEvent(RefTokenType.Subject, false, streamNumber: 1); + [Fact] + public async Task Should_not_send_app_email_for_initial_owner() + { + var @event = CreateAppEvent(RefTokenType.Subject, false, streamNumber: 1); - await sut.On(@event); + await sut.On(@event); - MustNotSendEmail(); - } + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_team_email_for_initial_owner() - { - var @event = CreateTeamEvent(false, streamNumber: 1); + [Fact] + public async Task Should_not_send_team_email_for_initial_owner() + { + var @event = CreateTeamEvent(false, streamNumber: 1); - await sut.On(@event); + await sut.On(@event); - MustNotSendEmail(); - } + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_app_email_for_old_events() - { - var created = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50)); + [Fact] + public async Task Should_not_send_app_email_for_old_events() + { + var created = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50)); - var @event = CreateAppEvent(RefTokenType.Subject, true, instant: created); + var @event = CreateAppEvent(RefTokenType.Subject, true, instant: created); - await sut.On(@event); + await sut.On(@event); - MustNotResolveUser(); - MustNotSendEmail(); - } + MustNotResolveUser(); + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_team_email_for_old_events() - { - var created = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50)); + [Fact] + public async Task Should_not_send_team_email_for_old_events() + { + var created = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50)); - var @event = CreateTeamEvent(true, instant: created); + var @event = CreateTeamEvent(true, instant: created); - await sut.On(@event); + await sut.On(@event); - MustNotResolveUser(); - MustNotSendEmail(); - } + MustNotResolveUser(); + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_app_email_for_old_contributor() - { - var @event = CreateAppEvent(RefTokenType.Subject, true, isNewContributor: false); + [Fact] + public async Task Should_not_send_app_email_for_old_contributor() + { + var @event = CreateAppEvent(RefTokenType.Subject, true, isNewContributor: false); - await sut.On(@event); + await sut.On(@event); - MustNotResolveUser(); - MustNotSendEmail(); - } + MustNotResolveUser(); + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_team_email_for_old_contributor() - { - var @event = CreateTeamEvent(true, isNewContributor: false); + [Fact] + public async Task Should_not_send_team_email_for_old_contributor() + { + var @event = CreateTeamEvent(true, isNewContributor: false); - await sut.On(@event); + await sut.On(@event); - MustNotResolveUser(); - MustNotSendEmail(); - } + MustNotResolveUser(); + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_app_email_if_sender_not_active() - { - var @event = CreateAppEvent(RefTokenType.Subject, true); + [Fact] + public async Task Should_not_send_app_email_if_sender_not_active() + { + var @event = CreateAppEvent(RefTokenType.Subject, true); - A.CallTo(() => userNotifications.IsActive) - .Returns(false); + A.CallTo(() => userNotifications.IsActive) + .Returns(false); - await sut.On(@event); + await sut.On(@event); - MustNotResolveUser(); - MustNotSendEmail(); - } + MustNotResolveUser(); + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_team_email_if_sender_not_active() - { - var @event = CreateTeamEvent(true); + [Fact] + public async Task Should_not_send_team_email_if_sender_not_active() + { + var @event = CreateTeamEvent(true); - A.CallTo(() => userNotifications.IsActive) - .Returns(false); + A.CallTo(() => userNotifications.IsActive) + .Returns(false); - await sut.On(@event); + await sut.On(@event); - MustNotResolveUser(); - MustNotSendEmail(); - } + MustNotResolveUser(); + MustNotSendEmail(); + } - [Fact] - public async Task Should_not_send_app_email_if_assigner_not_found() - { - var @event = CreateAppEvent(RefTokenType.Subject, true); + [Fact] + public async Task Should_not_send_app_email_if_assigner_not_found() + { + var @event = CreateAppEvent(RefTokenType.Subject, true); - A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) - .Returns(Task.FromResult<IUser?>(null)); + A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) + .Returns(Task.FromResult<IUser?>(null)); - await sut.On(@event); + await sut.On(@event); - MustNotSendEmail(); - MustLogWarning(); - } + MustNotSendEmail(); + MustLogWarning(); + } - [Fact] - public async Task Should_not_send_team_email_if_assigner_not_found() - { - var @event = CreateTeamEvent(true); + [Fact] + public async Task Should_not_send_team_email_if_assigner_not_found() + { + var @event = CreateTeamEvent(true); - A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) - .Returns(Task.FromResult<IUser?>(null)); + A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) + .Returns(Task.FromResult<IUser?>(null)); - await sut.On(@event); + await sut.On(@event); - MustNotSendEmail(); - MustLogWarning(); - } + MustNotSendEmail(); + MustLogWarning(); + } - [Fact] - public async Task Should_not_send_app_email_if_assignee_not_found() - { - var @event = CreateAppEvent(RefTokenType.Subject, true); + [Fact] + public async Task Should_not_send_app_email_if_assignee_not_found() + { + var @event = CreateAppEvent(RefTokenType.Subject, true); - A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) - .Returns(Task.FromResult<IUser?>(null)); + A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) + .Returns(Task.FromResult<IUser?>(null)); - await sut.On(@event); + await sut.On(@event); - MustNotSendEmail(); - MustLogWarning(); - } + MustNotSendEmail(); + MustLogWarning(); + } - [Fact] - public async Task Should_not_send_team_email_if_assignee_not_found() - { - var @event = CreateTeamEvent(true); + [Fact] + public async Task Should_not_send_team_email_if_assignee_not_found() + { + var @event = CreateTeamEvent(true); - A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) - .Returns(Task.FromResult<IUser?>(null)); + A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) + .Returns(Task.FromResult<IUser?>(null)); - await sut.On(@event); + await sut.On(@event); - MustNotSendEmail(); - MustLogWarning(); - } + MustNotSendEmail(); + MustLogWarning(); + } - [Fact] - public async Task Should_send_app_email_for_new_user() - { - var @event = CreateAppEvent(RefTokenType.Subject, true); + [Fact] + public async Task Should_send_app_email_for_new_user() + { + var @event = CreateAppEvent(RefTokenType.Subject, true); - await sut.On(@event); + await sut.On(@event); - A.CallTo(() => userNotifications.SendInviteAsync(assigner, assignee, app, default)) - .MustHaveHappened(); - } + A.CallTo(() => userNotifications.SendInviteAsync(assigner, assignee, app, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_send_team_email_for_new_user() - { - var @event = CreateTeamEvent(true); + [Fact] + public async Task Should_send_team_email_for_new_user() + { + var @event = CreateTeamEvent(true); - await sut.On(@event); + await sut.On(@event); - A.CallTo(() => userNotifications.SendInviteAsync(assigner, assignee, team, default)) - .MustHaveHappened(); - } + A.CallTo(() => userNotifications.SendInviteAsync(assigner, assignee, team, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_send_app_email_for_existing_user() - { - var @event = CreateAppEvent(RefTokenType.Subject, false); + [Fact] + public async Task Should_send_app_email_for_existing_user() + { + var @event = CreateAppEvent(RefTokenType.Subject, false); - await sut.On(@event); + await sut.On(@event); - A.CallTo(() => userNotifications.SendInviteAsync(assigner, assignee, app, default)) - .MustHaveHappened(); - } + A.CallTo(() => userNotifications.SendInviteAsync(assigner, assignee, app, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_send_team_email_for_existing_user() - { - var @event = CreateTeamEvent(false); + [Fact] + public async Task Should_send_team_email_for_existing_user() + { + var @event = CreateTeamEvent(false); - await sut.On(@event); + await sut.On(@event); - A.CallTo(() => userNotifications.SendInviteAsync(assigner, assignee, team, default)) - .MustHaveHappened(); - } + A.CallTo(() => userNotifications.SendInviteAsync(assigner, assignee, team, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_send_team_email_if_team_not_found() - { - var @event = CreateTeamEvent(true); + [Fact] + public async Task Should_not_send_team_email_if_team_not_found() + { + var @event = CreateTeamEvent(true); - A.CallTo(() => appProvider.GetTeamAsync(A<DomainId>._, default)) - .Returns(Task.FromResult<ITeamEntity?>(null)); + A.CallTo(() => appProvider.GetTeamAsync(A<DomainId>._, default)) + .Returns(Task.FromResult<ITeamEntity?>(null)); - await sut.On(@event); + await sut.On(@event); - MustNotSendEmail(); - } + MustNotSendEmail(); + } - private void MustLogWarning() - { - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Warning) - .MustHaveHappened(); - } + private void MustLogWarning() + { + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Warning) + .MustHaveHappened(); + } - private void MustNotResolveUser() - { - A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + private void MustNotResolveUser() + { + A.CallTo(() => userResolver.FindByIdAsync(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - private void MustNotSendEmail() - { - A.CallTo(() => userNotifications.SendInviteAsync(A<IUser>._, A<IUser>._, A<IAppEntity>._, default)) - .MustNotHaveHappened(); + private void MustNotSendEmail() + { + A.CallTo(() => userNotifications.SendInviteAsync(A<IUser>._, A<IUser>._, A<IAppEntity>._, default)) + .MustNotHaveHappened(); - A.CallTo(() => userNotifications.SendInviteAsync(A<IUser>._, A<IUser>._, A<IAppEntity>._, default)) - .MustNotHaveHappened(); - } + A.CallTo(() => userNotifications.SendInviteAsync(A<IUser>._, A<IUser>._, A<IAppEntity>._, default)) + .MustNotHaveHappened(); + } - private Envelope<IEvent> CreateAppEvent(RefTokenType assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) + private Envelope<IEvent> CreateAppEvent(RefTokenType assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) + { + var @event = new AppContributorAssigned { - var @event = new AppContributorAssigned - { - Actor = new RefToken(assignerType, assignerId), - AppId = app.NamedId(), - ContributorId = assigneeId, - IsCreated = isNewUser, - IsAdded = isNewContributor - }; + Actor = new RefToken(assignerType, assignerId), + AppId = app.NamedId(), + ContributorId = assigneeId, + IsCreated = isNewUser, + IsAdded = isNewContributor + }; - var envelope = Envelope.Create(@event); + var envelope = Envelope.Create(@event); - envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); - envelope.SetEventStreamNumber(streamNumber); + envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); + envelope.SetEventStreamNumber(streamNumber); - return envelope; - } + return envelope; + } - private Envelope<IEvent> CreateTeamEvent(bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) + private Envelope<IEvent> CreateTeamEvent(bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2) + { + var @event = new TeamContributorAssigned { - var @event = new TeamContributorAssigned - { - Actor = new RefToken(RefTokenType.Subject, assignerId), - ContributorId = assigneeId, - IsCreated = isNewUser, - IsAdded = isNewContributor, - TeamId = team.Id - }; - - var envelope = Envelope.Create(@event); - - envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); - envelope.SetEventStreamNumber(streamNumber); - - return envelope; - } + Actor = new RefToken(RefTokenType.Subject, assignerId), + ContributorId = assigneeId, + IsCreated = isNewUser, + IsAdded = isNewContributor, + TeamId = team.Id + }; + + var envelope = Envelope.Create(@event); + + envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant()); + envelope.SetEventStreamNumber(streamNumber); + + return envelope; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InviteUserCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InviteUserCommandMiddlewareTests.cs index 1452fa4f79..e0b95afbaf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InviteUserCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InviteUserCommandMiddlewareTests.cs @@ -17,145 +17,144 @@ using AssignAppContributor = Squidex.Domain.Apps.Entities.Apps.Commands.AssignContributor; using AssignTeamContributor = Squidex.Domain.Apps.Entities.Teams.Commands.AssignContributor; -namespace Squidex.Domain.Apps.Entities.Invitation +namespace Squidex.Domain.Apps.Entities.Invitation; + +public class InviteUserCommandMiddlewareTests { - public class InviteUserCommandMiddlewareTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + private readonly ITeamEntity team = Mocks.Team(DomainId.NewGuid()); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly InviteUserCommandMiddleware sut; + + public InviteUserCommandMiddlewareTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - private readonly ITeamEntity team = Mocks.Team(DomainId.NewGuid()); - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - private readonly InviteUserCommandMiddleware sut; - - public InviteUserCommandMiddlewareTests() - { - ct = cts.Token; + ct = cts.Token; - sut = new InviteUserCommandMiddleware(userResolver); - } + sut = new InviteUserCommandMiddleware(userResolver); + } - [Fact] - public async Task Should_invite_user_to_app_and_update_command() - { - var command = new AssignAppContributor { ContributorId = "me@email.com", Invite = true }; + [Fact] + public async Task Should_invite_user_to_app_and_update_command() + { + var command = new AssignAppContributor { ContributorId = "me@email.com", Invite = true }; - var context = - new CommandContext(command, commandBus) - .Complete(app); + var context = + new CommandContext(command, commandBus) + .Complete(app); - var user = UserMocks.User("123", command.ContributorId); + var user = UserMocks.User("123", command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) - .Returns((user, true)); + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .Returns((user, true)); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - Assert.Same(context.Result<InvitedResult<IAppEntity>>().Entity, app); - Assert.Equal(user.Id, command.ContributorId); + Assert.Same(context.Result<InvitedResult<IAppEntity>>().Entity, app); + Assert.Equal(user.Id, command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) - .MustHaveHappened(); - } + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_invite_user_to_team_and_update_command() - { - var command = new AssignTeamContributor { ContributorId = "me@email.com", Invite = true }; + [Fact] + public async Task Should_invite_user_to_team_and_update_command() + { + var command = new AssignTeamContributor { ContributorId = "me@email.com", Invite = true }; - var context = - new CommandContext(command, commandBus) - .Complete(team); + var context = + new CommandContext(command, commandBus) + .Complete(team); - var user = UserMocks.User("123", command.ContributorId); + var user = UserMocks.User("123", command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) - .Returns((user, true)); + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .Returns((user, true)); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - Assert.Same(context.Result<InvitedResult<ITeamEntity>>().Entity, team); - Assert.Equal(user.Id, command.ContributorId); + Assert.Same(context.Result<InvitedResult<ITeamEntity>>().Entity, team); + Assert.Equal(user.Id, command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) - .MustHaveHappened(); - } + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_invite_user_to_app_but_do_not_change_command_if_not_created() - { - var command = new AssignAppContributor { ContributorId = "me@email.com", Invite = true }; + [Fact] + public async Task Should_invite_user_to_app_but_do_not_change_command_if_not_created() + { + var command = new AssignAppContributor { ContributorId = "me@email.com", Invite = true }; - var context = - new CommandContext(command, commandBus) - .Complete(app); + var context = + new CommandContext(command, commandBus) + .Complete(app); - var user = UserMocks.User("123", command.ContributorId); + var user = UserMocks.User("123", command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) - .Returns((user, false)); + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .Returns((user, false)); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - Assert.Same(context.Result<IAppEntity>(), app); - Assert.Equal(user.Id, command.ContributorId); + Assert.Same(context.Result<IAppEntity>(), app); + Assert.Equal(user.Id, command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) - .MustHaveHappened(); - } + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_invite_user_to_team_but_do_not_change_command_if_not_created() - { - var command = new AssignTeamContributor { ContributorId = "me@email.com", Invite = true }; + [Fact] + public async Task Should_invite_user_to_team_but_do_not_change_command_if_not_created() + { + var command = new AssignTeamContributor { ContributorId = "me@email.com", Invite = true }; - var context = - new CommandContext(command, commandBus) - .Complete(team); + var context = + new CommandContext(command, commandBus) + .Complete(team); - var user = UserMocks.User("123", command.ContributorId); + var user = UserMocks.User("123", command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) - .Returns((user, false)); + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .Returns((user, false)); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - Assert.Same(context.Result<ITeamEntity>(), team); - Assert.Equal(user.Id, command.ContributorId); + Assert.Same(context.Result<ITeamEntity>(), team); + Assert.Equal(user.Id, command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) - .MustHaveHappened(); - } + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_call_user_resolver_if_not_email() - { - var command = new AssignAppContributor { ContributorId = "123", Invite = true }; + [Fact] + public async Task Should_not_call_user_resolver_if_not_email() + { + var command = new AssignAppContributor { ContributorId = "123", Invite = true }; - var context = - new CommandContext(command, commandBus) - .Complete(app); + var context = + new CommandContext(command, commandBus) + .Complete(app); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A<string>._, A<bool>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A<string>._, A<bool>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_call_user_resolver_if_not_inviting() - { - var command = new AssignAppContributor { ContributorId = "123", Invite = false }; + [Fact] + public async Task Should_not_call_user_resolver_if_not_inviting() + { + var command = new AssignAppContributor { ContributorId = "123", Invite = false }; - var context = - new CommandContext(command, commandBus) - .Complete(app); + var context = + new CommandContext(command, commandBus) + .Complete(app); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A<string>._, A<bool>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A<string>._, A<bool>._, A<CancellationToken>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/EmailUserNotificationsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/EmailUserNotificationsTests.cs index 3b079f66e2..5514f85d46 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/EmailUserNotificationsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/EmailUserNotificationsTests.cs @@ -18,213 +18,212 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Notifications +namespace Squidex.Domain.Apps.Entities.Notifications; + +public class EmailUserNotificationsTests { - public class EmailUserNotificationsTests - { - private readonly IEmailSender emailSender = A.Fake<IEmailSender>(); - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly IUser assigner = UserMocks.User("1", "1@email.com", "user1"); - private readonly IUser assigned = UserMocks.User("2", "2@email.com", "user2"); - private readonly ILogger<EmailUserNotifications> log = A.Fake<ILogger<EmailUserNotifications>>(); - private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - private readonly ITeamEntity team = Mocks.Team(DomainId.NewGuid()); - private readonly EmailUserNotificationOptions texts = new EmailUserNotificationOptions(); - private readonly EmailUserNotifications sut; - - public EmailUserNotificationsTests() - { - A.CallTo(() => urlGenerator.UI()) - .Returns("my-ui"); - - sut = new EmailUserNotifications(Options.Create(texts), emailSender, urlGenerator, log); - } - - [Fact] - public async Task Should_format_assigner_email_and_send_email() - { - await TestInvitationFormattingAsync("Email: $ASSIGNER_EMAIL", "Email: 1@email.com"); - } - - [Fact] - public async Task Should_format_assigner_name_and_send_email() - { - await TestInvitationFormattingAsync("Name: $ASSIGNER_NAME", "Name: user1"); - } - - [Fact] - public async Task Should_format_user_email_and_send_email() - { - await TestInvitationFormattingAsync("Email: $USER_EMAIL", "Email: 2@email.com"); - } - - [Fact] - public async Task Should_format_user_name_and_send_email() - { - await TestInvitationFormattingAsync("Name: $USER_NAME", "Name: user2"); - } - - [Fact] - public async Task Should_format_app_name_and_send_email() - { - await TestInvitationFormattingAsync("App: $APP_NAME", "App: my-app"); - } - - [Fact] - public async Task Should_format_ui_url_and_send_email() - { - await TestInvitationFormattingAsync("UI: $UI_URL", "UI: my-ui"); - } - - [Fact] - public async Task Should_format_api_calls_and_send_email() - { - await TestUsageFormattingAsync("ApiCalls: $API_CALLS", "ApiCalls: 100"); - } - - [Fact] - public async Task Should_format_api_calls_limit_and_send_email() - { - await TestUsageFormattingAsync("ApiCallsLimit: $API_CALLS_LIMIT", "ApiCallsLimit: 120"); - } - - [Fact] - public async Task Should_not_send_app_invitation_email_if_texts_for_new_user_are_empty() - { - await sut.SendInviteAsync(assigner, assigned, app); - - A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - - MustLogWarning(); - } - - [Fact] - public async Task Should_not_send_text_invitation_email_if_texts_for_new_user_are_empty() - { - await sut.SendInviteAsync(assigner, assigned, team); - - A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - - MustLogWarning(); - } - - [Fact] - public async Task Should_not_send_app_invitation_email_if_texts_for_existing_user_are_empty() - { - await sut.SendInviteAsync(assigner, assigned, app); - - A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - - MustLogWarning(); - } - - [Fact] - public async Task Should_not_send_text_invitation_email_if_texts_for_existing_user_are_empty() - { - await sut.SendInviteAsync(assigner, assigned, team); - - A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - - MustLogWarning(); - } + private readonly IEmailSender emailSender = A.Fake<IEmailSender>(); + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly IUser assigner = UserMocks.User("1", "1@email.com", "user1"); + private readonly IUser assigned = UserMocks.User("2", "2@email.com", "user2"); + private readonly ILogger<EmailUserNotifications> log = A.Fake<ILogger<EmailUserNotifications>>(); + private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + private readonly ITeamEntity team = Mocks.Team(DomainId.NewGuid()); + private readonly EmailUserNotificationOptions texts = new EmailUserNotificationOptions(); + private readonly EmailUserNotifications sut; + + public EmailUserNotificationsTests() + { + A.CallTo(() => urlGenerator.UI()) + .Returns("my-ui"); + + sut = new EmailUserNotifications(Options.Create(texts), emailSender, urlGenerator, log); + } + + [Fact] + public async Task Should_format_assigner_email_and_send_email() + { + await TestInvitationFormattingAsync("Email: $ASSIGNER_EMAIL", "Email: 1@email.com"); + } + + [Fact] + public async Task Should_format_assigner_name_and_send_email() + { + await TestInvitationFormattingAsync("Name: $ASSIGNER_NAME", "Name: user1"); + } + + [Fact] + public async Task Should_format_user_email_and_send_email() + { + await TestInvitationFormattingAsync("Email: $USER_EMAIL", "Email: 2@email.com"); + } + + [Fact] + public async Task Should_format_user_name_and_send_email() + { + await TestInvitationFormattingAsync("Name: $USER_NAME", "Name: user2"); + } + + [Fact] + public async Task Should_format_app_name_and_send_email() + { + await TestInvitationFormattingAsync("App: $APP_NAME", "App: my-app"); + } + + [Fact] + public async Task Should_format_ui_url_and_send_email() + { + await TestInvitationFormattingAsync("UI: $UI_URL", "UI: my-ui"); + } + + [Fact] + public async Task Should_format_api_calls_and_send_email() + { + await TestUsageFormattingAsync("ApiCalls: $API_CALLS", "ApiCalls: 100"); + } + + [Fact] + public async Task Should_format_api_calls_limit_and_send_email() + { + await TestUsageFormattingAsync("ApiCallsLimit: $API_CALLS_LIMIT", "ApiCallsLimit: 120"); + } + + [Fact] + public async Task Should_not_send_app_invitation_email_if_texts_for_new_user_are_empty() + { + await sut.SendInviteAsync(assigner, assigned, app); + + A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_text_invitation_email_if_texts_for_new_user_are_empty() + { + await sut.SendInviteAsync(assigner, assigned, team); + + A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_app_invitation_email_if_texts_for_existing_user_are_empty() + { + await sut.SendInviteAsync(assigner, assigned, app); + + A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_text_invitation_email_if_texts_for_existing_user_are_empty() + { + await sut.SendInviteAsync(assigner, assigned, team); + + A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + + MustLogWarning(); + } - [Fact] - public async Task Should_not_send_usage_email_if_texts_empty() - { - await sut.SendUsageAsync(assigned, app, 100, 120); + [Fact] + public async Task Should_not_send_usage_email_if_texts_empty() + { + await sut.SendUsageAsync(assigned, app, 100, 120); + + A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => emailSender.SendAsync(assigned.Email, A<string>._, A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + MustLogWarning(); + } - MustLogWarning(); - } - - [Fact] - public async Task Should_not_send_app_invitation_email_if_no_consent_given() - { - var withoutConsent = UserMocks.User("2", "2@email.com", "user", false); + [Fact] + public async Task Should_not_send_app_invitation_email_if_no_consent_given() + { + var withoutConsent = UserMocks.User("2", "2@email.com", "user", false); - texts.ExistingUserSubject = "email-subject"; - texts.ExistingUserBody = "email-body"; + texts.ExistingUserSubject = "email-subject"; + texts.ExistingUserBody = "email-body"; - await sut.SendInviteAsync(assigner, withoutConsent, app); + await sut.SendInviteAsync(assigner, withoutConsent, app); - A.CallTo(() => emailSender.SendAsync(withoutConsent.Email, "email-subject", "email-body", A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => emailSender.SendAsync(withoutConsent.Email, "email-subject", "email-body", A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_send_team_invitation_email_if_no_consent_given() - { - var withoutConsent = UserMocks.User("2", "2@email.com", "user", false); + [Fact] + public async Task Should_not_send_team_invitation_email_if_no_consent_given() + { + var withoutConsent = UserMocks.User("2", "2@email.com", "user", false); - texts.ExistingTeamUserSubject = "email-subject"; - texts.ExistingTeamUserBody = "email-body"; + texts.ExistingTeamUserSubject = "email-subject"; + texts.ExistingTeamUserBody = "email-body"; - await sut.SendInviteAsync(assigner, withoutConsent, team); + await sut.SendInviteAsync(assigner, withoutConsent, team); - A.CallTo(() => emailSender.SendAsync(withoutConsent.Email, "email-subject", "email-body", A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => emailSender.SendAsync(withoutConsent.Email, "email-subject", "email-body", A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_send_app_invitation_email_if_consent_given() - { - var withConsent = UserMocks.User("2", "2@email.com", "user", true); + [Fact] + public async Task Should_send_app_invitation_email_if_consent_given() + { + var withConsent = UserMocks.User("2", "2@email.com", "user", true); - texts.ExistingUserSubject = "email-subject"; - texts.ExistingUserBody = "email-body"; + texts.ExistingUserSubject = "email-subject"; + texts.ExistingUserBody = "email-body"; - await sut.SendInviteAsync(assigner, withConsent, app); + await sut.SendInviteAsync(assigner, withConsent, app); - A.CallTo(() => emailSender.SendAsync(withConsent.Email, "email-subject", "email-body", A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => emailSender.SendAsync(withConsent.Email, "email-subject", "email-body", A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_send_team_invitation_email_if_consent_given() - { - var withConsent = UserMocks.User("2", "2@email.com", "user", true); + [Fact] + public async Task Should_send_team_invitation_email_if_consent_given() + { + var withConsent = UserMocks.User("2", "2@email.com", "user", true); - texts.ExistingTeamUserSubject = "email-subject"; - texts.ExistingTeamUserBody = "email-body"; + texts.ExistingTeamUserSubject = "email-subject"; + texts.ExistingTeamUserBody = "email-body"; - await sut.SendInviteAsync(assigner, withConsent, team); + await sut.SendInviteAsync(assigner, withConsent, team); - A.CallTo(() => emailSender.SendAsync(withConsent.Email, "email-subject", "email-body", A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => emailSender.SendAsync(withConsent.Email, "email-subject", "email-body", A<CancellationToken>._)) + .MustHaveHappened(); + } - private async Task TestUsageFormattingAsync(string pattern, string actual) - { - texts.UsageSubject = pattern; - texts.UsageBody = pattern; + private async Task TestUsageFormattingAsync(string pattern, string actual) + { + texts.UsageSubject = pattern; + texts.UsageBody = pattern; - await sut.SendUsageAsync(assigned, app, 100, 120); + await sut.SendUsageAsync(assigned, app, 100, 120); - A.CallTo(() => emailSender.SendAsync(assigned.Email, actual, actual, A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => emailSender.SendAsync(assigned.Email, actual, actual, A<CancellationToken>._)) + .MustHaveHappened(); + } - private async Task TestInvitationFormattingAsync(string pattern, string actual) - { - texts.NewUserSubject = pattern; - texts.NewUserBody = pattern; + private async Task TestInvitationFormattingAsync(string pattern, string actual) + { + texts.NewUserSubject = pattern; + texts.NewUserBody = pattern; - await sut.SendInviteAsync(assigner, assigned, app); + await sut.SendInviteAsync(assigner, assigned, app); - A.CallTo(() => emailSender.SendAsync(assigned.Email, actual, actual, A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => emailSender.SendAsync(assigned.Email, actual, actual, A<CancellationToken>._)) + .MustHaveHappened(); + } - private void MustLogWarning() - { - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Warning) - .MustHaveHappened(); - } + private void MustLogWarning() + { + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Warning) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Properties/Resources.Designer.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Properties/Resources.Designer.cs index e018398836..ba0facdd3a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Properties/Resources.Designer.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Properties/Resources.Designer.cs @@ -8,88 +8,87 @@ // </auto-generated> //------------------------------------------------------------------------------ -namespace Squidex.Domain.Apps.Entities.Properties { - using System; +namespace Squidex.Domain.Apps.Entities.Properties; +using System; + + +/// <summary> +/// A strongly-typed resource class, for looking up localized strings, etc. +/// </summary> +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } /// <summary> - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Returns the cached ResourceManager instance used by this class. /// </summary> - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// <summary> - /// Returns the cached ResourceManager instance used by this class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Entities.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Entities.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// <summary> - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// <summary> - /// Looks up a localized string similar to <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" onload="alert(localStorage.getItem('oidc.user:https://cloud.squidex.io/identity-server/:squidex-frontend'))"> - /// <path d="M30,1h40l29,29v40l-29,29h-40l-29-29v-40z" stroke="#000" fill="none"/> - /// <path d="M31,3h38l28,28v38l-28,28h-38l-28-28v-38z" fill="#a23"/> - /// <text x="50" y="68" font-size="48" fill="#FFF" text-anchor="middle"><![CDATA[410]]></text> - ///</svg>. - /// </summary> - internal static string SvgInvalid { - get { - return ResourceManager.GetString("SvgInvalid", resourceCulture); - } + set { + resourceCulture = value; } - - /// <summary> - /// Looks up a localized string similar to <?xml version="1.0" encoding="UTF-8" standalone="no"?> - ///<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> - /// - ///<svg - /// xmlns:dc="http://purl.org/dc/elements/1.1/" - /// xmlns:cc="http://creativecommons.org/ns#" - /// xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - /// xmlns:svg="http://www.w3.org/2000/svg" - /// xmlns="http://www.w3.org/2000/svg" - /// xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - /// xmlns:inkscape="http://www.inkscape.org/n [rest of string was truncated]";. - /// </summary> - internal static string SvgValid { - get { - return ResourceManager.GetString("SvgValid", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" onload="alert(localStorage.getItem('oidc.user:https://cloud.squidex.io/identity-server/:squidex-frontend'))"> + /// <path d="M30,1h40l29,29v40l-29,29h-40l-29-29v-40z" stroke="#000" fill="none"/> + /// <path d="M31,3h38l28,28v38l-28,28h-38l-28-28v-38z" fill="#a23"/> + /// <text x="50" y="68" font-size="48" fill="#FFF" text-anchor="middle"><![CDATA[410]]></text> + ///</svg>. + /// </summary> + internal static string SvgInvalid { + get { + return ResourceManager.GetString("SvgInvalid", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to <?xml version="1.0" encoding="UTF-8" standalone="no"?> + ///<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> + /// + ///<svg + /// xmlns:dc="http://purl.org/dc/elements/1.1/" + /// xmlns:cc="http://creativecommons.org/ns#" + /// xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + /// xmlns:svg="http://www.w3.org/2000/svg" + /// xmlns="http://www.w3.org/2000/svg" + /// xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + /// xmlns:inkscape="http://www.inkscape.org/n [rest of string was truncated]";. + /// </summary> + internal static string SvgValid { + get { + return ResourceManager.GetString("SvgValid", resourceCulture); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs index 4d8c06e0fd..7d9ab92806 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs @@ -14,77 +14,76 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class BackupRulesTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly BackupRules sut; +namespace Squidex.Domain.Apps.Entities.Rules; - public BackupRulesTests() - { - ct = cts.Token; +public class BackupRulesTests +{ + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly BackupRules sut; - sut = new BackupRules(rebuilder); - } + public BackupRulesTests() + { + ct = cts.Token; - [Fact] - public void Should_provide_name() - { - Assert.Equal("Rules", sut.Name); - } + sut = new BackupRules(rebuilder); + } - [Fact] - public async Task Should_restore_indices_for_all_non_deleted_rules() - { - var ruleId1 = DomainId.NewGuid(); - var ruleId2 = DomainId.NewGuid(); - var ruleId3 = DomainId.NewGuid(); + [Fact] + public void Should_provide_name() + { + Assert.Equal("Rules", sut.Name); + } - var context = new RestoreContext(appId.Id, new UserMapping(RefToken.User("123")), A.Fake<IBackupReader>(), DomainId.NewGuid()); + [Fact] + public async Task Should_restore_indices_for_all_non_deleted_rules() + { + var ruleId1 = DomainId.NewGuid(); + var ruleId2 = DomainId.NewGuid(); + var ruleId3 = DomainId.NewGuid(); - await sut.RestoreEventAsync(AppEvent(new RuleCreated - { - RuleId = ruleId1 - }), context, ct); + var context = new RestoreContext(appId.Id, new UserMapping(RefToken.User("123")), A.Fake<IBackupReader>(), DomainId.NewGuid()); - await sut.RestoreEventAsync(AppEvent(new RuleCreated - { - RuleId = ruleId2 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new RuleCreated + { + RuleId = ruleId1 + }), context, ct); - await sut.RestoreEventAsync(AppEvent(new RuleCreated - { - RuleId = ruleId3 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new RuleCreated + { + RuleId = ruleId2 + }), context, ct); - await sut.RestoreEventAsync(AppEvent(new RuleDeleted - { - RuleId = ruleId3 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new RuleCreated + { + RuleId = ruleId3 + }), context, ct); - var rebuildAssets = new HashSet<DomainId>(); + await sut.RestoreEventAsync(AppEvent(new RuleDeleted + { + RuleId = ruleId3 + }), context, ct); - A.CallTo(() => rebuilder.InsertManyAsync<RuleDomainObject, RuleDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) - .Invokes(x => rebuildAssets.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); + var rebuildAssets = new HashSet<DomainId>(); - await sut.RestoreAsync(context, ct); + A.CallTo(() => rebuilder.InsertManyAsync<RuleDomainObject, RuleDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) + .Invokes(x => rebuildAssets.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); - Assert.Equal(new HashSet<DomainId> - { - DomainId.Combine(appId, ruleId1), - DomainId.Combine(appId, ruleId2) - }, rebuildAssets); - } + await sut.RestoreAsync(context, ct); - private Envelope<RuleEvent> AppEvent(RuleEvent @event) + Assert.Equal(new HashSet<DomainId> { - @event.AppId = appId; + DomainId.Combine(appId, ruleId1), + DomainId.Combine(appId, ruleId2) + }, rebuildAssets); + } + + private Envelope<RuleEvent> AppEvent(RuleEvent @event) + { + @event.AppId = appId; - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.RuleId)); - } + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.RuleId)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs index a66d21d7f6..660b9fcbef 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs @@ -16,125 +16,124 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards; + +public class GuardRuleTests : IClassFixture<TranslationsFixture> { - public class GuardRuleTests : IClassFixture<TranslationsFixture> + private readonly Uri validUrl = new Uri("https://squidex.io"); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + + public sealed record TestAction : RuleAction { - private readonly Uri validUrl = new Uri("https://squidex.io"); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + public Uri Url { get; set; } + } - public sealed record TestAction : RuleAction - { - public Uri Url { get; set; } - } + public GuardRuleTests() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) + .Returns(Mocks.Schema(appId, schemaId)); + } - public GuardRuleTests() + [Fact] + public async Task CanCreate_should_throw_exception_if_trigger_null() + { + var command = CreateCommand(new CreateRule { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) - .Returns(Mocks.Schema(appId, schemaId)); - } + Action = new TestAction + { + Url = validUrl + }, + Trigger = null! + }); + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), + new ValidationError("Trigger is required.", "Trigger")); + } - [Fact] - public async Task CanCreate_should_throw_exception_if_trigger_null() + [Fact] + public async Task CanCreate_should_throw_exception_if_action_null() + { + var command = CreateCommand(new CreateRule { - var command = CreateCommand(new CreateRule + Trigger = new ContentChangedTriggerV2 { - Action = new TestAction - { - Url = validUrl - }, - Trigger = null! - }); - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), - new ValidationError("Trigger is required.", "Trigger")); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_action_null() + Schemas = ReadonlyList.Empty<ContentChangedTriggerSchemaV2>() + }, + Action = null! + }); + + await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), + new ValidationError("Action is required.", "Action")); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = CreateCommand(new CreateRule { - var command = CreateCommand(new CreateRule + Trigger = new ContentChangedTriggerV2 { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadonlyList.Empty<ContentChangedTriggerSchemaV2>() - }, - Action = null! - }); - - await ValidationAssert.ThrowsAsync(() => GuardRule.CanCreate(command, appProvider), - new ValidationError("Action is required.", "Action")); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() - { - var command = CreateCommand(new CreateRule + Schemas = ReadonlyList.Empty<ContentChangedTriggerSchemaV2>() + }, + Action = new TestAction { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadonlyList.Empty<ContentChangedTriggerSchemaV2>() - }, - Action = new TestAction - { - Url = validUrl - } - }); - - await GuardRule.CanCreate(command, appProvider); - } - - [Fact] - public async Task CanUpdate_should_not_throw_exception_if_all_properties_are_null() - { - var command = new UpdateRule(); + Url = validUrl + } + }); - await GuardRule.CanUpdate(command, Rule(), appProvider); - } + await GuardRule.CanCreate(command, appProvider); + } - [Fact] - public async Task CanUpdate_should_not_throw_exception_if_rule_has_already_this_name() - { - var command = new UpdateRule { Name = "MyName" }; + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_all_properties_are_null() + { + var command = new UpdateRule(); - await GuardRule.CanUpdate(command, Rule(), appProvider); - } + await GuardRule.CanUpdate(command, Rule(), appProvider); + } + + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_rule_has_already_this_name() + { + var command = new UpdateRule { Name = "MyName" }; - [Fact] - public async Task CanUpdate_should_not_throw_exception_if_trigger_action__and_name_are_valid() + await GuardRule.CanUpdate(command, Rule(), appProvider); + } + + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_trigger_action__and_name_are_valid() + { + var command = new UpdateRule { - var command = new UpdateRule + Trigger = new ContentChangedTriggerV2 { - Trigger = new ContentChangedTriggerV2 - { - Schemas = ReadonlyList.Empty<ContentChangedTriggerSchemaV2>() - }, - Action = new TestAction - { - Url = validUrl - }, - Name = "NewName" - }; - - await GuardRule.CanUpdate(command, Rule(), appProvider); - } - - private CreateRule CreateCommand(CreateRule command) - { - command.AppId = appId; + Schemas = ReadonlyList.Empty<ContentChangedTriggerSchemaV2>() + }, + Action = new TestAction + { + Url = validUrl + }, + Name = "NewName" + }; - return command; - } + await GuardRule.CanUpdate(command, Rule(), appProvider); + } - private IRuleEntity Rule() - { - var rule = A.Fake<IRuleEntity>(); + private CreateRule CreateCommand(CreateRule command) + { + command.AppId = appId; + + return command; + } + + private IRuleEntity Rule() + { + var rule = A.Fake<IRuleEntity>(); - A.CallTo(() => rule.AppId).Returns(appId); + A.CallTo(() => rule.AppId).Returns(appId); - return rule; - } + return rule; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/ContentChangedTriggerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/ContentChangedTriggerTests.cs index 6cce7ece3e..650b72d674 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/ContentChangedTriggerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/ContentChangedTriggerTests.cs @@ -16,91 +16,90 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards.Triggers +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards.Triggers; + +public class ContentChangedTriggerTests : IClassFixture<TranslationsFixture> { - public class ContentChangedTriggerTests : IClassFixture<TranslationsFixture> - { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - [Fact] - public async Task Should_add_error_if_schema_id_is_not_defined() + [Fact] + public async Task Should_add_error_if_schema_id_is_not_defined() + { + var trigger = new ContentChangedTriggerV2 { - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadonlyList.Create(new ContentChangedTriggerSchemaV2()) - }; + Schemas = ReadonlyList.Create(new ContentChangedTriggerSchemaV2()) + }; - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Schema ID is required.", "Schemas") - }); + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Schema ID is required.", "Schemas") + }); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, false, default)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, false, default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_add_error_if_schemas_ids_are_not_valid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) - .Returns(Task.FromResult<ISchemaEntity?>(null)); + [Fact] + public async Task Should_add_error_if_schemas_ids_are_not_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) + .Returns(Task.FromResult<ISchemaEntity?>(null)); - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadonlyList.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) - }; + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadonlyList.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) + }; - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError($"Schema {schemaId.Id} does not exist.", "Schemas") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError($"Schema {schemaId.Id} does not exist.", "Schemas") + }); + } - [Fact] - public async Task Should_not_add_error_if_schemas_is_null() - { - var trigger = new ContentChangedTriggerV2(); + [Fact] + public async Task Should_not_add_error_if_schemas_is_null() + { + var trigger = new ContentChangedTriggerV2(); - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_schemas_is_empty() + [Fact] + public async Task Should_not_add_error_if_schemas_is_empty() + { + var trigger = new ContentChangedTriggerV2 { - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadonlyList.Empty<ContentChangedTriggerSchemaV2>() - }; + Schemas = ReadonlyList.Empty<ContentChangedTriggerSchemaV2>() + }; - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_schemas_ids_are_valid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, false, default)) - .Returns(Mocks.Schema(appId, schemaId)); + [Fact] + public async Task Should_not_add_error_if_schemas_ids_are_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, false, default)) + .Returns(Mocks.Schema(appId, schemaId)); - var trigger = new ContentChangedTriggerV2 - { - Schemas = ReadonlyList.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) - }; + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadonlyList.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId.Id }) + }; - var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId.Id, trigger, appProvider); - Assert.Empty(errors); - } + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/UsageTriggerValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/UsageTriggerValidationTests.cs index 5f5a31f9c1..29254ecf9b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/UsageTriggerValidationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/UsageTriggerValidationTests.cs @@ -13,59 +13,58 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards.Triggers +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards.Triggers; + +public class UsageTriggerValidationTests : IClassFixture<TranslationsFixture> { - public class UsageTriggerValidationTests : IClassFixture<TranslationsFixture> - { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly DomainId appId = DomainId.NewGuid(); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly DomainId appId = DomainId.NewGuid(); - [Fact] - public async Task Should_add_error_if_num_days_less_than_1() - { - var trigger = new UsageTrigger { NumDays = 0 }; + [Fact] + public async Task Should_add_error_if_num_days_less_than_1() + { + var trigger = new UsageTrigger { NumDays = 0 }; - var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Num days must be between 1 and 30.", "NumDays") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Num days must be between 1 and 30.", "NumDays") + }); + } - [Fact] - public async Task Should_add_error_if_num_days_greater_than_30() - { - var trigger = new UsageTrigger { NumDays = 32 }; + [Fact] + public async Task Should_add_error_if_num_days_greater_than_30() + { + var trigger = new UsageTrigger { NumDays = 32 }; - var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Num days must be between 1 and 30.", "NumDays") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Num days must be between 1 and 30.", "NumDays") + }); + } - [Fact] - public async Task Should_not_add_error_if_num_days_is_valid() - { - var trigger = new UsageTrigger { NumDays = 20 }; + [Fact] + public async Task Should_not_add_error_if_num_days_is_valid() + { + var trigger = new UsageTrigger { NumDays = 20 }; - var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public async Task Should_not_add_error_if_num_days_is_not_defined() - { - var trigger = new UsageTrigger(); + [Fact] + public async Task Should_not_add_error_if_num_days_is_not_defined() + { + var trigger = new UsageTrigger(); - var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); - Assert.Empty(errors); - } + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs index 60d0f95c89..c91e8a32d5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs @@ -12,92 +12,91 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject; + +public sealed class RuleCommandMiddlewareTests : HandlerTestBase<RuleDomainObject.State> { - public sealed class RuleCommandMiddlewareTests : HandlerTestBase<RuleDomainObject.State> + private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); + private readonly IRuleEnricher ruleEnricher = A.Fake<IRuleEnricher>(); + private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); + private readonly DomainId ruleId = DomainId.NewGuid(); + private readonly Context requestContext; + private readonly RuleCommandMiddleware sut; + + public sealed class MyCommand : SquidexCommand { - private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>(); - private readonly IRuleEnricher ruleEnricher = A.Fake<IRuleEnricher>(); - private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); - private readonly DomainId ruleId = DomainId.NewGuid(); - private readonly Context requestContext; - private readonly RuleCommandMiddleware sut; - - public sealed class MyCommand : SquidexCommand - { - } + } - protected override DomainId Id - { - get => ruleId; - } + protected override DomainId Id + { + get => ruleId; + } - public RuleCommandMiddlewareTests() - { - requestContext = Context.Anonymous(Mocks.App(AppNamedId)); + public RuleCommandMiddlewareTests() + { + requestContext = Context.Anonymous(Mocks.App(AppNamedId)); - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); - sut = new RuleCommandMiddleware(domainObjectFactory, ruleEnricher, contextProvider); - } + sut = new RuleCommandMiddleware(domainObjectFactory, ruleEnricher, contextProvider); + } - [Fact] - public async Task Should_not_invoke_enricher_for_other_actual() - { - await HandleAsync(new EnableRule(), 12); + [Fact] + public async Task Should_not_invoke_enricher_for_other_actual() + { + await HandleAsync(new EnableRule(), 12); - A.CallTo(() => ruleEnricher.EnrichAsync(A<IEnrichedRuleEntity>._, requestContext, default)) - .MustNotHaveHappened(); - } + A.CallTo(() => ruleEnricher.EnrichAsync(A<IEnrichedRuleEntity>._, requestContext, default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_invoke_enricher_if_already_enriched() - { - var actual = new RuleEntity(); + [Fact] + public async Task Should_not_invoke_enricher_if_already_enriched() + { + var actual = new RuleEntity(); - var context = - await HandleAsync(new EnableRule(), - actual); + var context = + await HandleAsync(new EnableRule(), + actual); - Assert.Same(actual, context.Result<IEnrichedRuleEntity>()); + Assert.Same(actual, context.Result<IEnrichedRuleEntity>()); - A.CallTo(() => ruleEnricher.EnrichAsync(A<IEnrichedRuleEntity>._, requestContext, default)) - .MustNotHaveHappened(); - } + A.CallTo(() => ruleEnricher.EnrichAsync(A<IEnrichedRuleEntity>._, requestContext, default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_enrich_rule_actual() - { - var actual = A.Fake<IRuleEntity>(); + [Fact] + public async Task Should_enrich_rule_actual() + { + var actual = A.Fake<IRuleEntity>(); - var enriched = new RuleEntity(); + var enriched = new RuleEntity(); - A.CallTo(() => ruleEnricher.EnrichAsync(actual, requestContext, default)) - .Returns(enriched); + A.CallTo(() => ruleEnricher.EnrichAsync(actual, requestContext, default)) + .Returns(enriched); - var context = - await HandleAsync(new EnableRule(), - actual); + var context = + await HandleAsync(new EnableRule(), + actual); - Assert.Same(enriched, context.Result<IEnrichedRuleEntity>()); - } + Assert.Same(enriched, context.Result<IEnrichedRuleEntity>()); + } - private Task<CommandContext> HandleAsync(RuleCommand command, object actual) - { - command.RuleId = ruleId; + private Task<CommandContext> HandleAsync(RuleCommand command, object actual) + { + command.RuleId = ruleId; - CreateCommand(command); + CreateCommand(command); - var domainObject = A.Fake<RuleDomainObject>(); + var domainObject = A.Fake<RuleDomainObject>(); - A.CallTo(() => domainObject.ExecuteAsync(A<IAggregateCommand>._, A<CancellationToken>._)) - .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); + A.CallTo(() => domainObject.ExecuteAsync(A<IAggregateCommand>._, A<CancellationToken>._)) + .Returns(new CommandResult(command.AggregateId, 1, 0, actual)); - A.CallTo(() => domainObjectFactory.Create<RuleDomainObject>(command.AggregateId)) - .Returns(domainObject); + A.CallTo(() => domainObjectFactory.Create<RuleDomainObject>(command.AggregateId)) + .Returns(domainObject); - return HandleAsync(sut, command); - } + return HandleAsync(sut, command); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs index 1ed66f905f..9fba742f60 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs @@ -17,282 +17,281 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject +namespace Squidex.Domain.Apps.Entities.Rules.DomainObject; + +public class RuleDomainObjectTests : HandlerTestBase<RuleDomainObject.State> { - public class RuleDomainObjectTests : HandlerTestBase<RuleDomainObject.State> - { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IRuleEnqueuer ruleEnqueuer = A.Fake<IRuleEnqueuer>(); - private readonly DomainId ruleId = DomainId.NewGuid(); - private readonly RuleDomainObject sut; + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IRuleEnqueuer ruleEnqueuer = A.Fake<IRuleEnqueuer>(); + private readonly DomainId ruleId = DomainId.NewGuid(); + private readonly RuleDomainObject sut; - protected override DomainId Id - { - get => DomainId.Combine(AppId, ruleId); - } + protected override DomainId Id + { + get => DomainId.Combine(AppId, ruleId); + } - public sealed record TestAction : RuleAction - { - public int Value { get; set; } - } + public sealed record TestAction : RuleAction + { + public int Value { get; set; } + } - public RuleDomainObjectTests() - { - var log = A.Fake<ILogger<RuleDomainObject>>(); + public RuleDomainObjectTests() + { + var log = A.Fake<ILogger<RuleDomainObject>>(); - var serviceProvider = - new ServiceCollection() - .AddSingleton(appProvider) - .AddSingleton(ruleEnqueuer) - .BuildServiceProvider(); + var serviceProvider = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(ruleEnqueuer) + .BuildServiceProvider(); #pragma warning disable MA0056 // Do not call overridable members in constructor - sut = new RuleDomainObject(Id, PersistenceFactory, log, serviceProvider); + sut = new RuleDomainObject(Id, PersistenceFactory, log, serviceProvider); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - [Fact] - public async Task Command_should_throw_exception_if_rule_is_deleted() - { - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); + [Fact] + public async Task Command_should_throw_exception_if_rule_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); - await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteDisableAsync); - } + await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecuteDisableAsync); + } - [Fact] - public async Task Create_should_create_events_and_set_intitial_state() - { - var command = MakeCreateCommand(); + [Fact] + public async Task Create_should_create_events_and_set_intitial_state() + { + var command = MakeCreateCommand(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(AppId, sut.Snapshot.AppId.Id); + Assert.Equal(AppId, sut.Snapshot.AppId.Id); - Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger); - Assert.Same(command.Action, sut.Snapshot.RuleDef.Action); + Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger); + Assert.Same(command.Action, sut.Snapshot.RuleDef.Action); - LastEvents - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleCreated { Trigger = command.Trigger!, Action = command.Action! }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleCreated { Trigger = command.Trigger!, Action = command.Action! }) + ); + } - [Fact] - public async Task Update_should_create_events_and_update_trigger_and_action() - { - var command = MakeUpdateCommand(); + [Fact] + public async Task Update_should_create_events_and_update_trigger_and_action() + { + var command = MakeUpdateCommand(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(sut.Snapshot.RuleDef.IsEnabled); + Assert.True(sut.Snapshot.RuleDef.IsEnabled); - Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger); - Assert.Same(command.Action, sut.Snapshot.RuleDef.Action); + Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger); + Assert.Same(command.Action, sut.Snapshot.RuleDef.Action); - Assert.Equal(command.Name, sut.Snapshot.RuleDef.Name); + Assert.Equal(command.Name, sut.Snapshot.RuleDef.Name); - LastEvents - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleUpdated { Trigger = command.Trigger, Action = command.Action, Name = command.Name }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleUpdated { Trigger = command.Trigger, Action = command.Action, Name = command.Name }) + ); + } - [Fact] - public async Task Enable_should_create_events_and_update_enabled_flag() - { - var command = new EnableRule(); + [Fact] + public async Task Enable_should_create_events_and_update_enabled_flag() + { + var command = new EnableRule(); - await ExecuteCreateAsync(); - await ExecuteDisableAsync(); + await ExecuteCreateAsync(); + await ExecuteDisableAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(sut.Snapshot.RuleDef.IsEnabled); + Assert.True(sut.Snapshot.RuleDef.IsEnabled); - LastEvents - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleEnabled()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleEnabled()) + ); + } - [Fact] - public async Task Enable_via_update_should_create_events_and_update_enabled_flag() + [Fact] + public async Task Enable_via_update_should_create_events_and_update_enabled_flag() + { + var command = new UpdateRule { - var command = new UpdateRule - { - IsEnabled = true - }; + IsEnabled = true + }; - await ExecuteCreateAsync(); - await ExecuteDisableAsync(); + await ExecuteCreateAsync(); + await ExecuteDisableAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(sut.Snapshot.RuleDef.IsEnabled); + Assert.True(sut.Snapshot.RuleDef.IsEnabled); - LastEvents - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleUpdated { IsEnabled = true }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleUpdated { IsEnabled = true }) + ); + } - [Fact] - public async Task Disable_should_create_events_and_update_enabled_flag() - { - var command = new DisableRule(); + [Fact] + public async Task Disable_should_create_events_and_update_enabled_flag() + { + var command = new DisableRule(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(sut.Snapshot.RuleDef.IsEnabled); + Assert.False(sut.Snapshot.RuleDef.IsEnabled); - LastEvents - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleDisabled()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDisabled()) + ); + } - [Fact] - public async Task Disable_via_update_should_create_events_and_update_enabled_flag() + [Fact] + public async Task Disable_via_update_should_create_events_and_update_enabled_flag() + { + var command = new UpdateRule { - var command = new UpdateRule - { - IsEnabled = false - }; + IsEnabled = false + }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(sut.Snapshot.RuleDef.IsEnabled); + Assert.False(sut.Snapshot.RuleDef.IsEnabled); - LastEvents - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleUpdated { IsEnabled = false }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleUpdated { IsEnabled = false }) + ); + } - [Fact] - public async Task Delete_should_create_events_and_update_deleted_flag() - { - var command = new DeleteRule(); + [Fact] + public async Task Delete_should_create_events_and_update_deleted_flag() + { + var command = new DeleteRule(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); - Assert.True(sut.Snapshot.IsDeleted); + Assert.True(sut.Snapshot.IsDeleted); - LastEvents - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleDeleted()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDeleted()) + ); + } - [Fact] - public async Task Trigger_should_invoke_rule_enqueue_but_not_change_snapshot() - { - var command = new TriggerRule(); + [Fact] + public async Task Trigger_should_invoke_rule_enqueue_but_not_change_snapshot() + { + var command = new TriggerRule(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - await PublishAsync(command); + await PublishAsync(command); - Assert.Equal(0, sut.Version); + Assert.Equal(0, sut.Version); - A.CallTo(() => ruleEnqueuer.EnqueueAsync(sut.Snapshot.RuleDef, sut.Snapshot.Id, - A<Envelope<IEvent>>.That.Matches(x => x.Payload is RuleManuallyTriggered))) - .MustHaveHappened(); - } + A.CallTo(() => ruleEnqueuer.EnqueueAsync(sut.Snapshot.RuleDef, sut.Snapshot.Id, + A<Envelope<IEvent>>.That.Matches(x => x.Payload is RuleManuallyTriggered))) + .MustHaveHappened(); + } - private Task ExecuteCreateAsync() - { - return PublishAsync(MakeCreateCommand()); - } + private Task ExecuteCreateAsync() + { + return PublishAsync(MakeCreateCommand()); + } - private Task ExecuteDisableAsync() - { - return PublishAsync(new DisableRule()); - } + private Task ExecuteDisableAsync() + { + return PublishAsync(new DisableRule()); + } - private Task ExecuteDeleteAsync() - { - return PublishAsync(new DeleteRule()); - } + private Task ExecuteDeleteAsync() + { + return PublishAsync(new DeleteRule()); + } - private static CreateRule MakeCreateCommand() + private static CreateRule MakeCreateCommand() + { + return new CreateRule { - return new CreateRule + Trigger = new ContentChangedTriggerV2 { - Trigger = new ContentChangedTriggerV2 - { - HandleAll = false - }, - Action = new TestAction - { - Value = 123 - } - }; - } - - private static UpdateRule MakeUpdateCommand() - { - return new UpdateRule + HandleAll = false + }, + Action = new TestAction { - Name = "NewName", - Trigger = new ContentChangedTriggerV2 - { - HandleAll = true - }, - Action = new TestAction - { - Value = 456 - } - }; - } - - private T CreateRuleEvent<T>(T @event) where T : RuleEvent + Value = 123 + } + }; + } + + private static UpdateRule MakeUpdateCommand() + { + return new UpdateRule { - @event.RuleId = ruleId; + Name = "NewName", + Trigger = new ContentChangedTriggerV2 + { + HandleAll = true + }, + Action = new TestAction + { + Value = 456 + } + }; + } - return CreateEvent(@event); - } + private T CreateRuleEvent<T>(T @event) where T : RuleEvent + { + @event.RuleId = ruleId; - private T CreateRuleCommand<T>(T command) where T : RuleCommand - { - command.RuleId = ruleId; + return CreateEvent(@event); + } - return CreateCommand(command); - } + private T CreateRuleCommand<T>(T command) where T : RuleCommand + { + command.RuleId = ruleId; - private Task<object> PublishIdempotentAsync(RuleCommand command) - { - return PublishIdempotentAsync(sut, CreateRuleCommand(command)); - } + return CreateCommand(command); + } - private async Task<object?> PublishAsync(RuleCommand command) - { - var actual = await sut.ExecuteAsync(CreateRuleCommand(command), default); + private Task<object> PublishIdempotentAsync(RuleCommand command) + { + return PublishIdempotentAsync(sut, CreateRuleCommand(command)); + } + + private async Task<object?> PublishAsync(RuleCommand command) + { + var actual = await sut.ExecuteAsync(CreateRuleCommand(command), default); - return actual.Payload; - } + return actual.Payload; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs index c4205d975a..47d832b078 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs @@ -10,65 +10,64 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.Indexes +namespace Squidex.Domain.Apps.Entities.Rules.Indexes; + +public class RulesIndexTests { - public class RulesIndexTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IRuleRepository ruleRepository = A.Fake<IRuleRepository>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RulesIndex sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IRuleRepository ruleRepository = A.Fake<IRuleRepository>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RulesIndex sut; - public RulesIndexTests() - { - ct = cts.Token; + public RulesIndexTests() + { + ct = cts.Token; - sut = new RulesIndex(ruleRepository); - } + sut = new RulesIndex(ruleRepository); + } - [Fact] - public async Task Should_resolve_rules_by_id() - { - var rule = SetupRule(0); + [Fact] + public async Task Should_resolve_rules_by_id() + { + var rule = SetupRule(0); - A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) - .Returns(new List<IRuleEntity> { rule }); + A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List<IRuleEntity> { rule }); - var actual = await sut.GetRulesAsync(appId.Id, ct); + var actual = await sut.GetRulesAsync(appId.Id, ct); - Assert.Same(actual[0], rule); - } + Assert.Same(actual[0], rule); + } - [Fact] - public async Task Should_return_empty_rules_if_rule_not_created() - { - var rule = SetupRule(-1); + [Fact] + public async Task Should_return_empty_rules_if_rule_not_created() + { + var rule = SetupRule(-1); - A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) - .Returns(new List<IRuleEntity> { rule }); + A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List<IRuleEntity> { rule }); - var actual = await sut.GetRulesAsync(appId.Id, ct); + var actual = await sut.GetRulesAsync(appId.Id, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_return_empty_rules_if_rule_deleted() - { - var rule = SetupRule(0, true); + [Fact] + public async Task Should_return_empty_rules_if_rule_deleted() + { + var rule = SetupRule(0, true); - A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) - .Returns(new List<IRuleEntity> { rule }); + A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List<IRuleEntity> { rule }); - var actual = await sut.GetRulesAsync(appId.Id, ct); + var actual = await sut.GetRulesAsync(appId.Id, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - private IRuleEntity SetupRule(long version, bool isDeleted = false) - { - return new RuleEntity { AppId = appId, Version = version, IsDeleted = isDeleted }; - } + private IRuleEntity SetupRule(long version, bool isDeleted = false) + { + return new RuleEntity { AppId = appId, Version = version, IsDeleted = isDeleted }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs index 174fb978e1..2a0ab70bf2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/ManualTriggerHandlerTests.cs @@ -14,68 +14,67 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public class ManualTriggerHandlerTests { - public class ManualTriggerHandlerTests - { - private readonly IRuleTriggerHandler sut = new ManualTriggerHandler(); + private readonly IRuleTriggerHandler sut = new ManualTriggerHandler(); - [Fact] - public void Should_return_false_if_asking_for_snapshot_support() - { - Assert.False(sut.CanCreateSnapshotEvents); - } + [Fact] + public void Should_return_false_if_asking_for_snapshot_support() + { + Assert.False(sut.CanCreateSnapshotEvents); + } - [Fact] - public void Should_calculate_name() - { - var @event = new RuleManuallyTriggered(); + [Fact] + public void Should_calculate_name() + { + var @event = new RuleManuallyTriggered(); - Assert.Equal("Manual", sut.GetName(@event)); - } + Assert.Equal("Manual", sut.GetName(@event)); + } - [Fact] - public async Task Should_create_event_with_name() - { - var @event = TestUtils.CreateEvent<RuleManuallyTriggered>(); - var envelope = Envelope.Create<AppEvent>(@event); + [Fact] + public async Task Should_create_event_with_name() + { + var @event = TestUtils.CreateEvent<RuleManuallyTriggered>(); + var envelope = Envelope.Create<AppEvent>(@event); - var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); - var enrichedEvent = (EnrichedManualEvent)actual.Single(); + var enrichedEvent = (EnrichedManualEvent)actual.Single(); - Assert.Equal(@event.Actor, enrichedEvent.Actor); - Assert.Equal(@event.AppId, enrichedEvent.AppId); - Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); - } + Assert.Equal(@event.Actor, enrichedEvent.Actor); + Assert.Equal(@event.AppId, enrichedEvent.AppId); + Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); + } - [Fact] - public async Task Should_create_event_with_actor() - { - var actor = RefToken.User("me"); + [Fact] + public async Task Should_create_event_with_actor() + { + var actor = RefToken.User("me"); - var @event = new RuleManuallyTriggered { Actor = actor }; - var envelope = Envelope.Create<AppEvent>(@event); + var @event = new RuleManuallyTriggered { Actor = actor }; + var envelope = Envelope.Create<AppEvent>(@event); - var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); + var actual = await sut.CreateEnrichedEventsAsync(envelope, default, default).ToListAsync(); - Assert.Equal(actor, ((EnrichedUserEventBase)actual.Single()).Actor); - } + Assert.Equal(actor, ((EnrichedUserEventBase)actual.Single()).Actor); + } - [Fact] - public void Should_always_trigger() - { - var @event = new RuleManuallyTriggered(); + [Fact] + public void Should_always_trigger() + { + var @event = new RuleManuallyTriggered(); - Assert.True(sut.Trigger(Envelope.Create<AppEvent>(@event), default)); - } + Assert.True(sut.Trigger(Envelope.Create<AppEvent>(@event), default)); + } - [Fact] - public void Should_always_trigger_enriched_event() - { - var @event = new EnrichedUsageExceededEvent(); + [Fact] + public void Should_always_trigger_enriched_event() + { + var @event = new EnrichedUsageExceededEvent(); - Assert.True(sut.Trigger(@event, default)); - } + Assert.True(sut.Trigger(@event, default)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs index 8362144e78..edbf8f812d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs @@ -13,74 +13,73 @@ using Squidex.Infrastructure.Caching; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.Queries +namespace Squidex.Domain.Apps.Entities.Rules.Queries; + +public class RuleEnricherTests { - public class RuleEnricherTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); + private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly RuleEnricher sut; + + public RuleEnricherTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); - private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext; - private readonly RuleEnricher sut; - - public RuleEnricherTests() - { - ct = cts.Token; + ct = cts.Token; + + requestContext = Context.Anonymous(Mocks.App(appId)); - requestContext = Context.Anonymous(Mocks.App(appId)); + sut = new RuleEnricher(ruleEventRepository, requestCache); + } - sut = new RuleEnricher(ruleEventRepository, requestCache); - } + [Fact] + public async Task Should_not_enrich_if_statistics_not_found() + { + var source = CreateRule(); - [Fact] - public async Task Should_not_enrich_if_statistics_not_found() - { - var source = CreateRule(); + var actual = await sut.EnrichAsync(source, requestContext, ct); - var actual = await sut.EnrichAsync(source, requestContext, ct); + Assert.Equal(0, actual.NumFailed); + Assert.Equal(0, actual.NumSucceeded); - Assert.Equal(0, actual.NumFailed); - Assert.Equal(0, actual.NumSucceeded); + Assert.Null(actual.LastExecuted); - Assert.Null(actual.LastExecuted); + A.CallTo(() => requestCache.AddDependency(source.UniqueId, source.Version)) + .MustHaveHappened(); - A.CallTo(() => requestCache.AddDependency(source.UniqueId, source.Version)) - .MustHaveHappened(); + A.CallTo(() => requestCache.AddDependency<Instant?>(null)) + .MustNotHaveHappened(); + } - A.CallTo(() => requestCache.AddDependency<Instant?>(null)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_enrich_rules_with_found_statistics() + { + var source = CreateRule(); - [Fact] - public async Task Should_enrich_rules_with_found_statistics() + var stats = new RuleStatistics { - var source = CreateRule(); - - var stats = new RuleStatistics - { - RuleId = source.Id, - NumFailed = 12, - NumSucceeded = 17, - LastExecuted = SystemClock.Instance.GetCurrentInstant() - }; + RuleId = source.Id, + NumFailed = 12, + NumSucceeded = 17, + LastExecuted = SystemClock.Instance.GetCurrentInstant() + }; - A.CallTo(() => ruleEventRepository.QueryStatisticsByAppAsync(appId.Id, ct)) - .Returns(new List<RuleStatistics> { stats }); + A.CallTo(() => ruleEventRepository.QueryStatisticsByAppAsync(appId.Id, ct)) + .Returns(new List<RuleStatistics> { stats }); - await sut.EnrichAsync(source, requestContext, ct); + await sut.EnrichAsync(source, requestContext, ct); - A.CallTo(() => requestCache.AddDependency(source.UniqueId, source.Version)) - .MustHaveHappened(); + A.CallTo(() => requestCache.AddDependency(source.UniqueId, source.Version)) + .MustHaveHappened(); - A.CallTo(() => requestCache.AddDependency(stats.LastExecuted)) - .MustHaveHappened(); - } + A.CallTo(() => requestCache.AddDependency(stats.LastExecuted)) + .MustHaveHappened(); + } - private IRuleEntity CreateRule() - { - return new RuleEntity { AppId = appId, Id = DomainId.NewGuid(), Version = 13 }; - } + private IRuleEntity CreateRule() + { + return new RuleEntity { AppId = appId, Id = DomainId.NewGuid(), Version = 13 }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs index b72bb0afff..c483077a58 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs @@ -11,49 +11,48 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.Queries +namespace Squidex.Domain.Apps.Entities.Rules.Queries; + +public class RuleQueryServiceTests { - public class RuleQueryServiceTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IRulesIndex rulesIndex = A.Fake<IRulesIndex>(); + private readonly IRuleEnricher ruleEnricher = A.Fake<IRuleEnricher>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly RuleQueryService sut; + + public RuleQueryServiceTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IRulesIndex rulesIndex = A.Fake<IRulesIndex>(); - private readonly IRuleEnricher ruleEnricher = A.Fake<IRuleEnricher>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext; - private readonly RuleQueryService sut; - - public RuleQueryServiceTests() - { - ct = cts.Token; + ct = cts.Token; - requestContext = Context.Anonymous(Mocks.App(appId)); + requestContext = Context.Anonymous(Mocks.App(appId)); - sut = new RuleQueryService(rulesIndex, ruleEnricher); - } + sut = new RuleQueryService(rulesIndex, ruleEnricher); + } - [Fact] - public async Task Should_get_rules_from_index_and_enrich() + [Fact] + public async Task Should_get_rules_from_index_and_enrich() + { + var original = new List<IRuleEntity> { - var original = new List<IRuleEntity> - { - new RuleEntity() - }; + new RuleEntity() + }; - var enriched = new List<IEnrichedRuleEntity> - { - new RuleEntity() - }; + var enriched = new List<IEnrichedRuleEntity> + { + new RuleEntity() + }; - A.CallTo(() => rulesIndex.GetRulesAsync(appId.Id, ct)) - .Returns(original); + A.CallTo(() => rulesIndex.GetRulesAsync(appId.Id, ct)) + .Returns(original); - A.CallTo(() => ruleEnricher.EnrichAsync(original, requestContext, ct)) - .Returns(enriched); + A.CallTo(() => ruleEnricher.EnrichAsync(original, requestContext, ct)) + .Returns(enriched); - var actual = await sut.QueryAsync(requestContext, ct); + var actual = await sut.QueryAsync(requestContext, ct); - Assert.Same(enriched, actual); - } + Assert.Same(enriched, actual); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs index 297a0d958b..ccffc9e9a9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs @@ -14,163 +14,162 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public class RuleDequeuerWorkerTests { - public class RuleDequeuerWorkerTests + private readonly IClock clock = A.Fake<IClock>(); + private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); + private readonly IRuleService ruleService = A.Fake<IRuleService>(); + private readonly ILogger<RuleDequeuerWorker> log = A.Dummy<ILogger<RuleDequeuerWorker>>(); + private readonly RuleDequeuerWorker sut; + + public RuleDequeuerWorkerTests() { - private readonly IClock clock = A.Fake<IClock>(); - private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); - private readonly IRuleService ruleService = A.Fake<IRuleService>(); - private readonly ILogger<RuleDequeuerWorker> log = A.Dummy<ILogger<RuleDequeuerWorker>>(); - private readonly RuleDequeuerWorker sut; + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - public RuleDequeuerWorkerTests() + sut = new RuleDequeuerWorker(ruleService, ruleEventRepository, log) { - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - - sut = new RuleDequeuerWorker(ruleService, ruleEventRepository, log) - { - Clock = clock - }; - } + Clock = clock + }; + } - [Fact] - public async Task Should_query_repository() - { - await sut.QueryAsync(); + [Fact] + public async Task Should_query_repository() + { + await sut.QueryAsync(); - A.CallTo(() => ruleEventRepository.QueryPendingAsync(A<Instant>._, A<Func<IRuleEventEntity, Task>>._, default)) - .MustHaveHappened(); - } + A.CallTo(() => ruleEventRepository.QueryPendingAsync(A<Instant>._, A<Func<IRuleEventEntity, Task>>._, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_ignore_repository_exceptions_and_log() - { - A.CallTo(() => ruleEventRepository.QueryPendingAsync(A<Instant>._, A<Func<IRuleEventEntity, Task>>._, default)) - .Throws(new InvalidOperationException()); + [Fact] + public async Task Should_ignore_repository_exceptions_and_log() + { + A.CallTo(() => ruleEventRepository.QueryPendingAsync(A<Instant>._, A<Func<IRuleEventEntity, Task>>._, default)) + .Throws(new InvalidOperationException()); - await sut.QueryAsync(); + await sut.QueryAsync(); - A.CallTo(log).Where(x => x.Method.Name == "Log") - .MustHaveHappened(); - } + A.CallTo(log).Where(x => x.Method.Name == "Log") + .MustHaveHappened(); + } - [Fact] - public async Task Should_ignore_rule_service_exceptions_and_log() - { - var @event = CreateEvent(1, "MyAction", "{}"); + [Fact] + public async Task Should_ignore_rule_service_exceptions_and_log() + { + var @event = CreateEvent(1, "MyAction", "{}"); - A.CallTo(() => ruleService.InvokeAsync(A<string>._, A<string>._, default)) - .Throws(new InvalidOperationException()); + A.CallTo(() => ruleService.InvokeAsync(A<string>._, A<string>._, default)) + .Throws(new InvalidOperationException()); - await sut.HandleAsync(@event); + await sut.HandleAsync(@event); - A.CallTo(log).Where(x => x.Method.Name == "Log") - .MustHaveHappened(); - } + A.CallTo(log).Where(x => x.Method.Name == "Log") + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_execute_if_already_running() - { - var id = DomainId.NewGuid(); + [Fact] + public async Task Should_not_execute_if_already_running() + { + var id = DomainId.NewGuid(); - var event1 = CreateEvent(1, "MyAction", "{}", id); - var event2 = CreateEvent(1, "MyAction", "{}", id); + var event1 = CreateEvent(1, "MyAction", "{}", id); + var event2 = CreateEvent(1, "MyAction", "{}", id); - A.CallTo(() => ruleService.InvokeAsync(A<string>._, A<string>._, default)) - .ReturnsLazily(async () => - { - await Task.Delay(500); + A.CallTo(() => ruleService.InvokeAsync(A<string>._, A<string>._, default)) + .ReturnsLazily(async () => + { + await Task.Delay(500); - return (Result.Ignored(), TimeSpan.Zero); - }); + return (Result.Ignored(), TimeSpan.Zero); + }); - await Task.WhenAll( - sut.HandleAsync(event1), - sut.HandleAsync(event2)); + await Task.WhenAll( + sut.HandleAsync(event1), + sut.HandleAsync(event2)); - A.CallTo(() => ruleService.InvokeAsync(A<string>._, A<string>._, default)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => ruleService.InvokeAsync(A<string>._, A<string>._, default)) + .MustHaveHappenedOnceExactly(); + } - [Theory] - [InlineData(0, 0, RuleResult.Success, RuleJobResult.Success)] - [InlineData(0, 5, RuleResult.Timeout, RuleJobResult.Retry)] - [InlineData(1, 60, RuleResult.Timeout, RuleJobResult.Retry)] - [InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] - [InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] - [InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] - public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult actual, RuleJobResult jobResult) - { - var actionData = "{}"; - var actionName = "MyAction"; + [Theory] + [InlineData(0, 0, RuleResult.Success, RuleJobResult.Success)] + [InlineData(0, 5, RuleResult.Timeout, RuleJobResult.Retry)] + [InlineData(1, 60, RuleResult.Timeout, RuleJobResult.Retry)] + [InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] + [InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] + [InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] + public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult actual, RuleJobResult jobResult) + { + var actionData = "{}"; + var actionName = "MyAction"; - var @event = CreateEvent(calls, actionName, actionData); + var @event = CreateEvent(calls, actionName, actionData); - var requestElapsed = TimeSpan.FromMinutes(1); - var requestDump = "Dump"; + var requestElapsed = TimeSpan.FromMinutes(1); + var requestDump = "Dump"; - A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData, default)) - .Returns((Result.Create(requestDump, actual), requestElapsed)); + A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData, default)) + .Returns((Result.Create(requestDump, actual), requestElapsed)); - var now = clock.GetCurrentInstant(); + var now = clock.GetCurrentInstant(); - Instant? nextCall = null; + Instant? nextCall = null; - if (minutes > 0) - { - nextCall = now.Plus(Duration.FromMinutes(minutes)); - } + if (minutes > 0) + { + nextCall = now.Plus(Duration.FromMinutes(minutes)); + } - await sut.HandleAsync(@event); + await sut.HandleAsync(@event); - if (actual == RuleResult.Failed) - { - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Warning) - .MustHaveHappened(); - } - else - { - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Warning) - .MustNotHaveHappened(); - } - - A.CallTo(() => ruleEventRepository.UpdateAsync(@event.Job, - A<RuleJobUpdate>.That.Matches(x => - x.Elapsed == requestElapsed && - x.ExecutionDump == requestDump && - x.ExecutionResult == actual && - x.Finished == now && - x.JobNext == nextCall && - x.JobResult == jobResult), - A<CancellationToken>._)) + if (actual == RuleResult.Failed) + { + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Warning) .MustHaveHappened(); } - - private IRuleEventEntity CreateEvent(int numCalls, string actionName, string actionData) + else { - return CreateEvent(numCalls, actionName, actionData, DomainId.NewGuid()); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Warning) + .MustNotHaveHappened(); } - private IRuleEventEntity CreateEvent(int numCalls, string actionName, string actionData, DomainId id) - { - var @event = A.Fake<IRuleEventEntity>(); + A.CallTo(() => ruleEventRepository.UpdateAsync(@event.Job, + A<RuleJobUpdate>.That.Matches(x => + x.Elapsed == requestElapsed && + x.ExecutionDump == requestDump && + x.ExecutionResult == actual && + x.Finished == now && + x.JobNext == nextCall && + x.JobResult == jobResult), + A<CancellationToken>._)) + .MustHaveHappened(); + } - var job = new RuleJob - { - Id = id, - ActionData = actionData, - ActionName = actionName, - Created = clock.GetCurrentInstant() - }; - - A.CallTo(() => @event.Id).Returns(id); - A.CallTo(() => @event.Job).Returns(job); - A.CallTo(() => @event.Created).Returns(clock.GetCurrentInstant()); - A.CallTo(() => @event.NumCalls).Returns(numCalls); - - return @event; - } + private IRuleEventEntity CreateEvent(int numCalls, string actionName, string actionData) + { + return CreateEvent(numCalls, actionName, actionData, DomainId.NewGuid()); + } + + private IRuleEventEntity CreateEvent(int numCalls, string actionName, string actionData, DomainId id) + { + var @event = A.Fake<IRuleEventEntity>(); + + var job = new RuleJob + { + Id = id, + ActionData = actionData, + ActionName = actionName, + Created = clock.GetCurrentInstant() + }; + + A.CallTo(() => @event.Id).Returns(id); + A.CallTo(() => @event.Job).Returns(job); + A.CallTo(() => @event.Created).Returns(clock.GetCurrentInstant()); + A.CallTo(() => @event.NumCalls).Returns(numCalls); + + return @event; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 5cc681fa65..f9b29497a8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -20,206 +20,205 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules +namespace Squidex.Domain.Apps.Entities.Rules; + +public class RuleEnqueuerTests { - public class RuleEnqueuerTests + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly ILocalCache localCache = A.Fake<ILocalCache>(); + private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); + private readonly IRuleService ruleService = A.Fake<IRuleService>(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RuleEnqueuer sut; + + public sealed record TestAction : RuleAction { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly ILocalCache localCache = A.Fake<ILocalCache>(); - private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); - private readonly IRuleService ruleService = A.Fake<IRuleService>(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RuleEnqueuer sut; - - public sealed record TestAction : RuleAction - { - public Uri Url { get; set; } - } + public Uri Url { get; set; } + } - public RuleEnqueuerTests() - { - var options = Options.Create(new RuleOptions()); - - sut = new RuleEnqueuer(cache, localCache, - appProvider, - ruleEventRepository, - ruleService, - options, - A.Fake<ILogger<RuleEnqueuer>>()); - } - - [Fact] - public void Should_return_wildcard_filter_for_events_filter() - { - IEventConsumer consumer = sut; + public RuleEnqueuerTests() + { + var options = Options.Create(new RuleOptions()); + + sut = new RuleEnqueuer(cache, localCache, + appProvider, + ruleEventRepository, + ruleService, + options, + A.Fake<ILogger<RuleEnqueuer>>()); + } - Assert.Equal(".*", consumer.EventsFilter); - } + [Fact] + public void Should_return_wildcard_filter_for_events_filter() + { + IEventConsumer consumer = sut; - [Fact] - public async Task Should_do_nothing_on_clear() - { - IEventConsumer consumer = sut; + Assert.Equal(".*", consumer.EventsFilter); + } - await consumer.ClearAsync(); - } + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; - [Fact] - public void Should_return_type_name_for_name() - { - IEventConsumer consumer = sut; + await consumer.ClearAsync(); + } + + [Fact] + public void Should_return_type_name_for_name() + { + IEventConsumer consumer = sut; + + Assert.Equal(nameof(RuleEnqueuer), consumer.Name); + } + + [Fact] + public async Task Should_not_insert_job_if_null() + { + var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); - Assert.Equal(nameof(RuleEnqueuer), consumer.Name); - } + var rule = CreateRule(); - [Fact] - public async Task Should_not_insert_job_if_null() + var job = new RuleJob { - var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); + Created = now + }; - var rule = CreateRule(); + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + .Returns(new List<JobResult> { new JobResult() }.ToAsyncEnumerable()); - var job = new RuleJob - { - Created = now - }; + await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) - .Returns(new List<JobResult> { new JobResult() }.ToAsyncEnumerable()); + A.CallTo(() => ruleEventRepository.EnqueueAsync(A<RuleJob>._, (Exception?)null, default)) + .MustNotHaveHappened(); + } - await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); + [Fact] + public async Task Should_not_insert_job_if_job_has_a_skip_reason() + { + var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); - A.CallTo(() => ruleEventRepository.EnqueueAsync(A<RuleJob>._, (Exception?)null, default)) - .MustNotHaveHappened(); - } + var rule = CreateRule(); - [Fact] - public async Task Should_not_insert_job_if_job_has_a_skip_reason() + var job = new RuleJob { - var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); + Created = now + }; - var rule = CreateRule(); + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + .Returns(new List<JobResult> { new JobResult { Job = job, SkipReason = SkipReason.TooOld } }.ToAsyncEnumerable()); - var job = new RuleJob - { - Created = now - }; + await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) - .Returns(new List<JobResult> { new JobResult { Job = job, SkipReason = SkipReason.TooOld } }.ToAsyncEnumerable()); + A.CallTo(() => ruleEventRepository.EnqueueAsync(A<RuleJob>._, (Exception?)null, default)) + .MustNotHaveHappened(); + } - await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); + [Fact] + public async Task Should_update_repository_if_enqueing() + { + var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); - A.CallTo(() => ruleEventRepository.EnqueueAsync(A<RuleJob>._, (Exception?)null, default)) - .MustNotHaveHappened(); - } + var rule = CreateRule(); - [Fact] - public async Task Should_update_repository_if_enqueing() + var job = new RuleJob { - var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); + Created = now + }; - var rule = CreateRule(); + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + .Returns(new List<JobResult> { new JobResult { Job = job } }.ToAsyncEnumerable()); - var job = new RuleJob - { - Created = now - }; + await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) - .Returns(new List<JobResult> { new JobResult { Job = job } }.ToAsyncEnumerable()); + A.CallTo(() => ruleEventRepository.EnqueueAsync(job, (Exception?)null, default)) + .MustHaveHappened(); + } - await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); + [Fact] + public async Task Should_update_repository_if_enqueing_broken_job() + { + var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); - A.CallTo(() => ruleEventRepository.EnqueueAsync(job, (Exception?)null, default)) - .MustHaveHappened(); - } + var rule = CreateRule(); - [Fact] - public async Task Should_update_repository_if_enqueing_broken_job() + var job = new RuleJob { - var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); - - var rule = CreateRule(); + Created = now + }; - var job = new RuleJob - { - Created = now - }; + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) + .Returns(new List<JobResult> { new JobResult { Job = job, SkipReason = SkipReason.Failed } }.ToAsyncEnumerable()); - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) - .Returns(new List<JobResult> { new JobResult { Job = job, SkipReason = SkipReason.Failed } }.ToAsyncEnumerable()); + await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); - await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); + A.CallTo(() => ruleEventRepository.EnqueueAsync(job, (Exception?)null, default)) + .MustHaveHappened(); + } - A.CallTo(() => ruleEventRepository.EnqueueAsync(job, (Exception?)null, default)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_update_repository_with_jobs_from_service() + { + var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); - [Fact] - public async Task Should_update_repository_with_jobs_from_service() + var job1 = new RuleJob { - var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); - - var job1 = new RuleJob - { - Created = now - }; + Created = now + }; - SetupRules(@event, job1); + SetupRules(@event, job1); - await sut.On(@event); + await sut.On(@event); - A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, (Exception?)null, default)) - .MustHaveHappened(); - } + A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, (Exception?)null, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_eqneue_if_event_restored() - { - var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); + [Fact] + public async Task Should_not_eqneue_if_event_restored() + { + var @event = Envelope.Create<IEvent>(new ContentCreated { AppId = appId }); - var job1 = new RuleJob { Created = now }; + var job1 = new RuleJob { Created = now }; - SetupRules(@event, job1); + SetupRules(@event, job1); - await sut.On(@event.SetRestored(true)); + await sut.On(@event.SetRestored(true)); - A.CallTo(() => ruleEventRepository.EnqueueAsync(A<RuleJob>._, A<Exception?>._, default)) - .MustNotHaveHappened(); - } + A.CallTo(() => ruleEventRepository.EnqueueAsync(A<RuleJob>._, A<Exception?>._, default)) + .MustNotHaveHappened(); + } - private void SetupRules(Envelope<IEvent> @event, RuleJob job1) - { - var rule1 = CreateRule(); - var rule2 = CreateRule(); + private void SetupRules(Envelope<IEvent> @event, RuleJob job1) + { + var rule1 = CreateRule(); + var rule2 = CreateRule(); - A.CallTo(() => appProvider.GetRulesAsync(appId.Id, A<CancellationToken>._)) - .Returns(new List<IRuleEntity> { rule1, rule2 }); + A.CallTo(() => appProvider.GetRulesAsync(appId.Id, A<CancellationToken>._)) + .Returns(new List<IRuleEntity> { rule1, rule2 }); - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule1), default)) - .Returns(new List<JobResult> { new JobResult { Job = job1 } }.ToAsyncEnumerable()); + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule1), default)) + .Returns(new List<JobResult> { new JobResult { Job = job1 } }.ToAsyncEnumerable()); - A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule2), default)) - .Returns(new List<JobResult>().ToAsyncEnumerable()); - } + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule2), default)) + .Returns(new List<JobResult>().ToAsyncEnumerable()); + } - private static RuleEntity CreateRule() - { - var rule = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); + private static RuleEntity CreateRule() + { + var rule = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); - return new RuleEntity { RuleDef = rule, Id = DomainId.NewGuid() }; - } + return new RuleEntity { RuleDef = rule, Id = DomainId.NewGuid() }; + } - private static RuleContext MatchingContext(RuleEntity rule) - { - // These two properties must not be set to true for performance reasons. - return A<RuleContext>.That.Matches(x => - x.Rule == rule.RuleDef && - !x.IncludeSkipped && - !x.IncludeStale); - } + private static RuleContext MatchingContext(RuleEntity rule) + { + // These two properties must not be set to true for performance reasons. + return A<RuleContext>.That.Matches(x => + x.Rule == rule.RuleDef && + !x.IncludeSkipped && + !x.IncludeStale); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs index c1efc2f836..f725b964cd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs @@ -16,80 +16,79 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking; + +public class UsageTriggerHandlerTests { - public class UsageTriggerHandlerTests + private readonly IRuleTriggerHandler sut = new UsageTriggerHandler(); + + [Fact] + public void Should_return_false_if_asking_for_snapshot_support() { - private readonly IRuleTriggerHandler sut = new UsageTriggerHandler(); + Assert.False(sut.CanCreateSnapshotEvents); + } - [Fact] - public void Should_return_false_if_asking_for_snapshot_support() - { - Assert.False(sut.CanCreateSnapshotEvents); - } + [Fact] + public void Should_handle_usage_event() + { + Assert.True(sut.Handles(new AppUsageExceeded())); + } - [Fact] - public void Should_handle_usage_event() - { - Assert.True(sut.Handles(new AppUsageExceeded())); - } + [Fact] + public void Should_not_handle_other_event() + { + Assert.False(sut.Handles(new ContentCreated())); + } - [Fact] - public void Should_not_handle_other_event() - { - Assert.False(sut.Handles(new ContentCreated())); - } + [Fact] + public async Task Should_create_enriched_event() + { + var ctx = Context(); - [Fact] - public async Task Should_create_enriched_event() - { - var ctx = Context(); + var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; + var envelope = Envelope.Create<AppEvent>(@event); - var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; - var envelope = Envelope.Create<AppEvent>(@event); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var enrichedEvent = actual.Single() as EnrichedUsageExceededEvent; - var enrichedEvent = actual.Single() as EnrichedUsageExceededEvent; + Assert.Equal(@event.CallsCurrent, enrichedEvent!.CallsCurrent); + Assert.Equal(@event.CallsLimit, enrichedEvent!.CallsLimit); + } - Assert.Equal(@event.CallsCurrent, enrichedEvent!.CallsCurrent); - Assert.Equal(@event.CallsLimit, enrichedEvent!.CallsLimit); - } + [Fact] + public void Should_not_trigger_precheck_if_rule_id_not_matchs() + { + var ctx = Context(); - [Fact] - public void Should_not_trigger_precheck_if_rule_id_not_matchs() - { - var ctx = Context(); + var @event = new AppUsageExceeded(); - var @event = new AppUsageExceeded(); + var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); - var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + Assert.True(actual); + } - Assert.True(actual); - } + [Fact] + public void Should_trigger_precheck_if_event_type_correct_and_rule_id_matchs() + { + var ctx = Context(); - [Fact] - public void Should_trigger_precheck_if_event_type_correct_and_rule_id_matchs() - { - var ctx = Context(); + var @event = new AppUsageExceeded { RuleId = ctx.RuleId }; - var @event = new AppUsageExceeded { RuleId = ctx.RuleId }; + var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); - var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + Assert.True(actual); + } - Assert.True(actual); - } + private static RuleContext Context(RuleTrigger? trigger = null) + { + trigger ??= new UsageTrigger(); - private static RuleContext Context(RuleTrigger? trigger = null) + return new RuleContext { - trigger ??= new UsageTrigger(); - - return new RuleContext - { - AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), - Rule = new Rule(trigger, A.Fake<RuleAction>()), - RuleId = DomainId.NewGuid() - }; - } + AppId = NamedId.Of(DomainId.NewGuid(), "my-app"), + Rule = new Rule(trigger, A.Fake<RuleAction>()), + RuleId = DomainId.NewGuid() + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs index f3e79498d4..57bb97ffa5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs @@ -15,77 +15,76 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public class BackupSchemasTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly BackupSchemas sut; +namespace Squidex.Domain.Apps.Entities.Schemas; - public BackupSchemasTests() - { - ct = cts.Token; +public class BackupSchemasTests +{ + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly Rebuilder rebuilder = A.Fake<Rebuilder>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly BackupSchemas sut; - sut = new BackupSchemas(rebuilder); - } + public BackupSchemasTests() + { + ct = cts.Token; - [Fact] - public void Should_provide_name() - { - Assert.Equal("Schemas", sut.Name); - } + sut = new BackupSchemas(rebuilder); + } - [Fact] - public async Task Should_restore_indices_for_all_non_deleted_schemas() - { - var schemaId1 = NamedId.Of(DomainId.NewGuid(), "my-schema1"); - var schemaId2 = NamedId.Of(DomainId.NewGuid(), "my-schema2"); - var schemaId3 = NamedId.Of(DomainId.NewGuid(), "my-schema3"); + [Fact] + public void Should_provide_name() + { + Assert.Equal("Schemas", sut.Name); + } - var context = new RestoreContext(appId.Id, new UserMapping(RefToken.User("123")), A.Fake<IBackupReader>(), DomainId.NewGuid()); + [Fact] + public async Task Should_restore_indices_for_all_non_deleted_schemas() + { + var schemaId1 = NamedId.Of(DomainId.NewGuid(), "my-schema1"); + var schemaId2 = NamedId.Of(DomainId.NewGuid(), "my-schema2"); + var schemaId3 = NamedId.Of(DomainId.NewGuid(), "my-schema3"); - await sut.RestoreEventAsync(AppEvent(new SchemaCreated - { - SchemaId = schemaId1 - }), context, ct); + var context = new RestoreContext(appId.Id, new UserMapping(RefToken.User("123")), A.Fake<IBackupReader>(), DomainId.NewGuid()); - await sut.RestoreEventAsync(AppEvent(new SchemaCreated - { - SchemaId = schemaId2 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new SchemaCreated + { + SchemaId = schemaId1 + }), context, ct); - await sut.RestoreEventAsync(AppEvent(new SchemaCreated - { - SchemaId = schemaId3 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new SchemaCreated + { + SchemaId = schemaId2 + }), context, ct); - await sut.RestoreEventAsync(AppEvent(new SchemaDeleted - { - SchemaId = schemaId3 - }), context, ct); + await sut.RestoreEventAsync(AppEvent(new SchemaCreated + { + SchemaId = schemaId3 + }), context, ct); - var rebuildContents = new HashSet<DomainId>(); + await sut.RestoreEventAsync(AppEvent(new SchemaDeleted + { + SchemaId = schemaId3 + }), context, ct); - A.CallTo(() => rebuilder.InsertManyAsync<SchemaDomainObject, SchemaDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) - .Invokes(x => rebuildContents.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); + var rebuildContents = new HashSet<DomainId>(); - await sut.RestoreAsync(context, ct); + A.CallTo(() => rebuilder.InsertManyAsync<SchemaDomainObject, SchemaDomainObject.State>(A<IEnumerable<DomainId>>._, A<int>._, ct)) + .Invokes(x => rebuildContents.AddRange(x.GetArgument<IEnumerable<DomainId>>(0)!)); - Assert.Equal(new HashSet<DomainId> - { - DomainId.Combine(appId, schemaId1.Id), - DomainId.Combine(appId, schemaId2.Id) - }, rebuildContents); - } + await sut.RestoreAsync(context, ct); - private Envelope<SchemaEvent> AppEvent(SchemaEvent @event) + Assert.Equal(new HashSet<DomainId> { - @event.AppId = appId; + DomainId.Combine(appId, schemaId1.Id), + DomainId.Combine(appId, schemaId2.Id) + }, rebuildContents); + } + + private Envelope<SchemaEvent> AppEvent(SchemaEvent @event) + { + @event.AppId = appId; - return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.SchemaId.Id)); - } + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId, @event.SchemaId.Id)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ArrayFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ArrayFieldPropertiesTests.cs index 974133aaf1..dbf31f1357 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ArrayFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ArrayFieldPropertiesTests.cs @@ -11,32 +11,31 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class ArrayFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class ArrayFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_min_items_greater_than_max_items() { - [Fact] - public void Should_add_error_if_min_items_greater_than_max_items() - { - var sut = new ArrayFieldProperties { MinItems = 10, MaxItems = 5 }; + var sut = new ArrayFieldProperties { MinItems = 10, MaxItems = 5 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") + }); + } - [Fact] - public void Should_not_add_error_if_min_items_equals_to_max_items() - { - var sut = new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }; + [Fact] + public void Should_not_add_error_if_min_items_equals_to_max_items() + { + var sut = new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/AssetsFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/AssetsFieldPropertiesTests.cs index 0ea6e9cabe..e662e538b5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/AssetsFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/AssetsFieldPropertiesTests.cs @@ -11,122 +11,121 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class AssetsFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class AssetsFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_min_items_greater_than_max_items() + { + var sut = new AssetsFieldProperties { MinItems = 10, MaxItems = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") + }); + } + + [Fact] + public void Should_not_add_error_if_min_equals_to_max_items() + { + var sut = new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_min_width_greater_than_max_width() + { + var sut = new AssetsFieldProperties { MinWidth = 10, MaxWidth = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max width must be greater or equal to min width.", "MinWidth", "MaxWidth") + }); + } + + [Fact] + public void Should_not_add_error_if_min_width_equals_to_max_width() + { + var sut = new AssetsFieldProperties { MinWidth = 2, MaxWidth = 2 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_min_height_greater_than_max_height() { - [Fact] - public void Should_add_error_if_min_items_greater_than_max_items() - { - var sut = new AssetsFieldProperties { MinItems = 10, MaxItems = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var sut = new AssetsFieldProperties { MinHeight = 10, MaxHeight = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max height must be greater or equal to min height.", "MinHeight", "MaxHeight") + }); + } + + [Fact] + public void Should_not_add_error_if_min_height_equals_to_max_height() + { + var sut = new AssetsFieldProperties { MinHeight = 2, MaxHeight = 2 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_min_size_greater_than_max_size() + { + var sut = new AssetsFieldProperties { MinSize = 10, MaxSize = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max size must be greater than min size.", "MinSize", "MaxSize") + }); + } + + [Fact] + public void Should_add_error_if_only_aspect_width_is_defined() + { + var sut = new AssetsFieldProperties { AspectWidth = 10 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("If aspect width or aspect height is used both must be defined.", "AspectWidth", "AspectHeight") + }); + } + + [Fact] + public void Should_add_error_if_only_aspect_height_is_defined() + { + var sut = new AssetsFieldProperties { AspectHeight = 10 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") - }); - } - - [Fact] - public void Should_not_add_error_if_min_equals_to_max_items() - { - var sut = new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - - [Fact] - public void Should_add_error_if_min_width_greater_than_max_width() - { - var sut = new AssetsFieldProperties { MinWidth = 10, MaxWidth = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max width must be greater or equal to min width.", "MinWidth", "MaxWidth") - }); - } - - [Fact] - public void Should_not_add_error_if_min_width_equals_to_max_width() - { - var sut = new AssetsFieldProperties { MinWidth = 2, MaxWidth = 2 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - - [Fact] - public void Should_add_error_if_min_height_greater_than_max_height() - { - var sut = new AssetsFieldProperties { MinHeight = 10, MaxHeight = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max height must be greater or equal to min height.", "MinHeight", "MaxHeight") - }); - } - - [Fact] - public void Should_not_add_error_if_min_height_equals_to_max_height() - { - var sut = new AssetsFieldProperties { MinHeight = 2, MaxHeight = 2 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - - [Fact] - public void Should_add_error_if_min_size_greater_than_max_size() - { - var sut = new AssetsFieldProperties { MinSize = 10, MaxSize = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max size must be greater than min size.", "MinSize", "MaxSize") - }); - } - - [Fact] - public void Should_add_error_if_only_aspect_width_is_defined() - { - var sut = new AssetsFieldProperties { AspectWidth = 10 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("If aspect width or aspect height is used both must be defined.", "AspectWidth", "AspectHeight") - }); - } - - [Fact] - public void Should_add_error_if_only_aspect_height_is_defined() - { - var sut = new AssetsFieldProperties { AspectHeight = 10 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("If aspect width or aspect height is used both must be defined.", "AspectWidth", "AspectHeight") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("If aspect width or aspect height is used both must be defined.", "AspectWidth", "AspectHeight") + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/BooleanFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/BooleanFieldPropertiesTests.cs index 7a34d620d0..63f3b11d22 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/BooleanFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/BooleanFieldPropertiesTests.cs @@ -11,22 +11,21 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class BooleanFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class BooleanFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_editor_is_not_valid() { - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new BooleanFieldProperties { Editor = (BooleanFieldEditor)123 }; + var sut = new BooleanFieldProperties { Editor = (BooleanFieldEditor)123 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Editor is not a valid value.", "Editor") + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ComponentsFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ComponentsFieldPropertiesTests.cs index 92ea5cdc5e..75adf4a91c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ComponentsFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ComponentsFieldPropertiesTests.cs @@ -11,32 +11,31 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class ComponentsFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class ComponentsFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_min_items_greater_than_max_items() { - [Fact] - public void Should_add_error_if_min_items_greater_than_max_items() - { - var sut = new ComponentsFieldProperties { MinItems = 10, MaxItems = 5 }; + var sut = new ComponentsFieldProperties { MinItems = 10, MaxItems = 5 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") + }); + } - [Fact] - public void Should_not_add_error_if_min_items_equals_to_max_items() - { - var sut = new ComponentsFieldProperties { MinItems = 2, MaxItems = 2 }; + [Fact] + public void Should_not_add_error_if_min_items_equals_to_max_items() + { + var sut = new ComponentsFieldProperties { MinItems = 2, MaxItems = 2 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs index 5bc9b1f357..62268ac5d5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs @@ -13,84 +13,83 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class DateTimeFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class DateTimeFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_not_add_error_if_sut_is_valid() { - [Fact] - public void Should_not_add_error_if_sut_is_valid() + var sut = new DateTimeFieldProperties { - var sut = new DateTimeFieldProperties - { - MinValue = FutureDays(10), - MaxValue = FutureDays(20), - DefaultValue = FutureDays(15) - }; + MinValue = FutureDays(10), + MaxValue = FutureDays(20), + DefaultValue = FutureDays(15) + }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public void Should_add_error_if_min_value_greater_than_max_value() - { - var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), MaxValue = FutureDays(5) }; + [Fact] + public void Should_add_error_if_min_value_greater_than_max_value() + { + var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), MaxValue = FutureDays(5) }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max value must be greater than min value.", "MinValue", "MaxValue") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max value must be greater than min value.", "MinValue", "MaxValue") + }); + } - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new DateTimeFieldProperties { Editor = (DateTimeFieldEditor)123 }; + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new DateTimeFieldProperties { Editor = (DateTimeFieldEditor)123 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } - [Fact] - public void Should_add_error_if_calculated_default_value_is_not_valid() - { - var sut = new DateTimeFieldProperties { CalculatedDefaultValue = (DateTimeCalculatedDefaultValue)123 }; + [Fact] + public void Should_add_error_if_calculated_default_value_is_not_valid() + { + var sut = new DateTimeFieldProperties { CalculatedDefaultValue = (DateTimeCalculatedDefaultValue)123 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Calculated default value is not a valid value.", "CalculatedDefaultValue") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Calculated default value is not a valid value.", "CalculatedDefaultValue") + }); + } - [Fact] - public void Should_add_error_if_calculated_default_value_default_value_is_defined() - { - var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now, DefaultValue = FutureDays(10) }; + [Fact] + public void Should_add_error_if_calculated_default_value_default_value_is_defined() + { + var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now, DefaultValue = FutureDays(10) }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Calculated default value and default value cannot be used together.", "CalculatedDefaultValue", "DefaultValue") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Calculated default value and default value cannot be used together.", "CalculatedDefaultValue", "DefaultValue") + }); + } - private static Instant FutureDays(int days) - { - return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days)); - } + private static Instant FutureDays(int days) + { + return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days)); } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs index f576650f0e..d2c165943d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/GeolocationFieldPropertiesTests.cs @@ -11,22 +11,21 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class GeolocationFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class GeolocationFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_editor_is_not_valid() { - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new GeolocationFieldProperties { Editor = (GeolocationFieldEditor)123 }; + var sut = new GeolocationFieldProperties { Editor = (GeolocationFieldEditor)123 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Editor is not a valid value.", "Editor") + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/JsonFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/JsonFieldPropertiesTests.cs index aa08ffddf7..b3acf84a36 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/JsonFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/JsonFieldPropertiesTests.cs @@ -9,18 +9,17 @@ using Squidex.Domain.Apps.Core.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class JsonFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class JsonFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_editor_is_not_valid() { - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new JsonFieldProperties(); + var sut = new JsonFieldProperties(); - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/NumberFieldPropertiesTests.cs index c4ea04baec..df1bc50ab8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/NumberFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/NumberFieldPropertiesTests.cs @@ -12,93 +12,92 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class NumberFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class NumberFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_not_add_error_if_sut_is_valid() { - [Fact] - public void Should_not_add_error_if_sut_is_valid() + var sut = new NumberFieldProperties { - var sut = new NumberFieldProperties - { - MinValue = 0, - MaxValue = 100, - DefaultValue = 5 - }; + MinValue = 0, + MaxValue = 100, + DefaultValue = 5 + }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public void Should_add_error_if_min_value_greater_than_max_value() - { - var sut = new NumberFieldProperties { MinValue = 10, MaxValue = 5 }; + [Fact] + public void Should_add_error_if_min_value_greater_than_max_value() + { + var sut = new NumberFieldProperties { MinValue = 10, MaxValue = 5 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max value must be greater than min value.", "MinValue", "MaxValue") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max value must be greater than min value.", "MinValue", "MaxValue") + }); + } - [Fact] - public void Should_add_error_if_radio_button_has_no_allowed_values() - { - var sut = new NumberFieldProperties { Editor = NumberFieldEditor.Radio }; + [Fact] + public void Should_add_error_if_radio_button_has_no_allowed_values() + { + var sut = new NumberFieldProperties { Editor = NumberFieldEditor.Radio }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Radio buttons or dropdown list need allowed values.", "AllowedValues") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Radio buttons or dropdown list need allowed values.", "AllowedValues") + }); + } - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new NumberFieldProperties { Editor = (NumberFieldEditor)123 }; + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new NumberFieldProperties { Editor = (NumberFieldEditor)123 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } - [Theory] - [InlineData(NumberFieldEditor.Radio)] - public void Should_add_error_if_inline_editing_is_not_allowed_for_editor(NumberFieldEditor editor) - { - var sut = new NumberFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ReadonlyList.Create(1.0) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Inline editing is not allowed for Radio editor.", "InlineEditable", "Editor") - }); - } - - [Theory] - [InlineData(NumberFieldEditor.Input)] - [InlineData(NumberFieldEditor.Dropdown)] - [InlineData(NumberFieldEditor.Stars)] - public void Should_not_add_error_if_inline_editing_is_allowed_for_editor(NumberFieldEditor editor) - { - var sut = new NumberFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ReadonlyList.Create(1.0) }; + [Theory] + [InlineData(NumberFieldEditor.Radio)] + public void Should_add_error_if_inline_editing_is_not_allowed_for_editor(NumberFieldEditor editor) + { + var sut = new NumberFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ReadonlyList.Create(1.0) }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Inline editing is not allowed for Radio editor.", "InlineEditable", "Editor") + }); + } + + [Theory] + [InlineData(NumberFieldEditor.Input)] + [InlineData(NumberFieldEditor.Dropdown)] + [InlineData(NumberFieldEditor.Stars)] + public void Should_not_add_error_if_inline_editing_is_allowed_for_editor(NumberFieldEditor editor) + { + var sut = new NumberFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ReadonlyList.Create(1.0) }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs index f8269c9d46..08156a299c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs @@ -11,60 +11,59 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class ReferencesFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class ReferencesFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_min_items_greater_than_max_items() { - [Fact] - public void Should_add_error_if_min_items_greater_than_max_items() - { - var sut = new ReferencesFieldProperties { MinItems = 10, MaxItems = 5 }; + var sut = new ReferencesFieldProperties { MinItems = 10, MaxItems = 5 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") + }); + } - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new ReferencesFieldProperties { Editor = (ReferencesFieldEditor)123 }; + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new ReferencesFieldProperties { Editor = (ReferencesFieldEditor)123 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } - [Fact] - public void Should_add_error_if_resolving_references_with_more_than_one_max_items() - { - var sut = new ReferencesFieldProperties { ResolveReference = true, MaxItems = 2 }; + [Fact] + public void Should_add_error_if_resolving_references_with_more_than_one_max_items() + { + var sut = new ReferencesFieldProperties { ResolveReference = true, MaxItems = 2 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Can only resolve references when MaxItems is 1.", "ResolveReference", "MaxItems") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Can only resolve references when MaxItems is 1.", "ResolveReference", "MaxItems") + }); + } - [Fact] - public void Should_not_add_error_if_min_items_greater_equals_to_max_items() - { - var sut = new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }; + [Fact] + public void Should_not_add_error_if_min_items_greater_equals_to_max_items() + { + var sut = new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/StringFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/StringFieldPropertiesTests.cs index 68745b27ed..81f24c7400 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/StringFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/StringFieldPropertiesTests.cs @@ -12,167 +12,166 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class StringFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class StringFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_min_length_greater_than_max() + { + var sut = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max length must be greater or equal to min length.", "MinLength", "MaxLength") + }); + } + + [Fact] + public void Should_not_add_error_if_min_length_equal_to_max_length() { - [Fact] - public void Should_add_error_if_min_length_greater_than_max() - { - var sut = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var sut = new StringFieldProperties { MinLength = 2, MaxLength = 2 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_min_characters_greater_than_max() + { + var sut = new StringFieldProperties { MinCharacters = 10, MaxCharacters = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max characters must be greater or equal to min characters.", "MinCharacters", "MaxCharacters") + }); + } + + [Fact] + public void Should_not_add_error_if_min_characters_equal_to_max_characters() + { + var sut = new StringFieldProperties { MinCharacters = 2, MaxCharacters = 2 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_min_words_greater_than_max() + { + var sut = new StringFieldProperties { MinWords = 10, MaxWords = 5 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max words must be greater or equal to min words.", "MinWords", "MaxWords") + }); + } + + [Fact] + public void Should_not_add_error_if_min_words_equal_to_max_words() + { + var sut = new StringFieldProperties { MinWords = 2, MaxWords = 2 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_radio_button_has_no_allowed_values() + { + var sut = new StringFieldProperties { Editor = StringFieldEditor.Radio }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Radio buttons or dropdown list need allowed values.", "AllowedValues") + }); + } + + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new StringFieldProperties { Editor = (StringFieldEditor)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Editor is not a valid value.", "Editor") + }); + } + + [Fact] + public void Should_add_error_if_content_type_is_not_valid() + { + var sut = new StringFieldProperties { ContentType = (StringContentType)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Content type is not a valid value.", "ContentType") + }); + } + + [Fact] + public void Should_add_error_if_pattern_is_not_valid_regex() + { + var sut = new StringFieldProperties { Pattern = "[0-9{1}" }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Pattern is not a valid value.", "Pattern") + }); + } + + [Theory] + [InlineData(StringFieldEditor.Markdown)] + [InlineData(StringFieldEditor.Radio)] + [InlineData(StringFieldEditor.RichText)] + [InlineData(StringFieldEditor.TextArea)] + public void Should_add_error_if_inline_editing_is_not_allowed_for_editor(StringFieldEditor editor) + { + var sut = new StringFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ReadonlyList.Create("Value") }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Inline editing is only allowed for dropdowns, slugs and input fields.", "InlineEditable", "Editor") + }); + } + + [Theory] + [InlineData(StringFieldEditor.Dropdown)] + [InlineData(StringFieldEditor.Input)] + [InlineData(StringFieldEditor.Slug)] + public void Should_not_add_error_if_inline_editing_is_allowed_for_editor(StringFieldEditor editor) + { + var sut = new StringFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ReadonlyList.Create("Value") }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max length must be greater or equal to min length.", "MinLength", "MaxLength") - }); - } - - [Fact] - public void Should_not_add_error_if_min_length_equal_to_max_length() - { - var sut = new StringFieldProperties { MinLength = 2, MaxLength = 2 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - - [Fact] - public void Should_add_error_if_min_characters_greater_than_max() - { - var sut = new StringFieldProperties { MinCharacters = 10, MaxCharacters = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max characters must be greater or equal to min characters.", "MinCharacters", "MaxCharacters") - }); - } - - [Fact] - public void Should_not_add_error_if_min_characters_equal_to_max_characters() - { - var sut = new StringFieldProperties { MinCharacters = 2, MaxCharacters = 2 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - - [Fact] - public void Should_add_error_if_min_words_greater_than_max() - { - var sut = new StringFieldProperties { MinWords = 10, MaxWords = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max words must be greater or equal to min words.", "MinWords", "MaxWords") - }); - } - - [Fact] - public void Should_not_add_error_if_min_words_equal_to_max_words() - { - var sut = new StringFieldProperties { MinWords = 2, MaxWords = 2 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } - - [Fact] - public void Should_add_error_if_radio_button_has_no_allowed_values() - { - var sut = new StringFieldProperties { Editor = StringFieldEditor.Radio }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Radio buttons or dropdown list need allowed values.", "AllowedValues") - }); - } - - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new StringFieldProperties { Editor = (StringFieldEditor)123 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } - - [Fact] - public void Should_add_error_if_content_type_is_not_valid() - { - var sut = new StringFieldProperties { ContentType = (StringContentType)123 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Content type is not a valid value.", "ContentType") - }); - } - - [Fact] - public void Should_add_error_if_pattern_is_not_valid_regex() - { - var sut = new StringFieldProperties { Pattern = "[0-9{1}" }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Pattern is not a valid value.", "Pattern") - }); - } - - [Theory] - [InlineData(StringFieldEditor.Markdown)] - [InlineData(StringFieldEditor.Radio)] - [InlineData(StringFieldEditor.RichText)] - [InlineData(StringFieldEditor.TextArea)] - public void Should_add_error_if_inline_editing_is_not_allowed_for_editor(StringFieldEditor editor) - { - var sut = new StringFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ReadonlyList.Create("Value") }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Inline editing is only allowed for dropdowns, slugs and input fields.", "InlineEditable", "Editor") - }); - } - - [Theory] - [InlineData(StringFieldEditor.Dropdown)] - [InlineData(StringFieldEditor.Input)] - [InlineData(StringFieldEditor.Slug)] - public void Should_not_add_error_if_inline_editing_is_allowed_for_editor(StringFieldEditor editor) - { - var sut = new StringFieldProperties { InlineEditable = true, Editor = editor, AllowedValues = ReadonlyList.Create("Value") }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - Assert.Empty(errors); - } + Assert.Empty(errors); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/TagsFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/TagsFieldPropertiesTests.cs index df7efaf5eb..49a0151878 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/TagsFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/TagsFieldPropertiesTests.cs @@ -11,60 +11,59 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class TagsFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class TagsFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_add_error_if_min_items_greater_than_max_items() { - [Fact] - public void Should_add_error_if_min_items_greater_than_max_items() - { - var sut = new TagsFieldProperties { MinItems = 10, MaxItems = 5 }; + var sut = new TagsFieldProperties { MinItems = 10, MaxItems = 5 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Max items must be greater or equal to min items.", "MinItems", "MaxItems") + }); + } - [Fact] - public void Should_not_add_error_if_min_items_equal_to_max_items() - { - var sut = new TagsFieldProperties { MinItems = 2, MaxItems = 2 }; + [Fact] + public void Should_not_add_error_if_min_items_equal_to_max_items() + { + var sut = new TagsFieldProperties { MinItems = 2, MaxItems = 2 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public void Should_add_error_if_radio_button_has_no_allowed_values() - { - var sut = new TagsFieldProperties { Editor = TagsFieldEditor.Checkboxes }; + [Fact] + public void Should_add_error_if_radio_button_has_no_allowed_values() + { + var sut = new TagsFieldProperties { Editor = TagsFieldEditor.Checkboxes }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Checkboxes or dropdown list need allowed values.", "AllowedValues") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Checkboxes or dropdown list need allowed values.", "AllowedValues") + }); + } - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new TagsFieldProperties { Editor = (TagsFieldEditor)123 }; + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new TagsFieldProperties { Editor = (TagsFieldEditor)123 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Editor is not a valid value.", "Editor") + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/UIFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/UIFieldPropertiesTests.cs index f08dde9bb4..fbf5d02a98 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/UIFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/UIFieldPropertiesTests.cs @@ -11,32 +11,31 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; + +public class UIFieldPropertiesTests : IClassFixture<TranslationsFixture> { - public class UIFieldPropertiesTests : IClassFixture<TranslationsFixture> + [Fact] + public void Should_not_add_error_if_editor_is_correct() { - [Fact] - public void Should_not_add_error_if_editor_is_correct() - { - var sut = new UIFieldProperties { Editor = UIFieldEditor.Separator }; + var sut = new UIFieldProperties { Editor = UIFieldEditor.Separator }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - Assert.Empty(errors); - } + Assert.Empty(errors); + } - [Fact] - public void Should_add_error_if_editor_is_not_valid() - { - var sut = new UIFieldProperties { Editor = (UIFieldEditor)123 }; + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new UIFieldProperties { Editor = (UIFieldEditor)123 }; - var errors = FieldPropertiesValidator.Validate(sut).ToList(); + var errors = FieldPropertiesValidator.Validate(sut).ToList(); - errors.Should().BeEquivalentTo( - new List<ValidationError> - { - new ValidationError("Editor is not a valid value.", "Editor") - }); - } + errors.Should().BeEquivalentTo( + new List<ValidationError> + { + new ValidationError("Editor is not a valid value.", "Editor") + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs index 7bd4493763..3fad23b05b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs @@ -16,310 +16,309 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards; + +public class GuardSchemaFieldTests : IClassFixture<TranslationsFixture> { - public class GuardSchemaFieldTests : IClassFixture<TranslationsFixture> - { - private readonly Schema schema_0; - private readonly StringFieldProperties validProperties = new StringFieldProperties(); - private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; - - public GuardSchemaFieldTests() - { - schema_0 = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Invariant) - .AddString(2, "field2", Partitioning.Invariant) - .AddArray(3, "field3", Partitioning.Invariant, f => f - .AddNumber(301, "field301")) - .AddUI(4, "field4", Partitioning.Invariant); - } - - private static Action<T, Schema> A<T>(Action<T, Schema> method) where T : FieldCommand - { - return method; - } - - public static IEnumerable<object[]> FieldCommandData() - { - yield return new object[] { A<EnableField>(GuardSchemaField.CanEnable) }; - yield return new object[] { A<DeleteField>(GuardSchemaField.CanDelete) }; - yield return new object[] { A<DisableField>(GuardSchemaField.CanDisable) }; - yield return new object[] { A<HideField>(GuardSchemaField.CanHide) }; - yield return new object[] { A<LockField>(GuardSchemaField.CanLock) }; - yield return new object[] { A<ShowField>(GuardSchemaField.CanShow) }; - yield return new object[] { A<UpdateField>(GuardSchemaField.CanUpdate) }; - } - - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_field_not_found<T>(Action<T, Schema> action) where T : FieldCommand, new() - { - var command = new T { FieldId = 5 }; - - Assert.Throws<DomainObjectNotFoundException>(() => action(command, schema_0)); - } - - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_parent_field_not_found<T>(Action<T, Schema> action) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 4, FieldId = 401 }; - - Assert.Throws<DomainObjectNotFoundException>(() => action(command, schema_0)); - } + private readonly Schema schema_0; + private readonly StringFieldProperties validProperties = new StringFieldProperties(); + private readonly StringFieldProperties invalidProperties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; - [Theory] - [MemberData(nameof(FieldCommandData))] - public void Commands_should_throw_exception_if_child_field_not_found<T>(Action<T, Schema> action) where T : FieldCommand, new() - { - var command = new T { ParentFieldId = 3, FieldId = 302 }; - - Assert.Throws<DomainObjectNotFoundException>(() => action(command, schema_0)); - } - - [Fact] - public void CanDisable_should_not_throw_exception_if_already_disabled() - { - var command = new DisableField { FieldId = 1 }; + public GuardSchemaFieldTests() + { + schema_0 = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Invariant) + .AddString(2, "field2", Partitioning.Invariant) + .AddArray(3, "field3", Partitioning.Invariant, f => f + .AddNumber(301, "field301")) + .AddUI(4, "field4", Partitioning.Invariant); + } - var schema_1 = schema_0.UpdateField(1, f => f.Disable()); + private static Action<T, Schema> A<T>(Action<T, Schema> method) where T : FieldCommand + { + return method; + } - GuardSchemaField.CanDisable(command, schema_1); - } + public static IEnumerable<object[]> FieldCommandData() + { + yield return new object[] { A<EnableField>(GuardSchemaField.CanEnable) }; + yield return new object[] { A<DeleteField>(GuardSchemaField.CanDelete) }; + yield return new object[] { A<DisableField>(GuardSchemaField.CanDisable) }; + yield return new object[] { A<HideField>(GuardSchemaField.CanHide) }; + yield return new object[] { A<LockField>(GuardSchemaField.CanLock) }; + yield return new object[] { A<ShowField>(GuardSchemaField.CanShow) }; + yield return new object[] { A<UpdateField>(GuardSchemaField.CanUpdate) }; + } - [Fact] - public void CanDisable_should_throw_exception_if_locked() - { - var command = new DisableField { FieldId = 1 }; + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_field_not_found<T>(Action<T, Schema> action) where T : FieldCommand, new() + { + var command = new T { FieldId = 5 }; - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + Assert.Throws<DomainObjectNotFoundException>(() => action(command, schema_0)); + } - Assert.Throws<DomainException>(() => GuardSchemaField.CanDisable(command, schema_1)); - } + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_parent_field_not_found<T>(Action<T, Schema> action) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 4, FieldId = 401 }; - [Fact] - public void CanDisable_should_throw_exception_if_ui_field() - { - var command = new DisableField { FieldId = 4 }; + Assert.Throws<DomainObjectNotFoundException>(() => action(command, schema_0)); + } - Assert.Throws<DomainException>(() => GuardSchemaField.CanDisable(command, schema_0)); - } + [Theory] + [MemberData(nameof(FieldCommandData))] + public void Commands_should_throw_exception_if_child_field_not_found<T>(Action<T, Schema> action) where T : FieldCommand, new() + { + var command = new T { ParentFieldId = 3, FieldId = 302 }; - [Fact] - public void CanEnable_should_not_throw_exception_if_already_enabled() - { - var command = new EnableField { FieldId = 1 }; + Assert.Throws<DomainObjectNotFoundException>(() => action(command, schema_0)); + } - var schema_1 = schema_0.UpdateField(1, f => f.Enable()); + [Fact] + public void CanDisable_should_not_throw_exception_if_already_disabled() + { + var command = new DisableField { FieldId = 1 }; - GuardSchemaField.CanEnable(command, schema_1); - } + var schema_1 = schema_0.UpdateField(1, f => f.Disable()); - [Fact] - public void CanEnable_should_throw_exception_if_locked() - { - var command = new EnableField { FieldId = 1 }; + GuardSchemaField.CanDisable(command, schema_1); + } - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + [Fact] + public void CanDisable_should_throw_exception_if_locked() + { + var command = new DisableField { FieldId = 1 }; - Assert.Throws<DomainException>(() => GuardSchemaField.CanEnable(command, schema_1)); - } + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - [Fact] - public void CanEnable_should_throw_exception_if_ui_field() - { - var command = new EnableField { FieldId = 4 }; + Assert.Throws<DomainException>(() => GuardSchemaField.CanDisable(command, schema_1)); + } - Assert.Throws<DomainException>(() => GuardSchemaField.CanEnable(command, schema_0)); - } + [Fact] + public void CanDisable_should_throw_exception_if_ui_field() + { + var command = new DisableField { FieldId = 4 }; - [Fact] - public void CanHide_should_not_throw_exception_if_already_hidden() - { - var command = new EnableField { FieldId = 1 }; + Assert.Throws<DomainException>(() => GuardSchemaField.CanDisable(command, schema_0)); + } - var schema_1 = schema_0.UpdateField(1, f => f.Hide()); + [Fact] + public void CanEnable_should_not_throw_exception_if_already_enabled() + { + var command = new EnableField { FieldId = 1 }; - GuardSchemaField.CanEnable(command, schema_1); - } + var schema_1 = schema_0.UpdateField(1, f => f.Enable()); - [Fact] - public void CanHide_should_throw_exception_if_locked() - { - var command = new HideField { FieldId = 1 }; + GuardSchemaField.CanEnable(command, schema_1); + } - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + [Fact] + public void CanEnable_should_throw_exception_if_locked() + { + var command = new EnableField { FieldId = 1 }; - Assert.Throws<DomainException>(() => GuardSchemaField.CanHide(command, schema_1)); - } + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - [Fact] - public void CanHide_should_throw_exception_if_ui_field() - { - var command = new HideField { FieldId = 4 }; + Assert.Throws<DomainException>(() => GuardSchemaField.CanEnable(command, schema_1)); + } - Assert.Throws<DomainException>(() => GuardSchemaField.CanHide(command, schema_0)); - } + [Fact] + public void CanEnable_should_throw_exception_if_ui_field() + { + var command = new EnableField { FieldId = 4 }; - [Fact] - public void CanShow_should_not_throw_exception_if_already_shown() - { - var command = new EnableField { FieldId = 1 }; + Assert.Throws<DomainException>(() => GuardSchemaField.CanEnable(command, schema_0)); + } - var schema_1 = schema_0.UpdateField(1, f => f.Show()); + [Fact] + public void CanHide_should_not_throw_exception_if_already_hidden() + { + var command = new EnableField { FieldId = 1 }; - GuardSchemaField.CanEnable(command, schema_1); - } + var schema_1 = schema_0.UpdateField(1, f => f.Hide()); - [Fact] - public void CanShow_should_throw_exception_if_locked() - { - var command = new ShowField { FieldId = 1 }; + GuardSchemaField.CanEnable(command, schema_1); + } - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + [Fact] + public void CanHide_should_throw_exception_if_locked() + { + var command = new HideField { FieldId = 1 }; - Assert.Throws<DomainException>(() => GuardSchemaField.CanShow(command, schema_1)); - } + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - [Fact] - public void CanShow_should_throw_exception_if_ui_field() - { - var command = new ShowField { FieldId = 4 }; + Assert.Throws<DomainException>(() => GuardSchemaField.CanHide(command, schema_1)); + } - Assert.Throws<DomainException>(() => GuardSchemaField.CanShow(command, schema_0)); - } + [Fact] + public void CanHide_should_throw_exception_if_ui_field() + { + var command = new HideField { FieldId = 4 }; - [Fact] - public void CanDelete_should_throw_exception_if_locked() - { - var command = new DeleteField { FieldId = 1 }; + Assert.Throws<DomainException>(() => GuardSchemaField.CanHide(command, schema_0)); + } - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + [Fact] + public void CanShow_should_not_throw_exception_if_already_shown() + { + var command = new EnableField { FieldId = 1 }; - Assert.Throws<DomainException>(() => GuardSchemaField.CanDelete(command, schema_1)); - } + var schema_1 = schema_0.UpdateField(1, f => f.Show()); - [Fact] - public void CanDelete_should_not_throw_exception_if_not_locked() - { - var command = new DeleteField { FieldId = 1 }; + GuardSchemaField.CanEnable(command, schema_1); + } - GuardSchemaField.CanDelete(command, schema_0); - } + [Fact] + public void CanShow_should_throw_exception_if_locked() + { + var command = new ShowField { FieldId = 1 }; - [Fact] - public void CanUpdate_should_throw_exception_if_locked() - { - var command = new UpdateField { FieldId = 1, Properties = validProperties }; + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - var schema_1 = schema_0.UpdateField(1, f => f.Lock()); + Assert.Throws<DomainException>(() => GuardSchemaField.CanShow(command, schema_1)); + } - Assert.Throws<DomainException>(() => GuardSchemaField.CanUpdate(command, schema_1)); - } + [Fact] + public void CanShow_should_throw_exception_if_ui_field() + { + var command = new ShowField { FieldId = 4 }; - [Fact] - public void CanUpdate_should_not_throw_exception_if_not_locked() - { - var command = new UpdateField { FieldId = 1, Properties = validProperties }; + Assert.Throws<DomainException>(() => GuardSchemaField.CanShow(command, schema_0)); + } - GuardSchemaField.CanUpdate(command, schema_0); - } + [Fact] + public void CanDelete_should_throw_exception_if_locked() + { + var command = new DeleteField { FieldId = 1 }; - [Fact] - public void CanUpdate_should_throw_exception_if_properties_null() - { - var command = new UpdateField { FieldId = 2, Properties = null! }; + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(command, schema_0), - new ValidationError("Properties is required.", "Properties")); - } + Assert.Throws<DomainException>(() => GuardSchemaField.CanDelete(command, schema_1)); + } - [Fact] - public void CanUpdate_should_throw_exception_if_properties_not_valid() - { - var command = new UpdateField { FieldId = 2, Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; + [Fact] + public void CanDelete_should_not_throw_exception_if_not_locked() + { + var command = new DeleteField { FieldId = 1 }; - ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(command, schema_0), - new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); - } + GuardSchemaField.CanDelete(command, schema_0); + } - [Fact] - public void CanAdd_should_throw_exception_if_field_already_exists() - { - var command = new AddField { Name = "field1", Properties = validProperties }; + [Fact] + public void CanUpdate_should_throw_exception_if_locked() + { + var command = new UpdateField { FieldId = 1, Properties = validProperties }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), - new ValidationError("A field with the same name already exists.")); - } + var schema_1 = schema_0.UpdateField(1, f => f.Lock()); - [Fact] - public void CanAdd_should_throw_exception_if_nested_field_already_exists() - { - var command = new AddField { Name = "field301", Properties = validProperties, ParentFieldId = 3 }; + Assert.Throws<DomainException>(() => GuardSchemaField.CanUpdate(command, schema_1)); + } - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), - new ValidationError("A field with the same name already exists.")); - } + [Fact] + public void CanUpdate_should_not_throw_exception_if_not_locked() + { + var command = new UpdateField { FieldId = 1, Properties = validProperties }; + + GuardSchemaField.CanUpdate(command, schema_0); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_properties_null() + { + var command = new UpdateField { FieldId = 2, Properties = null! }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(command, schema_0), + new ValidationError("Properties is required.", "Properties")); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_properties_not_valid() + { + var command = new UpdateField { FieldId = 2, Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; + + ValidationAssert.Throws(() => GuardSchemaField.CanUpdate(command, schema_0), + new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_field_already_exists() + { + var command = new AddField { Name = "field1", Properties = validProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), + new ValidationError("A field with the same name already exists.")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_nested_field_already_exists() + { + var command = new AddField { Name = "field301", Properties = validProperties, ParentFieldId = 3 }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), + new ValidationError("A field with the same name already exists.")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_not_valid() + { + var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; - [Fact] - public void CanAdd_should_throw_exception_if_name_not_valid() - { - var command = new AddField { Name = "INVALID_NAME", Properties = validProperties }; + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), + new ValidationError("Name is not a Javascript property name.", "Name")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_properties_not_valid() + { + var command = new AddField { Name = "field5", Properties = invalidProperties }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), - new ValidationError("Name is not a Javascript property name.", "Name")); - } + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), + new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_properties_null() + { + var command = new AddField { Name = "field5", Properties = null! }; - [Fact] - public void CanAdd_should_throw_exception_if_properties_not_valid() - { - var command = new AddField { Name = "field5", Properties = invalidProperties }; + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), + new ValidationError("Properties is required.", "Properties")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_partitioning_not_valid() + { + var command = new AddField { Name = "field5", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; + + ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), + new ValidationError("Partitioning is not a valid value.", "Partitioning")); + } + + [Fact] + public void CanAdd_should_throw_exception_if_parent_not_exists() + { + var command = new AddField { Name = "field302", Properties = validProperties, ParentFieldId = 99 }; + + Assert.Throws<DomainObjectNotFoundException>(() => GuardSchemaField.CanAdd(command, schema_0)); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_field_not_exists() + { + var command = new AddField { Name = "field5", Properties = validProperties }; + + GuardSchemaField.CanAdd(command, schema_0); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_field_exists_in_root() + { + var command = new AddField { Name = "field1", Properties = validProperties, ParentFieldId = 3 }; - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), - new ValidationError("Max length must be greater or equal to min length.", "Properties.MinLength", "Properties.MaxLength")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_properties_null() - { - var command = new AddField { Name = "field5", Properties = null! }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), - new ValidationError("Properties is required.", "Properties")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_partitioning_not_valid() - { - var command = new AddField { Name = "field5", Partitioning = "INVALID_PARTITIONING", Properties = validProperties }; - - ValidationAssert.Throws(() => GuardSchemaField.CanAdd(command, schema_0), - new ValidationError("Partitioning is not a valid value.", "Partitioning")); - } - - [Fact] - public void CanAdd_should_throw_exception_if_parent_not_exists() - { - var command = new AddField { Name = "field302", Properties = validProperties, ParentFieldId = 99 }; - - Assert.Throws<DomainObjectNotFoundException>(() => GuardSchemaField.CanAdd(command, schema_0)); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_field_not_exists() - { - var command = new AddField { Name = "field5", Properties = validProperties }; - - GuardSchemaField.CanAdd(command, schema_0); - } - - [Fact] - public void CanAdd_should_not_throw_exception_if_field_exists_in_root() - { - var command = new AddField { Name = "field1", Properties = validProperties, ParentFieldId = 3 }; - - GuardSchemaField.CanAdd(command, schema_0); - } + GuardSchemaField.CanAdd(command, schema_0); } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs index 83ba06d30e..e92ecbe08f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs @@ -17,661 +17,660 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards; + +public class GuardSchemaTests : IClassFixture<TranslationsFixture> { - public class GuardSchemaTests : IClassFixture<TranslationsFixture> + private readonly Schema schema_0; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + + public GuardSchemaTests() { - private readonly Schema schema_0; - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + schema_0 = + new Schema("my-schema") + .AddString(1, "field1", Partitioning.Invariant) + .AddString(2, "field2", Partitioning.Invariant) + .AddUI(4, "field4", Partitioning.Invariant); + } - public GuardSchemaTests() - { - schema_0 = - new Schema("my-schema") - .AddString(1, "field1", Partitioning.Invariant) - .AddString(2, "field2", Partitioning.Invariant) - .AddUI(4, "field4", Partitioning.Invariant); - } - - [Fact] - public void CanCreate_should_throw_exception_if_name_not_valid() - { - var command = CreateCommand(new CreateSchema { Name = "INVALID NAME" }); + [Fact] + public void CanCreate_should_throw_exception_if_name_not_valid() + { + var command = CreateCommand(new CreateSchema { Name = "INVALID NAME" }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Name is not a valid slug.", "Name")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Name is not a valid slug.", "Name")); + } - [Fact] - public void CanCreate_should_throw_exception_if_field_name_invalid() + [Fact] + public void CanCreate_should_throw_exception_if_field_name_invalid() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "invalid name", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }); + Name = "invalid name", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Name is not a Javascript property name.", - "Fields[0].Name")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Name is not a Javascript property name.", + "Fields[0].Name")); + } - [Fact] - public void CanCreate_should_throw_exception_if_field_properties_null() + [Fact] + public void CanCreate_should_throw_exception_if_field_properties_null() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "field1", - Properties = null!, - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }); + Name = "field1", + Properties = null!, + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Properties is required.", - "Fields[0].Properties")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Properties is required.", + "Fields[0].Properties")); + } - [Fact] - public void CanCreate_should_throw_exception_if_field_properties_not_valid() + [Fact] + public void CanCreate_should_throw_exception_if_field_properties_not_valid() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }, - Partitioning = Partitioning.Invariant.Key - } - }, - Name = "new-schema" - }); - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Max length must be greater or equal to min length.", - "Fields[0].Properties.MinLength", - "Fields[0].Properties.MaxLength")); - } + Name = "field1", + Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 }, + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }); + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Max length must be greater or equal to min length.", + "Fields[0].Properties.MinLength", + "Fields[0].Properties.MaxLength")); + } - [Fact] - public void CanCreate_should_throw_exception_if_field_partitioning_not_valid() + [Fact] + public void CanCreate_should_throw_exception_if_field_partitioning_not_valid() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = "INVALID" - } - }, - Name = "new-schema" - }); + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = "INVALID" + } + }, + Name = "new-schema" + }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Partitioning is not a valid value.", - "Fields[0].Partitioning")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Partitioning is not a valid value.", + "Fields[0].Partitioning")); + } - [Fact] - public void CanCreate_should_throw_exception_if_fields_contains_duplicate_name() + [Fact] + public void CanCreate_should_throw_exception_if_fields_contains_duplicate_name() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - } + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key }, - Name = "new-schema" - }); + new UpsertSchemaField + { + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key + } + }, + Name = "new-schema" + }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field 'field1' has been added twice.", - "Fields")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field 'field1' has been added twice.", + "Fields")); + } - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_name_invalid() + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_name_invalid() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new[] { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new[] + new UpsertSchemaNestedField { - new UpsertSchemaNestedField - { - Name = "invalid name", - Properties = new StringFieldProperties() - } + Name = "invalid name", + Properties = new StringFieldProperties() } } - }, - Name = "new-schema" - }); + } + }, + Name = "new-schema" + }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Name is not a Javascript property name.", - "Fields[0].Nested[0].Name")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Name is not a Javascript property name.", + "Fields[0].Nested[0].Name")); + } - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_properties_null() + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_properties_null() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new[] { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new[] + new UpsertSchemaNestedField { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = null! - } + Name = "nested1", + Properties = null! } } - }, - Name = "new-schema" - }); + } + }, + Name = "new-schema" + }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Properties is required.", - "Fields[0].Nested[0].Properties")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Properties is required.", + "Fields[0].Nested[0].Properties")); + } - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_is_array() + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_is_array() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new[] { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new[] + new UpsertSchemaNestedField { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new ArrayFieldProperties() - } + Name = "nested1", + Properties = new ArrayFieldProperties() } } - }, - Name = "new-schema" - }); + } + }, + Name = "new-schema" + }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Nested field cannot be array fields.", - "Fields[0].Nested[0].Properties")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Nested field cannot be array fields.", + "Fields[0].Nested[0].Properties")); + } - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_properties_not_valid() + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_properties_not_valid() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new[] { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new[] + new UpsertSchemaNestedField { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } - } + Name = "nested1", + Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } } } - }, - Name = "new-schema" - }); - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Max length must be greater or equal to min length.", - "Fields[0].Nested[0].Properties.MinLength", - "Fields[0].Nested[0].Properties.MaxLength")); - } + } + }, + Name = "new-schema" + }); + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Max length must be greater or equal to min length.", + "Fields[0].Nested[0].Properties.MinLength", + "Fields[0].Nested[0].Properties.MaxLength")); + } - [Fact] - public void CanCreate_should_throw_exception_if_nested_field_have_duplicate_names() + [Fact] + public void CanCreate_should_throw_exception_if_nested_field_have_duplicate_names() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField + Name = "array", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new[] { - Name = "array", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new[] + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = new StringFieldProperties() + }, + new UpsertSchemaNestedField { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties() - }, - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = new StringFieldProperties() - } + Name = "nested1", + Properties = new StringFieldProperties() } } - }, - Name = "new-schema" - }); + } + }, + Name = "new-schema" + }); - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field 'nested1' has been added twice.", - "Fields[0].Nested")); - } + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field 'nested1' has been added twice.", + "Fields[0].Nested")); + } - [Fact] - public void CanCreate_should_throw_exception_if_ui_field_is_invalid() + [Fact] + public void CanCreate_should_throw_exception_if_ui_field_is_invalid() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "field1", - Properties = new UIFieldProperties(), - IsHidden = true, - IsDisabled = true, - Partitioning = Partitioning.Invariant.Key - } - }, - FieldsInLists = FieldNames.Create("field1"), - FieldsInReferences = FieldNames.Create("field1"), - Name = "new-schema" - }); - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("UI field cannot be hidden.", - "Fields[0].IsHidden"), - new ValidationError("UI field cannot be disabled.", - "Fields[0].IsDisabled"), - new ValidationError("Field cannot be an UI field.", - "FieldsInLists[0]"), - new ValidationError("Field cannot be an UI field.", - "FieldsInReferences[0]")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_invalid_lists_field_are_used() + Name = "field1", + Properties = new UIFieldProperties(), + IsHidden = true, + IsDisabled = true, + Partitioning = Partitioning.Invariant.Key + } + }, + FieldsInLists = FieldNames.Create("field1"), + FieldsInReferences = FieldNames.Create("field1"), + Name = "new-schema" + }); + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("UI field cannot be hidden.", + "Fields[0].IsHidden"), + new ValidationError("UI field cannot be disabled.", + "Fields[0].IsDisabled"), + new ValidationError("Field cannot be an UI field.", + "FieldsInLists[0]"), + new ValidationError("Field cannot be an UI field.", + "FieldsInReferences[0]")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_invalid_lists_field_are_used() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field4", - Properties = new UIFieldProperties(), - Partitioning = Partitioning.Invariant.Key - } + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key }, - FieldsInLists = FieldNames.Create(null!, null!, "field3", "field1", "field1", "field4"), - FieldsInReferences = null, - Name = "new-schema" - }); - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field is required.", - "FieldsInLists[0]"), - new ValidationError("Field is required.", - "FieldsInLists[1]"), - new ValidationError("Field is not part of the schema.", - "FieldsInLists[2]"), - new ValidationError("Field cannot be an UI field.", - "FieldsInLists[5]"), - new ValidationError("Field 'field1' has been added twice.", - "FieldsInLists")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_invalid_references_field_are_used() + new UpsertSchemaField + { + Name = "field4", + Properties = new UIFieldProperties(), + Partitioning = Partitioning.Invariant.Key + } + }, + FieldsInLists = FieldNames.Create(null!, null!, "field3", "field1", "field1", "field4"), + FieldsInReferences = null, + Name = "new-schema" + }); + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field is required.", + "FieldsInLists[0]"), + new ValidationError("Field is required.", + "FieldsInLists[1]"), + new ValidationError("Field is not part of the schema.", + "FieldsInLists[2]"), + new ValidationError("Field cannot be an UI field.", + "FieldsInLists[5]"), + new ValidationError("Field 'field1' has been added twice.", + "FieldsInLists")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_invalid_references_field_are_used() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field4", - Properties = new UIFieldProperties(), - Partitioning = Partitioning.Invariant.Key - } + Name = "field1", + Properties = new StringFieldProperties(), + Partitioning = Partitioning.Invariant.Key }, - FieldsInLists = null, - FieldsInReferences = FieldNames.Create(null!, null!, "field3", "field1", "field1", "field4"), - Name = "new-schema" - }); - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field is required.", - "FieldsInReferences[0]"), - new ValidationError("Field is required.", - "FieldsInReferences[1]"), - new ValidationError("Field is not part of the schema.", - "FieldsInReferences[2]"), - new ValidationError("Field cannot be an UI field.", - "FieldsInReferences[5]"), - new ValidationError("Field 'field1' has been added twice.", - "FieldsInReferences")); - } - - [Fact] - public void CanCreate_should_throw_exception_if_references_contains_meta_field() + new UpsertSchemaField + { + Name = "field4", + Properties = new UIFieldProperties(), + Partitioning = Partitioning.Invariant.Key + } + }, + FieldsInLists = null, + FieldsInReferences = FieldNames.Create(null!, null!, "field3", "field1", "field1", "field4"), + Name = "new-schema" + }); + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field is required.", + "FieldsInReferences[0]"), + new ValidationError("Field is required.", + "FieldsInReferences[1]"), + new ValidationError("Field is not part of the schema.", + "FieldsInReferences[2]"), + new ValidationError("Field cannot be an UI field.", + "FieldsInReferences[5]"), + new ValidationError("Field 'field1' has been added twice.", + "FieldsInReferences")); + } + + [Fact] + public void CanCreate_should_throw_exception_if_references_contains_meta_field() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema - { - FieldsInLists = null, - FieldsInReferences = FieldNames.Create("meta.id"), - Name = "new-schema" - }); - - ValidationAssert.Throws(() => GuardSchema.CanCreate(command), - new ValidationError("Field is not part of the schema.", - "FieldsInReferences[0]")); - } - - [Fact] - public void CanCreate_should_not_throw_exception_if_command_is_valid() + FieldsInLists = null, + FieldsInReferences = FieldNames.Create("meta.id"), + Name = "new-schema" + }); + + ValidationAssert.Throws(() => GuardSchema.CanCreate(command), + new ValidationError("Field is not part of the schema.", + "FieldsInReferences[0]")); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_command_is_valid() + { + var command = CreateCommand(new CreateSchema { - var command = CreateCommand(new CreateSchema + Fields = new[] { - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField - { - Name = "field1", - Properties = new StringFieldProperties(), - IsHidden = true, - IsDisabled = true, - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField - { - Name = "field2", - Properties = ValidProperties(), - Partitioning = Partitioning.Invariant.Key - }, - new UpsertSchemaField + Name = "field1", + Properties = new StringFieldProperties(), + IsHidden = true, + IsDisabled = true, + Partitioning = Partitioning.Invariant.Key + }, + new UpsertSchemaField + { + Name = "field2", + Properties = ValidProperties(), + Partitioning = Partitioning.Invariant.Key + }, + new UpsertSchemaField + { + Name = "field3", + Properties = new ArrayFieldProperties(), + Partitioning = Partitioning.Invariant.Key, + Nested = new[] { - Name = "field3", - Properties = new ArrayFieldProperties(), - Partitioning = Partitioning.Invariant.Key, - Nested = new[] + new UpsertSchemaNestedField + { + Name = "nested1", + Properties = ValidProperties() + }, + new UpsertSchemaNestedField { - new UpsertSchemaNestedField - { - Name = "nested1", - Properties = ValidProperties() - }, - new UpsertSchemaNestedField - { - Name = "nested2", - Properties = ValidProperties() - } + Name = "nested2", + Properties = ValidProperties() } } - }, - FieldsInLists = FieldNames.Create("field1", "meta.id"), - FieldsInReferences = FieldNames.Create("field1"), - Name = "new-schema" - }); + } + }, + FieldsInLists = FieldNames.Create("field1", "meta.id"), + FieldsInReferences = FieldNames.Create("field1"), + Name = "new-schema" + }); - GuardSchema.CanCreate(command); - } + GuardSchema.CanCreate(command); + } - [Fact] - public void CanConfigureUIFields_should_throw_exception_if_invalid_lists_field_are_used() - { - var command = new ConfigureUIFields - { - FieldsInLists = FieldNames.Create(null!, null!, "field3", "field1", "field1", "field4"), - FieldsInReferences = null - }; - - ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), - new ValidationError("Field is required.", - "FieldsInLists[0]"), - new ValidationError("Field is required.", - "FieldsInLists[1]"), - new ValidationError("Field is not part of the schema.", - "FieldsInLists[2]"), - new ValidationError("Field cannot be an UI field.", - "FieldsInLists[5]"), - new ValidationError("Field 'field1' has been added twice.", - "FieldsInLists")); - } - - [Fact] - public void CanConfigureUIFields_should_throw_exception_if_invalid_references_field_are_used() - { - var command = new ConfigureUIFields - { - FieldsInLists = null, - FieldsInReferences = FieldNames.Create(null!, null!, "field3", "field1", "field1", "field4") - }; - - ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), - new ValidationError("Field is required.", - "FieldsInReferences[0]"), - new ValidationError("Field is required.", - "FieldsInReferences[1]"), - new ValidationError("Field is not part of the schema.", - "FieldsInReferences[2]"), - new ValidationError("Field cannot be an UI field.", - "FieldsInReferences[5]"), - new ValidationError("Field 'field1' has been added twice.", - "FieldsInReferences")); - } - - [Fact] - public void CanConfigureUIFields_should_throw_exception_if_references_contains_meta_field() - { - var command = new ConfigureUIFields - { - FieldsInLists = null, - FieldsInReferences = FieldNames.Create("meta.id") - }; + [Fact] + public void CanConfigureUIFields_should_throw_exception_if_invalid_lists_field_are_used() + { + var command = new ConfigureUIFields + { + FieldsInLists = FieldNames.Create(null!, null!, "field3", "field1", "field1", "field4"), + FieldsInReferences = null + }; + + ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), + new ValidationError("Field is required.", + "FieldsInLists[0]"), + new ValidationError("Field is required.", + "FieldsInLists[1]"), + new ValidationError("Field is not part of the schema.", + "FieldsInLists[2]"), + new ValidationError("Field cannot be an UI field.", + "FieldsInLists[5]"), + new ValidationError("Field 'field1' has been added twice.", + "FieldsInLists")); + } - ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), - new ValidationError("Field is not part of the schema.", - "FieldsInReferences[0]")); - } + [Fact] + public void CanConfigureUIFields_should_throw_exception_if_invalid_references_field_are_used() + { + var command = new ConfigureUIFields + { + FieldsInLists = null, + FieldsInReferences = FieldNames.Create(null!, null!, "field3", "field1", "field1", "field4") + }; + + ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), + new ValidationError("Field is required.", + "FieldsInReferences[0]"), + new ValidationError("Field is required.", + "FieldsInReferences[1]"), + new ValidationError("Field is not part of the schema.", + "FieldsInReferences[2]"), + new ValidationError("Field cannot be an UI field.", + "FieldsInReferences[5]"), + new ValidationError("Field 'field1' has been added twice.", + "FieldsInReferences")); + } - [Fact] - public void CanConfigureUIFields_should_not_throw_exception_if_command_is_valid() + [Fact] + public void CanConfigureUIFields_should_throw_exception_if_references_contains_meta_field() + { + var command = new ConfigureUIFields { - var command = new ConfigureUIFields - { - FieldsInLists = FieldNames.Create("field1", "meta.id"), - FieldsInReferences = FieldNames.Create("field2") - }; + FieldsInLists = null, + FieldsInReferences = FieldNames.Create("meta.id") + }; - GuardSchema.CanConfigureUIFields(command, schema_0); - } + ValidationAssert.Throws(() => GuardSchema.CanConfigureUIFields(command, schema_0), + new ValidationError("Field is not part of the schema.", + "FieldsInReferences[0]")); + } - [Fact] - public void CanConfigureFieldRules_should_throw_exception_if_field_rules_are_invalid() + [Fact] + public void CanConfigureUIFields_should_not_throw_exception_if_command_is_valid() + { + var command = new ConfigureUIFields { - var command = new ConfigureFieldRules - { - FieldRules = new[] - { - new FieldRuleCommand { Field = "field", Action = (FieldRuleAction)5 }, - new FieldRuleCommand() - } - }; + FieldsInLists = FieldNames.Create("field1", "meta.id"), + FieldsInReferences = FieldNames.Create("field2") + }; - ValidationAssert.Throws(() => GuardSchema.CanConfigureFieldRules(command), - new ValidationError("Action is not a valid value.", - "FieldRules[0].Action"), - new ValidationError("Field is required.", - "FieldRules[1].Field")); - } + GuardSchema.CanConfigureUIFields(command, schema_0); + } - [Fact] - public void CanConfigureFieldRules_should_not_throw_exception_if_field_rules_are_valid() + [Fact] + public void CanConfigureFieldRules_should_throw_exception_if_field_rules_are_invalid() + { + var command = new ConfigureFieldRules { - var command = new ConfigureFieldRules + FieldRules = new[] { - FieldRules = new[] - { - new FieldRuleCommand { Field = "field1", Action = FieldRuleAction.Disable, Condition = "a == b" }, - new FieldRuleCommand { Field = "field2" } - } - }; - - GuardSchema.CanConfigureFieldRules(command); - } + new FieldRuleCommand { Field = "field", Action = (FieldRuleAction)5 }, + new FieldRuleCommand() + } + }; + + ValidationAssert.Throws(() => GuardSchema.CanConfigureFieldRules(command), + new ValidationError("Action is not a valid value.", + "FieldRules[0].Action"), + new ValidationError("Field is required.", + "FieldRules[1].Field")); + } - [Fact] - public void CanConfigureFieldRules_should_not_throw_exception_if_field_rules_are_null() + [Fact] + public void CanConfigureFieldRules_should_not_throw_exception_if_field_rules_are_valid() + { + var command = new ConfigureFieldRules { - var command = new ConfigureFieldRules + FieldRules = new[] { - FieldRules = null - }; + new FieldRuleCommand { Field = "field1", Action = FieldRuleAction.Disable, Condition = "a == b" }, + new FieldRuleCommand { Field = "field2" } + } + }; - GuardSchema.CanConfigureFieldRules(command); - } + GuardSchema.CanConfigureFieldRules(command); + } - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() + [Fact] + public void CanConfigureFieldRules_should_not_throw_exception_if_field_rules_are_null() + { + var command = new ConfigureFieldRules { - var command = new ReorderFields { FieldIds = new[] { 1L, 3L } }; + FieldRules = null + }; - ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), - new ValidationError("Field ids do not cover all fields.", "FieldIds")); - } + GuardSchema.CanConfigureFieldRules(command); + } - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() - { - var command = new ReorderFields { FieldIds = new[] { 1L } }; + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() + { + var command = new ReorderFields { FieldIds = new[] { 1L, 3L } }; - ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), - new ValidationError("Field ids do not cover all fields.", "FieldIds")); - } + ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), + new ValidationError("Field ids do not cover all fields.", "FieldIds")); + } - [Fact] - public void CanReorder_should_throw_exception_if_field_ids_null() - { - var command = new ReorderFields { FieldIds = null! }; + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() + { + var command = new ReorderFields { FieldIds = new[] { 1L } }; - ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), - new ValidationError("Field IDs is required.", "FieldIds")); - } + ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), + new ValidationError("Field ids do not cover all fields.", "FieldIds")); + } - [Fact] - public void CanReorder_should_throw_exception_if_parent_field_not_found() - { - var command = new ReorderFields { FieldIds = new[] { 1L, 2L }, ParentFieldId = 99 }; + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_null() + { + var command = new ReorderFields { FieldIds = null! }; - Assert.Throws<DomainObjectNotFoundException>(() => GuardSchema.CanReorder(command, schema_0)); - } + ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), + new ValidationError("Field IDs is required.", "FieldIds")); + } - [Fact] - public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() - { - var command = new ReorderFields { FieldIds = new[] { 1L, 2L, 4L } }; + [Fact] + public void CanReorder_should_throw_exception_if_parent_field_not_found() + { + var command = new ReorderFields { FieldIds = new[] { 1L, 2L }, ParentFieldId = 99 }; - GuardSchema.CanReorder(command, schema_0); - } + Assert.Throws<DomainObjectNotFoundException>(() => GuardSchema.CanReorder(command, schema_0)); + } - [Fact] - public void CanConfigurePreviewUrls_should_throw_exception_if_preview_urls_null() - { - var command = new ConfigurePreviewUrls { PreviewUrls = null! }; + [Fact] + public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() + { + var command = new ReorderFields { FieldIds = new[] { 1L, 2L, 4L } }; - ValidationAssert.Throws(() => GuardSchema.CanConfigurePreviewUrls(command), - new ValidationError("Preview URLs is required.", "PreviewUrls")); - } + GuardSchema.CanReorder(command, schema_0); + } - [Fact] - public void CanConfigurePreviewUrls_should_not_throw_exception_if_valid() - { - var command = new ConfigurePreviewUrls { PreviewUrls = ReadonlyDictionary.Empty<string, string>() }; + [Fact] + public void CanConfigurePreviewUrls_should_throw_exception_if_preview_urls_null() + { + var command = new ConfigurePreviewUrls { PreviewUrls = null! }; - GuardSchema.CanConfigurePreviewUrls(command); - } + ValidationAssert.Throws(() => GuardSchema.CanConfigurePreviewUrls(command), + new ValidationError("Preview URLs is required.", "PreviewUrls")); + } - private CreateSchema CreateCommand(CreateSchema command) - { - command.AppId = appId; + [Fact] + public void CanConfigurePreviewUrls_should_not_throw_exception_if_valid() + { + var command = new ConfigurePreviewUrls { PreviewUrls = ReadonlyDictionary.Empty<string, string>() }; - return command; - } + GuardSchema.CanConfigurePreviewUrls(command); + } - private static StringFieldProperties ValidProperties() - { - return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; - } + private CreateSchema CreateCommand(CreateSchema command) + { + command.AppId = appId; + + return command; + } + + private static StringFieldProperties ValidProperties() + { + return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs index e53353346c..d6e8cc9785 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs @@ -17,777 +17,776 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject; + +public class SchemaDomainObjectTests : HandlerTestBase<SchemaDomainObject.State> { - public class SchemaDomainObjectTests : HandlerTestBase<SchemaDomainObject.State> + private readonly string fieldName = "age"; + private readonly string arrayName = "array"; + private readonly NamedId<long> fieldId = NamedId.Of(1L, "age"); + private readonly NamedId<long> arrayId = NamedId.Of(1L, "array"); + private readonly NamedId<long> nestedId = NamedId.Of(2L, "age"); + private readonly SchemaDomainObject sut; + + protected override DomainId Id { - private readonly string fieldName = "age"; - private readonly string arrayName = "array"; - private readonly NamedId<long> fieldId = NamedId.Of(1L, "age"); - private readonly NamedId<long> arrayId = NamedId.Of(1L, "array"); - private readonly NamedId<long> nestedId = NamedId.Of(2L, "age"); - private readonly SchemaDomainObject sut; - - protected override DomainId Id - { - get => DomainId.Combine(AppId, SchemaId); - } + get => DomainId.Combine(AppId, SchemaId); + } - public SchemaDomainObjectTests() - { - var log = A.Fake<ILogger<SchemaDomainObject>>(); + public SchemaDomainObjectTests() + { + var log = A.Fake<ILogger<SchemaDomainObject>>(); #pragma warning disable MA0056 // Do not call overridable members in constructor - sut = new SchemaDomainObject(Id, PersistenceFactory, log); + sut = new SchemaDomainObject(Id, PersistenceFactory, log); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - [Fact] - public async Task Command_should_throw_exception_if_schema_is_deleted() - { - await ExecuteCreateAsync(); - await ExecuteDeleteAsync(); + [Fact] + public async Task Command_should_throw_exception_if_schema_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); - await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecutePublishAsync); - } + await Assert.ThrowsAsync<DomainObjectDeletedException>(ExecutePublishAsync); + } - [Fact] - public async Task Create_should_create_events_and_set_intitial_state() - { - var properties = new SchemaProperties(); + [Fact] + public async Task Create_should_create_events_and_set_intitial_state() + { + var properties = new SchemaProperties(); - var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Type = SchemaType.Singleton }; + var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Type = SchemaType.Singleton }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(AppId, sut.Snapshot.AppId.Id); + Assert.Equal(AppId, sut.Snapshot.AppId.Id); - Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); - Assert.Equal(SchemaType.Singleton, sut.Snapshot.SchemaDef.Type); + Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); + Assert.Equal(SchemaType.Singleton, sut.Snapshot.SchemaDef.Type); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaCreated { Schema = new Schema(command.Name, command.Properties, SchemaType.Singleton) }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaCreated { Schema = new Schema(command.Name, command.Properties, SchemaType.Singleton) }) + ); + } - [Fact] - public async Task Create_should_create_events_and_schema_with_initial_fields() - { - var properties = new SchemaProperties(); + [Fact] + public async Task Create_should_create_events_and_schema_with_initial_fields() + { + var properties = new SchemaProperties(); - var fields = new[] + var fields = new[] + { + new UpsertSchemaField { Name = "field1", Properties = ValidProperties() }, + new UpsertSchemaField { Name = "field2", Properties = ValidProperties() }, + new UpsertSchemaField { - new UpsertSchemaField { Name = "field1", Properties = ValidProperties() }, - new UpsertSchemaField { Name = "field2", Properties = ValidProperties() }, - new UpsertSchemaField + Name = "field3", + Partitioning = Partitioning.Language.Key, + Properties = new ArrayFieldProperties(), + Nested = new[] { - Name = "field3", - Partitioning = Partitioning.Language.Key, - Properties = new ArrayFieldProperties(), - Nested = new[] - { - new UpsertSchemaNestedField { Name = "nested1", Properties = ValidProperties() }, - new UpsertSchemaNestedField { Name = "nested2", Properties = ValidProperties() } - } + new UpsertSchemaNestedField { Name = "nested1", Properties = ValidProperties() }, + new UpsertSchemaNestedField { Name = "nested2", Properties = ValidProperties() } } - }; + } + }; - var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Fields = fields }; + var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Fields = fields }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - var @event = (SchemaCreated)LastEvents.Single().Payload; + var @event = (SchemaCreated)LastEvents.Single().Payload; - Assert.Equal(AppId, sut.Snapshot.AppId.Id); - Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); - Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); + Assert.Equal(AppId, sut.Snapshot.AppId.Id); + Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); + Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); - Assert.Equal(3, @event.Schema.Fields.Count); - } + Assert.Equal(3, @event.Schema.Fields.Count); + } - [Fact] - public async Task Update_should_create_events_and_update_schema_properties() - { - var command = new UpdateSchema { Properties = new SchemaProperties { Label = "My Properties" } }; + [Fact] + public async Task Update_should_create_events_and_update_schema_properties() + { + var command = new UpdateSchema { Properties = new SchemaProperties { Label = "My Properties" } }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties); + Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaUpdated { Properties = command.Properties }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaUpdated { Properties = command.Properties }) + ); + } - [Fact] - public async Task ConfigureScripts_should_create_events_and_update_schema_scripts() + [Fact] + public async Task ConfigureScripts_should_create_events_and_update_schema_scripts() + { + var command = new ConfigureScripts { - var command = new ConfigureScripts + Scripts = new SchemaScripts { - Scripts = new SchemaScripts - { - Query = "<query-script>" - } - }; + Query = "<query-script>" + } + }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal("<query-script>", sut.Snapshot.SchemaDef.Scripts.Query); + Assert.Equal("<query-script>", sut.Snapshot.SchemaDef.Scripts.Query); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaScriptsConfigured { Scripts = command.Scripts }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaScriptsConfigured { Scripts = command.Scripts }) + ); + } - [Fact] - public async Task ConfigureFieldRules_should_create_events_and_update_schema_field_rules() + [Fact] + public async Task ConfigureFieldRules_should_create_events_and_update_schema_field_rules() + { + var command = new ConfigureFieldRules { - var command = new ConfigureFieldRules + FieldRules = new[] { - FieldRules = new[] - { - new FieldRuleCommand { Field = "field1" } - } - }; + new FieldRuleCommand { Field = "field1" } + } + }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.NotEmpty(sut.Snapshot.SchemaDef.FieldRules); + Assert.NotEmpty(sut.Snapshot.SchemaDef.FieldRules); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaFieldRulesConfigured { FieldRules = FieldRules.Create(FieldRule.Disable("field1")) }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaFieldRulesConfigured { FieldRules = FieldRules.Create(FieldRule.Disable("field1")) }) + ); + } - [Fact] - public async Task ConfigureUIFields_should_create_events_for_list_fields_and_update_schema() + [Fact] + public async Task ConfigureUIFields_should_create_events_for_list_fields_and_update_schema() + { + var command = new ConfigureUIFields { - var command = new ConfigureUIFields - { - FieldsInLists = FieldNames.Create(fieldName) - }; + FieldsInLists = FieldNames.Create(fieldName) + }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.FieldsInLists, sut.Snapshot.SchemaDef.FieldsInLists); + Assert.Equal(command.FieldsInLists, sut.Snapshot.SchemaDef.FieldsInLists); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaUIFieldsConfigured { FieldsInLists = command.FieldsInLists }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaUIFieldsConfigured { FieldsInLists = command.FieldsInLists }) + ); + } - [Fact] - public async Task ConfigureUIFields_should_create_events_for_reference_fields_and_update_schema() + [Fact] + public async Task ConfigureUIFields_should_create_events_for_reference_fields_and_update_schema() + { + var command = new ConfigureUIFields { - var command = new ConfigureUIFields - { - FieldsInReferences = FieldNames.Create(fieldName) - }; + FieldsInReferences = FieldNames.Create(fieldName) + }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.FieldsInReferences, sut.Snapshot.SchemaDef.FieldsInReferences); + Assert.Equal(command.FieldsInReferences, sut.Snapshot.SchemaDef.FieldsInReferences); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaUIFieldsConfigured { FieldsInReferences = command.FieldsInReferences }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaUIFieldsConfigured { FieldsInReferences = command.FieldsInReferences }) + ); + } - [Fact] - public async Task Publish_should_create_events_and_update_published_flag() - { - var command = new PublishSchema(); + [Fact] + public async Task Publish_should_create_events_and_update_published_flag() + { + var command = new PublishSchema(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(sut.Snapshot.SchemaDef.IsPublished); + Assert.True(sut.Snapshot.SchemaDef.IsPublished); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaPublished()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaPublished()) + ); + } - [Fact] - public async Task Unpublish_should_create_events_and_update_published_flag() - { - var command = new UnpublishSchema(); + [Fact] + public async Task Unpublish_should_create_events_and_update_published_flag() + { + var command = new UnpublishSchema(); - await ExecuteCreateAsync(); - await ExecutePublishAsync(); + await ExecuteCreateAsync(); + await ExecutePublishAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(sut.Snapshot.SchemaDef.IsPublished); + Assert.False(sut.Snapshot.SchemaDef.IsPublished); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaUnpublished()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaUnpublished()) + ); + } - [Fact] - public async Task ChangeCategory_should_create_events_and_update_category() - { - var command = new ChangeCategory { Name = "my-category" }; + [Fact] + public async Task ChangeCategory_should_create_events_and_update_category() + { + var command = new ChangeCategory { Name = "my-category" }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.Name, sut.Snapshot.SchemaDef.Category); + Assert.Equal(command.Name, sut.Snapshot.SchemaDef.Category); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaCategoryChanged { Name = command.Name }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaCategoryChanged { Name = command.Name }) + ); + } - [Fact] - public async Task ConfigurePreviewUrls_should_create_events_and_update_preview_urls() + [Fact] + public async Task ConfigurePreviewUrls_should_create_events_and_update_preview_urls() + { + var command = new ConfigurePreviewUrls { - var command = new ConfigurePreviewUrls + PreviewUrls = new Dictionary<string, string> { - PreviewUrls = new Dictionary<string, string> - { - ["Web"] = "web-url" - }.ToReadonlyDictionary() - }; + ["Web"] = "web-url" + }.ToReadonlyDictionary() + }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.PreviewUrls, sut.Snapshot.SchemaDef.PreviewUrls); + Assert.Equal(command.PreviewUrls, sut.Snapshot.SchemaDef.PreviewUrls); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaPreviewUrlsConfigured { PreviewUrls = command.PreviewUrls }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaPreviewUrlsConfigured { PreviewUrls = command.PreviewUrls }) + ); + } - [Fact] - public async Task Delete_should_create_events_and_update_deleted_flag() - { - var command = new DeleteSchema(); + [Fact] + public async Task Delete_should_create_events_and_update_deleted_flag() + { + var command = new DeleteSchema(); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(None.Value); + actual.ShouldBeEquivalent(None.Value); - Assert.True(sut.Snapshot.IsDeleted); + Assert.True(sut.Snapshot.IsDeleted); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaDeleted()) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaDeleted()) + ); + } - [Fact] - public async Task Reorder_should_create_events_and_reorder_fields() - { - var command = new ReorderFields { FieldIds = new[] { 2L, 1L } }; + [Fact] + public async Task Reorder_should_create_events_and_reorder_fields() + { + var command = new ReorderFields { FieldIds = new[] { 2L, 1L } }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync("field1"); - await ExecuteAddFieldAsync("field2"); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync("field1"); + await ExecuteAddFieldAsync("field2"); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaFieldsReordered { FieldIds = command.FieldIds }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaFieldsReordered { FieldIds = command.FieldIds }) + ); + } - [Fact] - public async Task Reorder_should_create_events_and_reorder_nestedy_fields() - { - var command = new ReorderFields { ParentFieldId = 1, FieldIds = new[] { 3L, 2L } }; + [Fact] + public async Task Reorder_should_create_events_and_reorder_nestedy_fields() + { + var command = new ReorderFields { ParentFieldId = 1, FieldIds = new[] { 3L, 2L } }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); - await ExecuteAddFieldAsync("field1", 1); - await ExecuteAddFieldAsync("field2", 1); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); + await ExecuteAddFieldAsync("field1", 1); + await ExecuteAddFieldAsync("field2", 1); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaFieldsReordered { ParentFieldId = arrayId, FieldIds = command.FieldIds }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaFieldsReordered { ParentFieldId = arrayId, FieldIds = command.FieldIds }) + ); + } - [Fact] - public async Task Add_should_create_events_and_add_field() - { - var command = new AddField { Name = fieldName, Properties = ValidProperties() }; + [Fact] + public async Task Add_should_create_events_and_add_field() + { + var command = new AddField { Name = fieldName, Properties = ValidProperties() }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.Properties, GetField(1).RawProperties); + Assert.Equal(command.Properties, GetField(1).RawProperties); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldAdded { Name = fieldName, FieldId = fieldId, Properties = command.Properties }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldAdded { Name = fieldName, FieldId = fieldId, Properties = command.Properties }) + ); + } - [Fact] - public async Task Add_should_create_events_and_add_field_to_array() - { - var command = new AddField { ParentFieldId = 1, Name = fieldName, Properties = ValidProperties() }; + [Fact] + public async Task Add_should_create_events_and_add_field_to_array() + { + var command = new AddField { ParentFieldId = 1, Name = fieldName, Properties = ValidProperties() }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); + Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldAdded { ParentFieldId = arrayId, Name = fieldName, FieldId = nestedId, Properties = command.Properties }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldAdded { ParentFieldId = arrayId, Name = fieldName, FieldId = nestedId, Properties = command.Properties }) + ); + } - [Fact] - public async Task UpdateField_should_create_events_and_update_field_properties() - { - var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; + [Fact] + public async Task UpdateField_should_create_events_and_update_field_properties() + { + var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.Properties, GetField(1).RawProperties); + Assert.Equal(command.Properties, GetField(1).RawProperties); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldUpdated { FieldId = fieldId, Properties = command.Properties }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldUpdated { FieldId = fieldId, Properties = command.Properties }) + ); + } - [Fact] - public async Task UpdateField_should_create_events_and_update_nested_field_properties() - { - var command = new UpdateField { ParentFieldId = 1, FieldId = 2, Properties = new StringFieldProperties() }; + [Fact] + public async Task UpdateField_should_create_events_and_update_nested_field_properties() + { + var command = new UpdateField { ParentFieldId = 1, FieldId = 2, Properties = new StringFieldProperties() }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); - await ExecuteAddFieldAsync(fieldName, 1); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); + await ExecuteAddFieldAsync(fieldName, 1); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); + Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldUpdated { ParentFieldId = arrayId, FieldId = nestedId, Properties = command.Properties }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldUpdated { ParentFieldId = arrayId, FieldId = nestedId, Properties = command.Properties }) + ); + } - [Fact] - public async Task LockField_should_create_events_and_update_field_locked_flag() - { - var command = new LockField { FieldId = 1 }; + [Fact] + public async Task LockField_should_create_events_and_update_field_locked_flag() + { + var command = new LockField { FieldId = 1 }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(GetField(1).IsLocked); + Assert.True(GetField(1).IsLocked); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldLocked { FieldId = fieldId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldLocked { FieldId = fieldId }) + ); + } - [Fact] - public async Task LockField_should_create_events_and_update_nested_field_locked_flag() - { - var command = new LockField { ParentFieldId = 1, FieldId = 2 }; + [Fact] + public async Task LockField_should_create_events_and_update_nested_field_locked_flag() + { + var command = new LockField { ParentFieldId = 1, FieldId = 2 }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); - await ExecuteAddFieldAsync(fieldName, 1); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); + await ExecuteAddFieldAsync(fieldName, 1); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(GetNestedField(1, 2).IsLocked); + Assert.True(GetNestedField(1, 2).IsLocked); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldLocked { ParentFieldId = arrayId, FieldId = nestedId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldLocked { ParentFieldId = arrayId, FieldId = nestedId }) + ); + } - [Fact] - public async Task HideField_should_create_events_and_update_field_hidden_flag() - { - var command = new HideField { FieldId = 1 }; + [Fact] + public async Task HideField_should_create_events_and_update_field_hidden_flag() + { + var command = new HideField { FieldId = 1 }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(GetField(1).IsHidden); + Assert.True(GetField(1).IsHidden); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldHidden { FieldId = fieldId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldHidden { FieldId = fieldId }) + ); + } - [Fact] - public async Task HideField_should_create_events_and_update_nested_field_hidden_flag() - { - var command = new HideField { ParentFieldId = 1, FieldId = 2 }; + [Fact] + public async Task HideField_should_create_events_and_update_nested_field_hidden_flag() + { + var command = new HideField { ParentFieldId = 1, FieldId = 2 }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); - await ExecuteAddFieldAsync(fieldName, 1); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); + await ExecuteAddFieldAsync(fieldName, 1); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(GetNestedField(1, 2).IsHidden); + Assert.True(GetNestedField(1, 2).IsHidden); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldHidden { ParentFieldId = arrayId, FieldId = nestedId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldHidden { ParentFieldId = arrayId, FieldId = nestedId }) + ); + } - [Fact] - public async Task ShowField_should_create_events_and_update_field_hidden_flag() - { - var command = new ShowField { FieldId = 1 }; + [Fact] + public async Task ShowField_should_create_events_and_update_field_hidden_flag() + { + var command = new ShowField { FieldId = 1 }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); - await ExecuteHideFieldAsync(1); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + await ExecuteHideFieldAsync(1); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(GetField(1).IsHidden); + Assert.False(GetField(1).IsHidden); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldShown { FieldId = fieldId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldShown { FieldId = fieldId }) + ); + } - [Fact] - public async Task ShowField_should_create_events_and_update_nested_field_hidden_flag() - { - var command = new ShowField { ParentFieldId = 1, FieldId = 2 }; + [Fact] + public async Task ShowField_should_create_events_and_update_nested_field_hidden_flag() + { + var command = new ShowField { ParentFieldId = 1, FieldId = 2 }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); - await ExecuteAddFieldAsync(fieldName, 1); - await ExecuteHideFieldAsync(2, 1); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); + await ExecuteAddFieldAsync(fieldName, 1); + await ExecuteHideFieldAsync(2, 1); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(GetNestedField(1, 2).IsHidden); + Assert.False(GetNestedField(1, 2).IsHidden); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldShown { ParentFieldId = arrayId, FieldId = nestedId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldShown { ParentFieldId = arrayId, FieldId = nestedId }) + ); + } - [Fact] - public async Task DisableField_should_create_events_and_update_field_disabled_flag() - { - var command = new DisableField { FieldId = 1 }; + [Fact] + public async Task DisableField_should_create_events_and_update_field_disabled_flag() + { + var command = new DisableField { FieldId = 1 }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(GetField(1).IsDisabled); + Assert.True(GetField(1).IsDisabled); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldDisabled { FieldId = fieldId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldDisabled { FieldId = fieldId }) + ); + } - [Fact] - public async Task DisableField_should_create_events_and_update_nested_field_disabled_flag() - { - var command = new DisableField { ParentFieldId = 1, FieldId = 2 }; + [Fact] + public async Task DisableField_should_create_events_and_update_nested_field_disabled_flag() + { + var command = new DisableField { ParentFieldId = 1, FieldId = 2 }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); - await ExecuteAddFieldAsync(fieldName, 1); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); + await ExecuteAddFieldAsync(fieldName, 1); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.True(GetNestedField(1, 2).IsDisabled); + Assert.True(GetNestedField(1, 2).IsDisabled); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldDisabled { ParentFieldId = arrayId, FieldId = nestedId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldDisabled { ParentFieldId = arrayId, FieldId = nestedId }) + ); + } - [Fact] - public async Task EnableField_should_create_events_and_update_field_disabled_flag() - { - var command = new EnableField { FieldId = 1 }; + [Fact] + public async Task EnableField_should_create_events_and_update_field_disabled_flag() + { + var command = new EnableField { FieldId = 1 }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); - await ExecuteDisableFieldAsync(1); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + await ExecuteDisableFieldAsync(1); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(GetField(1).IsDisabled); + Assert.False(GetField(1).IsDisabled); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldEnabled { FieldId = fieldId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldEnabled { FieldId = fieldId }) + ); + } - [Fact] - public async Task EnableField_should_create_events_and_update_nested_field_disabled_flag() - { - var command = new EnableField { ParentFieldId = 1, FieldId = 2 }; + [Fact] + public async Task EnableField_should_create_events_and_update_nested_field_disabled_flag() + { + var command = new EnableField { ParentFieldId = 1, FieldId = 2 }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); - await ExecuteAddFieldAsync(fieldName, 1); - await ExecuteDisableFieldAsync(2, 1); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); + await ExecuteAddFieldAsync(fieldName, 1); + await ExecuteDisableFieldAsync(2, 1); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(GetNestedField(1, 2).IsDisabled); + Assert.False(GetNestedField(1, 2).IsDisabled); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldEnabled { ParentFieldId = arrayId, FieldId = nestedId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldEnabled { ParentFieldId = arrayId, FieldId = nestedId }) + ); + } - [Fact] - public async Task DeleteField_should_create_events_and_delete_field() - { - var command = new DeleteField { FieldId = 1 }; + [Fact] + public async Task DeleteField_should_create_events_and_delete_field() + { + var command = new DeleteField { FieldId = 1 }; - await ExecuteCreateAsync(); - await ExecuteAddFieldAsync(fieldName); + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(GetField(1)); + Assert.Null(GetField(1)); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldDeleted { FieldId = fieldId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldDeleted { FieldId = fieldId }) + ); + } - [Fact] - public async Task DeleteField_should_create_events_and_delete_nested_field() - { - var command = new DeleteField { ParentFieldId = 1, FieldId = 2 }; + [Fact] + public async Task DeleteField_should_create_events_and_delete_nested_field() + { + var command = new DeleteField { ParentFieldId = 1, FieldId = 2 }; - await ExecuteCreateAsync(); - await ExecuteAddArrayFieldAsync(); - await ExecuteAddFieldAsync(fieldName, 1); + await ExecuteCreateAsync(); + await ExecuteAddArrayFieldAsync(); + await ExecuteAddFieldAsync(fieldName, 1); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Null(GetNestedField(1, 2)); + Assert.Null(GetNestedField(1, 2)); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new FieldDeleted { ParentFieldId = arrayId, FieldId = nestedId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldDeleted { ParentFieldId = arrayId, FieldId = nestedId }) + ); + } - [Fact] - public async Task Synchronize_should_create_events_and_update_schema() + [Fact] + public async Task Synchronize_should_create_events_and_update_schema() + { + var command = new SynchronizeSchema { - var command = new SynchronizeSchema - { - Category = "My-Category" - }; + Category = "My-Category" + }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.Category, sut.Snapshot.SchemaDef.Category); + Assert.Equal(command.Category, sut.Snapshot.SchemaDef.Category); - LastEvents - .ShouldHaveSameEvents( - CreateEvent(new SchemaCategoryChanged { Name = command.Category }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaCategoryChanged { Name = command.Category }) + ); + } - private Task ExecuteCreateAsync() - { - return PublishAsync(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - } + private Task ExecuteCreateAsync() + { + return PublishAsync(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); + } - private Task ExecuteAddArrayFieldAsync() - { - return PublishAsync(new AddField { Properties = new ArrayFieldProperties(), Name = arrayName }); - } + private Task ExecuteAddArrayFieldAsync() + { + return PublishAsync(new AddField { Properties = new ArrayFieldProperties(), Name = arrayName }); + } - private Task ExecuteAddFieldAsync(string name, long? parentId = null) - { - return PublishAsync(new AddField { ParentFieldId = parentId, Properties = ValidProperties(), Name = name }); - } + private Task ExecuteAddFieldAsync(string name, long? parentId = null) + { + return PublishAsync(new AddField { ParentFieldId = parentId, Properties = ValidProperties(), Name = name }); + } - private Task ExecuteHideFieldAsync(long id, long? parentId = null) - { - return PublishAsync(new HideField { ParentFieldId = parentId, FieldId = id }); - } + private Task ExecuteHideFieldAsync(long id, long? parentId = null) + { + return PublishAsync(new HideField { ParentFieldId = parentId, FieldId = id }); + } - private Task ExecuteDisableFieldAsync(long id, long? parentId = null) - { - return PublishAsync(new DisableField { ParentFieldId = parentId, FieldId = id }); - } + private Task ExecuteDisableFieldAsync(long id, long? parentId = null) + { + return PublishAsync(new DisableField { ParentFieldId = parentId, FieldId = id }); + } - private Task ExecutePublishAsync() - { - return PublishAsync(new PublishSchema()); - } + private Task ExecutePublishAsync() + { + return PublishAsync(new PublishSchema()); + } - private Task ExecuteDeleteAsync() - { - return PublishAsync(new DeleteSchema()); - } + private Task ExecuteDeleteAsync() + { + return PublishAsync(new DeleteSchema()); + } - private IField GetField(int id) - { - return sut.Snapshot.SchemaDef.FieldsById.GetValueOrDefault(id)!; - } + private IField GetField(int id) + { + return sut.Snapshot.SchemaDef.FieldsById.GetValueOrDefault(id)!; + } - private IField GetNestedField(int parentId, int childId) - { - return ((IArrayField)sut.Snapshot.SchemaDef.FieldsById[parentId]).FieldsById.GetValueOrDefault(childId)!; - } + private IField GetNestedField(int parentId, int childId) + { + return ((IArrayField)sut.Snapshot.SchemaDef.FieldsById[parentId]).FieldsById.GetValueOrDefault(childId)!; + } - private static StringFieldProperties ValidProperties() - { - return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; - } + private static StringFieldProperties ValidProperties() + { + return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; + } - private async Task<object?> PublishIdempotentAsync<T>(T command) where T : SquidexCommand, IAggregateCommand - { - var actual = await PublishAsync(command); + private async Task<object?> PublishIdempotentAsync<T>(T command) where T : SquidexCommand, IAggregateCommand + { + var actual = await PublishAsync(command); - var previousSnapshot = sut.Snapshot; - var previousVersion = sut.Snapshot.Version; + var previousSnapshot = sut.Snapshot; + var previousVersion = sut.Snapshot.Version; - await PublishAsync(command); + await PublishAsync(command); - Assert.Same(previousSnapshot, sut.Snapshot); - Assert.Equal(previousVersion, sut.Snapshot.Version); + Assert.Same(previousSnapshot, sut.Snapshot); + Assert.Equal(previousVersion, sut.Snapshot.Version); - return actual; - } + return actual; + } - private async Task<object> PublishAsync<T>(T command) where T : SquidexCommand, IAggregateCommand - { - var actual = await sut.ExecuteAsync(CreateCommand(command), default); + private async Task<object> PublishAsync<T>(T command) where T : SquidexCommand, IAggregateCommand + { + var actual = await sut.ExecuteAsync(CreateCommand(command), default); - return actual.Payload; - } + return actual.Payload; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaStateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaStateTests.cs index 3958cd77ea..722caeeb59 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaStateTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaStateTests.cs @@ -10,33 +10,32 @@ using Squidex.Infrastructure.Json; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject; + +public class SchemaStateTests { - public class SchemaStateTests + private readonly IJsonSerializer serializer = TestUtils.CreateSerializer(options => { - private readonly IJsonSerializer serializer = TestUtils.CreateSerializer(options => - { - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - }); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + }); - [Fact] - public void Should_deserialize_state() - { - var json = File.ReadAllText("Schemas/DomainObject/SchemaState.json"); + [Fact] + public void Should_deserialize_state() + { + var json = File.ReadAllText("Schemas/DomainObject/SchemaState.json"); - var deserialized = serializer.Deserialize<SchemaDomainObject.State>(json); + var deserialized = serializer.Deserialize<SchemaDomainObject.State>(json); - Assert.NotNull(deserialized); - } + Assert.NotNull(deserialized); + } - [Fact] - public void Should_serialize_deserialize_state() - { - var json = File.ReadAllText("Schemas/DomainObject/SchemaState.json").CleanJson(); + [Fact] + public void Should_serialize_deserialize_state() + { + var json = File.ReadAllText("Schemas/DomainObject/SchemaState.json").CleanJson(); - var serialized = serializer.Serialize(serializer.Deserialize<SchemaDomainObject.State>(json), true); + var serialized = serializer.Serialize(serializer.Deserialize<SchemaDomainObject.State>(json), true); - Assert.Equal(json, serialized); - } + Assert.Equal(json, serialized); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs index cb7ffe859f..a21790a05a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs @@ -20,269 +20,268 @@ using Squidex.Messaging; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes; + +public class SchemasIndexTests { - public class SchemasIndexTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState<NameReservationState.State> state; + private readonly ISchemaRepository schemaRepository = A.Fake<ISchemaRepository>(); + private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly SchemasIndex sut; + + public SchemasIndexTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly TestState<NameReservationState.State> state; - private readonly ISchemaRepository schemaRepository = A.Fake<ISchemaRepository>(); - private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly SchemasIndex sut; - - public SchemasIndexTests() - { - state = new TestState<NameReservationState.State>($"{appId.Id}_Schemas"); + state = new TestState<NameReservationState.State>($"{appId.Id}_Schemas"); - ct = cts.Token; + ct = cts.Token; - var replicatedCache = - new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake<IMessageBus>(), - Options.Create(new ReplicatedCacheOptions { Enable = true })); + var replicatedCache = + new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake<IMessageBus>(), + Options.Create(new ReplicatedCacheOptions { Enable = true })); - sut = new SchemasIndex(schemaRepository, replicatedCache, state.PersistenceFactory); - } + sut = new SchemasIndex(schemaRepository, replicatedCache, state.PersistenceFactory); + } - [Fact] - public async Task Should_resolve_schema_by_name() - { - var expected = SetupSchema(); + [Fact] + public async Task Should_resolve_schema_by_name() + { + var expected = SetupSchema(); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) - .Returns(expected); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(expected); - var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false, ct); - var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false, ct); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false, ct); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false, ct); - Assert.Same(expected, actual1); - Assert.Same(expected, actual2); + Assert.Same(expected, actual1); + Assert.Same(expected, actual2); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) - .MustHaveHappenedTwiceExactly(); - } + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .MustHaveHappenedTwiceExactly(); + } - [Fact] - public async Task Should_resolve_schema_by_name_and_id_if_cached_before() - { - var expected = SetupSchema(); + [Fact] + public async Task Should_resolve_schema_by_name_and_id_if_cached_before() + { + var expected = SetupSchema(); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) - .Returns(expected); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(expected); - var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); - var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); - var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); + var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); - Assert.Same(expected, actual1); - Assert.Same(expected, actual2); - Assert.Same(expected, actual3); + Assert.Same(expected, actual1); + Assert.Same(expected, actual2); + Assert.Same(expected, actual3); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_resolve_schema_by_id() - { - var expected = SetupSchema(); + [Fact] + public async Task Should_resolve_schema_by_id() + { + var expected = SetupSchema(); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) - .Returns(expected); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) + .Returns(expected); - var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, false, ct); - var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, false, ct); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, false, ct); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, false, ct); - Assert.Same(expected, actual1); - Assert.Same(expected, actual2); + Assert.Same(expected, actual1); + Assert.Same(expected, actual2); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) - .MustHaveHappenedTwiceExactly(); - } + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) + .MustHaveHappenedTwiceExactly(); + } - [Fact] - public async Task Should_resolve_schema_by_id_and_name_if_cached_before() - { - var expected = SetupSchema(); + [Fact] + public async Task Should_resolve_schema_by_id_and_name_if_cached_before() + { + var expected = SetupSchema(); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) - .Returns(expected); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) + .Returns(expected); - var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); - var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); - var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); + var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); - Assert.Same(expected, actual1); - Assert.Same(expected, actual2); - Assert.Same(expected, actual3); + Assert.Same(expected, actual1); + Assert.Same(expected, actual2); + Assert.Same(expected, actual3); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_resolve_schemas() - { - var expected = SetupSchema(); + [Fact] + public async Task Should_resolve_schemas() + { + var expected = SetupSchema(); - A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) - .Returns(new List<ISchemaEntity> { expected }); + A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List<ISchemaEntity> { expected }); - var actual = await sut.GetSchemasAsync(appId.Id, ct); + var actual = await sut.GetSchemasAsync(appId.Id, ct); - Assert.Same(actual[0], expected); - } + Assert.Same(actual[0], expected); + } - [Fact] - public async Task Should_return_empty_schemas_if_schema_not_created() - { - var expected = SetupSchema(EtagVersion.Empty); + [Fact] + public async Task Should_return_empty_schemas_if_schema_not_created() + { + var expected = SetupSchema(EtagVersion.Empty); - A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) - .Returns(new List<ISchemaEntity> { expected }); + A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List<ISchemaEntity> { expected }); - var actual = await sut.GetSchemasAsync(appId.Id, ct); + var actual = await sut.GetSchemasAsync(appId.Id, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_return_empty_schemas_if_schema_deleted() - { - var expected = SetupSchema(0, true); + [Fact] + public async Task Should_return_empty_schemas_if_schema_deleted() + { + var expected = SetupSchema(0, true); - A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) - .Returns(new List<ISchemaEntity> { expected }); + A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List<ISchemaEntity> { expected }); - var actual = await sut.GetSchemasAsync(appId.Id, ct); + var actual = await sut.GetSchemasAsync(appId.Id, ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_take_and_remove_reservation_if_created() - { - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) - .Returns(Task.FromResult<ISchemaEntity?>(null)); + [Fact] + public async Task Should_take_and_remove_reservation_if_created() + { + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(Task.FromResult<ISchemaEntity?>(null)); - var command = Create(schemaId.Name); + var command = Create(schemaId.Name); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - NameReservation? madeReservation = null; + NameReservation? madeReservation = null; - await sut.HandleAsync(context, (c, ct) => - { - madeReservation = state.Snapshot.Reservations.FirstOrDefault(); + await sut.HandleAsync(context, (c, ct) => + { + madeReservation = state.Snapshot.Reservations.FirstOrDefault(); - return Task.CompletedTask; - }, ct); + return Task.CompletedTask; + }, ct); - Assert.Empty(state.Snapshot.Reservations); + Assert.Empty(state.Snapshot.Reservations); - Assert.Equal(schemaId.Id, madeReservation?.Id); - Assert.Equal(schemaId.Name, madeReservation?.Name); - } + Assert.Equal(schemaId.Id, madeReservation?.Id); + Assert.Equal(schemaId.Name, madeReservation?.Name); + } - [Fact] - public async Task Should_clear_reservation_if_schema_creation_failed() - { - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) - .Returns(Task.FromResult<ISchemaEntity?>(null)); + [Fact] + public async Task Should_clear_reservation_if_schema_creation_failed() + { + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(Task.FromResult<ISchemaEntity?>(null)); - var command = Create(schemaId.Name); + var command = Create(schemaId.Name); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - NameReservation? madeReservation = null; + NameReservation? madeReservation = null; - await Assert.ThrowsAnyAsync<Exception>(() => sut.HandleAsync(context, (c, ct) => - { - madeReservation = state.Snapshot.Reservations.FirstOrDefault(); + await Assert.ThrowsAnyAsync<Exception>(() => sut.HandleAsync(context, (c, ct) => + { + madeReservation = state.Snapshot.Reservations.FirstOrDefault(); - throw new InvalidOperationException(); - }, ct)); + throw new InvalidOperationException(); + }, ct)); - Assert.Empty(state.Snapshot.Reservations); + Assert.Empty(state.Snapshot.Reservations); - Assert.Equal(schemaId.Id, madeReservation?.Id); - Assert.Equal(schemaId.Name, madeReservation?.Name); - } + Assert.Equal(schemaId.Id, madeReservation?.Id); + Assert.Equal(schemaId.Name, madeReservation?.Name); + } - [Fact] - public async Task Should_not_create_schema_if_name_is_reserved() - { - state.Snapshot.Reservations.Add(new NameReservation(RandomHash.Simple(), schemaId.Name, DomainId.NewGuid())); + [Fact] + public async Task Should_not_create_schema_if_name_is_reserved() + { + state.Snapshot.Reservations.Add(new NameReservation(RandomHash.Simple(), schemaId.Name, DomainId.NewGuid())); - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) - .Returns(Task.FromResult<ISchemaEntity?>(null)); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(Task.FromResult<ISchemaEntity?>(null)); - var command = Create(schemaId.Name); + var command = Create(schemaId.Name); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context, ct)); - } + await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context, ct)); + } - [Fact] - public async Task Should_not_create_schema_if_name_is_taken() - { - A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) - .Returns(SetupSchema()); + [Fact] + public async Task Should_not_create_schema_if_name_is_taken() + { + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(SetupSchema()); - var command = Create(schemaId.Name); + var command = Create(schemaId.Name); - var context = - new CommandContext(command, commandBus) - .Complete(); + var context = + new CommandContext(command, commandBus) + .Complete(); - await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context, ct)); + await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context, ct)); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<NameReservationState.State>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<NameReservationState.State>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_make_an_update_for_other_command() - { - var schema = SetupSchema(); + [Fact] + public async Task Should_not_make_an_update_for_other_command() + { + var schema = SetupSchema(); - var command = new UpdateSchema { SchemaId = schemaId, AppId = appId }; + var command = new UpdateSchema { SchemaId = schemaId, AppId = appId }; - var context = - new CommandContext(command, commandBus) - .Complete(schema); + var context = + new CommandContext(command, commandBus) + .Complete(schema); - await sut.HandleAsync(context, ct); + await sut.HandleAsync(context, ct); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<NameReservationState.State>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<NameReservationState.State>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - private CreateSchema Create(string name) - { - return new CreateSchema { SchemaId = schemaId.Id, Name = name, AppId = appId }; - } + private CreateSchema Create(string name) + { + return new CreateSchema { SchemaId = schemaId.Id, Name = name, AppId = appId }; + } - private ISchemaEntity SetupSchema(long version = 0, bool isDeleted = false) - { - var schema = A.Fake<ISchemaEntity>(); + private ISchemaEntity SetupSchema(long version = 0, bool isDeleted = false) + { + var schema = A.Fake<ISchemaEntity>(); - A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaId.Name)); - A.CallTo(() => schema.Id).Returns(schemaId.Id); - A.CallTo(() => schema.AppId).Returns(appId); - A.CallTo(() => schema.Version).Returns(version); - A.CallTo(() => schema.IsDeleted).Returns(isDeleted); + A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaId.Name)); + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.AppId).Returns(appId); + A.CallTo(() => schema.Version).Returns(version); + A.CallTo(() => schema.IsDeleted).Returns(isDeleted); - return schema; - } + return schema; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs index 035fcfddbb..b564168977 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs @@ -10,27 +10,26 @@ using Squidex.Domain.Apps.Entities.MongoDb.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; -namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb +namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb; + +public sealed class SchemasHashFixture { - public sealed class SchemasHashFixture - { - public MongoSchemasHash SchemasHash { get; } + public MongoSchemasHash SchemasHash { get; } - public SchemasHashFixture() - { - TestUtils.SetupBson(); + public SchemasHashFixture() + { + TestUtils.SetupBson(); - var mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); - var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); + var mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); + var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); - var schemasHash = new MongoSchemasHash(mongoDatabase); + var schemasHash = new MongoSchemasHash(mongoDatabase); - Task.Run(async () => - { - await schemasHash.InitializeAsync(default); - }).Wait(); + Task.Run(async () => + { + await schemasHash.InitializeAsync(default); + }).Wait(); - SchemasHash = schemasHash; - } + SchemasHash = schemasHash; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs index a61c988cd1..1aa49f70d1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs @@ -15,85 +15,84 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb +namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb; + +[Trait("Category", "Dependencies")] +public class SchemasHashTests : IClassFixture<SchemasHashFixture> { - [Trait("Category", "Dependencies")] - public class SchemasHashTests : IClassFixture<SchemasHashFixture> + public SchemasHashFixture _ { get; } + + public SchemasHashTests(SchemasHashFixture fixture) { - public SchemasHashFixture _ { get; } + _ = fixture; + } - public SchemasHashTests(SchemasHashFixture fixture) - { - _ = fixture; - } + [Fact] + public async Task Should_compute_cache_independent_from_order() + { + var app = CreateApp(DomainId.NewGuid(), 1); - [Fact] - public async Task Should_compute_cache_independent_from_order() - { - var app = CreateApp(DomainId.NewGuid(), 1); + var schema1 = CreateSchema(DomainId.NewGuid(), 2); + var schema2 = CreateSchema(DomainId.NewGuid(), 3); - var schema1 = CreateSchema(DomainId.NewGuid(), 2); - var schema2 = CreateSchema(DomainId.NewGuid(), 3); + var hash1 = await _.SchemasHash.ComputeHashAsync(app, new[] { schema1, schema2 }); + var hash2 = await _.SchemasHash.ComputeHashAsync(app, new[] { schema2, schema1 }); - var hash1 = await _.SchemasHash.ComputeHashAsync(app, new[] { schema1, schema2 }); - var hash2 = await _.SchemasHash.ComputeHashAsync(app, new[] { schema2, schema1 }); + Assert.NotNull(hash1); + Assert.NotNull(hash2); + Assert.Equal(hash1, hash2); + } - Assert.NotNull(hash1); - Assert.NotNull(hash2); - Assert.Equal(hash1, hash2); - } + [Fact] + public async Task Should_compute_cache_independent_from_db() + { + var app = CreateApp(DomainId.NewGuid(), 1); - [Fact] - public async Task Should_compute_cache_independent_from_db() - { - var app = CreateApp(DomainId.NewGuid(), 1); + var schema1 = CreateSchema(DomainId.NewGuid(), 2); + var schema2 = CreateSchema(DomainId.NewGuid(), 3); - var schema1 = CreateSchema(DomainId.NewGuid(), 2); - var schema2 = CreateSchema(DomainId.NewGuid(), 3); + var timestamp = SystemClock.Instance.GetCurrentInstant().WithoutMs(); - var timestamp = SystemClock.Instance.GetCurrentInstant().WithoutMs(); + var computedHash = await _.SchemasHash.ComputeHashAsync(app, new[] { schema1, schema2 }); - var computedHash = await _.SchemasHash.ComputeHashAsync(app, new[] { schema1, schema2 }); + await _.SchemasHash.On(new[] + { + Envelope.Create<IEvent>(new SchemaCreated + { + AppId = NamedId.Of(app.Id, "my-app"), + SchemaId = NamedId.Of(schema1.Id, "my-schema") + }).SetEventStreamNumber(schema1.Version).SetTimestamp(timestamp), - await _.SchemasHash.On(new[] + Envelope.Create<IEvent>(new SchemaCreated { - Envelope.Create<IEvent>(new SchemaCreated - { - AppId = NamedId.Of(app.Id, "my-app"), - SchemaId = NamedId.Of(schema1.Id, "my-schema") - }).SetEventStreamNumber(schema1.Version).SetTimestamp(timestamp), - - Envelope.Create<IEvent>(new SchemaCreated - { - AppId = NamedId.Of(app.Id, "my-app"), - SchemaId = NamedId.Of(schema2.Id, "my-schema") - }).SetEventStreamNumber(schema2.Version).SetTimestamp(timestamp) - }); - - var (dbTime, dbHash) = await _.SchemasHash.GetCurrentHashAsync(app); - - Assert.Equal(dbHash, computedHash); - Assert.Equal(dbTime, timestamp); - } - - private static IAppEntity CreateApp(DomainId id, long version) - { - var app = A.Fake<IAppEntity>(); + AppId = NamedId.Of(app.Id, "my-app"), + SchemaId = NamedId.Of(schema2.Id, "my-schema") + }).SetEventStreamNumber(schema2.Version).SetTimestamp(timestamp) + }); - A.CallTo(() => app.Id).Returns(id); - A.CallTo(() => app.Version).Returns(version); + var (dbTime, dbHash) = await _.SchemasHash.GetCurrentHashAsync(app); - return app; - } + Assert.Equal(dbHash, computedHash); + Assert.Equal(dbTime, timestamp); + } - private static ISchemaEntity CreateSchema(DomainId id, long version) - { - var schema = A.Fake<ISchemaEntity>(); + private static IAppEntity CreateApp(DomainId id, long version) + { + var app = A.Fake<IAppEntity>(); + + A.CallTo(() => app.Id).Returns(id); + A.CallTo(() => app.Version).Returns(version); + + return app; + } + + private static ISchemaEntity CreateSchema(DomainId id, long version) + { + var schema = A.Fake<ISchemaEntity>(); - A.CallTo(() => schema.Id).Returns(id); - A.CallTo(() => schema.Version).Returns(version); + A.CallTo(() => schema.Id).Returns(id); + A.CallTo(() => schema.Version).Returns(version); - return schema; - } + return schema; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs index cae4c6ebc6..74c294fe0f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs @@ -19,154 +19,153 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public class SchemaChangedTriggerHandlerTests { - public class SchemaChangedTriggerHandlerTests + private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); + private readonly IRuleTriggerHandler sut; + + public SchemaChangedTriggerHandlerTests() { - private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); - private readonly IRuleTriggerHandler sut; + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true", default)) + .Returns(true); - public SchemaChangedTriggerHandlerTests() - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "true", default)) - .Returns(true); + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false", default)) + .Returns(false); - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, "false", default)) - .Returns(false); + sut = new SchemaChangedTriggerHandler(scriptEngine); + } - sut = new SchemaChangedTriggerHandler(scriptEngine); - } + public static IEnumerable<object[]> TestEvents() + { + yield return new object[] { TestUtils.CreateEvent<SchemaCreated>(), EnrichedSchemaEventType.Created }; + yield return new object[] { TestUtils.CreateEvent<SchemaUpdated>(), EnrichedSchemaEventType.Updated }; + yield return new object[] { TestUtils.CreateEvent<SchemaDeleted>(), EnrichedSchemaEventType.Deleted }; + yield return new object[] { TestUtils.CreateEvent<SchemaPublished>(), EnrichedSchemaEventType.Published }; + yield return new object[] { TestUtils.CreateEvent<SchemaUnpublished>(), EnrichedSchemaEventType.Unpublished }; + } - public static IEnumerable<object[]> TestEvents() - { - yield return new object[] { TestUtils.CreateEvent<SchemaCreated>(), EnrichedSchemaEventType.Created }; - yield return new object[] { TestUtils.CreateEvent<SchemaUpdated>(), EnrichedSchemaEventType.Updated }; - yield return new object[] { TestUtils.CreateEvent<SchemaDeleted>(), EnrichedSchemaEventType.Deleted }; - yield return new object[] { TestUtils.CreateEvent<SchemaPublished>(), EnrichedSchemaEventType.Published }; - yield return new object[] { TestUtils.CreateEvent<SchemaUnpublished>(), EnrichedSchemaEventType.Unpublished }; - } + [Fact] + public void Should_return_false_if_asking_for_snapshot_support() + { + Assert.False(sut.CanCreateSnapshotEvents); + } - [Fact] - public void Should_return_false_if_asking_for_snapshot_support() - { - Assert.False(sut.CanCreateSnapshotEvents); - } + [Fact] + public void Should_handle_schema_event() + { + Assert.True(sut.Handles(new SchemaCreated())); + } - [Fact] - public void Should_handle_schema_event() - { - Assert.True(sut.Handles(new SchemaCreated())); - } + [Fact] + public void Should_not_handle_other_event() + { + Assert.False(sut.Handles(new AppCreated())); + } - [Fact] - public void Should_not_handle_other_event() - { - Assert.False(sut.Handles(new AppCreated())); - } + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_create_enriched_events(SchemaEvent @event, EnrichedSchemaEventType type) + { + var ctx = Context(appId: @event.AppId); - [Theory] - [MemberData(nameof(TestEvents))] - public async Task Should_create_enriched_events(SchemaEvent @event, EnrichedSchemaEventType type) - { - var ctx = Context(appId: @event.AppId); + var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); - var envelope = Envelope.Create<AppEvent>(@event).SetEventStreamNumber(12); + var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); - var actual = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var enrichedEvent = actual.Single() as EnrichedSchemaEvent; - var enrichedEvent = actual.Single() as EnrichedSchemaEvent; + Assert.Equal(type, enrichedEvent!.Type); + Assert.Equal(@event.Actor, enrichedEvent.Actor); + Assert.Equal(@event.AppId, enrichedEvent.AppId); + Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); + Assert.Equal(@event.SchemaId, enrichedEvent.SchemaId); + Assert.Equal(@event.SchemaId.Id, enrichedEvent.SchemaId.Id); + } - Assert.Equal(type, enrichedEvent!.Type); - Assert.Equal(@event.Actor, enrichedEvent.Actor); - Assert.Equal(@event.AppId, enrichedEvent.AppId); - Assert.Equal(@event.AppId.Id, enrichedEvent.AppId.Id); - Assert.Equal(@event.SchemaId, enrichedEvent.SchemaId); - Assert.Equal(@event.SchemaId.Id, enrichedEvent.SchemaId.Id); - } + [Fact] + public void Should_trigger_precheck_if_event_type_correct() + { + TestForCondition(string.Empty, ctx => + { + var @event = new SchemaCreated(); + + var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_precheck_if_event_type_correct() + [Fact] + public void Should_trigger_check_if_condition_is_empty() + { + TestForCondition(string.Empty, ctx => { - TestForCondition(string.Empty, ctx => - { - var @event = new SchemaCreated(); + var @event = new EnrichedSchemaEvent(); - var actual = sut.Trigger(Envelope.Create<AppEvent>(@event), ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_check_if_condition_is_empty() + [Fact] + public void Should_trigger_check_if_condition_matchs() + { + TestForCondition("true", ctx => { - TestForCondition(string.Empty, ctx => - { - var @event = new EnrichedSchemaEvent(); + var @event = new EnrichedSchemaEvent(); - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.True(actual); + }); + } - [Fact] - public void Should_trigger_check_if_condition_matchs() + [Fact] + public void Should_not_trigger_check_if_condition_does_not_match() + { + TestForCondition("false", ctx => { - TestForCondition("true", ctx => - { - var @event = new EnrichedSchemaEvent(); + var @event = new EnrichedSchemaEvent(); - var actual = sut.Trigger(@event, ctx); + var actual = sut.Trigger(@event, ctx); - Assert.True(actual); - }); - } + Assert.False(actual); + }); + } - [Fact] - public void Should_not_trigger_check_if_condition_does_not_match() + private void TestForCondition(string condition, Action<RuleContext> action) + { + var trigger = new SchemaChangedTrigger { - TestForCondition("false", ctx => - { - var @event = new EnrichedSchemaEvent(); + Condition = condition + }; - var actual = sut.Trigger(@event, ctx); + action(Context(trigger)); - Assert.False(actual); - }); + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) + .MustNotHaveHappened(); } - - private void TestForCondition(string condition, Action<RuleContext> action) + else { - var trigger = new SchemaChangedTrigger - { - Condition = condition - }; - - action(Context(trigger)); - - if (string.IsNullOrWhiteSpace(condition)) - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) - .MustNotHaveHappened(); - } - else - { - A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) - .MustHaveHappened(); - } + A.CallTo(() => scriptEngine.Evaluate(A<ScriptVars>._, condition, default)) + .MustHaveHappened(); } + } - private static RuleContext Context(RuleTrigger? trigger = null, NamedId<DomainId>? appId = null) + private static RuleContext Context(RuleTrigger? trigger = null, NamedId<DomainId>? appId = null) + { + trigger ??= new SchemaChangedTrigger(); + + return new RuleContext { - trigger ??= new SchemaChangedTrigger(); - - return new RuleContext - { - AppId = appId ?? NamedId.Of(DomainId.NewGuid(), "my-app"), - Rule = new Rule(trigger, A.Fake<RuleAction>()), - RuleId = DomainId.NewGuid() - }; - } + AppId = appId ?? NamedId.Of(DomainId.NewGuid(), "my-app"), + Rule = new Rule(trigger, A.Fake<RuleAction>()), + RuleId = DomainId.NewGuid() + }; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs index 774b47e7a6..46a52a1581 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs @@ -12,69 +12,68 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public class SchemaCommandsTests { - public class SchemaCommandsTests + [Fact] + public void Should_convert_upsert_command() { - [Fact] - public void Should_convert_upsert_command() + var command = new SynchronizeSchema { - var command = new SynchronizeSchema + IsPublished = true, + Properties = new SchemaProperties { Hints = "MyHints" }, + Fields = new[] { - IsPublished = true, - Properties = new SchemaProperties { Hints = "MyHints" }, - Fields = new[] + new UpsertSchemaField { - new UpsertSchemaField + Name = "myString", + IsDisabled = true, + IsHidden = true, + IsLocked = true, + Properties = new StringFieldProperties { - Name = "myString", - IsDisabled = true, - IsHidden = true, - IsLocked = true, - Properties = new StringFieldProperties - { - IsRequired = true - }, - Partitioning = "language" - } - }, - FieldsInLists = FieldNames.Create("meta.id", "myString"), - FieldsInReferences = FieldNames.Create("myString"), - Scripts = new SchemaScripts + IsRequired = true + }, + Partitioning = "language" + } + }, + FieldsInLists = FieldNames.Create("meta.id", "myString"), + FieldsInReferences = FieldNames.Create("myString"), + Scripts = new SchemaScripts + { + Change = "change-script" + }, + PreviewUrls = new Dictionary<string, string> + { + ["mobile"] = "http://mobile" + }.ToReadonlyDictionary(), + Category = "myCategory" + }; + + var expected = + new Schema("my-schema") + .Update(new SchemaProperties { Hints = "MyHints" }) + .AddString(1, "myString", Partitioning.Language, new StringFieldProperties + { + IsRequired = true + }) + .HideField(1).DisableField(1).LockField(1) + .ChangeCategory("myCategory") + .SetFieldsInLists(FieldNames.Create("meta.id", "myString")) + .SetFieldsInReferences(FieldNames.Create("myString")) + .SetScripts(new SchemaScripts { Change = "change-script" - }, - PreviewUrls = new Dictionary<string, string> + }) + .SetPreviewUrls(new Dictionary<string, string> { ["mobile"] = "http://mobile" - }.ToReadonlyDictionary(), - Category = "myCategory" - }; - - var expected = - new Schema("my-schema") - .Update(new SchemaProperties { Hints = "MyHints" }) - .AddString(1, "myString", Partitioning.Language, new StringFieldProperties - { - IsRequired = true - }) - .HideField(1).DisableField(1).LockField(1) - .ChangeCategory("myCategory") - .SetFieldsInLists(FieldNames.Create("meta.id", "myString")) - .SetFieldsInReferences(FieldNames.Create("myString")) - .SetScripts(new SchemaScripts - { - Change = "change-script" - }) - .SetPreviewUrls(new Dictionary<string, string> - { - ["mobile"] = "http://mobile" - }.ToReadonlyDictionary()) - .Publish(); + }.ToReadonlyDictionary()) + .Publish(); - var actual = command.BuildSchema("my-schema", SchemaType.Default); + var actual = command.BuildSchema("my-schema", SchemaType.Default); - actual.Should().BeEquivalentTo(expected); - } + actual.Should().BeEquivalentTo(expected); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs index a27248ac9f..c1051a0714 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs @@ -18,136 +18,135 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Domain.Apps.Entities.Schemas +namespace Squidex.Domain.Apps.Entities.Schemas; + +public class SchemasSearchSourceTests : IClassFixture<TranslationsFixture> { - public class SchemasSearchSourceTests : IClassFixture<TranslationsFixture> + private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly SchemasSearchSource sut; + + public SchemasSearchSourceTests() { - private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly SchemasSearchSource sut; + sut = new SchemasSearchSource(appProvider, urlGenerator); + } - public SchemasSearchSourceTests() - { - sut = new SchemasSearchSource(appProvider, urlGenerator); - } + [Fact] + public async Task Should_not_add_actual_to_contents_if_user_has_no_permission() + { + var ctx = ContextWithPermission(); - [Fact] - public async Task Should_not_add_actual_to_contents_if_user_has_no_permission() - { - var ctx = ContextWithPermission(); + var schema1 = CreateSchema("schemaA1"); - var schema1 = CreateSchema("schemaA1"); + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) + .Returns(new List<ISchemaEntity> { schema1 }); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) - .Returns(new List<ISchemaEntity> { schema1 }); + A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) + .Returns("schemaA1-url"); - A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) - .Returns("schemaA1-url"); + var actual = await sut.SearchAsync("schema", ctx, default); - var actual = await sut.SearchAsync("schema", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url")); - } + [Fact] + public async Task Should_not_add_actual_to_contents_if_schema_is_component() + { + var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA1"); - [Fact] - public async Task Should_not_add_actual_to_contents_if_schema_is_component() - { - var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA1"); + var ctx = ContextWithPermission(); - var ctx = ContextWithPermission(); + var schema1 = CreateSchema("schemaA1", SchemaType.Component); - var schema1 = CreateSchema("schemaA1", SchemaType.Component); + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) + .Returns(new List<ISchemaEntity> { schema1 }); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) - .Returns(new List<ISchemaEntity> { schema1 }); + A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) + .Returns("schemaA1-url"); - A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) - .Returns("schemaA1-url"); + var actual = await sut.SearchAsync("schema", ctx, default); - var actual = await sut.SearchAsync("schema", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url")); - } + [Fact] + public async Task Should_return_actual_to_schema_and_contents_if_matching_and_permission_given() + { + var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA2"); - [Fact] - public async Task Should_return_actual_to_schema_and_contents_if_matching_and_permission_given() - { - var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA2"); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + var schema1 = CreateSchema("schemaA1"); + var schema2 = CreateSchema("schemaA2"); + var schema3 = CreateSchema("schemaB2"); - var schema1 = CreateSchema("schemaA1"); - var schema2 = CreateSchema("schemaA2"); - var schema3 = CreateSchema("schemaB2"); + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) + .Returns(new List<ISchemaEntity> { schema1, schema2, schema3 }); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) - .Returns(new List<ISchemaEntity> { schema1, schema2, schema3 }); + A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) + .Returns("schemaA1-url"); - A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) - .Returns("schemaA1-url"); + A.CallTo(() => urlGenerator.SchemaUI(appId, schema2.NamedId())) + .Returns("schemaA2-url"); - A.CallTo(() => urlGenerator.SchemaUI(appId, schema2.NamedId())) - .Returns("schemaA2-url"); + A.CallTo(() => urlGenerator.ContentsUI(appId, schema2.NamedId())) + .Returns("schemaA2-contents-url"); - A.CallTo(() => urlGenerator.ContentsUI(appId, schema2.NamedId())) - .Returns("schemaA2-contents-url"); + var actual = await sut.SearchAsync("schemaA", ctx, default); - var actual = await sut.SearchAsync("schemaA", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url") + .Add("schemaA2 Schema", SearchResultType.Schema, "schemaA2-url") + .Add("schemaA2 Contents", SearchResultType.Content, "schemaA2-contents-url", "schemaA2")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url") - .Add("schemaA2 Schema", SearchResultType.Schema, "schemaA2-url") - .Add("schemaA2 Contents", SearchResultType.Content, "schemaA2-contents-url", "schemaA2")); - } + [Fact] + public async Task Should_return_actual_to_schema_and_contents_if_schema_is_singleton() + { + var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA1"); - [Fact] - public async Task Should_return_actual_to_schema_and_contents_if_schema_is_singleton() - { - var permission = PermissionIds.ForApp(PermissionIds.AppContentsReadOwn, appId.Name, "schemaA1"); + var ctx = ContextWithPermission(permission.Id); - var ctx = ContextWithPermission(permission.Id); + var schema1 = CreateSchema("schemaA1", SchemaType.Singleton); - var schema1 = CreateSchema("schemaA1", SchemaType.Singleton); + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) + .Returns(new List<ISchemaEntity> { schema1 }); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) - .Returns(new List<ISchemaEntity> { schema1 }); + A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) + .Returns("schemaA1-url"); - A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) - .Returns("schemaA1-url"); + A.CallTo(() => urlGenerator.ContentUI(appId, schema1.NamedId(), schema1.Id)) + .Returns("schemaA1-content-url"); - A.CallTo(() => urlGenerator.ContentUI(appId, schema1.NamedId(), schema1.Id)) - .Returns("schemaA1-content-url"); + var actual = await sut.SearchAsync("schemaA", ctx, default); - var actual = await sut.SearchAsync("schemaA", ctx, default); + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url") + .Add("schemaA1 Content", SearchResultType.Content, "schemaA1-content-url", "schemaA1")); + } - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("schemaA1 Schema", SearchResultType.Schema, "schemaA1-url") - .Add("schemaA1 Content", SearchResultType.Content, "schemaA1-content-url", "schemaA1")); - } + private ISchemaEntity CreateSchema(string name, SchemaType type = SchemaType.Default) + { + return Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), name), new Schema(name, type: type)); + } - private ISchemaEntity CreateSchema(string name, SchemaType type = SchemaType.Default) - { - return Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), name), new Schema(name, type: type)); - } + private Context ContextWithPermission(string? permission = null) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - private Context ContextWithPermission(string? permission = null) + if (permission != null) { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - - if (permission != null) - { - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); - } - - return new Context(claimsPrincipal, Mocks.App(appId)); + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); } + + return new Context(claimsPrincipal, Mocks.App(appId)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs index 9cf0f355f1..4b021904ab 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Search/SearchManagerTests.cs @@ -12,90 +12,89 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Search +namespace Squidex.Domain.Apps.Entities.Search; + +public class SearchManagerTests { - public class SearchManagerTests - { - private readonly ISearchSource source1 = A.Fake<ISearchSource>(); - private readonly ISearchSource source2 = A.Fake<ISearchSource>(); - private readonly ILogger<SearchManager> log = A.Fake<ILogger<SearchManager>>(); - private readonly Context requestContext = Context.Anonymous(Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app"))); - private readonly SearchManager sut; + private readonly ISearchSource source1 = A.Fake<ISearchSource>(); + private readonly ISearchSource source2 = A.Fake<ISearchSource>(); + private readonly ILogger<SearchManager> log = A.Fake<ILogger<SearchManager>>(); + private readonly Context requestContext = Context.Anonymous(Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app"))); + private readonly SearchManager sut; - public SearchManagerTests() - { - sut = new SearchManager(new[] { source1, source2 }, log); - } + public SearchManagerTests() + { + sut = new SearchManager(new[] { source1, source2 }, log); + } - [Fact] - public async Task Should_not_call_sources_and_return_empty_if_query_is_empty() - { - var actual = await sut.SearchAsync(string.Empty, requestContext); + [Fact] + public async Task Should_not_call_sources_and_return_empty_if_query_is_empty() + { + var actual = await sut.SearchAsync(string.Empty, requestContext); - Assert.Empty(actual); + Assert.Empty(actual); - A.CallTo(() => source1.SearchAsync(A<string>._, A<Context>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => source1.SearchAsync(A<string>._, A<Context>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => source2.SearchAsync(A<string>._, A<Context>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => source2.SearchAsync(A<string>._, A<Context>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_call_sources_and_return_empty_if_is_too_short() - { - var actual = await sut.SearchAsync("11", requestContext); + [Fact] + public async Task Should_not_call_sources_and_return_empty_if_is_too_short() + { + var actual = await sut.SearchAsync("11", requestContext); - Assert.Empty(actual); + Assert.Empty(actual); - A.CallTo(() => source1.SearchAsync(A<string>._, A<Context>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => source1.SearchAsync(A<string>._, A<Context>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => source2.SearchAsync(A<string>._, A<Context>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => source2.SearchAsync(A<string>._, A<Context>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_aggregate_actuals_from_all_sources() - { - var actual1 = new SearchResults().Add("Name1", SearchResultType.Setting, "Url1"); - var actual2 = new SearchResults().Add("Name2", SearchResultType.Setting, "Url2"); + [Fact] + public async Task Should_aggregate_actuals_from_all_sources() + { + var actual1 = new SearchResults().Add("Name1", SearchResultType.Setting, "Url1"); + var actual2 = new SearchResults().Add("Name2", SearchResultType.Setting, "Url2"); - var query = "a query"; + var query = "a query"; - A.CallTo(() => source1.SearchAsync(query, requestContext, A<CancellationToken>._)) - .Returns(actual1); + A.CallTo(() => source1.SearchAsync(query, requestContext, A<CancellationToken>._)) + .Returns(actual1); - A.CallTo(() => source2.SearchAsync(query, requestContext, A<CancellationToken>._)) - .Returns(actual2); + A.CallTo(() => source2.SearchAsync(query, requestContext, A<CancellationToken>._)) + .Returns(actual2); - var actual = await sut.SearchAsync(query, requestContext); + var actual = await sut.SearchAsync(query, requestContext); - actual.Should().BeEquivalentTo( - new SearchResults() - .Add("Name1", SearchResultType.Setting, "Url1") - .Add("Name2", SearchResultType.Setting, "Url2")); - } + actual.Should().BeEquivalentTo( + new SearchResults() + .Add("Name1", SearchResultType.Setting, "Url1") + .Add("Name2", SearchResultType.Setting, "Url2")); + } - [Fact] - public async Task Should_ignore_exception_from_source() - { - var actual2 = new SearchResults().Add("Name2", SearchResultType.Setting, "Url2"); + [Fact] + public async Task Should_ignore_exception_from_source() + { + var actual2 = new SearchResults().Add("Name2", SearchResultType.Setting, "Url2"); - var query = "a query"; + var query = "a query"; - A.CallTo(() => source1.SearchAsync(query, requestContext, A<CancellationToken>._)) - .Throws(new InvalidOperationException()); + A.CallTo(() => source1.SearchAsync(query, requestContext, A<CancellationToken>._)) + .Throws(new InvalidOperationException()); - A.CallTo(() => source2.SearchAsync(query, requestContext, A<CancellationToken>._)) - .Returns(actual2); + A.CallTo(() => source2.SearchAsync(query, requestContext, A<CancellationToken>._)) + .Returns(actual2); - var actual = await sut.SearchAsync(query, requestContext); + var actual = await sut.SearchAsync(query, requestContext); - actual.Should().BeEquivalentTo(actual2); + actual.Should().BeEquivalentTo(actual2); - A.CallTo(log).Where(x => x.Method.Name == "Log") - .MustHaveHappened(); - } + A.CallTo(log).Where(x => x.Method.Name == "Log") + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagServiceTests.cs index 6d2e0fffd8..f91a31a370 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagServiceTests.cs @@ -12,374 +12,373 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Domain.Apps.Entities.Tags +namespace Squidex.Domain.Apps.Entities.Tags; + +public class TagServiceTests { - public class TagServiceTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState<TagService.State> state; + private readonly DomainId appId = DomainId.NewGuid(); + private readonly string group = DomainId.NewGuid().ToString(); + private readonly string stateId; + private readonly TagService sut; + + public TagServiceTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly TestState<TagService.State> state; - private readonly DomainId appId = DomainId.NewGuid(); - private readonly string group = DomainId.NewGuid().ToString(); - private readonly string stateId; - private readonly TagService sut; - - public TagServiceTests() - { - ct = cts.Token; - - stateId = $"{appId}_{group}"; - state = new TestState<TagService.State>(stateId); + ct = cts.Token; - sut = new TagService(state.PersistenceFactory); - } - - [Fact] - public async Task Should_delete_and_reset_state_if_cleaning() - { - await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); - await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag2", "tag3"), ct); + stateId = $"{appId}_{group}"; + state = new TestState<TagService.State>(stateId); - await sut.ClearAsync(appId, group, ct); + sut = new TagService(state.PersistenceFactory); + } - var allTags = await sut.GetTagsAsync(appId, group, ct); + [Fact] + public async Task Should_delete_and_reset_state_if_cleaning() + { + await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); + await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag2", "tag3"), ct); - Assert.Empty(allTags); + await sut.ClearAsync(appId, group, ct); - A.CallTo(() => state.Persistence.DeleteAsync(ct)) - .MustHaveHappened(); - } + var allTags = await sut.GetTagsAsync(appId, group, ct); - [Fact] - public async Task Should_unset_count_on_full_clear() - { - var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); + Assert.Empty(allTags); - await sut.UpdateAsync(appId, group, new Dictionary<string, int> - { - [ids["tag1"]] = 1, - [ids["tag2"]] = 1 - }, ct); + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); + } - // Clear is called by the event consumer to fill the counts again, therefore we do not delete other things. - await sut.ClearAsync(ct); + [Fact] + public async Task Should_unset_count_on_full_clear() + { + var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + await sut.UpdateAsync(appId, group, new Dictionary<string, int> + { + [ids["tag1"]] = 1, + [ids["tag2"]] = 1 + }, ct); - Assert.Equal(new Dictionary<string, int> - { - ["tag1"] = 0, - ["tag2"] = 0 - }, allTags); + // Clear is called by the event consumer to fill the counts again, therefore we do not delete other things. + await sut.ClearAsync(ct); - A.CallTo(() => state.Persistence.DeleteAsync(ct)) - .MustNotHaveHappened(); - } + var allTags = await sut.GetTagsAsync(appId, group, ct); - [Fact] - public async Task Should_rename_tag() + Assert.Equal(new Dictionary<string, int> { - var ids_0 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); + ["tag1"] = 0, + ["tag2"] = 0 + }, allTags); - await sut.RenameTagAsync(appId, group, "tag_0", "tag_1", ct); + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustNotHaveHappened(); + } - // Both names should map to the same tag. - var ids_1 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); - var ids_2 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_1"), ct); + [Fact] + public async Task Should_rename_tag() + { + var ids_0 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); - Assert.Equal(ids_0.Values, ids_1.Values); - Assert.Equal(ids_0.Values, ids_2.Values); + await sut.RenameTagAsync(appId, group, "tag_0", "tag_1", ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + // Both names should map to the same tag. + var ids_1 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); + var ids_2 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_1"), ct); - Assert.Equal(new Dictionary<string, int> - { - ["tag_1"] = 0 - }, allTags); - } + Assert.Equal(ids_0.Values, ids_1.Values); + Assert.Equal(ids_0.Values, ids_2.Values); + + var allTags = await sut.GetTagsAsync(appId, group, ct); - [Fact] - public async Task Should_rename_tag_twice() + Assert.Equal(new Dictionary<string, int> { - var ids_0 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); + ["tag_1"] = 0 + }, allTags); + } - // Forward the old name to the new name. - await sut.RenameTagAsync(appId, group, "tag_0", "tag_1", ct); - await sut.RenameTagAsync(appId, group, "tag_1", "tag_2", ct); + [Fact] + public async Task Should_rename_tag_twice() + { + var ids_0 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); - // All names should map to the same tag. - var ids_1 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); - var ids_2 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_1"), ct); - var ids_3 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_2"), ct); + // Forward the old name to the new name. + await sut.RenameTagAsync(appId, group, "tag_0", "tag_1", ct); + await sut.RenameTagAsync(appId, group, "tag_1", "tag_2", ct); - Assert.Equal(ids_0.Values, ids_1.Values); - Assert.Equal(ids_0.Values, ids_2.Values); - Assert.Equal(ids_0.Values, ids_3.Values); + // All names should map to the same tag. + var ids_1 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); + var ids_2 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_1"), ct); + var ids_3 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_2"), ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + Assert.Equal(ids_0.Values, ids_1.Values); + Assert.Equal(ids_0.Values, ids_2.Values); + Assert.Equal(ids_0.Values, ids_3.Values); - Assert.Equal(new Dictionary<string, int> - { - ["tag_2"] = 0 - }, allTags); - } + var allTags = await sut.GetTagsAsync(appId, group, ct); - [Fact] - public async Task Should_rename_tag_back() + Assert.Equal(new Dictionary<string, int> { - var ids_0 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); + ["tag_2"] = 0 + }, allTags); + } - // Forward the old name to the new name. - await sut.RenameTagAsync(appId, group, "tag_0", "tag_1", ct); - await sut.RenameTagAsync(appId, group, "tag_1", "tag_0", ct); + [Fact] + public async Task Should_rename_tag_back() + { + var ids_0 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); - // All names should map to the same tag. - var ids_1 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); - var ids_2 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_1"), ct); + // Forward the old name to the new name. + await sut.RenameTagAsync(appId, group, "tag_0", "tag_1", ct); + await sut.RenameTagAsync(appId, group, "tag_1", "tag_0", ct); - Assert.Equal(ids_0.Values, ids_1.Values); - Assert.Equal(ids_0.Values, ids_2.Values); + // All names should map to the same tag. + var ids_1 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_0"), ct); + var ids_2 = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag_1"), ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + Assert.Equal(ids_0.Values, ids_1.Values); + Assert.Equal(ids_0.Values, ids_2.Values); - Assert.Equal(new Dictionary<string, int> - { - ["tag_0"] = 0 - }, allTags); - } + var allTags = await sut.GetTagsAsync(appId, group, ct); - [Fact] - public async Task Should_merge_tags_on_rename() + Assert.Equal(new Dictionary<string, int> { - var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); + ["tag_0"] = 0 + }, allTags); + } - await sut.UpdateAsync(appId, group, new Dictionary<string, int> - { - [ids["tag1"]] = 1, - [ids["tag2"]] = 2 - }, ct); + [Fact] + public async Task Should_merge_tags_on_rename() + { + var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); - await sut.RenameTagAsync(appId, group, "tag2", "tag1", ct); + await sut.UpdateAsync(appId, group, new Dictionary<string, int> + { + [ids["tag1"]] = 1, + [ids["tag2"]] = 2 + }, ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + await sut.RenameTagAsync(appId, group, "tag2", "tag1", ct); - Assert.Equal(new Dictionary<string, int> - { - ["tag1"] = 3 - }, allTags); - } + var allTags = await sut.GetTagsAsync(appId, group, ct); - [Fact] - public async Task Should_merge_tags_when_stored_with_duplicate_names() + Assert.Equal(new Dictionary<string, int> { - var tags = new TagsExport + ["tag1"] = 3 + }, allTags); + } + + [Fact] + public async Task Should_merge_tags_when_stored_with_duplicate_names() + { + var tags = new TagsExport + { + Tags = new Dictionary<string, Tag> { - Tags = new Dictionary<string, Tag> - { - ["id1"] = new Tag { Name = "tag1", Count = 10 }, - ["id2"] = new Tag { Name = "tag1", Count = 20 } - }, - Alias = null! - }; + ["id1"] = new Tag { Name = "tag1", Count = 10 }, + ["id2"] = new Tag { Name = "tag1", Count = 20 } + }, + Alias = null! + }; - await sut.RebuildTagsAsync(appId, group, tags, ct); + await sut.RebuildTagsAsync(appId, group, tags, ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + var allTags = await sut.GetTagsAsync(appId, group, ct); - Assert.Equal(new Dictionary<string, int> - { - ["tag1"] = 30 - }, allTags); - } + Assert.Equal(new Dictionary<string, int> + { + ["tag1"] = 30 + }, allTags); + } - [Fact] - public async Task Should_fix_names_when_stored_with_wrong_names() + [Fact] + public async Task Should_fix_names_when_stored_with_wrong_names() + { + var tags = new TagsExport { - var tags = new TagsExport + Tags = new Dictionary<string, Tag> { - Tags = new Dictionary<string, Tag> - { - ["id1"] = new Tag { Name = "tag1 ", Count = 10 }, - ["id2"] = new Tag { Name = "tag2,", Count = 20 }, - ["id3"] = new Tag { Name = " tag3,", Count = 30 }, - ["id4"] = new Tag { Name = ",tag4,", Count = 40 } - }, - Alias = null! - }; + ["id1"] = new Tag { Name = "tag1 ", Count = 10 }, + ["id2"] = new Tag { Name = "tag2,", Count = 20 }, + ["id3"] = new Tag { Name = " tag3,", Count = 30 }, + ["id4"] = new Tag { Name = ",tag4,", Count = 40 } + }, + Alias = null! + }; - await sut.RebuildTagsAsync(appId, group, tags, ct); + await sut.RebuildTagsAsync(appId, group, tags, ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + var allTags = await sut.GetTagsAsync(appId, group, ct); - Assert.Equal(new Dictionary<string, int> - { - ["tag1"] = 10, - ["tag2"] = 20, - ["tag3"] = 30, - ["tag4"] = 40 - }, allTags); - } - - [Fact] - public async Task Should_rebuild_tags() + Assert.Equal(new Dictionary<string, int> + { + ["tag1"] = 10, + ["tag2"] = 20, + ["tag3"] = 30, + ["tag4"] = 40 + }, allTags); + } + + [Fact] + public async Task Should_rebuild_tags() + { + var tags = new TagsExport { - var tags = new TagsExport + Tags = new Dictionary<string, Tag> { - Tags = new Dictionary<string, Tag> - { - ["id1"] = new Tag { Name = "tag1", Count = 1 }, - ["id2"] = new Tag { Name = "tag2", Count = 2 }, - ["id3"] = new Tag { Name = "tag3", Count = 6 } - }, - Alias = null! - }; + ["id1"] = new Tag { Name = "tag1", Count = 1 }, + ["id2"] = new Tag { Name = "tag2", Count = 2 }, + ["id3"] = new Tag { Name = "tag3", Count = 6 } + }, + Alias = null! + }; - await sut.RebuildTagsAsync(appId, group, tags, ct); + await sut.RebuildTagsAsync(appId, group, tags, ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + var allTags = await sut.GetTagsAsync(appId, group, ct); - Assert.Equal(new Dictionary<string, int> - { - ["tag1"] = 1, - ["tag2"] = 2, - ["tag3"] = 6 - }, allTags); + Assert.Equal(new Dictionary<string, int> + { + ["tag1"] = 1, + ["tag2"] = 2, + ["tag3"] = 6 + }, allTags); - var export = await sut.GetExportableTagsAsync(appId, group, ct); + var export = await sut.GetExportableTagsAsync(appId, group, ct); - Assert.Equal(tags.Tags, export.Tags); - Assert.Empty(export.Alias); - } + Assert.Equal(tags.Tags, export.Tags); + Assert.Empty(export.Alias); + } - [Fact] - public async Task Should_rebuild_with_broken_export() + [Fact] + public async Task Should_rebuild_with_broken_export() + { + var tags = new TagsExport { - var tags = new TagsExport + Alias = new Dictionary<string, string> { - Alias = new Dictionary<string, string> - { - ["id1"] = "id2" - }, - Tags = null! - }; + ["id1"] = "id2" + }, + Tags = null! + }; - await sut.RebuildTagsAsync(appId, group, tags, ct); + await sut.RebuildTagsAsync(appId, group, tags, ct); - var export = await sut.GetExportableTagsAsync(appId, group, ct); + var export = await sut.GetExportableTagsAsync(appId, group, ct); - Assert.Equal(tags.Alias, export.Alias); - Assert.Empty(export.Tags); - } + Assert.Equal(tags.Alias, export.Alias); + Assert.Empty(export.Tags); + } + + [Fact] + public async Task Should_add_tag_but_not_count_tags() + { + await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); + await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag2", "tag3"), ct); - [Fact] - public async Task Should_add_tag_but_not_count_tags() + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary<string, int> { - await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); - await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag2", "tag3"), ct); + ["tag1"] = 0, + ["tag2"] = 0, + ["tag3"] = 0 + }, allTags); + } - var allTags = await sut.GetTagsAsync(appId, group, ct); + [Fact] + public async Task Should_add_and_increment_tags() + { + var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2", "tag3"), ct); - Assert.Equal(new Dictionary<string, int> - { - ["tag1"] = 0, - ["tag2"] = 0, - ["tag3"] = 0 - }, allTags); - } - - [Fact] - public async Task Should_add_and_increment_tags() + await sut.UpdateAsync(appId, group, new Dictionary<string, int> { - var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2", "tag3"), ct); + [ids["tag1"]] = 1, + [ids["tag2"]] = 1 + }, ct); - await sut.UpdateAsync(appId, group, new Dictionary<string, int> - { - [ids["tag1"]] = 1, - [ids["tag2"]] = 1 - }, ct); + await sut.UpdateAsync(appId, group, new Dictionary<string, int> + { + [ids["tag2"]] = 1, + [ids["tag3"]] = 1 + }, ct); - await sut.UpdateAsync(appId, group, new Dictionary<string, int> - { - [ids["tag2"]] = 1, - [ids["tag3"]] = 1 - }, ct); + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary<string, int> + { + ["tag1"] = 1, + ["tag2"] = 2, + ["tag3"] = 1 + }, allTags); + } - var allTags = await sut.GetTagsAsync(appId, group, ct); + [Fact] + public async Task Should_add_and_decrement_tags() + { + var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2", "tag3"), ct); - Assert.Equal(new Dictionary<string, int> - { - ["tag1"] = 1, - ["tag2"] = 2, - ["tag3"] = 1 - }, allTags); - } - - [Fact] - public async Task Should_add_and_decrement_tags() + await sut.UpdateAsync(appId, group, new Dictionary<string, int> { - var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2", "tag3"), ct); + [ids["tag1"]] = 1, + [ids["tag2"]] = 1 + }, ct); - await sut.UpdateAsync(appId, group, new Dictionary<string, int> - { - [ids["tag1"]] = 1, - [ids["tag2"]] = 1 - }, ct); + await sut.UpdateAsync(appId, group, new Dictionary<string, int> + { + [ids["tag2"]] = -2, + [ids["tag3"]] = -2 + }, ct); - await sut.UpdateAsync(appId, group, new Dictionary<string, int> - { - [ids["tag2"]] = -2, - [ids["tag3"]] = -2 - }, ct); + var allTags = await sut.GetTagsAsync(appId, group, ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + Assert.Equal(new Dictionary<string, int> + { + ["tag1"] = 1, + ["tag2"] = 0, + ["tag3"] = 0 + }, allTags); + } - Assert.Equal(new Dictionary<string, int> - { - ["tag1"] = 1, - ["tag2"] = 0, - ["tag3"] = 0 - }, allTags); - } - - [Fact] - public async Task Should_not_update_non_existing_tags() + [Fact] + public async Task Should_not_update_non_existing_tags() + { + // We have no names for these IDs so we cannot update it. + await sut.UpdateAsync(appId, group, new Dictionary<string, int> { - // We have no names for these IDs so we cannot update it. - await sut.UpdateAsync(appId, group, new Dictionary<string, int> - { - ["id1"] = 1, - ["id2"] = 1 - }, ct); + ["id1"] = 1, + ["id2"] = 1 + }, ct); - var allTags = await sut.GetTagsAsync(appId, group, ct); + var allTags = await sut.GetTagsAsync(appId, group, ct); - Assert.Empty(allTags); - } + Assert.Empty(allTags); + } - [Fact] - public async Task Should_resolve_tag_names() - { - // Get IDs from names. - var tagIds = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); + [Fact] + public async Task Should_resolve_tag_names() + { + // Get IDs from names. + var tagIds = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); - // Get names from IDs (reverse operation). - var tagNames = await sut.GetTagNamesAsync(appId, group, tagIds.Values.ToHashSet(), ct); + // Get names from IDs (reverse operation). + var tagNames = await sut.GetTagNamesAsync(appId, group, tagIds.Values.ToHashSet(), ct); - Assert.Equal(tagIds.Keys.ToArray(), tagNames.Values.ToArray()); - } + Assert.Equal(tagIds.Keys.ToArray(), tagNames.Values.ToArray()); + } - [Fact] - public async Task Should_get_exportable_tags() - { - var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); + [Fact] + public async Task Should_get_exportable_tags() + { + var ids = await sut.GetTagIdsAsync(appId, group, HashSet.Of("tag1", "tag2"), ct); - var allTags = await sut.GetExportableTagsAsync(appId, group, ct); + var allTags = await sut.GetExportableTagsAsync(appId, group, ct); - allTags.Tags.Should().BeEquivalentTo(new Dictionary<string, Tag> - { - [ids["tag1"]] = new Tag { Name = "tag1", Count = 0 }, - [ids["tag2"]] = new Tag { Name = "tag2", Count = 0 }, - }); - } + allTags.Tags.Should().BeEquivalentTo(new Dictionary<string, Tag> + { + [ids["tag1"]] = new Tag { Name = "tag1", Count = 0 }, + [ids["tag2"]] = new Tag { Name = "tag2", Count = 0 }, + }); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamContributorsTests.cs index 4cd8b39408..b085cd5609 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamContributorsTests.cs @@ -18,181 +18,180 @@ #pragma warning disable SA1310 // Field names must not contain underscore -namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject.Guards; + +public class GuardTeamContributorsTests : IClassFixture<TranslationsFixture> { - public class GuardTeamContributorsTests : IClassFixture<TranslationsFixture> - { - private readonly IUser user1 = UserMocks.User("1"); - private readonly IUser user2 = UserMocks.User("2"); - private readonly IUser user3 = UserMocks.User("3"); - private readonly IUserResolver users = A.Fake<IUserResolver>(); - private readonly Contributors contributors_0 = Contributors.Empty; + private readonly IUser user1 = UserMocks.User("1"); + private readonly IUser user2 = UserMocks.User("2"); + private readonly IUser user3 = UserMocks.User("3"); + private readonly IUserResolver users = A.Fake<IUserResolver>(); + private readonly Contributors contributors_0 = Contributors.Empty; - public GuardTeamContributorsTests() - { - A.CallTo(() => user1.Id) - .Returns("1"); + public GuardTeamContributorsTests() + { + A.CallTo(() => user1.Id) + .Returns("1"); - A.CallTo(() => user2.Id) - .Returns("2"); + A.CallTo(() => user2.Id) + .Returns("2"); - A.CallTo(() => user3.Id) - .Returns("3"); + A.CallTo(() => user3.Id) + .Returns("3"); - A.CallTo(() => users.FindByIdAsync("1", default)) - .Returns(user1); + A.CallTo(() => users.FindByIdAsync("1", default)) + .Returns(user1); - A.CallTo(() => users.FindByIdAsync("2", default)) - .Returns(user2); + A.CallTo(() => users.FindByIdAsync("2", default)) + .Returns(user2); - A.CallTo(() => users.FindByIdAsync("3", default)) - .Returns(user3); + A.CallTo(() => users.FindByIdAsync("3", default)) + .Returns(user3); - A.CallTo(() => users.FindByIdAsync("notfound", default)) - .Returns(Task.FromResult<IUser?>(null)); - } + A.CallTo(() => users.FindByIdAsync("notfound", default)) + .Returns(Task.FromResult<IUser?>(null)); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() - { - var command = new AssignContributor(); + [Fact] + public async Task CanAssign_should_throw_exception_if_contributor_id_is_null() + { + var command = new AssignContributor(); - await ValidationAssert.ThrowsAsync(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users), - new ValidationError("Contributor ID or email is required.", "ContributorId")); - } + await ValidationAssert.ThrowsAsync(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users), + new ValidationError("Contributor ID or email is required.", "ContributorId")); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_role_not_valid() - { - var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; + [Fact] + public async Task CanAssign_should_throw_exception_if_role_not_valid() + { + var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; - await ValidationAssert.ThrowsAsync(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users), - new ValidationError("Role is not a valid value.", "Role")); - } + await ValidationAssert.ThrowsAsync(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users), + new ValidationError("Role is not a valid value.", "Role")); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_same_role() - { - var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_same_role() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_1 = contributors_0.Assign("1", Role.Owner); - await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); - } + await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() - { - var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IgnoreActor = true }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_already_exists_with_some_role_but_is_from_restore() + { + var command = new AssignContributor { ContributorId = "1", Role = Role.Owner, IgnoreActor = true }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_1 = contributors_0.Assign("1", Role.Owner); - await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); - } + await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_user_not_found() - { - var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; + [Fact] + public async Task CanAssign_should_throw_exception_if_user_not_found() + { + var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users)); + } - [Fact] - public async Task CanAssign_should_throw_exception_if_user_is_actor() - { - var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = RefToken.User("3") }; + [Fact] + public async Task CanAssign_should_throw_exception_if_user_is_actor() + { + var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = RefToken.User("3") }; - await Assert.ThrowsAsync<DomainForbiddenException>(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users)); - } + await Assert.ThrowsAsync<DomainForbiddenException>(() => GuardTeamContributors.CanAssign(command, Team(contributors_0), users)); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_user_found() - { - var command = new AssignContributor { ContributorId = "1" }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_user_found() + { + var command = new AssignContributor { ContributorId = "1" }; - await GuardTeamContributors.CanAssign(command, Team(contributors_0), users); - } + await GuardTeamContributors.CanAssign(command, Team(contributors_0), users); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_role_is_valid() - { - var command = new AssignContributor { ContributorId = "1" }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_role_is_valid() + { + var command = new AssignContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_1 = contributors_0.Assign("1", Role.Owner); - await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); - } + await GuardTeamContributors.CanAssign(command, Team(contributors_1), users); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() - { - var command = new AssignContributor { ContributorId = "1" }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_role_changed() + { + var command = new AssignContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Owner); + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Owner); - await GuardTeamContributors.CanAssign(command, Team(contributors_2), users); - } + await GuardTeamContributors.CanAssign(command, Team(contributors_2), users); + } - [Fact] - public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ígnored() - { - var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; + [Fact] + public async Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_ígnored() + { + var command = new AssignContributor { ContributorId = "3", IgnorePlans = true }; - var contributors_1 = contributors_0.Assign("1", Role.Editor); - var contributors_2 = contributors_1.Assign("2", Role.Editor); + var contributors_1 = contributors_0.Assign("1", Role.Editor); + var contributors_2 = contributors_1.Assign("2", Role.Editor); - await GuardTeamContributors.CanAssign(command, Team(contributors_2), users); - } + await GuardTeamContributors.CanAssign(command, Team(contributors_2), users); + } - [Fact] - public void CanRemove_should_throw_exception_if_contributor_id_is_null() - { - var command = new RemoveContributor(); + [Fact] + public void CanRemove_should_throw_exception_if_contributor_id_is_null() + { + var command = new RemoveContributor(); - ValidationAssert.Throws(() => GuardTeamContributors.CanRemove(command, Team(contributors_0)), - new ValidationError("Contributor ID or email is required.", "ContributorId")); - } + ValidationAssert.Throws(() => GuardTeamContributors.CanRemove(command, Team(contributors_0)), + new ValidationError("Contributor ID or email is required.", "ContributorId")); + } - [Fact] - public void CanRemove_should_throw_exception_if_contributor_not_found() - { - var command = new RemoveContributor { ContributorId = "1" }; + [Fact] + public void CanRemove_should_throw_exception_if_contributor_not_found() + { + var command = new RemoveContributor { ContributorId = "1" }; - Assert.Throws<DomainObjectNotFoundException>(() => GuardTeamContributors.CanRemove(command, Team(contributors_0))); - } + Assert.Throws<DomainObjectNotFoundException>(() => GuardTeamContributors.CanRemove(command, Team(contributors_0))); + } - [Fact] - public void CanRemove_should_throw_exception_if_contributor_is_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; + [Fact] + public void CanRemove_should_throw_exception_if_contributor_is_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Editor); + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Editor); - ValidationAssert.Throws(() => GuardTeamContributors.CanRemove(command, Team(contributors_2)), - new ValidationError("Cannot remove the only owner.")); - } + ValidationAssert.Throws(() => GuardTeamContributors.CanRemove(command, Team(contributors_2)), + new ValidationError("Cannot remove the only owner.")); + } - [Fact] - public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() - { - var command = new RemoveContributor { ContributorId = "1" }; + [Fact] + public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; - var contributors_1 = contributors_0.Assign("1", Role.Owner); - var contributors_2 = contributors_1.Assign("2", Role.Owner); + var contributors_1 = contributors_0.Assign("1", Role.Owner); + var contributors_2 = contributors_1.Assign("2", Role.Owner); - GuardTeamContributors.CanRemove(command, Team(contributors_2)); - } + GuardTeamContributors.CanRemove(command, Team(contributors_2)); + } - private static ITeamEntity Team(Contributors contributors) - { - var team = A.Fake<ITeamEntity>(); + private static ITeamEntity Team(Contributors contributors) + { + var team = A.Fake<ITeamEntity>(); - A.CallTo(() => team.Contributors).Returns(contributors); + A.CallTo(() => team.Contributors).Returns(contributors); - return team; - } + return team; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamTests.cs index 71cdd2a16a..9edf2ad2c1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/Guards/GuardTeamTests.cs @@ -16,71 +16,70 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Teams.Apps.Teams.DomainObject.Guards +namespace Squidex.Domain.Teams.Apps.Teams.DomainObject.Guards; + +public class GuardTeamTests : IClassFixture<TranslationsFixture> { - public class GuardTeamTests : IClassFixture<TranslationsFixture> + private readonly IUserResolver users = A.Fake<IUserResolver>(); + private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); + private readonly Plan planBasic = new Plan(); + private readonly Plan planFree = new Plan(); + + public GuardTeamTests() { - private readonly IUserResolver users = A.Fake<IUserResolver>(); - private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); - private readonly Plan planBasic = new Plan(); - private readonly Plan planFree = new Plan(); - - public GuardTeamTests() - { - A.CallTo(() => users.FindByIdOrEmailAsync(A<string>._, default)) - .Returns(A.Dummy<IUser>()); - - A.CallTo(() => billingPlans.GetPlan("notfound")) - .Returns(null!); - - A.CallTo(() => billingPlans.GetPlan("basic")) - .Returns(planBasic); - - A.CallTo(() => billingPlans.GetPlan("free")) - .Returns(planFree); - } - - [Fact] - public void CanCreate_should_throw_exception_if_name_not_valid() - { - var command = new CreateTeam { Name = null! }; - - ValidationAssert.Throws(() => GuardTeam.CanCreate(command), - new ValidationError("Name is required.", "Name")); - } - - [Fact] - public void CanCreate_should_not_throw_exception_if_team_name_is_valid() - { - var command = new CreateTeam { Name = "new-team" }; - - GuardTeam.CanCreate(command); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_id_is_null() - { - var command = new ChangePlan { Actor = RefToken.User("me") }; - - ValidationAssert.Throws(() => GuardTeam.CanChangePlan(command, billingPlans), - new ValidationError("Plan ID is required.", "PlanId")); - } - - [Fact] - public void CanChangePlan_should_throw_exception_if_plan_not_found() - { - var command = new ChangePlan { PlanId = "notfound", Actor = RefToken.User("me") }; - - ValidationAssert.Throws(() => GuardTeam.CanChangePlan(command, billingPlans), - new ValidationError("A plan with this id does not exist.", "PlanId")); - } - - [Fact] - public void CanChangePlan_should_not_throw_exception_if_plan_is_found() - { - var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; - - GuardTeam.CanChangePlan(command, billingPlans); - } + A.CallTo(() => users.FindByIdOrEmailAsync(A<string>._, default)) + .Returns(A.Dummy<IUser>()); + + A.CallTo(() => billingPlans.GetPlan("notfound")) + .Returns(null!); + + A.CallTo(() => billingPlans.GetPlan("basic")) + .Returns(planBasic); + + A.CallTo(() => billingPlans.GetPlan("free")) + .Returns(planFree); + } + + [Fact] + public void CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateTeam { Name = null! }; + + ValidationAssert.Throws(() => GuardTeam.CanCreate(command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_team_name_is_valid() + { + var command = new CreateTeam { Name = "new-team" }; + + GuardTeam.CanCreate(command); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_id_is_null() + { + var command = new ChangePlan { Actor = RefToken.User("me") }; + + ValidationAssert.Throws(() => GuardTeam.CanChangePlan(command, billingPlans), + new ValidationError("Plan ID is required.", "PlanId")); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_not_found() + { + var command = new ChangePlan { PlanId = "notfound", Actor = RefToken.User("me") }; + + ValidationAssert.Throws(() => GuardTeam.CanChangePlan(command, billingPlans), + new ValidationError("A plan with this id does not exist.", "PlanId")); + } + + [Fact] + public void CanChangePlan_should_not_throw_exception_if_plan_is_found() + { + var command = new ChangePlan { PlanId = "basic", Actor = RefToken.User("me") }; + + GuardTeam.CanChangePlan(command, billingPlans); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/TeamDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/TeamDomainObjectTests.cs index 98baab1a58..77f9463285 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/TeamDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/DomainObject/TeamDomainObjectTests.cs @@ -18,333 +18,332 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Teams.DomainObject +namespace Squidex.Domain.Apps.Entities.Teams.DomainObject; + +public class TeamDomainObjectTests : HandlerTestBase<TeamDomainObject.State> { - public class TeamDomainObjectTests : HandlerTestBase<TeamDomainObject.State> + private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); + private readonly IBillingManager billingManager = A.Fake<IBillingManager>(); + private readonly IUser user; + private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); + private readonly Plan planPaid = new Plan { Id = "premium" }; + private readonly Plan planFree = new Plan { Id = "free" }; + private readonly string contributorId = DomainId.NewGuid().ToString(); + private readonly string name = "My Team"; + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly TeamDomainObject sut; + + protected override DomainId Id + { + get => teamId; + } + + public TeamDomainObjectTests() { - private readonly IBillingPlans billingPlans = A.Fake<IBillingPlans>(); - private readonly IBillingManager billingManager = A.Fake<IBillingManager>(); - private readonly IUser user; - private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); - private readonly Plan planPaid = new Plan { Id = "premium" }; - private readonly Plan planFree = new Plan { Id = "free" }; - private readonly string contributorId = DomainId.NewGuid().ToString(); - private readonly string name = "My Team"; - private readonly DomainId teamId = DomainId.NewGuid(); - private readonly TeamDomainObject sut; - - protected override DomainId Id - { - get => teamId; - } - - public TeamDomainObjectTests() - { - user = UserMocks.User(contributorId); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId, default)) - .Returns(user); - - A.CallTo(() => billingPlans.GetFreePlan()) - .Returns(planFree); - - A.CallTo(() => billingPlans.GetPlan(planFree.Id)) - .Returns(planFree); - - A.CallTo(() => billingPlans.GetPlan(planPaid.Id)) - .Returns(planPaid); - - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, A<string>._, default)) - .Returns(Task.FromResult<Uri?>(null)); - - var serviceProvider = - new ServiceCollection() - .AddSingleton(billingPlans) - .AddSingleton(billingManager) - .AddSingleton(userResolver) - .BuildServiceProvider(); - - var log = A.Fake<ILogger<TeamDomainObject>>(); + user = UserMocks.User(contributorId); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId, default)) + .Returns(user); + + A.CallTo(() => billingPlans.GetFreePlan()) + .Returns(planFree); + + A.CallTo(() => billingPlans.GetPlan(planFree.Id)) + .Returns(planFree); + + A.CallTo(() => billingPlans.GetPlan(planPaid.Id)) + .Returns(planPaid); + + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, A<string>._, default)) + .Returns(Task.FromResult<Uri?>(null)); + + var serviceProvider = + new ServiceCollection() + .AddSingleton(billingPlans) + .AddSingleton(billingManager) + .AddSingleton(userResolver) + .BuildServiceProvider(); + + var log = A.Fake<ILogger<TeamDomainObject>>(); #pragma warning disable MA0056 // Do not call overridable members in constructor - sut = new TeamDomainObject(Id, PersistenceFactory, log, serviceProvider); + sut = new TeamDomainObject(Id, PersistenceFactory, log, serviceProvider); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - [Fact] - public async Task Create_should_create_events_and_set_intitial_state() - { - var command = new CreateTeam { Name = name }; + [Fact] + public async Task Create_should_create_events_and_set_intitial_state() + { + var command = new CreateTeam { Name = name }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(name, sut.Snapshot.Name); + Assert.Equal(name, sut.Snapshot.Name); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamCreated { Name = name }), - CreateTeamEvent(new TeamContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamCreated { Name = name }), + CreateTeamEvent(new TeamContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }) + ); + } - [Fact] - public async Task Create_should_not_assign_client_as_contributor() - { - var command = new CreateTeam { Name = name, Actor = ActorClient }; + [Fact] + public async Task Create_should_not_assign_client_as_contributor() + { + var command = new CreateTeam { Name = name, Actor = ActorClient }; - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(name, sut.Snapshot.Name); + Assert.Equal(name, sut.Snapshot.Name); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamCreated { Name = name }, true) // Must be with client actor. - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamCreated { Name = name }, true) // Must be with client actor. + ); + } - [Fact] - public async Task Update_should_create_events_and_update_label_and_description() - { - var command = new UpdateTeam { Name = "Changed Name" }; + [Fact] + public async Task Update_should_create_events_and_update_label_and_description() + { + var command = new UpdateTeam { Name = "Changed Name" }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.Name, sut.Snapshot.Name); + Assert.Equal(command.Name, sut.Snapshot.Name); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamUpdated { Name = command.Name }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamUpdated { Name = command.Name }) + ); + } - [Fact] - public async Task ChangePlan_should_create_events_and_update_plan() - { - var command = new ChangePlan { PlanId = planPaid.Id }; + [Fact] + public async Task ChangePlan_should_create_events_and_update_plan() + { + var command = new ChangePlan { PlanId = planPaid.Id }; - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) - .Returns(Task.FromResult<Uri?>(null)); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) + .Returns(Task.FromResult<Uri?>(null)); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); + actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); - Assert.Equal(planPaid.Id, sut.Snapshot.Plan!.PlanId); + Assert.Equal(planPaid.Id, sut.Snapshot.Plan!.PlanId); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamPlanChanged { PlanId = planPaid.Id }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamPlanChanged { PlanId = planPaid.Id }) + ); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) - .MustHaveHappened(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) + .MustHaveHappened(); - A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) - .MustHaveHappened(); - } + A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangePlan_from_callback_should_create_events_and_update_plan() - { - var command = new ChangePlan { PlanId = planPaid.Id, FromCallback = true }; + [Fact] + public async Task ChangePlan_from_callback_should_create_events_and_update_plan() + { + var command = new ChangePlan { PlanId = planPaid.Id, FromCallback = true }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); + actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); - Assert.Equal(planPaid.Id, sut.Snapshot.Plan!.PlanId); + Assert.Equal(planPaid.Id, sut.Snapshot.Plan!.PlanId); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamPlanChanged { PlanId = planPaid.Id }) - ); + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamPlanChanged { PlanId = planPaid.Id }) + ); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(A<string>._, A<ITeamEntity>._, A<string?>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(A<string>._, A<ITeamEntity>._, A<string?>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => billingManager.SubscribeAsync(A<string>._, A<ITeamEntity>._, A<string?>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => billingManager.SubscribeAsync(A<string>._, A<ITeamEntity>._, A<string?>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task ChangePlan_from_callback_should_reset_plan_for_free_plan() - { - var command = new ChangePlan { PlanId = planFree.Id, FromCallback = true }; + [Fact] + public async Task ChangePlan_from_callback_should_reset_plan_for_free_plan() + { + var command = new ChangePlan { PlanId = planFree.Id, FromCallback = true }; - await ExecuteCreateAsync(); - await ExecuteChangePlanAsync(); + await ExecuteCreateAsync(); + await ExecuteChangePlanAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planFree.Id, true)); + actual.ShouldBeEquivalent(new PlanChangedResult(planFree.Id, true)); - Assert.Null(sut.Snapshot.Plan); + Assert.Null(sut.Snapshot.Plan); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamPlanReset()) - ); + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamPlanReset()) + ); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(A<string>._, A<ITeamEntity>._, A<string?>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(A<string>._, A<ITeamEntity>._, A<string?>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => billingManager.UnsubscribeAsync(A<string>._, A<ITeamEntity>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => billingManager.UnsubscribeAsync(A<string>._, A<ITeamEntity>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task ChangePlan_should_reset_plan_for_free_plan() - { - var command = new ChangePlan { PlanId = planFree.Id }; + [Fact] + public async Task ChangePlan_should_reset_plan_for_free_plan() + { + var command = new ChangePlan { PlanId = planFree.Id }; - await ExecuteCreateAsync(); - await ExecuteChangePlanAsync(); + await ExecuteCreateAsync(); + await ExecuteChangePlanAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planFree.Id, true)); + actual.ShouldBeEquivalent(new PlanChangedResult(planFree.Id, true)); - Assert.Null(sut.Snapshot.Plan); + Assert.Null(sut.Snapshot.Plan); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamPlanReset()) - ); + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamPlanReset()) + ); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => billingManager.UnsubscribeAsync(A<string>._, A<ITeamEntity>._, A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => billingManager.UnsubscribeAsync(A<string>._, A<ITeamEntity>._, A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task ChangePlan_should_not_make_update_for_redirect_actual() - { - var command = new ChangePlan { PlanId = planPaid.Id }; + [Fact] + public async Task ChangePlan_should_not_make_update_for_redirect_actual() + { + var command = new ChangePlan { PlanId = planPaid.Id }; - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) - .Returns(new Uri("http://squidex.io")); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, default)) + .Returns(new Uri("http://squidex.io")); - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id, false, new Uri("http://squidex.io"))); + actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id, false, new Uri("http://squidex.io"))); - Assert.Null(sut.Snapshot.Plan); - } + Assert.Null(sut.Snapshot.Plan); + } - [Fact] - public async Task ChangePlan_should_not_call_billing_manager_for_callback() - { - var command = new ChangePlan { PlanId = planPaid.Id, FromCallback = true }; + [Fact] + public async Task ChangePlan_should_not_call_billing_manager_for_callback() + { + var command = new ChangePlan { PlanId = planPaid.Id, FromCallback = true }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); + actual.ShouldBeEquivalent(new PlanChangedResult(planPaid.Id)); - Assert.Equal(planPaid.Id, sut.Snapshot.Plan?.PlanId); + Assert.Equal(planPaid.Id, sut.Snapshot.Plan?.PlanId); - A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => billingManager.MustRedirectToPortalAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => billingManager.SubscribeAsync(Actor.Identifier, A<ITeamEntity>._, planPaid.Id, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task AssignContributor_should_create_events_and_add_contributor() - { - var command = new AssignContributor { ContributorId = contributorId }; + [Fact] + public async Task AssignContributor_should_create_events_and_add_contributor() + { + var command = new AssignContributor { ContributorId = contributorId }; - await ExecuteCreateAsync(); + await ExecuteCreateAsync(); - var actual = await PublishIdempotentAsync(command); + var actual = await PublishIdempotentAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.Equal(command.Role, sut.Snapshot.Contributors[contributorId]); + Assert.Equal(command.Role, sut.Snapshot.Contributors[contributorId]); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamContributorAssigned { ContributorId = contributorId, Role = command.Role, IsAdded = true }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamContributorAssigned { ContributorId = contributorId, Role = command.Role, IsAdded = true }) + ); + } - [Fact] - public async Task RemoveContributor_should_create_events_and_remove_contributor() - { - var command = new RemoveContributor { ContributorId = contributorId }; + [Fact] + public async Task RemoveContributor_should_create_events_and_remove_contributor() + { + var command = new RemoveContributor { ContributorId = contributorId }; - await ExecuteCreateAsync(); - await ExecuteAssignContributorAsync(); + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); - var actual = await PublishAsync(command); + var actual = await PublishAsync(command); - actual.ShouldBeEquivalent(sut.Snapshot); + actual.ShouldBeEquivalent(sut.Snapshot); - Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); + Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); - LastEvents - .ShouldHaveSameEvents( - CreateTeamEvent(new TeamContributorRemoved { ContributorId = contributorId }) - ); - } + LastEvents + .ShouldHaveSameEvents( + CreateTeamEvent(new TeamContributorRemoved { ContributorId = contributorId }) + ); + } - private Task ExecuteCreateAsync() - { - return PublishAsync(new CreateTeam { Name = name }); - } + private Task ExecuteCreateAsync() + { + return PublishAsync(new CreateTeam { Name = name }); + } - private Task ExecuteAssignContributorAsync() - { - return PublishAsync(new AssignContributor { ContributorId = contributorId }); - } + private Task ExecuteAssignContributorAsync() + { + return PublishAsync(new AssignContributor { ContributorId = contributorId }); + } - private Task ExecuteChangePlanAsync() - { - return PublishAsync(new ChangePlan { PlanId = planPaid.Id }); - } + private Task ExecuteChangePlanAsync() + { + return PublishAsync(new ChangePlan { PlanId = planPaid.Id }); + } - private T CreateTeamEvent<T>(T @event, bool fromClient = false) where T : TeamEvent - { - @event.TeamId = teamId; + private T CreateTeamEvent<T>(T @event, bool fromClient = false) where T : TeamEvent + { + @event.TeamId = teamId; - return CreateEvent(@event, fromClient); - } + return CreateEvent(@event, fromClient); + } - private T CreateTeamCommand<T>(T command) where T : TeamCommand - { - command.TeamId = teamId; + private T CreateTeamCommand<T>(T command) where T : TeamCommand + { + command.TeamId = teamId; - return CreateCommand(command); - } + return CreateCommand(command); + } - private Task<object> PublishIdempotentAsync(TeamCommand command) - { - return PublishIdempotentAsync(sut, CreateTeamCommand(command)); - } + private Task<object> PublishIdempotentAsync(TeamCommand command) + { + return PublishIdempotentAsync(sut, CreateTeamCommand(command)); + } - private async Task<object> PublishAsync(TeamCommand command) - { - var actual = await sut.ExecuteAsync(CreateTeamCommand(command), default); + private async Task<object> PublishAsync(TeamCommand command) + { + var actual = await sut.ExecuteAsync(CreateTeamCommand(command), default); - return actual.Payload; - } + return actual.Payload; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs index 9049359893..7634244c3d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs @@ -11,81 +11,80 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Teams.Indexes +namespace Squidex.Domain.Apps.Entities.Teams.Indexes; + +public class TeamsIndexTests { - public class TeamsIndexTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly ITeamRepository teamRepository = A.Fake<ITeamRepository>(); - private readonly TeamsIndex sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly ITeamRepository teamRepository = A.Fake<ITeamRepository>(); + private readonly TeamsIndex sut; - public TeamsIndexTests() - { - ct = cts.Token; + public TeamsIndexTests() + { + ct = cts.Token; - sut = new TeamsIndex(teamRepository); - } + sut = new TeamsIndex(teamRepository); + } - [Fact] - public async Task Should_resolve_teams_by_id() - { - var team = SetupTeam(0); + [Fact] + public async Task Should_resolve_teams_by_id() + { + var team = SetupTeam(0); - A.CallTo(() => teamRepository.QueryAllAsync("user1", ct)) - .Returns(new List<ITeamEntity> { team }); + A.CallTo(() => teamRepository.QueryAllAsync("user1", ct)) + .Returns(new List<ITeamEntity> { team }); - var actual = await sut.GetTeamsAsync("user1", ct); + var actual = await sut.GetTeamsAsync("user1", ct); - Assert.Same(actual[0], team); - } + Assert.Same(actual[0], team); + } - [Fact] - public async Task Should_return_empty_teams_if_team_not_created() - { - var team = SetupTeam(-1); + [Fact] + public async Task Should_return_empty_teams_if_team_not_created() + { + var team = SetupTeam(-1); - A.CallTo(() => teamRepository.QueryAllAsync("user1", ct)) - .Returns(new List<ITeamEntity> { team }); + A.CallTo(() => teamRepository.QueryAllAsync("user1", ct)) + .Returns(new List<ITeamEntity> { team }); - var actual = await sut.GetTeamsAsync("user1", ct); + var actual = await sut.GetTeamsAsync("user1", ct); - Assert.Empty(actual); - } + Assert.Empty(actual); + } - [Fact] - public async Task Should_resolve_team_by_id() - { - var team = SetupTeam(0); + [Fact] + public async Task Should_resolve_team_by_id() + { + var team = SetupTeam(0); - A.CallTo(() => teamRepository.FindAsync(team.Id, ct)) - .Returns(team); + A.CallTo(() => teamRepository.FindAsync(team.Id, ct)) + .Returns(team); - var actual = await sut.GetTeamAsync(team.Id, ct); + var actual = await sut.GetTeamAsync(team.Id, ct); - Assert.Same(actual, team); - } + Assert.Same(actual, team); + } - [Fact] - public async Task Should_return_null_team_if_team_not_created() - { - var team = SetupTeam(0); + [Fact] + public async Task Should_return_null_team_if_team_not_created() + { + var team = SetupTeam(0); - A.CallTo(() => teamRepository.FindAsync(team.Id, ct)) - .Returns(Task.FromResult<ITeamEntity?>(null)); + A.CallTo(() => teamRepository.FindAsync(team.Id, ct)) + .Returns(Task.FromResult<ITeamEntity?>(null)); - var actual = await sut.GetTeamAsync(team.Id, ct); + var actual = await sut.GetTeamAsync(team.Id, ct); - Assert.Null(actual); - } + Assert.Null(actual); + } - private static ITeamEntity SetupTeam(long version) - { - var team = Mocks.Team(DomainId.NewGuid()); + private static ITeamEntity SetupTeam(long version) + { + var team = Mocks.Team(DomainId.NewGuid()); - A.CallTo(() => team.Version).Returns(version); + A.CallTo(() => team.Version).Returns(version); - return team; - } + return team; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs index ff3623e423..9679938e7d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs @@ -9,33 +9,32 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.TestHelpers +namespace Squidex.Domain.Apps.Entities.TestHelpers; + +public static class AExtensions { - public static class AExtensions + public static Q HasQuery(this INegatableArgumentConstraintManager<Q> that, string query) { - public static Q HasQuery(this INegatableArgumentConstraintManager<Q> that, string query) - { - return that.Matches(x => x.Query!.ToString() == query); - } + return that.Matches(x => x.Query!.ToString() == query); + } - public static Q HasIdsWithoutTotal(this INegatableArgumentConstraintManager<Q> that, params DomainId[] ids) - { - return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids) && x.NoTotal); - } + public static Q HasIdsWithoutTotal(this INegatableArgumentConstraintManager<Q> that, params DomainId[] ids) + { + return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids) && x.NoTotal); + } - public static Q HasIds(this INegatableArgumentConstraintManager<Q> that, params DomainId[] ids) - { - return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids)); - } + public static Q HasIds(this INegatableArgumentConstraintManager<Q> that, params DomainId[] ids) + { + return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids)); + } - public static Q HasIds(this INegatableArgumentConstraintManager<Q> that, IEnumerable<DomainId> ids) - { - return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids.ToHashSet())); - } + public static Q HasIds(this INegatableArgumentConstraintManager<Q> that, IEnumerable<DomainId> ids) + { + return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids.ToHashSet())); + } - public static ClrQuery Is(this INegatableArgumentConstraintManager<ClrQuery> that, string query) - { - return that.Matches(x => x.ToString() == query); - } + public static ClrQuery Is(this INegatableArgumentConstraintManager<ClrQuery> that, string query) + { + return that.Matches(x => x.ToString() == query); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs index 83dcf20ad6..3d3a696bd3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs @@ -8,35 +8,34 @@ using FluentAssertions; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Entities.TestHelpers +namespace Squidex.Domain.Apps.Entities.TestHelpers; + +public static class AssertHelper { - public static class AssertHelper + public static void ShouldHaveSameEvents(this IEnumerable<Envelope<IEvent>> events, params IEvent[] others) { - public static void ShouldHaveSameEvents(this IEnumerable<Envelope<IEvent>> events, params IEvent[] others) - { - var source = events.Select(x => x.Payload).ToArray(); + var source = events.Select(x => x.Payload).ToArray(); - source.Should().HaveSameCount(others); + source.Should().HaveSameCount(others); - for (var i = 0; i < source.Length; i++) - { - var lhs = source[i]; - var rhs = others[i]; + for (var i = 0; i < source.Length; i++) + { + var lhs = source[i]; + var rhs = others[i]; - lhs.ShouldBeSameEvent(rhs); - } + lhs.ShouldBeSameEvent(rhs); } + } - public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) - { - lhs.Should().BeOfType(rhs.GetType()); + public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) + { + lhs.Should().BeOfType(rhs.GetType()); - ((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties()); - } + ((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties()); + } - public static void ShouldBeEquivalent<T>(this T lhs, T rhs) - { - lhs.Should().BeEquivalentTo(rhs, o => o.IncludingProperties()); - } + public static void ShouldBeEquivalent<T>(this T lhs, T rhs) + { + lhs.Should().BeEquivalentTo(rhs, o => o.IncludingProperties()); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index 83fba2921f..791565a3ae 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -14,132 +14,131 @@ using Squidex.Infrastructure.States; using Xunit; -namespace Squidex.Domain.Apps.Entities.TestHelpers +namespace Squidex.Domain.Apps.Entities.TestHelpers; + +public abstract class HandlerTestBase<TState> { - public abstract class HandlerTestBase<TState> - { - private readonly IPersistenceFactory<TState> persistenceFactory = A.Fake<IStore<TState>>(); - private readonly IPersistence<TState> persistence = A.Fake<IPersistence<TState>>(); + private readonly IPersistenceFactory<TState> persistenceFactory = A.Fake<IStore<TState>>(); + private readonly IPersistence<TState> persistence = A.Fake<IPersistence<TState>>(); - protected RefToken Actor { get; } = RefToken.User("me"); + protected RefToken Actor { get; } = RefToken.User("me"); - protected RefToken ActorClient { get; } = RefToken.Client("client"); + protected RefToken ActorClient { get; } = RefToken.Client("client"); - protected DomainId AppId { get; } = DomainId.NewGuid(); + protected DomainId AppId { get; } = DomainId.NewGuid(); - protected DomainId SchemaId { get; } = DomainId.NewGuid(); + protected DomainId SchemaId { get; } = DomainId.NewGuid(); - protected string AppName { get; } = "my-app"; + protected string AppName { get; } = "my-app"; - protected string SchemaName { get; } = "my-schema"; + protected string SchemaName { get; } = "my-schema"; - protected ClaimsPrincipal User { get; } = Mocks.FrontendUser(); + protected ClaimsPrincipal User { get; } = Mocks.FrontendUser(); - protected NamedId<DomainId> AppNamedId - { - get => NamedId.Of(AppId, AppName); - } + protected NamedId<DomainId> AppNamedId + { + get => NamedId.Of(AppId, AppName); + } - protected NamedId<DomainId> SchemaNamedId - { - get => NamedId.Of(SchemaId, SchemaName); - } + protected NamedId<DomainId> SchemaNamedId + { + get => NamedId.Of(SchemaId, SchemaName); + } - protected abstract DomainId Id { get; } + protected abstract DomainId Id { get; } - public IPersistenceFactory<TState> PersistenceFactory - { - get => persistenceFactory; - } + public IPersistenceFactory<TState> PersistenceFactory + { + get => persistenceFactory; + } - public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = Enumerable.Empty<Envelope<IEvent>>(); + public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = Enumerable.Empty<Envelope<IEvent>>(); - protected HandlerTestBase() - { + protected HandlerTestBase() + { #pragma warning disable MA0056 // Do not call overridable members in constructor - A.CallTo(() => persistenceFactory.WithSnapshotsAndEventSourcing(A<Type>._, Id, A<HandleSnapshot<TState>>._, A<HandleEvent>._)) - .Returns(persistence); + A.CallTo(() => persistenceFactory.WithSnapshotsAndEventSourcing(A<Type>._, Id, A<HandleSnapshot<TState>>._, A<HandleEvent>._)) + .Returns(persistence); - A.CallTo(() => persistenceFactory.WithEventSourcing(A<Type>._, Id, A<HandleEvent>._)) - .Returns(persistence); + A.CallTo(() => persistenceFactory.WithEventSourcing(A<Type>._, Id, A<HandleEvent>._)) + .Returns(persistence); - A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, default)) - .Invokes((IReadOnlyList<Envelope<IEvent>> events, CancellationToken _) => LastEvents = events); + A.CallTo(() => persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, default)) + .Invokes((IReadOnlyList<Envelope<IEvent>> events, CancellationToken _) => LastEvents = events); - A.CallTo(() => persistence.DeleteAsync(default)) - .Invokes(() => LastEvents = Enumerable.Empty<Envelope<IEvent>>()); + A.CallTo(() => persistence.DeleteAsync(default)) + .Invokes(() => LastEvents = Enumerable.Empty<Envelope<IEvent>>()); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - protected CommandContext CreateCommandContext<TCommand>(TCommand command) where TCommand : SquidexCommand - { - return new CommandContext(CreateCommand(command), A.Dummy<ICommandBus>()); - } + protected CommandContext CreateCommandContext<TCommand>(TCommand command) where TCommand : SquidexCommand + { + return new CommandContext(CreateCommand(command), A.Dummy<ICommandBus>()); + } - protected async Task<CommandContext> HandleAsync<TCommand>(ICommandMiddleware middleware, TCommand command, - CancellationToken ct = default) where TCommand : SquidexCommand - { - var context = new CommandContext(CreateCommand(command), A.Dummy<ICommandBus>()); + protected async Task<CommandContext> HandleAsync<TCommand>(ICommandMiddleware middleware, TCommand command, + CancellationToken ct = default) where TCommand : SquidexCommand + { + var context = new CommandContext(CreateCommand(command), A.Dummy<ICommandBus>()); - await middleware.HandleAsync(context, ct); + await middleware.HandleAsync(context, ct); - return context; - } + return context; + } - protected async Task<object> PublishIdempotentAsync<T>(DomainObject<T> domainObject, IAggregateCommand command, - CancellationToken ct = default) where T : class, IDomainState<T>, new() - { - var actual = await domainObject.ExecuteAsync(command, default); + protected async Task<object> PublishIdempotentAsync<T>(DomainObject<T> domainObject, IAggregateCommand command, + CancellationToken ct = default) where T : class, IDomainState<T>, new() + { + var actual = await domainObject.ExecuteAsync(command, default); - var previousSnapshot = domainObject.Snapshot; - var previousVersion = domainObject.Snapshot.Version; + var previousSnapshot = domainObject.Snapshot; + var previousVersion = domainObject.Snapshot.Version; - await domainObject.ExecuteAsync(command, ct); + await domainObject.ExecuteAsync(command, ct); - Assert.Same(previousSnapshot, domainObject.Snapshot); - Assert.Equal(previousVersion, domainObject.Snapshot.Version); + Assert.Same(previousSnapshot, domainObject.Snapshot); + Assert.Equal(previousVersion, domainObject.Snapshot.Version); - return actual.Payload; + return actual.Payload; + } + + protected TCommand CreateCommand<TCommand>(TCommand command) where TCommand : SquidexCommand + { + command.ExpectedVersion = EtagVersion.Any; + command.Actor ??= Actor; + + if (command.User == null && command.Actor.IsUser) + { + command.User = User; } - protected TCommand CreateCommand<TCommand>(TCommand command) where TCommand : SquidexCommand + if (command is IAppCommand { AppId: null } appCommand) { - command.ExpectedVersion = EtagVersion.Any; - command.Actor ??= Actor; + appCommand.AppId = AppNamedId; + } - if (command.User == null && command.Actor.IsUser) - { - command.User = User; - } + if (command is ISchemaCommand { SchemaId: null } schemaCommand) + { + schemaCommand.SchemaId = SchemaNamedId; + } - if (command is IAppCommand { AppId: null } appCommand) - { - appCommand.AppId = AppNamedId; - } + return command; + } - if (command is ISchemaCommand { SchemaId: null } schemaCommand) - { - schemaCommand.SchemaId = SchemaNamedId; - } + protected TEvent CreateEvent<TEvent>(TEvent @event, bool fromClient = false) where TEvent : SquidexEvent + { + @event.Actor = fromClient ? ActorClient : Actor; - return command; + if (@event is AppEvent appEvent) + { + appEvent.AppId = AppNamedId; } - protected TEvent CreateEvent<TEvent>(TEvent @event, bool fromClient = false) where TEvent : SquidexEvent + if (@event is SchemaEvent schemaEvent) { - @event.Actor = fromClient ? ActorClient : Actor; - - if (@event is AppEvent appEvent) - { - appEvent.AppId = AppNamedId; - } - - if (@event is SchemaEvent schemaEvent) - { - schemaEvent.SchemaId = SchemaNamedId; - } - - return @event; + schemaEvent.SchemaId = SchemaNamedId; } + + return @event; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs index 7fd2d09d21..95f38704a3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs @@ -18,83 +18,82 @@ using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Domain.Apps.Entities.TestHelpers +namespace Squidex.Domain.Apps.Entities.TestHelpers; + +public static class Mocks { - public static class Mocks + public static IAppEntity App(NamedId<DomainId> appId, params Language[] languages) { - public static IAppEntity App(NamedId<DomainId> appId, params Language[] languages) + var config = LanguagesConfig.English; + + foreach (var language in languages) { - var config = LanguagesConfig.English; + config = config.Set(language); + } - foreach (var language in languages) - { - config = config.Set(language); - } + var app = A.Fake<IAppEntity>(); - var app = A.Fake<IAppEntity>(); + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + A.CallTo(() => app.Languages).Returns(config); + A.CallTo(() => app.UniqueId).Returns(appId.Id); - A.CallTo(() => app.Id).Returns(appId.Id); - A.CallTo(() => app.Name).Returns(appId.Name); - A.CallTo(() => app.Languages).Returns(config); - A.CallTo(() => app.UniqueId).Returns(appId.Id); + return app; + } - return app; - } + public static ISchemaEntity Schema(NamedId<DomainId> appId, NamedId<DomainId> schemaId, Schema? schemaDef = null) + { + var schema = A.Fake<ISchemaEntity>(); - public static ISchemaEntity Schema(NamedId<DomainId> appId, NamedId<DomainId> schemaId, Schema? schemaDef = null) - { - var schema = A.Fake<ISchemaEntity>(); + schemaDef ??= new Schema(schemaId.Name); - schemaDef ??= new Schema(schemaId.Name); + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.AppId).Returns(appId); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + A.CallTo(() => schema.UniqueId).Returns(DomainId.Combine(appId, schemaId.Id)); - A.CallTo(() => schema.Id).Returns(schemaId.Id); - A.CallTo(() => schema.AppId).Returns(appId); - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - A.CallTo(() => schema.UniqueId).Returns(DomainId.Combine(appId, schemaId.Id)); + return schema; + } - return schema; - } + public static ITeamEntity Team(DomainId teamId, string teamName = "my-team", string contributor = "user") + { + var team = A.Fake<ITeamEntity>(); - public static ITeamEntity Team(DomainId teamId, string teamName = "my-team", string contributor = "user") - { - var team = A.Fake<ITeamEntity>(); + A.CallTo(() => team.Id).Returns(teamId); + A.CallTo(() => team.UniqueId).Returns(teamId); + A.CallTo(() => team.Name).Returns(teamName); + A.CallTo(() => team.Contributors).Returns(Contributors.Empty.Assign(contributor, Role.Owner)); - A.CallTo(() => team.Id).Returns(teamId); - A.CallTo(() => team.UniqueId).Returns(teamId); - A.CallTo(() => team.Name).Returns(teamName); - A.CallTo(() => team.Contributors).Returns(Contributors.Empty.Assign(contributor, Role.Owner)); + return team; + } - return team; - } + public static ClaimsPrincipal ApiUser(string? role = null) + { + return CreateUser(role, "api"); + } - public static ClaimsPrincipal ApiUser(string? role = null) - { - return CreateUser(role, "api"); - } + public static ClaimsPrincipal FrontendUser(string? role = null, string? permission = null) + { + return CreateUser(role, DefaultClients.Frontend, permission); + } + + private static ClaimsPrincipal CreateUser(string? role, string client, string? permission = null) + { + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - public static ClaimsPrincipal FrontendUser(string? role = null, string? permission = null) + claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client)); + + if (role != null) { - return CreateUser(role, DefaultClients.Frontend, permission); + claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); } - private static ClaimsPrincipal CreateUser(string? role, string client, string? permission = null) + if (permission != null) { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - - claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client)); - - if (role != null) - { - claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); - } - - if (permission != null) - { - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); - } - - return claimsPrincipal; + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); } + + return claimsPrincipal; } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs index 1d57dfca1d..0b25331b16 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/NoopAssetFile.cs @@ -7,18 +7,17 @@ using Squidex.Assets; -namespace Squidex.Domain.Apps.Entities.TestHelpers +namespace Squidex.Domain.Apps.Entities.TestHelpers; + +public sealed class NoopAssetFile : AssetFile { - public sealed class NoopAssetFile : AssetFile + public NoopAssetFile(string fileName = "image.png", string mimeType = "image/png", long fileSize = 1024) + : base(fileName, mimeType, fileSize) { - public NoopAssetFile(string fileName = "image.png", string mimeType = "image/png", long fileSize = 1024) - : base(fileName, mimeType, fileSize) - { - } + } - public override Stream OpenRead() - { - return new MemoryStream(); - } + public override Stream OpenRead() + { + return new MemoryStream(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/TestConfig.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/TestConfig.cs index 788eb38801..5446593773 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/TestConfig.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/TestConfig.cs @@ -7,22 +7,21 @@ using Microsoft.Extensions.Configuration; -namespace Squidex.Domain.Apps.Entities.TestHelpers +namespace Squidex.Domain.Apps.Entities.TestHelpers; + +public static class TestConfig { - public static class TestConfig - { - public static IConfiguration Configuration { get; } + public static IConfiguration Configuration { get; } - static TestConfig() - { - var basePath = Path.GetFullPath("../../../"); + static TestConfig() + { + var basePath = Path.GetFullPath("../../../"); - Configuration = new ConfigurationBuilder() - .SetBasePath(basePath) - .AddJsonFile("appsettings.json", true) - .AddJsonFile("appsettings.Development.json", true) - .AddEnvironmentVariables() - .Build(); - } + Configuration = new ConfigurationBuilder() + .SetBasePath(basePath) + .AddJsonFile("appsettings.json", true) + .AddJsonFile("appsettings.Development.json", true) + .AddEnvironmentVariables() + .Build(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs index 3837e05733..4fef4b05e7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/ValidationAssert.cs @@ -10,52 +10,51 @@ using Xunit; using Xunit.Sdk; -namespace Squidex.Domain.Apps.Entities.TestHelpers +namespace Squidex.Domain.Apps.Entities.TestHelpers; + +public static class ValidationAssert { - public static class ValidationAssert + public static void Throws(Action action, params ValidationError[] errors) { - public static void Throws(Action action, params ValidationError[] errors) + try { - try - { - action(); + action(); - Assert.True(false, $"Expected {typeof(ValidationException)} but succeeded"); - } - catch (ValidationException ex) - { - ex.Errors.ToArray().Should().BeEquivalentTo(errors); - } - catch (XunitException) - { - throw; - } - catch (Exception ex) - { - Assert.True(false, $"Excepted {typeof(ValidationException)}, but got {ex.GetType()}"); - } + Assert.True(false, $"Expected {typeof(ValidationException)} but succeeded"); + } + catch (ValidationException ex) + { + ex.Errors.ToArray().Should().BeEquivalentTo(errors); + } + catch (XunitException) + { + throw; + } + catch (Exception ex) + { + Assert.True(false, $"Excepted {typeof(ValidationException)}, but got {ex.GetType()}"); } + } - public static async Task ThrowsAsync(Func<Task> action, params ValidationError[] errors) + public static async Task ThrowsAsync(Func<Task> action, params ValidationError[] errors) + { + try { - try - { - await action(); + await action(); - Assert.True(false, $"Expected {typeof(ValidationException)} but succeeded"); - } - catch (ValidationException ex) - { - ex.Errors.ToArray().Should().BeEquivalentTo(errors); - } - catch (XunitException) - { - throw; - } - catch (Exception ex) - { - Assert.True(false, $"Excepted {typeof(ValidationException)}, but got {ex.GetType()}"); - } + Assert.True(false, $"Expected {typeof(ValidationException)} but succeeded"); + } + catch (ValidationException ex) + { + ex.Errors.ToArray().Should().BeEquivalentTo(errors); + } + catch (XunitException) + { + throw; + } + catch (Exception ex) + { + Assert.True(false, $"Excepted {typeof(ValidationException)}, but got {ex.GetType()}"); } } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs index 4e0e518348..1b405d2a44 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs @@ -14,85 +14,84 @@ using Squidex.Infrastructure.States; using Xunit; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public class DefaultKeyStoreTests { - public class DefaultKeyStoreTests + private readonly ISnapshotStore<DefaultKeyStore.State> store = A.Fake<ISnapshotStore<DefaultKeyStore.State>>(); + private readonly DefaultKeyStore sut; + + public DefaultKeyStoreTests() { - private readonly ISnapshotStore<DefaultKeyStore.State> store = A.Fake<ISnapshotStore<DefaultKeyStore.State>>(); - private readonly DefaultKeyStore sut; + sut = new DefaultKeyStore(store); + } - public DefaultKeyStoreTests() - { - sut = new DefaultKeyStore(store); - } + [Fact] + public void Should_configure_new_keys() + { + A.CallTo(() => store.ReadAsync(A<DomainId>._, default)) + .Returns(new SnapshotResult<DefaultKeyStore.State>(default, null!, 0)); - [Fact] - public void Should_configure_new_keys() - { - A.CallTo(() => store.ReadAsync(A<DomainId>._, default)) - .Returns(new SnapshotResult<DefaultKeyStore.State>(default, null!, 0)); + var options = new OpenIddictServerOptions(); - var options = new OpenIddictServerOptions(); + sut.Configure(options); - sut.Configure(options); + Assert.NotEmpty(options.SigningCredentials); + Assert.NotEmpty(options.EncryptionCredentials); - Assert.NotEmpty(options.SigningCredentials); - Assert.NotEmpty(options.EncryptionCredentials); + A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultKeyStore.State>>._, default)) + .MustHaveHappenedOnceExactly(); + } - A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultKeyStore.State>>._, default)) - .MustHaveHappenedOnceExactly(); - } + [Fact] + public void Should_configure_existing_keys() + { + A.CallTo(() => store.ReadAsync(A<DomainId>._, default)) + .Returns(new SnapshotResult<DefaultKeyStore.State>(default, ExistingKey(), 0)); - [Fact] - public void Should_configure_existing_keys() - { - A.CallTo(() => store.ReadAsync(A<DomainId>._, default)) - .Returns(new SnapshotResult<DefaultKeyStore.State>(default, ExistingKey(), 0)); + var options = new OpenIddictServerOptions(); - var options = new OpenIddictServerOptions(); + sut.Configure(options); - sut.Configure(options); + Assert.NotEmpty(options.SigningCredentials); + Assert.NotEmpty(options.EncryptionCredentials); - Assert.NotEmpty(options.SigningCredentials); - Assert.NotEmpty(options.EncryptionCredentials); + A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultKeyStore.State>>._, default)) + .MustNotHaveHappened(); + } - A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultKeyStore.State>>._, default)) - .MustNotHaveHappened(); - } + [Fact] + public void Should_configure_existing_keys_when_initial_setup_failed() + { + A.CallTo(() => store.ReadAsync(A<DomainId>._, default)) + .Returns(new SnapshotResult<DefaultKeyStore.State>(default, null!, 0)).Once() + .Then + .Returns(new SnapshotResult<DefaultKeyStore.State>(default, ExistingKey(), 0)); - [Fact] - public void Should_configure_existing_keys_when_initial_setup_failed() - { - A.CallTo(() => store.ReadAsync(A<DomainId>._, default)) - .Returns(new SnapshotResult<DefaultKeyStore.State>(default, null!, 0)).Once() - .Then - .Returns(new SnapshotResult<DefaultKeyStore.State>(default, ExistingKey(), 0)); + A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultKeyStore.State>>._, default)) + .Throws(new InconsistentStateException(0, 0)); - A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultKeyStore.State>>._, default)) - .Throws(new InconsistentStateException(0, 0)); + var options = new OpenIddictServerOptions(); - var options = new OpenIddictServerOptions(); + sut.Configure(options); - sut.Configure(options); + Assert.NotEmpty(options.SigningCredentials); + Assert.NotEmpty(options.EncryptionCredentials); - Assert.NotEmpty(options.SigningCredentials); - Assert.NotEmpty(options.EncryptionCredentials); + A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultKeyStore.State>>._, default)) + .MustHaveHappened(); + } - A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultKeyStore.State>>._, default)) - .MustHaveHappened(); - } + private static DefaultKeyStore.State ExistingKey() + { + var key = new RsaSecurityKey(RSA.Create(2048)) + { + KeyId = CryptoRandom.CreateUniqueId(16) + }; - private static DefaultKeyStore.State ExistingKey() + return new DefaultKeyStore.State { - var key = new RsaSecurityKey(RSA.Create(2048)) - { - KeyId = CryptoRandom.CreateUniqueId(16) - }; - - return new DefaultKeyStore.State - { - Parameters = key.Rsa.ExportParameters(includePrivateParameters: true) - }; - } + Parameters = key.Rsa.ExportParameters(includePrivateParameters: true) + }; } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserPictureStoreTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserPictureStoreTests.cs index c066053afc..b63cb70246 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserPictureStoreTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserPictureStoreTests.cs @@ -9,42 +9,41 @@ using Squidex.Assets; using Xunit; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public class DefaultUserPictureStoreTests { - public class DefaultUserPictureStoreTests - { - private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); - private readonly DefaultUserPictureStore sut; - private readonly string userId = Guid.NewGuid().ToString(); - private readonly string file; + private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); + private readonly DefaultUserPictureStore sut; + private readonly string userId = Guid.NewGuid().ToString(); + private readonly string file; - public DefaultUserPictureStoreTests() - { - file = $"{userId}_0_picture"; + public DefaultUserPictureStoreTests() + { + file = $"{userId}_0_picture"; - sut = new DefaultUserPictureStore(assetStore); - } + sut = new DefaultUserPictureStore(assetStore); + } - [Fact] - public async Task Should_invoke_asset_store_to_upload_picture_using_suffix_for_compatibility() - { - var stream = new MemoryStream(); + [Fact] + public async Task Should_invoke_asset_store_to_upload_picture_using_suffix_for_compatibility() + { + var stream = new MemoryStream(); - await sut.UploadAsync(userId, stream); + await sut.UploadAsync(userId, stream); - A.CallTo(() => assetStore.UploadAsync(file, stream, true, default)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.UploadAsync(file, stream, true, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_invoke_asset_store_to_download_picture_using_suffix_for_compatibility() - { - var stream = new MemoryStream(); + [Fact] + public async Task Should_invoke_asset_store_to_download_picture_using_suffix_for_compatibility() + { + var stream = new MemoryStream(); - await sut.DownloadAsync(userId, stream); + await sut.DownloadAsync(userId, stream); - A.CallTo(() => assetStore.DownloadAsync(file, stream, default, default)) - .MustHaveHappened(); - } + A.CallTo(() => assetStore.DownloadAsync(file, stream, default, default)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs index b6693b0e57..4a9fc93660 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs @@ -11,182 +11,181 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public class DefaultUserResolverTests { - public class DefaultUserResolverTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IUserService userService = A.Fake<IUserService>(); - private readonly DefaultUserResolver sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IUserService userService = A.Fake<IUserService>(); + private readonly DefaultUserResolver sut; - public DefaultUserResolverTests() - { - ct = cts.Token; + public DefaultUserResolverTests() + { + ct = cts.Token; - var serviceProvider = A.Fake<IServiceProvider>(); + var serviceProvider = A.Fake<IServiceProvider>(); - var scopeObject = A.Fake<IServiceScope>(); - var scopeFactory = A.Fake<IServiceScopeFactory>(); + var scopeObject = A.Fake<IServiceScope>(); + var scopeFactory = A.Fake<IServiceScopeFactory>(); - A.CallTo(() => scopeFactory.CreateScope()) - .Returns(scopeObject); + A.CallTo(() => scopeFactory.CreateScope()) + .Returns(scopeObject); - A.CallTo(() => scopeObject.ServiceProvider) - .Returns(serviceProvider); + A.CallTo(() => scopeObject.ServiceProvider) + .Returns(serviceProvider); - A.CallTo(() => serviceProvider.GetService(typeof(IServiceScopeFactory))) - .Returns(scopeFactory); + A.CallTo(() => serviceProvider.GetService(typeof(IServiceScopeFactory))) + .Returns(scopeFactory); - A.CallTo(() => serviceProvider.GetService(typeof(IUserService))) - .Returns(userService); + A.CallTo(() => serviceProvider.GetService(typeof(IUserService))) + .Returns(userService); - sut = new DefaultUserResolver(serviceProvider); - } + sut = new DefaultUserResolver(serviceProvider); + } - [Fact] - public async Task Should_create_user_and_return_true_if_created() - { - var email = "123@email.com"; + [Fact] + public async Task Should_create_user_and_return_true_if_created() + { + var email = "123@email.com"; - var user = A.Fake<IUser>(); + var user = A.Fake<IUser>(); - A.CallTo(() => userService.CreateAsync(email, A<UserValues>.That.Matches(x => x.Invited == true), false, ct)) - .Returns(user); + A.CallTo(() => userService.CreateAsync(email, A<UserValues>.That.Matches(x => x.Invited == true), false, ct)) + .Returns(user); - var actual = await sut.CreateUserIfNotExistsAsync(email, true, ct); + var actual = await sut.CreateUserIfNotExistsAsync(email, true, ct); - Assert.Equal((user, true), actual); - } + Assert.Equal((user, true), actual); + } - [Fact] - public async Task Should_create_user_and_return_false_if_exception_thrown() - { - var email = "123@email.com"; + [Fact] + public async Task Should_create_user_and_return_false_if_exception_thrown() + { + var email = "123@email.com"; - var user = A.Fake<IUser>(); + var user = A.Fake<IUser>(); - A.CallTo(() => userService.CreateAsync(email, A<UserValues>._, false, ct)) - .Throws(new InvalidOperationException()); + A.CallTo(() => userService.CreateAsync(email, A<UserValues>._, false, ct)) + .Throws(new InvalidOperationException()); - A.CallTo(() => userService.FindByEmailAsync(email, ct)) - .Returns(user); + A.CallTo(() => userService.FindByEmailAsync(email, ct)) + .Returns(user); - var actual = await sut.CreateUserIfNotExistsAsync(email, true, ct); + var actual = await sut.CreateUserIfNotExistsAsync(email, true, ct); - Assert.Equal((user, false), actual); - } + Assert.Equal((user, false), actual); + } - [Fact] - public async Task Should_add_claim_if_not_added_yet() - { - var id = "123"; + [Fact] + public async Task Should_add_claim_if_not_added_yet() + { + var id = "123"; - await sut.SetClaimAsync(id, "my-claim", "my-value", false, ct); + await sut.SetClaimAsync(id, "my-claim", "my-value", false, ct); - A.CallTo(() => userService.UpdateAsync(id, - A<UserValues>.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")), false, ct)) - .MustHaveHappened(); - } + A.CallTo(() => userService.UpdateAsync(id, + A<UserValues>.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")), false, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_add_claim_if_not_added_yet_silently() - { - var id = "123"; + [Fact] + public async Task Should_add_claim_if_not_added_yet_silently() + { + var id = "123"; - await sut.SetClaimAsync(id, "my-claim", "my-value", true, ct); + await sut.SetClaimAsync(id, "my-claim", "my-value", true, ct); - A.CallTo(() => userService.UpdateAsync(id, - A<UserValues>.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")), true, ct)) - .MustHaveHappened(); - } + A.CallTo(() => userService.UpdateAsync(id, + A<UserValues>.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")), true, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_resolve_user_by_email() - { - var id = "123@email.com"; + [Fact] + public async Task Should_resolve_user_by_email() + { + var id = "123@email.com"; - var user = A.Fake<IUser>(); + var user = A.Fake<IUser>(); - A.CallTo(() => userService.FindByEmailAsync(id, ct)) - .Returns(user); + A.CallTo(() => userService.FindByEmailAsync(id, ct)) + .Returns(user); - var actual = await sut.FindByIdOrEmailAsync(id, ct); + var actual = await sut.FindByIdOrEmailAsync(id, ct); - Assert.Equal(user, actual); - } + Assert.Equal(user, actual); + } - [Fact] - public async Task Should_resolve_user_by_id() - { - var id = "123"; + [Fact] + public async Task Should_resolve_user_by_id() + { + var id = "123"; - var user = A.Fake<IUser>(); + var user = A.Fake<IUser>(); - A.CallTo(() => userService.FindByIdAsync(id, ct)) - .Returns(user); + A.CallTo(() => userService.FindByIdAsync(id, ct)) + .Returns(user); - var actual = await sut.FindByIdOrEmailAsync(id, ct); + var actual = await sut.FindByIdOrEmailAsync(id, ct); - Assert.Equal(user, actual); - } + Assert.Equal(user, actual); + } - [Fact] - public async Task Should_resolve_user_by_id_only() - { - var id = "123"; + [Fact] + public async Task Should_resolve_user_by_id_only() + { + var id = "123"; - var user = A.Fake<IUser>(); + var user = A.Fake<IUser>(); - A.CallTo(() => userService.FindByIdAsync(id, ct)) - .Returns(user); + A.CallTo(() => userService.FindByIdAsync(id, ct)) + .Returns(user); - var actual = await sut.FindByIdOrEmailAsync(id, ct); + var actual = await sut.FindByIdOrEmailAsync(id, ct); - Assert.Equal(user, actual); - } + Assert.Equal(user, actual); + } - [Fact] - public async Task Should_query_many_by_email() - { - var email = "hello@squidex.io"; + [Fact] + public async Task Should_query_many_by_email() + { + var email = "hello@squidex.io"; - var users = ResultList.CreateFrom(0, A.Fake<IUser>()); + var users = ResultList.CreateFrom(0, A.Fake<IUser>()); - A.CallTo(() => userService.QueryAsync(email, 10, 0, ct)) - .Returns(users); + A.CallTo(() => userService.QueryAsync(email, 10, 0, ct)) + .Returns(users); - var actual = await sut.QueryByEmailAsync(email, ct); + var actual = await sut.QueryByEmailAsync(email, ct); - Assert.Single(actual); - } + Assert.Single(actual); + } - [Fact] - public async Task Should_query_by_ids() - { - var ids = new[] { "1", "2" }; + [Fact] + public async Task Should_query_by_ids() + { + var ids = new[] { "1", "2" }; - var users = ResultList.CreateFrom(0, A.Fake<IUser>()); + var users = ResultList.CreateFrom(0, A.Fake<IUser>()); - A.CallTo(() => userService.QueryAsync(ids, ct)) - .Returns(users); + A.CallTo(() => userService.QueryAsync(ids, ct)) + .Returns(users); - var actual = await sut.QueryManyAsync(ids, ct); + var actual = await sut.QueryManyAsync(ids, ct); - Assert.Single(actual); - } + Assert.Single(actual); + } - [Fact] - public async Task Should_query_all() - { - var users = ResultList.CreateFrom(0, A.Fake<IUser>()); + [Fact] + public async Task Should_query_all() + { + var users = ResultList.CreateFrom(0, A.Fake<IUser>()); - A.CallTo(() => userService.QueryAsync(null, int.MaxValue, 0, ct)) - .Returns(users); + A.CallTo(() => userService.QueryAsync(null, int.MaxValue, 0, ct)) + .Returns(users); - var actual = await sut.QueryAllAsync(ct); + var actual = await sut.QueryAllAsync(ct); - Assert.Single(actual); - } + Assert.Single(actual); } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs index 1fe1968f55..b9804b3bff 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs @@ -16,604 +16,603 @@ using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public class DefaultUserServiceTests { - public class DefaultUserServiceTests + private readonly UserManager<IdentityUser> userManager = A.Fake<UserManager<IdentityUser>>(); + private readonly IUserFactory userFactory = A.Fake<IUserFactory>(); + private readonly IUserEvents userEvents = A.Fake<IUserEvents>(); + private readonly DefaultUserService sut; + + public DefaultUserServiceTests() { - private readonly UserManager<IdentityUser> userManager = A.Fake<UserManager<IdentityUser>>(); - private readonly IUserFactory userFactory = A.Fake<IUserFactory>(); - private readonly IUserEvents userEvents = A.Fake<IUserEvents>(); - private readonly DefaultUserService sut; + A.CallTo(() => userFactory.IsId(A<string>._)) + .Returns(true); - public DefaultUserServiceTests() - { - A.CallTo(() => userFactory.IsId(A<string>._)) - .Returns(true); + A.CallTo(userManager).WithReturnType<Task<IdentityResult>>() + .Returns(IdentityResult.Success); - A.CallTo(userManager).WithReturnType<Task<IdentityResult>>() - .Returns(IdentityResult.Success); + var log = A.Fake<ILogger<DefaultUserService>>(); - var log = A.Fake<ILogger<DefaultUserService>>(); + sut = new DefaultUserService(userManager, userFactory, Enumerable.Repeat(userEvents, 1), log); + } - sut = new DefaultUserService(userManager, userFactory, Enumerable.Repeat(userEvents, 1), log); - } + [Fact] + public async Task Should_not_resolve_identity_if_id_not_valid() + { + var invalidId = "__"; - [Fact] - public async Task Should_not_resolve_identity_if_id_not_valid() - { - var invalidId = "__"; + A.CallTo(() => userFactory.IsId(invalidId)) + .Returns(false); - A.CallTo(() => userFactory.IsId(invalidId)) - .Returns(false); + var actual = await sut.FindByIdAsync(invalidId); - var actual = await sut.FindByIdAsync(invalidId); + Assert.Null(actual); - Assert.Null(actual); + A.CallTo(() => userManager.FindByIdAsync(invalidId)) + .MustNotHaveHappened(); + } - A.CallTo(() => userManager.FindByIdAsync(invalidId)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_return_identity_by_id_if_found() + { + var identity = CreateIdentity(found: true); - [Fact] - public async Task Should_return_identity_by_id_if_found() - { - var identity = CreateIdentity(found: true); + var actual = await sut.FindByIdAsync(identity.Id); - var actual = await sut.FindByIdAsync(identity.Id); + Assert.Same(identity, actual?.Identity); + } - Assert.Same(identity, actual?.Identity); - } + [Fact] + public async Task Should_return_null_if_identity_by_id_not_found() + { + var identity = CreateIdentity(found: false); - [Fact] - public async Task Should_return_null_if_identity_by_id_not_found() - { - var identity = CreateIdentity(found: false); + var actual = await sut.FindByIdAsync(identity.Id); - var actual = await sut.FindByIdAsync(identity.Id); + Assert.Null(actual); + } - Assert.Null(actual); - } + [Fact] + public async Task Should_return_identity_by_email_if_found() + { + var identity = CreateIdentity(found: true); - [Fact] - public async Task Should_return_identity_by_email_if_found() - { - var identity = CreateIdentity(found: true); + var actual = await sut.FindByEmailAsync(identity.Email); - var actual = await sut.FindByEmailAsync(identity.Email); + Assert.Same(identity, actual?.Identity); + } - Assert.Same(identity, actual?.Identity); - } + [Fact] + public async Task Should_return_null_if_identity_by_email_not_found() + { + var identity = CreateIdentity(found: false); - [Fact] - public async Task Should_return_null_if_identity_by_email_not_found() - { - var identity = CreateIdentity(found: false); + var actual = await sut.FindByEmailAsync(identity.Email); - var actual = await sut.FindByEmailAsync(identity.Email); + Assert.Null(actual); + } - Assert.Null(actual); - } + [Fact] + public async Task Should_return_identity_by_login_if_found() + { + var provider = "my-provider"; + var providerKey = "key"; - [Fact] - public async Task Should_return_identity_by_login_if_found() - { - var provider = "my-provider"; - var providerKey = "key"; + var identity = CreateIdentity(found: true); - var identity = CreateIdentity(found: true); + A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) + .Returns(identity); - A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) - .Returns(identity); + var actual = await sut.FindByLoginAsync(provider, providerKey); - var actual = await sut.FindByLoginAsync(provider, providerKey); + Assert.Same(identity, actual?.Identity); + } - Assert.Same(identity, actual?.Identity); - } + [Fact] + public async Task Should_return_null_if_identity_by_login_not_found() + { + var provider = "my-provider"; + var providerKey = "key"; - [Fact] - public async Task Should_return_null_if_identity_by_login_not_found() - { - var provider = "my-provider"; - var providerKey = "key"; + CreateIdentity(found: false); - CreateIdentity(found: false); + A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) + .Returns(Task.FromResult<IdentityUser>(null!)); - A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) - .Returns(Task.FromResult<IdentityUser>(null!)); + var actual = await sut.FindByLoginAsync(provider, providerKey); - var actual = await sut.FindByLoginAsync(provider, providerKey); + Assert.Null(actual); + } - Assert.Null(actual); - } + [Fact] + public async Task Should_provide_password_existence() + { + var identity = CreateIdentity(found: true); - [Fact] - public async Task Should_provide_password_existence() - { - var identity = CreateIdentity(found: true); + var user = A.Fake<IUser>(); - var user = A.Fake<IUser>(); + A.CallTo(() => user.Identity) + .Returns(identity); - A.CallTo(() => user.Identity) - .Returns(identity); + A.CallTo(() => userManager.HasPasswordAsync(identity)) + .Returns(true); - A.CallTo(() => userManager.HasPasswordAsync(identity)) - .Returns(true); + var actual = await sut.HasPasswordAsync(user); - var actual = await sut.HasPasswordAsync(user); + Assert.True(actual); + } - Assert.True(actual); - } + [Fact] + public async Task Should_provide_logins() + { + var logins = new List<UserLoginInfo>(); - [Fact] - public async Task Should_provide_logins() - { - var logins = new List<UserLoginInfo>(); + var identity = CreateIdentity(found: true); - var identity = CreateIdentity(found: true); + var user = A.Fake<IUser>(); - var user = A.Fake<IUser>(); + A.CallTo(() => user.Identity) + .Returns(identity); - A.CallTo(() => user.Identity) - .Returns(identity); + A.CallTo(() => userManager.GetLoginsAsync(identity)) + .Returns(logins); - A.CallTo(() => userManager.GetLoginsAsync(identity)) - .Returns(logins); + var actual = await sut.GetLoginsAsync(user); - var actual = await sut.GetLoginsAsync(user); + Assert.Same(logins, actual); + } - Assert.Same(logins, actual); - } + [Fact] + public async Task Create_should_add_user() + { + var identity = CreateIdentity(found: false); - [Fact] - public async Task Create_should_add_user() + var values = new UserValues { - var identity = CreateIdentity(found: false); + Email = identity.Email + }; - var values = new UserValues - { - Email = identity.Email - }; + SetupCreation(identity, 1); - SetupCreation(identity, 1); + await sut.CreateAsync(values.Email, values); - await sut.CreateAsync(values.Email, values); + A.CallTo(() => userEvents.OnUserRegisteredAsync(A<IUser>.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); - A.CallTo(() => userEvents.OnUserRegisteredAsync(A<IUser>.That.Matches(x => x.Identity == identity))) - .MustHaveHappened(); + A.CallTo(() => userEvents.OnConsentGivenAsync(A<IUser>.That.Matches(x => x.Identity == identity))) + .MustNotHaveHappened(); - A.CallTo(() => userEvents.OnConsentGivenAsync(A<IUser>.That.Matches(x => x.Identity == identity))) - .MustNotHaveHappened(); + A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions))) + .MustNotHaveHappened(); - A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions))) - .MustNotHaveHappened(); + A.CallTo(() => userManager.AddPasswordAsync(identity, A<string>._)) + .MustNotHaveHappened(); - A.CallTo(() => userManager.AddPasswordAsync(identity, A<string>._)) - .MustNotHaveHappened(); + A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A<DateTimeOffset>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A<DateTimeOffset>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Create_should_raise_event_if_consent_given() + { + var identity = CreateIdentity(found: false); - [Fact] - public async Task Create_should_raise_event_if_consent_given() + var values = new UserValues { - var identity = CreateIdentity(found: false); + Consent = true + }; - var values = new UserValues - { - Consent = true - }; + SetupCreation(identity, 1); - SetupCreation(identity, 1); + await sut.CreateAsync(identity.Email, values); - await sut.CreateAsync(identity.Email, values); + A.CallTo(() => userEvents.OnConsentGivenAsync(A<IUser>.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } - A.CallTo(() => userEvents.OnConsentGivenAsync(A<IUser>.That.Matches(x => x.Identity == identity))) - .MustHaveHappened(); - } + [Fact] + public async Task Create_should_set_admin_if_first_user() + { + var identity = CreateIdentity(found: false); - [Fact] - public async Task Create_should_set_admin_if_first_user() + var values = new UserValues { - var identity = CreateIdentity(found: false); + Consent = true + }; - var values = new UserValues - { - Consent = true - }; + SetupCreation(identity, 0); - SetupCreation(identity, 0); + await sut.CreateAsync(identity.Email, values); - await sut.CreateAsync(identity.Email, values); + A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions, PermissionIds.Admin))) + .MustHaveHappened(); + } - A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions, PermissionIds.Admin))) - .MustHaveHappened(); - } + [Fact] + public async Task Create_should_not_lock_first_user() + { + var identity = CreateIdentity(found: false); - [Fact] - public async Task Create_should_not_lock_first_user() + var values = new UserValues { - var identity = CreateIdentity(found: false); + Consent = true + }; - var values = new UserValues - { - Consent = true - }; + SetupCreation(identity, 0); - SetupCreation(identity, 0); + await sut.CreateAsync(identity.Email, values, true); - await sut.CreateAsync(identity.Email, values, true); + A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A<DateTimeOffset>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A<DateTimeOffset>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Create_should_lock_second_user() + { + var identity = CreateIdentity(found: false); - [Fact] - public async Task Create_should_lock_second_user() + var values = new UserValues { - var identity = CreateIdentity(found: false); + Consent = true + }; - var values = new UserValues - { - Consent = true - }; + SetupCreation(identity, 1); - SetupCreation(identity, 1); + await sut.CreateAsync(identity.Email, values, true); - await sut.CreateAsync(identity.Email, values, true); + A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) + .MustHaveHappened(); + } - A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) - .MustHaveHappened(); - } + [Fact] + public async Task Create_should_add_password() + { + var identity = CreateIdentity(found: false); - [Fact] - public async Task Create_should_add_password() + var values = new UserValues { - var identity = CreateIdentity(found: false); + Password = "password" + }; - var values = new UserValues - { - Password = "password" - }; + SetupCreation(identity, 1); - SetupCreation(identity, 1); + await sut.CreateAsync(identity.Email, values, false); - await sut.CreateAsync(identity.Email, values, false); - - A.CallTo(() => userManager.AddPasswordAsync(identity, values.Password)) - .MustHaveHappened(); - } + A.CallTo(() => userManager.AddPasswordAsync(identity, values.Password)) + .MustHaveHappened(); + } - [Fact] - public async Task Update_should_throw_exception_if_not_found() + [Fact] + public async Task Update_should_throw_exception_if_not_found() + { + var update = new UserValues { - var update = new UserValues - { - Email = "new@email.com" - }; + Email = "new@email.com" + }; - var identity = CreateIdentity(found: false); + var identity = CreateIdentity(found: false); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.UpdateAsync(identity.Id, update)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.UpdateAsync(identity.Id, update)); + } - [Fact] - public async Task Update_should_not_invoke_events_if_silent() - { - var update = new UserValues(); + [Fact] + public async Task Update_should_not_invoke_events_if_silent() + { + var update = new UserValues(); - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - await sut.UpdateAsync(identity.Id, update, true); + await sut.UpdateAsync(identity.Id, update, true); - A.CallTo(() => userEvents.OnUserUpdatedAsync(A<IUser>.That.Matches(x => x.Identity == identity), A<IUser>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userEvents.OnUserUpdatedAsync(A<IUser>.That.Matches(x => x.Identity == identity), A<IUser>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Update_should_do_nothing_for_new_update() - { - var update = new UserValues(); + [Fact] + public async Task Update_should_do_nothing_for_new_update() + { + var update = new UserValues(); - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - await sut.UpdateAsync(identity.Id, update); + await sut.UpdateAsync(identity.Id, update); - A.CallTo(() => userEvents.OnUserUpdatedAsync(A<IUser>.That.Matches(x => x.Identity == identity), A<IUser>._)) - .MustHaveHappened(); - } + A.CallTo(() => userEvents.OnUserUpdatedAsync(A<IUser>.That.Matches(x => x.Identity == identity), A<IUser>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Update_should_change_password_if_changed() + [Fact] + public async Task Update_should_change_password_if_changed() + { + var update = new UserValues { - var update = new UserValues - { - Password = "password" - }; + Password = "password" + }; - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - A.CallTo(() => userManager.HasPasswordAsync(identity)) - .Returns(true); + A.CallTo(() => userManager.HasPasswordAsync(identity)) + .Returns(true); - await sut.UpdateAsync(identity.Id, update); + await sut.UpdateAsync(identity.Id, update); - A.CallTo(() => userManager.RemovePasswordAsync(identity)) - .MustHaveHappened(); + A.CallTo(() => userManager.RemovePasswordAsync(identity)) + .MustHaveHappened(); - A.CallTo(() => userManager.AddPasswordAsync(identity, update.Password)) - .MustHaveHappened(); - } + A.CallTo(() => userManager.AddPasswordAsync(identity, update.Password)) + .MustHaveHappened(); + } - [Fact] - public async Task Update_should_change_email_if_changed() + [Fact] + public async Task Update_should_change_email_if_changed() + { + var update = new UserValues { - var update = new UserValues - { - Email = "new@email.com" - }; + Email = "new@email.com" + }; - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - await sut.UpdateAsync(identity.Id, update); + await sut.UpdateAsync(identity.Id, update); - A.CallTo(() => userManager.SetEmailAsync(identity, update.Email)) - .MustHaveHappened(); + A.CallTo(() => userManager.SetEmailAsync(identity, update.Email)) + .MustHaveHappened(); - A.CallTo(() => userManager.SetUserNameAsync(identity, update.Email)) - .MustHaveHappened(); - } + A.CallTo(() => userManager.SetUserNameAsync(identity, update.Email)) + .MustHaveHappened(); + } - [Fact] - public async Task Update_should_set_claim_if_consent_given() + [Fact] + public async Task Update_should_set_claim_if_consent_given() + { + var update = new UserValues { - var update = new UserValues - { - Consent = true - }; + Consent = true + }; - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - await sut.UpdateAsync(identity.Id, update); + await sut.UpdateAsync(identity.Id, update); - A.CallTo<Task<IdentityResult>>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Consent))) - .MustHaveHappened(); + A.CallTo<Task<IdentityResult>>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Consent))) + .MustHaveHappened(); - A.CallTo(() => userEvents.OnConsentGivenAsync(A<IUser>.That.Matches(x => x.Identity == identity))) - .MustHaveHappened(); - } + A.CallTo(() => userEvents.OnConsentGivenAsync(A<IUser>.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } - [Fact] - public async Task Update_should_set_claim_if_email_consent_given() + [Fact] + public async Task Update_should_set_claim_if_email_consent_given() + { + var update = new UserValues { - var update = new UserValues - { - ConsentForEmails = true - }; + ConsentForEmails = true + }; - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - await sut.UpdateAsync(identity.Id, update); + await sut.UpdateAsync(identity.Id, update); - A.CallTo<Task<IdentityResult>>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.ConsentForEmails))) - .MustHaveHappened(); + A.CallTo<Task<IdentityResult>>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.ConsentForEmails))) + .MustHaveHappened(); - A.CallTo(() => userEvents.OnConsentGivenAsync(A<IUser>.That.Matches(x => x.Identity == identity))) - .MustHaveHappened(); - } + A.CallTo(() => userEvents.OnConsentGivenAsync(A<IUser>.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } - [Fact] - public async Task SetPassword_should_throw_exception_if_not_found() - { - var password = "password"; + [Fact] + public async Task SetPassword_should_throw_exception_if_not_found() + { + var password = "password"; - var identity = CreateIdentity(found: false); + var identity = CreateIdentity(found: false); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.SetPasswordAsync(identity.Id, password, null)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.SetPasswordAsync(identity.Id, password, null)); + } - [Fact] - public async Task SetPassword_should_succeed_if_found() - { - var password = "password"; + [Fact] + public async Task SetPassword_should_succeed_if_found() + { + var password = "password"; - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - await sut.SetPasswordAsync(identity.Id, password, null); + await sut.SetPasswordAsync(identity.Id, password, null); - A.CallTo(() => userManager.AddPasswordAsync(identity, password)) - .MustHaveHappened(); - } + A.CallTo(() => userManager.AddPasswordAsync(identity, password)) + .MustHaveHappened(); + } - [Fact] - public async Task SetPassword_should_change_password_if_identity_has_password() - { - var password = "password"; + [Fact] + public async Task SetPassword_should_change_password_if_identity_has_password() + { + var password = "password"; - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - A.CallTo(() => userManager.HasPasswordAsync(identity)) - .Returns(true); + A.CallTo(() => userManager.HasPasswordAsync(identity)) + .Returns(true); - await sut.SetPasswordAsync(identity.Id, password, "old"); + await sut.SetPasswordAsync(identity.Id, password, "old"); - A.CallTo(() => userManager.ChangePasswordAsync(identity, "old", password)) - .MustHaveHappened(); - } + A.CallTo(() => userManager.ChangePasswordAsync(identity, "old", password)) + .MustHaveHappened(); + } - [Fact] - public async Task AddLogin_should_throw_exception_if_not_found() - { - var login = A.Fake<ExternalLoginInfo>(); + [Fact] + public async Task AddLogin_should_throw_exception_if_not_found() + { + var login = A.Fake<ExternalLoginInfo>(); - var identity = CreateIdentity(found: false); + var identity = CreateIdentity(found: false); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.AddLoginAsync(identity.Id, login)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.AddLoginAsync(identity.Id, login)); + } - [Fact] - public async Task AddLogin_should_succeed_if_found() - { - var login = A.Fake<ExternalLoginInfo>(); + [Fact] + public async Task AddLogin_should_succeed_if_found() + { + var login = A.Fake<ExternalLoginInfo>(); - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - await sut.AddLoginAsync(identity.Id, login); + await sut.AddLoginAsync(identity.Id, login); - A.CallTo(() => userManager.AddLoginAsync(identity, login)) - .MustHaveHappened(); - } + A.CallTo(() => userManager.AddLoginAsync(identity, login)) + .MustHaveHappened(); + } - [Fact] - public async Task RemoveLogin_should_throw_exception_if_not_found() - { - var provider = "my-provider"; - var providerKey = "key"; + [Fact] + public async Task RemoveLogin_should_throw_exception_if_not_found() + { + var provider = "my-provider"; + var providerKey = "key"; - var identity = CreateIdentity(found: false); + var identity = CreateIdentity(found: false); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.RemoveLoginAsync(identity.Id, provider, providerKey)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.RemoveLoginAsync(identity.Id, provider, providerKey)); + } - [Fact] - public async Task RemoveLogin_should_succeed_if_found() - { - var provider = "my-provider"; - var providerKey = "key"; + [Fact] + public async Task RemoveLogin_should_succeed_if_found() + { + var provider = "my-provider"; + var providerKey = "key"; - var identity = CreateIdentity(found: true); + var identity = CreateIdentity(found: true); - await sut.RemoveLoginAsync(identity.Id, provider, providerKey); + await sut.RemoveLoginAsync(identity.Id, provider, providerKey); - A.CallTo(() => userManager.RemoveLoginAsync(identity, provider, providerKey)) - .MustHaveHappened(); - } + A.CallTo(() => userManager.RemoveLoginAsync(identity, provider, providerKey)) + .MustHaveHappened(); + } - [Fact] - public async Task Lock_should_throw_exception_if_not_found() - { - var identity = CreateIdentity(found: false); + [Fact] + public async Task Lock_should_throw_exception_if_not_found() + { + var identity = CreateIdentity(found: false); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LockAsync(identity.Id)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LockAsync(identity.Id)); + } - [Fact] - public async Task Lock_should_succeed_if_found() - { - var identity = CreateIdentity(found: true); + [Fact] + public async Task Lock_should_succeed_if_found() + { + var identity = CreateIdentity(found: true); - await sut.LockAsync(identity.Id); + await sut.LockAsync(identity.Id); - A.CallTo<Task<IdentityResult>>(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) - .MustHaveHappened(); - } + A.CallTo<Task<IdentityResult>>(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) + .MustHaveHappened(); + } - [Fact] - public async Task Unlock_should_throw_exception_if_not_found() - { - var identity = CreateIdentity(found: false); + [Fact] + public async Task Unlock_should_throw_exception_if_not_found() + { + var identity = CreateIdentity(found: false); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.UnlockAsync(identity.Id)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.UnlockAsync(identity.Id)); + } - [Fact] - public async Task Unlock_should_succeeed_if_found() - { - var identity = CreateIdentity(found: true); + [Fact] + public async Task Unlock_should_succeeed_if_found() + { + var identity = CreateIdentity(found: true); - await sut.UnlockAsync(identity.Id); + await sut.UnlockAsync(identity.Id); - A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, null)) - .MustHaveHappened(); - } + A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, null)) + .MustHaveHappened(); + } - [Fact] - public async Task Delete_should_throw_exception_if_not_found() - { - var identity = CreateIdentity(found: false); + [Fact] + public async Task Delete_should_throw_exception_if_not_found() + { + var identity = CreateIdentity(found: false); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.DeleteAsync(identity.Id)); + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.DeleteAsync(identity.Id)); - A.CallTo(() => userEvents.OnUserDeletedAsync(A<IUser>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => userEvents.OnUserDeletedAsync(A<IUser>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Delete_should_succeed_if_found() - { - var identity = CreateIdentity(found: true); + [Fact] + public async Task Delete_should_succeed_if_found() + { + var identity = CreateIdentity(found: true); - await sut.DeleteAsync(identity.Id); + await sut.DeleteAsync(identity.Id); - A.CallTo(() => userManager.DeleteAsync(identity)) - .MustHaveHappened(); + A.CallTo(() => userManager.DeleteAsync(identity)) + .MustHaveHappened(); - A.CallTo(() => userEvents.OnUserDeletedAsync(A<IUser>.That.Matches(x => x.Identity == identity))) - .MustHaveHappened(); - } + A.CallTo(() => userEvents.OnUserDeletedAsync(A<IUser>.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } - private IdentityUser CreateIdentity(bool found, string id = "123") - { - var identity = CreatePendingUser(id); - - if (found) - { - A.CallTo(() => userManager.FindByIdAsync(identity.Id)) - .Returns(identity); - - A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) - .Returns(identity); - } - else - { - A.CallTo(() => userManager.FindByIdAsync(identity.Id)) - .Returns(Task.FromResult<IdentityUser>(null!)); - - A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) - .Returns(Task.FromResult<IdentityUser>(null!)); - } - - return identity; - } + private IdentityUser CreateIdentity(bool found, string id = "123") + { + var identity = CreatePendingUser(id); - private void SetupCreation(IdentityUser identity, int numCurrentUsers) + if (found) { - var users = new List<IdentityUser>(); - - for (var i = 0; i < numCurrentUsers; i++) - { - users.Add(CreatePendingUser(i.ToString(CultureInfo.InvariantCulture))); - } - - A.CallTo(() => userManager.Users) - .Returns(users.AsQueryable()); + A.CallTo(() => userManager.FindByIdAsync(identity.Id)) + .Returns(identity); - A.CallTo(() => userFactory.Create(identity.Email)) + A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) .Returns(identity); } - - private static IEnumerable<Claim> HasClaim(string claim) + else { - return A<IEnumerable<Claim>>.That.Matches(x => x.Any(y => y.Type == claim)); - } + A.CallTo(() => userManager.FindByIdAsync(identity.Id)) + .Returns(Task.FromResult<IdentityUser>(null!)); - private static IEnumerable<Claim> HasClaim(string claim, string value) - { - return A<IEnumerable<Claim>>.That.Matches(x => x.Any(y => y.Type == claim && y.Value == value)); + A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) + .Returns(Task.FromResult<IdentityUser>(null!)); } - private static DateTimeOffset InFuture() + return identity; + } + + private void SetupCreation(IdentityUser identity, int numCurrentUsers) + { + var users = new List<IdentityUser>(); + + for (var i = 0; i < numCurrentUsers; i++) { - return A<DateTimeOffset>.That.Matches(x => x >= DateTimeOffset.UtcNow.AddYears(1)); + users.Add(CreatePendingUser(i.ToString(CultureInfo.InvariantCulture))); } - private static IdentityUser CreatePendingUser(string id = "123") + A.CallTo(() => userManager.Users) + .Returns(users.AsQueryable()); + + A.CallTo(() => userFactory.Create(identity.Email)) + .Returns(identity); + } + + private static IEnumerable<Claim> HasClaim(string claim) + { + return A<IEnumerable<Claim>>.That.Matches(x => x.Any(y => y.Type == claim)); + } + + private static IEnumerable<Claim> HasClaim(string claim, string value) + { + return A<IEnumerable<Claim>>.That.Matches(x => x.Any(y => y.Type == claim && y.Value == value)); + } + + private static DateTimeOffset InFuture() + { + return A<DateTimeOffset>.That.Matches(x => x >= DateTimeOffset.UtcNow.AddYears(1)); + } + + private static IdentityUser CreatePendingUser(string id = "123") + { + return new IdentityUser { - return new IdentityUser - { - Id = id, - Email = $"{id}@email.com" - }; - } + Id = id, + Email = $"{id}@email.com" + }; } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs index 5da46e34be..1cc3c37482 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs @@ -11,48 +11,47 @@ using Squidex.Infrastructure.States; using Xunit; -namespace Squidex.Domain.Users +namespace Squidex.Domain.Users; + +public sealed class DefaultXmlRepositoryTests { - public sealed class DefaultXmlRepositoryTests + private readonly ISnapshotStore<DefaultXmlRepository.State> store = A.Fake<ISnapshotStore<DefaultXmlRepository.State>>(); + private readonly DefaultXmlRepository sut; + + public DefaultXmlRepositoryTests() { - private readonly ISnapshotStore<DefaultXmlRepository.State> store = A.Fake<ISnapshotStore<DefaultXmlRepository.State>>(); - private readonly DefaultXmlRepository sut; - - public DefaultXmlRepositoryTests() - { - sut = new DefaultXmlRepository(store); - } - - [Fact] - public void Should_read_from_store() - { - A.CallTo(() => store.ReadAllAsync(default)) - .Returns(new[] + sut = new DefaultXmlRepository(store); + } + + [Fact] + public void Should_read_from_store() + { + A.CallTo(() => store.ReadAllAsync(default)) + .Returns(new[] + { + new SnapshotResult<DefaultXmlRepository.State>(default, new DefaultXmlRepository.State + { + Xml = new XElement("xml").ToString() + }, 0L), + new SnapshotResult<DefaultXmlRepository.State>(default, new DefaultXmlRepository.State { - new SnapshotResult<DefaultXmlRepository.State>(default, new DefaultXmlRepository.State - { - Xml = new XElement("xml").ToString() - }, 0L), - new SnapshotResult<DefaultXmlRepository.State>(default, new DefaultXmlRepository.State - { - Xml = new XElement("xml").ToString() - }, 0L) - }.ToAsyncEnumerable()); - - var xml = sut.GetAllElements(); - - Assert.Equal(2, xml.Count); - } - - [Fact] - public void Should_write_to_store() - { - var xml = new XElement("xml"); - - sut.StoreElement(xml, "name"); - - A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultXmlRepository.State>>.That.Matches(x => x.Key == DomainId.Create("name")), default)) - .MustHaveHappened(); - } + Xml = new XElement("xml").ToString() + }, 0L) + }.ToAsyncEnumerable()); + + var xml = sut.GetAllElements(); + + Assert.Equal(2, xml.Count); + } + + [Fact] + public void Should_write_to_store() + { + var xml = new XElement("xml"); + + sut.StoreElement(xml, "name"); + + A.CallTo(() => store.WriteAsync(A<SnapshotWriteJob<DefaultXmlRepository.State>>.That.Matches(x => x.Key == DomainId.Create("name")), default)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs index a973930980..9fb709722b 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs @@ -11,170 +11,169 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Caching +namespace Squidex.Infrastructure.Caching; + +public class QueryCacheTests { - public class QueryCacheTests + private readonly IMemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + + private record CachedEntry(int Value) : IWithId<int> { - private readonly IMemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + public int Id => Value; + } - private record CachedEntry(int Value) : IWithId<int> - { - public int Id => Value; - } + [Fact] + public async Task Should_query_from_cache() + { + var sut = new QueryCache<int, CachedEntry>(); - [Fact] - public async Task Should_query_from_cache() - { - var sut = new QueryCache<int, CachedEntry>(); + var (queried, actual) = await ConfigureAsync(sut, 1, 2); - var (queried, actual) = await ConfigureAsync(sut, 1, 2); + Assert.Equal(new[] { 1, 2 }, queried); + Assert.Equal(new[] { 1, 2 }, actual); + } - Assert.Equal(new[] { 1, 2 }, queried); - Assert.Equal(new[] { 1, 2 }, actual); - } + [Fact] + public async Task Should_query_pending_from_cache() + { + var sut = new QueryCache<int, CachedEntry>(); - [Fact] - public async Task Should_query_pending_from_cache() - { - var sut = new QueryCache<int, CachedEntry>(); + var (queried1, actual1) = await ConfigureAsync(sut, 1, 2); + var (queried2, actual2) = await ConfigureAsync(sut, 1, 2, 3, 4); - var (queried1, actual1) = await ConfigureAsync(sut, 1, 2); - var (queried2, actual2) = await ConfigureAsync(sut, 1, 2, 3, 4); + Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); + Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); - Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); + Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); + } - Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); - } + [Fact] + public async Task Should_query_pending_from_cache_if_manually_added() + { + var sut = new QueryCache<int, CachedEntry>(); - [Fact] - public async Task Should_query_pending_from_cache_if_manually_added() - { - var sut = new QueryCache<int, CachedEntry>(); + sut.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); - sut.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); + var (queried, actual) = await ConfigureAsync(sut, 1, 2, 3, 4); - var (queried, actual) = await ConfigureAsync(sut, 1, 2, 3, 4); + Assert.Equal(new[] { 3, 4 }, queried); + Assert.Equal(new[] { 2, 3, 4 }, actual); + } - Assert.Equal(new[] { 3, 4 }, queried); - Assert.Equal(new[] { 2, 3, 4 }, actual); - } + [Fact] + public async Task Should_query_pending_from_memory_cache_if_manually_added() + { + var sut1 = new QueryCache<int, CachedEntry>(memoryCache); + var sut2 = new QueryCache<int, CachedEntry>(memoryCache); - [Fact] - public async Task Should_query_pending_from_memory_cache_if_manually_added() - { - var sut1 = new QueryCache<int, CachedEntry>(memoryCache); - var sut2 = new QueryCache<int, CachedEntry>(memoryCache); + var cacheDuration = TimeSpan.FromSeconds(10); - var cacheDuration = TimeSpan.FromSeconds(10); + sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); - sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); + var (queried, actual) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); - var (queried, actual) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); + Assert.Equal(new[] { 3, 4 }, queried); + Assert.Equal(new[] { 2, 3, 4 }, actual); + } - Assert.Equal(new[] { 3, 4 }, queried); - Assert.Equal(new[] { 2, 3, 4 }, actual); - } + [Fact] + public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_added_permanently() + { + var sut1 = new QueryCache<int, CachedEntry>(memoryCache); + var sut2 = new QueryCache<int, CachedEntry>(memoryCache); - [Fact] - public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_added_permanently() - { - var sut1 = new QueryCache<int, CachedEntry>(memoryCache); - var sut2 = new QueryCache<int, CachedEntry>(memoryCache); + sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); - sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); + var (queried, actual) = await ConfigureAsync(sut2, 1, 2, 3, 4); - var (queried, actual) = await ConfigureAsync(sut2, 1, 2, 3, 4); + Assert.Equal(new[] { 1, 2, 3, 4 }, queried); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual); + } - Assert.Equal(new[] { 1, 2, 3, 4 }, queried); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual); - } + [Fact] + public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_queried_permanently() + { + var sut1 = new QueryCache<int, CachedEntry>(memoryCache); + var sut2 = new QueryCache<int, CachedEntry>(memoryCache); - [Fact] - public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_queried_permanently() - { - var sut1 = new QueryCache<int, CachedEntry>(memoryCache); - var sut2 = new QueryCache<int, CachedEntry>(memoryCache); + var cacheDuration = TimeSpan.FromSeconds(10); - var cacheDuration = TimeSpan.FromSeconds(10); + sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); - sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); + var (queried, actual) = await ConfigureAsync(sut2, 1, 2, 3, 4); - var (queried, actual) = await ConfigureAsync(sut2, 1, 2, 3, 4); + Assert.Equal(new[] { 1, 2, 3, 4 }, queried); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual); + } - Assert.Equal(new[] { 1, 2, 3, 4 }, queried); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual); - } + [Fact] + public async Task Should_not_query_again_if_failed_before() + { + var sut = new QueryCache<int, CachedEntry>(); - [Fact] - public async Task Should_not_query_again_if_failed_before() - { - var sut = new QueryCache<int, CachedEntry>(); + var (queried1, actual1) = await ConfigureAsync(sut, x => x > 1, default, 1, 2); + var (queried2, actual2) = await ConfigureAsync(sut, 1, 2, 3, 4); - var (queried1, actual1) = await ConfigureAsync(sut, x => x > 1, default, 1, 2); - var (queried2, actual2) = await ConfigureAsync(sut, 1, 2, 3, 4); + Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); + Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); - Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); + Assert.Equal(new[] { 2 }, actual1.ToArray()); + Assert.Equal(new[] { 2, 3, 4 }, actual2.ToArray()); + } - Assert.Equal(new[] { 2 }, actual1.ToArray()); - Assert.Equal(new[] { 2, 3, 4 }, actual2.ToArray()); - } + [Fact] + public async Task Should_query_from_memory_cache() + { + var sut1 = new QueryCache<int, CachedEntry>(memoryCache); + var sut2 = new QueryCache<int, CachedEntry>(memoryCache); - [Fact] - public async Task Should_query_from_memory_cache() - { - var sut1 = new QueryCache<int, CachedEntry>(memoryCache); - var sut2 = new QueryCache<int, CachedEntry>(memoryCache); + var cacheDuration = TimeSpan.FromSeconds(10); - var cacheDuration = TimeSpan.FromSeconds(10); + var (queried1, actual1) = await ConfigureAsync(sut1, x => true, cacheDuration, 1, 2); + var (queried2, actual2) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); - var (queried1, actual1) = await ConfigureAsync(sut1, x => true, cacheDuration, 1, 2); - var (queried2, actual2) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); + Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); + Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); - Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); + Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); + } - Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); - } + [Fact] + public async Task Should_not_query_from_memory_cache_if_not_queried_permanently() + { + var sut1 = new QueryCache<int, CachedEntry>(memoryCache); + var sut2 = new QueryCache<int, CachedEntry>(memoryCache); - [Fact] - public async Task Should_not_query_from_memory_cache_if_not_queried_permanently() - { - var sut1 = new QueryCache<int, CachedEntry>(memoryCache); - var sut2 = new QueryCache<int, CachedEntry>(memoryCache); + var (queried1, actual1) = await ConfigureAsync(sut1, x => true, null, 1, 2); + var (queried2, actual2) = await ConfigureAsync(sut2, x => true, null, 1, 2, 3, 4); - var (queried1, actual1) = await ConfigureAsync(sut1, x => true, null, 1, 2); - var (queried2, actual2) = await ConfigureAsync(sut2, x => true, null, 1, 2, 3, 4); + Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); + Assert.Equal(new[] { 1, 2, 3, 4 }, queried2.ToArray()); - Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, queried2.ToArray()); + Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); + Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); + } - Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); - } + private static Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, params int[] ids) + { + return ConfigureAsync(sut, x => true, null, ids); + } - private static Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, params int[] ids) - { - return ConfigureAsync(sut, x => true, null, ids); - } + private static async Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, Func<int, bool> predicate, TimeSpan? cacheDuration, params int[] ids) + { + var queried = new HashSet<int>(); - private static async Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, Func<int, bool> predicate, TimeSpan? cacheDuration, params int[] ids) + var actual = await sut.CacheOrQueryAsync(ids, async pending => { - var queried = new HashSet<int>(); - - var actual = await sut.CacheOrQueryAsync(ids, async pending => - { - queried.AddRange(pending); + queried.AddRange(pending); - await Task.Yield(); + await Task.Yield(); - return pending.Where(predicate).Select(x => new CachedEntry(x)); - }, cacheDuration); + return pending.Where(predicate).Select(x => new CachedEntry(x)); + }, cacheDuration); - return (queried.ToArray(), actual.Select(x => x.Value).ToArray()); - } + return (queried.ToArray(), actual.Select(x => x.Value).ToArray()); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs index f66f90efe6..9a9e3526dd 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs @@ -7,332 +7,331 @@ using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class CollectionExtensionsTests { - public class CollectionExtensionsTests + private readonly Dictionary<int, int> valueDictionary = new Dictionary<int, int>(); + private readonly Dictionary<int, List<int>> listDictionary = new Dictionary<int, List<int>>(); + + [Fact] + public void SetEquals_should_return_false_if_subset() { - private readonly Dictionary<int, int> valueDictionary = new Dictionary<int, int>(); - private readonly Dictionary<int, List<int>> listDictionary = new Dictionary<int, List<int>>(); + var set1 = new[] { 1, 2 }; + var set2 = new[] { 1, 2, 3 }; - [Fact] - public void SetEquals_should_return_false_if_subset() - { - var set1 = new[] { 1, 2 }; - var set2 = new[] { 1, 2, 3 }; + Assert.False(set1.SetEquals(set2)); + Assert.False(set2.SetEquals(set1)); + } - Assert.False(set1.SetEquals(set2)); - Assert.False(set2.SetEquals(set1)); - } + [Fact] + public void SetEquals_should_return_true_for_same_items_in_different_order() + { + var set1 = new[] { 1, 2, 3 }; + var set2 = new[] { 3, 2, 1 }; - [Fact] - public void SetEquals_should_return_true_for_same_items_in_different_order() + Assert.True(set1.SetEquals(set2)); + Assert.True(set2.SetEquals(set1)); + } + + [Fact] + public void IndexOf_should_return_index_if_found() + { + var source = new List<(int Value, int Other)> { - var set1 = new[] { 1, 2, 3 }; - var set2 = new[] { 3, 2, 1 }; + (5, 5), + (4, 4) + }; - Assert.True(set1.SetEquals(set2)); - Assert.True(set2.SetEquals(set1)); - } + var index = source.IndexOf(x => x.Other == 4); - [Fact] - public void IndexOf_should_return_index_if_found() + Assert.Equal(1, index); + } + + [Fact] + public void IndexOf_should_return_negative_value_if_not_found() + { + var source = new List<(int Value, int Other)> { - var source = new List<(int Value, int Other)> - { - (5, 5), - (4, 4) - }; + (5, 5), + (4, 4) + }; - var index = source.IndexOf(x => x.Other == 4); + var index = source.IndexOf(x => x.Other == 2); - Assert.Equal(1, index); - } + Assert.Equal(-1, index); + } - [Fact] - public void IndexOf_should_return_negative_value_if_not_found() - { - var source = new List<(int Value, int Other)> - { - (5, 5), - (4, 4) - }; + [Fact] + public void GetOrAddDefault_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; - var index = source.IndexOf(x => x.Other == 2); + Assert.Equal(34, valueDictionary.GetOrAddDefault(12)); + } - Assert.Equal(-1, index); - } + [Fact] + public void GetOrAddDefault_should_return_default_and_add_it_if_key_not_exists() + { + Assert.Equal(0, valueDictionary.GetOrAddDefault(12)); + Assert.Equal(0, valueDictionary[12]); + } - [Fact] - public void GetOrAddDefault_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; + [Fact] + public void GetOrCreate_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; - Assert.Equal(34, valueDictionary.GetOrAddDefault(12)); - } + Assert.Equal(34, valueDictionary.GetOrCreate(12, x => 34)); + } - [Fact] - public void GetOrAddDefault_should_return_default_and_add_it_if_key_not_exists() - { - Assert.Equal(0, valueDictionary.GetOrAddDefault(12)); - Assert.Equal(0, valueDictionary[12]); - } + [Fact] + public void GetOrCreate_should_return_default_but_not_add_it_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrCreate(12, x => 24)); + Assert.False(valueDictionary.ContainsKey(12)); + } - [Fact] - public void GetOrCreate_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; + [Fact] + public void GetOrAdd_should_return_value_if_key_exists() + { + valueDictionary[12] = 34; - Assert.Equal(34, valueDictionary.GetOrCreate(12, x => 34)); - } + Assert.Equal(34, valueDictionary.GetOrAdd(12, x => 44)); + } - [Fact] - public void GetOrCreate_should_return_default_but_not_add_it_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrCreate(12, x => 24)); - Assert.False(valueDictionary.ContainsKey(12)); - } + [Fact] + public void GetOrAdd_should_return_default_and_add_it_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrAdd(12, 24)); + Assert.Equal(24, valueDictionary[12]); + } - [Fact] - public void GetOrAdd_should_return_value_if_key_exists() - { - valueDictionary[12] = 34; + [Fact] + public void GetOrAdd_should_return_default_and_add_it_with_fallback_if_key_not_exists() + { + Assert.Equal(24, valueDictionary.GetOrAdd(12, x => 24)); + Assert.Equal(24, valueDictionary[12]); + } - Assert.Equal(34, valueDictionary.GetOrAdd(12, x => 44)); - } + [Fact] + public void GetOrAddNew_should_return_value_if_key_exists() + { + var list = new List<int>(); + listDictionary[12] = list; - [Fact] - public void GetOrAdd_should_return_default_and_add_it_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrAdd(12, 24)); - Assert.Equal(24, valueDictionary[12]); - } + Assert.Equal(list, listDictionary.GetOrAddNew(12)); + } - [Fact] - public void GetOrAdd_should_return_default_and_add_it_with_fallback_if_key_not_exists() - { - Assert.Equal(24, valueDictionary.GetOrAdd(12, x => 24)); - Assert.Equal(24, valueDictionary[12]); - } + [Fact] + public void GetOrAddNew_should_return_default_but_not_add_it_if_key_not_exists() + { + var list = new List<int>(); - [Fact] - public void GetOrAddNew_should_return_value_if_key_exists() - { - var list = new List<int>(); - listDictionary[12] = list; + Assert.Equal(list, listDictionary.GetOrAddNew(12)); + Assert.Equal(list, listDictionary[12]); + } - Assert.Equal(list, listDictionary.GetOrAddNew(12)); - } + [Fact] + public void SequentialHashCode_should_ignore_null_values() + { + var collection = new string?[] { null, null }; - [Fact] - public void GetOrAddNew_should_return_default_but_not_add_it_if_key_not_exists() - { - var list = new List<int>(); + Assert.Equal(17, collection.SequentialHashCode()); + } - Assert.Equal(list, listDictionary.GetOrAddNew(12)); - Assert.Equal(list, listDictionary[12]); - } + [Fact] + public void SequentialHashCode_should_return_same_hash_codes_for_list_with_same_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 5, 6 }; - [Fact] - public void SequentialHashCode_should_ignore_null_values() + Assert.Equal(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_items() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 3, 4, 1 }; + + Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_order() + { + var collection1 = new[] { 3, 5, 6 }; + var collection2 = new[] { 6, 5, 3 }; + + Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); + } + + [Fact] + public void EqualsDictionary_should_return_true_for_equal_dictionaries() + { + var lhs = new Dictionary<int, int> + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary<int, int> { - var collection = new string?[] { null, null }; + [1] = 1, + [2] = 2 + }; - Assert.Equal(17, collection.SequentialHashCode()); - } + Assert.True(lhs.EqualsDictionary(rhs)); + } - [Fact] - public void SequentialHashCode_should_return_same_hash_codes_for_list_with_same_order() + [Fact] + public void EqualsDictionary_should_return_false_for_different_sizes() + { + var lhs = new Dictionary<int, int> + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary<int, int> { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 5, 6 }; + [1] = 1 + }; - Assert.Equal(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } + Assert.False(lhs.EqualsDictionary(rhs)); + } - [Fact] - public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_items() + [Fact] + public void EqualsDictionary_should_return_false_for_different_values() + { + var lhs = new Dictionary<int, int> + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary<int, int> { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 3, 4, 1 }; + [1] = 1, + [3] = 3 + }; - Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } + Assert.False(lhs.EqualsDictionary(rhs)); + } - [Fact] - public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_order() + [Fact] + public void Dictionary_should_return_same_hashcode_for_equal_dictionaries() + { + var lhs = new Dictionary<int, int> + { + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary<int, int> { - var collection1 = new[] { 3, 5, 6 }; - var collection2 = new[] { 6, 5, 3 }; + [1] = 1, + [2] = 2 + }; - Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode()); - } + Assert.Equal(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); + } - [Fact] - public void EqualsDictionary_should_return_true_for_equal_dictionaries() + [Fact] + public void Dictionary_should_return_different_hashcode_for_different_dictionaries() + { + var lhs = new Dictionary<int, int> { - var lhs = new Dictionary<int, int> - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary<int, int> - { - [1] = 1, - [2] = 2 - }; - - Assert.True(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void EqualsDictionary_should_return_false_for_different_sizes() + [1] = 1, + [2] = 2 + }; + var rhs = new Dictionary<int, int> { - var lhs = new Dictionary<int, int> - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary<int, int> - { - [1] = 1 - }; - - Assert.False(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void EqualsDictionary_should_return_false_for_different_values() + [1] = 1, + [3] = 3 + }; + + Assert.NotEqual(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); + } + + [Fact] + public void EqualsList_should_return_true_for_equal_lists() + { + var lhs = new List<int> { - var lhs = new Dictionary<int, int> - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary<int, int> - { - [1] = 1, - [3] = 3 - }; - - Assert.False(lhs.EqualsDictionary(rhs)); - } - - [Fact] - public void Dictionary_should_return_same_hashcode_for_equal_dictionaries() + 1, + 2 + }; + var rhs = new List<int> { - var lhs = new Dictionary<int, int> - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary<int, int> - { - [1] = 1, - [2] = 2 - }; - - Assert.Equal(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); - } - - [Fact] - public void Dictionary_should_return_different_hashcode_for_different_dictionaries() + 1, + 2 + }; + + Assert.True(lhs.EqualsList(rhs)); + } + + [Fact] + public void EqualsList_should_return_false_for_different_sizes() + { + var lhs = new List<int> { - var lhs = new Dictionary<int, int> - { - [1] = 1, - [2] = 2 - }; - var rhs = new Dictionary<int, int> - { - [1] = 1, - [3] = 3 - }; - - Assert.NotEqual(lhs.DictionaryHashCode(), rhs.DictionaryHashCode()); - } - - [Fact] - public void EqualsList_should_return_true_for_equal_lists() + 1, + 2 + }; + var rhs = new List<int> { - var lhs = new List<int> - { - 1, - 2 - }; - var rhs = new List<int> - { - 1, - 2 - }; - - Assert.True(lhs.EqualsList(rhs)); - } - - [Fact] - public void EqualsList_should_return_false_for_different_sizes() + 1 + }; + + Assert.False(lhs.EqualsList(rhs)); + } + + [Fact] + public void EqualsList_should_return_false_for_different_values() + { + var lhs = new List<int> { - var lhs = new List<int> - { - 1, - 2 - }; - var rhs = new List<int> - { - 1 - }; - - Assert.False(lhs.EqualsList(rhs)); - } - - [Fact] - public void EqualsList_should_return_false_for_different_values() + 1, + 2 + }; + var rhs = new List<int> { - var lhs = new List<int> - { - 1, - 2 - }; - var rhs = new List<int> - { - 1, - 3 - }; - - Assert.False(lhs.EqualsList(rhs)); - } - - [Fact] - public void EqualsList_should_return_false_for_different_order() + 1, + 3 + }; + + Assert.False(lhs.EqualsList(rhs)); + } + + [Fact] + public void EqualsList_should_return_false_for_different_order() + { + var lhs = new List<int> { - var lhs = new List<int> - { - 1, - 2 - }; - var rhs = new List<int> - { - 2, - 1 - }; - - Assert.False(lhs.EqualsList(rhs)); - } - - [Fact] - public void Foreach_should_call_action_foreach_item_with_index() + 1, + 2 + }; + var rhs = new List<int> { - var source = new List<int> { 3, 5, 1 }; + 2, + 1 + }; - var targetItems = new List<int>(); - var targetIndexes = new List<int>(); + Assert.False(lhs.EqualsList(rhs)); + } + + [Fact] + public void Foreach_should_call_action_foreach_item_with_index() + { + var source = new List<int> { 3, 5, 1 }; - source.Foreach((x, i) => - { - targetItems.Add(x); - targetIndexes.Add(i); - }); + var targetItems = new List<int>(); + var targetIndexes = new List<int>(); + + source.Foreach((x, i) => + { + targetItems.Add(x); + targetIndexes.Add(i); + }); - Assert.Equal(source, targetItems); - } + Assert.Equal(source, targetItems); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs index c9a5b18325..65b83e790d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs @@ -13,468 +13,467 @@ #pragma warning disable IDE0028 // Simplify collection initialization #pragma warning disable CA1841 // Prefer Dictionary.Contains methods -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public class ListDictionaryTests { - public class ListDictionaryTests + [Fact] + public void Should_create_empty() { - [Fact] - public void Should_create_empty() - { - var sut = new ListDictionary<int, int>(); + var sut = new ListDictionary<int, int>(); - Assert.Empty(sut); - Assert.Equal(1, sut.Capacity); - } + Assert.Empty(sut); + Assert.Equal(1, sut.Capacity); + } - [Fact] - public void Should_create_with_capacity() - { - var sut = new ListDictionary<int, int>(20); + [Fact] + public void Should_create_with_capacity() + { + var sut = new ListDictionary<int, int>(20); - Assert.Empty(sut); - Assert.Equal(20, sut.Capacity); - } + Assert.Empty(sut); + Assert.Equal(20, sut.Capacity); + } - [Fact] - public void Should_create_as_copy() - { - var source = new ListDictionary<int, int>(); + [Fact] + public void Should_create_as_copy() + { + var source = new ListDictionary<int, int>(); - source.Add(1, 10); - source.Add(2, 20); + source.Add(1, 10); + source.Add(2, 20); - var sut = new ListDictionary<int, int>(source); + var sut = new ListDictionary<int, int>(source); - Assert.Equal(2, sut.Count); - } + Assert.Equal(2, sut.Count); + } - [Fact] - public void Should_not_be_readonly() - { - var sut = new ListDictionary<int, int>(); + [Fact] + public void Should_not_be_readonly() + { + var sut = new ListDictionary<int, int>(); - Assert.False(sut.IsReadOnly); - Assert.False(sut.Keys.IsReadOnly); - Assert.False(sut.Values.IsReadOnly); - } + Assert.False(sut.IsReadOnly); + Assert.False(sut.Keys.IsReadOnly); + Assert.False(sut.Values.IsReadOnly); + } - [Fact] - public void Should_add_item() - { - var sut = new ListDictionary<int, int>(); + [Fact] + public void Should_add_item() + { + var sut = new ListDictionary<int, int>(); - sut.Add(1, 10); + sut.Add(1, 10); - Assert.Single(sut); - Assert.Equal(10, sut[1]); - } + Assert.Single(sut); + Assert.Equal(10, sut[1]); + } - [Fact] - public void Should_add_item_unsafe() - { - var sut = new ListDictionary<int, int>(); + [Fact] + public void Should_add_item_unsafe() + { + var sut = new ListDictionary<int, int>(); - sut.AddUnsafe(1, 10); + sut.AddUnsafe(1, 10); - Assert.Single(sut); - Assert.Equal(10, sut[1]); - } + Assert.Single(sut); + Assert.Equal(10, sut[1]); + } - [Fact] - public void Should_add_item_as_pair() - { - var sut = new ListDictionary<int, int>(); + [Fact] + public void Should_add_item_as_pair() + { + var sut = new ListDictionary<int, int>(); - sut.Add(new KeyValuePair<int, int>(1, 10)); + sut.Add(new KeyValuePair<int, int>(1, 10)); - Assert.Single(sut); - Assert.Equal(10, sut[1]); - } + Assert.Single(sut); + Assert.Equal(10, sut[1]); + } - [Fact] - public void Should_throw_exception_if_adding_existing_key() - { - var sut = new ListDictionary<int, int>(); + [Fact] + public void Should_throw_exception_if_adding_existing_key() + { + var sut = new ListDictionary<int, int>(); - sut.Add(1, 10); + sut.Add(1, 10); - Assert.Throws<ArgumentException>(() => sut.Add(1, 20)); - } + Assert.Throws<ArgumentException>(() => sut.Add(1, 20)); + } - [Fact] - public void Should_throw_exception_if_adding_pair_with_existing_key() - { - var sut = new ListDictionary<int, int>(); + [Fact] + public void Should_throw_exception_if_adding_pair_with_existing_key() + { + var sut = new ListDictionary<int, int>(); - sut.Add(1, 10); + sut.Add(1, 10); - Assert.Throws<ArgumentException>(() => sut.Add(new KeyValuePair<int, int>(1, 20))); - } - - [Fact] - public void Should_set_item() - { - var sut = new ListDictionary<int, int>(); + Assert.Throws<ArgumentException>(() => sut.Add(new KeyValuePair<int, int>(1, 20))); + } - sut[1] = 10; + [Fact] + public void Should_set_item() + { + var sut = new ListDictionary<int, int>(); - Assert.Single(sut); - Assert.Equal(10, sut[1]); - } + sut[1] = 10; - [Fact] - public void Should_override_item() - { - var sut = new ListDictionary<int, int>(); + Assert.Single(sut); + Assert.Equal(10, sut[1]); + } - sut[1] = 20; + [Fact] + public void Should_override_item() + { + var sut = new ListDictionary<int, int>(); - Assert.Single(sut); - Assert.Equal(20, sut[1]); - } + sut[1] = 20; - [Fact] - public void Should_return_true_when_dictionary_contains_value() - { - var sut = new ListDictionary<int, int>(); + Assert.Single(sut); + Assert.Equal(20, sut[1]); + } - sut.Add(1, 10); + [Fact] + public void Should_return_true_when_dictionary_contains_value() + { + var sut = new ListDictionary<int, int>(); - Assert.True(sut.Contains(new KeyValuePair<int, int>(1, 10))); - Assert.True(sut.ContainsKey(1)); - Assert.True(sut.Keys.Contains(1)); - Assert.True(sut.Values.Contains(10)); - } + sut.Add(1, 10); - [Fact] - public void Should_return_false_when_dictionary_does_not_contains_value() - { - var sut = new ListDictionary<int, int>(); + Assert.True(sut.Contains(new KeyValuePair<int, int>(1, 10))); + Assert.True(sut.ContainsKey(1)); + Assert.True(sut.Keys.Contains(1)); + Assert.True(sut.Values.Contains(10)); + } - sut.Add(1, 10); + [Fact] + public void Should_return_false_when_dictionary_does_not_contains_value() + { + var sut = new ListDictionary<int, int>(); - Assert.False(sut.Contains(new KeyValuePair<int, int>(1, 20))); - Assert.False(sut.ContainsKey(2)); - Assert.False(sut.Keys.Contains(2)); - Assert.False(sut.Values.Contains(20)); - } + sut.Add(1, 10); - [Fact] - public void Should_get_count() - { - var sut = new ListDictionary<int, int>(); + Assert.False(sut.Contains(new KeyValuePair<int, int>(1, 20))); + Assert.False(sut.ContainsKey(2)); + Assert.False(sut.Keys.Contains(2)); + Assert.False(sut.Values.Contains(20)); + } - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); + [Fact] + public void Should_get_count() + { + var sut = new ListDictionary<int, int>(); - Assert.Equal(3, sut.Count); - Assert.Equal(3, sut.Keys.Count); - Assert.Equal(3, sut.Values.Count); - } + sut.Add(1, 10); + sut.Add(2, 20); + sut.Add(3, 30); - [Fact] - public void Should_clear() - { - var sut = new ListDictionary<int, int>(); + Assert.Equal(3, sut.Count); + Assert.Equal(3, sut.Keys.Count); + Assert.Equal(3, sut.Values.Count); + } - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); - sut.Clear(); + [Fact] + public void Should_clear() + { + var sut = new ListDictionary<int, int>(); - Assert.Empty(sut); - } + sut.Add(1, 10); + sut.Add(2, 20); + sut.Add(3, 30); + sut.Clear(); - [Fact] - public void Should_remove_key() - { - var sut = new ListDictionary<int, int>(); + Assert.Empty(sut); + } - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); + [Fact] + public void Should_remove_key() + { + var sut = new ListDictionary<int, int>(); - Assert.True(sut.Remove(2)); - Assert.False(sut.ContainsKey(2)); - } + sut.Add(1, 10); + sut.Add(2, 20); + sut.Add(3, 30); - [Fact] - public void Should_not_remove_key_if_not_found() - { - var sut = new ListDictionary<int, int>(); + Assert.True(sut.Remove(2)); + Assert.False(sut.ContainsKey(2)); + } - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); + [Fact] + public void Should_not_remove_key_if_not_found() + { + var sut = new ListDictionary<int, int>(); - Assert.False(sut.Remove(4)); - } + sut.Add(1, 10); + sut.Add(2, 20); + sut.Add(3, 30); - [Fact] - public void Should_remove_item() - { - var sut = new ListDictionary<int, int>(); + Assert.False(sut.Remove(4)); + } - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); + [Fact] + public void Should_remove_item() + { + var sut = new ListDictionary<int, int>(); - Assert.True(sut.Remove(new KeyValuePair<int, int>(2, 20))); - Assert.False(sut.ContainsKey(2)); - } + sut.Add(1, 10); + sut.Add(2, 20); + sut.Add(3, 30); - [Fact] - public void Should_not_remove_item_if_key_not_found() - { - var sut = new ListDictionary<int, int>(); + Assert.True(sut.Remove(new KeyValuePair<int, int>(2, 20))); + Assert.False(sut.ContainsKey(2)); + } - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); + [Fact] + public void Should_not_remove_item_if_key_not_found() + { + var sut = new ListDictionary<int, int>(); - Assert.False(sut.Remove(new KeyValuePair<int, int>(4, 40))); - } + sut.Add(1, 10); + sut.Add(2, 20); + sut.Add(3, 30); - [Fact] - public void Should_not_remove_item_if_value_not_equal() - { - var sut = new ListDictionary<int, int>(); + Assert.False(sut.Remove(new KeyValuePair<int, int>(4, 40))); + } - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); + [Fact] + public void Should_not_remove_item_if_value_not_equal() + { + var sut = new ListDictionary<int, int>(); - Assert.False(sut.Remove(new KeyValuePair<int, int>(2, 40))); - } + sut.Add(1, 10); + sut.Add(2, 20); + sut.Add(3, 30); - [Fact] - public void Should_get_value_by_method_if_found() - { - var sut = new ListDictionary<int, int>(); + Assert.False(sut.Remove(new KeyValuePair<int, int>(2, 40))); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_get_value_by_method_if_found() + { + var sut = new ListDictionary<int, int>(); - Assert.True(sut.TryGetValue(2, out var found)); - Assert.Equal(20, found); - } + sut.Add(1, 10); + sut.Add(2, 20); - [Fact] - public void Should_not_get_value_by_method_if_not_found() - { - var sut = new ListDictionary<int, int>(); + Assert.True(sut.TryGetValue(2, out var found)); + Assert.Equal(20, found); + } - sut.Add(1, 10); - sut.Add(3, 30); + [Fact] + public void Should_not_get_value_by_method_if_not_found() + { + var sut = new ListDictionary<int, int>(); - Assert.False(sut.TryGetValue(4, out var found)); - Assert.Equal(0, found); - } + sut.Add(1, 10); + sut.Add(3, 30); - [Fact] - public void Should_get_value_by_indexer_if_found() - { - var sut = new ListDictionary<int, int>(); + Assert.False(sut.TryGetValue(4, out var found)); + Assert.Equal(0, found); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_get_value_by_indexer_if_found() + { + var sut = new ListDictionary<int, int>(); - Assert.Equal(20, sut[2]); - } + sut.Add(1, 10); + sut.Add(2, 20); - [Fact] - public void Should_not_get_value_by_indexer_if_not_found() - { - var sut = new ListDictionary<int, int>(); + Assert.Equal(20, sut[2]); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_not_get_value_by_indexer_if_not_found() + { + var sut = new ListDictionary<int, int>(); - Assert.Throws<KeyNotFoundException>(() => sut[4]); - } + sut.Add(1, 10); + sut.Add(2, 20); - [Fact] - public void Should_loop_over_entries() - { - var sut = new ListDictionary<int, int>(); + Assert.Throws<KeyNotFoundException>(() => sut[4]); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_loop_over_entries() + { + var sut = new ListDictionary<int, int>(); - var actual = new List<KeyValuePair<int, int>>(); + sut.Add(1, 10); + sut.Add(2, 20); - foreach (var entry in sut) - { - actual.Add(entry); - } + var actual = new List<KeyValuePair<int, int>>(); - Assert.Equal(new[] - { - new KeyValuePair<int, int>(1, 10), - new KeyValuePair<int, int>(2, 20) - }, actual.ToArray()); + foreach (var entry in sut) + { + actual.Add(entry); } - [Fact] - public void Should_loop_over_entries_with_old_enumerator() + Assert.Equal(new[] { - var sut = new ListDictionary<int, int>(); + new KeyValuePair<int, int>(1, 10), + new KeyValuePair<int, int>(2, 20) + }, actual.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_loop_over_entries_with_old_enumerator() + { + var sut = new ListDictionary<int, int>(); - var actual = new List<KeyValuePair<int, int>>(); + sut.Add(1, 10); + sut.Add(2, 20); - foreach (KeyValuePair<int, int> entry in (IEnumerable)sut) - { - actual.Add(entry); - } + var actual = new List<KeyValuePair<int, int>>(); - Assert.Equal(new[] - { - new KeyValuePair<int, int>(1, 10), - new KeyValuePair<int, int>(2, 20) - }, actual.ToArray()); + foreach (KeyValuePair<int, int> entry in (IEnumerable)sut) + { + actual.Add(entry); } - [Fact] - public void Should_copy_entries_to_array() + Assert.Equal(new[] { - var sut = new ListDictionary<int, int>(); + new KeyValuePair<int, int>(1, 10), + new KeyValuePair<int, int>(2, 20) + }, actual.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_copy_entries_to_array() + { + var sut = new ListDictionary<int, int>(); - Assert.Equal(new[] - { - new KeyValuePair<int, int>(1, 10), - new KeyValuePair<int, int>(2, 20) - }, sut.ToArray()); - } + sut.Add(1, 10); + sut.Add(2, 20); - [Fact] - public void Should_loop_over_keys() + Assert.Equal(new[] { - var sut = new ListDictionary<int, int>(); + new KeyValuePair<int, int>(1, 10), + new KeyValuePair<int, int>(2, 20) + }, sut.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_loop_over_keys() + { + var sut = new ListDictionary<int, int>(); - var actual = new List<int>(); + sut.Add(1, 10); + sut.Add(2, 20); - foreach (var entry in sut.Keys) - { - actual.Add(entry); - } + var actual = new List<int>(); - Assert.Equal(new[] { 1, 2 }, actual.ToArray()); + foreach (var entry in sut.Keys) + { + actual.Add(entry); } - [Fact] - public void Should_loop_over_keys_with_old_enumerator() - { - var sut = new ListDictionary<int, int>(); + Assert.Equal(new[] { 1, 2 }, actual.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_loop_over_keys_with_old_enumerator() + { + var sut = new ListDictionary<int, int>(); - var actual = new List<int>(); + sut.Add(1, 10); + sut.Add(2, 20); - foreach (int entry in (IEnumerable)sut.Keys) - { - actual.Add(entry); - } + var actual = new List<int>(); - Assert.Equal(new[] { 1, 2 }, actual.ToArray()); + foreach (int entry in (IEnumerable)sut.Keys) + { + actual.Add(entry); } - [Fact] - public void Should_copy_keys_to_array() - { - var sut = new ListDictionary<int, int>(); + Assert.Equal(new[] { 1, 2 }, actual.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_copy_keys_to_array() + { + var sut = new ListDictionary<int, int>(); - Assert.Equal(new[] { 1, 2 }, sut.Keys.ToArray()); - } + sut.Add(1, 10); + sut.Add(2, 20); - [Fact] - public void Should_loop_over_values() - { - var sut = new ListDictionary<int, int>(); + Assert.Equal(new[] { 1, 2 }, sut.Keys.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_loop_over_values() + { + var sut = new ListDictionary<int, int>(); - var actual = new List<int>(); + sut.Add(1, 10); + sut.Add(2, 20); - foreach (var entry in sut.Values) - { - actual.Add(entry); - } + var actual = new List<int>(); - Assert.Equal(new[] { 10, 20 }, actual.ToArray()); + foreach (var entry in sut.Values) + { + actual.Add(entry); } - [Fact] - public void Should_loop_over_values_with_old_enumerator() - { - var sut = new ListDictionary<int, int>(); + Assert.Equal(new[] { 10, 20 }, actual.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_loop_over_values_with_old_enumerator() + { + var sut = new ListDictionary<int, int>(); - var actual = new List<int>(); + sut.Add(1, 10); + sut.Add(2, 20); - foreach (int entry in (IEnumerable)sut.Values) - { - actual.Add(entry); - } + var actual = new List<int>(); - Assert.Equal(new[] { 10, 20 }, actual.ToArray()); + foreach (int entry in (IEnumerable)sut.Values) + { + actual.Add(entry); } - [Fact] - public void Should_copy_values_to_array() - { - var sut = new ListDictionary<int, int>(); + Assert.Equal(new[] { 10, 20 }, actual.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_copy_values_to_array() + { + var sut = new ListDictionary<int, int>(); - Assert.Equal(new[] { 10, 20 }, sut.Values.ToArray()); - } + sut.Add(1, 10); + sut.Add(2, 20); - [Fact] - public void Should_trim() - { - var sut = new ListDictionary<int, int>(20); + Assert.Equal(new[] { 10, 20 }, sut.Values.ToArray()); + } - sut.Add(1, 10); - sut.Add(2, 20); + [Fact] + public void Should_trim() + { + var sut = new ListDictionary<int, int>(20); - Assert.Equal(20, sut.Capacity); + sut.Add(1, 10); + sut.Add(2, 20); - sut.TrimExcess(); + Assert.Equal(20, sut.Capacity); - Assert.Equal(2, sut.Capacity); - } + sut.TrimExcess(); - [Fact] - public void Should_serialize_and_deserialize() + Assert.Equal(2, sut.Capacity); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var sut = new Dictionary<int, int> { - var sut = new Dictionary<int, int> - { - [11] = 1, - [12] = 2, - [13] = 3 - }.ToReadonlyDictionary(); + [11] = 1, + [12] = 2, + [13] = 3 + }.ToReadonlyDictionary(); - var serialized = sut.SerializeAndDeserialize(); + var serialized = sut.SerializeAndDeserialize(); - Assert.Equal(sut, serialized); - } + Assert.Equal(sut, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs index e634ff9304..35b8aaf14c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs @@ -8,116 +8,115 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public class ReadonlyDictionaryTests { - public class ReadonlyDictionaryTests + internal sealed class Inherited : ReadonlyDictionary<int, int> { - internal sealed class Inherited : ReadonlyDictionary<int, int> + public Inherited(IDictionary<int, int> inner) + : base(inner) { - public Inherited(IDictionary<int, int> inner) - : base(inner) - { - } } + } - [Fact] - public void Should_return_empty_instance_for_empty_source() - { - var actual = new Dictionary<int, int>().ToReadonlyDictionary(); + [Fact] + public void Should_return_empty_instance_for_empty_source() + { + var actual = new Dictionary<int, int>().ToReadonlyDictionary(); - Assert.Same(ReadonlyDictionary.Empty<int, int>(), actual); - } + Assert.Same(ReadonlyDictionary.Empty<int, int>(), actual); + } + + [Fact] + public void Should_return_empty_instance_for_empty_source_and_key_selector() + { + var actual = Enumerable.Empty<int>().ToReadonlyDictionary(x => x); - [Fact] - public void Should_return_empty_instance_for_empty_source_and_key_selector() + Assert.Same(ReadonlyDictionary.Empty<int, int>(), actual); + } + + [Fact] + public void Should_return_empty_instance_for_empty_source_and_value_selector() + { + var actual = Enumerable.Empty<int>().ToReadonlyDictionary(x => x, x => x); + + Assert.Same(ReadonlyDictionary.Empty<int, int>(), actual); + } + + [Fact] + public void Should_make_correct_object_equal_comparisons() + { + var obj1a = new Dictionary<int, int> { - var actual = Enumerable.Empty<int>().ToReadonlyDictionary(x => x); + [1] = 1 + }.ToReadonlyDictionary(); - Assert.Same(ReadonlyDictionary.Empty<int, int>(), actual); - } + var obj1b = new Dictionary<int, int> + { + [1] = 1 + }.ToReadonlyDictionary(); - [Fact] - public void Should_return_empty_instance_for_empty_source_and_value_selector() + var dictionaryOtherValue = new Dictionary<int, int> { - var actual = Enumerable.Empty<int>().ToReadonlyDictionary(x => x, x => x); + [1] = 2 + }.ToReadonlyDictionary(); - Assert.Same(ReadonlyDictionary.Empty<int, int>(), actual); - } + var dictionaryOtherKey = new Dictionary<int, int> + { + [2] = 1 + }.ToReadonlyDictionary(); - [Fact] - public void Should_make_correct_object_equal_comparisons() + var dictionaryOtherCount = new Dictionary<int, int> { - var obj1a = new Dictionary<int, int> - { - [1] = 1 - }.ToReadonlyDictionary(); - - var obj1b = new Dictionary<int, int> - { - [1] = 1 - }.ToReadonlyDictionary(); - - var dictionaryOtherValue = new Dictionary<int, int> - { - [1] = 2 - }.ToReadonlyDictionary(); - - var dictionaryOtherKey = new Dictionary<int, int> - { - [2] = 1 - }.ToReadonlyDictionary(); - - var dictionaryOtherCount = new Dictionary<int, int> - { - [1] = 1, - [2] = 2 - }.ToReadonlyDictionary(); - - Assert.Equal(obj1a, obj1b); - Assert.Equal(obj1a.GetHashCode(), obj1b.GetHashCode()); - Assert.True(obj1a.Equals((object)obj1b)); - - Assert.NotEqual(obj1a, dictionaryOtherValue); - Assert.NotEqual(obj1a.GetHashCode(), dictionaryOtherValue.GetHashCode()); - Assert.False(obj1a.Equals((object)dictionaryOtherValue)); - - Assert.NotEqual(obj1a, dictionaryOtherKey); - Assert.NotEqual(obj1a.GetHashCode(), dictionaryOtherKey.GetHashCode()); - Assert.False(obj1a.Equals((object)dictionaryOtherKey)); - - Assert.NotEqual(obj1a, dictionaryOtherCount); - Assert.NotEqual(obj1a.GetHashCode(), dictionaryOtherCount.GetHashCode()); - Assert.False(obj1a.Equals((object)dictionaryOtherCount)); - } + [1] = 1, + [2] = 2 + }.ToReadonlyDictionary(); + + Assert.Equal(obj1a, obj1b); + Assert.Equal(obj1a.GetHashCode(), obj1b.GetHashCode()); + Assert.True(obj1a.Equals((object)obj1b)); + + Assert.NotEqual(obj1a, dictionaryOtherValue); + Assert.NotEqual(obj1a.GetHashCode(), dictionaryOtherValue.GetHashCode()); + Assert.False(obj1a.Equals((object)dictionaryOtherValue)); + + Assert.NotEqual(obj1a, dictionaryOtherKey); + Assert.NotEqual(obj1a.GetHashCode(), dictionaryOtherKey.GetHashCode()); + Assert.False(obj1a.Equals((object)dictionaryOtherKey)); - [Fact] - public void Should_serialize_and_deserialize() + Assert.NotEqual(obj1a, dictionaryOtherCount); + Assert.NotEqual(obj1a.GetHashCode(), dictionaryOtherCount.GetHashCode()); + Assert.False(obj1a.Equals((object)dictionaryOtherCount)); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var sut = new Dictionary<int, int> { - var sut = new Dictionary<int, int> - { - [11] = 1, - [12] = 2, - [13] = 3 - }.ToReadonlyDictionary(); + [11] = 1, + [12] = 2, + [13] = 3 + }.ToReadonlyDictionary(); - var serialized = sut.SerializeAndDeserialize(); + var serialized = sut.SerializeAndDeserialize(); - Assert.Equal(sut, serialized); - } + Assert.Equal(sut, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_inherited() + [Fact] + public void Should_serialize_and_deserialize_inherited() + { + var sut = new Inherited(new Dictionary<int, int> { - var sut = new Inherited(new Dictionary<int, int> - { - [11] = 1, - [12] = 2, - [13] = 3 - }); + [11] = 1, + [12] = 2, + [13] = 3 + }); - var serialized = sut.SerializeAndDeserialize(); + var serialized = sut.SerializeAndDeserialize(); - Assert.Equal(sut, serialized); - } + Assert.Equal(sut, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs index 80a07b39f9..49a94e8e11 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs @@ -8,92 +8,91 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Collections +namespace Squidex.Infrastructure.Collections; + +public class ReadonlyListTests { - public class ReadonlyListTests + internal sealed class Inherited : ReadonlyList<int> { - internal sealed class Inherited : ReadonlyList<int> + public Inherited(IList<int> inner) + : base(inner) { - public Inherited(IList<int> inner) - : base(inner) - { - } } + } - [Fact] - public void Should_return_empty_instance_for_empty_array() - { - var actual = ReadonlyList.Create<int>(); + [Fact] + public void Should_return_empty_instance_for_empty_array() + { + var actual = ReadonlyList.Create<int>(); - Assert.Same(ReadonlyList.Empty<int>(), actual); - } + Assert.Same(ReadonlyList.Empty<int>(), actual); + } - [Fact] - public void Should_return_empty_instance_for_null_array() - { - var actual = ReadonlyList.Create((int[]?)null); + [Fact] + public void Should_return_empty_instance_for_null_array() + { + var actual = ReadonlyList.Create((int[]?)null); - Assert.Same(ReadonlyList.Empty<int>(), actual); - } + Assert.Same(ReadonlyList.Empty<int>(), actual); + } - [Fact] - public void Should_return_empty_instance_for_empty_enumerable() - { - var actual = Enumerable.Empty<int>().ToReadonlyList(); + [Fact] + public void Should_return_empty_instance_for_empty_enumerable() + { + var actual = Enumerable.Empty<int>().ToReadonlyList(); - Assert.Same(ReadonlyList.Empty<int>(), actual); - } + Assert.Same(ReadonlyList.Empty<int>(), actual); + } - [Fact] - public void Should_make_correct_equal_comparisons() - { - var list1a = ReadonlyList.Create(1); - var list1b = ReadonlyList.Create(1); + [Fact] + public void Should_make_correct_equal_comparisons() + { + var list1a = ReadonlyList.Create(1); + var list1b = ReadonlyList.Create(1); - var listOtherValue = ReadonlyList.Create(2); - var listOtherSize = ReadonlyList.Create(1, 2); + var listOtherValue = ReadonlyList.Create(2); + var listOtherSize = ReadonlyList.Create(1, 2); - Assert.Equal(list1a, list1b); - Assert.Equal(list1a.GetHashCode(), list1b.GetHashCode()); - Assert.True(list1a.Equals((object)list1b)); + Assert.Equal(list1a, list1b); + Assert.Equal(list1a.GetHashCode(), list1b.GetHashCode()); + Assert.True(list1a.Equals((object)list1b)); - Assert.NotEqual(list1a, listOtherValue); - Assert.NotEqual(list1a.GetHashCode(), listOtherValue.GetHashCode()); - Assert.False(list1a.Equals((object)listOtherValue)); + Assert.NotEqual(list1a, listOtherValue); + Assert.NotEqual(list1a.GetHashCode(), listOtherValue.GetHashCode()); + Assert.False(list1a.Equals((object)listOtherValue)); - Assert.NotEqual(list1a, listOtherSize); - Assert.NotEqual(list1a.GetHashCode(), listOtherSize.GetHashCode()); - Assert.False(list1a.Equals((object)listOtherSize)); - } + Assert.NotEqual(list1a, listOtherSize); + Assert.NotEqual(list1a.GetHashCode(), listOtherSize.GetHashCode()); + Assert.False(list1a.Equals((object)listOtherSize)); + } - [Fact] - public void Should_serialize_and_deserialize() + [Fact] + public void Should_serialize_and_deserialize() + { + var sut = new List<int> { - var sut = new List<int> - { - 1, - 2, - 3 - }.ToReadonlyList(); + 1, + 2, + 3 + }.ToReadonlyList(); - var serialized = sut.SerializeAndDeserialize(); + var serialized = sut.SerializeAndDeserialize(); - Assert.Equal(sut, serialized); - } + Assert.Equal(sut, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_inherited() + [Fact] + public void Should_serialize_and_deserialize_inherited() + { + var sut = new Inherited(new List<int> { - var sut = new Inherited(new List<int> - { - 1, - 2, - 3 - }); + 1, + 2, + 3 + }); - var serialized = sut.SerializeAndDeserialize(); + var serialized = sut.SerializeAndDeserialize(); - Assert.Equal(sut, serialized); - } + Assert.Equal(sut, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs index f81e768246..10e26a785a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs @@ -9,53 +9,52 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class CommandContextTests { - public class CommandContextTests - { - private readonly ICommand command = new MyCommand(); - private readonly CommandContext sut; + private readonly ICommand command = new MyCommand(); + private readonly CommandContext sut; - public CommandContextTests() - { - sut = new CommandContext(command, A.Dummy<ICommandBus>()); - } + public CommandContextTests() + { + sut = new CommandContext(command, A.Dummy<ICommandBus>()); + } - [Fact] - public void Should_instantiate_and_provide_command() - { - Assert.Equal(command, sut.Command); + [Fact] + public void Should_instantiate_and_provide_command() + { + Assert.Equal(command, sut.Command); - Assert.Null(sut.PlainResult); - Assert.Null(sut.Result<string>()); + Assert.Null(sut.PlainResult); + Assert.Null(sut.Result<string>()); - Assert.NotEqual(DomainId.Empty, sut.ContextId); + Assert.NotEqual(DomainId.Empty, sut.ContextId); - Assert.False(sut.IsCompleted); - } + Assert.False(sut.IsCompleted); + } - [Fact] - public void Should_be_handled_if_succeeded() - { - sut.Complete(); + [Fact] + public void Should_be_handled_if_succeeded() + { + sut.Complete(); - Assert.True(sut.IsCompleted); - } + Assert.True(sut.IsCompleted); + } - [Fact] - public void Should_provide_actual_if_succeeded_with_value() - { - sut.Complete("RESULT"); + [Fact] + public void Should_provide_actual_if_succeeded_with_value() + { + sut.Complete("RESULT"); - Assert.Equal("RESULT", sut.Result<string>()); - } + Assert.Equal("RESULT", sut.Result<string>()); + } - [Fact] - public void Should_provide_plain_actual_if_succeeded_with_value() - { - sut.Complete("RESULT"); + [Fact] + public void Should_provide_plain_actual_if_succeeded_with_value() + { + sut.Complete("RESULT"); - Assert.Equal("RESULT", sut.PlainResult); - } + Assert.Equal("RESULT", sut.PlainResult); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandResultTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandResultTests.cs index e71d21e5a9..487a34540d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandResultTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandResultTests.cs @@ -8,18 +8,17 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class CommandResultTests { - public class CommandResultTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var sut = new CommandResult(DomainId.NewGuid(), 3, 2, null!); + var sut = new CommandResult(DomainId.NewGuid(), 3, 2, null!); - var serialized = sut.SerializeAndDeserialize(); + var serialized = sut.SerializeAndDeserialize(); - Assert.Equal(sut, serialized); - } + Assert.Equal(sut, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs index 1fe01e5e67..265d0cdaea 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs @@ -8,63 +8,62 @@ using FakeItEasy; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class CustomCommandMiddlewareRunnerTests { - public class CustomCommandMiddlewareRunnerTests + public sealed class Command : ICommand { - public sealed class Command : ICommand - { - public List<int> Values { get; set; } = new List<int>(); + public List<int> Values { get; set; } = new List<int>(); - public long ExpectedVersion { get; set; } - } + public long ExpectedVersion { get; set; } + } + + public sealed class CustomMiddleware : ICustomCommandMiddleware + { + private readonly int value; - public sealed class CustomMiddleware : ICustomCommandMiddleware + public CustomMiddleware(int value) { - private readonly int value; + this.value = value; + } - public CustomMiddleware(int value) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + if (context.Command is Command command) { - this.value = value; + command.Values.Add(value); } - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - if (context.Command is Command command) - { - command.Values.Add(value); - } - - return next(context, ct); - } + return next(context, ct); } + } - [Fact] - public async Task Should_run_extensions_in_right_order() - { - var command = new Command(); - var context = new CommandContext(command, A.Fake<ICommandBus>()); + [Fact] + public async Task Should_run_extensions_in_right_order() + { + var command = new Command(); + var context = new CommandContext(command, A.Fake<ICommandBus>()); - var sut = new CustomCommandMiddlewareRunner(new[] - { - new CustomMiddleware(10), - new CustomMiddleware(12), - new CustomMiddleware(14) - }); + var sut = new CustomCommandMiddlewareRunner(new[] + { + new CustomMiddleware(10), + new CustomMiddleware(12), + new CustomMiddleware(14) + }); - var isNextCalled = false; + var isNextCalled = false; - await sut.HandleAsync(context, (c, _) => - { - isNextCalled = true; + await sut.HandleAsync(context, (c, _) => + { + isNextCalled = true; - Assert.Equal(new[] { 10, 12, 14 }, command.Values); + Assert.Equal(new[] { 10, 12, 14 }, command.Values); - return Task.CompletedTask; - }, default); + return Task.CompletedTask; + }, default); - Assert.True(isNextCalled); - } + Assert.True(isNextCalled); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs index 0d2e58ed34..f769ba6f3a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs @@ -12,86 +12,85 @@ using Squidex.Infrastructure.Json; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class DefaultDomainObjectCacheTests { - public class DefaultDomainObjectCacheTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IJsonSerializer serializer = A.Fake<IJsonSerializer>(); + private readonly IMemoryCache cache = A.Fake<IMemoryCache>(); + private readonly IDistributedCache distributedCache = A.Fake<IDistributedCache>(); + private readonly DomainId id = DomainId.NewGuid(); + private readonly DefaultDomainObjectCache sut; + + public DefaultDomainObjectCacheTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IJsonSerializer serializer = A.Fake<IJsonSerializer>(); - private readonly IMemoryCache cache = A.Fake<IMemoryCache>(); - private readonly IDistributedCache distributedCache = A.Fake<IDistributedCache>(); - private readonly DomainId id = DomainId.NewGuid(); - private readonly DefaultDomainObjectCache sut; - - public DefaultDomainObjectCacheTests() - { - ct = cts.Token; + ct = cts.Token; - var options = Options.Create(new DomainObjectCacheOptions()); + var options = Options.Create(new DomainObjectCacheOptions()); - sut = new DefaultDomainObjectCache(cache, serializer, distributedCache, options); - } + sut = new DefaultDomainObjectCache(cache, serializer, distributedCache, options); + } - [Fact] - public async Task Should_add_to_cache_and_memory_cache_on_set() - { - await sut.SetAsync(id, 10, 20, ct); + [Fact] + public async Task Should_add_to_cache_and_memory_cache_on_set() + { + await sut.SetAsync(id, 10, 20, ct); - A.CallTo(() => cache.CreateEntry($"{id}_10")) - .MustHaveHappened(); + A.CallTo(() => cache.CreateEntry($"{id}_10")) + .MustHaveHappened(); - A.CallTo(() => serializer.Serialize(20, A<Stream>._, true)) - .MustHaveHappened(); + A.CallTo(() => serializer.Serialize(20, A<Stream>._, true)) + .MustHaveHappened(); - A.CallTo(() => distributedCache.SetAsync($"{id}_10", A<byte[]>._, A<DistributedCacheEntryOptions>._, ct)) - .MustHaveHappened(); - } + A.CallTo(() => distributedCache.SetAsync($"{id}_10", A<byte[]>._, A<DistributedCacheEntryOptions>._, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_ignore_exception_on_set() - { - A.CallTo(() => distributedCache.SetAsync(A<string>._, A<byte[]>._, A<DistributedCacheEntryOptions>._, ct)) - .Throws(new InvalidOperationException()); + [Fact] + public async Task Should_ignore_exception_on_set() + { + A.CallTo(() => distributedCache.SetAsync(A<string>._, A<byte[]>._, A<DistributedCacheEntryOptions>._, ct)) + .Throws(new InvalidOperationException()); - await sut.SetAsync(id, 10, 20, ct); - } + await sut.SetAsync(id, 10, 20, ct); + } - [Fact] - public async Task Should_provide_from_cache_if_found() - { - object returned; + [Fact] + public async Task Should_provide_from_cache_if_found() + { + object returned; - A.CallTo(() => cache.TryGetValue($"{id}_10", out returned)) - .Returns(true) - .AssignsOutAndRefParameters(20); + A.CallTo(() => cache.TryGetValue($"{id}_10", out returned)) + .Returns(true) + .AssignsOutAndRefParameters(20); - var actual = await sut.GetAsync<int>(id, 10, ct); + var actual = await sut.GetAsync<int>(id, 10, ct); - Assert.Equal(20, actual); - } + Assert.Equal(20, actual); + } - [Fact] - public async Task Should_provide_from_distributed_cache_if_not_found_in_cache() - { - A.CallTo(() => serializer.Deserialize<int>(A<Stream>._, null, false)) - .Returns(20); + [Fact] + public async Task Should_provide_from_distributed_cache_if_not_found_in_cache() + { + A.CallTo(() => serializer.Deserialize<int>(A<Stream>._, null, false)) + .Returns(20); - var actual = await sut.GetAsync<int>(id, 10, ct); + var actual = await sut.GetAsync<int>(id, 10, ct); - Assert.Equal(20, actual); + Assert.Equal(20, actual); - A.CallTo(() => distributedCache.GetAsync($"{id}_10", ct)) - .MustHaveHappened(); - } + A.CallTo(() => distributedCache.GetAsync($"{id}_10", ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_ignore_exception_on_gett() - { - A.CallTo(() => distributedCache.GetAsync(A<string>._, ct)) - .Throws(new InvalidOperationException()); + [Fact] + public async Task Should_ignore_exception_on_gett() + { + A.CallTo(() => distributedCache.GetAsync(A<string>._, ct)) + .Throws(new InvalidOperationException()); - await sut.SetAsync(id, 10, 20, ct); - } + await sut.SetAsync(id, 10, 20, ct); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectFactoryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectFactoryTests.cs index 6db7093d1d..93e6048e72 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectFactoryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectFactoryTests.cs @@ -12,49 +12,48 @@ #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class DefaultDomainObjectFactoryTests { - public class DefaultDomainObjectFactoryTests - { - private record TestObject(DomainId Id, IPersistenceFactory<int> PersistenceFactory); + private record TestObject(DomainId Id, IPersistenceFactory<int> PersistenceFactory); - [Fact] - public void Should_create_with_service_locator() - { - var id = DomainId.NewGuid(); + [Fact] + public void Should_create_with_service_locator() + { + var id = DomainId.NewGuid(); - var persistenceFactory = A.Fake<IPersistenceFactory<int>>(); + var persistenceFactory = A.Fake<IPersistenceFactory<int>>(); - var serviceProvider = - new ServiceCollection() - .AddSingleton(persistenceFactory) - .BuildServiceProvider(); + var serviceProvider = + new ServiceCollection() + .AddSingleton(persistenceFactory) + .BuildServiceProvider(); - var sut = new DefaultDomainObjectFactory(serviceProvider); + var sut = new DefaultDomainObjectFactory(serviceProvider); - var created = sut.Create<TestObject>(id); + var created = sut.Create<TestObject>(id); - Assert.Equal(id, created.Id); - Assert.Same(persistenceFactory, created.PersistenceFactory); - } + Assert.Equal(id, created.Id); + Assert.Same(persistenceFactory, created.PersistenceFactory); + } - [Fact] - public void Should_create_with_service_locator_and_custom_persistence_factory() - { - var id = DomainId.NewGuid(); + [Fact] + public void Should_create_with_service_locator_and_custom_persistence_factory() + { + var id = DomainId.NewGuid(); - var persistenceFactory = A.Fake<IPersistenceFactory<int>>(); + var persistenceFactory = A.Fake<IPersistenceFactory<int>>(); - var serviceProvider = - new ServiceCollection() - .BuildServiceProvider(); + var serviceProvider = + new ServiceCollection() + .BuildServiceProvider(); - var sut = new DefaultDomainObjectFactory(serviceProvider); + var sut = new DefaultDomainObjectFactory(serviceProvider); - var created = sut.Create<TestObject, int>(id, persistenceFactory); + var created = sut.Create<TestObject, int>(id, persistenceFactory); - Assert.Equal(id, created.Id); - Assert.Same(persistenceFactory, created.PersistenceFactory); - } + Assert.Equal(id, created.Id); + Assert.Same(persistenceFactory, created.PersistenceFactory); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs index 6eeba54acc..12be04e2dc 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs @@ -11,502 +11,501 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class DomainObjectTests { - public class DomainObjectTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState<MyDomainState> state; + private readonly DomainId id = DomainId.NewGuid(); + private readonly MyDomainObject sut; + + public DomainObjectTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly TestState<MyDomainState> state; - private readonly DomainId id = DomainId.NewGuid(); - private readonly MyDomainObject sut; + ct = cts.Token; - public DomainObjectTests() - { - ct = cts.Token; + state = new TestState<MyDomainState>(id); - state = new TestState<MyDomainState>(id); + sut = new MyDomainObject(id, state.PersistenceFactory); + } - sut = new MyDomainObject(id, state.PersistenceFactory); - } + [Fact] + public void Should_instantiate() + { + Assert.Equal(EtagVersion.Empty, sut.Version); + AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty); + } - [Fact] - public void Should_instantiate() - { - Assert.Equal(EtagVersion.Empty, sut.Version); - AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty); - } + [Fact] + public async Task Should_repair_if_stale() + { + A.CallTo(() => state.Persistence.IsSnapshotStale) + .Returns(true); - [Fact] - public async Task Should_repair_if_stale() - { - A.CallTo(() => state.Persistence.IsSnapshotStale) - .Returns(true); + SetupCreated(1); - SetupCreated(1); + await sut.EnsureLoadedAsync(ct); - await sut.EnsureLoadedAsync(ct); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>._, default)) + .MustHaveHappened(); + } - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>._, default)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_not_repair_if_not_stale() + { + A.CallTo(() => state.Persistence.IsSnapshotStale) + .Returns(false); - [Fact] - public async Task Should_not_repair_if_not_stale() - { - A.CallTo(() => state.Persistence.IsSnapshotStale) - .Returns(false); + SetupCreated(1); - SetupCreated(1); + await sut.EnsureLoadedAsync(ct); - await sut.EnsureLoadedAsync(ct); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>._, default)) + .MustNotHaveHappened(); + } - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>._, default)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_write_state_and_events_if_created() + { + var actual = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); - [Fact] - public async Task Should_write_state_and_events_if_created() - { - var actual = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 4), default)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 4), default)) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.ReadAsync(A<long>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => state.Persistence.ReadAsync(A<long>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + Assert.Equal(CommandResult.Empty(id, 0, EtagVersion.Empty), actual); + Assert.Equal(0, sut.Version); + Assert.Equal(0, sut.Snapshot.Version); - Assert.Equal(CommandResult.Empty(id, 0, EtagVersion.Empty), actual); - Assert.Equal(0, sut.Version); - Assert.Equal(0, sut.Snapshot.Version); + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 4, 0); + } - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 4, 0); - } + [Fact] + public async Task Should_migrate_old_event_with_state() + { + SetupCreated(new ValueChanged { Value = 10 }, new MultipleByTwiceEvent()); - [Fact] - public async Task Should_migrate_old_event_with_state() - { - SetupCreated(new ValueChanged { Value = 10 }, new MultipleByTwiceEvent()); + await sut.EnsureLoadedAsync(ct); - await sut.EnsureLoadedAsync(ct); + Assert.Equal(1, sut.Version); + Assert.Equal(1, sut.Snapshot.Version); + Assert.Equal(20, sut.Snapshot.Value); + } - Assert.Equal(1, sut.Version); - Assert.Equal(1, sut.Snapshot.Version); - Assert.Equal(20, sut.Snapshot.Value); - } + [Fact] + public async Task Should_recreate_when_loading() + { + sut.RecreateEvent = true; - [Fact] - public async Task Should_recreate_when_loading() - { - sut.RecreateEvent = true; + SetupCreated( + new ValueChanged { Value = 2 }, + new ValueChanged { Value = 3 }, + new Deleted(), + new ValueChanged { Value = 4 }); - SetupCreated( - new ValueChanged { Value = 2 }, - new ValueChanged { Value = 3 }, - new Deleted(), - new ValueChanged { Value = 4 }); + await sut.EnsureLoadedAsync(ct); - await sut.EnsureLoadedAsync(ct); + Assert.Equal(3, sut.Version); + Assert.Equal(3, sut.Snapshot.Version); - Assert.Equal(3, sut.Version); - Assert.Equal(3, sut.Snapshot.Version); + AssertSnapshot(sut.Snapshot, 4, 3); + } - AssertSnapshot(sut.Snapshot, 4, 3); - } + [Fact] + public async Task Should_ignore_events_after_deleting_when_loading() + { + SetupCreated( + new ValueChanged { Value = 2 }, + new ValueChanged { Value = 3 }, + new Deleted(), + new ValueChanged { Value = 4 }); - [Fact] - public async Task Should_ignore_events_after_deleting_when_loading() - { - SetupCreated( - new ValueChanged { Value = 2 }, - new ValueChanged { Value = 3 }, - new Deleted(), - new ValueChanged { Value = 4 }); + await sut.EnsureLoadedAsync(ct); - await sut.EnsureLoadedAsync(ct); + Assert.Equal(2, sut.Version); + Assert.Equal(2, sut.Snapshot.Version); - Assert.Equal(2, sut.Version); - Assert.Equal(2, sut.Snapshot.Version); + AssertSnapshot(sut.Snapshot, 3, 2, true); + } - AssertSnapshot(sut.Snapshot, 3, 2, true); - } + [Fact] + public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception() + { + sut.Recreate = false; - [Fact] - public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception() - { - sut.Recreate = false; + SetupCreated(2); - SetupCreated(2); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) + .Throws(new InconsistentStateException(2, -1)).Once(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) - .Throws(new InconsistentStateException(2, -1)).Once(); + await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(new UpdateAuto(), ct)); + } - await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(new UpdateAuto(), ct)); - } + [Fact] + public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception_and_deleted() + { + sut.Recreate = false; - [Fact] - public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception_and_deleted() - { - sut.Recreate = false; + SetupCreated(2); + SetupDeleted(); - SetupCreated(2); - SetupDeleted(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) + .Throws(new InconsistentStateException(2, -1)).Once(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) - .Throws(new InconsistentStateException(2, -1)).Once(); + await Assert.ThrowsAsync<DomainObjectDeletedException>(() => sut.ExecuteAsync(new UpdateAuto(), ct)); + } - await Assert.ThrowsAsync<DomainObjectDeletedException>(() => sut.ExecuteAsync(new UpdateAuto(), ct)); - } + [Fact] + public async Task Should_recreate_with_create_command_if_deleted_before() + { + sut.Recreate = true; + sut.RecreateEvent = true; - [Fact] - public async Task Should_recreate_with_create_command_if_deleted_before() - { - sut.Recreate = true; - sut.RecreateEvent = true; + SetupCreated(2); + SetupDeleted(); - SetupCreated(2); - SetupDeleted(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) + .Throws(new InconsistentStateException(2, -1)).Once(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) - .Throws(new InconsistentStateException(2, -1)).Once(); + var actual = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); - var actual = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 3); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 3); + A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappened(); + Assert.Equal(CommandResult.Empty(id, 2, 1), actual); + Assert.Equal(2, sut.Version); + Assert.Equal(2, sut.Snapshot.Version); - Assert.Equal(CommandResult.Empty(id, 2, 1), actual); - Assert.Equal(2, sut.Version); - Assert.Equal(2, sut.Snapshot.Version); + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 4, 2); + } - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 4, 2); - } + [Fact] + public async Task Should_throw_exception_if_recreation_with_create_command_is_not_allowed() + { + sut.Recreate = false; - [Fact] - public async Task Should_throw_exception_if_recreation_with_create_command_is_not_allowed() - { - sut.Recreate = false; + SetupCreated(2); + SetupDeleted(); - SetupCreated(2); - SetupDeleted(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) + .Throws(new InconsistentStateException(2, -1)).Once(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) - .Throws(new InconsistentStateException(2, -1)).Once(); + await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); + } - await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); - } + [Fact] + public async Task Should_recreate_with_upsert_command_if_deleted_before() + { + sut.Recreate = true; + sut.RecreateEvent = true; - [Fact] - public async Task Should_recreate_with_upsert_command_if_deleted_before() - { - sut.Recreate = true; - sut.RecreateEvent = true; + SetupCreated(2); + SetupDeleted(); - SetupCreated(2); - SetupDeleted(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) + .Throws(new InconsistentStateException(2, -1)).Once(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) - .Throws(new InconsistentStateException(2, -1)).Once(); + var actual = await sut.ExecuteAsync(new Upsert { Value = 4 }, ct); - var actual = await sut.ExecuteAsync(new Upsert { Value = 4 }, ct); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 3); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 3); + A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappened(); + Assert.Equal(CommandResult.Empty(id, 2, 1), actual); + Assert.Equal(2, sut.Version); + Assert.Equal(2, sut.Snapshot.Version); - Assert.Equal(CommandResult.Empty(id, 2, 1), actual); - Assert.Equal(2, sut.Version); - Assert.Equal(2, sut.Snapshot.Version); + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 4, 2); + } - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 4, 2); - } + [Fact] + public async Task Should_throw_exception_if_recreation_with_upsert_command_is_not_allowed() + { + sut.Recreate = false; - [Fact] - public async Task Should_throw_exception_if_recreation_with_upsert_command_is_not_allowed() - { - sut.Recreate = false; + SetupCreated(2); + SetupDeleted(); - SetupCreated(2); - SetupDeleted(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) + .Throws(new InconsistentStateException(2, -1)).Once(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) - .Throws(new InconsistentStateException(2, -1)).Once(); + await Assert.ThrowsAsync<DomainObjectDeletedException>(() => sut.ExecuteAsync(new Upsert(), ct)); + } - await Assert.ThrowsAsync<DomainObjectDeletedException>(() => sut.ExecuteAsync(new Upsert(), ct)); - } + [Fact] + public async Task Should_write_state_and_events_if_updated_after_creation() + { + await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); - [Fact] - public async Task Should_write_state_and_events_if_updated_after_creation() - { - await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); + var actual = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); - var actual = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 8), default)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 8), default)) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.ReadAsync(A<long>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => state.Persistence.ReadAsync(A<long>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + Assert.Equal(CommandResult.Empty(id, 1, 0), actual); + Assert.Equal(1, sut.Version); + Assert.Equal(1, sut.Snapshot.Version); - Assert.Equal(CommandResult.Empty(id, 1, 0), actual); - Assert.Equal(1, sut.Version); - Assert.Equal(1, sut.Snapshot.Version); + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 8, 1); + } - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 8, 1); - } + [Fact] + public async Task Should_write_state_and_events_if_updated() + { + SetupCreated(4); - [Fact] - public async Task Should_write_state_and_events_if_updated() - { - SetupCreated(4); + var actual = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); - var actual = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 8), default)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 8), default)) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappenedOnceExactly(); + Assert.Equal(CommandResult.Empty(id, 1, 0), actual); + Assert.Equal(1, sut.Version); + Assert.Equal(1, sut.Snapshot.Version); - Assert.Equal(CommandResult.Empty(id, 1, 0), actual); - Assert.Equal(1, sut.Version); - Assert.Equal(1, sut.Snapshot.Version); + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 8, 1); + } - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 8, 1); - } + [Fact] + public async Task Should_not_load_on_create() + { + await sut.ExecuteAsync(new CreateAuto(), ct); - [Fact] - public async Task Should_not_load_on_create() - { - await sut.ExecuteAsync(new CreateAuto(), ct); + A.CallTo(() => state.Persistence.ReadAsync(A<long>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => state.Persistence.ReadAsync(A<long>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_load_once_on_update() + { + SetupCreated(4); - [Fact] - public async Task Should_load_once_on_update() - { - SetupCreated(4); + await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); + await sut.ExecuteAsync(new UpdateAuto { Value = 9, ExpectedVersion = 1 }, ct); - await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); - await sut.ExecuteAsync(new UpdateAuto { Value = 9, ExpectedVersion = 1 }, ct); + A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappenedOnceExactly(); + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 9, 2); + } - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 9, 2); - } + [Fact] + public async Task Should_rebuild_state() + { + SetupCreated(4); - [Fact] - public async Task Should_rebuild_state() - { - SetupCreated(4); + await sut.RebuildStateAsync(ct); - await sut.RebuildStateAsync(ct); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 4), ct)) + .MustHaveHappened(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 4), ct)) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_throw_on_rebuild_if_no_event_found() + { + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.RebuildStateAsync(ct)); + } - [Fact] - public async Task Should_throw_on_rebuild_if_no_event_found() - { - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.RebuildStateAsync(ct)); - } + [Fact] + public async Task Should_throw_exception_on_create_command_is_rejected_due_to_version_conflict() + { + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) + .Throws(new InconsistentStateException(4, EtagVersion.Empty)); - [Fact] - public async Task Should_throw_exception_on_create_command_is_rejected_due_to_version_conflict() - { - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) - .Throws(new InconsistentStateException(4, EtagVersion.Empty)); + await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); + } - await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); - } + [Fact] + public async Task Should_throw_exception_if_create_command_is_invoked_for_loaded_and_created_object() + { + await sut.ExecuteAsync(new CreateAuto(), ct); - [Fact] - public async Task Should_throw_exception_if_create_command_is_invoked_for_loaded_and_created_object() - { - await sut.ExecuteAsync(new CreateAuto(), ct); + await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); + } - await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); - } + [Fact] + public async Task Should_throw_exception_if_create_command_not_accepted() + { + await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new CreateAuto { Value = 99 }, ct)); + } - [Fact] - public async Task Should_throw_exception_if_create_command_not_accepted() - { - await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new CreateAuto { Value = 99 }, ct)); - } + [Fact] + public async Task Should_return_custom_actual_on_create() + { + var actual = await sut.ExecuteAsync(new CreateCustom(), ct); - [Fact] - public async Task Should_return_custom_actual_on_create() - { - var actual = await sut.ExecuteAsync(new CreateCustom(), ct); + Assert.Equal(new CommandResult(id, 0, EtagVersion.Empty, "CREATED"), actual); + } - Assert.Equal(new CommandResult(id, 0, EtagVersion.Empty, "CREATED"), actual); - } + [Fact] + public async Task Should_throw_exception_if_update_command_invoked_for_empty_object() + { + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(new UpdateAuto(), ct)); + } - [Fact] - public async Task Should_throw_exception_if_update_command_invoked_for_empty_object() - { - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(new UpdateAuto(), ct)); - } + [Fact] + public async Task Should_throw_exception_if_update_command_not_accepted() + { + SetupCreated(4); - [Fact] - public async Task Should_throw_exception_if_update_command_not_accepted() - { - SetupCreated(4); + await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 }, ct)); + } - await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 }, ct)); - } + [Fact] + public async Task Should_return_custom_actual_on_update() + { + SetupCreated(4); - [Fact] - public async Task Should_return_custom_actual_on_update() - { - SetupCreated(4); + var actual = await sut.ExecuteAsync(new UpdateCustom(), ct); - var actual = await sut.ExecuteAsync(new UpdateCustom(), ct); + Assert.Equal(new CommandResult(id, 1, 0, "UPDATED"), actual); + } - Assert.Equal(new CommandResult(id, 1, 0, "UPDATED"), actual); - } + [Fact] + public async Task Should_throw_exception_if_other_verison_expected() + { + SetupCreated(4); - [Fact] - public async Task Should_throw_exception_if_other_verison_expected() - { - SetupCreated(4); + await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 }, ct)); + } - await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 }, ct)); - } + [Fact] + public async Task Should_not_update_if_snapshot_is_not_changed() + { + SetupCreated(4); - [Fact] - public async Task Should_not_update_if_snapshot_is_not_changed() - { - SetupCreated(4); + var actual = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged }, ct); - var actual = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged }, ct); + Assert.Equal(CommandResult.Empty(id, 0, 0), actual); + Assert.Equal(0, sut.Version); + Assert.Equal(0, sut.Snapshot.Version); - Assert.Equal(CommandResult.Empty(id, 0, 0), actual); - Assert.Equal(0, sut.Version); - Assert.Equal(0, sut.Snapshot.Version); - - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 4, 0); - } - - [Fact] - public async Task Should_reset_state_if_writing_snapshot_for_create_failed() - { - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>._, default)) - .Throws(new InvalidOperationException()); + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 4, 0); + } - await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); - - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty); - } - - [Fact] - public async Task Should_reset_state_if_writing_snapshot_for_update_failed() - { - SetupCreated(4); - - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>._, default)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new UpdateAuto(), ct)); + [Fact] + public async Task Should_reset_state_if_writing_snapshot_for_create_failed() + { + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>._, default)) + .Throws(new InvalidOperationException()); - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(sut.Snapshot, 4, 0); - } + await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); - [Fact] - public async Task Should_write_events_to_delete_stream_on_delete() - { - SetupCreated(4); - SetupDeleted(); - - var deleteStream = A.Fake<IPersistence<MyDomainState>>(); + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty); + } + + [Fact] + public async Task Should_reset_state_if_writing_snapshot_for_update_failed() + { + SetupCreated(4); - A.CallTo(() => state.PersistenceFactory.WithEventSourcing(typeof(MyDomainObject), DomainId.Combine(id, DomainId.Create("deleted")), null)) - .Returns(deleteStream); - - await sut.ExecuteAsync(new DeletePermanent(), ct); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<MyDomainState>._, default)) + .Throws(new InvalidOperationException()); - AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty, false); - - A.CallTo(() => state.Persistence.DeleteAsync(ct)) - .MustHaveHappened(); + await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new UpdateAuto(), ct)); + + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(sut.Snapshot, 4, 0); + } - A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) - .MustHaveHappenedOnceExactly(); + [Fact] + public async Task Should_write_events_to_delete_stream_on_delete() + { + SetupCreated(4); + SetupDeleted(); - A.CallTo(() => deleteStream.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + var deleteStream = A.Fake<IPersistence<MyDomainState>>(); - [Fact] - public async Task Should_get_old_versions_from_query() - { - await sut.ExecuteAsync(new CreateAuto { Value = 3 }, ct); - await sut.ExecuteAsync(new UpdateAuto { Value = 4 }, ct); - await sut.ExecuteAsync(new UpdateAuto { Value = 5 }, ct); + A.CallTo(() => state.PersistenceFactory.WithEventSourcing(typeof(MyDomainObject), DomainId.Combine(id, DomainId.Create("deleted")), null)) + .Returns(deleteStream); - var version_Empty = await sut.GetSnapshotAsync(EtagVersion.Empty, ct); - var version_0 = await sut.GetSnapshotAsync(0, ct); - var version_1 = await sut.GetSnapshotAsync(1, ct); - var version_2 = await sut.GetSnapshotAsync(2, ct); + await sut.ExecuteAsync(new DeletePermanent(), ct); - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(version_Empty, 0, EtagVersion.Empty); - AssertSnapshot(version_0, 3, 0); - AssertSnapshot(version_1, 4, 1); - AssertSnapshot(version_2, 5, 2); + AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty, false); - A.CallTo(() => state.PersistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A<HandleEvent>._)) - .MustHaveHappened(3, Times.Exactly); - } + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); - private static void AssertSnapshot(MyDomainState state, int value, long version, bool isDeleted = false) - { - Assert.Equal(new MyDomainState { Value = value, Version = version, IsDeleted = isDeleted }, state); - } + A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) + .MustHaveHappenedOnceExactly(); - private void SetupDeleted() - { - sut.ExecuteAsync(new Delete(), ct).Wait(ct); - } + A.CallTo(() => deleteStream.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - private void SetupCreated(int value) - { - state.AddEvent(new ValueChanged { Value = value }); - } + [Fact] + public async Task Should_get_old_versions_from_query() + { + await sut.ExecuteAsync(new CreateAuto { Value = 3 }, ct); + await sut.ExecuteAsync(new UpdateAuto { Value = 4 }, ct); + await sut.ExecuteAsync(new UpdateAuto { Value = 5 }, ct); + + var version_Empty = await sut.GetSnapshotAsync(EtagVersion.Empty, ct); + var version_0 = await sut.GetSnapshotAsync(0, ct); + var version_1 = await sut.GetSnapshotAsync(1, ct); + var version_2 = await sut.GetSnapshotAsync(2, ct); + + Assert.Empty(sut.GetUncomittedEvents()); + AssertSnapshot(version_Empty, 0, EtagVersion.Empty); + AssertSnapshot(version_0, 3, 0); + AssertSnapshot(version_1, 4, 1); + AssertSnapshot(version_2, 5, 2); + + A.CallTo(() => state.PersistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A<HandleEvent>._)) + .MustHaveHappened(3, Times.Exactly); + } - private void SetupCreated(params IEvent[] events) - { - events.Foreach(@event => state.AddEvent(@event)); - } + private static void AssertSnapshot(MyDomainState state, int value, long version, bool isDeleted = false) + { + Assert.Equal(new MyDomainState { Value = value, Version = version, IsDeleted = isDeleted }, state); + } + + private void SetupDeleted() + { + sut.ExecuteAsync(new Delete(), ct).Wait(ct); + } + + private void SetupCreated(int value) + { + state.AddEvent(new ValueChanged { Value = value }); + } + + private void SetupCreated(params IEvent[] events) + { + events.Foreach(@event => state.AddEvent(@event)); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs index fbfb4c0589..5a392b9a20 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs @@ -10,42 +10,41 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class EnrichWithTimestampCommandMiddlewareTests { - public class EnrichWithTimestampCommandMiddlewareTests + private readonly IClock clock = A.Fake<IClock>(); + private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); + private readonly EnrichWithTimestampCommandMiddleware sut; + + public EnrichWithTimestampCommandMiddlewareTests() { - private readonly IClock clock = A.Fake<IClock>(); - private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); - private readonly EnrichWithTimestampCommandMiddleware sut; + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - public EnrichWithTimestampCommandMiddlewareTests() + sut = new EnrichWithTimestampCommandMiddleware { - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - - sut = new EnrichWithTimestampCommandMiddleware - { - Clock = clock - }; - } + Clock = clock + }; + } - [Fact] - public async Task Should_set_timestamp_for_timestamp_command() - { - var command = new MyCommand(); + [Fact] + public async Task Should_set_timestamp_for_timestamp_command() + { + var command = new MyCommand(); - await sut.HandleAsync(new CommandContext(command, commandBus), default); + await sut.HandleAsync(new CommandContext(command, commandBus), default); - Assert.Equal(clock.GetCurrentInstant(), command.Timestamp); - } + Assert.Equal(clock.GetCurrentInstant(), command.Timestamp); + } - [Fact] - public async Task Should_do_nothing_for_normal_command() - { - await sut.HandleAsync(new CommandContext(A.Dummy<ICommand>(), commandBus), default); + [Fact] + public async Task Should_do_nothing_for_normal_command() + { + await sut.HandleAsync(new CommandContext(A.Dummy<ICommand>(), commandBus), default); - A.CallTo(() => clock.GetCurrentInstant()) - .MustNotHaveHappened(); - } + A.CallTo(() => clock.GetCurrentInstant()) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs index 84c9b6cc50..bbd9a180a5 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs @@ -8,99 +8,98 @@ using FakeItEasy; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class InMemoryCommandBusTests { - public class InMemoryCommandBusTests + private readonly ICommand command = A.Fake<ICommand>(); + + private sealed class HandledHandler : ICommandMiddleware { - private readonly ICommand command = A.Fake<ICommand>(); + public ICommand LastCommand { get; private set; } - private sealed class HandledHandler : ICommandMiddleware + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { - public ICommand LastCommand { get; private set; } + LastCommand = context.Command; - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - LastCommand = context.Command; + context.Complete(true); - context.Complete(true); - - return Task.FromResult(true); - } + return Task.FromResult(true); } + } - private sealed class NonHandledHandler : ICommandMiddleware - { - public ICommand LastCommand { get; private set; } + private sealed class NonHandledHandler : ICommandMiddleware + { + public ICommand LastCommand { get; private set; } - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - LastCommand = context.Command; + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + LastCommand = context.Command; - return Task.CompletedTask; - } + return Task.CompletedTask; } + } - private sealed class ThrowHandledHandler : ICommandMiddleware - { - public ICommand LastCommand { get; private set; } + private sealed class ThrowHandledHandler : ICommandMiddleware + { + public ICommand LastCommand { get; private set; } - public Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - LastCommand = context.Command; + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + LastCommand = context.Command; - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); } + } - [Fact] - public async Task Should_not_set_handled_if_no_handler_registered() - { - var sut = new InMemoryCommandBus(Array.Empty<ICommandMiddleware>()); + [Fact] + public async Task Should_not_set_handled_if_no_handler_registered() + { + var sut = new InMemoryCommandBus(Array.Empty<ICommandMiddleware>()); - var context = await sut.PublishAsync(command, default); + var context = await sut.PublishAsync(command, default); - Assert.False(context.IsCompleted); - } + Assert.False(context.IsCompleted); + } - [Fact] - public async Task Should_not_set_succeeded_if_handler_returns_false() - { - var handler = new NonHandledHandler(); + [Fact] + public async Task Should_not_set_succeeded_if_handler_returns_false() + { + var handler = new NonHandledHandler(); - var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); + var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); - var context = await sut.PublishAsync(command, default); + var context = await sut.PublishAsync(command, default); - Assert.Equal(command, handler.LastCommand); - Assert.False(context.IsCompleted); - } + Assert.Equal(command, handler.LastCommand); + Assert.False(context.IsCompleted); + } - [Fact] - public async Task Should_set_succeeded_if_handler_marks_completed() - { - var handler = new HandledHandler(); + [Fact] + public async Task Should_set_succeeded_if_handler_marks_completed() + { + var handler = new HandledHandler(); - var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); + var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); - var context = await sut.PublishAsync(command, default); + var context = await sut.PublishAsync(command, default); - Assert.Equal(command, handler.LastCommand); - Assert.True(context.IsCompleted); - } + Assert.Equal(command, handler.LastCommand); + Assert.True(context.IsCompleted); + } - [Fact] - public async Task Should_throw_and_call_all_handlers_if_first_handler_fails() - { - var handler = new ThrowHandledHandler(); + [Fact] + public async Task Should_throw_and_call_all_handlers_if_first_handler_fails() + { + var handler = new ThrowHandledHandler(); - var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); + var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); - await Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.PublishAsync(command, default)); + await Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.PublishAsync(command, default)); - Assert.Equal(command, handler.LastCommand); - } + Assert.Equal(command, handler.LastCommand); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs index 222b93d218..cdd06f9504 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs @@ -9,77 +9,76 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class LogCommandMiddlewareTests { - public class LogCommandMiddlewareTests + private readonly ILogger<LogCommandMiddleware> log = A.Fake<ILogger<LogCommandMiddleware>>(); + private readonly LogCommandMiddleware sut; + private readonly ICommand command = A.Dummy<ICommand>(); + private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); + + public LogCommandMiddlewareTests() { - private readonly ILogger<LogCommandMiddleware> log = A.Fake<ILogger<LogCommandMiddleware>>(); - private readonly LogCommandMiddleware sut; - private readonly ICommand command = A.Dummy<ICommand>(); - private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); + A.CallTo(() => log.IsEnabled(A<LogLevel>._)) + .Returns(true); - public LogCommandMiddlewareTests() - { - A.CallTo(() => log.IsEnabled(A<LogLevel>._)) - .Returns(true); + sut = new LogCommandMiddleware(log); + } - sut = new LogCommandMiddleware(log); - } + [Fact] + public async Task Should_log_before_and_after_request() + { + var context = new CommandContext(command, commandBus); - [Fact] - public async Task Should_log_before_and_after_request() + await sut.HandleAsync(context, (c, ct) => { - var context = new CommandContext(command, commandBus); + context.Complete(true); - await sut.HandleAsync(context, (c, ct) => - { - context.Complete(true); + return Task.CompletedTask; + }, default); - return Task.CompletedTask; - }, default); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Debug) + .MustHaveHappened(); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Debug) - .MustHaveHappened(); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Information) + .MustHaveHappenedTwiceExactly(); + } - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Information) - .MustHaveHappenedTwiceExactly(); - } + [Fact] + public async Task Should_log_error_if_command_failed() + { + var context = new CommandContext(command, commandBus); - [Fact] - public async Task Should_log_error_if_command_failed() + await Assert.ThrowsAsync<InvalidOperationException>(async () => { - var context = new CommandContext(command, commandBus); - - await Assert.ThrowsAsync<InvalidOperationException>(async () => - { - await sut.HandleAsync(context, (c, ct) => throw new InvalidOperationException(), default); - }); + await sut.HandleAsync(context, (c, ct) => throw new InvalidOperationException(), default); + }); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Debug) - .MustHaveHappened(); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Debug) + .MustHaveHappened(); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Information) - .MustHaveHappened(); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Information) + .MustHaveHappened(); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Error) - .MustHaveHappened(); - } + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Error) + .MustHaveHappened(); + } - [Fact] - public async Task Should_log_if_command_is_not_handled() - { - var context = new CommandContext(command, commandBus); + [Fact] + public async Task Should_log_if_command_is_not_handled() + { + var context = new CommandContext(command, commandBus); - await sut.HandleAsync(context, (c, ct) => Task.CompletedTask, default); + await sut.HandleAsync(context, (c, ct) => Task.CompletedTask, default); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Debug) - .MustHaveHappened(); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Debug) + .MustHaveHappened(); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Information) - .MustHaveHappenedTwiceExactly(); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Information) + .MustHaveHappenedTwiceExactly(); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Critical) - .MustHaveHappened(); - } + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Critical) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs index 7d69bb1910..203c7f5305 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs @@ -9,52 +9,51 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Squidex.Infrastructure.Commands +namespace Squidex.Infrastructure.Commands; + +public class ReadonlyCommandMiddlewareTests { - public class ReadonlyCommandMiddlewareTests - { - private readonly ICommand command = A.Dummy<ICommand>(); - private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); - private readonly ReadonlyOptions options = new ReadonlyOptions(); - private readonly ReadonlyCommandMiddleware sut; + private readonly ICommand command = A.Dummy<ICommand>(); + private readonly ICommandBus commandBus = A.Dummy<ICommandBus>(); + private readonly ReadonlyOptions options = new ReadonlyOptions(); + private readonly ReadonlyCommandMiddleware sut; - public ReadonlyCommandMiddlewareTests() - { - sut = new ReadonlyCommandMiddleware(Options.Create(options)); - } + public ReadonlyCommandMiddlewareTests() + { + sut = new ReadonlyCommandMiddleware(Options.Create(options)); + } - [Fact] - public async Task Should_throw_exception_if_readonly() - { - var context = new CommandContext(command, commandBus); + [Fact] + public async Task Should_throw_exception_if_readonly() + { + var context = new CommandContext(command, commandBus); - options.IsReadonly = true; + options.IsReadonly = true; - await Assert.ThrowsAsync<DomainException>(() => MakeCallAsync(context)); + await Assert.ThrowsAsync<DomainException>(() => MakeCallAsync(context)); - Assert.False(context.IsCompleted); - } + Assert.False(context.IsCompleted); + } - [Fact] - public async Task Should_not_throw_exception_if_not_readonly() - { - var context = new CommandContext(command, commandBus); + [Fact] + public async Task Should_not_throw_exception_if_not_readonly() + { + var context = new CommandContext(command, commandBus); - options.IsReadonly = false; + options.IsReadonly = false; - await MakeCallAsync(context); + await MakeCallAsync(context); - Assert.True(context.IsCompleted); - } + Assert.True(context.IsCompleted); + } - private async Task MakeCallAsync(CommandContext context) + private async Task MakeCallAsync(CommandContext context) + { + await sut.HandleAsync(context, (c, ct) => { - await sut.HandleAsync(context, (c, ct) => - { - context.Complete(true); + context.Complete(true); - return Task.CompletedTask; - }, default); - } + return Task.CompletedTask; + }, default); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs index 674d162b2e..bd9bcead08 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs @@ -7,54 +7,53 @@ using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class DisposableObjectBaseTests { - public class DisposableObjectBaseTests + public sealed class MyDisposableObject : DisposableObjectBase { - public sealed class MyDisposableObject : DisposableObjectBase - { - public int DisposeCallCount { get; set; } - - protected override void DisposeObject(bool disposing) - { - DisposeCallCount++; - } + public int DisposeCallCount { get; set; } - public void Ensure() - { - ThrowIfDisposed(); - } + protected override void DisposeObject(bool disposing) + { + DisposeCallCount++; } - [Fact] - public void Should_not_throw_exception_if_not_disposed() + public void Ensure() { - var sut = new MyDisposableObject(); - - sut.Ensure(); + ThrowIfDisposed(); } + } - [Fact] - public void Should_dispose_once() - { - var sut = new MyDisposableObject(); + [Fact] + public void Should_not_throw_exception_if_not_disposed() + { + var sut = new MyDisposableObject(); - sut.Dispose(); - sut.Dispose(); + sut.Ensure(); + } - Assert.True(sut.IsDisposed); + [Fact] + public void Should_dispose_once() + { + var sut = new MyDisposableObject(); - Assert.Equal(1, sut.DisposeCallCount); - } + sut.Dispose(); + sut.Dispose(); - [Fact] - public void Should_throw_exception_if_disposed() - { - var sut = new MyDisposableObject(); + Assert.True(sut.IsDisposed); + + Assert.Equal(1, sut.DisposeCallCount); + } + + [Fact] + public void Should_throw_exception_if_disposed() + { + var sut = new MyDisposableObject(); - sut.Dispose(); + sut.Dispose(); - Assert.Throws<ObjectDisposedException>(() => sut.Ensure()); - } + Assert.Throws<ObjectDisposedException>(() => sut.Ensure()); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs index e64af0cea2..9b5c52c8f0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs @@ -11,171 +11,170 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class DomainIdTests { - public class DomainIdTests - { - private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(DomainId)); + private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(DomainId)); - public class MyTest + public class MyTest + { + [JsonIgnore] + public DomainId Calculated { - [JsonIgnore] - public DomainId Calculated - { - get => DomainId.Combine(Id0, Id1.Id); - } + get => DomainId.Combine(Id0, Id1.Id); + } - public DomainId Id0 { get; set; } + public DomainId Id0 { get; set; } - public NamedId<DomainId> Id1 { get; set; } + public NamedId<DomainId> Id1 { get; set; } - public NamedId<DomainId> Id2 { get; set; } - } + public NamedId<DomainId> Id2 { get; set; } + } - [Fact] - public void Should_initialize_default() - { - DomainId domainId = default; + [Fact] + public void Should_initialize_default() + { + DomainId domainId = default; - Assert.Equal(Guid.Empty.ToString(), domainId.ToString()); - } + Assert.Equal(Guid.Empty.ToString(), domainId.ToString()); + } - [Fact] - public void Should_initialize_default_from_string() - { - var domainId = DomainId.Create(Guid.Empty.ToString()); + [Fact] + public void Should_initialize_default_from_string() + { + var domainId = DomainId.Create(Guid.Empty.ToString()); - Assert.Equal(DomainId.Empty, domainId); - } + Assert.Equal(DomainId.Empty, domainId); + } - [Fact] - public void Should_create_nullable_from_string() - { - var domainId = DomainId.CreateNullable(null); + [Fact] + public void Should_create_nullable_from_string() + { + var domainId = DomainId.CreateNullable(null); - Assert.Null(domainId); - } + Assert.Null(domainId); + } - [Fact] - public void Should_convert_from_string() - { - var text = "123"; + [Fact] + public void Should_convert_from_string() + { + var text = "123"; - var actual = typeConverter.ConvertFromString(text); + var actual = typeConverter.ConvertFromString(text); - Assert.Equal(DomainId.Create(text), actual); - } + Assert.Equal(DomainId.Create(text), actual); + } - [Fact] - public void Should_convert_from_guid() - { - var guid = Guid.NewGuid(); + [Fact] + public void Should_convert_from_guid() + { + var guid = Guid.NewGuid(); - var actual = typeConverter.ConvertFrom(guid); + var actual = typeConverter.ConvertFrom(guid); - Assert.Equal(guid.ToString(), actual?.ToString()); - } + Assert.Equal(guid.ToString(), actual?.ToString()); + } - [Fact] - public void Should_convert_to_string() - { - var text = "123"; + [Fact] + public void Should_convert_to_string() + { + var text = "123"; - var actual = typeConverter.ConvertToString(DomainId.Create(text)); + var actual = typeConverter.ConvertToString(DomainId.Create(text)); - Assert.Equal(text, actual); - } + Assert.Equal(text, actual); + } - [Fact] - public void Should_initialize_domainId_from_guid() - { - var guid = Guid.NewGuid(); + [Fact] + public void Should_initialize_domainId_from_guid() + { + var guid = Guid.NewGuid(); - var domainId = DomainId.Create(guid); + var domainId = DomainId.Create(guid); - Assert.Equal(guid.ToString(), domainId.ToString()); - } + Assert.Equal(guid.ToString(), domainId.ToString()); + } - [Fact] - public void Should_initialize_domainId_from_string() - { - var text = "Custom"; + [Fact] + public void Should_initialize_domainId_from_string() + { + var text = "Custom"; - var domainId = DomainId.Create(text); + var domainId = DomainId.Create(text); - Assert.Equal(text, domainId.ToString()); - } + Assert.Equal(text, domainId.ToString()); + } - [Fact] - public void Should_compare_with_nullable() - { - DomainId? value = DomainId.Empty; + [Fact] + public void Should_compare_with_nullable() + { + DomainId? value = DomainId.Empty; - Assert.True(value == DomainId.Empty); - } + Assert.True(value == DomainId.Empty); + } - [Fact] - public void Should_compare_with_nullable2() - { - DomainId? value = DomainId.Create(Guid.Empty.ToString()); + [Fact] + public void Should_compare_with_nullable2() + { + DomainId? value = DomainId.Create(Guid.Empty.ToString()); - Assert.True(value == DomainId.Empty); - } + Assert.True(value == DomainId.Empty); + } - [Fact] - public void Should_compare_with_non_shared_nullable() - { - DomainId? value = DomainId.Create("0"); + [Fact] + public void Should_compare_with_non_shared_nullable() + { + DomainId? value = DomainId.Create("0"); - Assert.True(value == DomainId.Create("0")); - } + Assert.True(value == DomainId.Create("0")); + } - [Fact] - public void Should_make_correct_equal_comparisons() - { - var domainId_1_a = DomainId.Create("1"); - var domainId_1_b = DomainId.Create("1"); + [Fact] + public void Should_make_correct_equal_comparisons() + { + var domainId_1_a = DomainId.Create("1"); + var domainId_1_b = DomainId.Create("1"); - var domainId_2_a = DomainId.Create("2"); + var domainId_2_a = DomainId.Create("2"); - Assert.Equal(domainId_1_a, domainId_1_b); - Assert.Equal(domainId_1_a.GetHashCode(), domainId_1_b.GetHashCode()); - Assert.True(domainId_1_a.Equals((object)domainId_1_b)); + Assert.Equal(domainId_1_a, domainId_1_b); + Assert.Equal(domainId_1_a.GetHashCode(), domainId_1_b.GetHashCode()); + Assert.True(domainId_1_a.Equals((object)domainId_1_b)); - Assert.NotEqual(domainId_1_a, domainId_2_a); - Assert.NotEqual(domainId_1_a.GetHashCode(), domainId_2_a.GetHashCode()); - Assert.False(domainId_1_a.Equals((object)domainId_2_a)); + Assert.NotEqual(domainId_1_a, domainId_2_a); + Assert.NotEqual(domainId_1_a.GetHashCode(), domainId_2_a.GetHashCode()); + Assert.False(domainId_1_a.Equals((object)domainId_2_a)); - Assert.True(domainId_1_a == domainId_1_b); - Assert.True(domainId_1_a != domainId_2_a); + Assert.True(domainId_1_a == domainId_1_b); + Assert.True(domainId_1_a != domainId_2_a); - Assert.False(domainId_1_a != domainId_1_b); - Assert.False(domainId_1_a == domainId_2_a); - } + Assert.False(domainId_1_a != domainId_1_b); + Assert.False(domainId_1_a == domainId_2_a); + } - [Fact] - public void Should_serialize_and_deserialize() - { - var domainId = DomainId.Create("123"); + [Fact] + public void Should_serialize_and_deserialize() + { + var domainId = DomainId.Create("123"); - var serialized = domainId.SerializeAndDeserialize(); + var serialized = domainId.SerializeAndDeserialize(); - Assert.Equal(domainId, serialized); - } + Assert.Equal(domainId, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_in_object() + [Fact] + public void Should_serialize_and_deserialize_in_object() + { + var obj = new MyTest { - var obj = new MyTest - { - Id0 = DomainId.NewGuid(), - Id1 = NamedId.Of(DomainId.NewGuid(), "1"), - Id2 = NamedId.Of(DomainId.NewGuid(), "2") - }; + Id0 = DomainId.NewGuid(), + Id1 = NamedId.Of(DomainId.NewGuid(), "1"), + Id2 = NamedId.Of(DomainId.NewGuid(), "2") + }; - var serialized = obj.SerializeAndDeserialize(); + var serialized = obj.SerializeAndDeserialize(); - serialized.Should().BeEquivalentTo(obj); - } + serialized.Should().BeEquivalentTo(obj); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs index 93467d196f..637552980a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/DomainObjectExceptionTests.cs @@ -8,50 +8,49 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class DomainObjectExceptionTests { - public class DomainObjectExceptionTests + [Fact] + public void Should_serialize_and_deserialize_DomainException() + { + var source = new DomainException("Message", "ErrorCode"); + var actual = source.SerializeAndDeserializeBinary(); + + Assert.Equal(actual.ErrorCode, source.ErrorCode); + Assert.Equal(actual.Message, source.Message); + } + + [Fact] + public void Should_serialize_and_deserialize_DomainObjectDeletedException() + { + var source = new DomainObjectDeletedException("123"); + var actual = source.SerializeAndDeserializeBinary(); + + Assert.Equal(actual.Id, source.Id); + Assert.Equal(actual.Message, source.Message); + } + + [Fact] + public void Should_serialize_and_deserialize_DomainObjectNotFoundException() { - [Fact] - public void Should_serialize_and_deserialize_DomainException() - { - var source = new DomainException("Message", "ErrorCode"); - var actual = source.SerializeAndDeserializeBinary(); - - Assert.Equal(actual.ErrorCode, source.ErrorCode); - Assert.Equal(actual.Message, source.Message); - } - - [Fact] - public void Should_serialize_and_deserialize_DomainObjectDeletedException() - { - var source = new DomainObjectDeletedException("123"); - var actual = source.SerializeAndDeserializeBinary(); - - Assert.Equal(actual.Id, source.Id); - Assert.Equal(actual.Message, source.Message); - } - - [Fact] - public void Should_serialize_and_deserialize_DomainObjectNotFoundException() - { - var source = new DomainObjectNotFoundException("123"); - var actual = source.SerializeAndDeserializeBinary(); - - Assert.Equal(actual.Id, source.Id); - Assert.Equal(actual.Message, source.Message); - } - - [Fact] - public void Should_serialize_and_deserialize_DomainObjectVersionExceptionn() - { - var source = new DomainObjectVersionException("123", 100, 200); - var actual = source.SerializeAndDeserializeBinary(); - - Assert.Equal(actual.Id, source.Id); - Assert.Equal(actual.ExpectedVersion, source.ExpectedVersion); - Assert.Equal(actual.CurrentVersion, source.CurrentVersion); - Assert.Equal(actual.Message, source.Message); - } + var source = new DomainObjectNotFoundException("123"); + var actual = source.SerializeAndDeserializeBinary(); + + Assert.Equal(actual.Id, source.Id); + Assert.Equal(actual.Message, source.Message); + } + + [Fact] + public void Should_serialize_and_deserialize_DomainObjectVersionExceptionn() + { + var source = new DomainObjectVersionException("123", 100, 200); + var actual = source.SerializeAndDeserializeBinary(); + + Assert.Equal(actual.Id, source.Id); + Assert.Equal(actual.ExpectedVersion, source.ExpectedVersion); + Assert.Equal(actual.CurrentVersion, source.CurrentVersion); + Assert.Equal(actual.Message, source.Message); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Email/SmtpEmailSenderTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Email/SmtpEmailSenderTests.cs index 446e7a1e00..d2f9967124 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Email/SmtpEmailSenderTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Email/SmtpEmailSenderTests.cs @@ -10,27 +10,26 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Email +namespace Squidex.Infrastructure.Email; + +[Trait("Category", "Dependencies")] +public class SmtpEmailSenderTests { - [Trait("Category", "Dependencies")] - public class SmtpEmailSenderTests + [Fact] + public async Task Should_handle_timeout_properly() { - [Fact] - public async Task Should_handle_timeout_properly() - { - var options = TestConfig.Configuration.GetSection("email:smtp").Get<SmtpOptions>(); + var options = TestConfig.Configuration.GetSection("email:smtp").Get<SmtpOptions>(); - var recipient = TestConfig.Configuration["email:smtp:recipient"]; + var recipient = TestConfig.Configuration["email:smtp:recipient"]; - var testSubject = TestConfig.Configuration["email:smtp:testSubject"]; - var testBody = TestConfig.Configuration["email:smtp:testBody"]; + var testSubject = TestConfig.Configuration["email:smtp:testSubject"]; + var testBody = TestConfig.Configuration["email:smtp:testBody"]; - var sut = new SmtpEmailSender(Options.Create(options)); + var sut = new SmtpEmailSender(Options.Create(options)); - using (var cts = new CancellationTokenSource(5000)) - { - await sut.SendAsync(recipient, testSubject, testBody, cts.Token); - } + using (var cts = new CancellationTokenSource(5000)) + { + await sut.SendAsync(recipient, testSubject, testBody, cts.Token); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs index 8b60c0a92b..af1310c1dd 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs @@ -12,132 +12,131 @@ using Squidex.Messaging; using Xunit; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public class EventConsumerManagerTests { - public class EventConsumerManagerTests + private readonly IPersistenceFactory<EventConsumerState> persistenceFactory = A.Fake<IPersistenceFactory<EventConsumerState>>(); + private readonly IMessageBus messaging = A.Fake<IMessageBus>(); + private readonly string consumerName1 = Guid.NewGuid().ToString(); + private readonly string consumerName2 = Guid.NewGuid().ToString(); + private readonly EventConsumerManager sut; + + public EventConsumerManagerTests() { - private readonly IPersistenceFactory<EventConsumerState> persistenceFactory = A.Fake<IPersistenceFactory<EventConsumerState>>(); - private readonly IMessageBus messaging = A.Fake<IMessageBus>(); - private readonly string consumerName1 = Guid.NewGuid().ToString(); - private readonly string consumerName2 = Guid.NewGuid().ToString(); - private readonly EventConsumerManager sut; + var consumer1 = A.Fake<IEventConsumer>(); + var consumer2 = A.Fake<IEventConsumer>(); - public EventConsumerManagerTests() - { - var consumer1 = A.Fake<IEventConsumer>(); - var consumer2 = A.Fake<IEventConsumer>(); + A.CallTo(() => consumer1.Name) + .Returns(consumerName1); - A.CallTo(() => consumer1.Name) - .Returns(consumerName1); + A.CallTo(() => consumer2.Name) + .Returns(consumerName2); - A.CallTo(() => consumer2.Name) - .Returns(consumerName2); + sut = new EventConsumerManager(persistenceFactory, new[] { consumer1, consumer2 }, messaging); + } - sut = new EventConsumerManager(persistenceFactory, new[] { consumer1, consumer2 }, messaging); - } + [Fact] + public async Task Should_get_states_from_store_without_old_consumer() + { + var snapshotStore = A.Fake<ISnapshotStore<EventConsumerState>>(); - [Fact] - public async Task Should_get_states_from_store_without_old_consumer() - { - var snapshotStore = A.Fake<ISnapshotStore<EventConsumerState>>(); - - A.CallTo(() => persistenceFactory.Snapshots) - .Returns(snapshotStore); - - A.CallTo(() => snapshotStore.ReadAllAsync(default)) - .Returns(new List<SnapshotResult<EventConsumerState>> - { - new SnapshotResult<EventConsumerState>(DomainId.Create(consumerName1), - new EventConsumerState - { - Position = "1" - }, 1), - new SnapshotResult<EventConsumerState>(DomainId.Create(consumerName2), - new EventConsumerState - { - Position = "2" - }, 2), - new SnapshotResult<EventConsumerState>(DomainId.Create("oldConsumer"), - new EventConsumerState - { - Position = "2" - }, 2) - }.ToAsyncEnumerable()); - - var actual = await sut.GetConsumersAsync(default); - - actual.Should().BeEquivalentTo( - new List<EventConsumerInfo> - { - new EventConsumerInfo { Name = consumerName1, Position = "1" }, - new EventConsumerInfo { Name = consumerName2, Position = "2" } - }); - } - - [Fact] - public async Task Should_throw_exception_when_calling_old_consumer() - { - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.StartAsync("oldConsumer", default)); - } + A.CallTo(() => persistenceFactory.Snapshots) + .Returns(snapshotStore); - [Fact] - public async Task Should_publish_event_on_start() + A.CallTo(() => snapshotStore.ReadAllAsync(default)) + .Returns(new List<SnapshotResult<EventConsumerState>> + { + new SnapshotResult<EventConsumerState>(DomainId.Create(consumerName1), + new EventConsumerState + { + Position = "1" + }, 1), + new SnapshotResult<EventConsumerState>(DomainId.Create(consumerName2), + new EventConsumerState + { + Position = "2" + }, 2), + new SnapshotResult<EventConsumerState>(DomainId.Create("oldConsumer"), + new EventConsumerState + { + Position = "2" + }, 2) + }.ToAsyncEnumerable()); + + var actual = await sut.GetConsumersAsync(default); + + actual.Should().BeEquivalentTo( + new List<EventConsumerInfo> + { + new EventConsumerInfo { Name = consumerName1, Position = "1" }, + new EventConsumerInfo { Name = consumerName2, Position = "2" } + }); + } + + [Fact] + public async Task Should_throw_exception_when_calling_old_consumer() + { + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.StartAsync("oldConsumer", default)); + } + + [Fact] + public async Task Should_publish_event_on_start() + { + var testState = new TestState<EventConsumerState>(consumerName1, persistenceFactory) { - var testState = new TestState<EventConsumerState>(consumerName1, persistenceFactory) + Snapshot = new EventConsumerState { - Snapshot = new EventConsumerState - { - Position = "42" - }, - Version = 0 - }; + Position = "42" + }, + Version = 0 + }; - var response = await sut.StartAsync(consumerName1, default); + var response = await sut.StartAsync(consumerName1, default); - A.CallTo(() => messaging.PublishAsync(new EventConsumerStart(consumerName1), null, default)) - .MustHaveHappened(); + A.CallTo(() => messaging.PublishAsync(new EventConsumerStart(consumerName1), null, default)) + .MustHaveHappened(); - Assert.Equal("42", response.Position); - } + Assert.Equal("42", response.Position); + } - [Fact] - public async Task Should_publish_event_on_stop() + [Fact] + public async Task Should_publish_event_on_stop() + { + var testState = new TestState<EventConsumerState>(consumerName1, persistenceFactory) { - var testState = new TestState<EventConsumerState>(consumerName1, persistenceFactory) + Snapshot = new EventConsumerState { - Snapshot = new EventConsumerState - { - Position = "42" - }, - Version = 0 - }; + Position = "42" + }, + Version = 0 + }; - var response = await sut.StopAsync(consumerName1, default); + var response = await sut.StopAsync(consumerName1, default); - A.CallTo(() => messaging.PublishAsync(new EventConsumerStop(consumerName1), null, default)) - .MustHaveHappened(); + A.CallTo(() => messaging.PublishAsync(new EventConsumerStop(consumerName1), null, default)) + .MustHaveHappened(); - Assert.Equal("42", response.Position); - } + Assert.Equal("42", response.Position); + } - [Fact] - public async Task Should_publish_event_on_reset() + [Fact] + public async Task Should_publish_event_on_reset() + { + var testState = new TestState<EventConsumerState>(consumerName1, persistenceFactory) { - var testState = new TestState<EventConsumerState>(consumerName1, persistenceFactory) + Snapshot = new EventConsumerState { - Snapshot = new EventConsumerState - { - Position = "42" - }, - Version = 0 - }; + Position = "42" + }, + Version = 0 + }; - var response = await sut.ResetAsync(consumerName1, default); + var response = await sut.ResetAsync(consumerName1, default); - A.CallTo(() => messaging.PublishAsync(new EventConsumerReset(consumerName1), null, default)) - .MustHaveHappened(); + A.CallTo(() => messaging.PublishAsync(new EventConsumerReset(consumerName1), null, default)) + .MustHaveHappened(); - Assert.Equal("42", response.Position); - } + Assert.Equal("42", response.Position); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorIntegrationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorIntegrationTests.cs index 43a84acd5e..8e5994fc12 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorIntegrationTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorIntegrationTests.cs @@ -13,130 +13,129 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public abstract class EventConsumerProcessorIntegrationTests { - public abstract class EventConsumerProcessorIntegrationTests + private readonly Lazy<IEventStore> store; + + public sealed class EventConsumer : IEventConsumer { - private readonly Lazy<IEventStore> store; + public List<Guid> Events { get; } = new List<Guid>(); - public sealed class EventConsumer : IEventConsumer - { - public List<Guid> Events { get; } = new List<Guid>(); + public string Name => "Consumer"; - public string Name => "Consumer"; + public Func<int, Task> EventReceived { get; set; } - public Func<int, Task> EventReceived { get; set; } + public async Task On(Envelope<IEvent> @event) + { + Events.Add(@event.Headers.EventId()); - public async Task On(Envelope<IEvent> @event) + if (EventReceived != null) { - Events.Add(@event.Headers.EventId()); - - if (EventReceived != null) - { - await EventReceived(Events.Count); - } + await EventReceived(Events.Count); } } + } - protected IEventStore EventStore - { - get => store.Value; - } + protected IEventStore EventStore + { + get => store.Value; + } - protected EventConsumerProcessorIntegrationTests() - { + protected EventConsumerProcessorIntegrationTests() + { #pragma warning disable MA0056 // Do not call overridable members in constructor - store = new Lazy<IEventStore>(CreateStore); + store = new Lazy<IEventStore>(CreateStore); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } - public abstract IEventStore CreateStore(); + public abstract IEventStore CreateStore(); - [Theory] - [InlineData(0)] - [InlineData(100)] - public async Task Should_subscribe_with_parallel_writes(int startStop) + [Theory] + [InlineData(0)] + [InlineData(100)] + public async Task Should_subscribe_with_parallel_writes(int startStop) + { + var numTasks = 100; + var numEvents = 50; + + var eventConsumer = new EventConsumer(); + + var mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); + var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); + + var typeFactory = new TypeNameRegistry().Map(typeof(MyEvent), "MyEvent"); + + var services = new ServiceCollection() + .AddLogging() + .AddSingleton(TestUtils.DefaultSerializer) + .AddSingleton(EventStore) + .AddSingleton(eventConsumer) + .AddSingleton(mongoClient) + .AddSingleton(mongoDatabase) + .AddSingleton(typeFactory) + .AddSingleton(typeof(IPersistenceFactory<>), typeof(Store<>)) + .AddSingleton<EventConsumerProcessor>() + .AddSingleton<IEventConsumer>(eventConsumer) + .AddSingleton<IEventFormatter, DefaultEventFormatter>() + .AddSingleton<IEventStreamNames, DefaultEventStreamNames>() + .AddSingleton(typeof(ISnapshotStore<>), typeof(MongoSnapshotStore<>)) + .BuildServiceProvider(); + + var processor = services.GetRequiredService<EventConsumerProcessor>(); + + var persistenceFactory = services.GetRequiredService<IPersistenceFactory<None>>(); + + // Also start the event consumer, because it might be stopped from previous run. + await processor.InitializeAsync(default); + await processor.ActivateAsync(); + await processor.StartAsync(); + + async Task StartStop() { - var numTasks = 100; - var numEvents = 50; - - var eventConsumer = new EventConsumer(); - - var mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); - var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); - - var typeFactory = new TypeNameRegistry().Map(typeof(MyEvent), "MyEvent"); - - var services = new ServiceCollection() - .AddLogging() - .AddSingleton(TestUtils.DefaultSerializer) - .AddSingleton(EventStore) - .AddSingleton(eventConsumer) - .AddSingleton(mongoClient) - .AddSingleton(mongoDatabase) - .AddSingleton(typeFactory) - .AddSingleton(typeof(IPersistenceFactory<>), typeof(Store<>)) - .AddSingleton<EventConsumerProcessor>() - .AddSingleton<IEventConsumer>(eventConsumer) - .AddSingleton<IEventFormatter, DefaultEventFormatter>() - .AddSingleton<IEventStreamNames, DefaultEventStreamNames>() - .AddSingleton(typeof(ISnapshotStore<>), typeof(MongoSnapshotStore<>)) - .BuildServiceProvider(); - - var processor = services.GetRequiredService<EventConsumerProcessor>(); - - var persistenceFactory = services.GetRequiredService<IPersistenceFactory<None>>(); - - // Also start the event consumer, because it might be stopped from previous run. - await processor.InitializeAsync(default); - await processor.ActivateAsync(); + await processor.StopAsync(); await processor.StartAsync(); + } - async Task StartStop() + eventConsumer.EventReceived = i => + { + if (startStop > 0 && i % startStop == 0) { - await processor.StopAsync(); - await processor.StartAsync(); + // Do not await the task here, other wise we could create deadlock. + StartStop().Forget(); } - eventConsumer.EventReceived = i => - { - if (startStop > 0 && i % startStop == 0) - { - // Do not await the task here, other wise we could create deadlock. - StartStop().Forget(); - } + return Task.CompletedTask; + }; - return Task.CompletedTask; - }; + // Create events in parallel. + await Parallel.ForEachAsync(Enumerable.Range(0, numTasks), async (i, ct) => + { + var persistence = persistenceFactory.WithEventSourcing(typeof(None), DomainId.NewGuid(), null); - // Create events in parallel. - await Parallel.ForEachAsync(Enumerable.Range(0, numTasks), async (i, ct) => + for (var j = 0; j < numEvents; j++) { - var persistence = persistenceFactory.WithEventSourcing(typeof(None), DomainId.NewGuid(), null); - - for (var j = 0; j < numEvents; j++) + await persistence.WriteEventsAsync(new List<Envelope<IEvent>> { - await persistence.WriteEventsAsync(new List<Envelope<IEvent>> - { - Envelope.Create(new MyEvent()) - }); - } - }); + Envelope.Create(new MyEvent()) + }); + } + }); - var expectedEvents = numEvents * numTasks; + var expectedEvents = numEvents * numTasks; - // Wait for all events to arrive. - using (var cts = new CancellationTokenSource(20000)) + // Wait for all events to arrive. + using (var cts = new CancellationTokenSource(20000)) + { + while (!cts.IsCancellationRequested && eventConsumer.Events.Count < expectedEvents) { - while (!cts.IsCancellationRequested && eventConsumer.Events.Count < expectedEvents) - { - await Task.Delay(100); - } + await Task.Delay(100); } + } - await processor.StopAsync(); + await processor.StopAsync(); - Assert.Equal(expectedEvents, eventConsumer.Events.Count); - } + Assert.Equal(expectedEvents, eventConsumer.Events.Count); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs index 8446d77e63..54248d537b 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs @@ -12,562 +12,561 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public class EventConsumerProcessorTests { - public class EventConsumerProcessorTests + public sealed class MyEventConsumerProcessor : EventConsumerProcessor { - public sealed class MyEventConsumerProcessor : EventConsumerProcessor + public IEventSubscriber<StoredEvent>? Subscriber { get; set; } + + public MyEventConsumerProcessor( + IPersistenceFactory<EventConsumerState> persistenceFactory, + IEventConsumer eventConsumer, + IEventFormatter eventFormatter, + IEventStore eventStore, + ILogger<EventConsumerProcessor> log) + : base(persistenceFactory, eventConsumer, eventFormatter, eventStore, log) { - public IEventSubscriber<StoredEvent>? Subscriber { get; set; } - - public MyEventConsumerProcessor( - IPersistenceFactory<EventConsumerState> persistenceFactory, - IEventConsumer eventConsumer, - IEventFormatter eventFormatter, - IEventStore eventStore, - ILogger<EventConsumerProcessor> log) - : base(persistenceFactory, eventConsumer, eventFormatter, eventStore, log) - { - } + } - protected override IEventSubscription CreateRetrySubscription(IEventSubscriber<ParsedEvents> subscriber) - { - return CreatePipeline(subscriber); - } + protected override IEventSubscription CreateRetrySubscription(IEventSubscriber<ParsedEvents> subscriber) + { + return CreatePipeline(subscriber); + } - protected override IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> subscriber) - { - Subscriber = subscriber; + protected override IEventSubscription CreateSubscription(IEventSubscriber<StoredEvent> subscriber) + { + Subscriber = subscriber; - return base.CreateSubscription(subscriber); - } + return base.CreateSubscription(subscriber); } + } - private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>(); - private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>(); - private readonly TestState<EventConsumerState> state; - private readonly StoredEvent storedEvent; - private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); - private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent()); - private readonly MyEventConsumerProcessor sut; - private readonly string consumerName = Guid.NewGuid().ToString(); - private readonly string initialPosition = Guid.NewGuid().ToString(); - - public EventConsumerProcessorTests() + private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>(); + private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>(); + private readonly TestState<EventConsumerState> state; + private readonly StoredEvent storedEvent; + private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); + private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent()); + private readonly MyEventConsumerProcessor sut; + private readonly string consumerName = Guid.NewGuid().ToString(); + private readonly string initialPosition = Guid.NewGuid().ToString(); + + public EventConsumerProcessorTests() + { + state = new TestState<EventConsumerState>(DomainId.Create(consumerName)) { - state = new TestState<EventConsumerState>(DomainId.Create(consumerName)) + Snapshot = new EventConsumerState { - Snapshot = new EventConsumerState - { - Position = initialPosition - } - }; + Position = initialPosition + } + }; - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) - .Returns(eventSubscription); + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) + .Returns(eventSubscription); - A.CallTo(() => eventConsumer.Name) - .Returns(consumerName); + A.CallTo(() => eventConsumer.Name) + .Returns(consumerName); - A.CallTo(() => eventConsumer.CanClear) - .Returns(true); + A.CallTo(() => eventConsumer.CanClear) + .Returns(true); - A.CallTo(() => eventConsumer.Handles(A<StoredEvent>._)) - .Returns(true); + A.CallTo(() => eventConsumer.Handles(A<StoredEvent>._)) + .Returns(true); - A.CallTo(() => eventConsumer.On(A<IEnumerable<Envelope<IEvent>>>._)) - .Invokes((IEnumerable<Envelope<IEvent>> events) => + A.CallTo(() => eventConsumer.On(A<IEnumerable<Envelope<IEvent>>>._)) + .Invokes((IEnumerable<Envelope<IEvent>> events) => + { + foreach (var @event in events) { - foreach (var @event in events) - { - eventConsumer.On(@event).Wait(); - } - }); - - storedEvent = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - - A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) - .Returns(envelope); - - sut = new MyEventConsumerProcessor( - state.PersistenceFactory, - eventConsumer, - eventFormatter, - eventStore, - A.Fake<ILogger<EventConsumerProcessor>>()); - } + eventConsumer.On(@event).Wait(); + } + }); - [Fact] - public async Task Should_query_position_if_consumer_should_start_from_latest() - { - state.Snapshot = new EventConsumerState(); + storedEvent = new StoredEvent("Stream", Guid.NewGuid().ToString(), 123, eventData); - A.CallTo(() => eventConsumer.StartLatest) - .Returns(true); + A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) + .Returns(envelope); - A.CallTo(() => eventConsumer.EventsFilter) - .Returns("my-filter"); + sut = new MyEventConsumerProcessor( + state.PersistenceFactory, + eventConsumer, + eventFormatter, + eventStore, + A.Fake<ILogger<EventConsumerProcessor>>()); + } - var latestPosition = "LATEST"; + [Fact] + public async Task Should_query_position_if_consumer_should_start_from_latest() + { + state.Snapshot = new EventConsumerState(); - A.CallTo(() => eventStore.QueryAllReverseAsync("my-filter", default, 1, A<CancellationToken>._)) - .Returns(Enumerable.Repeat(new StoredEvent("Stream", latestPosition, 1, eventData), 1).ToAsyncEnumerable()); + A.CallTo(() => eventConsumer.StartLatest) + .Returns(true); - await sut.InitializeAsync(default); + A.CallTo(() => eventConsumer.EventsFilter) + .Returns("my-filter"); - AssertGrainState(isStopped: false, position: latestPosition); - } + var latestPosition = "LATEST"; - [Fact] - public async Task Should_not_subscribe_to_event_store_if_stopped_in_db() - { - state.Snapshot = state.Snapshot.Stopped(); + A.CallTo(() => eventStore.QueryAllReverseAsync("my-filter", default, 1, A<CancellationToken>._)) + .Returns(Enumerable.Repeat(new StoredEvent("Stream", latestPosition, 1, eventData), 1).ToAsyncEnumerable()); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + await sut.InitializeAsync(default); - await sut.CompleteAsync(); + AssertGrainState(isStopped: false, position: latestPosition); + } - AssertGrainState(isStopped: true, position: initialPosition); + [Fact] + public async Task Should_not_subscribe_to_event_store_if_stopped_in_db() + { + state.Snapshot = state.Snapshot.Stopped(); - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) - .MustNotHaveHappened(); - } + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - [Fact] - public async Task Should_subscribe_to_event_store_if_not_found_in_db() - { - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + await sut.CompleteAsync(); - await sut.CompleteAsync(); + AssertGrainState(isStopped: true, position: initialPosition); - AssertGrainState(isStopped: false, position: initialPosition); + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) - .MustHaveHappenedOnceExactly(); - } + [Fact] + public async Task Should_subscribe_to_event_store_if_not_found_in_db() + { + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - [Fact] - public async Task Should_subscribe_to_event_store_if_failed() - { - state.Snapshot = state.Snapshot.Stopped(new InvalidOperationException()); + await sut.CompleteAsync(); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + AssertGrainState(isStopped: false, position: initialPosition); - await sut.CompleteAsync(); + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) + .MustHaveHappenedOnceExactly(); + } - AssertGrainState(isStopped: false, position: initialPosition); + [Fact] + public async Task Should_subscribe_to_event_store_if_failed() + { + state.Snapshot = state.Snapshot.Stopped(new InvalidOperationException()); - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) - .MustHaveHappenedOnceExactly(); - } + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - [Fact] - public async Task Should_subscribe_to_event_store_if_not_stopped_in_db() - { - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + await sut.CompleteAsync(); - await sut.CompleteAsync(); + AssertGrainState(isStopped: false, position: initialPosition); - AssertGrainState(isStopped: false, position: initialPosition); + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) + .MustHaveHappenedOnceExactly(); + } - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) - .MustHaveHappenedOnceExactly(); - } + [Fact] + public async Task Should_subscribe_to_event_store_if_not_stopped_in_db() + { + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - [Fact] - public async Task Should_stop_subscription_if_stopped() - { - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + await sut.CompleteAsync(); - await sut.StopAsync(); - await sut.StopAsync(); + AssertGrainState(isStopped: false, position: initialPosition); - await sut.CompleteAsync(); + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) + .MustHaveHappenedOnceExactly(); + } - AssertGrainState(isStopped: true, position: initialPosition); + [Fact] + public async Task Should_stop_subscription_if_stopped() + { + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + await sut.StopAsync(); + await sut.StopAsync(); - A.CallTo(() => eventSubscription.Dispose()) - .MustHaveHappenedOnceExactly(); - } + await sut.CompleteAsync(); - [Fact] - public async Task Should_reset_consumer_if_resetting() - { - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + AssertGrainState(isStopped: true, position: initialPosition); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - await sut.StopAsync(); - await sut.ResetAsync(); + A.CallTo(() => eventSubscription.Dispose()) + .MustHaveHappenedOnceExactly(); + } - await sut.CompleteAsync(); + [Fact] + public async Task Should_reset_consumer_if_resetting() + { + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - AssertGrainState(isStopped: false, position: null); + await sut.StopAsync(); + await sut.ResetAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappened(2, Times.Exactly); + await sut.CompleteAsync(); - A.CallTo(() => eventConsumer.ClearAsync()) - .MustHaveHappenedOnceExactly(); + AssertGrainState(isStopped: false, position: null); - A.CallTo(() => eventSubscription.Dispose()) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappened(2, Times.Exactly); - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, state.Snapshot.Position)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => eventConsumer.ClearAsync()) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, null)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => eventSubscription.Dispose()) + .MustHaveHappenedOnceExactly(); - [Fact] - public async Task Should_not_reset_consumer_if_not_allowed() - { - A.CallTo(() => eventConsumer.CanClear) - .Returns(false); + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, state.Snapshot.Position)) + .MustHaveHappenedOnceExactly(); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, null)) + .MustHaveHappenedOnceExactly(); + } - await sut.StopAsync(); - await sut.ResetAsync(); + [Fact] + public async Task Should_not_reset_consumer_if_not_allowed() + { + A.CallTo(() => eventConsumer.CanClear) + .Returns(false); - await sut.CompleteAsync(); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - AssertGrainState(isStopped: true, position: initialPosition); + await sut.StopAsync(); + await sut.ResetAsync(); - A.CallTo(() => eventConsumer.ClearAsync()) - .MustNotHaveHappened(); - } + await sut.CompleteAsync(); - [Fact] - public async Task Should_invoke_and_update_position_if_event_received() - { - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + AssertGrainState(isStopped: true, position: initialPosition); - await OnNextAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); + A.CallTo(() => eventConsumer.ClearAsync()) + .MustNotHaveHappened(); + } - AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 1); + [Fact] + public async Task Should_invoke_and_update_position_if_event_received() + { + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + await OnNextAsync(eventSubscription, storedEvent); + await sut.CompleteAsync(); - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappenedOnceExactly(); - } + AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 1); - [Fact] - public async Task Should_invoke_and_update_position_if_event_received_one_by_one() - { - A.CallTo(() => eventConsumer.BatchSize) - .Returns(1); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappenedOnceExactly(); + } - await OnNextAsync(eventSubscription, storedEvent); - await OnNextAsync(eventSubscription, storedEvent); - await OnNextAsync(eventSubscription, storedEvent); - await OnNextAsync(eventSubscription, storedEvent); - await OnNextAsync(eventSubscription, storedEvent); + [Fact] + public async Task Should_invoke_and_update_position_if_event_received_one_by_one() + { + A.CallTo(() => eventConsumer.BatchSize) + .Returns(1); - await sut.CompleteAsync(); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 5); + await OnNextAsync(eventSubscription, storedEvent); + await OnNextAsync(eventSubscription, storedEvent); + await OnNextAsync(eventSubscription, storedEvent); + await OnNextAsync(eventSubscription, storedEvent); + await OnNextAsync(eventSubscription, storedEvent); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappened(5, Times.Exactly); + await sut.CompleteAsync(); - A.CallTo(() => eventConsumer.On(A<IEnumerable<Envelope<IEvent>>>._)) - .MustHaveHappened(5, Times.Exactly); - } + AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 5); - [Fact] - public async Task Should_invoke_and_update_position_if_events_received_batched() - { - A.CallTo(() => eventConsumer.BatchSize) - .Returns(5); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappened(5, Times.Exactly); - A.CallTo(() => eventConsumer.BatchDelay) - .Returns(int.MaxValue); + A.CallTo(() => eventConsumer.On(A<IEnumerable<Envelope<IEvent>>>._)) + .MustHaveHappened(5, Times.Exactly); + } - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + [Fact] + public async Task Should_invoke_and_update_position_if_events_received_batched() + { + A.CallTo(() => eventConsumer.BatchSize) + .Returns(5); - await OnNextAsync(eventSubscription, storedEvent); - await OnNextAsync(eventSubscription, storedEvent); - await OnNextAsync(eventSubscription, storedEvent); - await OnNextAsync(eventSubscription, storedEvent); - await OnNextAsync(eventSubscription, storedEvent); + A.CallTo(() => eventConsumer.BatchDelay) + .Returns(int.MaxValue); - await sut.CompleteAsync(); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 5); + await OnNextAsync(eventSubscription, storedEvent); + await OnNextAsync(eventSubscription, storedEvent); + await OnNextAsync(eventSubscription, storedEvent); + await OnNextAsync(eventSubscription, storedEvent); + await OnNextAsync(eventSubscription, storedEvent); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + await sut.CompleteAsync(); - A.CallTo(() => eventConsumer.On(A<IEnumerable<Envelope<IEvent>>>._)) - .MustHaveHappenedOnceExactly(); - } + AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 5); - [Fact] - public async Task Should_not_invoke_but_update_position_if_consumer_does_not_want_to_handle() - { - A.CallTo(() => eventConsumer.Handles(storedEvent)) - .Returns(false); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + A.CallTo(() => eventConsumer.On(A<IEnumerable<Envelope<IEvent>>>._)) + .MustHaveHappenedOnceExactly(); + } - await OnNextAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); + [Fact] + public async Task Should_not_invoke_but_update_position_if_consumer_does_not_want_to_handle() + { + A.CallTo(() => eventConsumer.Handles(storedEvent)) + .Returns(false); - AssertGrainState(isStopped: false, position: storedEvent.EventPosition); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + await OnNextAsync(eventSubscription, storedEvent); + await sut.CompleteAsync(); - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } + AssertGrainState(isStopped: false, position: storedEvent.EventPosition); - [Fact] - public async Task Should_ignore_old_events() - { - A.CallTo(() => eventFormatter.ParseIfKnown(A<StoredEvent>.That.Matches(x => x.Data == eventData))) - .Returns(null); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } - await OnNextAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); + [Fact] + public async Task Should_ignore_old_events() + { + A.CallTo(() => eventFormatter.ParseIfKnown(A<StoredEvent>.That.Matches(x => x.Data == eventData))) + .Returns(null); - AssertGrainState(isStopped: false, position: storedEvent.EventPosition); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + await OnNextAsync(eventSubscription, storedEvent); + await sut.CompleteAsync(); - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } + AssertGrainState(isStopped: false, position: storedEvent.EventPosition); - [Fact] - public async Task Should_not_invoke_and_update_position_if_event_is_from_another_subscription() - { - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - await sut.OnNextAsync(A.Fake<IEventSubscription>(), new ParsedEvents(new[] { envelope }.ToList(), storedEvent.EventPosition)); - await sut.CompleteAsync(); + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } - AssertGrainState(isStopped: false, position: initialPosition); + [Fact] + public async Task Should_not_invoke_and_update_position_if_event_is_from_another_subscription() + { + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); - } + await sut.OnNextAsync(A.Fake<IEventSubscription>(), new ParsedEvents(new[] { envelope }.ToList(), storedEvent.EventPosition)); + await sut.CompleteAsync(); - [Fact] - public async Task Should_stop_if_consumer_failed() - { - var ex = new InvalidOperationException(); + AssertGrainState(isStopped: false, position: initialPosition); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); + } - await OnErrorAsync(eventSubscription, ex); - await sut.CompleteAsync(); + [Fact] + public async Task Should_stop_if_consumer_failed() + { + var ex = new InvalidOperationException(); - AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + await OnErrorAsync(eventSubscription, ex); + await sut.CompleteAsync(); - A.CallTo(() => eventSubscription.Dispose()) - .MustHaveHappenedOnceExactly(); - } + AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); - [Fact] - public async Task Should_not_make_error_handling_if_exception_is_from_another_subscription() - { - var ex = new InvalidOperationException(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + A.CallTo(() => eventSubscription.Dispose()) + .MustHaveHappenedOnceExactly(); + } - await sut.OnErrorAsync(A.Fake<IEventSubscription>(), ex); - await sut.CompleteAsync(); + [Fact] + public async Task Should_not_make_error_handling_if_exception_is_from_another_subscription() + { + var ex = new InvalidOperationException(); - AssertGrainState(isStopped: false, position: initialPosition); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + await sut.OnErrorAsync(A.Fake<IEventSubscription>(), ex); + await sut.CompleteAsync(); - [Fact] - public async Task Should_wakeup_if_already_subscribed() - { - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + AssertGrainState(isStopped: false, position: initialPosition); - await sut.ActivateAsync(); - await sut.CompleteAsync(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - A.CallTo(() => eventSubscription.WakeUp()) - .MustHaveHappened(); - } + [Fact] + public async Task Should_wakeup_if_already_subscribed() + { + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - [Fact] - public async Task Should_stop_if_resetting_failed() - { - var ex = new InvalidOperationException(); + await sut.ActivateAsync(); + await sut.CompleteAsync(); - A.CallTo(() => eventConsumer.ClearAsync()) - .Throws(ex); + A.CallTo(() => eventSubscription.WakeUp()) + .MustHaveHappened(); + } - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + [Fact] + public async Task Should_stop_if_resetting_failed() + { + var ex = new InvalidOperationException(); - await sut.ResetAsync(); - await sut.CompleteAsync(); + A.CallTo(() => eventConsumer.ClearAsync()) + .Throws(ex); - AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + await sut.ResetAsync(); + await sut.CompleteAsync(); - A.CallTo(() => eventSubscription.Dispose()) - .MustHaveHappenedOnceExactly(); - } + AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); - [Fact] - public async Task Should_stop_if_handling_failed() - { - var ex = new InvalidOperationException(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventConsumer.On(envelope)) - .Throws(ex); + A.CallTo(() => eventSubscription.Dispose()) + .MustHaveHappenedOnceExactly(); + } - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + [Fact] + public async Task Should_stop_if_handling_failed() + { + var ex = new InvalidOperationException(); - await OnNextAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); + A.CallTo(() => eventConsumer.On(envelope)) + .Throws(ex); - AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappened(); + await OnNextAsync(eventSubscription, storedEvent); + await sut.CompleteAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); - A.CallTo(() => eventSubscription.Dispose()) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappened(); - [Fact] - public async Task Should_stop_if_deserialization_failed() - { - var ex = new InvalidOperationException(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventFormatter.ParseIfKnown(A<StoredEvent>.That.Matches(x => x.Data == eventData))) - .Throws(ex); + A.CallTo(() => eventSubscription.Dispose()) + .MustHaveHappenedOnceExactly(); + } - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + [Fact] + public async Task Should_stop_if_deserialization_failed() + { + var ex = new InvalidOperationException(); - await OnNextAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); + A.CallTo(() => eventFormatter.ParseIfKnown(A<StoredEvent>.That.Matches(x => x.Data == eventData))) + .Throws(ex); - AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - A.CallTo(() => eventConsumer.On(envelope)) - .MustNotHaveHappened(); + await OnNextAsync(eventSubscription, storedEvent); + await sut.CompleteAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); + AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); - A.CallTo(() => eventSubscription.Dispose()) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => eventConsumer.On(envelope)) + .MustNotHaveHappened(); - [Fact] - public async Task Should_start_after_stop_if_handling_failed() - { - var ex = new InvalidOperationException(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventConsumer.On(envelope)) - .Throws(ex); + A.CallTo(() => eventSubscription.Dispose()) + .MustHaveHappenedOnceExactly(); + } - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + [Fact] + public async Task Should_start_after_stop_if_handling_failed() + { + var ex = new InvalidOperationException(); - await OnNextAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); + A.CallTo(() => eventConsumer.On(envelope)) + .Throws(ex); - await sut.StopAsync(); - await sut.StartAsync(); - await sut.StartAsync(); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - AssertGrainState(isStopped: false, position: initialPosition); + await OnNextAsync(eventSubscription, storedEvent); + await sut.CompleteAsync(); - A.CallTo(() => eventConsumer.On(envelope)) - .MustHaveHappened(); + await sut.StopAsync(); + await sut.StartAsync(); + await sut.StartAsync(); - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .MustHaveHappened(2, Times.Exactly); + AssertGrainState(isStopped: false, position: initialPosition); - A.CallTo(() => eventSubscription.Dispose()) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => eventConsumer.On(envelope)) + .MustHaveHappened(); - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) - .MustHaveHappened(2, Times.Exactly); - } + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .MustHaveHappened(2, Times.Exactly); - [Fact] - public async Task Should_fail_if_writing_failed() - { - var ex = new InconsistentStateException(0, 1); + A.CallTo(() => eventSubscription.Dispose()) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) + .MustHaveHappened(2, Times.Exactly); + } - A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) - .Throws(ex); + [Fact] + public async Task Should_fail_if_writing_failed() + { + var ex = new InconsistentStateException(0, 1); - await sut.InitializeAsync(default); - await sut.ActivateAsync(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A<EventConsumerState>._, A<CancellationToken>._)) + .Throws(ex); - await OnNextAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); - AssertGrainState(isStopped: true, position: storedEvent.EventPosition, error: ex.Message, 1); - } + await OnNextAsync(eventSubscription, storedEvent); + await sut.CompleteAsync(); - private ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception) - { - return sut.Subscriber?.OnErrorAsync(subscription, exception) ?? default; - } + AssertGrainState(isStopped: true, position: storedEvent.EventPosition, error: ex.Message, 1); + } - private ValueTask OnNextAsync(IEventSubscription subscription, StoredEvent @event) - { - return sut.Subscriber?.OnNextAsync(subscription, @event) ?? default; - } + private ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception) + { + return sut.Subscriber?.OnErrorAsync(subscription, exception) ?? default; + } - private void AssertGrainState(bool isStopped = false, string? position = null, string? error = null, int count = 0) - { - sut.State.Should().BeEquivalentTo( - new EventConsumerState { IsStopped = isStopped, Position = position, Error = error, Count = count }); - } + private ValueTask OnNextAsync(IEventSubscription subscription, StoredEvent @event) + { + return sut.Subscriber?.OnNextAsync(subscription, @event) ?? default; + } + + private void AssertGrainState(bool isStopped = false, string? position = null, string? error = null, int count = 0) + { + sut.State.Should().BeEquivalentTo( + new EventConsumerState { IsStopped = isStopped, Position = position, Error = error, Count = count }); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerStateTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerStateTests.cs index 803c65d892..ffe31f1cb8 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerStateTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerStateTests.cs @@ -9,24 +9,23 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public class EventConsumerStateTests { - public class EventConsumerStateTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() + var state = new EventConsumerState { - var state = new EventConsumerState - { - Count = 1, - IsStopped = true, - Error = "Error", - Position = "Position" - }; + Count = 1, + IsStopped = true, + Error = "Error", + Position = "Position" + }; - var serialized = state.SerializeAndDeserialize(); + var serialized = state.SerializeAndDeserialize(); - serialized.Should().BeEquivalentTo(state); - } + serialized.Should().BeEquivalentTo(state); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs index 8077c4da10..bff776ee44 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs @@ -8,93 +8,92 @@ using FakeItEasy; using Xunit; -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +public class EventConsumerWorkerTests { - public class EventConsumerWorkerTests - { - private readonly IEventConsumer consumer1 = A.Fake<IEventConsumer>(); - private readonly IEventConsumer consumer2 = A.Fake<IEventConsumer>(); - private readonly EventConsumerProcessor processor1 = A.Fake<EventConsumerProcessor>(); - private readonly EventConsumerProcessor processor2 = A.Fake<EventConsumerProcessor>(); - private readonly EventConsumerWorker sut; + private readonly IEventConsumer consumer1 = A.Fake<IEventConsumer>(); + private readonly IEventConsumer consumer2 = A.Fake<IEventConsumer>(); + private readonly EventConsumerProcessor processor1 = A.Fake<EventConsumerProcessor>(); + private readonly EventConsumerProcessor processor2 = A.Fake<EventConsumerProcessor>(); + private readonly EventConsumerWorker sut; - public EventConsumerWorkerTests() + public EventConsumerWorkerTests() + { + var factory = new Func<IEventConsumer, EventConsumerProcessor>(consumer => { - var factory = new Func<IEventConsumer, EventConsumerProcessor>(consumer => - { - return consumer == consumer1 ? processor1 : processor2; - }); + return consumer == consumer1 ? processor1 : processor2; + }); - A.CallTo(() => consumer1.Name).Returns("1"); - A.CallTo(() => consumer2.Name).Returns("2"); + A.CallTo(() => consumer1.Name).Returns("1"); + A.CallTo(() => consumer2.Name).Returns("2"); - sut = new EventConsumerWorker(new[] { consumer1, consumer2 }, factory); - } + sut = new EventConsumerWorker(new[] { consumer1, consumer2 }, factory); + } - [Fact] - public async Task Should_stop_without_start() - { - await sut.StopAsync(default); - } + [Fact] + public async Task Should_stop_without_start() + { + await sut.StopAsync(default); + } - [Fact] - public async Task Should_initialize_all_processors_on_initialize() - { - await sut.StartAsync(default); + [Fact] + public async Task Should_initialize_all_processors_on_initialize() + { + await sut.StartAsync(default); - A.CallTo(() => processor1.InitializeAsync(default)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => processor1.InitializeAsync(default)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => processor2.InitializeAsync(default)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => processor2.InitializeAsync(default)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_activate_all_processors_on_initialize() - { - await sut.StartAsync(default); + [Fact] + public async Task Should_activate_all_processors_on_initialize() + { + await sut.StartAsync(default); - A.CallTo(() => processor1.ActivateAsync()) - .MustHaveHappened(); + A.CallTo(() => processor1.ActivateAsync()) + .MustHaveHappened(); - A.CallTo(() => processor2.ActivateAsync()) - .MustHaveHappened(); - } + A.CallTo(() => processor2.ActivateAsync()) + .MustHaveHappened(); + } - [Fact] - public async Task Shoud_start_correct_processor() - { - await sut.HandleAsync(new EventConsumerStart(consumer1.Name), default); + [Fact] + public async Task Shoud_start_correct_processor() + { + await sut.HandleAsync(new EventConsumerStart(consumer1.Name), default); - A.CallTo(() => processor1.StartAsync()) - .MustHaveHappened(); + A.CallTo(() => processor1.StartAsync()) + .MustHaveHappened(); - A.CallTo(() => processor2.StartAsync()) - .MustNotHaveHappened(); - } + A.CallTo(() => processor2.StartAsync()) + .MustNotHaveHappened(); + } - [Fact] - public async Task Shoud_stop_correct_processor() - { - await sut.HandleAsync(new EventConsumerStart(consumer1.Name), default); + [Fact] + public async Task Shoud_stop_correct_processor() + { + await sut.HandleAsync(new EventConsumerStart(consumer1.Name), default); - A.CallTo(() => processor1.StartAsync()) - .MustHaveHappened(); + A.CallTo(() => processor1.StartAsync()) + .MustHaveHappened(); - A.CallTo(() => processor2.StartAsync()) - .MustNotHaveHappened(); - } + A.CallTo(() => processor2.StartAsync()) + .MustNotHaveHappened(); + } - [Fact] - public async Task Shoud_reset_correct_processor() - { - await sut.HandleAsync(new EventConsumerReset(consumer1.Name), default); + [Fact] + public async Task Shoud_reset_correct_processor() + { + await sut.HandleAsync(new EventConsumerReset(consumer1.Name), default); - A.CallTo(() => processor1.ResetAsync()) - .MustHaveHappened(); + A.CallTo(() => processor1.ResetAsync()) + .MustHaveHappened(); - A.CallTo(() => processor2.ResetAsync()) - .MustNotHaveHappened(); - } + A.CallTo(() => processor2.ResetAsync()) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/MongoEventConsumerProcessorIntegrationTests_Direct.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/MongoEventConsumerProcessorIntegrationTests_Direct.cs index 7aeccdef48..8bb4ba620d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/MongoEventConsumerProcessorIntegrationTests_Direct.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/MongoEventConsumerProcessorIntegrationTests_Direct.cs @@ -9,21 +9,20 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Infrastructure.EventSourcing.Consume +namespace Squidex.Infrastructure.EventSourcing.Consume; + +[Trait("Category", "Dependencies")] +public class MongoEventConsumerProcessorIntegrationTests_Direct : EventConsumerProcessorIntegrationTests, IClassFixture<MongoEventStoreDirectFixture> { - [Trait("Category", "Dependencies")] - public class MongoEventConsumerProcessorIntegrationTests_Direct : EventConsumerProcessorIntegrationTests, IClassFixture<MongoEventStoreDirectFixture> - { - public MongoEventStoreFixture _ { get; } + public MongoEventStoreFixture _ { get; } - public MongoEventConsumerProcessorIntegrationTests_Direct(MongoEventStoreDirectFixture fixture) - { - _ = fixture; - } + public MongoEventConsumerProcessorIntegrationTests_Direct(MongoEventStoreDirectFixture fixture) + { + _ = fixture; + } - public override IEventStore CreateStore() - { - return _.EventStore; - } + public override IEventStore CreateStore() + { + return _.EventStore; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventFormatterTests.cs index c1e401ec95..0376a85991 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventFormatterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventFormatterTests.cs @@ -11,92 +11,91 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public class DefaultEventFormatterTests { - public class DefaultEventFormatterTests + public sealed class MyOldEvent : IEvent, IMigrated<IEvent> { - public sealed class MyOldEvent : IEvent, IMigrated<IEvent> - { - public string MyProperty { get; set; } + public string MyProperty { get; set; } - public IEvent Migrate() - { - return new MyEvent { MyProperty = MyProperty }; - } + public IEvent Migrate() + { + return new MyEvent { MyProperty = MyProperty }; } + } - private readonly DefaultEventFormatter sut; + private readonly DefaultEventFormatter sut; - public DefaultEventFormatterTests() - { - var typeNameRegistry = - new TypeNameRegistry() - .Map(typeof(MyEvent), "Event") - .Map(typeof(MyOldEvent), "OldEvent"); + public DefaultEventFormatterTests() + { + var typeNameRegistry = + new TypeNameRegistry() + .Map(typeof(MyEvent), "Event") + .Map(typeof(MyOldEvent), "OldEvent"); - sut = new DefaultEventFormatter(typeNameRegistry, TestUtils.DefaultSerializer); - } + sut = new DefaultEventFormatter(typeNameRegistry, TestUtils.DefaultSerializer); + } - [Fact] - public void Should_serialize_and_deserialize_envelope() - { - var commitId = Guid.NewGuid(); + [Fact] + public void Should_serialize_and_deserialize_envelope() + { + var commitId = Guid.NewGuid(); - var inputEvent = new Envelope<MyEvent>(new MyEvent { MyProperty = "My-Property" }); + var inputEvent = new Envelope<MyEvent>(new MyEvent { MyProperty = "My-Property" }); - inputEvent.SetAggregateId(DomainId.NewGuid()); - inputEvent.SetCommitId(commitId); - inputEvent.SetEventId(Guid.NewGuid()); - inputEvent.SetEventPosition("1"); - inputEvent.SetEventStreamNumber(1); - inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant()); + inputEvent.SetAggregateId(DomainId.NewGuid()); + inputEvent.SetCommitId(commitId); + inputEvent.SetEventId(Guid.NewGuid()); + inputEvent.SetEventPosition("1"); + inputEvent.SetEventStreamNumber(1); + inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant()); - var eventData = sut.ToEventData(inputEvent, commitId); - var eventStored = new StoredEvent("stream", "0", -1, eventData); + var eventData = sut.ToEventData(inputEvent, commitId); + var eventStored = new StoredEvent("stream", "0", -1, eventData); - var outputEvent = sut.Parse(eventStored).To<MyEvent>(); + var outputEvent = sut.Parse(eventStored).To<MyEvent>(); - AssertHeaders(inputEvent.Headers, outputEvent.Headers); - AssertPayload(inputEvent, outputEvent); - } + AssertHeaders(inputEvent.Headers, outputEvent.Headers); + AssertPayload(inputEvent, outputEvent); + } - [Fact] - public void Should_migrate_event_serializing() - { - var inputEvent = new Envelope<MyOldEvent>(new MyOldEvent { MyProperty = "My-Property" }); + [Fact] + public void Should_migrate_event_serializing() + { + var inputEvent = new Envelope<MyOldEvent>(new MyOldEvent { MyProperty = "My-Property" }); - var eventData = sut.ToEventData(inputEvent, Guid.NewGuid()); - var eventStored = new StoredEvent("stream", "0", -1, eventData); + var eventData = sut.ToEventData(inputEvent, Guid.NewGuid()); + var eventStored = new StoredEvent("stream", "0", -1, eventData); - var outputEvent = sut.Parse(eventStored).To<MyEvent>(); + var outputEvent = sut.Parse(eventStored).To<MyEvent>(); - Assert.Equal(inputEvent.Payload.MyProperty, outputEvent.Payload.MyProperty); - } + Assert.Equal(inputEvent.Payload.MyProperty, outputEvent.Payload.MyProperty); + } - [Fact] - public void Should_migrate_event_deserializing() - { - var inputEvent = new Envelope<MyOldEvent>(new MyOldEvent { MyProperty = "My-Property" }); + [Fact] + public void Should_migrate_event_deserializing() + { + var inputEvent = new Envelope<MyOldEvent>(new MyOldEvent { MyProperty = "My-Property" }); - var eventData = sut.ToEventData(inputEvent, Guid.NewGuid(), false); - var eventStored = new StoredEvent("stream", "0", -1, eventData); + var eventData = sut.ToEventData(inputEvent, Guid.NewGuid(), false); + var eventStored = new StoredEvent("stream", "0", -1, eventData); - var outputEvent = sut.Parse(eventStored).To<MyEvent>(); + var outputEvent = sut.Parse(eventStored).To<MyEvent>(); - Assert.Equal(inputEvent.Payload.MyProperty, outputEvent.Payload.MyProperty); - } + Assert.Equal(inputEvent.Payload.MyProperty, outputEvent.Payload.MyProperty); + } - private static void AssertPayload(Envelope<MyEvent> inputEvent, Envelope<MyEvent> outputEvent) - { - Assert.Equal(inputEvent.Payload.MyProperty, outputEvent.Payload.MyProperty); - } + private static void AssertPayload(Envelope<MyEvent> inputEvent, Envelope<MyEvent> outputEvent) + { + Assert.Equal(inputEvent.Payload.MyProperty, outputEvent.Payload.MyProperty); + } - private static void AssertHeaders(EnvelopeHeaders lhs, EnvelopeHeaders rhs) + private static void AssertHeaders(EnvelopeHeaders lhs, EnvelopeHeaders rhs) + { + foreach (var key in lhs.Keys.Concat(rhs.Keys).Distinct()) { - foreach (var key in lhs.Keys.Concat(rhs.Keys).Distinct()) - { - Assert.Equal(lhs[key].ToString(), rhs[key].ToString()); - } + Assert.Equal(lhs[key].ToString(), rhs[key].ToString()); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs index 9534e6d993..d0b8bff1ed 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs @@ -8,80 +8,79 @@ using NodaTime; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public class EnvelopeExtensionsTests { - public class EnvelopeExtensionsTests - { - private readonly Envelope<MyEvent> sut = new Envelope<MyEvent>(new MyEvent()); + private readonly Envelope<MyEvent> sut = new Envelope<MyEvent>(new MyEvent()); - public sealed class MyEvent : IEvent - { - } + public sealed class MyEvent : IEvent + { + } - [Fact] - public void Should_set_and_get_timestamp() - { - var timestamp = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); + [Fact] + public void Should_set_and_get_timestamp() + { + var timestamp = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); - sut.SetTimestamp(timestamp); + sut.SetTimestamp(timestamp); - Assert.Equal(timestamp, sut.Headers.Timestamp()); - Assert.Equal(timestamp, sut.Headers.GetInstant("Timestamp")); - } + Assert.Equal(timestamp, sut.Headers.Timestamp()); + Assert.Equal(timestamp, sut.Headers.GetInstant("Timestamp")); + } - [Fact] - public void Should_set_and_get_commit_id() - { - var commitId = Guid.NewGuid(); + [Fact] + public void Should_set_and_get_commit_id() + { + var commitId = Guid.NewGuid(); - sut.SetCommitId(commitId); + sut.SetCommitId(commitId); - Assert.Equal(commitId, sut.Headers.CommitId()); - Assert.Equal(commitId, sut.Headers.GetGuid("CommitId")); - } + Assert.Equal(commitId, sut.Headers.CommitId()); + Assert.Equal(commitId, sut.Headers.GetGuid("CommitId")); + } - [Fact] - public void Should_set_and_get_event_id() - { - var commitId = Guid.NewGuid(); + [Fact] + public void Should_set_and_get_event_id() + { + var commitId = Guid.NewGuid(); - sut.SetEventId(commitId); + sut.SetEventId(commitId); - Assert.Equal(commitId, sut.Headers.EventId()); - Assert.Equal(commitId, sut.Headers.GetGuid("EventId")); - } + Assert.Equal(commitId, sut.Headers.EventId()); + Assert.Equal(commitId, sut.Headers.GetGuid("EventId")); + } - [Fact] - public void Should_set_and_get_aggregate_id() - { - var commitId = DomainId.NewGuid(); + [Fact] + public void Should_set_and_get_aggregate_id() + { + var commitId = DomainId.NewGuid(); - sut.SetAggregateId(commitId); + sut.SetAggregateId(commitId); - Assert.Equal(commitId, sut.Headers.AggregateId()); - Assert.Equal(commitId.ToString(), sut.Headers.GetString("AggregateId")); - } + Assert.Equal(commitId, sut.Headers.AggregateId()); + Assert.Equal(commitId.ToString(), sut.Headers.GetString("AggregateId")); + } - [Fact] - public void Should_set_and_get_event_number() - { - const string eventNumber = "123"; + [Fact] + public void Should_set_and_get_event_number() + { + const string eventNumber = "123"; - sut.SetEventPosition(eventNumber); + sut.SetEventPosition(eventNumber); - Assert.Equal(eventNumber, sut.Headers.EventPosition()); - Assert.Equal(eventNumber, sut.Headers["EventNumber"].ToString()); - } + Assert.Equal(eventNumber, sut.Headers.EventPosition()); + Assert.Equal(eventNumber, sut.Headers["EventNumber"].ToString()); + } - [Fact] - public void Should_set_and_get_event_stream_number() - { - const int eventStreamNumber = 123; + [Fact] + public void Should_set_and_get_event_stream_number() + { + const int eventStreamNumber = 123; - sut.SetEventStreamNumber(eventStreamNumber); + sut.SetEventStreamNumber(eventStreamNumber); - Assert.Equal(eventStreamNumber, sut.Headers.EventStreamNumber()); - Assert.Equal(eventStreamNumber, sut.Headers.GetLong("EventStreamNumber")); - } + Assert.Equal(eventStreamNumber, sut.Headers.EventStreamNumber()); + Assert.Equal(eventStreamNumber, sut.Headers.GetLong("EventStreamNumber")); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs index 9f4b970efd..9b270bbe30 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeHeadersTests.cs @@ -9,63 +9,62 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public class EnvelopeHeadersTests { - public class EnvelopeHeadersTests + [Fact] + public void Should_create_headers() { - [Fact] - public void Should_create_headers() - { - var headers = new EnvelopeHeaders(); + var headers = new EnvelopeHeaders(); - Assert.Empty(headers); - } + Assert.Empty(headers); + } - [Fact] - public void Should_create_headers_as_copy() + [Fact] + public void Should_create_headers_as_copy() + { + var source = new EnvelopeHeaders { - var source = new EnvelopeHeaders - { - ["key1"] = JsonValue.Create(13) - }; + ["key1"] = JsonValue.Create(13) + }; - var headers = new EnvelopeHeaders(source); + var headers = new EnvelopeHeaders(source); - CompareHeaders(headers, source); - } + CompareHeaders(headers, source); + } - [Fact] - public void Should_clone_headers() + [Fact] + public void Should_clone_headers() + { + var source = new EnvelopeHeaders { - var source = new EnvelopeHeaders - { - ["key1"] = JsonValue.Create(13) - }; + ["key1"] = JsonValue.Create(13) + }; - var headers = source.CloneHeaders(); + var headers = source.CloneHeaders(); - CompareHeaders(headers, source); - } + CompareHeaders(headers, source); + } - [Fact] - public void Should_serialize_and_deserialize() + [Fact] + public void Should_serialize_and_deserialize() + { + var source = new EnvelopeHeaders { - var source = new EnvelopeHeaders - { - ["key1"] = JsonValue.Create(13) - }; + ["key1"] = JsonValue.Create(13) + }; - var deserialized = source.SerializeAndDeserialize(); + var deserialized = source.SerializeAndDeserialize(); - CompareHeaders(deserialized, source); - } + CompareHeaders(deserialized, source); + } - private static void CompareHeaders(EnvelopeHeaders lhs, EnvelopeHeaders rhs) + private static void CompareHeaders(EnvelopeHeaders lhs, EnvelopeHeaders rhs) + { + foreach (var key in lhs.Keys.Concat(rhs.Keys).Distinct()) { - foreach (var key in lhs.Keys.Concat(rhs.Keys).Distinct()) - { - Assert.Equal(lhs[key].ToString(), rhs[key].ToString()); - } + Assert.Equal(lhs[key].ToString(), rhs[key].ToString()); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs index 22fd4d4429..2d1965bc02 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs @@ -8,23 +8,22 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public class EnvelopeTests { - public class EnvelopeTests + public class MyEvent : IEvent { - public class MyEvent : IEvent - { - public int Value { get; set; } - } + public int Value { get; set; } + } - [Fact] - public void Should_serialize_and_deserialize() - { - var value = Envelope.Create(new MyEvent { Value = 1 }); + [Fact] + public void Should_serialize_and_deserialize() + { + var value = Envelope.Create(new MyEvent { Value = 1 }); - var deserialized = value.SerializeAndDeserialize(); + var deserialized = value.SerializeAndDeserialize(); - Assert.Equal(1, deserialized.Payload.Value); - } + Assert.Equal(1, deserialized.Payload.Value); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs index 1c9f6abb64..8e2045109f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs @@ -9,522 +9,521 @@ using FluentAssertions; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public abstract class EventStoreTests<T> where T : IEventStore { - public abstract class EventStoreTests<T> where T : IEventStore - { - private readonly Lazy<T> sut; - private string subscriptionPosition; + private readonly Lazy<T> sut; + private string subscriptionPosition; - public sealed class EventSubscriber : IEventSubscriber<StoredEvent> - { - public List<StoredEvent> Events { get; } = new List<StoredEvent>(); + public sealed class EventSubscriber : IEventSubscriber<StoredEvent> + { + public List<StoredEvent> Events { get; } = new List<StoredEvent>(); - public string LastPosition { get; set; } + public string LastPosition { get; set; } - public void Dispose() - { - } + public void Dispose() + { + } - public void WakeUp() - { - } + public void WakeUp() + { + } - public ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception) - { - throw exception; - } + public ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception) + { + throw exception; + } - public ValueTask OnNextAsync(IEventSubscription subscription, StoredEvent @event) - { - LastPosition = @event.EventPosition; + public ValueTask OnNextAsync(IEventSubscription subscription, StoredEvent @event) + { + LastPosition = @event.EventPosition; - Events.Add(@event); + Events.Add(@event); - return default; - } + return default; } + } - protected T Sut - { - get => sut.Value; - } + protected T Sut + { + get => sut.Value; + } - protected EventStoreTests() - { + protected EventStoreTests() + { #pragma warning disable MA0056 // Do not call overridable members in constructor - sut = new Lazy<T>(CreateStore); + sut = new Lazy<T>(CreateStore); #pragma warning restore MA0056 // Do not call overridable members in constructor - } + } + + public abstract T CreateStore(); - public abstract T CreateStore(); + [Fact] + public async Task Should_throw_exception_for_version_mismatch() + { + var streamName = $"test-{Guid.NewGuid()}"; - [Fact] - public async Task Should_throw_exception_for_version_mismatch() + var commit = new[] { - var streamName = $"test-{Guid.NewGuid()}"; + CreateEventData(1), + CreateEventData(2) + }; - var commit = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + await Assert.ThrowsAsync<WrongEventVersionException>(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, commit)); + } - await Assert.ThrowsAsync<WrongEventVersionException>(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, commit)); - } + [Fact] + public async Task Should_throw_exception_for_version_mismatch_and_update() + { + var streamName = $"test-{Guid.NewGuid()}"; - [Fact] - public async Task Should_throw_exception_for_version_mismatch_and_update() + var commit = new[] { - var streamName = $"test-{Guid.NewGuid()}"; + CreateEventData(1), + CreateEventData(2) + }; - var commit = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit); - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit); + await Assert.ThrowsAsync<WrongEventVersionException>(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, commit)); + } - await Assert.ThrowsAsync<WrongEventVersionException>(() => Sut.AppendAsync(Guid.NewGuid(), streamName, 0, commit)); - } + [Fact] + public async Task Should_append_events() + { + var streamName = $"test-{Guid.NewGuid()}"; - [Fact] - public async Task Should_append_events() + var commit1 = new[] { - var streamName = $"test-{Guid.NewGuid()}"; + CreateEventData(1), + CreateEventData(2) + }; - var commit1 = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + var commit2 = new[] + { + CreateEventData(1), + CreateEventData(2) + }; - var commit2 = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit1); + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit2); - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit1); - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit2); + var readEvents1 = await QueryAsync(streamName); + var readEvents2 = await QueryAllAsync(streamName); - var readEvents1 = await QueryAsync(streamName); - var readEvents2 = await QueryAllAsync(streamName); + var expected = new[] + { + new StoredEvent(streamName, "Position", 0, commit1[0]), + new StoredEvent(streamName, "Position", 1, commit1[1]), + new StoredEvent(streamName, "Position", 2, commit2[0]), + new StoredEvent(streamName, "Position", 3, commit2[1]) + }; + + ShouldBeEquivalentTo(readEvents1, expected); + ShouldBeEquivalentTo(readEvents2, expected); + } - var expected = new[] - { - new StoredEvent(streamName, "Position", 0, commit1[0]), - new StoredEvent(streamName, "Position", 1, commit1[1]), - new StoredEvent(streamName, "Position", 2, commit2[0]), - new StoredEvent(streamName, "Position", 3, commit2[1]) - }; - - ShouldBeEquivalentTo(readEvents1, expected); - ShouldBeEquivalentTo(readEvents2, expected); - } + [Fact] + public async Task Should_append_events_unsafe() + { + var streamName = $"test-{Guid.NewGuid()}"; - [Fact] - public async Task Should_append_events_unsafe() + var commit1 = new[] { - var streamName = $"test-{Guid.NewGuid()}"; + CreateEventData(1), + CreateEventData(2) + }; - var commit1 = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + await Sut.AppendUnsafeAsync(new List<EventCommit> + { + new EventCommit(Guid.NewGuid(), streamName, -1, commit1) + }); - await Sut.AppendUnsafeAsync(new List<EventCommit> - { - new EventCommit(Guid.NewGuid(), streamName, -1, commit1) - }); + var readEvents1 = await QueryAsync(streamName); + var readEvents2 = await QueryAllAsync(streamName); - var readEvents1 = await QueryAsync(streamName); - var readEvents2 = await QueryAllAsync(streamName); + var expected = new[] + { + new StoredEvent(streamName, "Position", 0, commit1[0]), + new StoredEvent(streamName, "Position", 1, commit1[1]) + }; - var expected = new[] - { - new StoredEvent(streamName, "Position", 0, commit1[0]), - new StoredEvent(streamName, "Position", 1, commit1[1]) - }; + ShouldBeEquivalentTo(readEvents1, expected); + ShouldBeEquivalentTo(readEvents2, expected); + } - ShouldBeEquivalentTo(readEvents1, expected); - ShouldBeEquivalentTo(readEvents2, expected); - } + [Fact] + public async Task Should_subscribe_to_events() + { + var streamName = $"test-{Guid.NewGuid()}"; - [Fact] - public async Task Should_subscribe_to_events() + var commit1 = new[] { - var streamName = $"test-{Guid.NewGuid()}"; + CreateEventData(1), + CreateEventData(2) + }; - var commit1 = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + var readEvents = await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit1); + }); - var readEvents = await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit1); - }); + var expected = new[] + { + new StoredEvent(streamName, "Position", 0, commit1[0]), + new StoredEvent(streamName, "Position", 1, commit1[1]) + }; - var expected = new[] - { - new StoredEvent(streamName, "Position", 0, commit1[0]), - new StoredEvent(streamName, "Position", 1, commit1[1]) - }; + ShouldBeEquivalentTo(readEvents, expected); + } - ShouldBeEquivalentTo(readEvents, expected); - } + [Fact] + public async Task Should_subscribe_to_next_events() + { + var streamName = $"test-{Guid.NewGuid()}"; - [Fact] - public async Task Should_subscribe_to_next_events() + var commit1 = new[] { - var streamName = $"test-{Guid.NewGuid()}"; - - var commit1 = new[] - { - CreateEventData(1), - CreateEventData(2) - }; - - // Append and read in parallel. - await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit1); - }); + CreateEventData(1), + CreateEventData(2) + }; - var commit2 = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + // Append and read in parallel. + await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit1); + }); - // Append and read in parallel. - var readEventsFromPosition = await QueryWithSubscriptionAsync(streamName, async () => - { - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit2); - }); + var commit2 = new[] + { + CreateEventData(1), + CreateEventData(2) + }; - var expectedFromPosition = new[] - { - new StoredEvent(streamName, "Position", 2, commit2[0]), - new StoredEvent(streamName, "Position", 3, commit2[1]) - }; + // Append and read in parallel. + var readEventsFromPosition = await QueryWithSubscriptionAsync(streamName, async () => + { + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit2); + }); - var readEventsFromBeginning = await QueryWithSubscriptionAsync(streamName, fromBeginning: true); + var expectedFromPosition = new[] + { + new StoredEvent(streamName, "Position", 2, commit2[0]), + new StoredEvent(streamName, "Position", 3, commit2[1]) + }; - var expectedFromBeginning = new[] - { - new StoredEvent(streamName, "Position", 0, commit1[0]), - new StoredEvent(streamName, "Position", 1, commit1[1]), - new StoredEvent(streamName, "Position", 2, commit2[0]), - new StoredEvent(streamName, "Position", 3, commit2[1]) - }; - - ShouldBeEquivalentTo(readEventsFromPosition?.TakeLast(2), expectedFromPosition); - ShouldBeEquivalentTo(readEventsFromBeginning?.TakeLast(4), expectedFromBeginning); - } + var readEventsFromBeginning = await QueryWithSubscriptionAsync(streamName, fromBeginning: true); - [Fact] - public async Task Should_subscribe_with_parallel_writes() + var expectedFromBeginning = new[] { - var streamName = $"test-{Guid.NewGuid()}"; + new StoredEvent(streamName, "Position", 0, commit1[0]), + new StoredEvent(streamName, "Position", 1, commit1[1]), + new StoredEvent(streamName, "Position", 2, commit2[0]), + new StoredEvent(streamName, "Position", 3, commit2[1]) + }; + + ShouldBeEquivalentTo(readEventsFromPosition?.TakeLast(2), expectedFromPosition); + ShouldBeEquivalentTo(readEventsFromBeginning?.TakeLast(4), expectedFromBeginning); + } + + [Fact] + public async Task Should_subscribe_with_parallel_writes() + { + var streamName = $"test-{Guid.NewGuid()}"; - var numTasks = 50; - var numEvents = 100; + var numTasks = 50; + var numEvents = 100; - // Append and read in parallel. - var readEvents = await QueryWithSubscriptionAsync($"^{streamName}", async () => + // Append and read in parallel. + var readEvents = await QueryWithSubscriptionAsync($"^{streamName}", async () => + { + await Parallel.ForEachAsync(Enumerable.Range(0, numTasks), async (i, ct) => { - await Parallel.ForEachAsync(Enumerable.Range(0, numTasks), async (i, ct) => - { - var fullStreamName = $"{streamName}-{Guid.NewGuid()}"; + var fullStreamName = $"{streamName}-{Guid.NewGuid()}"; - for (var j = 0; j < numEvents; j++) + for (var j = 0; j < numEvents; j++) + { + var commit1 = new[] { - var commit1 = new[] - { - CreateEventData(i * j) - }; + CreateEventData(i * j) + }; - await Sut.AppendAsync(Guid.NewGuid(), fullStreamName, commit1); - } - }); + await Sut.AppendAsync(Guid.NewGuid(), fullStreamName, commit1); + } }); + }); - Assert.Equal(numEvents * numTasks, readEvents?.Count); - } + Assert.Equal(numEvents * numTasks, readEvents?.Count); + } + + [Fact] + public async Task Should_read_events_from_offset() + { + var streamName = $"test-{Guid.NewGuid()}"; - [Fact] - public async Task Should_read_events_from_offset() + var commit = new[] { - var streamName = $"test-{Guid.NewGuid()}"; + CreateEventData(1), + CreateEventData(2) + }; - var commit = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit); - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit); + var firstRead = await QueryAsync(streamName); - var firstRead = await QueryAsync(streamName); + var readEvents1 = await QueryAsync(streamName, 1); + var readEvents2 = await QueryAllAsync(streamName, firstRead[0].EventPosition); - var readEvents1 = await QueryAsync(streamName, 1); - var readEvents2 = await QueryAllAsync(streamName, firstRead[0].EventPosition); + var expected = new[] + { + new StoredEvent(streamName, "Position", 1, commit[1]) + }; - var expected = new[] - { - new StoredEvent(streamName, "Position", 1, commit[1]) - }; + ShouldBeEquivalentTo(readEvents1, expected); + ShouldBeEquivalentTo(readEvents2, expected); + } - ShouldBeEquivalentTo(readEvents1, expected); - ShouldBeEquivalentTo(readEvents2, expected); - } + [Fact] + public async Task Should_read_multiple_streams() + { + var streamName1 = $"test-{Guid.NewGuid()}"; + var streamName2 = $"test-{Guid.NewGuid()}"; - [Fact] - public async Task Should_read_multiple_streams() + var stream1Commit = new[] { - var streamName1 = $"test-{Guid.NewGuid()}"; - var streamName2 = $"test-{Guid.NewGuid()}"; + CreateEventData(1), + CreateEventData(2) + }; - var stream1Commit = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + var stream2Commit = new[] + { + CreateEventData(3), + CreateEventData(4) + }; - var stream2Commit = new[] - { - CreateEventData(3), - CreateEventData(4) - }; + await Sut.AppendAsync(Guid.NewGuid(), streamName1, stream1Commit); + await Sut.AppendAsync(Guid.NewGuid(), streamName2, stream2Commit); - await Sut.AppendAsync(Guid.NewGuid(), streamName1, stream1Commit); - await Sut.AppendAsync(Guid.NewGuid(), streamName2, stream2Commit); + var readEvents = await Sut.QueryManyAsync(new[] { streamName1, streamName2 }); - var readEvents = await Sut.QueryManyAsync(new[] { streamName1, streamName2 }); + var expected1 = new[] + { + new StoredEvent(streamName1, "Position", 0, stream1Commit[0]), + new StoredEvent(streamName1, "Position", 1, stream1Commit[1]) + }; - var expected1 = new[] - { - new StoredEvent(streamName1, "Position", 0, stream1Commit[0]), - new StoredEvent(streamName1, "Position", 1, stream1Commit[1]) - }; + var expected2 = new[] + { + new StoredEvent(streamName2, "Position", 0, stream2Commit[0]), + new StoredEvent(streamName2, "Position", 1, stream2Commit[1]) + }; - var expected2 = new[] - { - new StoredEvent(streamName2, "Position", 0, stream2Commit[0]), - new StoredEvent(streamName2, "Position", 1, stream2Commit[1]) - }; + ShouldBeEquivalentTo(readEvents[streamName1], expected1); + ShouldBeEquivalentTo(readEvents[streamName2], expected2); + } + + [Theory] + [InlineData(5, 30)] + [InlineData(5, 300)] + public async Task Should_read_latest_events(int commitSize, int count) + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new List<EventData>(); - ShouldBeEquivalentTo(readEvents[streamName1], expected1); - ShouldBeEquivalentTo(readEvents[streamName2], expected2); + for (var i = 0; i < count; i++) + { + events.Add(CreateEventData(i)); } - [Theory] - [InlineData(5, 30)] - [InlineData(5, 300)] - public async Task Should_read_latest_events(int commitSize, int count) + for (var i = 0; i < events.Count / commitSize; i++) { - var streamName = $"test-{Guid.NewGuid()}"; + var commit = events.Skip(i * commitSize).Take(commitSize); - var events = new List<EventData>(); + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit.ToArray()); + } - for (var i = 0; i < count; i++) - { - events.Add(CreateEventData(i)); - } + var allExpected = events.Select((x, i) => new StoredEvent(streamName, "Position", i, events[i])).ToArray(); - for (var i = 0; i < events.Count / commitSize; i++) - { - var commit = events.Skip(i * commitSize).Take(commitSize); + for (var take = 0; take < count; take += count / 10) + { + var eventsExpected = allExpected.TakeLast(take).ToArray(); + var eventsQueried = await Sut.QueryReverseAsync(streamName, take); - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit.ToArray()); - } + ShouldBeEquivalentTo(eventsQueried, eventsExpected); + } + } - var allExpected = events.Select((x, i) => new StoredEvent(streamName, "Position", i, events[i])).ToArray(); + [Theory] + [InlineData(5, 30)] + [InlineData(5, 300)] + public async Task Should_read_reverse(int commitSize, int count) + { + var streamName = $"test-{Guid.NewGuid()}"; - for (var take = 0; take < count; take += count / 10) - { - var eventsExpected = allExpected.TakeLast(take).ToArray(); - var eventsQueried = await Sut.QueryReverseAsync(streamName, take); + var events = new List<EventData>(); - ShouldBeEquivalentTo(eventsQueried, eventsExpected); - } + for (var i = 0; i < count; i++) + { + events.Add(CreateEventData(i)); } - [Theory] - [InlineData(5, 30)] - [InlineData(5, 300)] - public async Task Should_read_reverse(int commitSize, int count) + for (var i = 0; i < events.Count / commitSize; i++) { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new List<EventData>(); - - for (var i = 0; i < count; i++) - { - events.Add(CreateEventData(i)); - } - - for (var i = 0; i < events.Count / commitSize; i++) - { - var commit = events.Skip(i * commitSize).Take(commitSize); + var commit = events.Skip(i * commitSize).Take(commitSize); - await Sut.AppendAsync(Guid.NewGuid(), streamName, commit.ToArray()); - } + await Sut.AppendAsync(Guid.NewGuid(), streamName, commit.ToArray()); + } - var allExpected = events.Select((x, i) => new StoredEvent(streamName, "Position", i, events[i])).ToArray(); + var allExpected = events.Select((x, i) => new StoredEvent(streamName, "Position", i, events[i])).ToArray(); - for (var take = 0; take < count; take += count / 10) - { - var eventsExpected = allExpected.Reverse().Take(take).ToArray(); - var eventsQueried = await Sut.QueryAllReverseAsync(streamName, default, take).ToArrayAsync(); + for (var take = 0; take < count; take += count / 10) + { + var eventsExpected = allExpected.Reverse().Take(take).ToArray(); + var eventsQueried = await Sut.QueryAllReverseAsync(streamName, default, take).ToArrayAsync(); - ShouldBeEquivalentTo(eventsQueried, eventsExpected); - } + ShouldBeEquivalentTo(eventsQueried, eventsExpected); } + } - [Fact] - public async Task Should_delete_by_filter() - { - var streamName = $"test-{Guid.NewGuid()}"; - - var events = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + [Fact] + public async Task Should_delete_by_filter() + { + var streamName = $"test-{Guid.NewGuid()}"; - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + var events = new[] + { + CreateEventData(1), + CreateEventData(2) + }; - IReadOnlyList<StoredEvent>? readEvents = null; + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - for (var i = 0; i < 5; i++) - { - await Sut.DeleteAsync($"^{streamName[..10]}"); + IReadOnlyList<StoredEvent>? readEvents = null; - readEvents = await QueryAsync(streamName); + for (var i = 0; i < 5; i++) + { + await Sut.DeleteAsync($"^{streamName[..10]}"); - if (readEvents.Count == 0) - { - break; - } + readEvents = await QueryAsync(streamName); - // Get event store needs a little bit of time for the projections. - await Task.Delay(1000); + if (readEvents.Count == 0) + { + break; } - Assert.Empty(readEvents); + // Get event store needs a little bit of time for the projections. + await Task.Delay(1000); } - [Fact] - public async Task Should_delete_stream() - { - var streamName = $"test-{Guid.NewGuid()}"; + Assert.Empty(readEvents); + } - var events = new[] - { - CreateEventData(1), - CreateEventData(2) - }; + [Fact] + public async Task Should_delete_stream() + { + var streamName = $"test-{Guid.NewGuid()}"; - await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + var events = new[] + { + CreateEventData(1), + CreateEventData(2) + }; - IReadOnlyList<StoredEvent>? readEvents = null; + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); - for (var i = 0; i < 5; i++) - { - await Sut.DeleteStreamAsync(streamName); + IReadOnlyList<StoredEvent>? readEvents = null; - readEvents = await QueryAsync(streamName); + for (var i = 0; i < 5; i++) + { + await Sut.DeleteStreamAsync(streamName); - if (readEvents.Count == 0) - { - break; - } + readEvents = await QueryAsync(streamName); - // Get event store needs a little bit of time for the projections. - await Task.Delay(1000); + if (readEvents.Count == 0) + { + break; } - Assert.Empty(readEvents); + // Get event store needs a little bit of time for the projections. + await Task.Delay(1000); } - private Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long position = EtagVersion.Any) - { - return Sut.QueryAsync(streamName, position); - } + Assert.Empty(readEvents); + } + + private Task<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long position = EtagVersion.Any) + { + return Sut.QueryAsync(streamName, position); + } - private static EventData CreateEventData(int i) + private static EventData CreateEventData(int i) + { + var headers = new EnvelopeHeaders { - var headers = new EnvelopeHeaders - { - [CommonHeaders.EventId] = Guid.NewGuid().ToString() - }; + [CommonHeaders.EventId] = Guid.NewGuid().ToString() + }; - return new EventData($"Type{i}", headers, i.ToString(CultureInfo.InvariantCulture)); - } + return new EventData($"Type{i}", headers, i.ToString(CultureInfo.InvariantCulture)); + } - private async Task<IReadOnlyList<StoredEvent>?> QueryAllAsync(string? streamFilter = null, string? position = null) + private async Task<IReadOnlyList<StoredEvent>?> QueryAllAsync(string? streamFilter = null, string? position = null) + { + var readEvents = new List<StoredEvent>(); + + await foreach (var storedEvent in Sut.QueryAllAsync(streamFilter, position)) { - var readEvents = new List<StoredEvent>(); + readEvents.Add(storedEvent); + } - await foreach (var storedEvent in Sut.QueryAllAsync(streamFilter, position)) - { - readEvents.Add(storedEvent); - } + return readEvents; + } - return readEvents; - } + private async Task<IReadOnlyList<StoredEvent>?> QueryWithSubscriptionAsync(string streamFilter, + Func<Task>? subscriptionRunning = null, bool fromBeginning = false) + { + var subscriber = new EventSubscriber(); - private async Task<IReadOnlyList<StoredEvent>?> QueryWithSubscriptionAsync(string streamFilter, - Func<Task>? subscriptionRunning = null, bool fromBeginning = false) + IEventSubscription? subscription = null; + try { - var subscriber = new EventSubscriber(); + subscription = Sut.CreateSubscription(subscriber, streamFilter, fromBeginning ? null : subscriptionPosition); - IEventSubscription? subscription = null; - try + if (subscriptionRunning != null) { - subscription = Sut.CreateSubscription(subscriber, streamFilter, fromBeginning ? null : subscriptionPosition); - - if (subscriptionRunning != null) - { - await subscriptionRunning(); - } + await subscriptionRunning(); + } - using (var cts = new CancellationTokenSource(30000)) + using (var cts = new CancellationTokenSource(30000)) + { + while (!cts.IsCancellationRequested) { - while (!cts.IsCancellationRequested) - { - subscription.WakeUp(); + subscription.WakeUp(); - await Task.Delay(2000, cts.Token); + await Task.Delay(2000, cts.Token); - if (subscriber.Events.Count > 0) - { - subscriptionPosition = subscriber.LastPosition; + if (subscriber.Events.Count > 0) + { + subscriptionPosition = subscriber.LastPosition; - return subscriber.Events; - } + return subscriber.Events; } + } - cts.Token.ThrowIfCancellationRequested(); + cts.Token.ThrowIfCancellationRequested(); - return null; - } - } - finally - { - subscription?.Dispose(); + return null; } } - - private static void ShouldBeEquivalentTo(IEnumerable<StoredEvent>? actual, params StoredEvent[] expected) + finally { - actual.Should().BeEquivalentTo(expected, opts => opts.ComparingByMembers<StoredEvent>().Including(x => x.EventStreamNumber)); + subscription?.Dispose(); } } + + private static void ShouldBeEquivalentTo(IEnumerable<StoredEvent>? actual, params StoredEvent[] expected) + { + actual.Should().BeEquivalentTo(expected, opts => opts.ComparingByMembers<StoredEvent>().Including(x => x.EventStreamNumber)); + } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs index 71597b2be5..1467f86fed 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs @@ -9,40 +9,39 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public sealed class GetEventStoreFixture : IAsyncLifetime { - public sealed class GetEventStoreFixture : IAsyncLifetime - { - private readonly EventStoreClientSettings settings; + private readonly EventStoreClientSettings settings; - public GetEventStore EventStore { get; } + public GetEventStore EventStore { get; } - public GetEventStoreFixture() - { - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + public GetEventStoreFixture() + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - settings = EventStoreClientSettings.Create(TestConfig.Configuration["eventStore:configuration"]); + settings = EventStoreClientSettings.Create(TestConfig.Configuration["eventStore:configuration"]); - EventStore = new GetEventStore(settings, TestUtils.DefaultSerializer); - } + EventStore = new GetEventStore(settings, TestUtils.DefaultSerializer); + } - public Task InitializeAsync() - { - return EventStore.InitializeAsync(default); - } + public Task InitializeAsync() + { + return EventStore.InitializeAsync(default); + } - public async Task DisposeAsync() + public async Task DisposeAsync() + { + var projectionsManager = new EventStoreProjectionManagementClient(settings); + + await foreach (var projection in projectionsManager.ListAllAsync()) { - var projectionsManager = new EventStoreProjectionManagementClient(settings); + var name = projection.Name; - await foreach (var projection in projectionsManager.ListAllAsync()) + if (name.StartsWith("by-squidex-test", StringComparison.OrdinalIgnoreCase)) { - var name = projection.Name; - - if (name.StartsWith("by-squidex-test", StringComparison.OrdinalIgnoreCase)) - { - await projectionsManager.DisableAsync(name); - } + await projectionsManager.DisableAsync(name); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs index 8c2168122f..b51a1c0e31 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs @@ -9,21 +9,20 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +[Trait("Category", "Dependencies")] +public class GetEventStoreTests : EventStoreTests<GetEventStore>, IClassFixture<GetEventStoreFixture> { - [Trait("Category", "Dependencies")] - public class GetEventStoreTests : EventStoreTests<GetEventStore>, IClassFixture<GetEventStoreFixture> - { - public GetEventStoreFixture _ { get; } + public GetEventStoreFixture _ { get; } - public GetEventStoreTests(GetEventStoreFixture fixture) - { - _ = fixture; - } + public GetEventStoreTests(GetEventStoreFixture fixture) + { + _ = fixture; + } - public override GetEventStore CreateStore() - { - return _.EventStore; - } + public override GetEventStore CreateStore() + { + return _.EventStore; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs index 8ab042f11c..97143486b3 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs @@ -13,50 +13,49 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public abstract class MongoEventStoreFixture : IAsyncLifetime { - public abstract class MongoEventStoreFixture : IAsyncLifetime - { - private readonly IMongoClient mongoClient; - private readonly IMongoDatabase mongoDatabase; - private readonly IEventNotifier notifier = A.Fake<IEventNotifier>(); + private readonly IMongoClient mongoClient; + private readonly IMongoDatabase mongoDatabase; + private readonly IEventNotifier notifier = A.Fake<IEventNotifier>(); - public MongoEventStore EventStore { get; } + public MongoEventStore EventStore { get; } - protected MongoEventStoreFixture(string connectionString) - { - mongoClient = new MongoClient(connectionString); - mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); + protected MongoEventStoreFixture(string connectionString) + { + mongoClient = new MongoClient(connectionString); + mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); - BsonJsonConvention.Register(TestUtils.DefaultOptions()); + BsonJsonConvention.Register(TestUtils.DefaultOptions()); - EventStore = new MongoEventStore(mongoDatabase, notifier); - } + EventStore = new MongoEventStore(mongoDatabase, notifier); + } - public Task InitializeAsync() - { - return EventStore.InitializeAsync(default); - } + public Task InitializeAsync() + { + return EventStore.InitializeAsync(default); + } - public Task DisposeAsync() - { - return mongoClient.DropDatabaseAsync(mongoDatabase.DatabaseNamespace.DatabaseName); - } + public Task DisposeAsync() + { + return mongoClient.DropDatabaseAsync(mongoDatabase.DatabaseNamespace.DatabaseName); } +} - public sealed class MongoEventStoreDirectFixture : MongoEventStoreFixture +public sealed class MongoEventStoreDirectFixture : MongoEventStoreFixture +{ + public MongoEventStoreDirectFixture() + : base(TestConfig.Configuration["mongodb:configuration"]) { - public MongoEventStoreDirectFixture() - : base(TestConfig.Configuration["mongodb:configuration"]) - { - } } +} - public sealed class MongoEventStoreReplicaSetFixture : MongoEventStoreFixture +public sealed class MongoEventStoreReplicaSetFixture : MongoEventStoreFixture +{ + public MongoEventStoreReplicaSetFixture() + : base(TestConfig.Configuration["mongodb:configurationReplica"]) { - public MongoEventStoreReplicaSetFixture() - : base(TestConfig.Configuration["mongodb:configurationReplica"]) - { - } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests_Direct.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests_Direct.cs index b7abd1d723..5d6d73b58c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests_Direct.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests_Direct.cs @@ -9,21 +9,20 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +[Trait("Category", "Dependencies")] +public class MongoEventStoreTests_Direct : EventStoreTests<MongoEventStore>, IClassFixture<MongoEventStoreDirectFixture> { - [Trait("Category", "Dependencies")] - public class MongoEventStoreTests_Direct : EventStoreTests<MongoEventStore>, IClassFixture<MongoEventStoreDirectFixture> - { - public MongoEventStoreFixture _ { get; } + public MongoEventStoreFixture _ { get; } - public MongoEventStoreTests_Direct(MongoEventStoreDirectFixture fixture) - { - _ = fixture; - } + public MongoEventStoreTests_Direct(MongoEventStoreDirectFixture fixture) + { + _ = fixture; + } - public override MongoEventStore CreateStore() - { - return _.EventStore; - } + public override MongoEventStore CreateStore() + { + return _.EventStore; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests_ReplicaSet.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests_ReplicaSet.cs index dd481fd92b..4458554e2c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests_ReplicaSet.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests_ReplicaSet.cs @@ -9,21 +9,20 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +[Trait("Category", "Dependencies")] +public class MongoEventStoreTests_ReplicaSet : EventStoreTests<MongoEventStore>, IClassFixture<MongoEventStoreReplicaSetFixture> { - [Trait("Category", "Dependencies")] - public class MongoEventStoreTests_ReplicaSet : EventStoreTests<MongoEventStore>, IClassFixture<MongoEventStoreReplicaSetFixture> - { - public MongoEventStoreFixture _ { get; } + public MongoEventStoreFixture _ { get; } - public MongoEventStoreTests_ReplicaSet(MongoEventStoreReplicaSetFixture fixture) - { - _ = fixture; - } + public MongoEventStoreTests_ReplicaSet(MongoEventStoreReplicaSetFixture fixture) + { + _ = fixture; + } - public override MongoEventStore CreateStore() - { - return _.EventStore; - } + public override MongoEventStore CreateStore() + { + return _.EventStore; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs index 995fb5ebb2..d3b9e6e56b 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs @@ -14,222 +14,221 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +[Trait("Category", "Dependencies")] +public sealed class MongoParallelInsertTests : IClassFixture<MongoEventStoreReplicaSetFixture> { - [Trait("Category", "Dependencies")] - public sealed class MongoParallelInsertTests : IClassFixture<MongoEventStoreReplicaSetFixture> + private readonly TestState<EventConsumerState> state = new TestState<EventConsumerState>(DomainId.Empty); + private readonly IEventFormatter eventFormatter; + + public MongoEventStoreFixture _ { get; } + + public class MyEvent : IEvent { - private readonly TestState<EventConsumerState> state = new TestState<EventConsumerState>(DomainId.Empty); - private readonly IEventFormatter eventFormatter; + } - public MongoEventStoreFixture _ { get; } + public sealed class MyEventConsumer : IEventConsumer + { + private readonly HashSet<Guid> uniqueReceivedEvents = new HashSet<Guid>(); + private readonly TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); + private readonly int expectedCount; - public class MyEvent : IEvent - { - } + public Func<int, Task> EventReceived { get; set; } - public sealed class MyEventConsumer : IEventConsumer - { - private readonly HashSet<Guid> uniqueReceivedEvents = new HashSet<Guid>(); - private readonly TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); - private readonly int expectedCount; + public int Received { get; set; } - public Func<int, Task> EventReceived { get; set; } + public string Name { get; } = RandomHash.Simple(); - public int Received { get; set; } + public string EventsFilter => $"^{Name}"; - public string Name { get; } = RandomHash.Simple(); + public Task Completed => tcs.Task; - public string EventsFilter => $"^{Name}"; + public MyEventConsumer(int expectedCount) + { + this.expectedCount = expectedCount; + } - public Task Completed => tcs.Task; + public async Task On(Envelope<IEvent> @event) + { + Received++; + + uniqueReceivedEvents.Add(@event.Headers.EventId()); - public MyEventConsumer(int expectedCount) + if (uniqueReceivedEvents.Count == expectedCount) { - this.expectedCount = expectedCount; + tcs.TrySetResult(true); } - public async Task On(Envelope<IEvent> @event) + if (EventReceived != null) { - Received++; - - uniqueReceivedEvents.Add(@event.Headers.EventId()); - - if (uniqueReceivedEvents.Count == expectedCount) - { - tcs.TrySetResult(true); - } - - if (EventReceived != null) - { - await EventReceived(Received); - } + await EventReceived(Received); } } + } - public MongoParallelInsertTests(MongoEventStoreReplicaSetFixture fixture) - { - _ = fixture; + public MongoParallelInsertTests(MongoEventStoreReplicaSetFixture fixture) + { + _ = fixture; - var typeNameRegistry = new TypeNameRegistry().Map(typeof(MyEvent), "My"); + var typeNameRegistry = new TypeNameRegistry().Map(typeof(MyEvent), "My"); - eventFormatter = new DefaultEventFormatter(typeNameRegistry, TestUtils.DefaultSerializer); - } + eventFormatter = new DefaultEventFormatter(typeNameRegistry, TestUtils.DefaultSerializer); + } - [Fact] - public async Task Should_insert_and_retrieve_parallel() - { - const int expectedEvents = 2_000; + [Fact] + public async Task Should_insert_and_retrieve_parallel() + { + const int expectedEvents = 2_000; - var eventConsumer = new MyEventConsumer(expectedEvents); - var eventProcessor = BuildProcessor(eventConsumer); + var eventConsumer = new MyEventConsumer(expectedEvents); + var eventProcessor = BuildProcessor(eventConsumer); - await eventProcessor.InitializeAsync(default); - await eventProcessor.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await InsertAsync(eventConsumer, expectedEvents, parallelism: 20); + await InsertAsync(eventConsumer, expectedEvents, parallelism: 20); - await AssertConsumerAsync(expectedEvents, eventConsumer); - } + await AssertConsumerAsync(expectedEvents, eventConsumer); + } - [Fact] - public async Task Should_insert_and_retrieve_parallel_with_multiple_events_per_commit() - { - const int expectedEvents = 2_000; + [Fact] + public async Task Should_insert_and_retrieve_parallel_with_multiple_events_per_commit() + { + const int expectedEvents = 2_000; - var eventConsumer = new MyEventConsumer(expectedEvents); - var eventProcessor = BuildProcessor(eventConsumer); + var eventConsumer = new MyEventConsumer(expectedEvents); + var eventProcessor = BuildProcessor(eventConsumer); - await eventProcessor.InitializeAsync(default); - await eventProcessor.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await InsertAsync(eventConsumer, expectedEvents, messagesPerCommit: 2); + await InsertAsync(eventConsumer, expectedEvents, messagesPerCommit: 2); - await AssertConsumerAsync(expectedEvents, eventConsumer); - } + await AssertConsumerAsync(expectedEvents, eventConsumer); + } - [Fact] - public async Task Should_insert_and_retrieve_afterwards() - { - const int expectedEvents = 2_000; + [Fact] + public async Task Should_insert_and_retrieve_afterwards() + { + const int expectedEvents = 2_000; - var eventConsumer = new MyEventConsumer(expectedEvents); - var eventProcessor = BuildProcessor(eventConsumer); + var eventConsumer = new MyEventConsumer(expectedEvents); + var eventProcessor = BuildProcessor(eventConsumer); - await InsertAsync(eventConsumer, expectedEvents); + await InsertAsync(eventConsumer, expectedEvents); - await eventProcessor.InitializeAsync(default); - await eventProcessor.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await AssertConsumerAsync(expectedEvents, eventConsumer); - } + await AssertConsumerAsync(expectedEvents, eventConsumer); + } - [Fact] - public async Task Should_insert_and_retrieve_partially_afterwards() - { - const int expectedEvents = 2_000; + [Fact] + public async Task Should_insert_and_retrieve_partially_afterwards() + { + const int expectedEvents = 2_000; - var eventConsumer = new MyEventConsumer(expectedEvents); - var eventProcessor = BuildProcessor(eventConsumer); + var eventConsumer = new MyEventConsumer(expectedEvents); + var eventProcessor = BuildProcessor(eventConsumer); - await InsertAsync(eventConsumer, expectedEvents / 2); + await InsertAsync(eventConsumer, expectedEvents / 2); - await eventProcessor.InitializeAsync(default); - await eventProcessor.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await InsertAsync(eventConsumer, expectedEvents / 2); + await InsertAsync(eventConsumer, expectedEvents / 2); - await AssertConsumerAsync(expectedEvents, eventConsumer); - } + await AssertConsumerAsync(expectedEvents, eventConsumer); + } - [Fact] - public async Task Should_insert_and_retrieve_parallel_with_waits() - { - const int expectedEvents = 2_000; + [Fact] + public async Task Should_insert_and_retrieve_parallel_with_waits() + { + const int expectedEvents = 2_000; - var eventConsumer = new MyEventConsumer(expectedEvents); - var eventProcessor = BuildProcessor(eventConsumer); + var eventConsumer = new MyEventConsumer(expectedEvents); + var eventProcessor = BuildProcessor(eventConsumer); - await eventProcessor.InitializeAsync(default); - await eventProcessor.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await InsertAsync(eventConsumer, expectedEvents, iterations: 10); + await InsertAsync(eventConsumer, expectedEvents, iterations: 10); - await AssertConsumerAsync(expectedEvents, eventConsumer); - } + await AssertConsumerAsync(expectedEvents, eventConsumer); + } - [Fact] - public async Task Should_insert_and_retrieve_parallel_with_stops_and_starts() - { - const int expectedEvents = 2_000; + [Fact] + public async Task Should_insert_and_retrieve_parallel_with_stops_and_starts() + { + const int expectedEvents = 2_000; - var eventConsumer = new MyEventConsumer(expectedEvents); - var eventProcessor = BuildProcessor(eventConsumer); + var eventConsumer = new MyEventConsumer(expectedEvents); + var eventProcessor = BuildProcessor(eventConsumer); - eventConsumer.EventReceived = async count => + eventConsumer.EventReceived = async count => + { + if (count % 1000 == 0) { - if (count % 1000 == 0) - { - await eventProcessor.StopAsync(); - await eventProcessor.StartAsync(); - } - }; + await eventProcessor.StopAsync(); + await eventProcessor.StartAsync(); + } + }; - await eventProcessor.InitializeAsync(default); - await eventProcessor.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await InsertAsync(eventConsumer, expectedEvents); + await InsertAsync(eventConsumer, expectedEvents); - await AssertConsumerAsync(expectedEvents, eventConsumer); - } + await AssertConsumerAsync(expectedEvents, eventConsumer); + } - private EventConsumerProcessor BuildProcessor(IEventConsumer eventConsumer) - { - return new EventConsumerProcessor( - state.PersistenceFactory, - eventConsumer, - eventFormatter, - _.EventStore, - A.Fake<ILogger<EventConsumerProcessor>>()); - } + private EventConsumerProcessor BuildProcessor(IEventConsumer eventConsumer) + { + return new EventConsumerProcessor( + state.PersistenceFactory, + eventConsumer, + eventFormatter, + _.EventStore, + A.Fake<ILogger<EventConsumerProcessor>>()); + } - private Task InsertAsync(IEventConsumer consumer, int numItems, int parallelism = 5, int messagesPerCommit = 1, int iterations = 1) - { - var perTask = numItems / (parallelism * messagesPerCommit * iterations); + private Task InsertAsync(IEventConsumer consumer, int numItems, int parallelism = 5, int messagesPerCommit = 1, int iterations = 1) + { + var perTask = numItems / (parallelism * messagesPerCommit * iterations); - return Parallel.ForEachAsync(Enumerable.Range(0, parallelism), async (_, _) => + return Parallel.ForEachAsync(Enumerable.Range(0, parallelism), async (_, _) => + { + for (var i = 0; i < iterations; i++) { - for (var i = 0; i < iterations; i++) + for (var j = 0; j < perTask; j++) { - for (var j = 0; j < perTask; j++) - { - var streamName = $"{consumer.Name}-{Guid.NewGuid()}"; + var streamName = $"{consumer.Name}-{Guid.NewGuid()}"; - var commitId = Guid.NewGuid(); - var commitList = new List<EventData>(); + var commitId = Guid.NewGuid(); + var commitList = new List<EventData>(); - for (var k = 0; k < messagesPerCommit; k++) - { - commitList.Add(eventFormatter.ToEventData(Envelope.Create<IEvent>(new MyEvent()), commitId)); - } - - await _.EventStore.AppendAsync(commitId, streamName, commitList); - } - - if (i < iterations - 1) + for (var k = 0; k < messagesPerCommit; k++) { - await Task.Delay(1000); + commitList.Add(eventFormatter.ToEventData(Envelope.Create<IEvent>(new MyEvent()), commitId)); } + + await _.EventStore.AppendAsync(commitId, streamName, commitList); } - }); - } - private static async Task AssertConsumerAsync(int expectedEvents, MyEventConsumer eventConsumer) - { - await Task.WhenAny(eventConsumer.Completed, Task.Delay(TimeSpan.FromSeconds(20))); - await Task.Delay(2000); + if (i < iterations - 1) + { + await Task.Delay(1000); + } + } + }); + } - Assert.Equal(expectedEvents, eventConsumer.Received); - } + private static async Task AssertConsumerAsync(int expectedEvents, MyEventConsumer eventConsumer) + { + await Task.WhenAny(eventConsumer.Completed, Task.Delay(TimeSpan.FromSeconds(20))); + await Task.Delay(2000); + + Assert.Equal(expectedEvents, eventConsumer.Received); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs index 66e91f4ca3..d6209d4f23 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs @@ -10,151 +10,150 @@ using FluentAssertions; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public class PollingSubscriptionTests { - public class PollingSubscriptionTests + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly IEventSubscriber<StoredEvent> eventSubscriber = A.Fake<IEventSubscriber<StoredEvent>>(); + private readonly string position = Guid.NewGuid().ToString(); + private readonly string filter = "^my-stream"; + + [Fact] + public async Task Should_subscribe_on_start() { - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly IEventSubscriber<StoredEvent> eventSubscriber = A.Fake<IEventSubscriber<StoredEvent>>(); - private readonly string position = Guid.NewGuid().ToString(); - private readonly string filter = "^my-stream"; + await SubscribeAsync(false); - [Fact] - public async Task Should_subscribe_on_start() - { - await SubscribeAsync(false); + A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); + } - A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); - } + [Fact] + public async Task Should_forward_exception_to_subscriber() + { + var ex = new InvalidOperationException(); - [Fact] - public async Task Should_forward_exception_to_subscriber() - { - var ex = new InvalidOperationException(); + A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) + .Throws(ex); - A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) - .Throws(ex); + var sut = await SubscribeAsync(false); - var sut = await SubscribeAsync(false); + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustHaveHappened(); + } - A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_forward_operation_cancelled_exception_to_subscriber() + { + var ex = new OperationCanceledException(); - [Fact] - public async Task Should_forward_operation_cancelled_exception_to_subscriber() - { - var ex = new OperationCanceledException(); + A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) + .Throws(ex); - A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) - .Throws(ex); + var sut = await SubscribeAsync(false); - var sut = await SubscribeAsync(false); + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustHaveHappened(); + } - A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_forward_aggregate_operation_cancelled_exception_to_subscriber() + { + var ex = new AggregateException(new OperationCanceledException()); - [Fact] - public async Task Should_forward_aggregate_operation_cancelled_exception_to_subscriber() - { - var ex = new AggregateException(new OperationCanceledException()); + A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) + .Throws(ex); - A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) - .Throws(ex); + var sut = await SubscribeAsync(false); - var sut = await SubscribeAsync(false); + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustHaveHappened(); + } - A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_wake_up() + { + var sut = await SubscribeAsync(true); - [Fact] - public async Task Should_wake_up() - { - var sut = await SubscribeAsync(true); + A.CallTo(() => eventStore.QueryAllAsync(filter, A<string>._, A<int>._, A<CancellationToken>._)) + .MustHaveHappened(2, Times.Exactly); + } - A.CallTo(() => eventStore.QueryAllAsync(filter, A<string>._, A<int>._, A<CancellationToken>._)) - .MustHaveHappened(2, Times.Exactly); - } + [Fact] + public async Task Should_forward_events_to_subscriber() + { + var events = Enumerable.Range(0, 50).Select(CreateEvent).ToArray(); - [Fact] - public async Task Should_forward_events_to_subscriber() - { - var events = Enumerable.Range(0, 50).Select(CreateEvent).ToArray(); + var receivedEvents = new List<StoredEvent>(); - var receivedEvents = new List<StoredEvent>(); + A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) + .Returns(events.ToAsyncEnumerable()); - A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) - .Returns(events.ToAsyncEnumerable()); + A.CallTo(() => eventSubscriber.OnNextAsync(A<IEventSubscription>._, A<StoredEvent>._)) + .Invokes(x => receivedEvents.Add(x.GetArgument<StoredEvent>(1)!)); - A.CallTo(() => eventSubscriber.OnNextAsync(A<IEventSubscription>._, A<StoredEvent>._)) - .Invokes(x => receivedEvents.Add(x.GetArgument<StoredEvent>(1)!)); + await SubscribeAsync(true); - await SubscribeAsync(true); + receivedEvents.Should().BeEquivalentTo(events); + } - receivedEvents.Should().BeEquivalentTo(events); - } + [Fact] + public async Task Should_continue_on_last_position() + { + var events1 = Enumerable.Range(10, 10).Select(CreateEvent).ToArray(); + var events2 = Enumerable.Range(20, 10).Select(CreateEvent).ToArray(); - [Fact] - public async Task Should_continue_on_last_position() - { - var events1 = Enumerable.Range(10, 10).Select(CreateEvent).ToArray(); - var events2 = Enumerable.Range(20, 10).Select(CreateEvent).ToArray(); + var lastPosition = events1[^1].EventPosition; - var lastPosition = events1[^1].EventPosition; + var receivedEvents = new List<StoredEvent>(); - var receivedEvents = new List<StoredEvent>(); + A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) + .Returns(events1.ToAsyncEnumerable()); - A.CallTo(() => eventStore.QueryAllAsync(filter, position, A<int>._, A<CancellationToken>._)) - .Returns(events1.ToAsyncEnumerable()); + A.CallTo(() => eventStore.QueryAllAsync(filter, lastPosition, A<int>._, A<CancellationToken>._)) + .Returns(events2.ToAsyncEnumerable()); - A.CallTo(() => eventStore.QueryAllAsync(filter, lastPosition, A<int>._, A<CancellationToken>._)) - .Returns(events2.ToAsyncEnumerable()); + A.CallTo(() => eventSubscriber.OnNextAsync(A<IEventSubscription>._, A<StoredEvent>._)) + .Invokes(x => receivedEvents.Add(x.GetArgument<StoredEvent>(1)!)); - A.CallTo(() => eventSubscriber.OnNextAsync(A<IEventSubscription>._, A<StoredEvent>._)) - .Invokes(x => receivedEvents.Add(x.GetArgument<StoredEvent>(1)!)); + await SubscribeAsync(true); - await SubscribeAsync(true); + receivedEvents.Should().BeEquivalentTo(events1.Union(events2)); + } - receivedEvents.Should().BeEquivalentTo(events1.Union(events2)); - } + private StoredEvent CreateEvent(int position) + { + return new StoredEvent( + "my-stream", + position.ToString(CultureInfo.InvariantCulture)!, + position, + new EventData( + "type", + new EnvelopeHeaders + { + [CommonHeaders.EventId] = Guid.NewGuid().ToString() + }, + "payload")); + } - private StoredEvent CreateEvent(int position) - { - return new StoredEvent( - "my-stream", - position.ToString(CultureInfo.InvariantCulture)!, - position, - new EventData( - "type", - new EnvelopeHeaders - { - [CommonHeaders.EventId] = Guid.NewGuid().ToString() - }, - "payload")); - } + private async Task<IEventSubscription> SubscribeAsync(bool wakeup = true) + { + var sut = new PollingSubscription(eventStore, eventSubscriber, filter, position); - private async Task<IEventSubscription> SubscribeAsync(bool wakeup = true) + try { - var sut = new PollingSubscription(eventStore, eventSubscriber, filter, position); - - try - { - if (wakeup) - { - sut.WakeUp(); - } - - await Task.Delay(200); - } - finally + if (wakeup) { - sut.Dispose(); + sut.WakeUp(); } - return sut; + await Task.Delay(200); } + finally + { + sut.Dispose(); + } + + return sut; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs index a1a15e74ab..dc1af01a4f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs @@ -8,157 +8,156 @@ using FakeItEasy; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public class RetrySubscriptionTests { - public class RetrySubscriptionTests - { - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly IEventSubscriber<StoredEvent> eventSubscriber = A.Fake<IEventSubscriber<StoredEvent>>(); - private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>(); - private readonly IEventSubscriber<StoredEvent> sutSubscriber; - private readonly RetrySubscription<StoredEvent> sut; + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly IEventSubscriber<StoredEvent> eventSubscriber = A.Fake<IEventSubscriber<StoredEvent>>(); + private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>(); + private readonly IEventSubscriber<StoredEvent> sutSubscriber; + private readonly RetrySubscription<StoredEvent> sut; - public RetrySubscriptionTests() - { - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) - .Returns(eventSubscription); + public RetrySubscriptionTests() + { + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) + .Returns(eventSubscription); - sut = new RetrySubscription<StoredEvent>(eventSubscriber, s => eventStore.CreateSubscription(s)) { ReconnectWaitMs = 50 }; - sutSubscriber = sut; - } + sut = new RetrySubscription<StoredEvent>(eventSubscriber, s => eventStore.CreateSubscription(s)) { ReconnectWaitMs = 50 }; + sutSubscriber = sut; + } - [Fact] - public void Should_subscribe_after_constructor() - { - sut.Dispose(); + [Fact] + public void Should_subscribe_after_constructor() + { + sut.Dispose(); - A.CallTo(() => eventStore.CreateSubscription(sut, A<string>._, A<string>._)) - .MustHaveHappened(); - } + A.CallTo(() => eventStore.CreateSubscription(sut, A<string>._, A<string>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_reopen_subscription_once_if_exception_is_retrieved() - { - var ex = new InvalidOperationException(); + [Fact] + public async Task Should_reopen_subscription_once_if_exception_is_retrieved() + { + var ex = new InvalidOperationException(); - await OnErrorAsync(eventSubscription, ex, times: 1); + await OnErrorAsync(eventSubscription, ex, times: 1); - await Task.Delay(1000); + await Task.Delay(1000); - sut.Dispose(); + sut.Dispose(); - A.CallTo(() => eventSubscription.Dispose()) - .MustHaveHappened(2, Times.Exactly); + A.CallTo(() => eventSubscription.Dispose()) + .MustHaveHappened(2, Times.Exactly); - A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) - .MustHaveHappened(2, Times.Exactly); + A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber<StoredEvent>>._, A<string>._, A<string>._)) + .MustHaveHappened(2, Times.Exactly); - A.CallTo(() => eventSubscriber.OnErrorAsync(eventSubscription, A<Exception>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => eventSubscriber.OnErrorAsync(eventSubscription, A<Exception>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_forward_error_from_inner_subscription_if_failed_often() - { - var ex = new InvalidOperationException(); + [Fact] + public async Task Should_forward_error_from_inner_subscription_if_failed_often() + { + var ex = new InvalidOperationException(); - await OnErrorAsync(eventSubscription, ex, times: 6); + await OnErrorAsync(eventSubscription, ex, times: 6); - sut.Dispose(); + sut.Dispose(); - A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) - .MustHaveHappened(); - } + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_ignore_operation_cancelled_error_from_inner_subscription_if_failed_often() - { - var ex = new OperationCanceledException(); + [Fact] + public async Task Should_ignore_operation_cancelled_error_from_inner_subscription_if_failed_often() + { + var ex = new OperationCanceledException(); - await OnErrorAsync(eventSubscription, ex, times: 6); + await OnErrorAsync(eventSubscription, ex, times: 6); - sut.Dispose(); + sut.Dispose(); - A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) - .MustNotHaveHappened(); - } + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_forward_error_if_exception_is_raised_after_unsubscribe() - { - var ex = new InvalidOperationException(); + [Fact] + public async Task Should_not_forward_error_if_exception_is_raised_after_unsubscribe() + { + var ex = new InvalidOperationException(); - await OnErrorAsync(eventSubscription, ex, times: 1); + await OnErrorAsync(eventSubscription, ex, times: 1); - sut.Dispose(); + sut.Dispose(); - A.CallTo(() => eventSubscriber.OnErrorAsync(eventSubscription, A<Exception>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => eventSubscriber.OnErrorAsync(eventSubscription, A<Exception>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_forward_event_from_inner_subscription() - { - var @event = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + [Fact] + public async Task Should_forward_event_from_inner_subscription() + { + var @event = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - await OnNextAsync(eventSubscription, @event); + await OnNextAsync(eventSubscription, @event); - sut.Dispose(); + sut.Dispose(); - A.CallTo(() => eventSubscriber.OnNextAsync(sut, @event)) - .MustHaveHappened(); - } + A.CallTo(() => eventSubscriber.OnNextAsync(sut, @event)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_forward_event_if_message_is_from_another_subscription() - { - var @event = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + [Fact] + public async Task Should_not_forward_event_if_message_is_from_another_subscription() + { + var @event = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - await OnNextAsync(A.Fake<IEventSubscription>(), @event); + await OnNextAsync(A.Fake<IEventSubscription>(), @event); - sut.Dispose(); + sut.Dispose(); - A.CallTo(() => eventSubscriber.OnNextAsync(A<IEventSubscription>._, A<StoredEvent>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => eventSubscriber.OnNextAsync(A<IEventSubscription>._, A<StoredEvent>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_be_able_to_unsubscribe_within_exception_handler() - { - var ex = new InvalidOperationException(); + [Fact] + public async Task Should_be_able_to_unsubscribe_within_exception_handler() + { + var ex = new InvalidOperationException(); - A.CallTo(() => eventSubscriber.OnErrorAsync(A<IEventSubscription>._, A<Exception>._)) - .Invokes(() => sut.Dispose()); + A.CallTo(() => eventSubscriber.OnErrorAsync(A<IEventSubscription>._, A<Exception>._)) + .Invokes(() => sut.Dispose()); - await OnErrorAsync(eventSubscription, ex, times: 6); + await OnErrorAsync(eventSubscription, ex, times: 6); - Assert.False(sut.IsSubscribed); - } + Assert.False(sut.IsSubscribed); + } - [Fact] - public async Task Should_be_able_to_unsubscribe_within_event_handler() - { - var @event = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + [Fact] + public async Task Should_be_able_to_unsubscribe_within_event_handler() + { + var @event = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - A.CallTo(() => eventSubscriber.OnNextAsync(A<IEventSubscription>._, A<StoredEvent>._)) - .Invokes(() => sut.Dispose()); + A.CallTo(() => eventSubscriber.OnNextAsync(A<IEventSubscription>._, A<StoredEvent>._)) + .Invokes(() => sut.Dispose()); - await OnNextAsync(eventSubscription, @event); + await OnNextAsync(eventSubscription, @event); - Assert.False(sut.IsSubscribed); - } + Assert.False(sut.IsSubscribed); + } - private async ValueTask OnErrorAsync(IEventSubscription subscriber, Exception ex, int times) + private async ValueTask OnErrorAsync(IEventSubscription subscriber, Exception ex, int times) + { + for (var i = 0; i < times; i++) { - for (var i = 0; i < times; i++) - { - await sutSubscriber.OnErrorAsync(subscriber, ex); - } + await sutSubscriber.OnErrorAsync(subscriber, ex); } + } - private ValueTask OnNextAsync(IEventSubscription subscriber, StoredEvent ev) - { - return sutSubscriber.OnNextAsync(subscriber, ev); - } + private ValueTask OnNextAsync(IEventSubscription subscriber, StoredEvent ev) + { + return sutSubscriber.OnNextAsync(subscriber, ev); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs index 4fff4009fc..fc647a21d6 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/WrongEventVersionExceptionTests.cs @@ -8,20 +8,19 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Infrastructure.EventSourcing; + +public class WrongEventVersionExceptionTests { - public class WrongEventVersionExceptionTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var source = new WrongEventVersionException(100, 200); - var actual = source.SerializeAndDeserializeBinary(); + var source = new WrongEventVersionException(100, 200); + var actual = source.SerializeAndDeserializeBinary(); - Assert.Equal(actual.ExpectedVersion, source.ExpectedVersion); - Assert.Equal(actual.CurrentVersion, source.CurrentVersion); + Assert.Equal(actual.ExpectedVersion, source.ExpectedVersion); + Assert.Equal(actual.CurrentVersion, source.CurrentVersion); - Assert.Equal(actual.Message, source.Message); - } + Assert.Equal(actual.Message, source.Message); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs index b8b01ba9ce..7402ef9f45 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs @@ -7,51 +7,50 @@ using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class FileExtensionsTests { - public class FileExtensionsTests + [Theory] + [InlineData("test.mp4", "mp4")] + [InlineData("test.MP4", "mp4")] + [InlineData("test.txt", "txt")] + [InlineData("test.TXT", "txt")] + [InlineData("test.jpg", "jpg")] + [InlineData("test.jpeg", "jpg")] + public void Should_calculate_file_type(string fileName, string expected) + { + var actual = fileName.FileType(); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Should_blob_for_invalid_file_types(string fileName) { - [Theory] - [InlineData("test.mp4", "mp4")] - [InlineData("test.MP4", "mp4")] - [InlineData("test.txt", "txt")] - [InlineData("test.TXT", "txt")] - [InlineData("test.jpg", "jpg")] - [InlineData("test.jpeg", "jpg")] - public void Should_calculate_file_type(string fileName, string expected) - { - var actual = fileName.FileType(); - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] - public void Should_blob_for_invalid_file_types(string fileName) - { - var actual = fileName.FileType(); - - Assert.Equal("blob", actual); - } - - [Theory] - [InlineData(-1, "")] - [InlineData(-2, "")] - [InlineData(0, "0 bytes")] - [InlineData(50, "50 bytes")] - [InlineData(1024, "1 kB")] - [InlineData(870400, "850 kB")] - [InlineData(1572864, "1.5 MB")] - [InlineData(4294967296, "4 GB")] - [InlineData(3408486046105, "3.1 TB")] - [InlineData(3490289711212134, "3174.4 TB")] - public void Should_calculate_file_size(long bytes, string expected) - { - var actual = bytes.ToReadableSize(); - - Assert.Equal(expected, actual); - } + var actual = fileName.FileType(); + + Assert.Equal("blob", actual); + } + + [Theory] + [InlineData(-1, "")] + [InlineData(-2, "")] + [InlineData(0, "0 bytes")] + [InlineData(50, "50 bytes")] + [InlineData(1024, "1 kB")] + [InlineData(870400, "850 kB")] + [InlineData(1572864, "1.5 MB")] + [InlineData(4294967296, "4 GB")] + [InlineData(3408486046105, "3.1 TB")] + [InlineData(3490289711212134, "3174.4 TB")] + public void Should_calculate_file_size(long bytes, string expected) + { + var actual = bytes.ToReadableSize(); + + Assert.Equal(expected, actual); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs b/backend/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs index e82653857a..2e29b75fba 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/GravatarHelperTests.cs @@ -7,30 +7,29 @@ using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class GravatarHelperTests { - public class GravatarHelperTests + [Theory] + [InlineData("me@email.com ")] + [InlineData("me@email.com")] + [InlineData("ME@email.com")] + public void Should_generate_picture_url(string email) { - [Theory] - [InlineData("me@email.com ")] - [InlineData("me@email.com")] - [InlineData("ME@email.com")] - public void Should_generate_picture_url(string email) - { - var url = GravatarHelper.CreatePictureUrl(email); + var url = GravatarHelper.CreatePictureUrl(email); - Assert.Equal("https://www.gravatar.com/avatar/8f9dc04e6abdcc9fea53e81945c7294b", url); - } + Assert.Equal("https://www.gravatar.com/avatar/8f9dc04e6abdcc9fea53e81945c7294b", url); + } - [Theory] - [InlineData("me@email.com ")] - [InlineData("me@email.com")] - [InlineData("ME@email.com")] - public void Should_generate_profile_url(string email) - { - var url = GravatarHelper.CreateProfileUrl(email); + [Theory] + [InlineData("me@email.com ")] + [InlineData("me@email.com")] + [InlineData("ME@email.com")] + public void Should_generate_profile_url(string email) + { + var url = GravatarHelper.CreateProfileUrl(email); - Assert.Equal("https://www.gravatar.com/8f9dc04e6abdcc9fea53e81945c7294b", url); - } + Assert.Equal("https://www.gravatar.com/8f9dc04e6abdcc9fea53e81945c7294b", url); } } \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs b/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs index e026ee93d2..04cd0ad002 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/GuardTests.cs @@ -7,337 +7,336 @@ using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class GuardTests { - public class GuardTests - { - [Theory] - [InlineData("")] - [InlineData(" ")] - public void NotNullOrEmpty_should_throw_for_empy_strings(string invalidString) - { - Assert.Throws<ArgumentException>(() => Guard.NotNullOrEmpty(invalidString, "parameter")); - } - - [Fact] - public void NotNullOrEmpty_should_throw_for_null_string() - { - Assert.Throws<ArgumentNullException>(() => Guard.NotNullOrEmpty(null, "parameter")); - } - - [Fact] - public void NotNullOrEmpty_should_do_nothing_for_vaid_string() - { - Guard.NotNullOrEmpty("value", "parameter"); - } - - [Fact] - public void NotNull_should_throw_for_null_value() - { - Assert.Throws<ArgumentNullException>(() => Guard.NotNull(null, "parameter")); - } - - [Fact] - public void NotNull_should_do_nothing_for_valid_value() - { - Guard.NotNull("value", "parameter"); - } - - [Fact] - public void Enum_should_throw_for_invalid_enum() - { - Assert.Throws<ArgumentException>(() => Guard.Enum((DateTimeKind)13, "Parameter")); - } - - [Fact] - public void Enum_should_do_nothing_for_valid_enum() - { - Guard.Enum(DateTimeKind.Local, "Parameter"); - } - - [Fact] - public void NotEmpty_should_throw_for_empty_guid() - { - Assert.Throws<ArgumentException>(() => Guard.NotEmpty(Guid.Empty, "parameter")); - } - - [Fact] - public void NotEmpty_should_throw_for_empty_domainId() - { - Assert.Throws<ArgumentException>(() => Guard.NotEmpty((DomainId)default, "parameter")); - } - - [Fact] - public void NotEmpty_should_do_nothing_for_valid_guid() - { - Guard.NotEmpty(Guid.NewGuid(), "parameter"); - } - - [Fact] - public void NotEmpty_should_do_nothing_for_valid_id() - { - Guard.NotEmpty(DomainId.NewGuid(), "parameter"); - } - - [Fact] - public void HasType_should_throw_for_other_type() - { - Assert.Throws<ArgumentException>(() => Guard.HasType<int>("value", "parameter")); - } - - [Fact] - public void HasType_should_do_nothing_for_null_value() - { - Guard.HasType<int>(null, "parameter"); - } - - [Fact] - public void HasType_should_do_nothing_for_correct_type() - { - Guard.HasType<int>(123, "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_throw_for_other_type() - { - Assert.Throws<ArgumentException>(() => Guard.HasType("value", typeof(int), "parameter")); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_null_value() - { - Guard.HasType(null, typeof(int), "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_correct_type() - { - Guard.HasType(123, typeof(int), "parameter"); - } - - [Fact] - public void HasType_nongeneric_should_do_nothing_for_null_type() - { - Guard.HasType(123, null, "parameter"); - } - - [Fact] - public void NotDefault_should_throw_for_default_values() - { - Assert.Throws<ArgumentException>(() => Guard.NotDefault(Guid.Empty, "parameter")); - Assert.Throws<ArgumentException>(() => Guard.NotDefault(0, "parameter")); - Assert.Throws<ArgumentException>(() => Guard.NotDefault((string?)null, "parameter")); - Assert.Throws<ArgumentException>(() => Guard.NotDefault(false, "parameter")); - } - - [Fact] - public void NotDefault_should_do_nothing_for_non_default_value() - { - Guard.NotDefault(Guid.NewGuid(), "parameter"); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(" Not a Slug ")] - [InlineData(" not--a--slug ")] - [InlineData(" not-a-slug ")] - [InlineData("-not-a-slug-")] - [InlineData("not$-a-slug")] - [InlineData("not-a-Slug")] - public void ValidSlug_should_throw_for_invalid_slugs(string slug) - { - Assert.Throws<ArgumentException>(() => Guard.ValidSlug(slug, "parameter")); - } - - [Theory] - [InlineData("slug")] - [InlineData("slug23")] - [InlineData("other-slug")] - [InlineData("just-another-slug")] - public void ValidSlug_should_do_nothing_for_valid_slugs(string slug) - { - Guard.ValidSlug(slug, "parameter"); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(" Not a Property ")] - [InlineData(" not--a--property ")] - [InlineData(" not-a-property ")] - [InlineData("-not-a-property-")] - [InlineData("not$-a-property")] - public void ValidPropertyName_should_throw_for_invalid_slugs(string slug) - { - Assert.Throws<ArgumentException>(() => Guard.ValidPropertyName(slug, "property")); - } - - [Theory] - [InlineData("property")] - [InlineData("property23")] - [InlineData("other-property")] - [InlineData("other-Property")] - [InlineData("otherProperty")] - [InlineData("just-another-property")] - [InlineData("just-Another-Property")] - [InlineData("justAnotherProperty")] - public void ValidPropertyName_should_do_nothing_for_valid_slugs(string property) - { - Guard.ValidPropertyName(property, "parameter"); - } - - [Theory] - [InlineData(double.PositiveInfinity)] - [InlineData(double.NegativeInfinity)] - [InlineData(double.NaN)] - public void ValidNumber_should_throw_for_invalid_doubles(double value) - { - Assert.Throws<ArgumentException>(() => Guard.ValidNumber(value, "parameter")); - } - - [Theory] - [InlineData(0d)] - [InlineData(-1000d)] - [InlineData(1000d)] - public void ValidNumber_do_nothing_for_valid_double(double value) - { - Guard.ValidNumber(value, "parameter"); - } - - [Theory] - [InlineData(float.PositiveInfinity)] - [InlineData(float.NegativeInfinity)] - [InlineData(float.NaN)] - public void ValidNumber_should_throw_for_invalid_float(float value) - { - Assert.Throws<ArgumentException>(() => Guard.ValidNumber(value, "parameter")); - } - - [Theory] - [InlineData(0f)] - [InlineData(-1000f)] - [InlineData(1000f)] - public void ValidNumber_do_nothing_for_valid_float(float value) - { - Guard.ValidNumber(value, "parameter"); - } - - [Theory] - [InlineData(4)] - [InlineData(104)] - public void Between_should_throw_for_values_outside_of_range(int value) - { - Assert.Throws<ArgumentException>(() => Guard.Between(value, 10, 100, "parameter")); - } - - [Theory] - [InlineData(10)] - [InlineData(55)] - [InlineData(100)] - public void Between_should_do_nothing_for_values_in_range(int value) - { - Guard.Between(value, 10, 100, "parameter"); - } - - [Theory] - [InlineData(0)] - [InlineData(100)] - public void GreaterThan_should_throw_for_smaller_values(int value) - { - Assert.Throws<ArgumentException>(() => Guard.GreaterThan(value, 100, "parameter")); - } - - [Theory] - [InlineData(101)] - [InlineData(200)] - public void GreaterThan_should_do_nothing_for_greater_values(int value) - { - Guard.GreaterThan(value, 100, "parameter"); - } - - [Theory] - [InlineData(0)] - [InlineData(99)] - public void GreaterEquals_should_throw_for_smaller_values(int value) - { - Assert.Throws<ArgumentException>(() => Guard.GreaterEquals(value, 100, "parameter")); - } - - [Theory] - [InlineData(100)] - [InlineData(200)] - public void GreaterEquals_should_do_nothing_for_greater_values(int value) - { - Guard.GreaterEquals(value, 100, "parameter"); - } - - [Theory] - [InlineData(1000)] - [InlineData(100)] - public void LessThan_should_throw_for_greater_values(int value) - { - Assert.Throws<ArgumentException>(() => Guard.LessThan(value, 100, "parameter")); - } - - [Theory] - [InlineData(99)] - [InlineData(50)] - public void LessThan_should_do_nothing_for_smaller_values(int value) - { - Guard.LessThan(value, 100, "parameter"); - } - - [Theory] - [InlineData(1000)] - [InlineData(101)] - public void LessEquals_should_throw_for_greater_values(int value) - { - Assert.Throws<ArgumentException>(() => Guard.LessEquals(value, 100, "parameter")); - } - - [Theory] - [InlineData(100)] - [InlineData(50)] - public void LessEquals_should_do_nothing_for_smaller_values(int value) - { - Guard.LessEquals(value, 100, "parameter"); - } - - [Fact] - public void NotEmpty_should_throw_for_empty_collection() - { - Assert.Throws<ArgumentException>(() => Guard.NotEmpty(Array.Empty<int>(), "parameter")); - } - - [Fact] - public void NotEmpty_should_throw_for_null_collection() - { - Assert.Throws<ArgumentNullException>(() => Guard.NotEmpty((int[]?)null, "parameter")); - } - - [Fact] - public void NotEmpty_should_do_nothing_for_value_collection() - { - Guard.NotEmpty(new[] { 1, 2, 3 }, "parameter"); - } - - [Fact] - public void ValidFileName_should_throw_for_invalid_file_name() - { - Assert.Throws<ArgumentException>(() => Guard.ValidFileName("File/Name", "Parameter")); - } - - [Fact] - public void ValidFileName_should_throw_for_null_file_name() - { - Assert.Throws<ArgumentNullException>(() => Guard.ValidFileName(null, "Parameter")); - } - - [Fact] - public void ValidFileName_should_do_nothing_for_valid_file_name() - { - Guard.ValidFileName("FileName", "Parameter"); - } + [Theory] + [InlineData("")] + [InlineData(" ")] + public void NotNullOrEmpty_should_throw_for_empy_strings(string invalidString) + { + Assert.Throws<ArgumentException>(() => Guard.NotNullOrEmpty(invalidString, "parameter")); + } + + [Fact] + public void NotNullOrEmpty_should_throw_for_null_string() + { + Assert.Throws<ArgumentNullException>(() => Guard.NotNullOrEmpty(null, "parameter")); + } + + [Fact] + public void NotNullOrEmpty_should_do_nothing_for_vaid_string() + { + Guard.NotNullOrEmpty("value", "parameter"); + } + + [Fact] + public void NotNull_should_throw_for_null_value() + { + Assert.Throws<ArgumentNullException>(() => Guard.NotNull(null, "parameter")); + } + + [Fact] + public void NotNull_should_do_nothing_for_valid_value() + { + Guard.NotNull("value", "parameter"); + } + + [Fact] + public void Enum_should_throw_for_invalid_enum() + { + Assert.Throws<ArgumentException>(() => Guard.Enum((DateTimeKind)13, "Parameter")); + } + + [Fact] + public void Enum_should_do_nothing_for_valid_enum() + { + Guard.Enum(DateTimeKind.Local, "Parameter"); + } + + [Fact] + public void NotEmpty_should_throw_for_empty_guid() + { + Assert.Throws<ArgumentException>(() => Guard.NotEmpty(Guid.Empty, "parameter")); + } + + [Fact] + public void NotEmpty_should_throw_for_empty_domainId() + { + Assert.Throws<ArgumentException>(() => Guard.NotEmpty((DomainId)default, "parameter")); + } + + [Fact] + public void NotEmpty_should_do_nothing_for_valid_guid() + { + Guard.NotEmpty(Guid.NewGuid(), "parameter"); + } + + [Fact] + public void NotEmpty_should_do_nothing_for_valid_id() + { + Guard.NotEmpty(DomainId.NewGuid(), "parameter"); + } + + [Fact] + public void HasType_should_throw_for_other_type() + { + Assert.Throws<ArgumentException>(() => Guard.HasType<int>("value", "parameter")); + } + + [Fact] + public void HasType_should_do_nothing_for_null_value() + { + Guard.HasType<int>(null, "parameter"); + } + + [Fact] + public void HasType_should_do_nothing_for_correct_type() + { + Guard.HasType<int>(123, "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_throw_for_other_type() + { + Assert.Throws<ArgumentException>(() => Guard.HasType("value", typeof(int), "parameter")); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_null_value() + { + Guard.HasType(null, typeof(int), "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_correct_type() + { + Guard.HasType(123, typeof(int), "parameter"); + } + + [Fact] + public void HasType_nongeneric_should_do_nothing_for_null_type() + { + Guard.HasType(123, null, "parameter"); + } + + [Fact] + public void NotDefault_should_throw_for_default_values() + { + Assert.Throws<ArgumentException>(() => Guard.NotDefault(Guid.Empty, "parameter")); + Assert.Throws<ArgumentException>(() => Guard.NotDefault(0, "parameter")); + Assert.Throws<ArgumentException>(() => Guard.NotDefault((string?)null, "parameter")); + Assert.Throws<ArgumentException>(() => Guard.NotDefault(false, "parameter")); + } + + [Fact] + public void NotDefault_should_do_nothing_for_non_default_value() + { + Guard.NotDefault(Guid.NewGuid(), "parameter"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" Not a Slug ")] + [InlineData(" not--a--slug ")] + [InlineData(" not-a-slug ")] + [InlineData("-not-a-slug-")] + [InlineData("not$-a-slug")] + [InlineData("not-a-Slug")] + public void ValidSlug_should_throw_for_invalid_slugs(string slug) + { + Assert.Throws<ArgumentException>(() => Guard.ValidSlug(slug, "parameter")); + } + + [Theory] + [InlineData("slug")] + [InlineData("slug23")] + [InlineData("other-slug")] + [InlineData("just-another-slug")] + public void ValidSlug_should_do_nothing_for_valid_slugs(string slug) + { + Guard.ValidSlug(slug, "parameter"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" Not a Property ")] + [InlineData(" not--a--property ")] + [InlineData(" not-a-property ")] + [InlineData("-not-a-property-")] + [InlineData("not$-a-property")] + public void ValidPropertyName_should_throw_for_invalid_slugs(string slug) + { + Assert.Throws<ArgumentException>(() => Guard.ValidPropertyName(slug, "property")); + } + + [Theory] + [InlineData("property")] + [InlineData("property23")] + [InlineData("other-property")] + [InlineData("other-Property")] + [InlineData("otherProperty")] + [InlineData("just-another-property")] + [InlineData("just-Another-Property")] + [InlineData("justAnotherProperty")] + public void ValidPropertyName_should_do_nothing_for_valid_slugs(string property) + { + Guard.ValidPropertyName(property, "parameter"); + } + + [Theory] + [InlineData(double.PositiveInfinity)] + [InlineData(double.NegativeInfinity)] + [InlineData(double.NaN)] + public void ValidNumber_should_throw_for_invalid_doubles(double value) + { + Assert.Throws<ArgumentException>(() => Guard.ValidNumber(value, "parameter")); + } + + [Theory] + [InlineData(0d)] + [InlineData(-1000d)] + [InlineData(1000d)] + public void ValidNumber_do_nothing_for_valid_double(double value) + { + Guard.ValidNumber(value, "parameter"); + } + + [Theory] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + [InlineData(float.NaN)] + public void ValidNumber_should_throw_for_invalid_float(float value) + { + Assert.Throws<ArgumentException>(() => Guard.ValidNumber(value, "parameter")); + } + + [Theory] + [InlineData(0f)] + [InlineData(-1000f)] + [InlineData(1000f)] + public void ValidNumber_do_nothing_for_valid_float(float value) + { + Guard.ValidNumber(value, "parameter"); + } + + [Theory] + [InlineData(4)] + [InlineData(104)] + public void Between_should_throw_for_values_outside_of_range(int value) + { + Assert.Throws<ArgumentException>(() => Guard.Between(value, 10, 100, "parameter")); + } + + [Theory] + [InlineData(10)] + [InlineData(55)] + [InlineData(100)] + public void Between_should_do_nothing_for_values_in_range(int value) + { + Guard.Between(value, 10, 100, "parameter"); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + public void GreaterThan_should_throw_for_smaller_values(int value) + { + Assert.Throws<ArgumentException>(() => Guard.GreaterThan(value, 100, "parameter")); + } + + [Theory] + [InlineData(101)] + [InlineData(200)] + public void GreaterThan_should_do_nothing_for_greater_values(int value) + { + Guard.GreaterThan(value, 100, "parameter"); + } + + [Theory] + [InlineData(0)] + [InlineData(99)] + public void GreaterEquals_should_throw_for_smaller_values(int value) + { + Assert.Throws<ArgumentException>(() => Guard.GreaterEquals(value, 100, "parameter")); + } + + [Theory] + [InlineData(100)] + [InlineData(200)] + public void GreaterEquals_should_do_nothing_for_greater_values(int value) + { + Guard.GreaterEquals(value, 100, "parameter"); + } + + [Theory] + [InlineData(1000)] + [InlineData(100)] + public void LessThan_should_throw_for_greater_values(int value) + { + Assert.Throws<ArgumentException>(() => Guard.LessThan(value, 100, "parameter")); + } + + [Theory] + [InlineData(99)] + [InlineData(50)] + public void LessThan_should_do_nothing_for_smaller_values(int value) + { + Guard.LessThan(value, 100, "parameter"); + } + + [Theory] + [InlineData(1000)] + [InlineData(101)] + public void LessEquals_should_throw_for_greater_values(int value) + { + Assert.Throws<ArgumentException>(() => Guard.LessEquals(value, 100, "parameter")); + } + + [Theory] + [InlineData(100)] + [InlineData(50)] + public void LessEquals_should_do_nothing_for_smaller_values(int value) + { + Guard.LessEquals(value, 100, "parameter"); + } + + [Fact] + public void NotEmpty_should_throw_for_empty_collection() + { + Assert.Throws<ArgumentException>(() => Guard.NotEmpty(Array.Empty<int>(), "parameter")); + } + + [Fact] + public void NotEmpty_should_throw_for_null_collection() + { + Assert.Throws<ArgumentNullException>(() => Guard.NotEmpty((int[]?)null, "parameter")); + } + + [Fact] + public void NotEmpty_should_do_nothing_for_value_collection() + { + Guard.NotEmpty(new[] { 1, 2, 3 }, "parameter"); + } + + [Fact] + public void ValidFileName_should_throw_for_invalid_file_name() + { + Assert.Throws<ArgumentException>(() => Guard.ValidFileName("File/Name", "Parameter")); + } + + [Fact] + public void ValidFileName_should_throw_for_null_file_name() + { + Assert.Throws<ArgumentNullException>(() => Guard.ValidFileName(null, "Parameter")); + } + + [Fact] + public void ValidFileName_should_do_nothing_for_valid_file_name() + { + Guard.ValidFileName("FileName", "Parameter"); } } \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs index a915f525c1..f5e835fa05 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Http/DumpFormatterTests.cs @@ -12,118 +12,117 @@ #pragma warning disable SA1122 // Use string.Empty for empty strings -namespace Squidex.Infrastructure.Http +namespace Squidex.Infrastructure.Http; + +public class DumpFormatterTests { - public class DumpFormatterTests + [Fact] + public void Should_format_dump_without_response() + { + var httpRequest = CreateRequest(); + + var dump = DumpFormatter.BuildDump(httpRequest, null, null, null, TimeSpan.FromMinutes(1), true); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "", + "", + "Response:", + "Timeout after 00:01:00"); + + Assert.Equal(expected, dump); + } + + [Fact] + public void Should_format_dump_without_content() + { + var httpRequest = CreateRequest(); + var httpResponse = CreateResponse(); + + var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, null, null, TimeSpan.FromMinutes(1), false); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "", + "", + "Response:", + "HTTP/1.1 200 OK", + "Transfer-Encoding: UTF-8", + "Trailer: Expires", + "", + "Elapsed: 00:01:00"); + + Assert.Equal(expected, dump); + } + + [Fact] + public void Should_format_dump_with_content_without_timeout() + { + var httpRequest = CreateRequest(new StringContent("Hello Squidex", Encoding.UTF8, "text/plain")); + var httpResponse = CreateResponse(new StringContent("Hello Back", Encoding.UTF8, "text/plain")); + + var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, "Hello Squidex", "Hello Back", TimeSpan.FromMinutes(1), false); + + var expected = CreateExpectedDump( + "Request:", + "POST: https://cloud.squidex.io/ HTTP/1.1", + "User-Agent: Squidex/1.0", + "Accept-Language: de; en", + "Accept-Encoding: UTF-8", + "Content-Type: text/plain; charset=utf-8", + "", + "Hello Squidex", + "", + "", + "Response:", + "HTTP/1.1 200 OK", + "Transfer-Encoding: UTF-8", + "Trailer: Expires", + "Content-Type: text/plain; charset=utf-8", + "", + "Hello Back", + "", + "Elapsed: 00:01:00"); + + Assert.Equal(expected, dump); + } + + private static HttpRequestMessage CreateRequest(HttpContent? content = null) + { + var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://cloud.squidex.io")); + + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("Squidex", "1.0")); + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("de")); + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en")); + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("UTF-8")); + + request.Content = content; + + return request; + } + + private static HttpResponseMessage CreateResponse(HttpContent? content = null) + { + var response = new HttpResponseMessage(HttpStatusCode.OK); + + response.Headers.TransferEncoding.Add(new TransferCodingHeaderValue("UTF-8")); + response.Headers.Trailer.Add("Expires"); + + response.Content = content; + + return response; + } + + private static string CreateExpectedDump(params string[] input) { - [Fact] - public void Should_format_dump_without_response() - { - var httpRequest = CreateRequest(); - - var dump = DumpFormatter.BuildDump(httpRequest, null, null, null, TimeSpan.FromMinutes(1), true); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/1.1", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "", - "", - "Response:", - "Timeout after 00:01:00"); - - Assert.Equal(expected, dump); - } - - [Fact] - public void Should_format_dump_without_content() - { - var httpRequest = CreateRequest(); - var httpResponse = CreateResponse(); - - var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, null, null, TimeSpan.FromMinutes(1), false); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/1.1", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "", - "", - "Response:", - "HTTP/1.1 200 OK", - "Transfer-Encoding: UTF-8", - "Trailer: Expires", - "", - "Elapsed: 00:01:00"); - - Assert.Equal(expected, dump); - } - - [Fact] - public void Should_format_dump_with_content_without_timeout() - { - var httpRequest = CreateRequest(new StringContent("Hello Squidex", Encoding.UTF8, "text/plain")); - var httpResponse = CreateResponse(new StringContent("Hello Back", Encoding.UTF8, "text/plain")); - - var dump = DumpFormatter.BuildDump(httpRequest, httpResponse, "Hello Squidex", "Hello Back", TimeSpan.FromMinutes(1), false); - - var expected = CreateExpectedDump( - "Request:", - "POST: https://cloud.squidex.io/ HTTP/1.1", - "User-Agent: Squidex/1.0", - "Accept-Language: de; en", - "Accept-Encoding: UTF-8", - "Content-Type: text/plain; charset=utf-8", - "", - "Hello Squidex", - "", - "", - "Response:", - "HTTP/1.1 200 OK", - "Transfer-Encoding: UTF-8", - "Trailer: Expires", - "Content-Type: text/plain; charset=utf-8", - "", - "Hello Back", - "", - "Elapsed: 00:01:00"); - - Assert.Equal(expected, dump); - } - - private static HttpRequestMessage CreateRequest(HttpContent? content = null) - { - var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://cloud.squidex.io")); - - request.Headers.UserAgent.Add(new ProductInfoHeaderValue("Squidex", "1.0")); - request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("de")); - request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en")); - request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("UTF-8")); - - request.Content = content; - - return request; - } - - private static HttpResponseMessage CreateResponse(HttpContent? content = null) - { - var response = new HttpResponseMessage(HttpStatusCode.OK); - - response.Headers.TransferEncoding.Add(new TransferCodingHeaderValue("UTF-8")); - response.Headers.Trailer.Add("Expires"); - - response.Content = content; - - return response; - } - - private static string CreateExpectedDump(params string[] input) - { - return string.Join(Environment.NewLine, input) + Environment.NewLine; - } + return string.Join(Environment.NewLine, input) + Environment.NewLine; } } \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs b/backend/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs index eb30be4918..40aca201fa 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs @@ -8,16 +8,15 @@ using NodaTime; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class InstantExtensions { - public class InstantExtensions + [Fact] + public void Should_remove_ms_from_instant() { - [Fact] - public void Should_remove_ms_from_instant() - { - var source = Instant.FromUnixTimeMilliseconds((30 * 1000) + 100); + var source = Instant.FromUnixTimeMilliseconds((30 * 1000) + 100); - Assert.Equal(Instant.FromUnixTimeSeconds(30), source.WithoutMs()); - } + Assert.Equal(Instant.FromUnixTimeSeconds(30), source.WithoutMs()); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs index 79fdeee58f..fd076e8141 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs @@ -9,46 +9,45 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Json +namespace Squidex.Infrastructure.Json; + +public class ClaimsPrincipalConverterTests { - public class ClaimsPrincipalConverterTests + [Fact] + public void Should_serialize_and_deserialize() + { + var value = new ClaimsPrincipal( + new[] + { + new ClaimsIdentity( + new[] + { + new Claim("email", "me@email.com"), + new Claim("username", "me@email.com") + }, + "Cookie"), + new ClaimsIdentity( + new[] + { + new Claim("user_id", "12345"), + new Claim("login", "me") + }, + "Google") + }); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value.Identities.ElementAt(0).AuthenticationType, serialized.Identities.ElementAt(0).AuthenticationType); + Assert.Equal(value.Identities.ElementAt(1).AuthenticationType, serialized.Identities.ElementAt(1).AuthenticationType); + } + + [Fact] + public void Should_serialize_and_deserialize_null_principal() { - [Fact] - public void Should_serialize_and_deserialize() - { - var value = new ClaimsPrincipal( - new[] - { - new ClaimsIdentity( - new[] - { - new Claim("email", "me@email.com"), - new Claim("username", "me@email.com") - }, - "Cookie"), - new ClaimsIdentity( - new[] - { - new Claim("user_id", "12345"), - new Claim("login", "me") - }, - "Google") - }); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value.Identities.ElementAt(0).AuthenticationType, serialized.Identities.ElementAt(0).AuthenticationType); - Assert.Equal(value.Identities.ElementAt(1).AuthenticationType, serialized.Identities.ElementAt(1).AuthenticationType); - } - - [Fact] - public void Should_serialize_and_deserialize_null_principal() - { - ClaimsPrincipal? value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Null(serialized); - } + ClaimsPrincipal? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Null(serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs index 8c16b96c78..e0ef57c73d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs @@ -9,38 +9,37 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Json +namespace Squidex.Infrastructure.Json; + +public class InstantConverterTests { - public class InstantConverterTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); + var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_nullable_with_value() - { - Instant? value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); + [Fact] + public void Should_serialize_and_deserialize_nullable_with_value() + { + Instant? value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_nullable_with_null() - { - Instant? value = null; + [Fact] + public void Should_serialize_and_deserialize_nullable_with_null() + { + Instant? value = null; - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs index 76eb29c0d6..53b86e56d9 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonObjectTests.cs @@ -10,537 +10,536 @@ #pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions -namespace Squidex.Infrastructure.Json.Objects +namespace Squidex.Infrastructure.Json.Objects; + +public class JsonObjectTests { - public class JsonObjectTests + [Fact] + public void Should_make_correct_object_equal_comparisons() { - [Fact] - public void Should_make_correct_object_equal_comparisons() - { - var obj1a = new JsonObject().Add("key1", 1); - var obj1b = new JsonObject().Add("key1", 1); + var obj1a = new JsonObject().Add("key1", 1); + var obj1b = new JsonObject().Add("key1", 1); - var objOtherValue = new JsonObject().Add("key1", 2); - var objOtherKey = new JsonObject().Add("key2", 1); - var objOtherSize = new JsonObject().Add("key1", 1).Add("key2", 2); + var objOtherValue = new JsonObject().Add("key1", 2); + var objOtherKey = new JsonObject().Add("key2", 1); + var objOtherSize = new JsonObject().Add("key1", 1).Add("key2", 2); - var number = JsonValue.Create(1); + var number = JsonValue.Create(1); - Assert.Equal(obj1a, obj1b); - Assert.Equal(obj1a.GetHashCode(), obj1b.GetHashCode()); - Assert.True(obj1a.Equals((object)obj1b)); + Assert.Equal(obj1a, obj1b); + Assert.Equal(obj1a.GetHashCode(), obj1b.GetHashCode()); + Assert.True(obj1a.Equals((object)obj1b)); - Assert.NotEqual(obj1a, objOtherValue); - Assert.NotEqual(obj1a.GetHashCode(), objOtherValue.GetHashCode()); - Assert.False(obj1a.Equals((object)objOtherValue)); + Assert.NotEqual(obj1a, objOtherValue); + Assert.NotEqual(obj1a.GetHashCode(), objOtherValue.GetHashCode()); + Assert.False(obj1a.Equals((object)objOtherValue)); - Assert.NotEqual(obj1a, objOtherKey); - Assert.NotEqual(obj1a.GetHashCode(), objOtherKey.GetHashCode()); - Assert.False(obj1a.Equals((object)objOtherKey)); + Assert.NotEqual(obj1a, objOtherKey); + Assert.NotEqual(obj1a.GetHashCode(), objOtherKey.GetHashCode()); + Assert.False(obj1a.Equals((object)objOtherKey)); - Assert.NotEqual(obj1a, objOtherSize); - Assert.NotEqual(obj1a.GetHashCode(), objOtherSize.GetHashCode()); - Assert.False(obj1a.Equals((object)objOtherSize)); + Assert.NotEqual(obj1a, objOtherSize); + Assert.NotEqual(obj1a.GetHashCode(), objOtherSize.GetHashCode()); + Assert.False(obj1a.Equals((object)objOtherSize)); - Assert.NotEqual(obj1a, number); - Assert.NotEqual(obj1a.GetHashCode(), number.GetHashCode()); - Assert.False(obj1a.Equals((object)number)); - } + Assert.NotEqual(obj1a, number); + Assert.NotEqual(obj1a.GetHashCode(), number.GetHashCode()); + Assert.False(obj1a.Equals((object)number)); + } - [Fact] - public void Should_make_correct_array_equal_comparisons() - { - var array1a = JsonValue.Array(1); - var array1b = JsonValue.Array(1); + [Fact] + public void Should_make_correct_array_equal_comparisons() + { + var array1a = JsonValue.Array(1); + var array1b = JsonValue.Array(1); - var arrayOtherValue = JsonValue.Array(2); - var arrayOtherSize = JsonValue.Array(1, 2); + var arrayOtherValue = JsonValue.Array(2); + var arrayOtherSize = JsonValue.Array(1, 2); - var number = JsonValue.Create(1); + var number = JsonValue.Create(1); - Assert.Equal(array1a, array1b); - Assert.Equal(array1a.GetHashCode(), array1b.GetHashCode()); - Assert.True(array1a.Equals((object)array1b)); + Assert.Equal(array1a, array1b); + Assert.Equal(array1a.GetHashCode(), array1b.GetHashCode()); + Assert.True(array1a.Equals((object)array1b)); - Assert.NotEqual(array1a, arrayOtherValue); - Assert.NotEqual(array1a.GetHashCode(), arrayOtherValue.GetHashCode()); - Assert.False(array1a.Equals((object)arrayOtherValue)); + Assert.NotEqual(array1a, arrayOtherValue); + Assert.NotEqual(array1a.GetHashCode(), arrayOtherValue.GetHashCode()); + Assert.False(array1a.Equals((object)arrayOtherValue)); - Assert.NotEqual(array1a, arrayOtherSize); - Assert.NotEqual(array1a.GetHashCode(), arrayOtherSize.GetHashCode()); - Assert.False(array1a.Equals((object)arrayOtherSize)); + Assert.NotEqual(array1a, arrayOtherSize); + Assert.NotEqual(array1a.GetHashCode(), arrayOtherSize.GetHashCode()); + Assert.False(array1a.Equals((object)arrayOtherSize)); - Assert.NotEqual(array1a, number); - Assert.NotEqual(array1a.GetHashCode(), number.GetHashCode()); - Assert.False(array1a.Equals((object)number)); - } + Assert.NotEqual(array1a, number); + Assert.NotEqual(array1a.GetHashCode(), number.GetHashCode()); + Assert.False(array1a.Equals((object)number)); + } - [Fact] - public void Should_make_correct_scalar_comparisons() - { - var number1a = JsonValue.Create(1); - var number1b = JsonValue.Create(1); + [Fact] + public void Should_make_correct_scalar_comparisons() + { + var number1a = JsonValue.Create(1); + var number1b = JsonValue.Create(1); - var number2 = JsonValue.Create(2); + var number2 = JsonValue.Create(2); - var boolean = JsonValue.True; + var boolean = JsonValue.True; - Assert.Equal(number1a, number1b); - Assert.Equal(number1a.GetHashCode(), number1b.GetHashCode()); - Assert.True(number1a.Equals((object)number1b)); + Assert.Equal(number1a, number1b); + Assert.Equal(number1a.GetHashCode(), number1b.GetHashCode()); + Assert.True(number1a.Equals((object)number1b)); - Assert.NotEqual(number1a, number2); - Assert.NotEqual(number1a.GetHashCode(), number2.GetHashCode()); - Assert.False(number1a.Equals((object)number2)); + Assert.NotEqual(number1a, number2); + Assert.NotEqual(number1a.GetHashCode(), number2.GetHashCode()); + Assert.False(number1a.Equals((object)number2)); - Assert.NotEqual(number1a, boolean); - Assert.NotEqual(number1a.GetHashCode(), boolean.GetHashCode()); - Assert.False(number1a.Equals((object)boolean)); - } + Assert.NotEqual(number1a, boolean); + Assert.NotEqual(number1a.GetHashCode(), boolean.GetHashCode()); + Assert.False(number1a.Equals((object)boolean)); + } - [Fact] - public void Should_make_correct_null_comparisons() - { - var null1 = JsonValue.Null; - var null2 = JsonValue.Null; + [Fact] + public void Should_make_correct_null_comparisons() + { + var null1 = JsonValue.Null; + var null2 = JsonValue.Null; - var boolean = JsonValue.True; + var boolean = JsonValue.True; - Assert.Equal(null1, null2); - Assert.Equal(null1.GetHashCode(), null2.GetHashCode()); - Assert.True(null1.Equals((object)null2)); + Assert.Equal(null1, null2); + Assert.Equal(null1.GetHashCode(), null2.GetHashCode()); + Assert.True(null1.Equals((object)null2)); - Assert.NotEqual(null1, boolean); - Assert.NotEqual(null1.GetHashCode(), boolean.GetHashCode()); - Assert.False(null1.Equals((object)boolean)); - } + Assert.NotEqual(null1, boolean); + Assert.NotEqual(null1.GetHashCode(), boolean.GetHashCode()); + Assert.False(null1.Equals((object)boolean)); + } - [Fact] - public void Should_create_null() + [Fact] + public void Should_create_null() + { + var jsons = new[] { - var jsons = new[] - { - new JsonValue((string?)null), - JsonValue.Create((string?)null), - JsonValue.Create((object?)null), - default - }; - - foreach (var json in jsons) - { - Assert.Null(json.Value); - Assert.Equal(JsonValueType.Null, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsBoolean); - Assert.Throws<InvalidOperationException>(() => json.AsNumber); - Assert.Throws<InvalidOperationException>(() => json.AsString); - Assert.Throws<InvalidOperationException>(() => json.AsArray); - Assert.Throws<InvalidOperationException>(() => json.AsObject); - } - } + new JsonValue((string?)null), + JsonValue.Create((string?)null), + JsonValue.Create((object?)null), + default + }; - [Fact] - public void Should_create_booleans() + foreach (var json in jsons) { - var jsons = new[] - { - new JsonValue(true), - JsonValue.Create(true), - JsonValue.Create((object?)true), - true - }; - - foreach (var json in jsons) - { - Assert.Equal(true, json.Value); - Assert.Equal(true, json.AsBoolean); - Assert.Equal(JsonValueType.Boolean, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsNumber); - Assert.Throws<InvalidOperationException>(() => json.AsString); - Assert.Throws<InvalidOperationException>(() => json.AsArray); - Assert.Throws<InvalidOperationException>(() => json.AsObject); - } + Assert.Null(json.Value); + Assert.Equal(JsonValueType.Null, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsBoolean); + Assert.Throws<InvalidOperationException>(() => json.AsNumber); + Assert.Throws<InvalidOperationException>(() => json.AsString); + Assert.Throws<InvalidOperationException>(() => json.AsArray); + Assert.Throws<InvalidOperationException>(() => json.AsObject); } + } - [Fact] - public void Should_create_floats() + [Fact] + public void Should_create_booleans() + { + var jsons = new[] { - var jsons = new[] - { - new JsonValue(12.5), - JsonValue.Create(12.5), - JsonValue.Create((object?)12.5), - JsonValue.Create((object?)12.5f), - 12.5 - }; - - foreach (var json in jsons) - { - Assert.Equal(12.5, json.Value); - Assert.Equal(12.5, json.AsNumber); - Assert.Equal(JsonValueType.Number, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsBoolean); - Assert.Throws<InvalidOperationException>(() => json.AsString); - Assert.Throws<InvalidOperationException>(() => json.AsArray); - Assert.Throws<InvalidOperationException>(() => json.AsObject); - } - } + new JsonValue(true), + JsonValue.Create(true), + JsonValue.Create((object?)true), + true + }; - [Fact] - public void Should_create_more_integers() + foreach (var json in jsons) { - var jsons = new[] - { - new JsonValue(12), - JsonValue.Create(12), - JsonValue.Create((object?)12L), - JsonValue.Create((object?)12), - 12 - }; - - foreach (var json in jsons) - { - Assert.Equal(12d, json.Value); - Assert.Equal(12d, json.AsNumber); - Assert.Equal(JsonValueType.Number, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsBoolean); - Assert.Throws<InvalidOperationException>(() => json.AsString); - Assert.Throws<InvalidOperationException>(() => json.AsArray); - Assert.Throws<InvalidOperationException>(() => json.AsObject); - } + Assert.Equal(true, json.Value); + Assert.Equal(true, json.AsBoolean); + Assert.Equal(JsonValueType.Boolean, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsNumber); + Assert.Throws<InvalidOperationException>(() => json.AsString); + Assert.Throws<InvalidOperationException>(() => json.AsArray); + Assert.Throws<InvalidOperationException>(() => json.AsObject); } + } - [Fact] - public void Should_create_strings() + [Fact] + public void Should_create_floats() + { + var jsons = new[] { - var jsons = new[] - { - new JsonValue("text"), - JsonValue.Create("text"), - JsonValue.Create((object?)"text"), - "text" - }; - - foreach (var json in jsons) - { - Assert.Equal("text", json.Value); - Assert.Equal("text", json.AsString); - Assert.Equal(JsonValueType.String, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsBoolean); - Assert.Throws<InvalidOperationException>(() => json.AsNumber); - Assert.Throws<InvalidOperationException>(() => json.AsArray); - Assert.Throws<InvalidOperationException>(() => json.AsObject); - } + new JsonValue(12.5), + JsonValue.Create(12.5), + JsonValue.Create((object?)12.5), + JsonValue.Create((object?)12.5f), + 12.5 + }; + + foreach (var json in jsons) + { + Assert.Equal(12.5, json.Value); + Assert.Equal(12.5, json.AsNumber); + Assert.Equal(JsonValueType.Number, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsBoolean); + Assert.Throws<InvalidOperationException>(() => json.AsString); + Assert.Throws<InvalidOperationException>(() => json.AsArray); + Assert.Throws<InvalidOperationException>(() => json.AsObject); } + } - [Fact] - public void Should_create_instants() + [Fact] + public void Should_create_more_integers() + { + var jsons = new[] + { + new JsonValue(12), + JsonValue.Create(12), + JsonValue.Create((object?)12L), + JsonValue.Create((object?)12), + 12 + }; + + foreach (var json in jsons) { - var instant = Instant.FromUnixTimeSeconds(4123125455); - - var jsons = new[] - { - JsonValue.Create(instant), - JsonValue.Create((object?)instant), - instant - }; - - foreach (var json in jsons) - { - Assert.Equal(instant.ToString(), json.Value); - Assert.Equal(instant.ToString(), json.AsString); - Assert.Equal(JsonValueType.String, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsBoolean); - Assert.Throws<InvalidOperationException>(() => json.AsNumber); - Assert.Throws<InvalidOperationException>(() => json.AsArray); - Assert.Throws<InvalidOperationException>(() => json.AsObject); - } + Assert.Equal(12d, json.Value); + Assert.Equal(12d, json.AsNumber); + Assert.Equal(JsonValueType.Number, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsBoolean); + Assert.Throws<InvalidOperationException>(() => json.AsString); + Assert.Throws<InvalidOperationException>(() => json.AsArray); + Assert.Throws<InvalidOperationException>(() => json.AsObject); } + } - [Fact] - public void Should_create_ids() + [Fact] + public void Should_create_strings() + { + var jsons = new[] { - var id = DomainId.NewGuid(); - - var jsons = new[] - { - JsonValue.Create(id), - JsonValue.Create((object?)id), - id - }; - - var actual = id.ToString(); - - foreach (var json in jsons) - { - Assert.Equal(actual, json.Value); - Assert.Equal(actual, json.AsString); - Assert.Equal(JsonValueType.String, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsBoolean); - Assert.Throws<InvalidOperationException>(() => json.AsNumber); - Assert.Throws<InvalidOperationException>(() => json.AsArray); - Assert.Throws<InvalidOperationException>(() => json.AsObject); - } - } + new JsonValue("text"), + JsonValue.Create("text"), + JsonValue.Create((object?)"text"), + "text" + }; - [Fact] - public void Should_create_arrays() + foreach (var json in jsons) { - var input = new JsonArray { 1, 2 }; - - var jsons = new[] - { - new JsonValue(input), - JsonValue.Array(1, 2), - JsonValue.Array(new int[] { 1, 2 }), - JsonValue.Create(input), - JsonValue.Create((object?)input), - JsonValue.Create(new object[] { 1, 2 }), - input - }; - - var actual = new JsonArray { 1, 2 }; - - foreach (var json in jsons) - { - Assert.Equal(actual, json.Value); - Assert.Equal(actual, json.AsArray); - Assert.Equal(JsonValueType.Array, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsBoolean); - Assert.Throws<InvalidOperationException>(() => json.AsNumber); - Assert.Throws<InvalidOperationException>(() => json.AsString); - Assert.Throws<InvalidOperationException>(() => json.AsObject); - } + Assert.Equal("text", json.Value); + Assert.Equal("text", json.AsString); + Assert.Equal(JsonValueType.String, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsBoolean); + Assert.Throws<InvalidOperationException>(() => json.AsNumber); + Assert.Throws<InvalidOperationException>(() => json.AsArray); + Assert.Throws<InvalidOperationException>(() => json.AsObject); } + } + + [Fact] + public void Should_create_instants() + { + var instant = Instant.FromUnixTimeSeconds(4123125455); - [Fact] - public void Should_create_objects() + var jsons = new[] { - var input = new JsonObject().Add("1", 1).Add("2", 2); - - var jsons = new[] - { - new JsonValue(input), - JsonValue.Create(input), - JsonValue.Create((object?)input), - JsonValue.Create(input.ToDictionary(x => x.Key, x => x.Value.Value)), - input - }; - - var actual = new JsonObject().Add("1", 1).Add("2", 2); - - foreach (var json in jsons) - { - Assert.Equal(actual, json.Value); - Assert.Equal(actual, json.AsObject); - Assert.Equal(JsonValueType.Object, json.Type); - - Assert.Throws<InvalidOperationException>(() => json.AsBoolean); - Assert.Throws<InvalidOperationException>(() => json.AsNumber); - Assert.Throws<InvalidOperationException>(() => json.AsString); - Assert.Throws<InvalidOperationException>(() => json.AsArray); - } - } + JsonValue.Create(instant), + JsonValue.Create((object?)instant), + instant + }; - [Fact] - public void Should_clone_number_and_return_same() + foreach (var json in jsons) { - var source = JsonValue.Create(1); + Assert.Equal(instant.ToString(), json.Value); + Assert.Equal(instant.ToString(), json.AsString); + Assert.Equal(JsonValueType.String, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsBoolean); + Assert.Throws<InvalidOperationException>(() => json.AsNumber); + Assert.Throws<InvalidOperationException>(() => json.AsArray); + Assert.Throws<InvalidOperationException>(() => json.AsObject); + } + } - var clone = source.Clone(); + [Fact] + public void Should_create_ids() + { + var id = DomainId.NewGuid(); - Assert.Same(source.Value, clone.Value); - } + var jsons = new[] + { + JsonValue.Create(id), + JsonValue.Create((object?)id), + id + }; + + var actual = id.ToString(); - [Fact] - public void Should_clone_string_and_return_same() + foreach (var json in jsons) { - var source = JsonValue.Create("test"); + Assert.Equal(actual, json.Value); + Assert.Equal(actual, json.AsString); + Assert.Equal(JsonValueType.String, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsBoolean); + Assert.Throws<InvalidOperationException>(() => json.AsNumber); + Assert.Throws<InvalidOperationException>(() => json.AsArray); + Assert.Throws<InvalidOperationException>(() => json.AsObject); + } + } - var clone = source.Clone(); + [Fact] + public void Should_create_arrays() + { + var input = new JsonArray { 1, 2 }; - Assert.Same(source.Value, clone.Value); + var jsons = new[] + { + new JsonValue(input), + JsonValue.Array(1, 2), + JsonValue.Array(new int[] { 1, 2 }), + JsonValue.Create(input), + JsonValue.Create((object?)input), + JsonValue.Create(new object[] { 1, 2 }), + input + }; + + var actual = new JsonArray { 1, 2 }; + + foreach (var json in jsons) + { + Assert.Equal(actual, json.Value); + Assert.Equal(actual, json.AsArray); + Assert.Equal(JsonValueType.Array, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsBoolean); + Assert.Throws<InvalidOperationException>(() => json.AsNumber); + Assert.Throws<InvalidOperationException>(() => json.AsString); + Assert.Throws<InvalidOperationException>(() => json.AsObject); } + } + + [Fact] + public void Should_create_objects() + { + var input = new JsonObject().Add("1", 1).Add("2", 2); - [Fact] - public void Should_clone_boolean_and_return_same() + var jsons = new[] { - var source = JsonValue.Create(true); + new JsonValue(input), + JsonValue.Create(input), + JsonValue.Create((object?)input), + JsonValue.Create(input.ToDictionary(x => x.Key, x => x.Value.Value)), + input + }; - var clone = source.Clone(); + var actual = new JsonObject().Add("1", 1).Add("2", 2); - Assert.Same(source.Value, clone.Value); + foreach (var json in jsons) + { + Assert.Equal(actual, json.Value); + Assert.Equal(actual, json.AsObject); + Assert.Equal(JsonValueType.Object, json.Type); + + Assert.Throws<InvalidOperationException>(() => json.AsBoolean); + Assert.Throws<InvalidOperationException>(() => json.AsNumber); + Assert.Throws<InvalidOperationException>(() => json.AsString); + Assert.Throws<InvalidOperationException>(() => json.AsArray); } + } - [Fact] - public void Should_clone_null_and_return_same() - { - var source = JsonValue.Null; + [Fact] + public void Should_clone_number_and_return_same() + { + var source = JsonValue.Create(1); - var clone = source.Clone(); + var clone = source.Clone(); - Assert.Same(source.Value, clone.Value); - } + Assert.Same(source.Value, clone.Value); + } - [Fact] - public void Should_clone_array_and_also_children() - { - var source = JsonValue.Array(new JsonArray(), new JsonArray()).AsArray; + [Fact] + public void Should_clone_string_and_return_same() + { + var source = JsonValue.Create("test"); - var clone = ((JsonValue)source).Clone().AsArray; + var clone = source.Clone(); - Assert.NotSame(source, clone); + Assert.Same(source.Value, clone.Value); + } - for (var i = 0; i < source.Count; i++) - { - Assert.NotSame(clone[i].Value, source[i].Value); - } - } + [Fact] + public void Should_clone_boolean_and_return_same() + { + var source = JsonValue.Create(true); - [Fact] - public void Should_clone_object_and_also_children() - { - var source = new JsonObject().Add("1", new JsonArray()).Add("2", new JsonArray()); + var clone = source.Clone(); - var clone = ((JsonValue)source).Clone().AsObject; + Assert.Same(source.Value, clone.Value); + } - Assert.NotSame(source, clone); + [Fact] + public void Should_clone_null_and_return_same() + { + var source = JsonValue.Null; - foreach (var (key, value) in clone) - { - Assert.NotSame(value.Value, source[key].Value); - } - } + var clone = source.Clone(); + + Assert.Same(source.Value, clone.Value); + } + + [Fact] + public void Should_clone_array_and_also_children() + { + var source = JsonValue.Array(new JsonArray(), new JsonArray()).AsArray; - [Fact] - public void Should_throw_exception_if_creation_value_from_invalid_type() + var clone = ((JsonValue)source).Clone().AsArray; + + Assert.NotSame(source, clone); + + for (var i = 0; i < source.Count; i++) { - Assert.Throws<ArgumentException>(() => JsonValue.Create(default(TimeSpan))); + Assert.NotSame(clone[i].Value, source[i].Value); } + } - [Fact] - public void Should_return_null_if_getting_value_by_path_segment_from_null() - { - var json = JsonValue.Null; + [Fact] + public void Should_clone_object_and_also_children() + { + var source = new JsonObject().Add("1", new JsonArray()).Add("2", new JsonArray()); - var found = json.TryGetByPath("path", out var actual); + var clone = ((JsonValue)source).Clone().AsObject; - Assert.False(found); - Assert.Equal(default, actual); - } + Assert.NotSame(source, clone); - [Fact] - public void Should_return_null_if_getting_value_by_path_segment_from_string() + foreach (var (key, value) in clone) { - var json = JsonValue.Create("string"); + Assert.NotSame(value.Value, source[key].Value); + } + } - var found = json.TryGetByPath("path", out var actual); + [Fact] + public void Should_throw_exception_if_creation_value_from_invalid_type() + { + Assert.Throws<ArgumentException>(() => JsonValue.Create(default(TimeSpan))); + } - Assert.False(found); - Assert.Equal(default, actual); - } + [Fact] + public void Should_return_null_if_getting_value_by_path_segment_from_null() + { + var json = JsonValue.Null; - [Fact] - public void Should_return_null_if_getting_value_by_path_segment_from_boolean() - { - var json = JsonValue.True; + var found = json.TryGetByPath("path", out var actual); - var found = json.TryGetByPath("path", out var actual); + Assert.False(found); + Assert.Equal(default, actual); + } - Assert.False(found); - Assert.Equal(default, actual); - } + [Fact] + public void Should_return_null_if_getting_value_by_path_segment_from_string() + { + var json = JsonValue.Create("string"); - [Fact] - public void Should_return_null_if_getting_value_by_path_segment_from_number() - { - var json = JsonValue.Create(12); + var found = json.TryGetByPath("path", out var actual); - var found = json.TryGetByPath("path", out var actual); + Assert.False(found); + Assert.Equal(default, actual); + } - Assert.False(found); - Assert.Equal(default, actual); - } + [Fact] + public void Should_return_null_if_getting_value_by_path_segment_from_boolean() + { + var json = JsonValue.True; - [Fact] - public void Should_return_same_object_if_path_is_null() - { - JsonValue json = new JsonObject().Add("property", 12); + var found = json.TryGetByPath("path", out var actual); - var found = json.TryGetByPath((string?)null, out var actual); + Assert.False(found); + Assert.Equal(default, actual); + } - Assert.False(found); - Assert.Equal(json, actual); - } + [Fact] + public void Should_return_null_if_getting_value_by_path_segment_from_number() + { + var json = JsonValue.Create(12); - [Fact] - public void Should_return_same_object_if_path_is_empty() - { - JsonValue json = new JsonObject().Add("property", 12); + var found = json.TryGetByPath("path", out var actual); - var found = json.TryGetByPath(string.Empty, out var actual); + Assert.False(found); + Assert.Equal(default, actual); + } - Assert.False(found); - Assert.Equal(json, actual); - } + [Fact] + public void Should_return_same_object_if_path_is_null() + { + JsonValue json = new JsonObject().Add("property", 12); - [Fact] - public void Should_return_from_nested_array() - { - JsonValue json = - new JsonObject() - .Add("property", - JsonValue.Array( - JsonValue.Create(12), - new JsonObject() - .Add("nested", 13))); - - var found = json.TryGetByPath("property[1].nested", out var actual); - - Assert.True(found); - Assert.Equal(JsonValue.Create(13), actual); - } + var found = json.TryGetByPath((string?)null, out var actual); - [Fact] - public void Should_return_null_if_property_not_found() - { - JsonValue json = - new JsonObject() - .Add("property", 12); + Assert.False(found); + Assert.Equal(json, actual); + } - var found = json.TryGetByPath("notfound", out var actual); + [Fact] + public void Should_return_same_object_if_path_is_empty() + { + JsonValue json = new JsonObject().Add("property", 12); - Assert.False(found); - Assert.Equal(default, actual); - } + var found = json.TryGetByPath(string.Empty, out var actual); - [Fact] - public void Should_return_null_if_out_of_index1() - { - JsonValue json = JsonValue.Array(12, 14); + Assert.False(found); + Assert.Equal(json, actual); + } - var found = json.TryGetByPath("-1", out var actual); + [Fact] + public void Should_return_from_nested_array() + { + JsonValue json = + new JsonObject() + .Add("property", + JsonValue.Array( + JsonValue.Create(12), + new JsonObject() + .Add("nested", 13))); + + var found = json.TryGetByPath("property[1].nested", out var actual); + + Assert.True(found); + Assert.Equal(JsonValue.Create(13), actual); + } - Assert.False(found); - Assert.Equal(default, actual); - } + [Fact] + public void Should_return_null_if_property_not_found() + { + JsonValue json = + new JsonObject() + .Add("property", 12); - [Fact] - public void Should_return_null_if_out_of_index2() - { - JsonValue json = JsonValue.Array(12, 14); + var found = json.TryGetByPath("notfound", out var actual); - var found = json.TryGetByPath("2", out var actual); + Assert.False(found); + Assert.Equal(default, actual); + } - Assert.False(found); - Assert.Equal(default, actual); - } + [Fact] + public void Should_return_null_if_out_of_index1() + { + JsonValue json = JsonValue.Array(12, 14); + + var found = json.TryGetByPath("-1", out var actual); + + Assert.False(found); + Assert.Equal(default, actual); + } + + [Fact] + public void Should_return_null_if_out_of_index2() + { + JsonValue json = JsonValue.Array(12, 14); + + var found = json.TryGetByPath("2", out var actual); + + Assert.False(found); + Assert.Equal(default, actual); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs index 5350544d00..e9bcf89346 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs @@ -9,168 +9,167 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Json.Objects +namespace Squidex.Infrastructure.Json.Objects; + +public class JsonValuesSerializationTests { - public class JsonValuesSerializationTests + public enum SerializerMode { - public enum SerializerMode - { - Json, - Bson - } + Json, + Bson + } + + private static IEnumerable<object[]> Serializers() + { + yield return new object[] { SerializerMode.Json }; + yield return new object[] { SerializerMode.Bson }; + } - private static IEnumerable<object[]> Serializers() + private static T Serialize<T>(T input, SerializerMode mode) + { + if (mode == SerializerMode.Bson) { - yield return new object[] { SerializerMode.Json }; - yield return new object[] { SerializerMode.Bson }; + return input.SerializeAndDeserializeBson(); } - - private static T Serialize<T>(T input, SerializerMode mode) + else { - if (mode == SerializerMode.Bson) - { - return input.SerializeAndDeserializeBson(); - } - else - { - return input.SerializeAndDeserialize(); - } + return input.SerializeAndDeserialize(); } + } - [Fact] - public void Should_deserialize_integer() - { - var value = 123; + [Fact] + public void Should_deserialize_integer() + { + var value = 123; - var serialized = value.SerializeAndDeserialize<JsonValue, int>(); + var serialized = value.SerializeAndDeserialize<JsonValue, int>(); - Assert.Equal(JsonValue.Create(value), serialized); - } + Assert.Equal(JsonValue.Create(value), serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_null(SerializerMode mode) - { - var value = JsonValue.Null; + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_null(SerializerMode mode) + { + var value = JsonValue.Null; - var serialized = Serialize(value, mode); + var serialized = Serialize(value, mode); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_date(SerializerMode mode) - { - var value = JsonValue.Create("2008-09-15T15:53:00"); + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_date(SerializerMode mode) + { + var value = JsonValue.Create("2008-09-15T15:53:00"); - var serialized = Serialize(value, mode); + var serialized = Serialize(value, mode); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_string(SerializerMode mode) - { - var value = JsonValue.Create("my-string"); + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_string(SerializerMode mode) + { + var value = JsonValue.Create("my-string"); - var serialized = Serialize(value, mode); + var serialized = Serialize(value, mode); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_boolean(SerializerMode mode) - { - var value = JsonValue.Create(true); + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_boolean(SerializerMode mode) + { + var value = JsonValue.Create(true); - var serialized = Serialize(value, mode); + var serialized = Serialize(value, mode); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_number(SerializerMode mode) - { - var value = JsonValue.Create(123); + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_number(SerializerMode mode) + { + var value = JsonValue.Create(123); - var serialized = Serialize(value, mode); + var serialized = Serialize(value, mode); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_double_number(SerializerMode mode) - { - var value = JsonValue.Create(123.5); + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_double_number(SerializerMode mode) + { + var value = JsonValue.Create(123.5); - var serialized = Serialize(value, mode); + var serialized = Serialize(value, mode); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_array(SerializerMode mode) - { - var value = JsonValue.Array(1, 2); + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_array(SerializerMode mode) + { + var value = JsonValue.Array(1, 2); - var serialized = Serialize(value, mode); + var serialized = Serialize(value, mode); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_object(SerializerMode mode) - { - var value = - new JsonObject() - .Add("1", 1) - .Add("2", 1); + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_object(SerializerMode mode) + { + var value = + new JsonObject() + .Add("1", 1) + .Add("2", 1); - var serialized = Serialize(value, mode); + var serialized = Serialize(value, mode); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Theory] - [MemberData(nameof(Serializers))] - public void Should_serialize_and_deserialize_complex_object(SerializerMode mode) - { - var value = - new JsonObject() - .Add("1", - JsonValue.Array( - new JsonObject().Add("1_1", 11), - new JsonObject().Add("1_2", 12))) - .Add("2", - new JsonObject().Add("2_1", 11)); - - var serialized = Serialize(value, mode); - - Assert.Equal(value, serialized); - } + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_complex_object(SerializerMode mode) + { + var value = + new JsonObject() + .Add("1", + JsonValue.Array( + new JsonObject().Add("1_1", 11), + new JsonObject().Add("1_2", 12))) + .Add("2", + new JsonObject().Add("2_1", 11)); + + var serialized = Serialize(value, mode); + + Assert.Equal(value, serialized); + } - [Fact] - public void Should_deserialize_from_escaped_dot() + [Fact] + public void Should_deserialize_from_escaped_dot() + { + var value = new Dictionary<string, int> { - var value = new Dictionary<string, int> - { - ["key.with.dot".JsonToBsonName()] = 10 - }; + ["key.with.dot".JsonToBsonName()] = 10 + }; - var expected = - new JsonObject() - .Add("key.with.dot", 10); + var expected = + new JsonObject() + .Add("key.with.dot", 10); - var serialized = TestUtils.SerializeAndDeserializeBson<JsonObject, Dictionary<string, int>>(value); + var serialized = TestUtils.SerializeAndDeserializeBson<JsonObject, Dictionary<string, int>>(value); - Assert.Equal(expected, serialized); - } + Assert.Equal(expected, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterBaseTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterBaseTests.cs index f2afea9001..8b8c66444c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterBaseTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterBaseTests.cs @@ -8,95 +8,94 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public class JsonInheritanceConverterBaseTests { - public class JsonInheritanceConverterBaseTests + private record Base; + + private record A : Base + { + public int PropertyA { get; init; } + } + + private record B : Base { - private record Base; + public int PropertyB { get; init; } + } - private record A : Base + private sealed class Converter : InheritanceConverterBase<Base> + { + public Converter() + : base("$type") { - public int PropertyA { get; init; } } - private record B : Base + public override Type GetDiscriminatorType(string name, Type typeToConvert) { - public int PropertyB { get; init; } + return name == "A" ? typeof(A) : typeof(B); } - private sealed class Converter : InheritanceConverterBase<Base> + public override string GetDiscriminatorValue(Type type) { - public Converter() - : base("$type") - { - } - - public override Type GetDiscriminatorType(string name, Type typeToConvert) - { - return name == "A" ? typeof(A) : typeof(B); - } - - public override string GetDiscriminatorValue(Type type) - { - return type == typeof(A) ? "A" : "B"; - } + return type == typeof(A) ? "A" : "B"; } + } - [Fact] - public void Should_serialize_and_deserialize() + [Fact] + public void Should_serialize_and_deserialize() + { + var serializer = CreateSerializer(); + + Base source = new A { - var serializer = CreateSerializer(); + PropertyA = 42 + }; - Base source = new A - { - PropertyA = 42 - }; + var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); - var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); + Assert.Equal(new A { PropertyA = 42 }, serialized); + } - Assert.Equal(new A { PropertyA = 42 }, serialized); - } + [Fact] + public void Should_deserialize_when_discriminiator_is_first_property() + { + var serializer = CreateSerializer(); - [Fact] - public void Should_deserialize_when_discriminiator_is_first_property() + var source = new Dictionary<string, object> { - var serializer = CreateSerializer(); + ["$type"] = "A", + ["propertyA"] = 42, + ["propertyOther"] = 44 + }; - var source = new Dictionary<string, object> - { - ["$type"] = "A", - ["propertyA"] = 42, - ["propertyOther"] = 44 - }; + var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); - var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); + Assert.Equal(new A { PropertyA = 42 }, serialized); + } - Assert.Equal(new A { PropertyA = 42 }, serialized); - } + [Fact] + public void Should_deserialize_when_discriminiator_is_not_first_property() + { + var serializer = CreateSerializer(); - [Fact] - public void Should_deserialize_when_discriminiator_is_not_first_property() + var source = new Dictionary<string, object> { - var serializer = CreateSerializer(); + ["propertyB"] = 42, + ["propertyOther"] = 44, + ["$type"] = "B" + }; - var source = new Dictionary<string, object> - { - ["propertyB"] = 42, - ["propertyOther"] = 44, - ["$type"] = "B" - }; + var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); - var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); - - Assert.Equal(new B { PropertyB = 42 }, serialized); - } + Assert.Equal(new B { PropertyB = 42 }, serialized); + } - private static IJsonSerializer CreateSerializer() + private static IJsonSerializer CreateSerializer() + { + return TestUtils.CreateSerializer(options => { - return TestUtils.CreateSerializer(options => - { - options.Converters.Add(new Converter()); - }); - } + options.Converters.Add(new Converter()); + }); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterTests.cs index 6470f9e2f3..d97555a84a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterTests.cs @@ -9,82 +9,81 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public class JsonInheritanceConverterTests { - public class JsonInheritanceConverterTests + private record Base; + + private record A : Base { - private record Base; + public int PropertyA { get; init; } + } - private record A : Base - { - public int PropertyA { get; init; } - } + private record B : Base + { + public int PropertyB { get; init; } + } - private record B : Base - { - public int PropertyB { get; init; } - } + [Fact] + public void Should_serialize_and_deserialize() + { + var serializer = CreateSerializer(); - [Fact] - public void Should_serialize_and_deserialize() + Base source = new A { - var serializer = CreateSerializer(); + PropertyA = 42 + }; - Base source = new A - { - PropertyA = 42 - }; + var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); - var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); + Assert.Equal(new A { PropertyA = 42 }, serialized); + } - Assert.Equal(new A { PropertyA = 42 }, serialized); - } + [Fact] + public void Should_deserialize_when_discriminiator_is_first_property() + { + var serializer = CreateSerializer(); - [Fact] - public void Should_deserialize_when_discriminiator_is_first_property() + var source = new Dictionary<string, object> { - var serializer = CreateSerializer(); + ["$type"] = "A", + ["propertyA"] = 42, + ["propertyOther"] = 44 + }; - var source = new Dictionary<string, object> - { - ["$type"] = "A", - ["propertyA"] = 42, - ["propertyOther"] = 44 - }; + var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); - var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); + Assert.Equal(new A { PropertyA = 42 }, serialized); + } - Assert.Equal(new A { PropertyA = 42 }, serialized); - } + [Fact] + public void Should_deserialize_when_discriminiator_is_not_first_property() + { + var serializer = CreateSerializer(); - [Fact] - public void Should_deserialize_when_discriminiator_is_not_first_property() + var source = new Dictionary<string, object> { - var serializer = CreateSerializer(); + ["propertyB"] = 42, + ["propertyOther"] = 44, + ["$type"] = "B" + }; - var source = new Dictionary<string, object> - { - ["propertyB"] = 42, - ["propertyOther"] = 44, - ["$type"] = "B" - }; + var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); - var serialized = serializer.Deserialize<Base>(serializer.Serialize(source)); - - Assert.Equal(new B { PropertyB = 42 }, serialized); - } + Assert.Equal(new B { PropertyB = 42 }, serialized); + } - private static IJsonSerializer CreateSerializer() + private static IJsonSerializer CreateSerializer() + { + return TestUtils.CreateSerializer(options => { - return TestUtils.CreateSerializer(options => - { - var typeRegistry = - new TypeNameRegistry() - .Map(typeof(A), "A") - .Map(typeof(B), "B"); - - options.Converters.Add(new InheritanceConverter<Base>(typeRegistry)); - }); - } + var typeRegistry = + new TypeNameRegistry() + .Map(typeof(A), "A") + .Map(typeof(B), "B"); + + options.Converters.Add(new InheritanceConverter<Base>(typeRegistry)); + }); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/System/ReadOnlyCollectionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/System/ReadOnlyCollectionTests.cs index b8fb35ca8b..c9068b8e26 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/System/ReadOnlyCollectionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/System/ReadOnlyCollectionTests.cs @@ -8,47 +8,46 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Json.System +namespace Squidex.Infrastructure.Json.System; + +public class ReadOnlyCollectionTests { - public class ReadOnlyCollectionTests + public sealed class MyClass<T> { - public sealed class MyClass<T> - { - public T Values { get; set; } - } + public T Values { get; set; } + } - [Fact] - public void Should_serialize_and_deserialize_dictionary() + [Fact] + public void Should_serialize_and_deserialize_dictionary() + { + var source = new MyClass<IReadOnlyDictionary<int, int>> { - var source = new MyClass<IReadOnlyDictionary<int, int>> + Values = new Dictionary<int, int> { - Values = new Dictionary<int, int> - { - [2] = 4, - [3] = 9 - } - }; + [2] = 4, + [3] = 9 + } + }; - var serialized = source.SerializeAndDeserialize(); + var serialized = source.SerializeAndDeserialize(); - Assert.Equal(2, serialized.Values.Count); - } + Assert.Equal(2, serialized.Values.Count); + } - [Fact] - public void Should_serialize_and_deserialize_list_without_type_name() + [Fact] + public void Should_serialize_and_deserialize_list_without_type_name() + { + var source = new MyClass<IReadOnlyList<int>> { - var source = new MyClass<IReadOnlyList<int>> + Values = new List<int> { - Values = new List<int> - { - 2, - 3 - } - }; + 2, + 3 + } + }; - var serialized = source.SerializeAndDeserialize(); + var serialized = source.SerializeAndDeserialize(); - Assert.Equal(2, serialized.Values.Count); - } + Assert.Equal(2, serialized.Values.Count); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs index af204e5d33..f7908650a7 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -8,151 +8,150 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class LanguageTests { - public class LanguageTests - { - [Theory] - [InlineData("")] - [InlineData(" ")] - public void Should_throw_exception_if_getting_by_empty_key(string key) - { - Assert.Throws<ArgumentException>(() => Language.GetLanguage(key)); - } - - [Fact] - public void Should_throw_exception_if_getting_by_null_key() - { - Assert.Throws<ArgumentNullException>(() => Language.GetLanguage(null!)); - } - - [Fact] - public void Should_provide_custom_language() - { - var actual = Language.GetLanguage("xy"); - - Assert.Equal("xy", actual.Iso2Code); - } - - [Fact] - public void Should_trim_custom_language() - { - var actual = Language.GetLanguage("xy "); - - Assert.Equal("xy", actual.Iso2Code); - } - - [Fact] - public void Should_provide_default_language() - { - var actual = Language.GetLanguage("de"); - - Assert.Same(Language.DE, actual); - } - - [Fact] - public void Should_provide_all_languages() - { - Assert.True(Language.AllLanguages.Count > 100); - } - - [Fact] - public void Should_return_true_for_default_language() - { - Assert.True(Language.IsDefault("de")); - } - - [Fact] - public void Should_return_false_for_custom_language() - { - Assert.False(Language.IsDefault("xx")); - } - - [Fact] - public void Should_make_implicit_conversion_to_language() - { - Language language = "de"!; - - Assert.Equal(Language.DE, language); - } - - [Fact] - public void Should_make_implicit_conversion_to_string() - { - string iso2Code = Language.DE!; - - Assert.Equal("de", iso2Code); - } - - [Theory] - [InlineData("de", "German")] - [InlineData("en", "English")] - [InlineData("sv", "Swedish")] - [InlineData("zh", "Chinese")] - public void Should_provide_correct_english_name(string key, string englishName) - { - var language = Language.GetLanguage(key); - - Assert.Equal(key, language.Iso2Code); - Assert.Equal(englishName, language.EnglishName); - Assert.Equal(englishName, language.ToString()); - } - - [Theory] - [InlineData("en", "en")] - [InlineData("en ", "en")] - [InlineData("EN", "en")] - [InlineData("EN ", "en")] - public void Should_parse_valid_languages(string input, string languageCode) - { - var language = Language.ParseOrNull(input); - - Assert.Equal(language, Language.GetLanguage(languageCode)); - } - - [Theory] - [InlineData("en-US", "en")] - [InlineData("en-GB", "en")] - [InlineData("EN-US", "en")] - [InlineData("EN-GB", "en")] - public void Should_parse_lanuages_from_culture(string input, string languageCode) - { - var language = Language.ParseOrNull(input); - - Assert.Equal(language, Language.GetLanguage(languageCode)); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData("xx")] - [InlineData("invalid")] - [InlineData(null)] - public void Should_parse_invalid_languages(string input) - { - var language = Language.ParseOrNull(input); - - Assert.Null(language); - } - - [Fact] - public void Should_serialize_and_deserialize_null_language() - { - Language? value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_language() - { - var value = Language.DE; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Should_throw_exception_if_getting_by_empty_key(string key) + { + Assert.Throws<ArgumentException>(() => Language.GetLanguage(key)); + } + + [Fact] + public void Should_throw_exception_if_getting_by_null_key() + { + Assert.Throws<ArgumentNullException>(() => Language.GetLanguage(null!)); + } + + [Fact] + public void Should_provide_custom_language() + { + var actual = Language.GetLanguage("xy"); + + Assert.Equal("xy", actual.Iso2Code); + } + + [Fact] + public void Should_trim_custom_language() + { + var actual = Language.GetLanguage("xy "); + + Assert.Equal("xy", actual.Iso2Code); + } + + [Fact] + public void Should_provide_default_language() + { + var actual = Language.GetLanguage("de"); + + Assert.Same(Language.DE, actual); + } + + [Fact] + public void Should_provide_all_languages() + { + Assert.True(Language.AllLanguages.Count > 100); + } + + [Fact] + public void Should_return_true_for_default_language() + { + Assert.True(Language.IsDefault("de")); + } + + [Fact] + public void Should_return_false_for_custom_language() + { + Assert.False(Language.IsDefault("xx")); + } + + [Fact] + public void Should_make_implicit_conversion_to_language() + { + Language language = "de"!; + + Assert.Equal(Language.DE, language); + } + + [Fact] + public void Should_make_implicit_conversion_to_string() + { + string iso2Code = Language.DE!; + + Assert.Equal("de", iso2Code); + } + + [Theory] + [InlineData("de", "German")] + [InlineData("en", "English")] + [InlineData("sv", "Swedish")] + [InlineData("zh", "Chinese")] + public void Should_provide_correct_english_name(string key, string englishName) + { + var language = Language.GetLanguage(key); + + Assert.Equal(key, language.Iso2Code); + Assert.Equal(englishName, language.EnglishName); + Assert.Equal(englishName, language.ToString()); + } + + [Theory] + [InlineData("en", "en")] + [InlineData("en ", "en")] + [InlineData("EN", "en")] + [InlineData("EN ", "en")] + public void Should_parse_valid_languages(string input, string languageCode) + { + var language = Language.ParseOrNull(input); + + Assert.Equal(language, Language.GetLanguage(languageCode)); + } + + [Theory] + [InlineData("en-US", "en")] + [InlineData("en-GB", "en")] + [InlineData("EN-US", "en")] + [InlineData("EN-GB", "en")] + public void Should_parse_lanuages_from_culture(string input, string languageCode) + { + var language = Language.ParseOrNull(input); + + Assert.Equal(language, Language.GetLanguage(languageCode)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("xx")] + [InlineData("invalid")] + [InlineData(null)] + public void Should_parse_invalid_languages(string input) + { + var language = Language.ParseOrNull(input); + + Assert.Null(language); + } + + [Fact] + public void Should_serialize_and_deserialize_null_language() + { + Language? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_language() + { + var value = Language.DE; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs index ccc54cd582..096f7909f9 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs @@ -8,53 +8,52 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public sealed class LanguagesInitializerTests { - public sealed class LanguagesInitializerTests + [Fact] + public async Task Should_add_custom_languages() { - [Fact] - public async Task Should_add_custom_languages() + var options = Options.Create(new LanguagesOptions { - var options = Options.Create(new LanguagesOptions - { - ["en-NO"] = "English (Norwegian)" - }); + ["en-NO"] = "English (Norwegian)" + }); - var sut = new LanguagesInitializer(options); + var sut = new LanguagesInitializer(options); - await sut.InitializeAsync(default); + await sut.InitializeAsync(default); - Assert.Equal("English (Norwegian)", Language.GetLanguage("en-NO").EnglishName); - } + Assert.Equal("English (Norwegian)", Language.GetLanguage("en-NO").EnglishName); + } - [Fact] - public async Task Should_not_add_invalid_languages() + [Fact] + public async Task Should_not_add_invalid_languages() + { + var options = Options.Create(new LanguagesOptions { - var options = Options.Create(new LanguagesOptions - { - ["en-Error"] = null! - }); + ["en-Error"] = null! + }); - var sut = new LanguagesInitializer(options); + var sut = new LanguagesInitializer(options); - await sut.InitializeAsync(default); + await sut.InitializeAsync(default); - Assert.False(Language.TryGetLanguage("en-Error", out _)); - } + Assert.False(Language.TryGetLanguage("en-Error", out _)); + } - [Fact] - public async Task Should_not_override_existing_languages() + [Fact] + public async Task Should_not_override_existing_languages() + { + var options = Options.Create(new LanguagesOptions { - var options = Options.Create(new LanguagesOptions - { - ["de"] = "German (Germany)" - }); + ["de"] = "German (Germany)" + }); - var sut = new LanguagesInitializer(options); + var sut = new LanguagesInitializer(options); - await sut.InitializeAsync(default); + await sut.InitializeAsync(default); - Assert.Equal("German", Language.GetLanguage("de").EnglishName); - } + Assert.Equal("German", Language.GetLanguage("de").EnglishName); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs index 33c89b8ac5..16d80146e3 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs @@ -11,130 +11,129 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Squidex.Infrastructure.Log +namespace Squidex.Infrastructure.Log; + +public class BackgroundRequestLogStoreTests { - public class BackgroundRequestLogStoreTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IRequestLogRepository requestLogRepository = A.Fake<IRequestLogRepository>(); + private readonly RequestLogStoreOptions options = new RequestLogStoreOptions(); + private readonly BackgroundRequestLogStore sut; + + public BackgroundRequestLogStoreTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IRequestLogRepository requestLogRepository = A.Fake<IRequestLogRepository>(); - private readonly RequestLogStoreOptions options = new RequestLogStoreOptions(); - private readonly BackgroundRequestLogStore sut; + ct = cts.Token; - public BackgroundRequestLogStoreTests() - { - ct = cts.Token; + options.StoreEnabled = true; - options.StoreEnabled = true; + var log = A.Fake<ILogger<BackgroundRequestLogStore>>(); - var log = A.Fake<ILogger<BackgroundRequestLogStore>>(); + sut = new BackgroundRequestLogStore(Options.Create(options), requestLogRepository, log) + { + ForceWrite = true + }; + } - sut = new BackgroundRequestLogStore(Options.Create(options), requestLogRepository, log) - { - ForceWrite = true - }; - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Should_provide_disabled_from_options(bool enabled) + { + options.StoreEnabled = enabled; - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Should_provide_disabled_from_options(bool enabled) - { - options.StoreEnabled = enabled; + Assert.Equal(enabled, sut.IsEnabled); + } - Assert.Equal(enabled, sut.IsEnabled); - } + [Fact] + public async Task Should_forward_delete_call() + { + await sut.DeleteAsync("my-key", ct); - [Fact] - public async Task Should_forward_delete_call() - { - await sut.DeleteAsync("my-key", ct); + A.CallTo(() => requestLogRepository.DeleteAsync("my-key", ct)) + .MustHaveHappened(); + } - A.CallTo(() => requestLogRepository.DeleteAsync("my-key", ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_not_log_if_disabled() + { + options.StoreEnabled = false; - [Fact] - public async Task Should_not_log_if_disabled() + for (var i = 0; i < 2500; i++) { - options.StoreEnabled = false; - - for (var i = 0; i < 2500; i++) - { - await sut.LogAsync(new Request { Key = i.ToString(CultureInfo.InvariantCulture) }, ct); - } + await sut.LogAsync(new Request { Key = i.ToString(CultureInfo.InvariantCulture) }, ct); + } - sut.Next(); - sut.Dispose(); + sut.Next(); + sut.Dispose(); - // Wait for the timer to not trigger. - await Task.Delay(500, ct); + // Wait for the timer to not trigger. + await Task.Delay(500, ct); - A.CallTo(() => requestLogRepository.InsertManyAsync(A<IEnumerable<Request>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => requestLogRepository.InsertManyAsync(A<IEnumerable<Request>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_provide_actuals_from_repository() - { - var key = "my-key"; + [Fact] + public async Task Should_provide_actuals_from_repository() + { + var key = "my-key"; - var dateFrom = DateTime.Today; - var dateTo = dateFrom.AddDays(4); + var dateFrom = DateTime.Today; + var dateTo = dateFrom.AddDays(4); - A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, ct)) - .Returns(AsyncEnumerable.Repeat(new Request { Key = key }, 1)); + A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, ct)) + .Returns(AsyncEnumerable.Repeat(new Request { Key = key }, 1)); - var actuals = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); + var actuals = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); - Assert.NotEmpty(actuals); - } + Assert.NotEmpty(actuals); + } - [Fact] - public async Task Should_not_provide_actuals_from_repository_if_disabled() - { - options.StoreEnabled = false; + [Fact] + public async Task Should_not_provide_actuals_from_repository_if_disabled() + { + options.StoreEnabled = false; - var key = "my-key"; + var key = "my-key"; - var dateFrom = DateTime.Today; - var dateTo = dateFrom.AddDays(4); + var dateFrom = DateTime.Today; + var dateTo = dateFrom.AddDays(4); - var actuals = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); + var actuals = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); - Assert.Empty(actuals); + Assert.Empty(actuals); - A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_write_logs_in_batches() + [Fact] + public async Task Should_write_logs_in_batches() + { + for (var i = 0; i < 2500; i++) { - for (var i = 0; i < 2500; i++) - { - await sut.LogAsync(new Request { Key = i.ToString(CultureInfo.InvariantCulture) }, ct); - } + await sut.LogAsync(new Request { Key = i.ToString(CultureInfo.InvariantCulture) }, ct); + } - sut.Next(); - sut.Dispose(); + sut.Next(); + sut.Dispose(); - // Wait for the timer to trigger. - await Task.Delay(500, ct); + // Wait for the timer to trigger. + await Task.Delay(500, ct); - A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("0", "999"), A<CancellationToken>._)) - .MustHaveHappened(); + A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("0", "999"), A<CancellationToken>._)) + .MustHaveHappened(); - A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("1000", "1999"), A<CancellationToken>._)) - .MustHaveHappened(); + A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("1000", "1999"), A<CancellationToken>._)) + .MustHaveHappened(); - A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("2000", "2499"), A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("2000", "2499"), A<CancellationToken>._)) + .MustHaveHappened(); + } - private static IEnumerable<Request> Batch(string from, string to) - { - return A<IEnumerable<Request>>.That.Matches(x => x.First().Key == from && x.Last().Key == to); - } + private static IEnumerable<Request> Batch(string from, string to) + { + return A<IEnumerable<Request>>.That.Matches(x => x.First().Key == from && x.Last().Key == to); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs index fd70844fe7..a2b07cdc08 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -9,269 +9,268 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace Squidex.Infrastructure.Migrations +namespace Squidex.Infrastructure.Migrations; + +public class MigratorTests { - public class MigratorTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IMigrationStatus status = A.Fake<IMigrationStatus>(); + private readonly IMigrationPath path = A.Fake<IMigrationPath>(); + private readonly ILogger<Migrator> log = A.Fake<ILogger<Migrator>>(); + private readonly List<(int From, int To, IMigration Migration)> migrations = new List<(int From, int To, IMigration Migration)>(); + + public sealed class InMemoryStatus : IMigrationStatus { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IMigrationStatus status = A.Fake<IMigrationStatus>(); - private readonly IMigrationPath path = A.Fake<IMigrationPath>(); - private readonly ILogger<Migrator> log = A.Fake<ILogger<Migrator>>(); - private readonly List<(int From, int To, IMigration Migration)> migrations = new List<(int From, int To, IMigration Migration)>(); - - public sealed class InMemoryStatus : IMigrationStatus + private readonly object lockObject = new object(); + private int version; + private bool isLocked; + + public Task<int> GetVersionAsync( + CancellationToken ct = default) { - private readonly object lockObject = new object(); - private int version; - private bool isLocked; + return Task.FromResult(version); + } - public Task<int> GetVersionAsync( - CancellationToken ct = default) - { - return Task.FromResult(version); - } + public Task<bool> TryLockAsync( + CancellationToken ct = default) + { + var lockAcquired = false; - public Task<bool> TryLockAsync( - CancellationToken ct = default) + lock (lockObject) { - var lockAcquired = false; - - lock (lockObject) + if (!isLocked) { - if (!isLocked) - { - isLocked = true; + isLocked = true; - lockAcquired = true; - } + lockAcquired = true; } - - return Task.FromResult(lockAcquired); } - public Task CompleteAsync(int newVersion, - CancellationToken ct = default) - { - lock (lockObject) - { - version = newVersion; - } + return Task.FromResult(lockAcquired); + } - return Task.CompletedTask; + public Task CompleteAsync(int newVersion, + CancellationToken ct = default) + { + lock (lockObject) + { + version = newVersion; } - public Task UnlockAsync( - CancellationToken ct = default) - { - lock (lockObject) - { - isLocked = false; - } + return Task.CompletedTask; + } - return Task.CompletedTask; + public Task UnlockAsync( + CancellationToken ct = default) + { + lock (lockObject) + { + isLocked = false; } + + return Task.CompletedTask; } + } - public MigratorTests() - { - ct = cts.Token; + public MigratorTests() + { + ct = cts.Token; - A.CallTo(() => path.GetNext(A<int>._)) - .ReturnsLazily((int version) => - { - var selected = migrations.Where(x => x.From == version).ToList(); + A.CallTo(() => path.GetNext(A<int>._)) + .ReturnsLazily((int version) => + { + var selected = migrations.Where(x => x.From == version).ToList(); - if (selected.Count == 0) - { - return (0, null); - } + if (selected.Count == 0) + { + return (0, null); + } - var newVersion = selected.Max(x => x.To); + var newVersion = selected.Max(x => x.To); - return (newVersion, migrations.Select(x => x.Migration)); - }); + return (newVersion, migrations.Select(x => x.Migration)); + }); - A.CallTo(() => status.GetVersionAsync(ct)) - .Returns(0); + A.CallTo(() => status.GetVersionAsync(ct)) + .Returns(0); - A.CallTo(() => status.TryLockAsync(ct)) - .Returns(true); - } + A.CallTo(() => status.TryLockAsync(ct)) + .Returns(true); + } - [Fact] - public async Task Should_migrate_in_one_step() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(0, 2); - var migrator_2_3 = BuildMigration(0, 3); + [Fact] + public async Task Should_migrate_in_one_step() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(0, 2); + var migrator_2_3 = BuildMigration(0, 3); - var sut = new Migrator(status, path, log); + var sut = new Migrator(status, path, log); - await sut.MigrateAsync(ct); + await sut.MigrateAsync(ct); - A.CallTo(() => migrator_0_1.UpdateAsync(ct)) - .MustHaveHappened(); + A.CallTo(() => migrator_0_1.UpdateAsync(ct)) + .MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(ct)) - .MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) + .MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(ct)) - .MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync(ct)) + .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(1, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => status.CompleteAsync(1, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(2, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => status.CompleteAsync(2, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(3, ct)) - .MustHaveHappened(); + A.CallTo(() => status.CompleteAsync(3, ct)) + .MustHaveHappened(); - A.CallTo(() => status.UnlockAsync(default)) - .MustHaveHappened(); - } + A.CallTo(() => status.UnlockAsync(default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_migrate_step_by_step() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - var migrator_2_3 = BuildMigration(2, 3); + [Fact] + public async Task Should_migrate_step_by_step() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); - var sut = new Migrator(status, path, log); + var sut = new Migrator(status, path, log); - await sut.MigrateAsync(ct); + await sut.MigrateAsync(ct); - A.CallTo(() => migrator_0_1.UpdateAsync(ct)) - .MustHaveHappened(); + A.CallTo(() => migrator_0_1.UpdateAsync(ct)) + .MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(ct)) - .MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) + .MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(ct)) - .MustHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync(ct)) + .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(1, ct)) - .MustHaveHappened(); + A.CallTo(() => status.CompleteAsync(1, ct)) + .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(2, ct)) - .MustHaveHappened(); + A.CallTo(() => status.CompleteAsync(2, ct)) + .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(3, ct)) - .MustHaveHappened(); + A.CallTo(() => status.CompleteAsync(3, ct)) + .MustHaveHappened(); - A.CallTo(() => status.UnlockAsync(A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => status.UnlockAsync(A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_unlock_if_migration_failed() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); - var migrator_2_3 = BuildMigration(2, 3); + [Fact] + public async Task Should_unlock_if_migration_failed() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); + var migrator_2_3 = BuildMigration(2, 3); - var sut = new Migrator(status, path, log); + var sut = new Migrator(status, path, log); - A.CallTo(() => migrator_1_2.UpdateAsync(ct)) - .Throws(new InvalidOperationException()); + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) + .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync<MigrationFailedException>(() => sut.MigrateAsync(ct)); + await Assert.ThrowsAsync<MigrationFailedException>(() => sut.MigrateAsync(ct)); - A.CallTo(() => migrator_0_1.UpdateAsync(A<CancellationToken>._)) - .MustHaveHappened(); + A.CallTo(() => migrator_0_1.UpdateAsync(A<CancellationToken>._)) + .MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(A<CancellationToken>._)) - .MustHaveHappened(); + A.CallTo(() => migrator_1_2.UpdateAsync(A<CancellationToken>._)) + .MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => migrator_2_3.UpdateAsync(A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(1, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => status.CompleteAsync(1, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(2, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => status.CompleteAsync(2, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(3, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => status.CompleteAsync(3, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => status.UnlockAsync(default)) - .MustHaveHappened(); - } + A.CallTo(() => status.UnlockAsync(default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_log_exception_if_migration_failed() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(1, 2); + [Fact] + public async Task Should_log_exception_if_migration_failed() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(1, 2); - var ex = new InvalidOperationException(); + var ex = new InvalidOperationException(); - A.CallTo(() => migrator_0_1.UpdateAsync(ct)) - .Throws(ex); + A.CallTo(() => migrator_0_1.UpdateAsync(ct)) + .Throws(ex); - var sut = new Migrator(status, path, log); + var sut = new Migrator(status, path, log); - await Assert.ThrowsAsync<MigrationFailedException>(() => sut.MigrateAsync(ct)); + await Assert.ThrowsAsync<MigrationFailedException>(() => sut.MigrateAsync(ct)); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Critical && x.GetArgument<Exception>(3) == ex) - .MustHaveHappened(); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Critical && x.GetArgument<Exception>(3) == ex) + .MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => migrator_1_2.UpdateAsync(A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_prevent_multiple_updates() - { - var migrator_0_1 = BuildMigration(0, 1); - var migrator_1_2 = BuildMigration(0, 2); + [Fact] + public async Task Should_prevent_multiple_updates() + { + var migrator_0_1 = BuildMigration(0, 1); + var migrator_1_2 = BuildMigration(0, 2); - var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; + var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; - await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync(ct), ct))); + await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync(ct), ct))); - A.CallTo(() => migrator_0_1.UpdateAsync(ct)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => migrator_0_1.UpdateAsync(ct)) + .MustHaveHappenedOnceExactly(); - A.CallTo(() => migrator_1_2.UpdateAsync(ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_not_release_lock_if_not_acquired() + [Fact] + public async Task Should_not_release_lock_if_not_acquired() + { + using (var tcs = new CancellationTokenSource()) { - using (var tcs = new CancellationTokenSource()) - { - var sut = new Migrator(status, path, log) { LockWaitMs = 2 }; + var sut = new Migrator(status, path, log) { LockWaitMs = 2 }; - A.CallTo(() => status.TryLockAsync(tcs.Token)) - .Returns(false); + A.CallTo(() => status.TryLockAsync(tcs.Token)) + .Returns(false); - var task = sut.MigrateAsync(tcs.Token); + var task = sut.MigrateAsync(tcs.Token); #pragma warning disable MA0040 // Flow the cancellation token - await Task.Delay(100); + await Task.Delay(100); #pragma warning restore MA0040 // Flow the cancellation token - tcs.Cancel(); + tcs.Cancel(); - await task; + await task; - A.CallTo(() => status.UnlockAsync(A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => status.UnlockAsync(A<CancellationToken>._)) + .MustNotHaveHappened(); } + } - private IMigration BuildMigration(int fromVersion, int toVersion) - { - var migration = A.Fake<IMigration>(); + private IMigration BuildMigration(int fromVersion, int toVersion) + { + var migration = A.Fake<IMigration>(); - migrations.Add((fromVersion, toVersion, migration)); + migrations.Add((fromVersion, toVersion, migration)); - return migration; - } + return migration; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs index 30ad1ef300..999d7bb577 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs @@ -11,211 +11,210 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public class BsonJsonSerializerTests { - public class BsonJsonSerializerTests + public class TestWrapper<T> { - public class TestWrapper<T> - { - [BsonJson] - public T Value { get; set; } - } + [BsonJson] + public T Value { get; set; } + } - public class TestWrapperDocument<T> - { - [BsonJson] - [BsonRepresentation(BsonType.Document)] - public T Value { get; set; } - } + public class TestWrapperDocument<T> + { + [BsonJson] + [BsonRepresentation(BsonType.Document)] + public T Value { get; set; } + } - public class TestWrapperString<T> - { - [BsonJson] - [BsonRepresentation(BsonType.String)] - public T Value { get; set; } - } + public class TestWrapperString<T> + { + [BsonJson] + [BsonRepresentation(BsonType.String)] + public T Value { get; set; } + } - public class TestWrapperBinary<T> - { - [BsonJson] - [BsonRepresentation(BsonType.Binary)] - public T Value { get; set; } - } + public class TestWrapperBinary<T> + { + [BsonJson] + [BsonRepresentation(BsonType.Binary)] + public T Value { get; set; } + } - public class TestObject - { - public bool Bool { get; set; } + public class TestObject + { + public bool Bool { get; set; } + + public byte Byte { get; set; } - public byte Byte { get; set; } + public byte[] Bytes { get; set; } - public byte[] Bytes { get; set; } + public int Int32 { get; set; } - public int Int32 { get; set; } + public long Int64 { get; set; } - public long Int64 { get; set; } + public short Int16 { get; set; } - public short Int16 { get; set; } + public uint UInt32 { get; set; } - public uint UInt32 { get; set; } + public ulong UInt64 { get; set; } - public ulong UInt64 { get; set; } + public ushort UInt16 { get; set; } - public ushort UInt16 { get; set; } + public string String { get; set; } - public string String { get; set; } + public float Float32 { get; set; } - public float Float32 { get; set; } + public double Float64 { get; set; } - public double Float64 { get; set; } + public string[] Strings { get; set; } - public string[] Strings { get; set; } + public Uri Uri { get; set; } - public Uri Uri { get; set; } + public Guid Guid { get; set; } - public Guid Guid { get; set; } + public TimeSpan TimeSpan { get; set; } - public TimeSpan TimeSpan { get; set; } + public DateTime DateTime { get; set; } - public DateTime DateTime { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } - public DateTimeOffset DateTimeOffset { get; set; } + public TestObject Nested { get; set; } - public TestObject Nested { get; set; } + public TestObject[] NestedArray { get; set; } - public TestObject[] NestedArray { get; set; } + public static TestObject CreateWithValues(bool nested = true) + { + var actual = new TestObject + { + Bool = true, + Byte = 0x2, + Bytes = new byte[] { 0x10, 0x12, 0x13 }, + DateTimeOffset = DateTime.Today, + DateTime = DateTime.UtcNow.Date, + Float32 = 32.5f, + Float64 = 32.5d, + Guid = Guid.NewGuid(), + Int64 = 64, + Int32 = 32, + Int16 = 16, + String = "squidex", + Strings = new[] { "hello", "squidex " }, + TimeSpan = TimeSpan.FromSeconds(123), + UInt64 = 164, + UInt32 = 132, + UInt16 = 116, + Uri = new Uri("http://squidex.io") + }; - public static TestObject CreateWithValues(bool nested = true) + if (nested) { - var actual = new TestObject - { - Bool = true, - Byte = 0x2, - Bytes = new byte[] { 0x10, 0x12, 0x13 }, - DateTimeOffset = DateTime.Today, - DateTime = DateTime.UtcNow.Date, - Float32 = 32.5f, - Float64 = 32.5d, - Guid = Guid.NewGuid(), - Int64 = 64, - Int32 = 32, - Int16 = 16, - String = "squidex", - Strings = new[] { "hello", "squidex " }, - TimeSpan = TimeSpan.FromSeconds(123), - UInt64 = 164, - UInt32 = 132, - UInt16 = 116, - Uri = new Uri("http://squidex.io") - }; - - if (nested) - { - actual.Nested = CreateWithValues(false); - actual.NestedArray = Enumerable.Repeat(0, 4).Select(x => CreateWithValues(false)).ToArray(); - } - - return actual; + actual.Nested = CreateWithValues(false); + actual.NestedArray = Enumerable.Repeat(0, 4).Select(x => CreateWithValues(false)).ToArray(); } + + return actual; } + } - [Fact] - public void Should_serialize_and_deserialize() + [Fact] + public void Should_serialize_and_deserialize() + { + var source = new TestWrapper<TestObject> { - var source = new TestWrapper<TestObject> - { - Value = TestObject.CreateWithValues() - }; + Value = TestObject.CreateWithValues() + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - deserialized.Should().BeEquivalentTo(source); - } + deserialized.Should().BeEquivalentTo(source); + } - [Fact] - public void Should_serialize_and_deserialize_as_document() + [Fact] + public void Should_serialize_and_deserialize_as_document() + { + var source = new TestWrapperDocument<TestObject> { - var source = new TestWrapperDocument<TestObject> - { - Value = TestObject.CreateWithValues() - }; + Value = TestObject.CreateWithValues() + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - deserialized.Should().BeEquivalentTo(source); - } + deserialized.Should().BeEquivalentTo(source); + } - [Fact] - public void Should_serialize_and_deserialize_as_string() + [Fact] + public void Should_serialize_and_deserialize_as_string() + { + var source = new TestWrapperString<TestObject> { - var source = new TestWrapperString<TestObject> - { - Value = TestObject.CreateWithValues() - }; + Value = TestObject.CreateWithValues() + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - deserialized.Should().BeEquivalentTo(source); - } + deserialized.Should().BeEquivalentTo(source); + } - [Fact] - public void Should_serialize_and_deserialize_as_binary() + [Fact] + public void Should_serialize_and_deserialize_as_binary() + { + var source = new TestWrapperBinary<TestObject> { - var source = new TestWrapperBinary<TestObject> - { - Value = TestObject.CreateWithValues() - }; + Value = TestObject.CreateWithValues() + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - deserialized.Should().BeEquivalentTo(source); - } + deserialized.Should().BeEquivalentTo(source); + } - [Fact] - public void Should_serialize_and_deserialize_property_with_dollar() + [Fact] + public void Should_serialize_and_deserialize_property_with_dollar() + { + var source = new TestWrapper<Dictionary<string, int>> { - var source = new TestWrapper<Dictionary<string, int>> + Value = new Dictionary<string, int> { - Value = new Dictionary<string, int> - { - ["$key"] = 12 - } - }; + ["$key"] = 12 + } + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - deserialized.Should().BeEquivalentTo(source); - } + deserialized.Should().BeEquivalentTo(source); + } - [Fact] - public void Should_serialize_and_deserialize_property_with_dot() + [Fact] + public void Should_serialize_and_deserialize_property_with_dot() + { + var source = new TestWrapper<Dictionary<string, int>> { - var source = new TestWrapper<Dictionary<string, int>> + Value = new Dictionary<string, int> { - Value = new Dictionary<string, int> - { - ["type.of.value"] = 12 - } - }; + ["type.of.value"] = 12 + } + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - deserialized.Should().BeEquivalentTo(source); - } + deserialized.Should().BeEquivalentTo(source); + } - [Fact] - public void Should_serialize_and_deserialize_property_as_empty_string() + [Fact] + public void Should_serialize_and_deserialize_property_as_empty_string() + { + var source = new TestWrapper<Dictionary<string, int>> { - var source = new TestWrapper<Dictionary<string, int>> + Value = new Dictionary<string, int> { - Value = new Dictionary<string, int> - { - [string.Empty] = 12 - } - }; + [string.Empty] = 12 + } + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - deserialized.Should().BeEquivalentTo(source); - } + deserialized.Should().BeEquivalentTo(source); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs index b8e0d8dffe..7b73a479f2 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs @@ -12,81 +12,80 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public class DomainIdSerializerTests { - public class DomainIdSerializerTests + private sealed class StringEntity<T> { - private sealed class StringEntity<T> - { - [BsonRepresentation(BsonType.String)] - public T Id { get; set; } - } + [BsonRepresentation(BsonType.String)] + public T Id { get; set; } + } - private sealed class IdEntity<T> - { - public T Id { get; set; } - } + private sealed class IdEntity<T> + { + public T Id { get; set; } + } - public DomainIdSerializerTests() - { - TestUtils.SetupBson(); - } + public DomainIdSerializerTests() + { + TestUtils.SetupBson(); + } - [Fact] - public void Should_deserialize_from_string() - { - var id = Guid.NewGuid(); + [Fact] + public void Should_deserialize_from_string() + { + var id = Guid.NewGuid(); - var source = new IdEntity<string> { Id = id.ToString() }; + var source = new IdEntity<string> { Id = id.ToString() }; - var actual = SerializeAndDeserializeBson<IdEntity<string>, IdEntity<DomainId>>(source); + var actual = SerializeAndDeserializeBson<IdEntity<string>, IdEntity<DomainId>>(source); - Assert.Equal(actual.Id.ToString(), id.ToString()); - } + Assert.Equal(actual.Id.ToString(), id.ToString()); + } - [Fact] - public void Should_deserialize_from_guid_string() - { - var id = Guid.NewGuid(); + [Fact] + public void Should_deserialize_from_guid_string() + { + var id = Guid.NewGuid(); - var source = new StringEntity<Guid> { Id = id }; + var source = new StringEntity<Guid> { Id = id }; - var actual = SerializeAndDeserializeBson<StringEntity<Guid>, IdEntity<DomainId>>(source); + var actual = SerializeAndDeserializeBson<StringEntity<Guid>, IdEntity<DomainId>>(source); - Assert.Equal(actual.Id.ToString(), id.ToString()); - } + Assert.Equal(actual.Id.ToString(), id.ToString()); + } - [Fact] - public void Should_deserialize_from_guid_bytes() - { - var id = Guid.NewGuid(); + [Fact] + public void Should_deserialize_from_guid_bytes() + { + var id = Guid.NewGuid(); - var source = new IdEntity<Guid> { Id = id }; + var source = new IdEntity<Guid> { Id = id }; - var actual = SerializeAndDeserializeBson<IdEntity<Guid>, IdEntity<DomainId>>(source); + var actual = SerializeAndDeserializeBson<IdEntity<Guid>, IdEntity<DomainId>>(source); - Assert.Equal(actual.Id.ToString(), id.ToString()); - } + Assert.Equal(actual.Id.ToString(), id.ToString()); + } - private static TOut SerializeAndDeserializeBson<TIn, TOut>(TIn source) - { - var stream = new MemoryStream(); + private static TOut SerializeAndDeserializeBson<TIn, TOut>(TIn source) + { + var stream = new MemoryStream(); - using (var writer = new BsonBinaryWriter(stream)) - { - BsonSerializer.Serialize(writer, source); + using (var writer = new BsonBinaryWriter(stream)) + { + BsonSerializer.Serialize(writer, source); - writer.Flush(); - } + writer.Flush(); + } - stream.Position = 0; + stream.Position = 0; - using (var reader = new BsonBinaryReader(stream)) - { - var target = BsonSerializer.Deserialize<TOut>(reader); + using (var reader = new BsonBinaryReader(stream)) + { + var target = BsonSerializer.Deserialize<TOut>(reader); - return target; - } + return target; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/Entities.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/Entities.cs index 9e3b40c8fb..c0cc46ad29 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/Entities.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/Entities.cs @@ -8,43 +8,42 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public static class Entities { - public static class Entities + public sealed class DateTimeEntity<T> + { + [BsonRepresentation(BsonType.DateTime)] + public T Value { get; set; } + } + + public sealed class Int64Entity<T> + { + [BsonRepresentation(BsonType.Int64)] + public T Value { get; set; } + } + + public sealed class Int32Entity<T> + { + [BsonRepresentation(BsonType.Int32)] + public T Value { get; set; } + } + + public sealed class StringEntity<T> + { + [BsonRepresentation(BsonType.String)] + public T Value { get; set; } + } + + public sealed class BinaryEntity<T> + { + [BsonRepresentation(BsonType.Binary)] + public T Value { get; set; } + } + + public sealed class DefaultEntity<T> { - public sealed class DateTimeEntity<T> - { - [BsonRepresentation(BsonType.DateTime)] - public T Value { get; set; } - } - - public sealed class Int64Entity<T> - { - [BsonRepresentation(BsonType.Int64)] - public T Value { get; set; } - } - - public sealed class Int32Entity<T> - { - [BsonRepresentation(BsonType.Int32)] - public T Value { get; set; } - } - - public sealed class StringEntity<T> - { - [BsonRepresentation(BsonType.String)] - public T Value { get; set; } - } - - public sealed class BinaryEntity<T> - { - [BsonRepresentation(BsonType.Binary)] - public T Value { get; set; } - } - - public sealed class DefaultEntity<T> - { - public T Value { get; set; } - } + public T Value { get; set; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs index 6ab4cdcd84..ddea5bd07d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs @@ -9,70 +9,69 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public class InstantSerializerTests { - public class InstantSerializerTests + public InstantSerializerTests() { - public InstantSerializerTests() - { - TestUtils.SetupBson(); - } + TestUtils.SetupBson(); + } - [Fact] - public void Should_serialize_and_deserialize() + [Fact] + public void Should_serialize_and_deserialize() + { + var source = new Entities.DefaultEntity<Instant> { - var source = new Entities.DefaultEntity<Instant> - { - Value = GetTime() - }; + Value = GetTime() + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - Assert.Equal(source.Value, deserialized.Value); - } + Assert.Equal(source.Value, deserialized.Value); + } - [Fact] - public void Should_serialize_and_deserialize_as_string() + [Fact] + public void Should_serialize_and_deserialize_as_string() + { + var source = new Entities.StringEntity<Instant> { - var source = new Entities.StringEntity<Instant> - { - Value = GetTime() - }; + Value = GetTime() + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - Assert.Equal(source.Value, deserialized.Value); - } + Assert.Equal(source.Value, deserialized.Value); + } - [Fact] - public void Should_serialize_and_deserialize_as_int64() + [Fact] + public void Should_serialize_and_deserialize_as_int64() + { + var source = new Entities.Int64Entity<Instant> { - var source = new Entities.Int64Entity<Instant> - { - Value = GetTime() - }; + Value = GetTime() + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - Assert.Equal(source.Value, deserialized.Value); - } + Assert.Equal(source.Value, deserialized.Value); + } - [Fact] - public void Should_serialize_and_deserialize_as_datetime() + [Fact] + public void Should_serialize_and_deserialize_as_datetime() + { + var source = new Entities.DateTimeEntity<Instant> { - var source = new Entities.DateTimeEntity<Instant> - { - Value = GetTime() - }; + Value = GetTime() + }; - var deserialized = source.SerializeAndDeserializeBson(); + var deserialized = source.SerializeAndDeserializeBson(); - Assert.Equal(source.Value, deserialized.Value); - } + Assert.Equal(source.Value, deserialized.Value); + } - private static Instant GetTime() - { - return SystemClock.Instance.GetCurrentInstant().WithoutNs(); - } + private static Instant GetTime() + { + return SystemClock.Instance.GetCurrentInstant().WithoutNs(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoFieldTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoFieldTests.cs index 5487156bd7..06e0094fc2 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoFieldTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoFieldTests.cs @@ -8,76 +8,75 @@ using MongoDB.Bson.Serialization.Attributes; using Xunit; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public class MongoFieldTests { - public class MongoFieldTests + public class Entity { - public class Entity - { - public string Id { get; set; } + public string Id { get; set; } - public string Default { get; set; } + public string Default { get; set; } - [BsonElement("_c")] - public string Custom { get; set; } - } + [BsonElement("_c")] + public string Custom { get; set; } + } - public sealed class Inherited : Entity - { - } + public sealed class Inherited : Entity + { + } - [Fact] - public void Should_resolve_id_field() - { - var name = Field.Of<Entity>(x => nameof(x.Id)); + [Fact] + public void Should_resolve_id_field() + { + var name = Field.Of<Entity>(x => nameof(x.Id)); - Assert.Equal("_id", name); - } + Assert.Equal("_id", name); + } - [Fact] - public void Should_resolve_id_field_from_base_class() - { - var name = Field.Of<Inherited>(x => nameof(x.Id)); + [Fact] + public void Should_resolve_id_field_from_base_class() + { + var name = Field.Of<Inherited>(x => nameof(x.Id)); - Assert.Equal("_id", name); - } + Assert.Equal("_id", name); + } - [Fact] - public void Should_resolve_default_field() - { - var name = Field.Of<Entity>(x => nameof(x.Default)); + [Fact] + public void Should_resolve_default_field() + { + var name = Field.Of<Entity>(x => nameof(x.Default)); - Assert.Equal("Default", name); - } + Assert.Equal("Default", name); + } - [Fact] - public void Should_resolve_default_field_from_base_class() - { - var name = Field.Of<Inherited>(x => nameof(x.Default)); + [Fact] + public void Should_resolve_default_field_from_base_class() + { + var name = Field.Of<Inherited>(x => nameof(x.Default)); - Assert.Equal("Default", name); - } + Assert.Equal("Default", name); + } - [Fact] - public void Should_resolve_custom_field() - { - var name = Field.Of<Entity>(x => nameof(x.Custom)); + [Fact] + public void Should_resolve_custom_field() + { + var name = Field.Of<Entity>(x => nameof(x.Custom)); - Assert.Equal("_c", name); - } + Assert.Equal("_c", name); + } - [Fact] - public void Should_resolve_custom_field_from_base_class() - { - var name = Field.Of<Inherited>(x => nameof(x.Custom)); + [Fact] + public void Should_resolve_custom_field_from_base_class() + { + var name = Field.Of<Inherited>(x => nameof(x.Custom)); - Assert.Equal("_c", name); - } + Assert.Equal("_c", name); + } - [Fact] - public void Should_throw_exception_if_field_not_found() - { - Assert.Throws<InvalidOperationException>(() => Field.Of<Entity>(x => "invalid")); - } + [Fact] + public void Should_throw_exception_if_field_not_found() + { + Assert.Throws<InvalidOperationException>(() => Field.Of<Entity>(x => "invalid")); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs index 5e7ab26979..2707628a68 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs @@ -17,339 +17,338 @@ using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter; using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public class MongoQueryTests { - public class MongoQueryTests + public class TestEntity { - public class TestEntity - { - public DomainId Id { get; set; } + public DomainId Id { get; set; } - public Instant Created { get; set; } + public Instant Created { get; set; } - public RefToken CreatedBy { get; set; } + public RefToken CreatedBy { get; set; } - public string Text { get; set; } + public string Text { get; set; } - public long Version { get; set; } - } + public long Version { get; set; } + } - static MongoQueryTests() - { - TestUtils.SetupBson(); - } + static MongoQueryTests() + { + TestUtils.SetupBson(); + } - [Fact] - public void Should_not_throw_exception_for_invalid_field() - { - var filter = ClrFilter.Eq("invalid", "Value"); + [Fact] + public void Should_not_throw_exception_for_invalid_field() + { + var filter = ClrFilter.Eq("invalid", "Value"); - AssertQuery("{ 'invalid' : 'Value' }", filter); - } + AssertQuery("{ 'invalid' : 'Value' }", filter); + } - [Fact] - public void Should_make_query_with_id_guid() - { - var id = Guid.NewGuid(); + [Fact] + public void Should_make_query_with_id_guid() + { + var id = Guid.NewGuid(); - var filter = ClrFilter.Eq("Id", id); + var filter = ClrFilter.Eq("Id", id); - AssertQuery("{ '_id' : '[value]' }", filter, id); - } + AssertQuery("{ '_id' : '[value]' }", filter, id); + } - [Fact] - public void Should_make_query_with_id_string() - { - var id = DomainId.NewGuid().ToString(); + [Fact] + public void Should_make_query_with_id_string() + { + var id = DomainId.NewGuid().ToString(); - var filter = ClrFilter.Eq("Id", id); + var filter = ClrFilter.Eq("Id", id); - AssertQuery("{ '_id' : '[value]' }", filter, id); - } + AssertQuery("{ '_id' : '[value]' }", filter, id); + } - [Fact] - public void Should_make_query_with_id_guid_list() - { - var id = Guid.NewGuid(); + [Fact] + public void Should_make_query_with_id_guid_list() + { + var id = Guid.NewGuid(); - var filter = ClrFilter.In("Id", new List<Guid> { id }); + var filter = ClrFilter.In("Id", new List<Guid> { id }); - AssertQuery("{ '_id' : { '$in' : ['[value]'] } }", filter, id); - } + AssertQuery("{ '_id' : { '$in' : ['[value]'] } }", filter, id); + } - [Fact] - public void Should_make_query_with_id_string_list() - { - var id = DomainId.NewGuid().ToString(); + [Fact] + public void Should_make_query_with_id_string_list() + { + var id = DomainId.NewGuid().ToString(); - var filter = ClrFilter.In("Id", new List<string> { id }); + var filter = ClrFilter.In("Id", new List<string> { id }); - AssertQuery("{ '_id' : { '$in' : ['[value]'] } }", filter, id); - } + AssertQuery("{ '_id' : { '$in' : ['[value]'] } }", filter, id); + } - [Fact] - public void Should_make_query_with_instant() - { - var time = "1988-01-19T12:00:00Z"; + [Fact] + public void Should_make_query_with_instant() + { + var time = "1988-01-19T12:00:00Z"; - var filter = ClrFilter.Eq("Version", InstantPattern.ExtendedIso.Parse(time).Value); + var filter = ClrFilter.Eq("Version", InstantPattern.ExtendedIso.Parse(time).Value); - AssertQuery("{ 'Version' : ISODate('[value]') }", filter, time); - } + AssertQuery("{ 'Version' : ISODate('[value]') }", filter, time); + } - [Fact] - public void Should_make_query_with_reftoken() - { - var filter = ClrFilter.Eq("CreatedBy", "subject:me"); + [Fact] + public void Should_make_query_with_reftoken() + { + var filter = ClrFilter.Eq("CreatedBy", "subject:me"); - AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); - } + AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); + } - [Fact] - public void Should_make_query_with_reftoken_cleanup() - { - var filter = ClrFilter.Eq("CreatedBy", "me"); + [Fact] + public void Should_make_query_with_reftoken_cleanup() + { + var filter = ClrFilter.Eq("CreatedBy", "me"); - AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); - } + AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); + } - [Fact] - public void Should_make_query_with_reftoken_fix() - { - var filter = ClrFilter.Eq("CreatedBy", "user:me"); + [Fact] + public void Should_make_query_with_reftoken_fix() + { + var filter = ClrFilter.Eq("CreatedBy", "user:me"); - AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); - } + AssertQuery("{ 'CreatedBy' : 'subject:me' }", filter); + } - [Fact] - public void Should_make_query_with_number() - { - var filter = ClrFilter.Eq("Version", 0L); + [Fact] + public void Should_make_query_with_number() + { + var filter = ClrFilter.Eq("Version", 0L); - AssertQuery("{ 'Version' : NumberLong(0) }", filter); - } + AssertQuery("{ 'Version' : NumberLong(0) }", filter); + } - [Fact] - public void Should_make_query_with_number_and_list() - { - var filter = ClrFilter.In("Version", new List<long> { 0L, 2L, 5L }); + [Fact] + public void Should_make_query_with_number_and_list() + { + var filter = ClrFilter.In("Version", new List<long> { 0L, 2L, 5L }); - AssertQuery("{ 'Version' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }", filter); - } + AssertQuery("{ 'Version' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }", filter); + } - [Fact] - public void Should_make_query_with_contains_and_null_value() - { - var filter = ClrFilter.Contains("Text", null!); + [Fact] + public void Should_make_query_with_contains_and_null_value() + { + var filter = ClrFilter.Contains("Text", null!); - AssertQuery("{ 'Text' : /null/i }", filter); - } + AssertQuery("{ 'Text' : /null/i }", filter); + } - [Fact] - public void Should_make_query_with_contains() - { - var filter = ClrFilter.Contains("Text", "search"); + [Fact] + public void Should_make_query_with_contains() + { + var filter = ClrFilter.Contains("Text", "search"); - AssertQuery("{ 'Text' : /search/i }", filter); - } + AssertQuery("{ 'Text' : /search/i }", filter); + } - [Fact] - public void Should_make_query_with_contains_and_invalid_character() - { - var filter = ClrFilter.Contains("Text", "search("); + [Fact] + public void Should_make_query_with_contains_and_invalid_character() + { + var filter = ClrFilter.Contains("Text", "search("); - AssertQuery("{ 'Text' : /search\\(/i }", filter); - } + AssertQuery("{ 'Text' : /search\\(/i }", filter); + } - [Fact] - public void Should_make_query_with_endswith_and_null_value() - { - var filter = ClrFilter.EndsWith("Text", null!); + [Fact] + public void Should_make_query_with_endswith_and_null_value() + { + var filter = ClrFilter.EndsWith("Text", null!); - AssertQuery("{ 'Text' : /null$/i }", filter); - } + AssertQuery("{ 'Text' : /null$/i }", filter); + } - [Fact] - public void Should_make_query_with_endswith() - { - var filter = ClrFilter.EndsWith("Text", "search"); + [Fact] + public void Should_make_query_with_endswith() + { + var filter = ClrFilter.EndsWith("Text", "search"); - AssertQuery("{ 'Text' : /search$/i }", filter); - } + AssertQuery("{ 'Text' : /search$/i }", filter); + } - [Fact] - public void Should_make_query_with_endswithand_invalid_character() - { - var filter = ClrFilter.EndsWith("Text", "search("); + [Fact] + public void Should_make_query_with_endswithand_invalid_character() + { + var filter = ClrFilter.EndsWith("Text", "search("); - AssertQuery("{ 'Text' : /search\\($/i }", filter); - } + AssertQuery("{ 'Text' : /search\\($/i }", filter); + } - [Fact] - public void Should_make_query_with_startswith_and_null_value() - { - var filter = ClrFilter.StartsWith("Text", null!); + [Fact] + public void Should_make_query_with_startswith_and_null_value() + { + var filter = ClrFilter.StartsWith("Text", null!); - AssertQuery("{ 'Text' : /^null/i }", filter); - } + AssertQuery("{ 'Text' : /^null/i }", filter); + } - [Fact] - public void Should_make_query_with_startswith() - { - var filter = ClrFilter.StartsWith("Text", "search"); + [Fact] + public void Should_make_query_with_startswith() + { + var filter = ClrFilter.StartsWith("Text", "search"); - AssertQuery("{ 'Text' : /^search/i }", filter); - } + AssertQuery("{ 'Text' : /^search/i }", filter); + } - [Fact] - public void Should_make_query_with_startswith_and_invalid_character() - { - var filter = ClrFilter.StartsWith("Text", "search("); + [Fact] + public void Should_make_query_with_startswith_and_invalid_character() + { + var filter = ClrFilter.StartsWith("Text", "search("); - AssertQuery("{ 'Text' : /^search\\(/i }", filter); - } + AssertQuery("{ 'Text' : /^search\\(/i }", filter); + } - [Fact] - public void Should_make_query_with_matchs_and_null_value() - { - var filter = ClrFilter.Matchs("Text", null!); + [Fact] + public void Should_make_query_with_matchs_and_null_value() + { + var filter = ClrFilter.Matchs("Text", null!); - AssertQuery("{ 'Text' : /null/i }", filter); - } + AssertQuery("{ 'Text' : /null/i }", filter); + } - [Fact] - public void Should_make_query_with_matchs() - { - var filter = ClrFilter.Matchs("Text", "^search$"); + [Fact] + public void Should_make_query_with_matchs() + { + var filter = ClrFilter.Matchs("Text", "^search$"); - AssertQuery("{ 'Text' : /^search$/i }", filter); - } + AssertQuery("{ 'Text' : /^search$/i }", filter); + } - [Fact] - public void Should_make_query_with_matchs_and_regex_syntax() - { - var filter = ClrFilter.Matchs("Text", "/search/i"); + [Fact] + public void Should_make_query_with_matchs_and_regex_syntax() + { + var filter = ClrFilter.Matchs("Text", "/search/i"); - AssertQuery("{ 'Text' : /search/i }", filter); - } + AssertQuery("{ 'Text' : /search/i }", filter); + } - [Fact] - public void Should_make_query_with_matchs_and_regex_case_sensitive_syntax() - { - var filter = ClrFilter.Matchs("Text", "/search/"); + [Fact] + public void Should_make_query_with_matchs_and_regex_case_sensitive_syntax() + { + var filter = ClrFilter.Matchs("Text", "/search/"); - AssertQuery("{ 'Text' : /search/ }", filter); - } + AssertQuery("{ 'Text' : /search/ }", filter); + } - [Fact] - public void Should_make_query_with_empty_for_class() - { - var filter = ClrFilter.Empty("Text"); + [Fact] + public void Should_make_query_with_empty_for_class() + { + var filter = ClrFilter.Empty("Text"); - AssertQuery("{ '$or' : [{ 'Text' : { '$exists' : false } }, { 'Text' : null }, { 'Text' : '' }, { 'Text' : { '$size' : 0 } }] }", filter); - } + AssertQuery("{ '$or' : [{ 'Text' : { '$exists' : false } }, { 'Text' : null }, { 'Text' : '' }, { 'Text' : { '$size' : 0 } }] }", filter); + } - [Fact] - public void Should_make_query_with_exists() - { - var filter = ClrFilter.Exists("Text"); + [Fact] + public void Should_make_query_with_exists() + { + var filter = ClrFilter.Exists("Text"); - AssertQuery("{ 'Text' : { '$exists' : true, '$ne' : null } }", filter); - } + AssertQuery("{ 'Text' : { '$exists' : true, '$ne' : null } }", filter); + } - [Fact] - public void Should_make_query_with_full_text() - { - var query = new ClrQuery { FullText = "Hello my World" }; + [Fact] + public void Should_make_query_with_full_text() + { + var query = new ClrQuery { FullText = "Hello my World" }; - AssertQuery(query, "{ '$text' : { '$search' : 'Hello my World' } }"); - } + AssertQuery(query, "{ '$text' : { '$search' : 'Hello my World' } }"); + } - [Fact] - public void Should_make_orderby_with_single_field() - { - var sorting = SortBuilder.Descending("Number"); + [Fact] + public void Should_make_orderby_with_single_field() + { + var sorting = SortBuilder.Descending("Number"); - AssertSorting("{ 'Number' : -1 }", sorting); - } + AssertSorting("{ 'Number' : -1 }", sorting); + } - [Fact] - public void Should_make_orderby_with_multiple_fields() - { - var sorting1 = SortBuilder.Ascending("Number"); - var sorting2 = SortBuilder.Descending("Text"); + [Fact] + public void Should_make_orderby_with_multiple_fields() + { + var sorting1 = SortBuilder.Ascending("Number"); + var sorting2 = SortBuilder.Descending("Text"); - AssertSorting("{ 'Number' : 1, 'Text' : -1 }", sorting1, sorting2); - } + AssertSorting("{ 'Number' : 1, 'Text' : -1 }", sorting1, sorting2); + } - [Fact] - public void Should_make_take_statement() - { - var query = new ClrQuery { Take = 3 }; + [Fact] + public void Should_make_take_statement() + { + var query = new ClrQuery { Take = 3 }; - var cursor = A.Fake<IFindFluent<TestEntity, TestEntity>>(); + var cursor = A.Fake<IFindFluent<TestEntity, TestEntity>>(); - cursor.QueryLimit(query); + cursor.QueryLimit(query); - A.CallTo(() => cursor.Limit(3)) - .MustHaveHappened(); - } + A.CallTo(() => cursor.Limit(3)) + .MustHaveHappened(); + } - [Fact] - public void Should_make_skip_statement() - { - var query = new ClrQuery { Skip = 3 }; + [Fact] + public void Should_make_skip_statement() + { + var query = new ClrQuery { Skip = 3 }; - var cursor = A.Fake<IFindFluent<TestEntity, TestEntity>>(); + var cursor = A.Fake<IFindFluent<TestEntity, TestEntity>>(); - cursor.QuerySkip(query); + cursor.QuerySkip(query); - A.CallTo(() => cursor.Skip(3)) - .MustHaveHappened(); - } + A.CallTo(() => cursor.Skip(3)) + .MustHaveHappened(); + } - private static void AssertQuery(string expected, FilterNode<ClrValue> filter, object? arg = null) - { - AssertQuery(new ClrQuery { Filter = filter }, expected, arg); - } + private static void AssertQuery(string expected, FilterNode<ClrValue> filter, object? arg = null) + { + AssertQuery(new ClrQuery { Filter = filter }, expected, arg); + } - private static void AssertQuery(ClrQuery query, string expected, object? arg = null) - { - var filter = query.BuildFilter<TestEntity>().Filter!; + private static void AssertQuery(ClrQuery query, string expected, object? arg = null) + { + var filter = query.BuildFilter<TestEntity>().Filter!; - var rendered = - filter.Render( - BsonSerializer.SerializerRegistry.GetSerializer<TestEntity>(), - BsonSerializer.SerializerRegistry) - .ToString(); + var rendered = + filter.Render( + BsonSerializer.SerializerRegistry.GetSerializer<TestEntity>(), + BsonSerializer.SerializerRegistry) + .ToString(); - Assert.Equal(Cleanup(expected, arg), rendered); - } + Assert.Equal(Cleanup(expected, arg), rendered); + } - private static void AssertSorting(string expected, params SortNode[] sort) - { - var cursor = A.Fake<IFindFluent<TestEntity, TestEntity>>(); + private static void AssertSorting(string expected, params SortNode[] sort) + { + var cursor = A.Fake<IFindFluent<TestEntity, TestEntity>>(); - var rendered = string.Empty; + var rendered = string.Empty; - A.CallTo(() => cursor.Sort(A<SortDefinition<TestEntity>>._)) - .Invokes((SortDefinition<TestEntity> sortDefinition) => - { - rendered = - sortDefinition.Render( - BsonSerializer.SerializerRegistry.GetSerializer<TestEntity>(), - BsonSerializer.SerializerRegistry) - .ToString(); - }); + A.CallTo(() => cursor.Sort(A<SortDefinition<TestEntity>>._)) + .Invokes((SortDefinition<TestEntity> sortDefinition) => + { + rendered = + sortDefinition.Render( + BsonSerializer.SerializerRegistry.GetSerializer<TestEntity>(), + BsonSerializer.SerializerRegistry) + .ToString(); + }); - cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }); + cursor.QuerySort(new ClrQuery { Sort = sort.ToList() }); - Assert.Equal(Cleanup(expected), rendered); - } + Assert.Equal(Cleanup(expected), rendered); + } - private static string Cleanup(string filter, object? arg = null) - { - return filter.Replace('\'', '"').Replace("[value]", arg?.ToString(), StringComparison.Ordinal); - } + private static string Cleanup(string filter, object? arg = null) + { + return filter.Replace('\'', '"').Replace("[value]", arg?.ToString(), StringComparison.Ordinal); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs index 4a46da085e..69a949e399 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs @@ -9,105 +9,104 @@ using MongoDB.Bson.Serialization; using Xunit; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure.MongoDb; + +public class TypeConverterStringSerializerTests { - public class TypeConverterStringSerializerTests + private sealed record ValueHolder<T> { - private sealed record ValueHolder<T> - { - public T Value { get; set; } - } + public T Value { get; set; } + } - public TypeConverterStringSerializerTests() - { - BsonStringSerializer<TimeSpan>.Register(); - BsonStringSerializer<RefToken>.Register(); - } + public TypeConverterStringSerializerTests() + { + BsonStringSerializer<TimeSpan>.Register(); + BsonStringSerializer<RefToken>.Register(); + } - [Fact] - public void Should_serialize_struct() + [Fact] + public void Should_serialize_struct() + { + var source = new ValueHolder<TimeSpan> { - var source = new ValueHolder<TimeSpan> - { - Value = TimeSpan.Zero - }; + Value = TimeSpan.Zero + }; - var deserialized = SerializeAndDeserializeBson(source); + var deserialized = SerializeAndDeserializeBson(source); - Assert.Equal(source, deserialized); - } + Assert.Equal(source, deserialized); + } - [Fact] - public void Should_serialize_nullable_struct() + [Fact] + public void Should_serialize_nullable_struct() + { + var source = new ValueHolder<TimeSpan?> { - var source = new ValueHolder<TimeSpan?> - { - Value = TimeSpan.Zero - }; + Value = TimeSpan.Zero + }; - var deserialized = SerializeAndDeserializeBson(source); + var deserialized = SerializeAndDeserializeBson(source); - Assert.Equal(source, deserialized); - } + Assert.Equal(source, deserialized); + } - [Fact] - public void Should_serialize_nullable_null_struct() + [Fact] + public void Should_serialize_nullable_null_struct() + { + var source = new ValueHolder<TimeSpan?> { - var source = new ValueHolder<TimeSpan?> - { - Value = null - }; + Value = null + }; - var deserialized = SerializeAndDeserializeBson(source); + var deserialized = SerializeAndDeserializeBson(source); - Assert.Equal(source, deserialized); - } + Assert.Equal(source, deserialized); + } - [Fact] - public void Should_serialize_class() + [Fact] + public void Should_serialize_class() + { + var source = new ValueHolder<RefToken> { - var source = new ValueHolder<RefToken> - { - Value = RefToken.Client("client") - }; + Value = RefToken.Client("client") + }; - var deserialized = SerializeAndDeserializeBson(source); + var deserialized = SerializeAndDeserializeBson(source); - Assert.Equal(source, deserialized); - } + Assert.Equal(source, deserialized); + } - [Fact] - public void Should_serialize_null_class() + [Fact] + public void Should_serialize_null_class() + { + var source = new ValueHolder<RefToken?> { - var source = new ValueHolder<RefToken?> - { - Value = null - }; + Value = null + }; - var deserialized = SerializeAndDeserializeBson(source); + var deserialized = SerializeAndDeserializeBson(source); - Assert.Equal(source, deserialized); - } + Assert.Equal(source, deserialized); + } - private static T SerializeAndDeserializeBson<T>(T value) - { - var stream = new MemoryStream(); + private static T SerializeAndDeserializeBson<T>(T value) + { + var stream = new MemoryStream(); - using (var writer = new BsonBinaryWriter(stream)) - { - BsonSerializer.Serialize(writer, value); + using (var writer = new BsonBinaryWriter(stream)) + { + BsonSerializer.Serialize(writer, value); - writer.Flush(); - } + writer.Flush(); + } - stream.Position = 0; + stream.Position = 0; - using (var reader = new BsonBinaryReader(stream)) - { - var actual = BsonSerializer.Deserialize<T>(reader); + using (var reader = new BsonBinaryReader(stream)) + { + var actual = BsonSerializer.Deserialize<T>(reader); - return actual; - } + return actual; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs b/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs index d82c22306e..0b952faeb8 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs @@ -10,161 +10,160 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class NamedIdTests { - public class NamedIdTests + internal sealed record Wrapper { - internal sealed record Wrapper - { - [JsonConverter(typeof(StringConverter<NamedId<long>>))] - public NamedId<long> Value { get; set; } - } + [JsonConverter(typeof(StringConverter<NamedId<long>>))] + public NamedId<long> Value { get; set; } + } - [Fact] - public void Should_instantiate_token() - { - var id = Guid.NewGuid(); + [Fact] + public void Should_instantiate_token() + { + var id = Guid.NewGuid(); - var namedId = NamedId.Of(id, "my-name"); + var namedId = NamedId.Of(id, "my-name"); - Assert.Equal(id, namedId.Id); - Assert.Equal("my-name", namedId.Name); - } + Assert.Equal(id, namedId.Id); + Assert.Equal("my-name", namedId.Name); + } - [Fact] - public void Should_convert_named_id_to_string() - { - var id = Guid.NewGuid(); + [Fact] + public void Should_convert_named_id_to_string() + { + var id = Guid.NewGuid(); - var namedId = NamedId.Of(id, "my-name"); + var namedId = NamedId.Of(id, "my-name"); - Assert.Equal($"{id},my-name", namedId.ToString()); - } + Assert.Equal($"{id},my-name", namedId.ToString()); + } - [Fact] - public void Should_serialize_and_deserialize_null_guid_token() - { - NamedId<Guid>? value = null; + [Fact] + public void Should_serialize_and_deserialize_null_guid_token() + { + NamedId<Guid>? value = null; - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_valid_guid_token() - { - var value = NamedId.Of(Guid.NewGuid(), "my-name"); + [Fact] + public void Should_serialize_and_deserialize_valid_guid_token() + { + var value = NamedId.Of(Guid.NewGuid(), "my-name"); - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_null_long_token() - { - NamedId<long>? value = null; + [Fact] + public void Should_serialize_and_deserialize_null_long_token() + { + NamedId<long>? value = null; - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_valid_long_token() - { - var value = NamedId.Of(123L, "my-name"); + [Fact] + public void Should_serialize_and_deserialize_valid_long_token() + { + var value = NamedId.Of(123L, "my-name"); - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_null_string_token() - { - NamedId<string>? value = null; + [Fact] + public void Should_serialize_and_deserialize_null_string_token() + { + NamedId<string>? value = null; - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_valid_string_token() - { - var value = NamedId.Of(Guid.NewGuid().ToString(), "my-name"); + [Fact] + public void Should_serialize_and_deserialize_valid_string_token() + { + var value = NamedId.Of(Guid.NewGuid().ToString(), "my-name"); - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_null_id_token() - { - NamedId<DomainId>? value = null; + [Fact] + public void Should_serialize_and_deserialize_null_id_token() + { + NamedId<DomainId>? value = null; - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_valid_id_token() - { - var value = NamedId.Of(DomainId.NewGuid().ToString(), "my-name"); + [Fact] + public void Should_serialize_and_deserialize_valid_id_token() + { + var value = NamedId.Of(DomainId.NewGuid().ToString(), "my-name"); - var serialized = value.SerializeAndDeserialize(); + var serialized = value.SerializeAndDeserialize(); - Assert.Equal(value, serialized); - } + Assert.Equal(value, serialized); + } - [Fact] - public void Should_serialize_and_deserialize_old_object() - { - var value = new { id = 42L, name = "my-name" }; + [Fact] + public void Should_serialize_and_deserialize_old_object() + { + var value = new { id = 42L, name = "my-name" }; - var serialized = value.SerializeAndDeserialize<NamedId<long>, object>(); + var serialized = value.SerializeAndDeserialize<NamedId<long>, object>(); - Assert.Equal(NamedId.Of(42L, "my-name"), serialized); - } + Assert.Equal(NamedId.Of(42L, "my-name"), serialized); + } - [Fact] - public void Should_deserialize_from_old_object_with_explicit_converter() + [Fact] + public void Should_deserialize_from_old_object_with_explicit_converter() + { + var value = new { - var value = new - { - value = new { id = 42, name = "my-name" } - }; + value = new { id = 42, name = "my-name" } + }; - var expected = new Wrapper - { - Value = NamedId.Of(42L, "my-name") - }; + var expected = new Wrapper + { + Value = NamedId.Of(42L, "my-name") + }; - var serialized = value.SerializeAndDeserialize<Wrapper, object>(); + var serialized = value.SerializeAndDeserialize<Wrapper, object>(); - Assert.Equal(expected, serialized); - } + Assert.Equal(expected, serialized); + } - [Fact] - public void Should_throw_exception_if_string_id_is_not_valid() - { - Assert.ThrowsAny<Exception>(() => TestUtils.Deserialize<NamedId<string>>("123")); - } + [Fact] + public void Should_throw_exception_if_string_id_is_not_valid() + { + Assert.ThrowsAny<Exception>(() => TestUtils.Deserialize<NamedId<string>>("123")); + } - [Fact] - public void Should_throw_exception_if_long_id_is_not_valid() - { - Assert.ThrowsAny<Exception>(() => TestUtils.Deserialize<NamedId<long>>("invalid-long,name")); - } + [Fact] + public void Should_throw_exception_if_long_id_is_not_valid() + { + Assert.ThrowsAny<Exception>(() => TestUtils.Deserialize<NamedId<long>>("invalid-long,name")); + } - [Fact] - public void Should_throw_exception_if_guid_id_is_not_valid() - { - Assert.ThrowsAny<Exception>(() => TestUtils.Deserialize<NamedId<Guid>>("invalid-guid,name")); - } + [Fact] + public void Should_throw_exception_if_guid_id_is_not_valid() + { + Assert.ThrowsAny<Exception>(() => TestUtils.Deserialize<NamedId<Guid>>("invalid-guid,name")); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs index 1b1ea3451c..fa40123b75 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Net/IPAddressComparerTests.cs @@ -8,36 +8,35 @@ using System.Net; using Xunit; -namespace Squidex.Infrastructure.Net +namespace Squidex.Infrastructure.Net; + +public class IPAddressComparerTests { - public class IPAddressComparerTests + [Fact] + public void Should_sort_ip_addresses() { - [Fact] - public void Should_sort_ip_addresses() + var source = new[] { - var source = new[] - { - IPAddress.IPv6Loopback, - IPAddress.Parse("127.0.0.200"), - IPAddress.Parse("127.0.0.255"), - IPAddress.Parse("129.0.0.1"), - IPAddress.Parse("127.0.0.1"), - IPAddress.Parse("127.0.0.200") - }; + IPAddress.IPv6Loopback, + IPAddress.Parse("127.0.0.200"), + IPAddress.Parse("127.0.0.255"), + IPAddress.Parse("129.0.0.1"), + IPAddress.Parse("127.0.0.1"), + IPAddress.Parse("127.0.0.200") + }; - var sorted = source.OrderBy(x => x, IPAddressComparer.Instance); + var sorted = source.OrderBy(x => x, IPAddressComparer.Instance); - var expected = new[] - { - IPAddress.Parse("127.0.0.1"), - IPAddress.Parse("127.0.0.200"), - IPAddress.Parse("127.0.0.200"), - IPAddress.Parse("127.0.0.255"), - IPAddress.Parse("129.0.0.1"), - IPAddress.IPv6Loopback - }; + var expected = new[] + { + IPAddress.Parse("127.0.0.1"), + IPAddress.Parse("127.0.0.200"), + IPAddress.Parse("127.0.0.200"), + IPAddress.Parse("127.0.0.255"), + IPAddress.Parse("129.0.0.1"), + IPAddress.IPv6Loopback + }; - Assert.Equal(expected, sorted); - } + Assert.Equal(expected, sorted); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/FilterSchemaTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/FilterSchemaTests.cs index 1c69d12eb3..175bb2c61a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/FilterSchemaTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/FilterSchemaTests.cs @@ -8,176 +8,175 @@ using Squidex.Infrastructure.Collections; using Xunit; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public class FilterSchemaTests { - public class FilterSchemaTests + [Fact] + public void Should_flatten_schema() { - [Fact] - public void Should_flatten_schema() + var schema = new FilterSchema(FilterSchemaType.Object) { - var schema = new FilterSchema(FilterSchemaType.Object) + Fields = new[] { - Fields = new[] + new FilterField(new FilterSchema(FilterSchemaType.Object) { - new FilterField(new FilterSchema(FilterSchemaType.Object) + Fields = new[] { - Fields = new[] + new FilterField(new FilterSchema(FilterSchemaType.Object) { - new FilterField(new FilterSchema(FilterSchemaType.Object) + Fields = new[] { - Fields = new[] - { - new FilterField(FilterSchema.Number, "nested3") - }.ToReadonlyList() - }, "nested2") - }.ToReadonlyList() - }, "nested1") - }.ToReadonlyList() - }; - - var expected = new FilterSchema(FilterSchemaType.Object) + new FilterField(FilterSchema.Number, "nested3") + }.ToReadonlyList() + }, "nested2") + }.ToReadonlyList() + }, "nested1") + }.ToReadonlyList() + }; + + var expected = new FilterSchema(FilterSchemaType.Object) + { + Fields = new[] { - Fields = new[] - { - new FilterField(new FilterSchema(FilterSchemaType.Object), "nested1"), - new FilterField(new FilterSchema(FilterSchemaType.Object), "nested1.nested2"), - new FilterField(new FilterSchema(FilterSchemaType.Number), "nested1.nested2.nested3") - }.ToReadonlyList() - }; + new FilterField(new FilterSchema(FilterSchemaType.Object), "nested1"), + new FilterField(new FilterSchema(FilterSchemaType.Object), "nested1.nested2"), + new FilterField(new FilterSchema(FilterSchemaType.Number), "nested1.nested2.nested3") + }.ToReadonlyList() + }; - var actual = schema.Flatten(); + var actual = schema.Flatten(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_ignore_conflicts_when_flatten() + [Fact] + public void Should_ignore_conflicts_when_flatten() + { + var schema = new FilterSchema(FilterSchemaType.Object) { - var schema = new FilterSchema(FilterSchemaType.Object) + Fields = new[] { - Fields = new[] - { - new FilterField(FilterSchema.Number, "property1"), - new FilterField(FilterSchema.String, "property1"), - new FilterField(FilterSchema.String, "property2"), - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; - - var expected = new FilterSchema(FilterSchemaType.Object) + new FilterField(FilterSchema.Number, "property1"), + new FilterField(FilterSchema.String, "property1"), + new FilterField(FilterSchema.String, "property2"), + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; + + var expected = new FilterSchema(FilterSchemaType.Object) + { + Fields = new[] { - Fields = new[] - { - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; - var actual = schema.Flatten(); + var actual = schema.Flatten(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_remove_Descriptions_for_merged_fields() + [Fact] + public void Should_remove_Descriptions_for_merged_fields() + { + var schema = new FilterSchema(FilterSchemaType.Object) { - var schema = new FilterSchema(FilterSchemaType.Object) + Fields = new[] { - Fields = new[] - { - new FilterField(FilterSchema.String, "property1", "Description1"), - new FilterField(FilterSchema.String, "property2", "Description2"), - new FilterField(FilterSchema.String, "property2", "Description3") - }.ToReadonlyList() - }; + new FilterField(FilterSchema.String, "property1", "Description1"), + new FilterField(FilterSchema.String, "property2", "Description2"), + new FilterField(FilterSchema.String, "property2", "Description3") + }.ToReadonlyList() + }; - var expected = new FilterSchema(FilterSchemaType.Object) + var expected = new FilterSchema(FilterSchemaType.Object) + { + Fields = new[] { - Fields = new[] - { - new FilterField(FilterSchema.String, "property1", "Description1"), - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; + new FilterField(FilterSchema.String, "property1", "Description1"), + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; - var actual = schema.Flatten(); + var actual = schema.Flatten(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_filter_out_fields_by_predicate_when_flatten() + [Fact] + public void Should_filter_out_fields_by_predicate_when_flatten() + { + var schema = new FilterSchema(FilterSchemaType.Object) { - var schema = new FilterSchema(FilterSchemaType.Object) + Fields = new[] { - Fields = new[] - { - new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"), - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; + new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"), + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; - var expected = new FilterSchema(FilterSchemaType.Object) + var expected = new FilterSchema(FilterSchemaType.Object) + { + Fields = new[] { - Fields = new[] - { - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; - var actual = schema.Flatten(predicate: x => x.Type != FilterSchemaType.Object); + var actual = schema.Flatten(predicate: x => x.Type != FilterSchemaType.Object); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_filter_out_fields_without_operators_when_flattened_by_model() + [Fact] + public void Should_filter_out_fields_without_operators_when_flattened_by_model() + { + var schema = new FilterSchema(FilterSchemaType.Object) { - var schema = new FilterSchema(FilterSchemaType.Object) + Fields = new[] { - Fields = new[] - { - new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"), - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; + new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"), + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; - var expected = new FilterSchema(FilterSchemaType.Object) + var expected = new FilterSchema(FilterSchemaType.Object) + { + Fields = new[] { - Fields = new[] - { - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; - var actual = new QueryModel { Schema = schema }.Flatten().Schema; + var actual = new QueryModel { Schema = schema }.Flatten().Schema; - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void Should_not_filter_out_fields_without_operators_when_flattened_by_model_but_flag_is_false() + [Fact] + public void Should_not_filter_out_fields_without_operators_when_flattened_by_model_but_flag_is_false() + { + var schema = new FilterSchema(FilterSchemaType.Object) { - var schema = new FilterSchema(FilterSchemaType.Object) + Fields = new[] { - Fields = new[] - { - new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"), - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; + new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"), + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; - var expected = new FilterSchema(FilterSchemaType.Object) + var expected = new FilterSchema(FilterSchemaType.Object) + { + Fields = new[] { - Fields = new[] - { - new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"), - new FilterField(FilterSchema.String, "property2") - }.ToReadonlyList() - }; + new FilterField(new FilterSchema(FilterSchemaType.Object), "property1"), + new FilterField(FilterSchema.String, "property2") + }.ToReadonlyList() + }; - var actual = new QueryModel { Schema = schema }.Flatten(onlyWithOperators: false).Schema; + var actual = new QueryModel { Schema = schema }.Flatten(onlyWithOperators: false).Schema; - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs index 5001788f19..d628e0a365 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs @@ -7,26 +7,25 @@ using Xunit; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public class PascalCasePathConverterTests { - public class PascalCasePathConverterTests + [Fact] + public void Should_convert_property() { - [Fact] - public void Should_convert_property() - { - var source = ClrFilter.Eq("property", 1); - var actual = PascalCasePathConverter<ClrValue>.Transform(source); + var source = ClrFilter.Eq("property", 1); + var actual = PascalCasePathConverter<ClrValue>.Transform(source); - Assert.Equal("Property == 1", actual!.ToString()); - } + Assert.Equal("Property == 1", actual!.ToString()); + } - [Fact] - public void Should_convert_properties() - { - var source = ClrFilter.Eq("root.child", 1); - var actual = PascalCasePathConverter<ClrValue>.Transform(source); + [Fact] + public void Should_convert_properties() + { + var source = ClrFilter.Eq("root.child", 1); + var actual = PascalCasePathConverter<ClrValue>.Transform(source); - Assert.Equal("Root.Child == 1", actual!.ToString()); - } + Assert.Equal("Root.Child == 1", actual!.ToString()); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/PropertyPathTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/PropertyPathTests.cs index a3900bee33..a94a93bd6c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/PropertyPathTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/PropertyPathTests.cs @@ -7,96 +7,95 @@ using Xunit; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public class PropertyPathTests { - public class PropertyPathTests + [Fact] + public void Should_create() + { + var path = new PropertyPath(new[] { "path", "to", "property" }); + + Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); + } + + [Fact] + public void Should_convert_to_string() + { + var path = new PropertyPath(new[] { "path", "to", "property" }); + + Assert.Equal("path.to.property", path.ToString()); + } + + [Fact] + public void Should_throw_exception_for_empty_path() + { + Assert.Throws<ArgumentException>(() => new PropertyPath(Array.Empty<string>())); + } + + [Fact] + public void Should_throw_exception_for_empty_path_from_string() + { + Assert.Throws<ArgumentException>(() => { PropertyPath p = string.Empty; }); + } + + [Fact] + public void Should_throw_exception_for_empty_path_from_null_string() + { + Assert.Throws<ArgumentException>(() => { PropertyPath p = (string)null!; }); + } + + [Fact] + public void Should_throw_exception_for_empty_path_from_list() + { + Assert.Throws<ArgumentException>(() => { PropertyPath p = new List<string>(); }); + } + + [Fact] + public void Should_create_from_dot_string() { - [Fact] - public void Should_create() - { - var path = new PropertyPath(new[] { "path", "to", "property" }); - - Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); - } - - [Fact] - public void Should_convert_to_string() - { - var path = new PropertyPath(new[] { "path", "to", "property" }); - - Assert.Equal("path.to.property", path.ToString()); - } - - [Fact] - public void Should_throw_exception_for_empty_path() - { - Assert.Throws<ArgumentException>(() => new PropertyPath(Array.Empty<string>())); - } - - [Fact] - public void Should_throw_exception_for_empty_path_from_string() - { - Assert.Throws<ArgumentException>(() => { PropertyPath p = string.Empty; }); - } - - [Fact] - public void Should_throw_exception_for_empty_path_from_null_string() - { - Assert.Throws<ArgumentException>(() => { PropertyPath p = (string)null!; }); - } - - [Fact] - public void Should_throw_exception_for_empty_path_from_list() - { - Assert.Throws<ArgumentException>(() => { PropertyPath p = new List<string>(); }); - } - - [Fact] - public void Should_create_from_dot_string() - { - PropertyPath path = "path.to.property"; - - Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); - } - - [Fact] - public void Should_create_from_broken_dot_string() - { - PropertyPath path = ".path...to...property."; - - Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); - } - - [Fact] - public void Should_create_from_slash_string() - { - PropertyPath path = "path/to/property"; - - Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); - } - - [Fact] - public void Should_create_from_broken_slash_string() - { - PropertyPath path = "/path///to///property/"; - - Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); - } - - [Fact] - public void Should_create_from_dot_string_and_escape() - { - PropertyPath path = "path.to.complex\\.property"; - - Assert.Equal(new[] { "path", "to", "complex.property" }, path.ToArray()); - } - - [Fact] - public void Should_create_from_slash_string_and_escape() - { - PropertyPath path = "path.to.complex\\/property"; - - Assert.Equal(new[] { "path", "to", "complex/property" }, path.ToArray()); - } + PropertyPath path = "path.to.property"; + + Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); + } + + [Fact] + public void Should_create_from_broken_dot_string() + { + PropertyPath path = ".path...to...property."; + + Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); + } + + [Fact] + public void Should_create_from_slash_string() + { + PropertyPath path = "path/to/property"; + + Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); + } + + [Fact] + public void Should_create_from_broken_slash_string() + { + PropertyPath path = "/path///to///property/"; + + Assert.Equal(new[] { "path", "to", "property" }, path.ToArray()); + } + + [Fact] + public void Should_create_from_dot_string_and_escape() + { + PropertyPath path = "path.to.complex\\.property"; + + Assert.Equal(new[] { "path", "to", "complex.property" }, path.ToArray()); + } + + [Fact] + public void Should_create_from_slash_string_and_escape() + { + PropertyPath path = "path.to.complex\\/property"; + + Assert.Equal(new[] { "path", "to", "complex/property" }, path.ToArray()); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs index 3ae6b7e5c5..c474b227f4 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs @@ -12,756 +12,755 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public sealed class QueryFromJsonTests { - public sealed class QueryFromJsonTests - { - private static readonly (string Name, string Operator, string Output)[] AllOps = - { - ("Contains", "contains", "contains($FIELD, $VALUE)"), - ("Empty", "empty", "empty($FIELD)"), - ("Exists", "exists", "exists($FIELD)"), - ("EndsWith", "endswith", "endsWith($FIELD, $VALUE)"), - ("Equals", "eq", "$FIELD == $VALUE"), - ("GreaterThanOrEqual", "ge", "$FIELD >= $VALUE"), - ("GreaterThan", "gt", "$FIELD > $VALUE"), - ("LessThanOrEqual", "le", "$FIELD <= $VALUE"), - ("LessThan", "lt", "$FIELD < $VALUE"), - ("NotEquals", "ne", "$FIELD != $VALUE"), - ("StartsWith", "startswith", "startsWith($FIELD, $VALUE)") + private static readonly (string Name, string Operator, string Output)[] AllOps = + { + ("Contains", "contains", "contains($FIELD, $VALUE)"), + ("Empty", "empty", "empty($FIELD)"), + ("Exists", "exists", "exists($FIELD)"), + ("EndsWith", "endswith", "endsWith($FIELD, $VALUE)"), + ("Equals", "eq", "$FIELD == $VALUE"), + ("GreaterThanOrEqual", "ge", "$FIELD >= $VALUE"), + ("GreaterThan", "gt", "$FIELD > $VALUE"), + ("LessThanOrEqual", "le", "$FIELD <= $VALUE"), + ("LessThan", "lt", "$FIELD < $VALUE"), + ("NotEquals", "ne", "$FIELD != $VALUE"), + ("StartsWith", "startswith", "startsWith($FIELD, $VALUE)") + }; + + private static readonly QueryModel Model = new QueryModel(); + + static QueryFromJsonTests() + { + var nestedSchema = new FilterSchema(FilterSchemaType.Object) + { + Fields = ReadonlyList.Create(new FilterField(FilterSchema.String, "property")) }; - private static readonly QueryModel Model = new QueryModel(); + var fields = new List<FilterField> + { + new FilterField(nestedSchema, "object"), + new FilterField(FilterSchema.Any, "json"), + new FilterField(FilterSchema.Boolean, "boolean"), + new FilterField(FilterSchema.Boolean, "booleanNullable", IsNullable: true), + new FilterField(FilterSchema.DateTime, "datetime"), + new FilterField(FilterSchema.DateTime, "datetimeNullable", IsNullable: true), + new FilterField(FilterSchema.GeoObject, "geo"), + new FilterField(FilterSchema.Guid, "guid"), + new FilterField(FilterSchema.Guid, "guidNullable", IsNullable: true), + new FilterField(FilterSchema.Number, "number"), + new FilterField(FilterSchema.Number, "numberNullable", IsNullable: true), + new FilterField(FilterSchema.Number, "union"), + new FilterField(FilterSchema.String, "string"), + new FilterField(FilterSchema.String, "stringNullable", IsNullable: true), + new FilterField(FilterSchema.String, "union"), + new FilterField(FilterSchema.StringArray, "stringArray"), + new FilterField(FilterSchema.StringArray, "stringArrayNullable", IsNullable: true), + new FilterField(FilterSchema.String, "nested2.value") + }; - static QueryFromJsonTests() + var schema = new FilterSchema(FilterSchemaType.Object) { - var nestedSchema = new FilterSchema(FilterSchemaType.Object) - { - Fields = ReadonlyList.Create(new FilterField(FilterSchema.String, "property")) - }; - - var fields = new List<FilterField> - { - new FilterField(nestedSchema, "object"), - new FilterField(FilterSchema.Any, "json"), - new FilterField(FilterSchema.Boolean, "boolean"), - new FilterField(FilterSchema.Boolean, "booleanNullable", IsNullable: true), - new FilterField(FilterSchema.DateTime, "datetime"), - new FilterField(FilterSchema.DateTime, "datetimeNullable", IsNullable: true), - new FilterField(FilterSchema.GeoObject, "geo"), - new FilterField(FilterSchema.Guid, "guid"), - new FilterField(FilterSchema.Guid, "guidNullable", IsNullable: true), - new FilterField(FilterSchema.Number, "number"), - new FilterField(FilterSchema.Number, "numberNullable", IsNullable: true), - new FilterField(FilterSchema.Number, "union"), - new FilterField(FilterSchema.String, "string"), - new FilterField(FilterSchema.String, "stringNullable", IsNullable: true), - new FilterField(FilterSchema.String, "union"), - new FilterField(FilterSchema.StringArray, "stringArray"), - new FilterField(FilterSchema.StringArray, "stringArrayNullable", IsNullable: true), - new FilterField(FilterSchema.String, "nested2.value") - }; - - var schema = new FilterSchema(FilterSchemaType.Object) - { - Fields = fields.ToReadonlyList() - }; + Fields = fields.ToReadonlyList() + }; - Model = new QueryModel { Schema = schema }; - } + Model = new QueryModel { Schema = schema }; + } - public class DateTime + public class DateTime + { + public static IEnumerable<object[]> ValidTests() { - public static IEnumerable<object[]> ValidTests() - { - const string value = "2012-11-10T09:08:07Z"; - - return BuildTests("datetime", x => true, value, value); - } - - [Theory] - [MemberData(nameof(ValidTests))] - public void Should_parse_filter(string field, string op, string value, string expected) - { - var json = new { path = field, op, value }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_parse_filter_with_null() - { - var json = new { path = "datetimeNullable", op = "eq", value = (object?)null }; - - AssertFilter(json, "datetimeNullable == null"); - } - - [Fact] - public void Should_add_error_if_field_is_not_nullable() - { - var json = new { path = "datetime", op = "eq", value = (object?)null }; - - AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got Null."); - } - - [Fact] - public void Should_add_error_if_value_is_invalid() - { - var json = new { path = "datetime", op = "eq", value = "invalid" }; - - AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got invalid String."); - } + const string value = "2012-11-10T09:08:07Z"; - [Fact] - public void Should_add_error_if_value_type_is_invalid() - { - var json = new { path = "datetime", op = "eq", value = 1 }; - - AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got Number."); - } + return BuildTests("datetime", x => true, value, value); } - public class Guid + [Theory] + [MemberData(nameof(ValidTests))] + public void Should_parse_filter(string field, string op, string value, string expected) { - public static IEnumerable<object[]> ValidTests() - { - const string value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3"; - - return BuildTests("guid", x => true, value, value); - } - - [Theory] - [MemberData(nameof(ValidTests))] - public void Should_parse_filter(string field, string op, string value, string expected) - { - var json = new { path = field, op, value }; - - AssertFilter(json, expected); - } - - [Fact] - public void Should_parse_filter_with_null() - { - var json = new { path = "guidNullable", op = "eq", value = (object?)null }; - - AssertFilter(json, "guidNullable == null"); - } + var json = new { path = field, op, value }; - [Fact] - public void Should_add_error_if_field_is_not_nullable() - { - var json = new { path = "guid", op = "eq", value = (object?)null }; - - AssertFilterError(json, "Expected String (Guid) for path 'guid', but got Null."); - } - - [Fact] - public void Should_add_error_if_value_is_invalid() - { - var json = new { path = "guid", op = "eq", value = "invalid" }; - - AssertFilterError(json, "Expected String (Guid) for path 'guid', but got invalid String."); - } - - [Fact] - public void Should_add_error_if_value_type_is_invalid() - { - var json = new { path = "guid", op = "eq", value = 1 }; - - AssertFilterError(json, "Expected String (Guid) for path 'guid', but got Number."); - } + AssertFilter(json, expected); } - public class String + [Fact] + public void Should_parse_filter_with_null() { - public static IEnumerable<object[]> ValidTests() - { - const string value = "Hello"; - - return BuildTests("string", x => true, value, $"'{value}'"); - } - - public static IEnumerable<object[]> ValidInTests() - { - const string value = "Hello"; - - return BuildInTests("string", value, $"'{value}'"); - } - - [Theory] - [MemberData(nameof(ValidTests))] - public void Should_parse_filter(string field, string op, string value, string expected) - { - var json = new { path = field, op, value }; - - AssertFilter(json, expected); - } + var json = new { path = "datetimeNullable", op = "eq", value = (object?)null }; - [Theory] - [MemberData(nameof(ValidInTests))] - public void Should_parse_in_filter(string field, string value, string expected) - { - var json = new { path = field, op = "in", value = new[] { value } }; - - AssertFilter(json, expected); - } + AssertFilter(json, "datetimeNullable == null"); + } - [Fact] - public void Should_parse_filter_with_null() - { - var json = new { path = "stringNullable", op = "eq", value = (object?)null }; + [Fact] + public void Should_add_error_if_field_is_not_nullable() + { + var json = new { path = "datetime", op = "eq", value = (object?)null }; - AssertFilter(json, "stringNullable == null"); - } + AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got Null."); + } - [Fact] - public void Should_add_error_if_field_is_not_nullable() - { - var json = new { path = "string", op = "eq", value = (object?)null }; + [Fact] + public void Should_add_error_if_value_is_invalid() + { + var json = new { path = "datetime", op = "eq", value = "invalid" }; - AssertFilterError(json, "Expected String for path 'string', but got Null."); - } + AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got invalid String."); + } - [Fact] - public void Should_add_error_if_value_type_is_invalid() - { - var json = new { path = "string", op = "eq", value = 1 }; + [Fact] + public void Should_add_error_if_value_type_is_invalid() + { + var json = new { path = "datetime", op = "eq", value = 1 }; - AssertFilterError(json, "Expected String for path 'string', but got Number."); - } + AssertFilterError(json, "Expected String (ISO8601 DateTime) for path 'datetime', but got Number."); + } + } - [Fact] - public void Should_add_error_if_valid_is_not_a_valid_regex() - { - var json = new { path = "string", op = "matchs", value = "((" }; + public class Guid + { + public static IEnumerable<object[]> ValidTests() + { + const string value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3"; - AssertFilterError(json, "'((' is not a valid regular expression at path 'string'."); - } + return BuildTests("guid", x => true, value, value); + } - [Fact] - public void Should_parse_nested_string_filter() - { - var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; + [Theory] + [MemberData(nameof(ValidTests))] + public void Should_parse_filter(string field, string op, string value, string expected) + { + var json = new { path = field, op, value }; - AssertFilter(json, "object.property in ['Hello']"); - } + AssertFilter(json, expected); } - public class Geo + [Fact] + public void Should_parse_filter_with_null() { - private static bool ValidOperator(string op) - { - return op is "lt" or "exists"; - } + var json = new { path = "guidNullable", op = "eq", value = (object?)null }; - public static IEnumerable<object[]> ValidTests() - { - var value = new { longitude = 10, latitude = 20, distance = 30 }; - - return BuildFlatTests("geo", ValidOperator, value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); - } + AssertFilter(json, "guidNullable == null"); + } - public static IEnumerable<object[]> InvalidTests() - { - var value = new { longitude = 10, latitude = 20, distance = 30 }; + [Fact] + public void Should_add_error_if_field_is_not_nullable() + { + var json = new { path = "guid", op = "eq", value = (object?)null }; - return BuildInvalidOperatorTests("geo", ValidOperator, value); - } + AssertFilterError(json, "Expected String (Guid) for path 'guid', but got Null."); + } - [Theory] - [MemberData(nameof(ValidTests))] - public void Should_parse_filter(string field, string op, object value, string expected) - { - var json = new { path = field, op, value }; + [Fact] + public void Should_add_error_if_value_is_invalid() + { + var json = new { path = "guid", op = "eq", value = "invalid" }; - AssertFilter(json, expected); - } + AssertFilterError(json, "Expected String (Guid) for path 'guid', but got invalid String."); + } - [Theory] - [MemberData(nameof(InvalidTests))] - public void Should_add_error_if_operator_is_invalid(string field, string op, object value, string expected) - { - var json = new { path = field, op, value }; + [Fact] + public void Should_add_error_if_value_type_is_invalid() + { + var json = new { path = "guid", op = "eq", value = 1 }; - AssertFilterError(json, $"'{expected}' is not a valid operator for type GeoObject at '{field}'."); - } + AssertFilterError(json, "Expected String (Guid) for path 'guid', but got Number."); + } + } - [Fact] - public void Should_add_error_if_value_is_invalid() - { - var json = new { path = "geo", op = "lt", value = new { latitude = 10, longitude = 20 } }; + public class String + { + public static IEnumerable<object[]> ValidTests() + { + const string value = "Hello"; - AssertFilterError(json, "Expected Object(geo-json) for path 'geo', but got Object."); - } + return BuildTests("string", x => true, value, $"'{value}'"); + } - [Fact] - public void Should_add_error_if_value_type_is_invalid() - { - var json = new { path = "geo", op = "lt", value = 1 }; + public static IEnumerable<object[]> ValidInTests() + { + const string value = "Hello"; - AssertFilterError(json, "Expected Object(geo-json) for path 'geo', but got Number."); - } + return BuildInTests("string", value, $"'{value}'"); } - public class Number + [Theory] + [MemberData(nameof(ValidTests))] + public void Should_parse_filter(string field, string op, string value, string expected) { - private static bool ValidOperator(string op) - { - return op.Length == 2 || op == "exists"; - } + var json = new { path = field, op, value }; - public static IEnumerable<object[]> ValidTests() - { - const int value = 12; + AssertFilter(json, expected); + } - return BuildTests("number", ValidOperator, value, $"{value}"); - } + [Theory] + [MemberData(nameof(ValidInTests))] + public void Should_parse_in_filter(string field, string value, string expected) + { + var json = new { path = field, op = "in", value = new[] { value } }; - public static IEnumerable<object[]> InvalidTests() - { - const int value = 12; + AssertFilter(json, expected); + } - return BuildInvalidOperatorTests("number", ValidOperator, $"{value}"); - } + [Fact] + public void Should_parse_filter_with_null() + { + var json = new { path = "stringNullable", op = "eq", value = (object?)null }; - public static IEnumerable<object[]> ValidInTests() - { - const int value = 12; + AssertFilter(json, "stringNullable == null"); + } - return BuildInTests("number", value, $"{value}"); - } + [Fact] + public void Should_add_error_if_field_is_not_nullable() + { + var json = new { path = "string", op = "eq", value = (object?)null }; - [Theory] - [MemberData(nameof(ValidTests))] - public void Should_parse_filter(string field, string op, int value, string expected) - { - var json = new { path = field, op, value }; + AssertFilterError(json, "Expected String for path 'string', but got Null."); + } - AssertFilter(json, expected); - } + [Fact] + public void Should_add_error_if_value_type_is_invalid() + { + var json = new { path = "string", op = "eq", value = 1 }; - [Theory] - [MemberData(nameof(InvalidTests))] - public void Should_add_error_if_operator_is_invalid(string field, string op, int value, string expected) - { - var json = new { path = field, op, value }; + AssertFilterError(json, "Expected String for path 'string', but got Number."); + } - AssertFilterError(json, $"'{expected}' is not a valid operator for type Number at '{field}'."); - } + [Fact] + public void Should_add_error_if_valid_is_not_a_valid_regex() + { + var json = new { path = "string", op = "matchs", value = "((" }; - [Theory] - [MemberData(nameof(ValidInTests))] - public void Should_parse_in_filter(string field, int value, string expected) - { - var json = new { path = field, op = "in", value = new[] { value } }; + AssertFilterError(json, "'((' is not a valid regular expression at path 'string'."); + } - AssertFilter(json, expected); - } + [Fact] + public void Should_parse_nested_string_filter() + { + var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; - [Fact] - public void Should_parse_filter_with_null() - { - var json = new { path = "numberNullable", op = "eq", value = (object?)null }; + AssertFilter(json, "object.property in ['Hello']"); + } + } - AssertFilter(json, "numberNullable == null"); - } + public class Geo + { + private static bool ValidOperator(string op) + { + return op is "lt" or "exists"; + } - [Fact] - public void Should_add_error_if_field_is_not_nullable() - { - var json = new { path = "number", op = "eq", value = (object?)null }; + public static IEnumerable<object[]> ValidTests() + { + var value = new { longitude = 10, latitude = 20, distance = 30 }; - AssertFilterError(json, "Expected Number for path 'number', but got Null."); - } + return BuildFlatTests("geo", ValidOperator, value, $"Radius({value.longitude}, {value.latitude}, {value.distance})"); + } - [Fact] - public void Should_add_error_if_value_type_is_invalid() - { - var json = new { path = "number", op = "eq", value = true }; + public static IEnumerable<object[]> InvalidTests() + { + var value = new { longitude = 10, latitude = 20, distance = 30 }; - AssertFilterError(json, "Expected Number for path 'number', but got Boolean."); - } + return BuildInvalidOperatorTests("geo", ValidOperator, value); } - public class Boolean + [Theory] + [MemberData(nameof(ValidTests))] + public void Should_parse_filter(string field, string op, object value, string expected) { - private static bool ValidOperator(string op) - { - return op is "eq" or "ne" or "exists"; - } + var json = new { path = field, op, value }; - public static IEnumerable<object[]> ValidTests() - { - const bool value = true; + AssertFilter(json, expected); + } - return BuildTests("boolean", ValidOperator, value, $"{value}"); - } + [Theory] + [MemberData(nameof(InvalidTests))] + public void Should_add_error_if_operator_is_invalid(string field, string op, object value, string expected) + { + var json = new { path = field, op, value }; - public static IEnumerable<object[]> InvalidTests() - { - const bool value = true; + AssertFilterError(json, $"'{expected}' is not a valid operator for type GeoObject at '{field}'."); + } - return BuildInvalidOperatorTests("boolean", ValidOperator, value); - } + [Fact] + public void Should_add_error_if_value_is_invalid() + { + var json = new { path = "geo", op = "lt", value = new { latitude = 10, longitude = 20 } }; - public static IEnumerable<object[]> ValidInTests() - { - const bool value = true; + AssertFilterError(json, "Expected Object(geo-json) for path 'geo', but got Object."); + } - return BuildInTests("boolean", value, $"{value}"); - } + [Fact] + public void Should_add_error_if_value_type_is_invalid() + { + var json = new { path = "geo", op = "lt", value = 1 }; - [Theory] - [MemberData(nameof(ValidTests))] - public void Should_parse_filter(string field, string op, bool value, string expected) - { - var json = new { path = field, op, value }; + AssertFilterError(json, "Expected Object(geo-json) for path 'geo', but got Number."); + } + } - AssertFilter(json, expected); - } + public class Number + { + private static bool ValidOperator(string op) + { + return op.Length == 2 || op == "exists"; + } - [Theory] - [MemberData(nameof(InvalidTests))] - public void Should_add_error_if_operator_is_invalid(string field, string op, bool value, string expected) - { - var json = new { path = field, op, value }; + public static IEnumerable<object[]> ValidTests() + { + const int value = 12; - AssertFilterError(json, $"'{expected}' is not a valid operator for type Boolean at '{field}'."); - } + return BuildTests("number", ValidOperator, value, $"{value}"); + } - [Theory] - [MemberData(nameof(ValidInTests))] - public void Should_parse_in_filter(string field, bool value, string expected) - { - var json = new { path = field, op = "in", value = new[] { value } }; + public static IEnumerable<object[]> InvalidTests() + { + const int value = 12; - AssertFilter(json, expected); - } + return BuildInvalidOperatorTests("number", ValidOperator, $"{value}"); + } - [Fact] - public void Should_parse_filter_with_null() - { - var json = new { path = "booleanNullable", op = "eq", value = (object?)null }; + public static IEnumerable<object[]> ValidInTests() + { + const int value = 12; - AssertFilter(json, "booleanNullable == null"); - } + return BuildInTests("number", value, $"{value}"); + } - [Fact] - public void Should_add_error_if_field_is_not_nullable() - { - var json = new { path = "boolean", op = "eq", value = (object?)null }; + [Theory] + [MemberData(nameof(ValidTests))] + public void Should_parse_filter(string field, string op, int value, string expected) + { + var json = new { path = field, op, value }; - AssertFilterError(json, "Expected Boolean for path 'boolean', but got Null."); - } + AssertFilter(json, expected); + } - [Fact] - public void Should_add_error_if_boolean_property_got_invalid_value() - { - var json = new { path = "boolean", op = "eq", value = 1 }; + [Theory] + [MemberData(nameof(InvalidTests))] + public void Should_add_error_if_operator_is_invalid(string field, string op, int value, string expected) + { + var json = new { path = field, op, value }; - AssertFilterError(json, "Expected Boolean for path 'boolean', but got Number."); - } + AssertFilterError(json, $"'{expected}' is not a valid operator for type Number at '{field}'."); } - public class Array + [Theory] + [MemberData(nameof(ValidInTests))] + public void Should_parse_in_filter(string field, int value, string expected) { - private static bool ValidOperator(string op) - { - return op is "eq" or "ne" or "empty" or "exists"; - } + var json = new { path = field, op = "in", value = new[] { value } }; - public static IEnumerable<object[]> ValidTests() - { - const string value = "Hello"; + AssertFilter(json, expected); + } - return BuildTests("stringArray", ValidOperator, value, $"'{value}'"); - } + [Fact] + public void Should_parse_filter_with_null() + { + var json = new { path = "numberNullable", op = "eq", value = (object?)null }; - public static IEnumerable<object[]> ValidInTests() - { - const string value = "Hello"; + AssertFilter(json, "numberNullable == null"); + } - return BuildInTests("stringArray", value, $"'{value}'"); - } + [Fact] + public void Should_add_error_if_field_is_not_nullable() + { + var json = new { path = "number", op = "eq", value = (object?)null }; - [Theory] - [MemberData(nameof(ValidTests))] - public void Should_parse_array_filter(string field, string op, string value, string expected) - { - var json = new { path = field, op, value }; + AssertFilterError(json, "Expected Number for path 'number', but got Null."); + } - AssertFilter(json, expected); - } + [Fact] + public void Should_add_error_if_value_type_is_invalid() + { + var json = new { path = "number", op = "eq", value = true }; - [Theory] - [MemberData(nameof(ValidInTests))] - public void Should_parse_array_in_filter(string field, string value, string expected) - { - var json = new { path = field, op = "in", value = new[] { value } }; + AssertFilterError(json, "Expected Number for path 'number', but got Boolean."); + } + } - AssertFilter(json, expected); - } + public class Boolean + { + private static bool ValidOperator(string op) + { + return op is "eq" or "ne" or "exists"; + } - [Fact] - public void Should_parse_filter_with_null() - { - var json = new { path = "stringArrayNullable", op = "eq", value = (object?)null }; + public static IEnumerable<object[]> ValidTests() + { + const bool value = true; - AssertFilter(json, "stringArrayNullable == null"); - } + return BuildTests("boolean", ValidOperator, value, $"{value}"); + } - [Fact] - public void Should_add_error_if_field_is_not_nullable() - { - var json = new { path = "stringArray", op = "eq", value = (object?)null }; + public static IEnumerable<object[]> InvalidTests() + { + const bool value = true; - AssertFilterError(json, "Expected String for path 'stringArray', but got Null."); - } + return BuildInvalidOperatorTests("boolean", ValidOperator, value); + } - [Fact] - public void Should_add_error_if_using_array_value_for_non_allowed_operator() - { - var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; + public static IEnumerable<object[]> ValidInTests() + { + const bool value = true; - AssertFilterError(json, "Array value is not allowed for 'Equals' operator and path 'string'."); - } + return BuildInTests("boolean", value, $"{value}"); + } - [Fact] - public void Should_convert_single_value_to_list_for_in_operator() - { - var json = new { path = "string", op = "in", value = "Hello" }; + [Theory] + [MemberData(nameof(ValidTests))] + public void Should_parse_filter(string field, string op, bool value, string expected) + { + var json = new { path = field, op, value }; - AssertFilter(json, "string in ['Hello']"); - } + AssertFilter(json, expected); } - [Fact] - public void Should_filter_union_by_string() + [Theory] + [MemberData(nameof(InvalidTests))] + public void Should_add_error_if_operator_is_invalid(string field, string op, bool value, string expected) { - var json = new { path = "union", op = "eq", value = "Hello" }; + var json = new { path = field, op, value }; - AssertFilter(json, "union == 'Hello'"); + AssertFilterError(json, $"'{expected}' is not a valid operator for type Boolean at '{field}'."); } - [Fact] - public void Should_filter_union_by_number() + [Theory] + [MemberData(nameof(ValidInTests))] + public void Should_parse_in_filter(string field, bool value, string expected) { - var json = new { path = "union", op = "eq", value = 42 }; + var json = new { path = field, op = "in", value = new[] { value } }; - AssertFilter(json, "union == 42"); + AssertFilter(json, expected); } [Fact] - public void Should_not_filter_union_by_boolean() + public void Should_parse_filter_with_null() { - var json = new { path = "union", op = "eq", value = true }; + var json = new { path = "booleanNullable", op = "eq", value = (object?)null }; - AssertFilterError(json, "Expected String for path 'union', but got Boolean."); + AssertFilter(json, "booleanNullable == null"); } [Fact] - public void Should_add_error_if_property_does_not_exist() + public void Should_add_error_if_field_is_not_nullable() { - var json = new { path = "notfound", op = "eq", value = 1 }; + var json = new { path = "boolean", op = "eq", value = (object?)null }; - AssertFilterError(json, "Path 'notfound' does not point to a valid property in the model."); + AssertFilterError(json, "Expected Boolean for path 'boolean', but got Null."); } [Fact] - public void Should_add_error_if_nested_property_does_not_exist() + public void Should_add_error_if_boolean_property_got_invalid_value() { - var json = new { path = "object.notfound", op = "eq", value = 1 }; + var json = new { path = "boolean", op = "eq", value = 1 }; - AssertFilterError(json, "Path 'object.notfound' does not point to a valid property in the model."); + AssertFilterError(json, "Expected Boolean for path 'boolean', but got Number."); } + } - [Fact] - public void Should_parse_filter() + public class Array + { + private static bool ValidOperator(string op) { - var json = new { Filter = new { path = "string", op = "eq", value = "Hello" } }; - - AssertQuery(json, "Filter: string == 'Hello'"); + return op is "eq" or "ne" or "empty" or "exists"; } - [Fact] - public void Should_parse_fulltext() + public static IEnumerable<object[]> ValidTests() { - var json = new { FullText = "Hello" }; + const string value = "Hello"; - AssertQuery(json, "FullText: 'Hello'"); + return BuildTests("stringArray", ValidOperator, value, $"'{value}'"); } - [Fact] - public void Should_parse_sort() + public static IEnumerable<object[]> ValidInTests() { - var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; + const string value = "Hello"; - AssertQuery(json, "Sort: string Ascending"); + return BuildInTests("stringArray", value, $"'{value}'"); } - [Fact] - public void Should_parse_top() + [Theory] + [MemberData(nameof(ValidTests))] + public void Should_parse_array_filter(string field, string op, string value, string expected) { - var json = new { top = 20 }; + var json = new { path = field, op, value }; - AssertQuery(json, "Take: 20"); + AssertFilter(json, expected); } - [Fact] - public void Should_parse_take() + [Theory] + [MemberData(nameof(ValidInTests))] + public void Should_parse_array_in_filter(string field, string value, string expected) { - var json = new { take = 20 }; + var json = new { path = field, op = "in", value = new[] { value } }; - AssertQuery(json, "Take: 20"); + AssertFilter(json, expected); } [Fact] - public void Should_parse_skip() + public void Should_parse_filter_with_null() { - var json = new { skip = 10 }; + var json = new { path = "stringArrayNullable", op = "eq", value = (object?)null }; - AssertQuery(json, "Skip: 10"); + AssertFilter(json, "stringArrayNullable == null"); } [Fact] - public void Should_parse_random() + public void Should_add_error_if_field_is_not_nullable() { - var json = new { random = 4 }; + var json = new { path = "stringArray", op = "eq", value = (object?)null }; - AssertQuery(json, "Random: 4"); + AssertFilterError(json, "Expected String for path 'stringArray', but got Null."); } [Fact] - public void Should_throw_exception_for_invalid_query() + public void Should_add_error_if_using_array_value_for_non_allowed_operator() { - var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; + var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; - Assert.Throws<ValidationException>(() => AssertQuery(json, null)); + AssertFilterError(json, "Array value is not allowed for 'Equals' operator and path 'string'."); } [Fact] - public void Should_throw_exception_if_parsing_invalid_json() + public void Should_convert_single_value_to_list_for_in_operator() { - var json = "invalid"; + var json = new { path = "string", op = "in", value = "Hello" }; - Assert.Throws<ValidationException>(() => AssertQuery(json, null)); + AssertFilter(json, "string in ['Hello']"); } + } - private static void AssertQuery(object json, string? expectedFilter) - { - var errors = new List<string>(); + [Fact] + public void Should_filter_union_by_string() + { + var json = new { path = "union", op = "eq", value = "Hello" }; - var filter = ConvertQuery(json); + AssertFilter(json, "union == 'Hello'"); + } - Assert.Empty(errors); - Assert.Equal(expectedFilter, filter); - } + [Fact] + public void Should_filter_union_by_number() + { + var json = new { path = "union", op = "eq", value = 42 }; - private static void AssertFilter(object json, string? expectedFilter) - { - var errors = new List<string>(); + AssertFilter(json, "union == 42"); + } - var filter = ConvertFilter(json, errors); + [Fact] + public void Should_not_filter_union_by_boolean() + { + var json = new { path = "union", op = "eq", value = true }; - Assert.Empty(errors); - Assert.Equal(expectedFilter, filter); - } + AssertFilterError(json, "Expected String for path 'union', but got Boolean."); + } - private static void AssertFilterError(object json, string expectedError) - { - var errors = new List<string>(); + [Fact] + public void Should_add_error_if_property_does_not_exist() + { + var json = new { path = "notfound", op = "eq", value = 1 }; - var filter = ConvertFilter(json, errors); + AssertFilterError(json, "Path 'notfound' does not point to a valid property in the model."); + } - Assert.Equal(expectedError, errors.FirstOrDefault()); - Assert.Null(filter); - } + [Fact] + public void Should_add_error_if_nested_property_does_not_exist() + { + var json = new { path = "object.notfound", op = "eq", value = 1 }; - private static string? ConvertFilter<T>(T value, List<string> errors) - { - var json = TestUtils.DefaultSerializer.Serialize(value, true); + AssertFilterError(json, "Path 'object.notfound' does not point to a valid property in the model."); + } - var jsonFilter = TestUtils.DefaultSerializer.Deserialize<FilterNode<JsonValue>>(json); + [Fact] + public void Should_parse_filter() + { + var json = new { Filter = new { path = "string", op = "eq", value = "Hello" } }; - return JsonFilterVisitor.Parse(jsonFilter, Model, errors)?.ToString(); - } + AssertQuery(json, "Filter: string == 'Hello'"); + } - private static string? ConvertQuery<T>(T value) - { - var json = TestUtils.DefaultSerializer.Serialize(value, true); + [Fact] + public void Should_parse_fulltext() + { + var json = new { FullText = "Hello" }; + + AssertQuery(json, "FullText: 'Hello'"); + } - var jsonFilter = Model.Parse(json, TestUtils.DefaultSerializer); + [Fact] + public void Should_parse_sort() + { + var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; - return jsonFilter.ToString(); - } + AssertQuery(json, "Sort: string Ascending"); + } + + [Fact] + public void Should_parse_top() + { + var json = new { top = 20 }; + + AssertQuery(json, "Take: 20"); + } + + [Fact] + public void Should_parse_take() + { + var json = new { take = 20 }; + + AssertQuery(json, "Take: 20"); + } + + [Fact] + public void Should_parse_skip() + { + var json = new { skip = 10 }; + + AssertQuery(json, "Skip: 10"); + } + + [Fact] + public void Should_parse_random() + { + var json = new { random = 4 }; + + AssertQuery(json, "Random: 4"); + } + + [Fact] + public void Should_throw_exception_for_invalid_query() + { + var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; + + Assert.Throws<ValidationException>(() => AssertQuery(json, null)); + } + + [Fact] + public void Should_throw_exception_if_parsing_invalid_json() + { + var json = "invalid"; + + Assert.Throws<ValidationException>(() => AssertQuery(json, null)); + } + + private static void AssertQuery(object json, string? expectedFilter) + { + var errors = new List<string>(); - public static IEnumerable<object[]> BuildFlatTests(string field, Predicate<string> opFilter, object value, string valueString) + var filter = ConvertQuery(json); + + Assert.Empty(errors); + Assert.Equal(expectedFilter, filter); + } + + private static void AssertFilter(object json, string? expectedFilter) + { + var errors = new List<string>(); + + var filter = ConvertFilter(json, errors); + + Assert.Empty(errors); + Assert.Equal(expectedFilter, filter); + } + + private static void AssertFilterError(object json, string expectedError) + { + var errors = new List<string>(); + + var filter = ConvertFilter(json, errors); + + Assert.Equal(expectedError, errors.FirstOrDefault()); + Assert.Null(filter); + } + + private static string? ConvertFilter<T>(T value, List<string> errors) + { + var json = TestUtils.DefaultSerializer.Serialize(value, true); + + var jsonFilter = TestUtils.DefaultSerializer.Deserialize<FilterNode<JsonValue>>(json); + + return JsonFilterVisitor.Parse(jsonFilter, Model, errors)?.ToString(); + } + + private static string? ConvertQuery<T>(T value) + { + var json = TestUtils.DefaultSerializer.Serialize(value, true); + + var jsonFilter = Model.Parse(json, TestUtils.DefaultSerializer); + + return jsonFilter.ToString(); + } + + public static IEnumerable<object[]> BuildFlatTests(string field, Predicate<string> opFilter, object value, string valueString) + { + var fields = new[] { - var fields = new[] - { - $"{field}" - }; + $"{field}" + }; - foreach (var fieldName in fields) + foreach (var fieldName in fields) + { + foreach (var (_, op, output) in AllOps.Where(x => opFilter(x.Operator))) { - foreach (var (_, op, output) in AllOps.Where(x => opFilter(x.Operator))) - { - var expected = - output - .Replace("$FIELD", fieldName, StringComparison.Ordinal) - .Replace("$VALUE", valueString, StringComparison.Ordinal); - - yield return new[] { fieldName, op, value, expected }; - } + var expected = + output + .Replace("$FIELD", fieldName, StringComparison.Ordinal) + .Replace("$VALUE", valueString, StringComparison.Ordinal); + + yield return new[] { fieldName, op, value, expected }; } } + } - public static IEnumerable<object[]> BuildTests(string field, Predicate<string> opFilter, object value, string valueString) + public static IEnumerable<object[]> BuildTests(string field, Predicate<string> opFilter, object value, string valueString) + { + var fields = new[] { - var fields = new[] - { - $"{field}", - $"json.{field}", - $"json.nested.{field}" - }; + $"{field}", + $"json.{field}", + $"json.nested.{field}" + }; - foreach (var fieldName in fields) + foreach (var fieldName in fields) + { + foreach (var (_, op, output) in AllOps.Where(x => opFilter(x.Operator))) { - foreach (var (_, op, output) in AllOps.Where(x => opFilter(x.Operator))) - { - var expected = - output - .Replace("$FIELD", fieldName, StringComparison.Ordinal) - .Replace("$VALUE", valueString, StringComparison.Ordinal); - - yield return new[] { fieldName, op, value, expected }; - } + var expected = + output + .Replace("$FIELD", fieldName, StringComparison.Ordinal) + .Replace("$VALUE", valueString, StringComparison.Ordinal); + + yield return new[] { fieldName, op, value, expected }; } } + } - public static IEnumerable<object[]> BuildInTests(string field, object value, string valueString) + public static IEnumerable<object[]> BuildInTests(string field, object value, string valueString) + { + var fields = new[] { - var fields = new[] - { - $"{field}", - $"json.{field}", - $"json.nested.{field}" - }; + $"{field}", + $"json.{field}", + $"json.nested.{field}" + }; - foreach (var f in fields) - { - var expected = $"{f} in [{valueString}]"; + foreach (var f in fields) + { + var expected = $"{f} in [{valueString}]"; - yield return new[] { f, value, expected }; - } + yield return new[] { f, value, expected }; } + } - public static IEnumerable<object[]> BuildInvalidOperatorTests(string field, Predicate<string> opFilter, object value) + public static IEnumerable<object[]> BuildInvalidOperatorTests(string field, Predicate<string> opFilter, object value) + { + foreach (var (name, op, _) in AllOps.Where(x => !opFilter(x.Operator))) { - foreach (var (name, op, _) in AllOps.Where(x => !opFilter(x.Operator))) - { - yield return new[] { field, op, value, name }; - } + yield return new[] { field, op, value, name }; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs index c755d3c50f..dc7ae9e1d1 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs @@ -13,516 +13,515 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable IDE1006 // Naming Styles -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public class QueryFromODataTests { - public class QueryFromODataTests + private static readonly IEdmModel EdmModel; + + static QueryFromODataTests() { - private static readonly IEdmModel EdmModel; + var fields = new List<FilterField> + { + new FilterField(FilterSchema.Guid, "id"), + new FilterField(FilterSchema.Guid, "idNullable", IsNullable: true), + new FilterField(FilterSchema.DateTime, "created"), + new FilterField(FilterSchema.DateTime, "createdNullable", IsNullable: true), + new FilterField(FilterSchema.Boolean, "isComicFigure"), + new FilterField(FilterSchema.Boolean, "isComicFigureNullable", IsNullable: true), + new FilterField(FilterSchema.String, "firstName"), + new FilterField(FilterSchema.String, "firstNameNullable", IsNullable: true), + new FilterField(FilterSchema.String, "lastName"), + new FilterField(FilterSchema.String, "lastNameNullable", IsNullable: true), + new FilterField(FilterSchema.Number, "age"), + new FilterField(FilterSchema.Number, "ageNullable", IsNullable: true), + new FilterField(FilterSchema.Number, "incomeMio"), + new FilterField(FilterSchema.Number, "incomeMioNullable", IsNullable: true), + new FilterField(FilterSchema.GeoObject, "geo"), + new FilterField(FilterSchema.GeoObject, "geoNullable", IsNullable: true), + new FilterField(FilterSchema.Any, "properties") + }; + + var filterSchema = new FilterSchema(FilterSchemaType.Object) + { + Fields = fields.ToReadonlyList() + }; + + var queryModel = new QueryModel { Schema = filterSchema }; + + EdmModel = queryModel.ConvertToEdm("Squidex", "Content"); + } - static QueryFromODataTests() - { - var fields = new List<FilterField> - { - new FilterField(FilterSchema.Guid, "id"), - new FilterField(FilterSchema.Guid, "idNullable", IsNullable: true), - new FilterField(FilterSchema.DateTime, "created"), - new FilterField(FilterSchema.DateTime, "createdNullable", IsNullable: true), - new FilterField(FilterSchema.Boolean, "isComicFigure"), - new FilterField(FilterSchema.Boolean, "isComicFigureNullable", IsNullable: true), - new FilterField(FilterSchema.String, "firstName"), - new FilterField(FilterSchema.String, "firstNameNullable", IsNullable: true), - new FilterField(FilterSchema.String, "lastName"), - new FilterField(FilterSchema.String, "lastNameNullable", IsNullable: true), - new FilterField(FilterSchema.Number, "age"), - new FilterField(FilterSchema.Number, "ageNullable", IsNullable: true), - new FilterField(FilterSchema.Number, "incomeMio"), - new FilterField(FilterSchema.Number, "incomeMioNullable", IsNullable: true), - new FilterField(FilterSchema.GeoObject, "geo"), - new FilterField(FilterSchema.GeoObject, "geoNullable", IsNullable: true), - new FilterField(FilterSchema.Any, "properties") - }; - - var filterSchema = new FilterSchema(FilterSchemaType.Object) - { - Fields = fields.ToReadonlyList() - }; - - var queryModel = new QueryModel { Schema = filterSchema }; - - EdmModel = queryModel.ConvertToEdm("Squidex", "Content"); - } - - [Fact] - public void Should_parse_query() - { - var parser = EdmModel.ParseQuery("$filter=firstName eq 'Dagobert'"); + [Fact] + public void Should_parse_query() + { + var parser = EdmModel.ParseQuery("$filter=firstName eq 'Dagobert'"); - Assert.NotNull(parser); - } + Assert.NotNull(parser); + } - [Fact] - public void Should_escape_field_name() - { - Assert.Equal("field_name", "field-name".EscapeEdmField()); - } + [Fact] + public void Should_escape_field_name() + { + Assert.Equal("field_name", "field-name".EscapeEdmField()); + } - [Fact] - public void Should_unescape_field_name() - { - Assert.Equal("field-name", "field_name".UnescapeEdmField()); - } - - [Theory] - [InlineData("created")] - [InlineData("createdNullable")] - [InlineData("properties/datetime")] - [InlineData("properties/nested/datetime")] - public void Should_parse_filter_if_type_is_datetime(string field) - { - var i = _Q($"$filter={field} eq 1988-01-19T12:00:00Z"); - var o = _C($"Filter: {field} == 1988-01-19T12:00:00Z"); - - Assert.Equal(o, i); - } - - [Theory] - [InlineData("created")] - [InlineData("createdNullable")] - [InlineData("properties/datetime")] - [InlineData("properties/nested/datetime")] - public void Should_parse_filter_if_type_is_datetime_and_value_is_null(string field) - { - var i = _Q($"$filter={field} eq null"); - var o = _C($"Filter: {field} == null"); + [Fact] + public void Should_unescape_field_name() + { + Assert.Equal("field-name", "field_name".UnescapeEdmField()); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("created")] + [InlineData("createdNullable")] + [InlineData("properties/datetime")] + [InlineData("properties/nested/datetime")] + public void Should_parse_filter_if_type_is_datetime(string field) + { + var i = _Q($"$filter={field} eq 1988-01-19T12:00:00Z"); + var o = _C($"Filter: {field} == 1988-01-19T12:00:00Z"); - [Fact] - public void Should_parse_filter_if_type_is_datetime_list() - { - var i = _Q("$filter=created in ('1988-01-19T12:00:00Z')"); - var o = _C("Filter: created in [1988-01-19T12:00:00Z]"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("created")] + [InlineData("createdNullable")] + [InlineData("properties/datetime")] + [InlineData("properties/nested/datetime")] + public void Should_parse_filter_if_type_is_datetime_and_value_is_null(string field) + { + var i = _Q($"$filter={field} eq null"); + var o = _C($"Filter: {field} == null"); - [Fact] - public void Should_parse_filter_if_type_is_datetime_and_and_value_is_date() - { - var i = _Q("$filter=created eq 1988-01-19"); - var o = _C("Filter: created == 1988-01-19T00:00:00Z"); - - Assert.Equal(o, i); - } - - [Theory] - [InlineData("id")] - [InlineData("idNullable")] - [InlineData("properties/uid")] - [InlineData("properties/nested/guid")] - public void Should_parse_filter_if_type_is_guid(string field) - { - var i = _Q($"$filter={field} eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); - var o = _C($"Filter: {field} == b5fe25e3-b262-4b17-91ef-b3772a6b62bb"); - - Assert.Equal(o, i); - } - - [Theory] - [InlineData("id")] - [InlineData("idNullable")] - [InlineData("properties/uid")] - [InlineData("properties/nested/guid")] - public void Should_parse_filter_if_type_is_guid_and_value_is_null(string field) - { - var i = _Q($"$filter={field} eq null"); - var o = _C($"Filter: {field} == null"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_if_type_is_datetime_list() + { + var i = _Q("$filter=created in ('1988-01-19T12:00:00Z')"); + var o = _C("Filter: created in [1988-01-19T12:00:00Z]"); - [Fact] - public void Should_parse_filter_if_type_is_guid_list() - { - var i = _Q("$filter=id in ('B5FE25E3-B262-4B17-91EF-B3772A6B62BB')"); - var o = _C("Filter: id in [b5fe25e3-b262-4b17-91ef-b3772a6b62bb]"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_if_type_is_datetime_and_and_value_is_date() + { + var i = _Q("$filter=created eq 1988-01-19"); + var o = _C("Filter: created == 1988-01-19T00:00:00Z"); - [Fact] - public void Should_parse_filter_if_type_is_null() - { - var i = _Q("$filter=firstName eq null"); - var o = _C("Filter: firstName == null"); - - Assert.Equal(o, i); - } - - [Theory] - [InlineData("firstName")] - [InlineData("firstNameNullable")] - [InlineData("properties/string")] - [InlineData("properties/nested/string")] - public void Should_parse_filter_if_type_is_string(string field) - { - var i = _Q($"$filter={field} eq 'Dagobert'"); - var o = _C($"Filter: {field} == 'Dagobert'"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("id")] + [InlineData("idNullable")] + [InlineData("properties/uid")] + [InlineData("properties/nested/guid")] + public void Should_parse_filter_if_type_is_guid(string field) + { + var i = _Q($"$filter={field} eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB"); + var o = _C($"Filter: {field} == b5fe25e3-b262-4b17-91ef-b3772a6b62bb"); - [Fact] - public void Should_parse_filter_if_type_is_string_list() - { - var i = _Q("$filter=firstName in ('Dagobert')"); - var o = _C("Filter: firstName in ['Dagobert']"); - - Assert.Equal(o, i); - } - - [Theory] - [InlineData("isComicFigure")] - [InlineData("isComicFigureNullable")] - [InlineData("properties/boolean")] - [InlineData("properties/nested/boolean")] - public void Should_parse_filter_if_type_is_boolean(string field) - { - var i = _Q($"$filter={field} eq true"); - var o = _C($"Filter: {field} == True"); - - Assert.Equal(o, i); - } - - [Theory] - [InlineData("isComicFigure")] - [InlineData("isComicFigureNullable")] - [InlineData("properties/boolean")] - [InlineData("properties/nested/boolean")] - public void Should_parse_filter_if_type_is_boolean_and_value_is_null(string field) - { - var i = _Q($"$filter={field} eq null"); - var o = _C($"Filter: {field} == null"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("id")] + [InlineData("idNullable")] + [InlineData("properties/uid")] + [InlineData("properties/nested/guid")] + public void Should_parse_filter_if_type_is_guid_and_value_is_null(string field) + { + var i = _Q($"$filter={field} eq null"); + var o = _C($"Filter: {field} == null"); - [Fact] - public void Should_parse_filter_if_type_is_boolean_list() - { - var i = _Q("$filter=isComicFigure in (true)"); - var o = _C("Filter: isComicFigure in [True]"); - - Assert.Equal(o, i); - } - - [Theory] - [InlineData("incomeMio")] - [InlineData("incomeMioNullable")] - [InlineData("properties/double")] - [InlineData("properties/nested/double")] - public void Should_parse_filter_if_type_is_double(string field) - { - var i = _Q($"$filter={field} eq 5634474356.1233"); - var o = _C($"Filter: {field} == 5634474356.1233"); - - Assert.Equal(o, i); - } - - [Theory] - [InlineData("geo")] - [InlineData("geoNullable")] - [InlineData("properties/geo")] - [InlineData("properties/nested/geo")] - public void Should_parse_filter_if_type_is_geograph(string field) - { - var i = _Q($"$filter=geo.distance({field}, geography'POINT(10 20)') lt 30.0"); - var o = _C($"Filter: {field} < Radius(10, 20, 30)"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_if_type_is_guid_list() + { + var i = _Q("$filter=id in ('B5FE25E3-B262-4B17-91EF-B3772A6B62BB')"); + var o = _C("Filter: id in [b5fe25e3-b262-4b17-91ef-b3772a6b62bb]"); - [Fact] - public void Should_parse_filter_if_type_is_double_list() - { - var i = _Q("$filter=incomeMio in (5634474356.1233)"); - var o = _C("Filter: incomeMio in [5634474356.1233]"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_if_type_is_null() + { + var i = _Q("$filter=firstName eq null"); + var o = _C("Filter: firstName == null"); - [Fact] - public void Should_parse_filter_with_negation() - { - var i = _Q("$filter=not endswith(lastName, 'Duck')"); - var o = _C("Filter: !(endsWith(lastName, 'Duck'))"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("firstName")] + [InlineData("firstNameNullable")] + [InlineData("properties/string")] + [InlineData("properties/nested/string")] + public void Should_parse_filter_if_type_is_string(string field) + { + var i = _Q($"$filter={field} eq 'Dagobert'"); + var o = _C($"Filter: {field} == 'Dagobert'"); - [Fact] - public void Should_parse_filter_with_startswith() - { - var i = _Q("$filter=startswith(lastName, 'Duck')"); - var o = _C("Filter: startsWith(lastName, 'Duck')"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_if_type_is_string_list() + { + var i = _Q("$filter=firstName in ('Dagobert')"); + var o = _C("Filter: firstName in ['Dagobert']"); - [Fact] - public void Should_parse_filter_with_endswith() - { - var i = _Q("$filter=endswith(lastName, 'Duck')"); - var o = _C("Filter: endsWith(lastName, 'Duck')"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("isComicFigure")] + [InlineData("isComicFigureNullable")] + [InlineData("properties/boolean")] + [InlineData("properties/nested/boolean")] + public void Should_parse_filter_if_type_is_boolean(string field) + { + var i = _Q($"$filter={field} eq true"); + var o = _C($"Filter: {field} == True"); - [Fact] - public void Should_parse_filter_with_matchs() - { - var i = _Q("$filter=matchs(lastName, 'Duck')"); - var o = _C("Filter: matchs(lastName, 'Duck')"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("isComicFigure")] + [InlineData("isComicFigureNullable")] + [InlineData("properties/boolean")] + [InlineData("properties/nested/boolean")] + public void Should_parse_filter_if_type_is_boolean_and_value_is_null(string field) + { + var i = _Q($"$filter={field} eq null"); + var o = _C($"Filter: {field} == null"); - [Fact] - public void Should_parse_filter_with_empty() - { - var i = _Q("$filter=empty(lastName)"); - var o = _C("Filter: empty(lastName)"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_if_type_is_boolean_list() + { + var i = _Q("$filter=isComicFigure in (true)"); + var o = _C("Filter: isComicFigure in [True]"); - [Fact] - public void Should_parse_filter_with_empty_to_true() - { - var i = _Q("$filter=empty(lastName) eq true"); - var o = _C("Filter: empty(lastName)"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("incomeMio")] + [InlineData("incomeMioNullable")] + [InlineData("properties/double")] + [InlineData("properties/nested/double")] + public void Should_parse_filter_if_type_is_double(string field) + { + var i = _Q($"$filter={field} eq 5634474356.1233"); + var o = _C($"Filter: {field} == 5634474356.1233"); - [Fact] - public void Should_parse_filter_with_exists() - { - var i = _Q("$filter=exists(lastName)"); - var o = _C("Filter: exists(lastName)"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Theory] + [InlineData("geo")] + [InlineData("geoNullable")] + [InlineData("properties/geo")] + [InlineData("properties/nested/geo")] + public void Should_parse_filter_if_type_is_geograph(string field) + { + var i = _Q($"$filter=geo.distance({field}, geography'POINT(10 20)') lt 30.0"); + var o = _C($"Filter: {field} < Radius(10, 20, 30)"); - [Fact] - public void Should_parse_filter_with_exists_to_true() - { - var i = _Q("$filter=exists(lastName) eq true"); - var o = _C("Filter: exists(lastName)"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_if_type_is_double_list() + { + var i = _Q("$filter=incomeMio in (5634474356.1233)"); + var o = _C("Filter: incomeMio in [5634474356.1233]"); - [Fact] - public void Should_parse_filter_with_exists_to_false() - { - var i = _Q("$filter=exists(lastName) eq false"); - var o = _C("Filter: !(exists(lastName))"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_negation() + { + var i = _Q("$filter=not endswith(lastName, 'Duck')"); + var o = _C("Filter: !(endsWith(lastName, 'Duck'))"); - [Fact] - public void Should_parse_filter_with_contains() - { - var i = _Q("$filter=contains(lastName, 'Duck')"); - var o = _C("Filter: contains(lastName, 'Duck')"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_startswith() + { + var i = _Q("$filter=startswith(lastName, 'Duck')"); + var o = _C("Filter: startsWith(lastName, 'Duck')"); - [Fact] - public void Should_parse_filter_with_contains_to_true() - { - var i = _Q("$filter=contains(lastName, 'Duck') eq true"); - var o = _C("Filter: contains(lastName, 'Duck')"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_endswith() + { + var i = _Q("$filter=endswith(lastName, 'Duck')"); + var o = _C("Filter: endsWith(lastName, 'Duck')"); - [Fact] - public void Should_parse_filter_with_contains_to_false() - { - var i = _Q("$filter=contains(lastName, 'Duck') eq false"); - var o = _C("Filter: !(contains(lastName, 'Duck'))"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_matchs() + { + var i = _Q("$filter=matchs(lastName, 'Duck')"); + var o = _C("Filter: matchs(lastName, 'Duck')"); - [Fact] - public void Should_parse_filter_with_equals() - { - var i = _Q("$filter=age eq 1"); - var o = _C("Filter: age == 1"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_empty() + { + var i = _Q("$filter=empty(lastName)"); + var o = _C("Filter: empty(lastName)"); - [Fact] - public void Should_parse_filter_with_notequals() - { - var i = _Q("$filter=age ne 1"); - var o = _C("Filter: age != 1"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_empty_to_true() + { + var i = _Q("$filter=empty(lastName) eq true"); + var o = _C("Filter: empty(lastName)"); - [Fact] - public void Should_parse_filter_with_lessthan() - { - var i = _Q("$filter=age lt 1"); - var o = _C("Filter: age < 1"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_exists() + { + var i = _Q("$filter=exists(lastName)"); + var o = _C("Filter: exists(lastName)"); - [Fact] - public void Should_parse_filter_with_lessthanorequal() - { - var i = _Q("$filter=age le 1"); - var o = _C("Filter: age <= 1"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_exists_to_true() + { + var i = _Q("$filter=exists(lastName) eq true"); + var o = _C("Filter: exists(lastName)"); - [Fact] - public void Should_parse_filter_with_greaterthan() - { - var i = _Q("$filter=age gt 1"); - var o = _C("Filter: age > 1"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_exists_to_false() + { + var i = _Q("$filter=exists(lastName) eq false"); + var o = _C("Filter: !(exists(lastName))"); - [Fact] - public void Should_parse_filter_with_greaterthanorequal() - { - var i = _Q("$filter=age ge 1"); - var o = _C("Filter: age >= 1"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_contains() + { + var i = _Q("$filter=contains(lastName, 'Duck')"); + var o = _C("Filter: contains(lastName, 'Duck')"); - [Fact] - public void Should_parse_filter_with_conjunction_and_contains() - { - var i = _Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true"); - var o = _C("Filter: (!(contains(firstName, 'Sebastian')) && isComicFigure == True)"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_contains_to_true() + { + var i = _Q("$filter=contains(lastName, 'Duck') eq true"); + var o = _C("Filter: contains(lastName, 'Duck')"); - [Fact] - public void Should_parse_filter_with_conjunction() - { - var i = _Q("$filter=age eq 1 and age eq 2"); - var o = _C("Filter: (age == 1 && age == 2)"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_contains_to_false() + { + var i = _Q("$filter=contains(lastName, 'Duck') eq false"); + var o = _C("Filter: !(contains(lastName, 'Duck'))"); - [Fact] - public void Should_parse_filter_with_disjunction() - { - var i = _Q("$filter=age eq 1 or age eq 2"); - var o = _C("Filter: (age == 1 || age == 2)"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_equals() + { + var i = _Q("$filter=age eq 1"); + var o = _C("Filter: age == 1"); - [Fact] - public void Should_full_text() - { - var i = _Q("$search=Duck"); - var o = _C("FullText: 'Duck'"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_notequals() + { + var i = _Q("$filter=age ne 1"); + var o = _C("Filter: age != 1"); - [Fact] - public void Should_text_and_multiple_terms() - { - var i = _Q("$search=Dagobert or Donald"); - var o = _C("FullText: 'Dagobert or Donald'"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_lessthan() + { + var i = _Q("$filter=age lt 1"); + var o = _C("Filter: age < 1"); - [Fact] - public void Should_full_text_numbers() - { - var i = _Q("$search=\"33k\""); - var o = _C("FullText: '33k'"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_lessthanorequal() + { + var i = _Q("$filter=age le 1"); + var o = _C("Filter: age <= 1"); - [Fact] - public void Should_parse_orderby() - { - var i = _Q("$orderby=age desc"); - var o = _C("Sort: age Descending"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_greaterthan() + { + var i = _Q("$filter=age gt 1"); + var o = _C("Filter: age > 1"); - [Fact] - public void Should_parse_orderby_with_multiple_field() - { - var i = _Q("$orderby=age, incomeMio desc"); - var o = _C("Sort: age Ascending, incomeMio Descending"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_greaterthanorequal() + { + var i = _Q("$filter=age ge 1"); + var o = _C("Filter: age >= 1"); - [Fact] - public void Should_parse_top() - { - var i = _Q("$top=3"); - var o = _C("Take: 3"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_conjunction_and_contains() + { + var i = _Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true"); + var o = _C("Filter: (!(contains(firstName, 'Sebastian')) && isComicFigure == True)"); - [Fact] - public void Should_parse_skip() - { - var i = _Q("$skip=4"); - var o = _C("Skip: 4"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_conjunction() + { + var i = _Q("$filter=age eq 1 and age eq 2"); + var o = _C("Filter: (age == 1 && age == 2)"); - [Fact] - public void Should_parse_random() - { - var i = _Q("$random=4"); - var o = _C("Random: 4"); + Assert.Equal(o, i); + } - Assert.Equal(o, i); - } + [Fact] + public void Should_parse_filter_with_disjunction() + { + var i = _Q("$filter=age eq 1 or age eq 2"); + var o = _C("Filter: (age == 1 || age == 2)"); - private static string _C(string value) - { - return value.Replace('/', '.'); - } + Assert.Equal(o, i); + } - private static string? _Q(string value) - { - var parser = EdmModel.ParseQuery(value); + [Fact] + public void Should_full_text() + { + var i = _Q("$search=Duck"); + var o = _C("FullText: 'Duck'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_text_and_multiple_terms() + { + var i = _Q("$search=Dagobert or Donald"); + var o = _C("FullText: 'Dagobert or Donald'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_full_text_numbers() + { + var i = _Q("$search=\"33k\""); + var o = _C("FullText: '33k'"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_orderby() + { + var i = _Q("$orderby=age desc"); + var o = _C("Sort: age Descending"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_orderby_with_multiple_field() + { + var i = _Q("$orderby=age, incomeMio desc"); + var o = _C("Sort: age Ascending, incomeMio Descending"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_top() + { + var i = _Q("$top=3"); + var o = _C("Take: 3"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_skip() + { + var i = _Q("$skip=4"); + var o = _C("Skip: 4"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_random() + { + var i = _Q("$random=4"); + var o = _C("Random: 4"); + + Assert.Equal(o, i); + } + + private static string _C(string value) + { + return value.Replace('/', '.'); + } + + private static string? _Q(string value) + { + var parser = EdmModel.ParseQuery(value); - return parser?.ToQuery().ToString(); - } + return parser?.ToQuery().ToString(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs index 0a81e4b06c..7278493eda 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs @@ -10,176 +10,175 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public class QueryJsonTests { - public class QueryJsonTests + [Theory] + [InlineData("contains", "contains(property, 12)")] + [InlineData("endswith", "endsWith(property, 12)")] + [InlineData("eq", "property == 12")] + [InlineData("le", "property <= 12")] + [InlineData("lt", "property < 12")] + [InlineData("ge", "property >= 12")] + [InlineData("gt", "property > 12")] + [InlineData("ne", "property != 12")] + [InlineData("startswith", "startsWith(property, 12)")] + public void Should_convert_comparison(string op, string expected) { - [Theory] - [InlineData("contains", "contains(property, 12)")] - [InlineData("endswith", "endsWith(property, 12)")] - [InlineData("eq", "property == 12")] - [InlineData("le", "property <= 12")] - [InlineData("lt", "property < 12")] - [InlineData("ge", "property >= 12")] - [InlineData("gt", "property > 12")] - [InlineData("ne", "property != 12")] - [InlineData("startswith", "startsWith(property, 12)")] - public void Should_convert_comparison(string op, string expected) + var json = new { - var json = new - { - path = "property", - op, - value = 12 - }; + path = "property", + op, + value = 12 + }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal(expected, filter.ToString()); - } + Assert.Equal(expected, filter.ToString()); + } - [Fact] - public void Should_convert_comparison_without_operator() - { - var json = new { path = "property" }; + [Fact] + public void Should_convert_comparison_without_operator() + { + var json = new { path = "property" }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal("property == null", filter.ToString()); - } + Assert.Equal("property == null", filter.ToString()); + } - [Fact] - public void Should_convert_comparison_empty() - { - var json = new { path = "property", op = "empty" }; + [Fact] + public void Should_convert_comparison_empty() + { + var json = new { path = "property", op = "empty" }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal("empty(property)", filter.ToString()); - } + Assert.Equal("empty(property)", filter.ToString()); + } - [Fact] - public void Should_convert_comparison_with_radius() - { - var json = new { path = "property", op = "lt", value = new { latitude = 10, longitude = 20 } }; + [Fact] + public void Should_convert_comparison_with_radius() + { + var json = new { path = "property", op = "lt", value = new { latitude = 10, longitude = 20 } }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal("property < {\"latitude\":10, \"longitude\":20}", filter.ToString()); - } + Assert.Equal("property < {\"latitude\":10, \"longitude\":20}", filter.ToString()); + } - [Fact] - public void Should_convert_comparison_in() - { - var json = new { path = "property", op = "in", value = new[] { 12, 13 } }; + [Fact] + public void Should_convert_comparison_in() + { + var json = new { path = "property", op = "in", value = new[] { 12, 13 } }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal("property in [12, 13]", filter.ToString()); - } + Assert.Equal("property in [12, 13]", filter.ToString()); + } - [Fact] - public void Should_convert_comparison_with_deep_path() - { - var json = new { path = "property.nested", op = "eq", value = 12 }; + [Fact] + public void Should_convert_comparison_with_deep_path() + { + var json = new { path = "property.nested", op = "eq", value = 12 }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal("property.nested == 12", filter.ToString()); - } + Assert.Equal("property.nested == 12", filter.ToString()); + } - [Fact] - public void Should_convert_logical_and() + [Fact] + public void Should_convert_logical_and() + { + var json = new { - var json = new + and = new[] { - and = new[] - { - new { path = "property", op = "ge", value = 10 }, - new { path = "property", op = "lt", value = 20 } - } - }; + new { path = "property", op = "ge", value = 10 }, + new { path = "property", op = "lt", value = 20 } + } + }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal("(property >= 10 && property < 20)", filter.ToString()); - } + Assert.Equal("(property >= 10 && property < 20)", filter.ToString()); + } - [Fact] - public void Should_convert_logical_or() + [Fact] + public void Should_convert_logical_or() + { + var json = new { - var json = new + or = new[] { - or = new[] - { - new { path = "property", op = "ge", value = 10 }, - new { path = "property", op = "lt", value = 20 } - } - }; + new { path = "property", op = "ge", value = 10 }, + new { path = "property", op = "lt", value = 20 } + } + }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal("(property >= 10 || property < 20)", filter.ToString()); - } + Assert.Equal("(property >= 10 || property < 20)", filter.ToString()); + } - [Fact] - public void Should_convert_logical_not() + [Fact] + public void Should_convert_logical_not() + { + var json = new { - var json = new - { - not = new { path = "property", op = "ge", value = 10 } - }; + not = new { path = "property", op = "ge", value = 10 } + }; - var filter = SerializeAndDeserialize(json); + var filter = SerializeAndDeserialize(json); - Assert.Equal("!(property >= 10)", filter.ToString()); - } + Assert.Equal("!(property >= 10)", filter.ToString()); + } - [Fact] - public void Should_throw_exception_for_invalid_operator() - { - var json = new { path = "property", op = "invalid", value = 12 }; + [Fact] + public void Should_throw_exception_for_invalid_operator() + { + var json = new { path = "property", op = "invalid", value = 12 }; - Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); - } + Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); + } - [Fact] - public void Should_throw_exception_for_missing_path() - { - var json = new { op = "invalid", value = 12 }; + [Fact] + public void Should_throw_exception_for_missing_path() + { + var json = new { op = "invalid", value = 12 }; - Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); - } + Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); + } - [Fact] - public void Should_throw_exception_for_missing_value() - { - var json = new { path = "property", op = "invalid" }; + [Fact] + public void Should_throw_exception_for_missing_value() + { + var json = new { path = "property", op = "invalid" }; - Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); - } + Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json)); + } - [Fact] - public void Should_not_throw_exception_if_filter_has_unknown_property() + [Fact] + public void Should_not_throw_exception_if_filter_has_unknown_property() + { + var json = new { - var json = new + and = new[] { - and = new[] - { - new { path = "property", op = "ge", value = 10 }, - new { path = "property", op = "lt", value = 20 } - }, - additional = 1 - }; - - SerializeAndDeserialize(json); - } - - private static FilterNode<JsonValue> SerializeAndDeserialize<T>(T value) - { - var json = TestUtils.DefaultSerializer.Serialize(value, true); + new { path = "property", op = "ge", value = 10 }, + new { path = "property", op = "lt", value = 20 } + }, + additional = 1 + }; + + SerializeAndDeserialize(json); + } + + private static FilterNode<JsonValue> SerializeAndDeserialize<T>(T value) + { + var json = TestUtils.DefaultSerializer.Serialize(value, true); - return TestUtils.DefaultSerializer.Deserialize<FilterNode<JsonValue>>(json); - } + return TestUtils.DefaultSerializer.Deserialize<FilterNode<JsonValue>>(json); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs index 60ed820ede..3c3709bd75 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryOptimizationTests.cs @@ -7,88 +7,87 @@ using Xunit; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public class QueryOptimizationTests { - public class QueryOptimizationTests + [Fact] + public void Should_not_convert_optimize_valid_logical_filter() { - [Fact] - public void Should_not_convert_optimize_valid_logical_filter() - { - var source = ClrFilter.Or(ClrFilter.Eq("path", 2), ClrFilter.Eq("path", 3)); + var source = ClrFilter.Or(ClrFilter.Eq("path", 2), ClrFilter.Eq("path", 3)); - var actual = Optimizer<ClrValue>.Optimize(source); + var actual = Optimizer<ClrValue>.Optimize(source); - Assert.Equal("(path == 2 || path == 3)", actual!.ToString()); - } + Assert.Equal("(path == 2 || path == 3)", actual!.ToString()); + } - [Fact] - public void Should_return_filter_When_logical_filter_has_one_child() - { - var source = ClrFilter.And(ClrFilter.Eq("path", 1), ClrFilter.Or()); + [Fact] + public void Should_return_filter_When_logical_filter_has_one_child() + { + var source = ClrFilter.And(ClrFilter.Eq("path", 1), ClrFilter.Or()); - var actual = Optimizer<ClrValue>.Optimize(source); + var actual = Optimizer<ClrValue>.Optimize(source); - Assert.Equal("path == 1", actual!.ToString()); - } + Assert.Equal("path == 1", actual!.ToString()); + } - [Fact] - public void Should_return_null_if_filters_of_logical_filter_get_optimized_away() - { - var source = ClrFilter.And(ClrFilter.And()); + [Fact] + public void Should_return_null_if_filters_of_logical_filter_get_optimized_away() + { + var source = ClrFilter.And(ClrFilter.And()); - var actual = Optimizer<ClrValue>.Optimize(source); + var actual = Optimizer<ClrValue>.Optimize(source); - Assert.Null(actual); - } + Assert.Null(actual); + } - [Fact] - public void Should_return_null_if_logical_filter_has_no_filter() - { - var source = ClrFilter.And(); + [Fact] + public void Should_return_null_if_logical_filter_has_no_filter() + { + var source = ClrFilter.And(); - var actual = Optimizer<ClrValue>.Optimize(source); + var actual = Optimizer<ClrValue>.Optimize(source); - Assert.Null(actual); - } + Assert.Null(actual); + } - [Fact] - public void Should_return_null_if_filter_of_negation_get_optimized_away() - { - var source = ClrFilter.Not(ClrFilter.And()); + [Fact] + public void Should_return_null_if_filter_of_negation_get_optimized_away() + { + var source = ClrFilter.Not(ClrFilter.And()); - var actual = Optimizer<ClrValue>.Optimize(source); + var actual = Optimizer<ClrValue>.Optimize(source); - Assert.Null(actual); - } + Assert.Null(actual); + } - [Fact] - public void Should_invert_equals_not_filter() - { - var source = ClrFilter.Not(ClrFilter.Eq("path", 1)); + [Fact] + public void Should_invert_equals_not_filter() + { + var source = ClrFilter.Not(ClrFilter.Eq("path", 1)); - var actual = Optimizer<ClrValue>.Optimize(source); + var actual = Optimizer<ClrValue>.Optimize(source); - Assert.Equal("path != 1", actual!.ToString()); - } + Assert.Equal("path != 1", actual!.ToString()); + } - [Fact] - public void Should_invert_notequals_not_filter() - { - var source = ClrFilter.Not(ClrFilter.Ne("path", 1)); + [Fact] + public void Should_invert_notequals_not_filter() + { + var source = ClrFilter.Not(ClrFilter.Ne("path", 1)); - var actual = Optimizer<ClrValue>.Optimize(source); + var actual = Optimizer<ClrValue>.Optimize(source); - Assert.Equal("path == 1", actual!.ToString()); - } + Assert.Equal("path == 1", actual!.ToString()); + } - [Fact] - public void Should_not_convert_number_operator() - { - var source = ClrFilter.Not(ClrFilter.Lt("path", 1)); + [Fact] + public void Should_not_convert_number_operator() + { + var source = ClrFilter.Not(ClrFilter.Lt("path", 1)); - var actual = Optimizer<ClrValue>.Optimize(source); + var actual = Optimizer<ClrValue>.Optimize(source); - Assert.Equal("!(path < 1)", actual!.ToString()); - } + Assert.Equal("!(path < 1)", actual!.ToString()); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryTests.cs index bd49d4fe6e..ebd3dff133 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryTests.cs @@ -7,57 +7,56 @@ using Xunit; -namespace Squidex.Infrastructure.Queries +namespace Squidex.Infrastructure.Queries; + +public class QueryTests { - public class QueryTests + [Fact] + public void Should_add_fields_from_sorting() { - [Fact] - public void Should_add_fields_from_sorting() + var query = new ClrQuery { - var query = new ClrQuery + Sort = new List<SortNode> { - Sort = new List<SortNode> - { - new SortNode("field1", SortOrder.Ascending), - new SortNode("field1", SortOrder.Ascending), - new SortNode("field2", SortOrder.Ascending) - } - }; + new SortNode("field1", SortOrder.Ascending), + new SortNode("field1", SortOrder.Ascending), + new SortNode("field2", SortOrder.Ascending) + } + }; - var fields = query.GetAllFields(); + var fields = query.GetAllFields(); - var expected = new HashSet<string> - { - "field1", - "field2" - }; + var expected = new HashSet<string> + { + "field1", + "field2" + }; - Assert.Equal(expected, fields); - } + Assert.Equal(expected, fields); + } - [Fact] - public void Should_add_fields_from_filters() + [Fact] + public void Should_add_fields_from_filters() + { + var query = new ClrQuery { - var query = new ClrQuery - { - Filter = - ClrFilter.And( - ClrFilter.Not( - ClrFilter.Eq("field1", 1)), - ClrFilter.Or( - ClrFilter.Eq("field2", 2), - ClrFilter.Eq("field2", 4))) - }; - - var fields = query.GetAllFields(); - - var expected = new HashSet<string> - { - "field1", - "field2" - }; + Filter = + ClrFilter.And( + ClrFilter.Not( + ClrFilter.Eq("field1", 1)), + ClrFilter.Or( + ClrFilter.Eq("field2", 2), + ClrFilter.Eq("field2", 4))) + }; + + var fields = query.GetAllFields(); + + var expected = new HashSet<string> + { + "field1", + "field2" + }; - Assert.Equal(expected, fields); - } + Assert.Equal(expected, fields); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs index d072b8f350..35fdc86e80 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/RandomHashTests.cs @@ -7,25 +7,24 @@ using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class RandomHashTests { - public class RandomHashTests + [Fact] + public void Should_create_long_hash() { - [Fact] - public void Should_create_long_hash() - { - var hash = RandomHash.New(); + var hash = RandomHash.New(); - Assert.Equal(44, hash.Length); - } + Assert.Equal(44, hash.Length); + } - [Fact] - public void Should_create_new_hashs() - { - var hash1 = RandomHash.New(); - var hash2 = RandomHash.New(); + [Fact] + public void Should_create_new_hashs() + { + var hash1 = RandomHash.New(); + var hash2 = RandomHash.New(); - Assert.NotEqual(hash1, hash2); - } + Assert.NotEqual(hash1, hash2); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs index c11597593c..88d75b27e6 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs @@ -8,107 +8,106 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class RefTokenTests { - public class RefTokenTests + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(":")] + public void Should_throw_exception_if_parsing_invalid_input(string input) + { + Assert.Throws<ArgumentException>(() => RefToken.Parse(input)); + } + + [Fact] + public void Should_instantiate_client_token() + { + var token = RefToken.Client("client1"); + + Assert.Equal("client1", token.Identifier); + Assert.True(token.IsClient); + } + + [Fact] + public void Should_instantiate_subject_token() + { + var token = RefToken.User("subject1"); + + Assert.Equal("subject1", token.Identifier); + Assert.True(token.IsUser); + } + + [Fact] + public void Should_instantiate_token_and_lower_type() + { + var token = RefToken.Client("client1"); + + Assert.Equal("client:client1", token.ToString()); + } + + [Fact] + public void Should_parse_token_without_type() { - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(":")] - public void Should_throw_exception_if_parsing_invalid_input(string input) - { - Assert.Throws<ArgumentException>(() => RefToken.Parse(input)); - } - - [Fact] - public void Should_instantiate_client_token() - { - var token = RefToken.Client("client1"); - - Assert.Equal("client1", token.Identifier); - Assert.True(token.IsClient); - } - - [Fact] - public void Should_instantiate_subject_token() - { - var token = RefToken.User("subject1"); - - Assert.Equal("subject1", token.Identifier); - Assert.True(token.IsUser); - } - - [Fact] - public void Should_instantiate_token_and_lower_type() - { - var token = RefToken.Client("client1"); - - Assert.Equal("client:client1", token.ToString()); - } - - [Fact] - public void Should_parse_token_without_type() - { - var token = RefToken.Parse("subject1"); - - Assert.Equal("subject1", token.Identifier); - Assert.True(token.IsUser); - } - - [Fact] - public void Should_parse_token_with_unknown_type() - { - var token = RefToken.Parse("user:subject1"); - - Assert.Equal("subject1", token.Identifier); - Assert.True(token.IsUser); - } - - [Fact] - public void Should_parse_token_from_string() - { - var token = RefToken.Parse("client:client1"); - - Assert.Equal("client1", token.Identifier); - Assert.True(token.IsClient); - } - - [Fact] - public void Should_parse_token_with_colon_in_identifier() - { - var token = RefToken.Parse("client:client1:app"); - - Assert.Equal("client1:app", token.Identifier); - Assert.True(token.IsClient); - } - - [Fact] - public void Should_convert_user_token_to_string() - { - var token = RefToken.Parse("client:client1"); - - Assert.Equal("client:client1", token.ToString()); - } - - [Fact] - public void Should_serialize_and_deserialize_null_token() - { - RefToken? value = null; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_token() - { - var value = RefToken.Parse("client:client1"); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } + var token = RefToken.Parse("subject1"); + + Assert.Equal("subject1", token.Identifier); + Assert.True(token.IsUser); + } + + [Fact] + public void Should_parse_token_with_unknown_type() + { + var token = RefToken.Parse("user:subject1"); + + Assert.Equal("subject1", token.Identifier); + Assert.True(token.IsUser); + } + + [Fact] + public void Should_parse_token_from_string() + { + var token = RefToken.Parse("client:client1"); + + Assert.Equal("client1", token.Identifier); + Assert.True(token.IsClient); + } + + [Fact] + public void Should_parse_token_with_colon_in_identifier() + { + var token = RefToken.Parse("client:client1:app"); + + Assert.Equal("client1:app", token.Identifier); + Assert.True(token.IsClient); + } + + [Fact] + public void Should_convert_user_token_to_string() + { + var token = RefToken.Parse("client:client1"); + + Assert.Equal("client:client1", token.ToString()); + } + + [Fact] + public void Should_serialize_and_deserialize_null_token() + { + RefToken? value = null; + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_token() + { + var value = RefToken.Parse("client:client1"); + + var serialized = value.SerializeAndDeserialize(); + + Assert.Equal(value, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs index 06378fc2c9..e1d94f97f7 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs @@ -8,93 +8,92 @@ using Squidex.Infrastructure.Reflection.Internal; using Xunit; -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +public class PropertiesTypeAccessorTests { - public class PropertiesTypeAccessorTests + public class TestClass { - public class TestClass - { - private int target; - - public int ReadWrite - { - get - { - return target; - } - set - { - target = value; - } - } + private int target; - public int Read + public int ReadWrite + { + get { - get => target; + return target; } - - public int Write + set { - set => target = value; + target = value; } } - private readonly TestClass target = new TestClass(); + public int Read + { + get => target; + } - [Fact] - public void Should_set_read_write_property() + public int Write { - var sut = new PropertyAccessor(typeof(TestClass).GetProperty("ReadWrite")!); + set => target = value; + } + } - sut.Set(target, 123); + private readonly TestClass target = new TestClass(); - Assert.Equal(123, target.Read); - } + [Fact] + public void Should_set_read_write_property() + { + var sut = new PropertyAccessor(typeof(TestClass).GetProperty("ReadWrite")!); - [Fact] - public void Should_set_write_property() - { - var accessor = new PropertyAccessor(typeof(TestClass).GetProperty("Write")!); + sut.Set(target, 123); - accessor.Set(target, 123); + Assert.Equal(123, target.Read); + } - Assert.Equal(123, target.Read); - } + [Fact] + public void Should_set_write_property() + { + var accessor = new PropertyAccessor(typeof(TestClass).GetProperty("Write")!); - [Fact] - public void Should_throw_exception_if_setting_readonly() - { - var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Read")!); + accessor.Set(target, 123); - Assert.Throws<NotSupportedException>(() => sut.Set(target, 123)); - } + Assert.Equal(123, target.Read); + } - [Fact] - public void Should_get_read_write_property() - { - var sut = new PropertyAccessor(typeof(TestClass).GetProperty("ReadWrite")!); + [Fact] + public void Should_throw_exception_if_setting_readonly() + { + var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Read")!); - target.Write = 123; + Assert.Throws<NotSupportedException>(() => sut.Set(target, 123)); + } - Assert.Equal(123, sut.Get(target)); - } + [Fact] + public void Should_get_read_write_property() + { + var sut = new PropertyAccessor(typeof(TestClass).GetProperty("ReadWrite")!); - [Fact] - public void Should_get_read_property() - { - var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Read")!); + target.Write = 123; - target.Write = 123; + Assert.Equal(123, sut.Get(target)); + } - Assert.Equal(123, sut.Get(target)); - } + [Fact] + public void Should_get_read_property() + { + var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Read")!); - [Fact] - public void Should_throw_exception_if_getting_writeonly_property() - { - var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Write")!); + target.Write = 123; - Assert.Throws<NotSupportedException>(() => sut.Get(target)); - } + Assert.Equal(123, sut.Get(target)); + } + + [Fact] + public void Should_throw_exception_if_getting_writeonly_property() + { + var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Write")!); + + Assert.Throws<NotSupportedException>(() => sut.Get(target)); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs index 17328259b7..aa97177e36 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs @@ -8,44 +8,43 @@ using Squidex.Infrastructure.Reflection.Internal; using Xunit; -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +public class ReflectionExtensionTests { - public class ReflectionExtensionTests + private interface IMyMain : IMySub1 + { + string MainProp { get; set; } + } + + private interface IMySub1 : IMySub2 + { + string Sub1Prop { get; set; } + } + + private interface IMySub2 + { + string Sub2Prop { get; set; } + } + + private sealed class MyMain { - private interface IMyMain : IMySub1 - { - string MainProp { get; set; } - } - - private interface IMySub1 : IMySub2 - { - string Sub1Prop { get; set; } - } - - private interface IMySub2 - { - string Sub2Prop { get; set; } - } - - private sealed class MyMain - { - public string MainProp { get; set; } - } - - [Fact] - public void Should_find_all_public_properties_of_interfaces() - { - var properties = typeof(IMyMain).GetPublicProperties().Select(x => x.Name).OrderBy(x => x).ToArray(); - - Assert.Equal(new[] { "MainProp", "Sub1Prop", "Sub2Prop" }, properties); - } - - [Fact] - public void Should_find_all_public_properties_of_classes() - { - var properties = typeof(MyMain).GetPublicProperties().Select(x => x.Name).OrderBy(x => x).ToArray(); - - Assert.Equal(new[] { "MainProp" }, properties); - } + public string MainProp { get; set; } + } + + [Fact] + public void Should_find_all_public_properties_of_interfaces() + { + var properties = typeof(IMyMain).GetPublicProperties().Select(x => x.Name).OrderBy(x => x).ToArray(); + + Assert.Equal(new[] { "MainProp", "Sub1Prop", "Sub2Prop" }, properties); + } + + [Fact] + public void Should_find_all_public_properties_of_classes() + { + var properties = typeof(MyMain).GetPublicProperties().Select(x => x.Name).OrderBy(x => x).ToArray(); + + Assert.Equal(new[] { "MainProp" }, properties); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs index c7094d758a..def3bb485e 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleMapperTests.cs @@ -8,185 +8,184 @@ using System.Diagnostics; using Xunit; -namespace Squidex.Infrastructure.Reflection +namespace Squidex.Infrastructure.Reflection; + +public class SimpleMapperTests { - public class SimpleMapperTests + public class Class1Base<T1> { - public class Class1Base<T1> - { - public T1 P1 { get; set; } - } + public T1 P1 { get; set; } + } - public class Class1<T1, T2> : Class1Base<T1> - { - public T2 P2 { get; set; } - } + public class Class1<T1, T2> : Class1Base<T1> + { + public T2 P2 { get; set; } + } - public class Class2Base<T2> - { - public T2 P2 { get; set; } - } + public class Class2Base<T2> + { + public T2 P2 { get; set; } + } - public class Class2<T2, T3> : Class2Base<T2> - { - public T3 P3 { get; set; } - } + public class Class2<T2, T3> : Class2Base<T2> + { + public T3 P3 { get; set; } + } - public class Readonly<T> - { - public T P1 { get; } - } + public class Readonly<T> + { + public T P1 { get; } + } - public class Writeonly<T> - { + public class Writeonly<T> + { #pragma warning disable MA0041 // Make property static - public T P1 + public T P1 #pragma warning restore MA0041 // Make property static - { - set => Debug.WriteLine(value); - } - } - - [Fact] - public void Should_throw_exception_if_mapping_with_null_source() { - Assert.Throws<ArgumentNullException>(() => SimpleMapper.Map((Class2<int, int>?)null!, new Class2<int, int>())); + set => Debug.WriteLine(value); } + } - [Fact] - public void Should_throw_exception_if_mapping_with_null_target() - { - Assert.Throws<ArgumentNullException>(() => SimpleMapper.Map(new Class2<int, int>(), (Class2<int, int>?)null!)); - } + [Fact] + public void Should_throw_exception_if_mapping_with_null_source() + { + Assert.Throws<ArgumentNullException>(() => SimpleMapper.Map((Class2<int, int>?)null!, new Class2<int, int>())); + } - [Fact] - public void Should_map_to_same_type() - { - var obj1 = new Class1<int, int> - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class2<int, int>()); - - Assert.Equal(8, obj2.P2); - Assert.Equal(0, obj2.P3); - } + [Fact] + public void Should_throw_exception_if_mapping_with_null_target() + { + Assert.Throws<ArgumentNullException>(() => SimpleMapper.Map(new Class2<int, int>(), (Class2<int, int>?)null!)); + } - [Fact] - public void Should_map_all_properties() + [Fact] + public void Should_map_to_same_type() + { + var obj1 = new Class1<int, int> { - var obj1 = new Class1<int, int> - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class1<int, int>()); - - Assert.Equal(6, obj2.P1); - Assert.Equal(8, obj2.P2); - } + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class2<int, int>()); - [Fact] - public void Should_map_to_convertible_type() - { - var obj1 = new Class1<long, long> - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class2<int, int>()); - - Assert.Equal(8, obj2.P2); - Assert.Equal(0, obj2.P3); - } + Assert.Equal(8, obj2.P2); + Assert.Equal(0, obj2.P3); + } - [Fact] - public void Should_map_from_nullable() + [Fact] + public void Should_map_all_properties() + { + var obj1 = new Class1<int, int> { - var obj1 = new Class1<long?, long?> - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class2<long, long>()); - - Assert.Equal(8, obj2.P2); - Assert.Equal(0, obj2.P3); - } + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class1<int, int>()); - [Fact] - public void Should_map_to_nullable() + Assert.Equal(6, obj2.P1); + Assert.Equal(8, obj2.P2); + } + + [Fact] + public void Should_map_to_convertible_type() + { + var obj1 = new Class1<long, long> { - var obj1 = new Class1<long, long> - { - P1 = 6, - P2 = 8 - }; - var obj2 = SimpleMapper.Map(obj1, new Class2<long?, long?>()); - - Assert.Equal(8, obj2.P2); - Assert.Null(obj2.P3); - } + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class2<int, int>()); + + Assert.Equal(8, obj2.P2); + Assert.Equal(0, obj2.P3); + } - [Fact] - public void Should_map_if_convertible_is_null() + [Fact] + public void Should_map_from_nullable() + { + var obj1 = new Class1<long?, long?> { - var obj1 = new Class1<int?, int?> - { - P1 = null, - P2 = null - }; - var obj2 = SimpleMapper.Map(obj1, new Class1<int, int>()); - - Assert.Equal(0, obj2.P1); - Assert.Equal(0, obj2.P2); - } + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class2<long, long>()); + + Assert.Equal(8, obj2.P2); + Assert.Equal(0, obj2.P3); + } - [Fact] - public void Should_convert_to_string() + [Fact] + public void Should_map_to_nullable() + { + var obj1 = new Class1<long, long> { - var obj1 = new Class1<RefToken, RefToken> - { - P1 = RefToken.User("1"), - P2 = RefToken.User("2") - }; - var obj2 = SimpleMapper.Map(obj1, new Class2<string, string>()); - - Assert.Equal("subject:2", obj2.P2); - Assert.Null(obj2.P3); - } + P1 = 6, + P2 = 8 + }; + var obj2 = SimpleMapper.Map(obj1, new Class2<long?, long?>()); + + Assert.Equal(8, obj2.P2); + Assert.Null(obj2.P3); + } - [Fact] - public void Should_return_default_if_conversion_failed() + [Fact] + public void Should_map_if_convertible_is_null() + { + var obj1 = new Class1<int?, int?> { - var obj1 = new Class1<long, long> - { - P1 = long.MaxValue, - P2 = long.MaxValue - }; - var obj2 = SimpleMapper.Map(obj1, new Class2<int, int>()); - - Assert.Equal(0, obj2.P2); - Assert.Equal(0, obj2.P3); - } + P1 = null, + P2 = null + }; + var obj2 = SimpleMapper.Map(obj1, new Class1<int, int>()); - [Fact] - public void Should_ignore_write_only() + Assert.Equal(0, obj2.P1); + Assert.Equal(0, obj2.P2); + } + + [Fact] + public void Should_convert_to_string() + { + var obj1 = new Class1<RefToken, RefToken> { - var obj1 = new Writeonly<int>(); - var obj2 = SimpleMapper.Map(obj1, new Class1<int, int>()); + P1 = RefToken.User("1"), + P2 = RefToken.User("2") + }; + var obj2 = SimpleMapper.Map(obj1, new Class2<string, string>()); - Assert.Equal(0, obj2.P1); - } + Assert.Equal("subject:2", obj2.P2); + Assert.Null(obj2.P3); + } - [Fact] - public void Should_ignore_read_only() + [Fact] + public void Should_return_default_if_conversion_failed() + { + var obj1 = new Class1<long, long> { - var obj1 = new Class1<int, int> { P1 = 10 }; - var obj2 = SimpleMapper.Map(obj1, new Readonly<int>()); + P1 = long.MaxValue, + P2 = long.MaxValue + }; + var obj2 = SimpleMapper.Map(obj1, new Class2<int, int>()); - Assert.Equal(0, obj2.P1); - } + Assert.Equal(0, obj2.P2); + Assert.Equal(0, obj2.P3); + } + + [Fact] + public void Should_ignore_write_only() + { + var obj1 = new Writeonly<int>(); + var obj2 = SimpleMapper.Map(obj1, new Class1<int, int>()); + + Assert.Equal(0, obj2.P1); + } + + [Fact] + public void Should_ignore_read_only() + { + var obj1 = new Class1<int, int> { P1 = 10 }; + var obj2 = SimpleMapper.Map(obj1, new Readonly<int>()); + + Assert.Equal(0, obj2.P1); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs index a3870f3764..7a9857e674 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs @@ -9,92 +9,91 @@ using NodaTime; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class RetryWindowTests { - public class RetryWindowTests + private readonly IClock clock = A.Fake<IClock>(); + + public RetryWindowTests() + { + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + } + + [Fact] + public void Should_allow_to_retry_after_reset() { - private readonly IClock clock = A.Fake<IClock>(); + var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5); - public RetryWindowTests() + for (var i = 0; i < 5 * 2; i++) { - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + sut.CanRetryAfterFailure(); } - [Fact] - public void Should_allow_to_retry_after_reset() - { - var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5); + sut.Reset(); - for (var i = 0; i < 5 * 2; i++) - { - sut.CanRetryAfterFailure(); - } + Assert.True(sut.CanRetryAfterFailure()); + } - sut.Reset(); + [Theory] + [InlineData(6)] + [InlineData(7)] + public void Should_not_allow_to_retry_after_many_errors(int errors) + { + var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); + for (var i = 0; i < 5; i++) + { Assert.True(sut.CanRetryAfterFailure()); } - [Theory] - [InlineData(6)] - [InlineData(7)] - public void Should_not_allow_to_retry_after_many_errors(int errors) - { - var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); - - for (var i = 0; i < 5; i++) - { - Assert.True(sut.CanRetryAfterFailure()); - } - - var remaining = errors - 5; + var remaining = errors - 5; - for (var i = 0; i < remaining; i++) - { - Assert.False(sut.CanRetryAfterFailure()); - } + for (var i = 0; i < remaining; i++) + { + Assert.False(sut.CanRetryAfterFailure()); } + } - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - public void Should_allow_to_retry_after_few_errors(int errors) - { - var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + public void Should_allow_to_retry_after_few_errors(int errors) + { + var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); - for (var i = 0; i < errors; i++) - { - Assert.True(sut.CanRetryAfterFailure()); - } + for (var i = 0; i < errors; i++) + { + Assert.True(sut.CanRetryAfterFailure()); } + } - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - [InlineData(6)] - [InlineData(7)] - [InlineData(8)] - public void Should_allow_to_retry_after_few_errors_in_window(int errors) - { - var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + public void Should_allow_to_retry_after_few_errors_in_window(int errors) + { + var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); - var now = SystemClock.Instance.GetCurrentInstant(); + var now = SystemClock.Instance.GetCurrentInstant(); - A.CallTo(() => clock.GetCurrentInstant()) - .ReturnsLazily(() => now); + A.CallTo(() => clock.GetCurrentInstant()) + .ReturnsLazily(() => now); - for (var i = 0; i < errors; i++) - { - now = now.Plus(Duration.FromMilliseconds(300)); + for (var i = 0; i < errors; i++) + { + now = now.Plus(Duration.FromMilliseconds(300)); - Assert.True(sut.CanRetryAfterFailure()); - } + Assert.True(sut.CanRetryAfterFailure()); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs index 656fdea577..885c328f25 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Security/ExtensionsTests.cs @@ -8,54 +8,53 @@ using System.Security.Claims; using Xunit; -namespace Squidex.Infrastructure.Security +namespace Squidex.Infrastructure.Security; + +public class ExtensionsTests { - public class ExtensionsTests + [Fact] + public void Should_retrieve_subject() + { + TestClaimExtension(OpenIdClaims.Subject, x => x.OpenIdSubject()); + } + + [Fact] + public void Should_retrieve_client_id() { - [Fact] - public void Should_retrieve_subject() - { - TestClaimExtension(OpenIdClaims.Subject, x => x.OpenIdSubject()); - } - - [Fact] - public void Should_retrieve_client_id() - { - TestClaimExtension(OpenIdClaims.ClientId, x => x.OpenIdClientId()); - } - - [Fact] - public void Should_retrieve_preferred_user_name() - { - TestClaimExtension(OpenIdClaims.PreferredUserName, x => x.OpenIdPreferredUserName()); - } - - [Fact] - public void Should_retrieve_name() - { - TestClaimExtension(OpenIdClaims.Name, x => x.OpenIdName()); - } - - [Fact] - public void Should_retrieve_email() - { - TestClaimExtension(OpenIdClaims.Email, x => x.OpenIdEmail()); - } - - private static void TestClaimExtension(string claimType, Func<ClaimsPrincipal, string?> getter) - { - var claimValue = Guid.NewGuid().ToString(); - - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(); - - claimsIdentity.AddClaim(new Claim(claimType, claimValue)); - - Assert.Null(getter(claimsPrincipal)); - - claimsPrincipal.AddIdentity(claimsIdentity); - - Assert.Equal(claimValue, getter(claimsPrincipal)); - } + TestClaimExtension(OpenIdClaims.ClientId, x => x.OpenIdClientId()); + } + + [Fact] + public void Should_retrieve_preferred_user_name() + { + TestClaimExtension(OpenIdClaims.PreferredUserName, x => x.OpenIdPreferredUserName()); + } + + [Fact] + public void Should_retrieve_name() + { + TestClaimExtension(OpenIdClaims.Name, x => x.OpenIdName()); + } + + [Fact] + public void Should_retrieve_email() + { + TestClaimExtension(OpenIdClaims.Email, x => x.OpenIdEmail()); + } + + private static void TestClaimExtension(string claimType, Func<ClaimsPrincipal, string?> getter) + { + var claimValue = Guid.NewGuid().ToString(); + + var claimsIdentity = new ClaimsIdentity(); + var claimsPrincipal = new ClaimsPrincipal(); + + claimsIdentity.AddClaim(new Claim(claimType, claimValue)); + + Assert.Null(getter(claimsPrincipal)); + + claimsPrincipal.AddIdentity(claimsIdentity); + + Assert.Equal(claimValue, getter(claimsPrincipal)); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs index f0815acbcd..8cd1cdf026 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs @@ -9,138 +9,137 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Security +namespace Squidex.Infrastructure.Security; + +public sealed class PermissionSetTests { - public sealed class PermissionSetTests + [Fact] + public void Should_provide_collection_features() { - [Fact] - public void Should_provide_collection_features() + var source = new List<Permission> { - var source = new List<Permission> - { - new Permission("c"), - new Permission("b"), - new Permission("a") - }; + new Permission("c"), + new Permission("b"), + new Permission("a") + }; - var sut = new PermissionSet(source); + var sut = new PermissionSet(source); - Assert.Equal(sut.ToList(), source); - Assert.Equal(((IEnumerable)sut).OfType<Permission>().ToList(), source); + Assert.Equal(sut.ToList(), source); + Assert.Equal(((IEnumerable)sut).OfType<Permission>().ToList(), source); - Assert.Equal(3, source.Count); + Assert.Equal(3, source.Count); - Assert.Equal("c;b;a", sut.ToString()); - } + Assert.Equal("c;b;a", sut.ToString()); + } - [Fact] - public void Should_give_permission_if_any_permission_allows() - { - var sut = new PermissionSet( - new Permission("app.contents"), - new Permission("app.assets")); + [Fact] + public void Should_give_permission_if_any_permission_allows() + { + var sut = new PermissionSet( + new Permission("app.contents"), + new Permission("app.assets")); - Assert.True(sut.Allows(new Permission("app.contents"))); - } + Assert.True(sut.Allows(new Permission("app.contents"))); + } - [Fact] - public void Should_not_give_permission_if_none_permission_allows() - { - var sut = new PermissionSet( - new Permission("app.contents"), - new Permission("app.assets")); + [Fact] + public void Should_not_give_permission_if_none_permission_allows() + { + var sut = new PermissionSet( + new Permission("app.contents"), + new Permission("app.assets")); - Assert.False(sut.Allows(new Permission("app.schemas"))); - } + Assert.False(sut.Allows(new Permission("app.schemas"))); + } - [Fact] - public void Should_not_give_permission_if_requested_is_null() - { - var sut = new PermissionSet( - new Permission("app.contents"), - new Permission("app.assets")); + [Fact] + public void Should_not_give_permission_if_requested_is_null() + { + var sut = new PermissionSet( + new Permission("app.contents"), + new Permission("app.assets")); - Assert.False(sut.Allows(null)); - } + Assert.False(sut.Allows(null)); + } - [Fact] - public void Should_include_permission_if_any_permission_includes_parent_given() - { - var sut = new PermissionSet( - new Permission("app.contents"), - new Permission("app.assets")); + [Fact] + public void Should_include_permission_if_any_permission_includes_parent_given() + { + var sut = new PermissionSet( + new Permission("app.contents"), + new Permission("app.assets")); - Assert.True(sut.Includes(new Permission("app"))); - } + Assert.True(sut.Includes(new Permission("app"))); + } - [Fact] - public void Should_include_permission_if_any_permission_includes_child_given() - { - var sut = new PermissionSet( - new Permission("app.contents"), - new Permission("app.assets")); + [Fact] + public void Should_include_permission_if_any_permission_includes_child_given() + { + var sut = new PermissionSet( + new Permission("app.contents"), + new Permission("app.assets")); - Assert.True(sut.Includes(new Permission("app.contents.read"))); - } + Assert.True(sut.Includes(new Permission("app.contents.read"))); + } - [Fact] - public void Should_include_permission_even_if_negation_exists() - { - var sut = new PermissionSet( - new Permission("app.contents"), - new Permission("app.assets")); + [Fact] + public void Should_include_permission_even_if_negation_exists() + { + var sut = new PermissionSet( + new Permission("app.contents"), + new Permission("app.assets")); - Assert.True(sut.Includes(new Permission("app.contents.read"))); - } + Assert.True(sut.Includes(new Permission("app.contents.read"))); + } - [Fact] - public void Should_not_include_permission_if_none_permission_includes_given() - { - var sut = new PermissionSet( - new Permission("app.contents"), - new Permission("app.assets")); + [Fact] + public void Should_not_include_permission_if_none_permission_includes_given() + { + var sut = new PermissionSet( + new Permission("app.contents"), + new Permission("app.assets")); - Assert.False(sut.Includes(new Permission("other"))); - } + Assert.False(sut.Includes(new Permission("other"))); + } - [Fact] - public void Should_not_include_permission_if_permission_to_include_is_null() - { - var sut = new PermissionSet( - new Permission("app.contents"), - new Permission("app.assets")); + [Fact] + public void Should_not_include_permission_if_permission_to_include_is_null() + { + var sut = new PermissionSet( + new Permission("app.contents"), + new Permission("app.assets")); - Assert.False(sut.Includes(null)); - } + Assert.False(sut.Includes(null)); + } - [Fact] - public void Should_add_permission_by_string() - { - var sut = - new PermissionSet("app.contents") - .Add("admin.*"); + [Fact] + public void Should_add_permission_by_string() + { + var sut = + new PermissionSet("app.contents") + .Add("admin.*"); - Assert.True(sut.Includes(new Permission("admin"))); - } + Assert.True(sut.Includes(new Permission("admin"))); + } - [Fact] - public void Should_add_permission() - { - var sut = - new PermissionSet("app.contents") - .Add(new Permission("admin.*")); + [Fact] + public void Should_add_permission() + { + var sut = + new PermissionSet("app.contents") + .Add(new Permission("admin.*")); - Assert.True(sut.Includes(new Permission("admin"))); - } + Assert.True(sut.Includes(new Permission("admin"))); + } - [Fact] - public void Should_serialize_and_deserialize() - { - var permissions = new PermissionSet("a", "b", "c"); + [Fact] + public void Should_serialize_and_deserialize() + { + var permissions = new PermissionSet("a", "b", "c"); - var serialized = permissions.SerializeAndDeserialize(); + var serialized = permissions.SerializeAndDeserialize(); - Assert.Equal(permissions, serialized); - } + Assert.Equal(permissions, serialized); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs index c1a20168da..cf4f057bb2 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs @@ -7,173 +7,172 @@ using Xunit; -namespace Squidex.Infrastructure.Security +namespace Squidex.Infrastructure.Security; + +public class PermissionTests { - public class PermissionTests + [Fact] + public void Should_generate_permission() { - [Fact] - public void Should_generate_permission() - { - var sut = new Permission("app.contents"); + var sut = new Permission("app.contents"); - Assert.Equal("app.contents", sut.ToString()); - Assert.Equal("app.contents", sut.Id); - } + Assert.Equal("app.contents", sut.ToString()); + Assert.Equal("app.contents", sut.Id); + } - [Fact] - public void Should_allow_and_include_if_permissions_are_equal() - { - var g = new Permission("app.contents"); - var r = new Permission("app.contents"); + [Fact] + public void Should_allow_and_include_if_permissions_are_equal() + { + var g = new Permission("app.contents"); + var r = new Permission("app.contents"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_not_allow_and_include_if_permissions_are_not_equal() - { - var g = new Permission("app.contents"); - var r = new Permission("app.assets"); + [Fact] + public void Should_not_allow_and_include_if_permissions_are_not_equal() + { + var g = new Permission("app.contents"); + var r = new Permission("app.assets"); - Assert.False(g.Allows(r)); - Assert.False(g.Includes(r)); - } + Assert.False(g.Allows(r)); + Assert.False(g.Includes(r)); + } - [Fact] - public void Should_allow_and_include_if_permissions_have_same_wildcards() - { - var g = new Permission("app.*"); - var r = new Permission("app.*"); + [Fact] + public void Should_allow_and_include_if_permissions_have_same_wildcards() + { + var g = new Permission("app.*"); + var r = new Permission("app.*"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_allow_and_include_if_given_is_parent_of_requested() - { - var g = new Permission("app"); - var r = new Permission("app.contents"); + [Fact] + public void Should_allow_and_include_if_given_is_parent_of_requested() + { + var g = new Permission("app"); + var r = new Permission("app.contents"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_not_allow_but_include_if_requested_is_parent_of_given() - { - var g = new Permission("app.contents"); - var r = new Permission("app"); + [Fact] + public void Should_not_allow_but_include_if_requested_is_parent_of_given() + { + var g = new Permission("app.contents"); + var r = new Permission("app"); - Assert.False(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.False(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_allow_and_include_if_given_is_wildcard_of_requested() - { - var g = new Permission("app.*"); - var r = new Permission("app.contents"); + [Fact] + public void Should_allow_and_include_if_given_is_wildcard_of_requested() + { + var g = new Permission("app.*"); + var r = new Permission("app.contents"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_not_allow_but_include_if_given_is_wildcard_of_requested() - { - var g = new Permission("app.contents"); - var r = new Permission("app.*"); + [Fact] + public void Should_not_allow_but_include_if_given_is_wildcard_of_requested() + { + var g = new Permission("app.contents"); + var r = new Permission("app.*"); - Assert.False(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.False(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_allow_and_include_if_given_has_alternatives_of_requested() - { - var g = new Permission("app.contents|schemas"); - var r = new Permission("app.contents"); + [Fact] + public void Should_allow_and_include_if_given_has_alternatives_of_requested() + { + var g = new Permission("app.contents|schemas"); + var r = new Permission("app.contents"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_allow_and_include_if_given_has_not_excluded_requested() - { - var g = new Permission("app.^schemas"); - var r = new Permission("app.contents"); + [Fact] + public void Should_allow_and_include_if_given_has_not_excluded_requested() + { + var g = new Permission("app.^schemas"); + var r = new Permission("app.contents"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_allow_and_include_if_requested_has_not_excluded_given() - { - var g = new Permission("app.contents"); - var r = new Permission("app.^schemas"); + [Fact] + public void Should_allow_and_include_if_requested_has_not_excluded_given() + { + var g = new Permission("app.contents"); + var r = new Permission("app.^schemas"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_not_allow_and_include_if_given_has_excluded_requested() - { - var g = new Permission("app.^contents"); - var r = new Permission("app.contents"); + [Fact] + public void Should_not_allow_and_include_if_given_has_excluded_requested() + { + var g = new Permission("app.^contents"); + var r = new Permission("app.contents"); - Assert.False(g.Allows(r)); - Assert.False(g.Includes(r)); - } + Assert.False(g.Allows(r)); + Assert.False(g.Includes(r)); + } - [Fact] - public void Should_not_allow_and_include_if_given_and_requested_have_same_exclusion() - { - var g = new Permission("app.^contents"); - var r = new Permission("app.^contents"); + [Fact] + public void Should_not_allow_and_include_if_given_and_requested_have_same_exclusion() + { + var g = new Permission("app.^contents"); + var r = new Permission("app.^contents"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_allow_and_include_if_requested_is_has_alternatives_of_given() - { - var g = new Permission("app.contents"); - var r = new Permission("app.contents|schemas"); + [Fact] + public void Should_allow_and_include_if_requested_is_has_alternatives_of_given() + { + var g = new Permission("app.contents"); + var r = new Permission("app.contents|schemas"); - Assert.True(g.Allows(r)); - Assert.True(g.Includes(r)); - } + Assert.True(g.Allows(r)); + Assert.True(g.Includes(r)); + } - [Fact] - public void Should_sort_by_name() - { - var source = new List<Permission> - { - new Permission("c"), - new Permission("b"), - new Permission("a") - }; - - var sorted = source.OrderBy(x => x).ToList(); - - Assert.Equal(new List<Permission> { source[2], source[1], source[0] }, sorted); - } - - [Theory] - [InlineData("permission")] - [InlineData("permission...")] - [InlineData("permission.||..")] - public void Should_parse_invalid_permissions(string source) + [Fact] + public void Should_sort_by_name() + { + var source = new List<Permission> { - var permission = new Permission(source); + new Permission("c"), + new Permission("b"), + new Permission("a") + }; + + var sorted = source.OrderBy(x => x).ToList(); + + Assert.Equal(new List<Permission> { source[2], source[1], source[0] }, sorted); + } + + [Theory] + [InlineData("permission")] + [InlineData("permission...")] + [InlineData("permission.||..")] + public void Should_parse_invalid_permissions(string source) + { + var permission = new Permission(source); - permission.Allows(new Permission(Permission.Any)); - } + permission.Allows(new Permission(Permission.Any)); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/DefaultEventStreamNamesTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/DefaultEventStreamNamesTests.cs index 6c759ac966..ca22b677ec 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/DefaultEventStreamNamesTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/DefaultEventStreamNamesTests.cs @@ -7,36 +7,35 @@ using Xunit; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public class DefaultEventStreamNamesTests { - public class DefaultEventStreamNamesTests - { - private readonly DefaultEventStreamNames sut = new DefaultEventStreamNames(); + private readonly DefaultEventStreamNames sut = new DefaultEventStreamNames(); - private sealed class MyUser - { - } + private sealed class MyUser + { + } - private sealed class MyUserDomainObject - { - } + private sealed class MyUserDomainObject + { + } - private readonly string id = Guid.NewGuid().ToString(); + private readonly string id = Guid.NewGuid().ToString(); - [Fact] - public void Should_calculate_name() - { - var name = sut.GetStreamName(typeof(MyUser), id); + [Fact] + public void Should_calculate_name() + { + var name = sut.GetStreamName(typeof(MyUser), id); - Assert.Equal($"myUser-{id}", name); - } + Assert.Equal($"myUser-{id}", name); + } - [Fact] - public void Should_calculate_name_and_remove_suffix() - { - var name = sut.GetStreamName(typeof(MyUserDomainObject), id); + [Fact] + public void Should_calculate_name_and_remove_suffix() + { + var name = sut.GetStreamName(typeof(MyUserDomainObject), id); - Assert.Equal($"myUser-{id}", name); - } + Assert.Equal($"myUser-{id}", name); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs index f8a4bbd99c..5204d30d7d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/InconsistentStateExceptionTests.cs @@ -8,24 +8,23 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public class InconsistentStateExceptionTests { - public class InconsistentStateExceptionTests + [Fact] + public void Should_serialize_and_deserialize() { - [Fact] - public void Should_serialize_and_deserialize() - { - var source = new InconsistentStateException(100, 200, new InvalidOperationException("Inner")); - var actual = source.SerializeAndDeserializeBinary(); + var source = new InconsistentStateException(100, 200, new InvalidOperationException("Inner")); + var actual = source.SerializeAndDeserializeBinary(); - Assert.IsType<InvalidOperationException>(actual.InnerException); + Assert.IsType<InvalidOperationException>(actual.InnerException); - Assert.Equal("Inner", actual.InnerException?.Message); + Assert.Equal("Inner", actual.InnerException?.Message); - Assert.Equal(actual.VersionExpected, source.VersionExpected); - Assert.Equal(actual.VersionCurrent, source.VersionCurrent); + Assert.Equal(actual.VersionExpected, source.VersionExpected); + Assert.Equal(actual.VersionCurrent, source.VersionCurrent); - Assert.Equal(actual.Message, source.Message); - } + Assert.Equal(actual.Message, source.Message); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceBatchTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceBatchTests.cs index f59a4cf774..9d0cb23dc0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceBatchTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceBatchTests.cs @@ -11,213 +11,212 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public class PersistenceBatchTests { - public class PersistenceBatchTests + private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly IEventStreamNames eventStreamNames = A.Fake<IEventStreamNames>(); + private readonly ISnapshotStore<int> snapshotStore = A.Fake<ISnapshotStore<int>>(); + private readonly IStore<int> sut; + + public PersistenceBatchTests() { - private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly IEventStreamNames eventStreamNames = A.Fake<IEventStreamNames>(); - private readonly ISnapshotStore<int> snapshotStore = A.Fake<ISnapshotStore<int>>(); - private readonly IStore<int> sut; + A.CallTo(() => eventStreamNames.GetStreamName(None.Type, A<string>._)) + .ReturnsLazily(x => x.GetArgument<string>(1)!); - public PersistenceBatchTests() - { - A.CallTo(() => eventStreamNames.GetStreamName(None.Type, A<string>._)) - .ReturnsLazily(x => x.GetArgument<string>(1)!); + sut = new Store<int>(eventFormatter, eventStore, eventStreamNames, snapshotStore); + } - sut = new Store<int>(eventFormatter, eventStore, eventStreamNames, snapshotStore); - } + [Fact] + public async Task Should_read_from_preloaded_events() + { + var event1_1 = new MyEvent { MyProperty = "event1_1" }; + var event1_2 = new MyEvent { MyProperty = "event1_2" }; + var event2_1 = new MyEvent { MyProperty = "event2_1" }; + var event2_2 = new MyEvent { MyProperty = "event2_2" }; - [Fact] - public async Task Should_read_from_preloaded_events() - { - var event1_1 = new MyEvent { MyProperty = "event1_1" }; - var event1_2 = new MyEvent { MyProperty = "event1_2" }; - var event2_1 = new MyEvent { MyProperty = "event2_1" }; - var event2_2 = new MyEvent { MyProperty = "event2_2" }; + var key1 = DomainId.NewGuid(); + var key2 = DomainId.NewGuid(); - var key1 = DomainId.NewGuid(); - var key2 = DomainId.NewGuid(); + var bulk = sut.WithBatchContext(None.Type); - var bulk = sut.WithBatchContext(None.Type); + SetupEventStore(new Dictionary<DomainId, List<MyEvent>> + { + [key1] = new List<MyEvent> { event1_1, event1_2 }, + [key2] = new List<MyEvent> { event2_1, event2_2 } + }); - SetupEventStore(new Dictionary<DomainId, List<MyEvent>> - { - [key1] = new List<MyEvent> { event1_1, event1_2 }, - [key2] = new List<MyEvent> { event2_1, event2_2 } - }); + await bulk.LoadAsync(new[] { key1, key2 }); - await bulk.LoadAsync(new[] { key1, key2 }); + var persistedEvents1 = Save.Events(); + var persistence1 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1.Write); - var persistedEvents1 = Save.Events(); - var persistence1 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1.Write); + await persistence1.ReadAsync(); - await persistence1.ReadAsync(); + var persistedEvents2 = Save.Events(); + var persistence2 = bulk.WithEventSourcing(None.Type, key2, persistedEvents2.Write); - var persistedEvents2 = Save.Events(); - var persistence2 = bulk.WithEventSourcing(None.Type, key2, persistedEvents2.Write); + await persistence2.ReadAsync(); - await persistence2.ReadAsync(); + Assert.Equal(persistedEvents1.ToArray(), new[] { event1_1, event1_2 }); + Assert.Equal(persistedEvents2.ToArray(), new[] { event2_1, event2_2 }); + } - Assert.Equal(persistedEvents1.ToArray(), new[] { event1_1, event1_2 }); - Assert.Equal(persistedEvents2.ToArray(), new[] { event2_1, event2_2 }); - } + [Fact] + public async Task Should_provide_empty_events_if_nothing_loaded() + { + var key = DomainId.NewGuid(); - [Fact] - public async Task Should_provide_empty_events_if_nothing_loaded() - { - var key = DomainId.NewGuid(); + var bulk = sut.WithBatchContext(None.Type); - var bulk = sut.WithBatchContext(None.Type); + await bulk.LoadAsync(new[] { key }); - await bulk.LoadAsync(new[] { key }); + var persistedEvents = Save.Events(); + var persistence = bulk.WithEventSourcing(None.Type, key, persistedEvents.Write); - var persistedEvents = Save.Events(); - var persistence = bulk.WithEventSourcing(None.Type, key, persistedEvents.Write); + await persistence.ReadAsync(); - await persistence.ReadAsync(); + Assert.Empty(persistedEvents.ToArray()); + Assert.Empty(persistedEvents.ToArray()); + } - Assert.Empty(persistedEvents.ToArray()); - Assert.Empty(persistedEvents.ToArray()); - } + [Fact] + public void Should_throw_exception_if_not_preloaded() + { + var key = DomainId.NewGuid(); - [Fact] - public void Should_throw_exception_if_not_preloaded() - { - var key = DomainId.NewGuid(); + var bulk = sut.WithBatchContext(None.Type); - var bulk = sut.WithBatchContext(None.Type); + Assert.Throws<KeyNotFoundException>(() => bulk.WithEventSourcing(None.Type, key, null)); + } - Assert.Throws<KeyNotFoundException>(() => bulk.WithEventSourcing(None.Type, key, null)); - } + [Fact] + public async Task Should_write_batched() + { + var key1 = DomainId.NewGuid(); + var key2 = DomainId.NewGuid(); - [Fact] - public async Task Should_write_batched() - { - var key1 = DomainId.NewGuid(); - var key2 = DomainId.NewGuid(); + var bulk = sut.WithBatchContext(None.Type); - var bulk = sut.WithBatchContext(None.Type); + await bulk.LoadAsync(new[] { key1, key2 }); - await bulk.LoadAsync(new[] { key1, key2 }); + var persistedEvents1 = Save.Events(); + var persistence1 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1.Write); - var persistedEvents1 = Save.Events(); - var persistence1 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1.Write); + var persistedEvents2 = Save.Events(); + var persistence2 = bulk.WithEventSourcing(None.Type, key2, persistedEvents2.Write); - var persistedEvents2 = Save.Events(); - var persistence2 = bulk.WithEventSourcing(None.Type, key2, persistedEvents2.Write); + await persistence1.WriteSnapshotAsync(12); + await persistence2.WriteSnapshotAsync(12); - await persistence1.WriteSnapshotAsync(12); - await persistence2.WriteSnapshotAsync(12); + A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<int>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<int>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + await bulk.CommitAsync(); + await bulk.DisposeAsync(); - await bulk.CommitAsync(); - await bulk.DisposeAsync(); + A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>.That.Matches(x => x.Count() == 2), A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); + } - A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>.That.Matches(x => x.Count() == 2), A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); - } + [Fact] + public async Task Should_write_each_id_only_once_if_same_id_requested_twice() + { + var key1 = DomainId.NewGuid(); + var key2 = DomainId.NewGuid(); - [Fact] - public async Task Should_write_each_id_only_once_if_same_id_requested_twice() - { - var key1 = DomainId.NewGuid(); - var key2 = DomainId.NewGuid(); + var bulk = sut.WithBatchContext(None.Type); - var bulk = sut.WithBatchContext(None.Type); + await bulk.LoadAsync(new[] { key1, key2 }); - await bulk.LoadAsync(new[] { key1, key2 }); + var persistedEvents1_1 = Save.Events(); + var persistence1_1 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1_1.Write); - var persistedEvents1_1 = Save.Events(); - var persistence1_1 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1_1.Write); + var persistedEvents1_2 = Save.Events(); + var persistence1_2 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1_2.Write); - var persistedEvents1_2 = Save.Events(); - var persistence1_2 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1_2.Write); + await persistence1_1.WriteSnapshotAsync(12); + await persistence1_2.WriteSnapshotAsync(12); - await persistence1_1.WriteSnapshotAsync(12); - await persistence1_2.WriteSnapshotAsync(12); + A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<int>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<int>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + await bulk.CommitAsync(); + await bulk.DisposeAsync(); - await bulk.CommitAsync(); - await bulk.DisposeAsync(); + A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>.That.Matches(x => x.Count() == 1), A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); + } - A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>.That.Matches(x => x.Count() == 1), A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); - } + [Fact] + public async Task Should_write_each_id_only_once_if_same_persistence_written_twice() + { + var key1 = DomainId.NewGuid(); + var key2 = DomainId.NewGuid(); - [Fact] - public async Task Should_write_each_id_only_once_if_same_persistence_written_twice() - { - var key1 = DomainId.NewGuid(); - var key2 = DomainId.NewGuid(); + var bulk = sut.WithBatchContext(None.Type); - var bulk = sut.WithBatchContext(None.Type); + await bulk.LoadAsync(new[] { key1, key2 }); - await bulk.LoadAsync(new[] { key1, key2 }); + var persistedEvents1 = Save.Events(); + var persistence1 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1.Write); - var persistedEvents1 = Save.Events(); - var persistence1 = bulk.WithEventSourcing(None.Type, key1, persistedEvents1.Write); + await persistence1.WriteSnapshotAsync(12); + await persistence1.WriteSnapshotAsync(13); - await persistence1.WriteSnapshotAsync(12); - await persistence1.WriteSnapshotAsync(13); + A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<int>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<int>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + await bulk.CommitAsync(); + await bulk.DisposeAsync(); - await bulk.CommitAsync(); - await bulk.DisposeAsync(); + A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>.That.Matches(x => x.Count() == 1), A<CancellationToken>._)) + .MustHaveHappenedOnceExactly(); + } - A.CallTo(() => snapshotStore.WriteManyAsync(A<IEnumerable<SnapshotWriteJob<int>>>.That.Matches(x => x.Count() == 1), A<CancellationToken>._)) - .MustHaveHappenedOnceExactly(); - } + private void SetupEventStore(Dictionary<DomainId, List<MyEvent>> streams) + { + var storedStreams = new Dictionary<string, IReadOnlyList<StoredEvent>>(); - private void SetupEventStore(Dictionary<DomainId, List<MyEvent>> streams) + foreach (var (id, stream) in streams) { - var storedStreams = new Dictionary<string, IReadOnlyList<StoredEvent>>(); - - foreach (var (id, stream) in streams) - { - var storedStream = new List<StoredEvent>(); - - var i = 0; + var storedStream = new List<StoredEvent>(); - foreach (var @event in stream) - { - var eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); - var eventStored = new StoredEvent(id.ToString(), i.ToString(CultureInfo.InvariantCulture), i, eventData); + var i = 0; - storedStream.Add(eventStored); + foreach (var @event in stream) + { + var eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); + var eventStored = new StoredEvent(id.ToString(), i.ToString(CultureInfo.InvariantCulture), i, eventData); - A.CallTo(() => eventFormatter.Parse(eventStored)) - .Returns(new Envelope<IEvent>(@event)); + storedStream.Add(eventStored); - A.CallTo(() => eventFormatter.ParseIfKnown(eventStored)) - .Returns(new Envelope<IEvent>(@event)); + A.CallTo(() => eventFormatter.Parse(eventStored)) + .Returns(new Envelope<IEvent>(@event)); - i++; - } + A.CallTo(() => eventFormatter.ParseIfKnown(eventStored)) + .Returns(new Envelope<IEvent>(@event)); - storedStreams[id.ToString()] = storedStream; + i++; } - var streamNames = streams.Keys.Select(x => x.ToString()).ToArray(); - - A.CallTo(() => eventStore.QueryManyAsync(A<IEnumerable<string>>.That.IsSameSequenceAs(streamNames), A<CancellationToken>._)) - .Returns(storedStreams); + storedStreams[id.ToString()] = storedStream; } + + var streamNames = streams.Keys.Select(x => x.ToString()).ToArray(); + + A.CallTo(() => eventStore.QueryManyAsync(A<IEnumerable<string>>.That.IsSameSequenceAs(streamNames), A<CancellationToken>._)) + .Returns(storedStreams); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs index 4ba1c0d5f4..476e490e63 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs @@ -11,381 +11,380 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public class PersistenceEventSourcingTests { - public class PersistenceEventSourcingTests + private readonly DomainId key = DomainId.NewGuid(); + private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly IEventStreamNames eventStreamNames = A.Fake<IEventStreamNames>(); + private readonly ISnapshotStore<string> snapshotStore = A.Fake<ISnapshotStore<string>>(); + private readonly IStore<string> sut; + + public PersistenceEventSourcingTests() { - private readonly DomainId key = DomainId.NewGuid(); - private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly IEventStreamNames eventStreamNames = A.Fake<IEventStreamNames>(); - private readonly ISnapshotStore<string> snapshotStore = A.Fake<ISnapshotStore<string>>(); - private readonly IStore<string> sut; - - public PersistenceEventSourcingTests() - { - A.CallTo(() => eventStreamNames.GetStreamName(None.Type, A<string>._)) - .ReturnsLazily(x => x.GetArgument<string>(1)!); + A.CallTo(() => eventStreamNames.GetStreamName(None.Type, A<string>._)) + .ReturnsLazily(x => x.GetArgument<string>(1)!); - sut = new Store<string>(eventFormatter, eventStore, eventStreamNames, snapshotStore); - } + sut = new Store<string>(eventFormatter, eventStore, eventStreamNames, snapshotStore); + } - [Fact] - public async Task Should_read_from_store() - { - var event1 = new MyEvent { MyProperty = "event1" }; - var event2 = new MyEvent { MyProperty = "event2" }; + [Fact] + public async Task Should_read_from_store() + { + var event1 = new MyEvent { MyProperty = "event1" }; + var event2 = new MyEvent { MyProperty = "event2" }; - SetupEventStore(event1, event2); + SetupEventStore(event1, event2); - var persistedEvents = Save.Events(); - var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); + var persistedEvents = Save.Events(); + var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.Equal(persistedEvents.ToArray(), new[] { event1, event2 }); - } + Assert.Equal(persistedEvents.ToArray(), new[] { event1, event2 }); + } - [Fact] - public async Task Should_read_until_stopped() - { - var event1 = new MyEvent { MyProperty = "event1" }; - var event2 = new MyEvent { MyProperty = "event2" }; + [Fact] + public async Task Should_read_until_stopped() + { + var event1 = new MyEvent { MyProperty = "event1" }; + var event2 = new MyEvent { MyProperty = "event2" }; - SetupEventStore(event1, event2); + SetupEventStore(event1, event2); - var persistedEvents = Save.Events(1); - var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); + var persistedEvents = Save.Events(1); + var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.Equal(persistedEvents.ToArray(), new[] { event1 }); - } + Assert.Equal(persistedEvents.ToArray(), new[] { event1 }); + } - [Fact] - public async Task Should_ignore_old_events() - { - var storedEvent = new StoredEvent("1", "1", 0, new EventData("Type", new EnvelopeHeaders(), "Payload")); + [Fact] + public async Task Should_ignore_old_events() + { + var storedEvent = new StoredEvent("1", "1", 0, new EventData("Type", new EnvelopeHeaders(), "Payload")); - A.CallTo(() => eventStore.QueryAsync(key.ToString(), 0, A<CancellationToken>._)) - .Returns(new List<StoredEvent> { storedEvent }); + A.CallTo(() => eventStore.QueryAsync(key.ToString(), 0, A<CancellationToken>._)) + .Returns(new List<StoredEvent> { storedEvent }); - A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) - .Returns(null); + A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) + .Returns(null); - var persistedEvents = Save.Events(); - var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); + var persistedEvents = Save.Events(); + var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.Empty(persistedEvents); - Assert.Equal(0, persistence.Version); - } + Assert.Empty(persistedEvents); + Assert.Equal(0, persistence.Version); + } - [Fact] - public async Task Should_read_read_from_snapshot_store() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<string>(key, "2", 2)); + [Fact] + public async Task Should_read_read_from_snapshot_store() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<string>(key, "2", 2)); - SetupEventStore(3, 2); + SetupEventStore(3, 2); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.False(persistence.IsSnapshotStale); + Assert.False(persistence.IsSnapshotStale); - A.CallTo(() => eventStore.QueryAsync(key.ToString(), 3, A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => eventStore.QueryAsync(key.ToString(), 3, A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_mark_as_stale_if_snapshot_old_than_events() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<string>(key, "2", 1)); + [Fact] + public async Task Should_mark_as_stale_if_snapshot_old_than_events() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<string>(key, "2", 1)); - SetupEventStore(3, 2, 2); + SetupEventStore(3, 2, 2); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.True(persistence.IsSnapshotStale); + Assert.True(persistence.IsSnapshotStale); - A.CallTo(() => eventStore.QueryAsync(key.ToString(), 2, A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => eventStore.QueryAsync(key.ToString(), 2, A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_throw_exception_if_events_are_older_than_snapshot() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<string>(key, "2", 2)); + [Fact] + public async Task Should_throw_exception_if_events_are_older_than_snapshot() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<string>(key, "2", 2)); - SetupEventStore(3, 0, 3); + SetupEventStore(3, 0, 3); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await Assert.ThrowsAsync<InvalidOperationException>(() => persistence.ReadAsync()); - } + await Assert.ThrowsAsync<InvalidOperationException>(() => persistence.ReadAsync()); + } - [Fact] - public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<string>(key, "2", 2)); + [Fact] + public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<string>(key, "2", 2)); - SetupEventStore(3, 4, 3); + SetupEventStore(3, 4, 3); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await Assert.ThrowsAsync<InvalidOperationException>(() => persistence.ReadAsync()); - } + await Assert.ThrowsAsync<InvalidOperationException>(() => persistence.ReadAsync()); + } - [Fact] - public async Task Should_throw_exception_if_not_found() - { - SetupEventStore(0); + [Fact] + public async Task Should_throw_exception_if_not_found() + { + SetupEventStore(0); - var persistedEvents = Save.Events(); - var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); + var persistedEvents = Save.Events(); + var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => persistence.ReadAsync(1)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => persistence.ReadAsync(1)); + } - [Fact] - public async Task Should_throw_exception_if_other_version_found() - { - SetupEventStore(3); + [Fact] + public async Task Should_throw_exception_if_other_version_found() + { + SetupEventStore(3); - var persistedEvents = Save.Events(); - var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); + var persistedEvents = Save.Events(); + var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); - await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.ReadAsync(1)); - } + await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.ReadAsync(1)); + } - [Fact] - public async Task Should_throw_exception_if_other_version_found_from_snapshot() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<string>(key, "2", 2)); + [Fact] + public async Task Should_throw_exception_if_other_version_found_from_snapshot() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<string>(key, "2", 2)); - SetupEventStore(0); + SetupEventStore(0); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.ReadAsync(1)); - } + await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.ReadAsync(1)); + } - [Fact] - public async Task Should_not_throw_exception_if_nothing_expected() - { - SetupEventStore(0); + [Fact] + public async Task Should_not_throw_exception_if_nothing_expected() + { + SetupEventStore(0); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await persistence.ReadAsync(); - } + await persistence.ReadAsync(); + } - [Fact] - public async Task Should_write_events_to_store() - { - SetupEventStore(3); + [Fact] + public async Task Should_write_events_to_store() + { + SetupEventStore(3); - var persistedEvents = Save.Events(); - var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); + var persistedEvents = Save.Events(); + var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 1), A<CancellationToken>._)) - .MustHaveHappened(); - A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), 3, A<ICollection<EventData>>.That.Matches(x => x.Count == 1), A<CancellationToken>._)) - .MustHaveHappened(); + A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 1), A<CancellationToken>._)) + .MustHaveHappened(); + A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), 3, A<ICollection<EventData>>.That.Matches(x => x.Count == 1), A<CancellationToken>._)) + .MustHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<string>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<string>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_write_events_to_store_with_empty_version() - { - var persistence = sut.WithEventSourcing(None.Type, key, null); + [Fact] + public async Task Should_write_events_to_store_with_empty_version() + { + var persistence = sut.WithEventSourcing(None.Type, key, null); - await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), EtagVersion.Empty, A<ICollection<EventData>>.That.Matches(x => x.Count == 1), A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), EtagVersion.Empty, A<ICollection<EventData>>.That.Matches(x => x.Count == 1), A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_write_snapshot_to_store() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<string>(key, "2", 2)); + [Fact] + public async Task Should_write_snapshot_to_store() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<string>(key, "2", 2)); - SetupEventStore(3); + SetupEventStore(3); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - await persistence.WriteSnapshotAsync("4"); + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + await persistence.WriteSnapshotAsync("4"); - await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - await persistence.WriteSnapshotAsync("5"); + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + await persistence.WriteSnapshotAsync("5"); - A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<string>(key, "4", 3, 2), A<CancellationToken>._)) - .MustHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<string>(key, "5", 4, 3), A<CancellationToken>._)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_write_snapshot_to_store_if_not_read_before() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<string>(key, null!, EtagVersion.Empty)); + A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<string>(key, "4", 3, 2), A<CancellationToken>._)) + .MustHaveHappened(); + A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<string>(key, "5", 4, 3), A<CancellationToken>._)) + .MustHaveHappened(); + } - SetupEventStore(3); + [Fact] + public async Task Should_write_snapshot_to_store_if_not_read_before() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<string>(key, null!, EtagVersion.Empty)); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + SetupEventStore(3); - await persistence.ReadAsync(); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - await persistence.WriteSnapshotAsync("4"); + await persistence.ReadAsync(); - await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - await persistence.WriteSnapshotAsync("5"); + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + await persistence.WriteSnapshotAsync("4"); - A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<string>(key, "4", 3, 2), A<CancellationToken>._)) - .MustHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<string>(key, "5", 4, 3), A<CancellationToken>._)) - .MustHaveHappened(); - } + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + await persistence.WriteSnapshotAsync("5"); - [Fact] - public async Task Should_not_write_snapshot_to_store_if_not_changed() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<string>(key, "0", 2)); + A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<string>(key, "4", 3, 2), A<CancellationToken>._)) + .MustHaveHappened(); + A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<string>(key, "5", 4, 3), A<CancellationToken>._)) + .MustHaveHappened(); + } - SetupEventStore(3); + [Fact] + public async Task Should_not_write_snapshot_to_store_if_not_changed() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<string>(key, "0", 2)); - var persistedState = Save.Snapshot(string.Empty); - var persistedEvents = Save.Events(); - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); + SetupEventStore(3); - await persistence.ReadAsync(); + var persistedState = Save.Snapshot(string.Empty); + var persistedEvents = Save.Events(); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, persistedState.Write, persistedEvents.Write); - await persistence.WriteSnapshotAsync("4"); + await persistence.ReadAsync(); - A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<string>>._, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + await persistence.WriteSnapshotAsync("4"); - [Fact] - public async Task Should_wrap_exception_if_writing_to_store_with_previous_version() - { - SetupEventStore(3); + A.CallTo(() => snapshotStore.WriteAsync(A<SnapshotWriteJob<string>>._, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - var persistedEvents = Save.Events(); - var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); + [Fact] + public async Task Should_wrap_exception_if_writing_to_store_with_previous_version() + { + SetupEventStore(3); - await persistence.ReadAsync(); + var persistedEvents = Save.Events(); + var persistence = sut.WithEventSourcing(None.Type, key, persistedEvents.Write); - A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 1), A<CancellationToken>._)) - .Throws(new WrongEventVersionException(1, 1)); + await persistence.ReadAsync(); - await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.WriteEventAsync(Envelope.Create(new MyEvent()))); - } + A.CallTo(() => eventStore.AppendAsync(A<Guid>._, key.ToString(), 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 1), A<CancellationToken>._)) + .Throws(new WrongEventVersionException(1, 1)); - [Fact] - public async Task Should_delete_events_but_not_snapshot_if_deleted_snapshot_only() - { - var persistence = sut.WithEventSourcing(None.Type, key, null); + await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.WriteEventAsync(Envelope.Create(new MyEvent()))); + } - await persistence.DeleteAsync(); + [Fact] + public async Task Should_delete_events_but_not_snapshot_if_deleted_snapshot_only() + { + var persistence = sut.WithEventSourcing(None.Type, key, null); - A.CallTo(() => eventStore.DeleteStreamAsync(key.ToString(), A<CancellationToken>._)) - .MustHaveHappened(); + await persistence.DeleteAsync(); - A.CallTo(() => snapshotStore.RemoveAsync(key, A<CancellationToken>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => eventStore.DeleteStreamAsync(key.ToString(), A<CancellationToken>._)) + .MustHaveHappened(); - [Fact] - public async Task Should_delete_events_and_snapshot_if_deleted() - { - var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, null, null); + A.CallTo(() => snapshotStore.RemoveAsync(key, A<CancellationToken>._)) + .MustNotHaveHappened(); + } - await persistence.DeleteAsync(); + [Fact] + public async Task Should_delete_events_and_snapshot_if_deleted() + { + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, null, null); - A.CallTo(() => eventStore.DeleteStreamAsync(key.ToString(), A<CancellationToken>._)) - .MustHaveHappened(); + await persistence.DeleteAsync(); - A.CallTo(() => snapshotStore.RemoveAsync(key, A<CancellationToken>._)) - .MustHaveHappened(); + A.CallTo(() => eventStore.DeleteStreamAsync(key.ToString(), A<CancellationToken>._)) + .MustHaveHappened(); - Assert.Equal(EtagVersion.Empty, persistence.Version); - } + A.CallTo(() => snapshotStore.RemoveAsync(key, A<CancellationToken>._)) + .MustHaveHappened(); - private void SetupEventStore(int count, int eventOffset = 0, int readPosition = 0) - { - SetupEventStore(Enumerable.Repeat(0, count).Select(x => new MyEvent()).ToArray(), eventOffset, readPosition); - } + Assert.Equal(EtagVersion.Empty, persistence.Version); + } - private void SetupEventStore(params MyEvent[] events) - { - SetupEventStore(events, 0, 0); - } + private void SetupEventStore(int count, int eventOffset = 0, int readPosition = 0) + { + SetupEventStore(Enumerable.Repeat(0, count).Select(x => new MyEvent()).ToArray(), eventOffset, readPosition); + } - private void SetupEventStore(MyEvent[] events, int eventOffset, int readPosition = 0) - { - var eventsStored = new List<StoredEvent>(); + private void SetupEventStore(params MyEvent[] events) + { + SetupEventStore(events, 0, 0); + } - var i = eventOffset; + private void SetupEventStore(MyEvent[] events, int eventOffset, int readPosition = 0) + { + var eventsStored = new List<StoredEvent>(); - foreach (var @event in events) - { - var eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); - var eventStored = new StoredEvent(key.ToString(), i.ToString(CultureInfo.InvariantCulture), i, eventData); + var i = eventOffset; - eventsStored.Add(eventStored); + foreach (var @event in events) + { + var eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); + var eventStored = new StoredEvent(key.ToString(), i.ToString(CultureInfo.InvariantCulture), i, eventData); - A.CallTo(() => eventFormatter.Parse(eventStored)) - .Returns(new Envelope<IEvent>(@event)); + eventsStored.Add(eventStored); - A.CallTo(() => eventFormatter.ParseIfKnown(eventStored)) - .Returns(new Envelope<IEvent>(@event)); + A.CallTo(() => eventFormatter.Parse(eventStored)) + .Returns(new Envelope<IEvent>(@event)); - i++; - } + A.CallTo(() => eventFormatter.ParseIfKnown(eventStored)) + .Returns(new Envelope<IEvent>(@event)); - A.CallTo(() => eventStore.QueryAsync(key.ToString(), readPosition, A<CancellationToken>._)) - .Returns(eventsStored); + i++; } + + A.CallTo(() => eventStore.QueryAsync(key.ToString(), readPosition, A<CancellationToken>._)) + .Returns(eventsStored); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs index 161e8edf26..e2e04a3848 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs @@ -9,177 +9,176 @@ using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public class PersistenceSnapshotTests { - public class PersistenceSnapshotTests + private readonly DomainId key = DomainId.NewGuid(); + private readonly ISnapshotStore<int> snapshotStore = A.Fake<ISnapshotStore<int>>(); + private readonly IEventStreamNames eventStreamNames = A.Fake<IEventStreamNames>(); + private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); + private readonly IEventStore eventStore = A.Fake<IEventStore>(); + private readonly IStore<int> sut; + + public PersistenceSnapshotTests() { - private readonly DomainId key = DomainId.NewGuid(); - private readonly ISnapshotStore<int> snapshotStore = A.Fake<ISnapshotStore<int>>(); - private readonly IEventStreamNames eventStreamNames = A.Fake<IEventStreamNames>(); - private readonly IEventFormatter eventFormatter = A.Fake<IEventFormatter>(); - private readonly IEventStore eventStore = A.Fake<IEventStore>(); - private readonly IStore<int> sut; - - public PersistenceSnapshotTests() - { - sut = new Store<int>(eventFormatter, eventStore, eventStreamNames, snapshotStore); - } + sut = new Store<int>(eventFormatter, eventStore, eventStreamNames, snapshotStore); + } - [Fact] - public async Task Should_read_from_store() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<int>(key, 20, 10)); + [Fact] + public async Task Should_read_from_store() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<int>(key, 20, 10)); - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.Equal(10, persistence.Version); - Assert.Equal(20, persistedState.Value); - } + Assert.Equal(10, persistence.Version); + Assert.Equal(20, persistedState.Value); + } - [Fact] - public async Task Should_not_read_from_store_if_not_valid() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<int>(key, 20, 10, false)); + [Fact] + public async Task Should_not_read_from_store_if_not_valid() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<int>(key, 20, 10, false)); - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.Equal(10, persistence.Version); - Assert.Equal(0, persistedState.Value); - } + Assert.Equal(10, persistence.Version); + Assert.Equal(0, persistedState.Value); + } - [Fact] - public async Task Should_return_empty_version_if_version_negative() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<int>(key, 20, -10)); + [Fact] + public async Task Should_return_empty_version_if_version_negative() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<int>(key, 20, -10)); - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.Equal(EtagVersion.Empty, persistence.Version); - } + Assert.Equal(EtagVersion.Empty, persistence.Version); + } - [Fact] - public async Task Should_set_to_empty_if_store_returns_not_found() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<int>(key, 20, EtagVersion.Empty)); + [Fact] + public async Task Should_set_to_empty_if_store_returns_not_found() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<int>(key, 20, EtagVersion.Empty)); - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.Equal(-1, persistence.Version); - Assert.Equal(0, persistedState.Value); - } + Assert.Equal(-1, persistence.Version); + Assert.Equal(0, persistedState.Value); + } - [Fact] - public async Task Should_throw_exception_if_not_found_and_version_expected() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<int>(key, 42, EtagVersion.Empty)); + [Fact] + public async Task Should_throw_exception_if_not_found_and_version_expected() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<int>(key, 42, EtagVersion.Empty)); - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => persistence.ReadAsync(1)); - } + await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => persistence.ReadAsync(1)); + } - [Fact] - public async Task Should_throw_exception_if_other_version_found() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<int>(key, 42, 2)); + [Fact] + public async Task Should_throw_exception_if_other_version_found() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<int>(key, 42, 2)); - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.ReadAsync(1)); - } + await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.ReadAsync(1)); + } - [Fact] - public async Task Should_write_to_store_with_previous_version() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<int>(key, 20, 10)); + [Fact] + public async Task Should_write_to_store_with_previous_version() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<int>(key, 20, 10)); - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - Assert.Equal(10, persistence.Version); - Assert.Equal(20, persistedState.Value); + Assert.Equal(10, persistence.Version); + Assert.Equal(20, persistedState.Value); - await persistence.WriteSnapshotAsync(100); + await persistence.WriteSnapshotAsync(100); - A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<int>(key, 100, 11, 10), A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<int>(key, 100, 11, 10), A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_write_snapshot_to_store_with_empty_version() - { - var persistence = sut.WithSnapshots(None.Type, key, null); + [Fact] + public async Task Should_write_snapshot_to_store_with_empty_version() + { + var persistence = sut.WithSnapshots(None.Type, key, null); - await persistence.WriteSnapshotAsync(100); + await persistence.WriteSnapshotAsync(100); - A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<int>(key, 100, 0, -1), A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<int>(key, 100, 0, -1), A<CancellationToken>._)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_wrap_exception_if_writing_to_store_with_previous_version() - { - A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) - .Returns(new SnapshotResult<int>(key, 42, 10)); + [Fact] + public async Task Should_not_wrap_exception_if_writing_to_store_with_previous_version() + { + A.CallTo(() => snapshotStore.ReadAsync(key, A<CancellationToken>._)) + .Returns(new SnapshotResult<int>(key, 42, 10)); - A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<int>(key, 100, 11, 10), A<CancellationToken>._)) - .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); + A.CallTo(() => snapshotStore.WriteAsync(new SnapshotWriteJob<int>(key, 100, 11, 10), A<CancellationToken>._)) + .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await persistence.ReadAsync(); + await persistence.ReadAsync(); - await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.WriteSnapshotAsync(100)); - } + await Assert.ThrowsAsync<InconsistentStateException>(() => persistence.WriteSnapshotAsync(100)); + } - [Fact] - public async Task Should_delete_snapshot_but_not_events_if_deleted() - { - var persistedState = Save.Snapshot(0); - var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); + [Fact] + public async Task Should_delete_snapshot_but_not_events_if_deleted() + { + var persistedState = Save.Snapshot(0); + var persistence = sut.WithSnapshots(None.Type, key, persistedState.Write); - await persistence.DeleteAsync(); + await persistence.DeleteAsync(); - A.CallTo(() => eventStore.DeleteStreamAsync(A<string>._, A<CancellationToken>._)) - .MustNotHaveHappened(); + A.CallTo(() => eventStore.DeleteStreamAsync(A<string>._, A<CancellationToken>._)) + .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.RemoveAsync(key, A<CancellationToken>._)) - .MustHaveHappened(); + A.CallTo(() => snapshotStore.RemoveAsync(key, A<CancellationToken>._)) + .MustHaveHappened(); - Assert.Equal(EtagVersion.Empty, persistence.Version); - } + Assert.Equal(EtagVersion.Empty, persistence.Version); + } - [Fact] - public async Task Should_call_snapshot_store_on_clear() - { - await sut.ClearSnapshotsAsync(); + [Fact] + public async Task Should_call_snapshot_store_on_clear() + { + await sut.ClearSnapshotsAsync(); - A.CallTo(() => snapshotStore.ClearAsync(A<CancellationToken>._)) - .MustHaveHappened(); - } + A.CallTo(() => snapshotStore.ClearAsync(A<CancellationToken>._)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/Save.cs b/backend/tests/Squidex.Infrastructure.Tests/States/Save.cs index f3d91e0445..f9bbe8c3cb 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/Save.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/Save.cs @@ -7,50 +7,49 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public static class Save { - public static class Save + public static SnapshotInstance<T> Snapshot<T>(T initial) { - public static SnapshotInstance<T> Snapshot<T>(T initial) - { - return new SnapshotInstance<T>(initial); - } + return new SnapshotInstance<T>(initial); + } - public static EventsInstance Events(int keep = int.MaxValue) - { - return new EventsInstance(keep); - } + public static EventsInstance Events(int keep = int.MaxValue) + { + return new EventsInstance(keep); + } - public sealed class SnapshotInstance<T> - { - public T Value { get; set; } + public sealed class SnapshotInstance<T> + { + public T Value { get; set; } - public HandleSnapshot<T> Write { get; } + public HandleSnapshot<T> Write { get; } - public SnapshotInstance(T initial) - { - Value = initial; + public SnapshotInstance(T initial) + { + Value = initial; - Write = (state, _) => - { - Value = state; - }; - } + Write = (state, _) => + { + Value = state; + }; } + } - public sealed class EventsInstance : List<IEvent> - { - public HandleEvent Write { get; } + public sealed class EventsInstance : List<IEvent> + { + public HandleEvent Write { get; } - public EventsInstance(int keep = int.MaxValue) + public EventsInstance(int keep = int.MaxValue) + { + Write = @event => { - Write = @event => - { - Add(@event.Payload); + Add(@event.Payload); - return Count < keep; - }; - } + return Count < keep; + }; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs index 780f2498ce..39e648068b 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/SimpleStateTests.cs @@ -10,224 +10,223 @@ using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.States; + +public class SimpleStateTests { - public class SimpleStateTests - { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly TestState<MyDomainState> testState = new TestState<MyDomainState>(DomainId.NewGuid()); - private readonly SimpleState<MyDomainState> sut; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState<MyDomainState> testState = new TestState<MyDomainState>(DomainId.NewGuid()); + private readonly SimpleState<MyDomainState> sut; - public SimpleStateTests() - { - ct = cts.Token; + public SimpleStateTests() + { + ct = cts.Token; - sut = new SimpleState<MyDomainState>(testState.PersistenceFactory, GetType(), testState.Id); - } + sut = new SimpleState<MyDomainState>(testState.PersistenceFactory, GetType(), testState.Id); + } - [Fact] - public void Should_init_with_base_data() - { - Assert.Equal(-1, sut.Version); - Assert.NotNull(sut.Value); - } + [Fact] + public void Should_init_with_base_data() + { + Assert.Equal(-1, sut.Version); + Assert.NotNull(sut.Value); + } - [Fact] - public async Task Should_get_state_from_persistence_on_load() - { - testState.Version = 42; - testState.Snapshot = new MyDomainState { Value = 50 }; + [Fact] + public async Task Should_get_state_from_persistence_on_load() + { + testState.Version = 42; + testState.Snapshot = new MyDomainState { Value = 50 }; - await sut.LoadAsync(ct); + await sut.LoadAsync(ct); - Assert.Equal(42, sut.Version); - Assert.Equal(50, sut.Value.Value); + Assert.Equal(42, sut.Version); + Assert.Equal(50, sut.Value.Value); - A.CallTo(() => testState.Persistence.ReadAsync(-2, ct)) - .MustHaveHappened(); - } + A.CallTo(() => testState.Persistence.ReadAsync(-2, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_delete_when_clearing() - { - await sut.ClearAsync(ct); + [Fact] + public async Task Should_delete_when_clearing() + { + await sut.ClearAsync(ct); - A.CallTo(() => testState.Persistence.DeleteAsync(ct)) - .MustHaveHappened(); - } + A.CallTo(() => testState.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_invoke_persistence_when_writing_state() - { - await sut.WriteAsync(ct); + [Fact] + public async Task Should_invoke_persistence_when_writing_state() + { + await sut.WriteAsync(ct); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappened(); - } + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_load_once_on_update() - { - await sut.UpdateAsync(x => true, ct: ct); - await sut.UpdateAsync(x => true, ct: ct); + [Fact] + public async Task Should_load_once_on_update() + { + await sut.UpdateAsync(x => true, ct: ct); + await sut.UpdateAsync(x => true, ct: ct); - A.CallTo(() => testState.Persistence.ReadAsync(-2, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => testState.Persistence.ReadAsync(-2, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_write_state_on_update_when_callback_returns_true() - { - await sut.UpdateAsync(x => true, ct: ct); + [Fact] + public async Task Should_write_state_on_update_when_callback_returns_true() + { + await sut.UpdateAsync(x => true, ct: ct); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappened(); - } + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_write_state_on_update_when_callback_returns_false() - { - await sut.UpdateAsync(x => true, ct: ct); + [Fact] + public async Task Should_not_write_state_on_update_when_callback_returns_false() + { + await sut.UpdateAsync(x => true, ct: ct); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappened(); - } + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_write_state_on_update_and_return_when_callback_returns_true() - { - var actual = await sut.UpdateAsync(x => (true, 42), ct: ct); + [Fact] + public async Task Should_write_state_on_update_and_return_when_callback_returns_true() + { + var actual = await sut.UpdateAsync(x => (true, 42), ct: ct); - Assert.Equal(42, actual); + Assert.Equal(42, actual); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappened(); - } + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_write_state_on_update_and_return_when_callback_returns_false() - { - var actual = await sut.UpdateAsync(x => (false, 42), ct: ct); + [Fact] + public async Task Should_not_write_state_on_update_and_return_when_callback_returns_false() + { + var actual = await sut.UpdateAsync(x => (false, 42), ct: ct); - Assert.Equal(42, actual); + Assert.Equal(42, actual); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustNotHaveHappened(); - } + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_retry_update_when_failed_with_inconsistency_issue() - { - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .Throws(new InconsistentStateException(1, 2)).NumberOfTimes(5); + [Fact] + public async Task Should_retry_update_when_failed_with_inconsistency_issue() + { + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .Throws(new InconsistentStateException(1, 2)).NumberOfTimes(5); - await sut.UpdateAsync(x => true, ct: ct); + await sut.UpdateAsync(x => true, ct: ct); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 6); + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 6); - A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 6); - } + A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 6); + } - [Fact] - public async Task Should_give_up_update_after_too_many_inconsistency_issues() - { - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .Throws(new InconsistentStateException(1, 2)).NumberOfTimes(100); + [Fact] + public async Task Should_give_up_update_after_too_many_inconsistency_issues() + { + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .Throws(new InconsistentStateException(1, 2)).NumberOfTimes(100); - await Assert.ThrowsAsync<InconsistentStateException>(() => sut.UpdateAsync(x => true, ct: ct)); + await Assert.ThrowsAsync<InconsistentStateException>(() => sut.UpdateAsync(x => true, ct: ct)); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 20); + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 20); - A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 20); - } + A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 20); + } - [Fact] - public async Task Should_not_retry_update_with_other_exception() - { - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .Throws(new InvalidOperationException()); + [Fact] + public async Task Should_not_retry_update_with_other_exception() + { + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync<InvalidOperationException>(() => sut.UpdateAsync(x => true, ct: ct)); + await Assert.ThrowsAsync<InvalidOperationException>(() => sut.UpdateAsync(x => true, ct: ct)); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 1); + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 1); - A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 1); - } + A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 1); + } - [Fact] - public async Task Should_retry_update_and_return_when_failed_with_inconsistency_issue() - { - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .Throws(new InconsistentStateException(1, 2)).NumberOfTimes(5); + [Fact] + public async Task Should_retry_update_and_return_when_failed_with_inconsistency_issue() + { + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .Throws(new InconsistentStateException(1, 2)).NumberOfTimes(5); - await sut.UpdateAsync(x => (true, 42), ct: ct); + await sut.UpdateAsync(x => (true, 42), ct: ct); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 6); + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 6); - A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 6); - } + A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 6); + } - [Fact] - public async Task Should_give_up_update_and_return_after_too_many_inconsistency_issues() - { - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .Throws(new InconsistentStateException(1, 2)).NumberOfTimes(100); + [Fact] + public async Task Should_give_up_update_and_return_after_too_many_inconsistency_issues() + { + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .Throws(new InconsistentStateException(1, 2)).NumberOfTimes(100); - await Assert.ThrowsAsync<InconsistentStateException>(() => sut.UpdateAsync(x => (true, 42), ct: ct)); + await Assert.ThrowsAsync<InconsistentStateException>(() => sut.UpdateAsync(x => (true, 42), ct: ct)); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 20); + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 20); - A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 20); - } + A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 20); + } - [Fact] - public async Task Should_not_retry_update_and_return_with_other_exception() - { - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .Throws(new InvalidOperationException()); + [Fact] + public async Task Should_not_retry_update_and_return_with_other_exception() + { + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync<InvalidOperationException>(() => sut.UpdateAsync(x => (true, 42), ct: ct)); + await Assert.ThrowsAsync<InvalidOperationException>(() => sut.UpdateAsync(x => (true, 42), ct: ct)); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 1); + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 1); - A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 1); - } + A.CallTo(() => testState.Persistence.ReadAsync(A<long>._, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 1); + } - [Fact] - public async Task Should_not_written_if_period_not_over() - { - var now = SystemClock.Instance.GetCurrentInstant(); + [Fact] + public async Task Should_not_written_if_period_not_over() + { + var now = SystemClock.Instance.GetCurrentInstant(); - var clock = A.Fake<IClock>(); + var clock = A.Fake<IClock>(); - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(now).NumberOfTimes(5).Then - .Returns(now.Plus(Duration.FromSeconds(2))); + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now).NumberOfTimes(5).Then + .Returns(now.Plus(Duration.FromSeconds(2))); - sut.Clock = clock; + sut.Clock = clock; - for (var i = 0; i < 10; i++) - { - await sut.WriteAsync(1000, ct); - } + for (var i = 0; i < 10; i++) + { + await sut.WriteAsync(1000, ct); + } - await sut.UpdateAsync(x => true, ct: ct); + await sut.UpdateAsync(x => true, ct: ct); - A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) - .MustHaveHappenedANumberOfTimesMatching(x => x == 3); - } + A.CallTo(() => testState.Persistence.WriteSnapshotAsync(sut.Value, ct)) + .MustHaveHappenedANumberOfTimesMatching(x => x == 3); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs index 17dc38a2a9..82563ee5ec 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs @@ -7,110 +7,109 @@ using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class StringExtensionsTests { - public class StringExtensionsTests + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("me", false)] + [InlineData("me@@web.com", false)] + [InlineData("me@web.com", true)] + [InlineData("Me@web.com", true)] + public void Should_check_email(string email, bool isEmail) + { + Assert.Equal(isEmail, email.IsEmail()); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Should_provide_fallback_if_invalid(string value) + { + Assert.Equal("fallback", value.Or("fallback")); + } + + [Fact] + public void Should_provide_value() + { + const string value = "value"; + + Assert.Equal(value, value.Or("fallback")); + } + + [Fact] + public void Should_join_non_empty_if_all_are_valid() + { + var actual = StringExtensions.JoinNonEmpty("_", "1", "2", "3"); + + Assert.Equal("1_2_3", actual); + } + + [Fact] + public void Should_join_non_empty_if_first_invalid() + { + var actual = StringExtensions.JoinNonEmpty("_", null, "2", "3"); + + Assert.Equal("2_3", actual); + } + + [Fact] + public void Should_join_non_empty_if_middle_invalid() + { + var actual = StringExtensions.JoinNonEmpty("_", "1", null, "3"); + + Assert.Equal("1_3", actual); + } + + [Fact] + public void Should_join_non_empty_if_last_invalid() + { + var actual = StringExtensions.JoinNonEmpty("_", "1", "2", null); + + Assert.Equal("1_2", actual); + } + + [Fact] + public void Should_escape_json() + { + var actual = StringExtensions.JsonEscape("Hello \"World\""); + + Assert.Equal("Hello \\\"World\\\"", actual); + } + + [Fact] + public void Should_calculate_hex_code_from_empty_array() + { + var actual = Array.Empty<byte>().ToHexString(); + + Assert.Equal(string.Empty, actual); + } + + [Fact] + public void Should_calculate_hex_code_from_byte_array() { - [Theory] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData("me", false)] - [InlineData("me@@web.com", false)] - [InlineData("me@web.com", true)] - [InlineData("Me@web.com", true)] - public void Should_check_email(string email, bool isEmail) - { - Assert.Equal(isEmail, email.IsEmail()); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void Should_provide_fallback_if_invalid(string value) - { - Assert.Equal("fallback", value.Or("fallback")); - } - - [Fact] - public void Should_provide_value() - { - const string value = "value"; - - Assert.Equal(value, value.Or("fallback")); - } - - [Fact] - public void Should_join_non_empty_if_all_are_valid() - { - var actual = StringExtensions.JoinNonEmpty("_", "1", "2", "3"); - - Assert.Equal("1_2_3", actual); - } - - [Fact] - public void Should_join_non_empty_if_first_invalid() - { - var actual = StringExtensions.JoinNonEmpty("_", null, "2", "3"); - - Assert.Equal("2_3", actual); - } - - [Fact] - public void Should_join_non_empty_if_middle_invalid() - { - var actual = StringExtensions.JoinNonEmpty("_", "1", null, "3"); - - Assert.Equal("1_3", actual); - } - - [Fact] - public void Should_join_non_empty_if_last_invalid() - { - var actual = StringExtensions.JoinNonEmpty("_", "1", "2", null); - - Assert.Equal("1_2", actual); - } - - [Fact] - public void Should_escape_json() - { - var actual = StringExtensions.JsonEscape("Hello \"World\""); - - Assert.Equal("Hello \\\"World\\\"", actual); - } - - [Fact] - public void Should_calculate_hex_code_from_empty_array() - { - var actual = Array.Empty<byte>().ToHexString(); - - Assert.Equal(string.Empty, actual); - } - - [Fact] - public void Should_calculate_hex_code_from_byte_array() - { - var actual = new byte[] { 0x00, 0x01, 0xFF, 0x1A, 0x2B, 0x3C }.ToHexString(); - - Assert.Equal("0001FF1A2B3C", actual); - } - - [Theory] - [InlineData("", "")] - [InlineData(" ", "")] - [InlineData("-", "")] - [InlineData("--", "")] - [InlineData("text1 ", "text1")] - [InlineData("texts-", "texts")] - [InlineData(" 1text", "1text")] - [InlineData("-atext", "atext")] - [InlineData("a-text", "a-text")] - public void Should_trim_non_digits_or_letters(string input, string output) - { - var result = input.TrimNonLetterOrDigit(); - - Assert.Equal(output, result); - } + var actual = new byte[] { 0x00, 0x01, 0xFF, 0x1A, 0x2B, 0x3C }.ToHexString(); + + Assert.Equal("0001FF1A2B3C", actual); + } + + [Theory] + [InlineData("", "")] + [InlineData(" ", "")] + [InlineData("-", "")] + [InlineData("--", "")] + [InlineData("text1 ", "text1")] + [InlineData("texts-", "texts")] + [InlineData(" 1text", "1text")] + [InlineData("-atext", "atext")] + [InlineData("a-text", "a-text")] + public void Should_trim_non_digits_or_letters(string input, string output) + { + var result = input.TrimNonLetterOrDigit(); + + Assert.Equal(output, result); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs index 2dc6af86fa..9bc9088a48 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs @@ -8,18 +8,17 @@ using Squidex.Infrastructure.Tasks; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class TaskExtensionsTests { - public class TaskExtensionsTests + [Fact] + public void Should_do_nothing_on_forget() { - [Fact] - public void Should_do_nothing_on_forget() - { - var task = Task.FromResult(123); + var task = Task.FromResult(123); - task.Forget(); + task.Forget(); - Assert.Equal(123, task.Result); - } + Assert.Equal(123, task.Result); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs index d57c9b0ffb..063c4a100a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs @@ -8,52 +8,51 @@ using System.Threading.Tasks.Dataflow; using Xunit; -namespace Squidex.Infrastructure.Tasks +namespace Squidex.Infrastructure.Tasks; + +public class PartitionedActionBlockTests { - public class PartitionedActionBlockTests + private const int Partitions = 10; + + [Fact] + public async Task Should_propagate_in_order() { - private const int Partitions = 10; + var lists = new List<int>[Partitions]; - [Fact] - public async Task Should_propagate_in_order() + for (var i = 0; i < Partitions; i++) { - var lists = new List<int>[Partitions]; - - for (var i = 0; i < Partitions; i++) - { - lists[i] = new List<int>(); - } + lists[i] = new List<int>(); + } - var block = new PartitionedActionBlock<(int P, int V)>(x => - { - Random.Shared.Next(10); + var block = new PartitionedActionBlock<(int P, int V)>(x => + { + Random.Shared.Next(10); - lists[x.P].Add(x.V); + lists[x.P].Add(x.V); - return Task.CompletedTask; - }, x => x.P, new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = 100, - MaxMessagesPerTask = 1, - BoundedCapacity = 100 - }); + return Task.CompletedTask; + }, x => x.P, new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = 100, + MaxMessagesPerTask = 1, + BoundedCapacity = 100 + }); - for (var i = 0; i < Partitions; i++) + for (var i = 0; i < Partitions; i++) + { + for (var j = 0; j < 10; j++) { - for (var j = 0; j < 10; j++) - { - await block.SendAsync((i, j)); - } + await block.SendAsync((i, j)); } + } - block.Complete(); + block.Complete(); - await block.Completion; + await block.Completion; - foreach (var list in lists) - { - Assert.Equal(Enumerable.Range(0, 10).ToList(), list); - } + foreach (var list in lists) + { + Assert.Equal(Enumerable.Range(0, 10).ToList(), list); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs index cfdd45a626..aa8c22e13a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs @@ -8,105 +8,104 @@ using System.Collections.Concurrent; using Xunit; -namespace Squidex.Infrastructure.Tasks +namespace Squidex.Infrastructure.Tasks; + +public class SchedulerTests { - public class SchedulerTests + private readonly ConcurrentBag<int> actuals = new ConcurrentBag<int>(); + private readonly Scheduler sut = new Scheduler(); + + [Fact] + public async Task Should_schedule_single_task() { - private readonly ConcurrentBag<int> actuals = new ConcurrentBag<int>(); - private readonly Scheduler sut = new Scheduler(); + Schedule(1); - [Fact] - public async Task Should_schedule_single_task() - { - Schedule(1); + await sut.CompleteAsync(); - await sut.CompleteAsync(); + Assert.Equal(new[] { 1 }, actuals.ToArray()); + } - Assert.Equal(new[] { 1 }, actuals.ToArray()); - } + [Fact] + public async Task Should_schedule_lot_of_tasks_with_limited_concurrency() + { + var limited = new Scheduler(1); - [Fact] - public async Task Should_schedule_lot_of_tasks_with_limited_concurrency() + for (var i = 1; i <= 10; i++) { - var limited = new Scheduler(1); - - for (var i = 1; i <= 10; i++) - { - Schedule(i, limited); - } + Schedule(i, limited); + } - await limited.CompleteAsync(); + await limited.CompleteAsync(); - Assert.Equal(Enumerable.Range(1, 10).ToArray(), actuals.OrderBy(x => x).ToArray()); - } + Assert.Equal(Enumerable.Range(1, 10).ToArray(), actuals.OrderBy(x => x).ToArray()); + } - [Fact] - public async Task Should_schedule_multiple_tasks() - { - Schedule(1); - Schedule(2); + [Fact] + public async Task Should_schedule_multiple_tasks() + { + Schedule(1); + Schedule(2); - await sut.CompleteAsync(); + await sut.CompleteAsync(); - Assert.Equal(new[] { 1, 2 }, actuals.OrderBy(x => x).ToArray()); - } + Assert.Equal(new[] { 1, 2 }, actuals.OrderBy(x => x).ToArray()); + } - [Fact] - public async Task Should_schedule_nested_tasks() + [Fact] + public async Task Should_schedule_nested_tasks() + { + sut.Schedule(async _ => { + await Task.Delay(1); + + actuals.Add(1); + sut.Schedule(async _ => { await Task.Delay(1); - actuals.Add(1); - - sut.Schedule(async _ => - { - await Task.Delay(1); + actuals.Add(2); - actuals.Add(2); - - Schedule(3); - }); + Schedule(3); }); + }); - await sut.CompleteAsync(); + await sut.CompleteAsync(); - Assert.Equal(new[] { 1, 2, 3 }, actuals.OrderBy(x => x).ToArray()); - } + Assert.Equal(new[] { 1, 2, 3 }, actuals.OrderBy(x => x).ToArray()); + } - [Fact] - public async Task Should_ignore_schedule_after_completion() - { - Schedule(1); + [Fact] + public async Task Should_ignore_schedule_after_completion() + { + Schedule(1); - await sut.CompleteAsync(); + await sut.CompleteAsync(); - Schedule(3); + Schedule(3); - await Task.Delay(50); + await Task.Delay(50); - Assert.Equal(new[] { 1 }, actuals.OrderBy(x => x).ToArray()); - } + Assert.Equal(new[] { 1 }, actuals.OrderBy(x => x).ToArray()); + } - private void Schedule(int value) + private void Schedule(int value) + { + sut.Schedule(async _ => { - sut.Schedule(async _ => - { - await Task.Delay(1); + await Task.Delay(1); - actuals.Add(value); - }); - } + actuals.Add(value); + }); + } - private void Schedule(int value, Scheduler target) + private void Schedule(int value, Scheduler target) + { + target.Schedule(async _ => { - target.Schedule(async _ => - { - await Task.Delay(1); + await Task.Delay(1); - actuals.Add(value); - }); - } + actuals.Add(value); + }); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs index f0f07f5806..0ac1dea152 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs @@ -8,14 +8,13 @@ using NodaTime; using Squidex.Infrastructure.Commands; -namespace Squidex.Infrastructure.TestHelpers +namespace Squidex.Infrastructure.TestHelpers; + +public class MyCommand : IAggregateCommand, ITimestampCommand { - public class MyCommand : IAggregateCommand, ITimestampCommand - { - public DomainId AggregateId { get; set; } + public DomainId AggregateId { get; set; } - public long ExpectedVersion { get; set; } = EtagVersion.Any; + public long ExpectedVersion { get; set; } = EtagVersion.Any; - public Instant Timestamp { get; set; } - } + public Instant Timestamp { get; set; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index b89bc579c8..c3552895a0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -13,141 +13,140 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.TestHelpers +namespace Squidex.Infrastructure.TestHelpers; + +public sealed class MyDomainObject : DomainObject<MyDomainState> { - public sealed class MyDomainObject : DomainObject<MyDomainState> - { - public bool Recreate { get; set; } + public bool Recreate { get; set; } - public bool RecreateEvent { get; set; } + public bool RecreateEvent { get; set; } - public MyDomainObject(DomainId id, IPersistenceFactory<MyDomainState> factory) - : base(id, factory, A.Dummy<ILogger>()) - { - } + public MyDomainObject(DomainId id, IPersistenceFactory<MyDomainState> factory) + : base(id, factory, A.Dummy<ILogger>()) + { + } - protected override bool CanRecreate(IEvent @event) - { - return RecreateEvent && @event is ValueChanged; - } + protected override bool CanRecreate(IEvent @event) + { + return RecreateEvent && @event is ValueChanged; + } - protected override bool CanRecreate() - { - return Recreate; - } + protected override bool CanRecreate() + { + return Recreate; + } - protected override bool CanAcceptCreation(ICommand command) + protected override bool CanAcceptCreation(ICommand command) + { + if (command is CreateAuto update) { - if (command is CreateAuto update) - { - return update.Value != 99; - } - - return true; + return update.Value != 99; } - protected override bool CanAccept(ICommand command) - { - if (command is UpdateAuto update) - { - return update.Value != 99; - } - - return true; - } + return true; + } - protected override bool IsDeleted(MyDomainState snapshot) + protected override bool CanAccept(ICommand command) + { + if (command is UpdateAuto update) { - return snapshot.IsDeleted; + return update.Value != 99; } - public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, - CancellationToken ct) - { - switch (command) - { - case Upsert c: - return Upsert(c, createAuto => - { - RaiseEvent(new ValueChanged { Value = createAuto.Value }); - }, ct); - - case CreateAuto c: - return Create(c, createAuto => - { - RaiseEvent(new ValueChanged { Value = createAuto.Value }); - }, ct); - - case CreateCustom c: - return CreateReturn(c, createCustom => - { - RaiseEvent(new ValueChanged { Value = createCustom.Value }); - - return "CREATED"; - }, ct); - - case UpdateAuto c: - return Update(c, updateAuto => - { - RaiseEvent(new ValueChanged { Value = updateAuto.Value }); - }, ct); - - case UpdateCustom c: - return UpdateReturn(c, updateCustom => - { - RaiseEvent(new ValueChanged { Value = updateCustom.Value }); - - return "UPDATED"; - }, ct); - - case Delete c: - return Update(c, delete => - { - RaiseEvent(new Deleted()); - }, ct); - - case DeletePermanent c: - return DeletePermanent(c, delete => - { - RaiseEvent(new Deleted()); - }, ct); - - default: - throw new NotSupportedException(); - } - } + return true; } - public sealed class Delete : MyCommand + protected override bool IsDeleted(MyDomainState snapshot) { + return snapshot.IsDeleted; } - public sealed class DeletePermanent : MyCommand + public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { + switch (command) + { + case Upsert c: + return Upsert(c, createAuto => + { + RaiseEvent(new ValueChanged { Value = createAuto.Value }); + }, ct); + + case CreateAuto c: + return Create(c, createAuto => + { + RaiseEvent(new ValueChanged { Value = createAuto.Value }); + }, ct); + + case CreateCustom c: + return CreateReturn(c, createCustom => + { + RaiseEvent(new ValueChanged { Value = createCustom.Value }); + + return "CREATED"; + }, ct); + + case UpdateAuto c: + return Update(c, updateAuto => + { + RaiseEvent(new ValueChanged { Value = updateAuto.Value }); + }, ct); + + case UpdateCustom c: + return UpdateReturn(c, updateCustom => + { + RaiseEvent(new ValueChanged { Value = updateCustom.Value }); + + return "UPDATED"; + }, ct); + + case Delete c: + return Update(c, delete => + { + RaiseEvent(new Deleted()); + }, ct); + + case DeletePermanent c: + return DeletePermanent(c, delete => + { + RaiseEvent(new Deleted()); + }, ct); + + default: + throw new NotSupportedException(); + } } +} - public sealed class Upsert : MyCommand - { - public long Value { get; set; } - } +public sealed class Delete : MyCommand +{ +} - public sealed class CreateAuto : MyCommand - { - public long Value { get; set; } - } +public sealed class DeletePermanent : MyCommand +{ +} - public sealed class CreateCustom : MyCommand - { - public long Value { get; set; } - } +public sealed class Upsert : MyCommand +{ + public long Value { get; set; } +} - public sealed class UpdateAuto : MyCommand - { - public long Value { get; set; } - } +public sealed class CreateAuto : MyCommand +{ + public long Value { get; set; } +} - public sealed class UpdateCustom : MyCommand - { - public long Value { get; set; } - } +public sealed class CreateCustom : MyCommand +{ + public long Value { get; set; } +} + +public sealed class UpdateAuto : MyCommand +{ + public long Value { get; set; } +} + +public sealed class UpdateCustom : MyCommand +{ + public long Value { get; set; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs index e313dd399c..6afb49cd0f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs @@ -10,54 +10,53 @@ #pragma warning disable MA0048 // File name must match type name -namespace Squidex.Infrastructure.TestHelpers +namespace Squidex.Infrastructure.TestHelpers; + +public sealed record MyDomainState : IDomainState<MyDomainState> { - public sealed record MyDomainState : IDomainState<MyDomainState> - { - public const long Unchanged = 13; + public const long Unchanged = 13; - public bool IsDeleted { get; set; } + public bool IsDeleted { get; set; } - public long Version { get; set; } + public long Version { get; set; } - public long Value { get; set; } + public long Value { get; set; } - public MyDomainState Copy() - { - return (MyDomainState)MemberwiseClone(); - } + public MyDomainState Copy() + { + return (MyDomainState)MemberwiseClone(); + } - public MyDomainState Apply(Envelope<IEvent> @event) + public MyDomainState Apply(Envelope<IEvent> @event) + { + switch (@event.Payload) { - switch (@event.Payload) - { - case ValueChanged valueChanged when valueChanged.Value != Unchanged: - return this with { Value = valueChanged.Value }; - case Deleted when !IsDeleted: - return this with { IsDeleted = true }; - } - - return this; + case ValueChanged valueChanged when valueChanged.Value != Unchanged: + return this with { Value = valueChanged.Value }; + case Deleted when !IsDeleted: + return this with { IsDeleted = true }; } - } - public sealed class ValueChanged : IEvent - { - public long Value { get; set; } + return this; } +} - public sealed class Deleted : IEvent - { - } +public sealed class ValueChanged : IEvent +{ + public long Value { get; set; } +} - public sealed class MultipleByTwiceEvent : IEvent, IMigratedStateEvent<MyDomainState> +public sealed class Deleted : IEvent +{ +} + +public sealed class MultipleByTwiceEvent : IEvent, IMigratedStateEvent<MyDomainState> +{ + public IEvent Migrate(MyDomainState state) { - public IEvent Migrate(MyDomainState state) + return new ValueChanged { - return new ValueChanged - { - Value = state.Value * 2 - }; - } + Value = state.Value * 2 + }; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs index 0a21901916..c1ee73fac6 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs @@ -7,10 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.TestHelpers +namespace Squidex.Infrastructure.TestHelpers; + +internal sealed class MyEvent : IEvent { - internal sealed class MyEvent : IEvent - { - public string MyProperty { get; set; } - } + public string MyProperty { get; set; } } \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestConfig.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestConfig.cs index 709e9e6356..96df1efd4e 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestConfig.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestConfig.cs @@ -7,22 +7,21 @@ using Microsoft.Extensions.Configuration; -namespace Squidex.Infrastructure.TestHelpers +namespace Squidex.Infrastructure.TestHelpers; + +public static class TestConfig { - public static class TestConfig - { - public static IConfiguration Configuration { get; } + public static IConfiguration Configuration { get; } - static TestConfig() - { - var basePath = Path.GetFullPath("../../../"); + static TestConfig() + { + var basePath = Path.GetFullPath("../../../"); - Configuration = new ConfigurationBuilder() - .SetBasePath(basePath) - .AddJsonFile("appsettings.json", true) - .AddJsonFile("appsettings.Development.json", true) - .AddEnvironmentVariables() - .Build(); - } + Configuration = new ConfigurationBuilder() + .SetBasePath(basePath) + .AddJsonFile("appsettings.json", true) + .AddJsonFile("appsettings.Development.json", true) + .AddEnvironmentVariables() + .Build(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestState.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestState.cs index 6cc935ac80..aeb2fb53cb 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestState.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestState.cs @@ -9,111 +9,110 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; -namespace Squidex.Infrastructure.TestHelpers -{ - public sealed class TestState<T> where T : class, new() - { - private readonly List<Envelope<IEvent>> events = new List<Envelope<IEvent>>(); - private readonly ISnapshotStore<T> snapshotStore = A.Fake<ISnapshotStore<T>>(); - private HandleSnapshot<T>? handleSnapshot; - private HandleEvent? handleEvent; - - public DomainId Id { get; } - - public IPersistenceFactory<T> PersistenceFactory { get; } - - public IPersistence<T> Persistence { get; } = A.Fake<IPersistence<T>>(); - - public long Version { get; set; } = EtagVersion.Empty; - - public T Snapshot { get; set; } = new T(); +namespace Squidex.Infrastructure.TestHelpers; - public TestState(string id, IPersistenceFactory<T>? persistenceFactory = null) - : this(DomainId.Create(id), persistenceFactory) - { - } - - public TestState(DomainId id, IPersistenceFactory<T>? persistenceFactory = null) - { - Id = id; - - PersistenceFactory = persistenceFactory ?? A.Fake<IPersistenceFactory<T>>(); - - A.CallTo(() => PersistenceFactory.Snapshots) - .Returns(snapshotStore); - - A.CallTo(() => Persistence.Version) - .ReturnsLazily(() => Version); +public sealed class TestState<T> where T : class, new() +{ + private readonly List<Envelope<IEvent>> events = new List<Envelope<IEvent>>(); + private readonly ISnapshotStore<T> snapshotStore = A.Fake<ISnapshotStore<T>>(); + private HandleSnapshot<T>? handleSnapshot; + private HandleEvent? handleEvent; - A.CallTo(() => snapshotStore.ReadAllAsync(A<CancellationToken>._)) - .ReturnsLazily(() => new List<SnapshotResult<T>> - { - new SnapshotResult<T>(id, Snapshot, Version, true) - }.ToAsyncEnumerable()); + public DomainId Id { get; } - A.CallTo(() => PersistenceFactory.WithEventSourcing(A<Type>._, id, A<HandleEvent>._)) - .Invokes(x => - { - handleEvent = x.GetArgument<HandleEvent>(2); - }) - .Returns(Persistence); + public IPersistenceFactory<T> PersistenceFactory { get; } - A.CallTo(() => PersistenceFactory.WithSnapshots(A<Type>._, id, A<HandleSnapshot<T>>._)) - .Invokes(x => - { - handleSnapshot = x.GetArgument<HandleSnapshot<T>>(2); - }) - .Returns(Persistence); + public IPersistence<T> Persistence { get; } = A.Fake<IPersistence<T>>(); - A.CallTo(() => PersistenceFactory.WithSnapshotsAndEventSourcing(A<Type>._, id, A<HandleSnapshot<T>>._, A<HandleEvent>._)) - .Invokes(x => - { - handleSnapshot = x.GetArgument<HandleSnapshot<T>>(2); - handleEvent = x.GetArgument<HandleEvent>(3); - }) - .Returns(Persistence); + public long Version { get; set; } = EtagVersion.Empty; - A.CallTo(() => Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._)) - .Invokes(x => - { - events.AddRange(x.GetArgument<IReadOnlyList<Envelope<IEvent>>>(0)!); - }); + public T Snapshot { get; set; } = new T(); - A.CallTo(() => Persistence.WriteSnapshotAsync(A<T>._, A<CancellationToken>._)) - .Invokes(x => - { - Snapshot = x.GetArgument<T>(0)!; - }); + public TestState(string id, IPersistenceFactory<T>? persistenceFactory = null) + : this(DomainId.Create(id), persistenceFactory) + { + } - A.CallTo(() => Persistence.ReadAsync(A<long>._, A<CancellationToken>._)) - .Invokes(x => + public TestState(DomainId id, IPersistenceFactory<T>? persistenceFactory = null) + { + Id = id; + + PersistenceFactory = persistenceFactory ?? A.Fake<IPersistenceFactory<T>>(); + + A.CallTo(() => PersistenceFactory.Snapshots) + .Returns(snapshotStore); + + A.CallTo(() => Persistence.Version) + .ReturnsLazily(() => Version); + + A.CallTo(() => snapshotStore.ReadAllAsync(A<CancellationToken>._)) + .ReturnsLazily(() => new List<SnapshotResult<T>> + { + new SnapshotResult<T>(id, Snapshot, Version, true) + }.ToAsyncEnumerable()); + + A.CallTo(() => PersistenceFactory.WithEventSourcing(A<Type>._, id, A<HandleEvent>._)) + .Invokes(x => + { + handleEvent = x.GetArgument<HandleEvent>(2); + }) + .Returns(Persistence); + + A.CallTo(() => PersistenceFactory.WithSnapshots(A<Type>._, id, A<HandleSnapshot<T>>._)) + .Invokes(x => + { + handleSnapshot = x.GetArgument<HandleSnapshot<T>>(2); + }) + .Returns(Persistence); + + A.CallTo(() => PersistenceFactory.WithSnapshotsAndEventSourcing(A<Type>._, id, A<HandleSnapshot<T>>._, A<HandleEvent>._)) + .Invokes(x => + { + handleSnapshot = x.GetArgument<HandleSnapshot<T>>(2); + handleEvent = x.GetArgument<HandleEvent>(3); + }) + .Returns(Persistence); + + A.CallTo(() => Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._)) + .Invokes(x => + { + events.AddRange(x.GetArgument<IReadOnlyList<Envelope<IEvent>>>(0)!); + }); + + A.CallTo(() => Persistence.WriteSnapshotAsync(A<T>._, A<CancellationToken>._)) + .Invokes(x => + { + Snapshot = x.GetArgument<T>(0)!; + }); + + A.CallTo(() => Persistence.ReadAsync(A<long>._, A<CancellationToken>._)) + .Invokes(x => + { + handleSnapshot?.Invoke(Snapshot, Version); + + if (handleEvent != null) { - handleSnapshot?.Invoke(Snapshot, Version); - - if (handleEvent != null) + foreach (var @event in events) { - foreach (var @event in events) - { - handleEvent(@event); - } + handleEvent(@event); } - }); + } + }); + + A.CallTo(() => Persistence.DeleteAsync(A<CancellationToken>._)) + .Invokes(x => + { + Snapshot = new T(); + }); + } - A.CallTo(() => Persistence.DeleteAsync(A<CancellationToken>._)) - .Invokes(x => - { - Snapshot = new T(); - }); - } - - public void AddEvent(Envelope<IEvent> @event) - { - events.Add(@event); - } - - public void AddEvent(IEvent @event) - { - events.Add(Envelope.Create(@event)); - } + public void AddEvent(Envelope<IEvent> @event) + { + events.Add(@event); + } + + public void AddEvent(IEvent @event) + { + events.Add(Envelope.Create(@event)); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs index 6e6adf4c73..c1e191ad10 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs @@ -23,129 +23,128 @@ #pragma warning disable SYSLIB0011 // Type or member is obsolete -namespace Squidex.Infrastructure.TestHelpers +namespace Squidex.Infrastructure.TestHelpers; + +public static class TestUtils { - public static class TestUtils - { - public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); + public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - public sealed class ObjectHolder<T> - { - [BsonRequired] - public T Value1 { get; set; } + public sealed class ObjectHolder<T> + { + [BsonRequired] + public T Value1 { get; set; } - [BsonRequired] - public T Value2 { get; set; } - } + [BsonRequired] + public T Value2 { get; set; } + } - static TestUtils() - { - SetupBson(); - } + static TestUtils() + { + SetupBson(); + } - public static void SetupBson() - { - BsonDomainIdSerializer.Register(); - BsonEscapedDictionarySerializer<JsonValue, JsonObject>.Register(); - BsonInstantSerializer.Register(); - BsonJsonConvention.Register(DefaultOptions()); - BsonJsonValueSerializer.Register(); - BsonStringSerializer<RefToken>.Register(); - } + public static void SetupBson() + { + BsonDomainIdSerializer.Register(); + BsonEscapedDictionarySerializer<JsonValue, JsonObject>.Register(); + BsonInstantSerializer.Register(); + BsonJsonConvention.Register(DefaultOptions()); + BsonJsonValueSerializer.Register(); + BsonStringSerializer<RefToken>.Register(); + } - public static IJsonSerializer CreateSerializer(Action<JsonSerializerOptions>? configure = null) - { - var serializerSettings = DefaultOptions(configure); + public static IJsonSerializer CreateSerializer(Action<JsonSerializerOptions>? configure = null) + { + var serializerSettings = DefaultOptions(configure); - return new SystemJsonSerializer(serializerSettings); - } + return new SystemJsonSerializer(serializerSettings); + } - public static JsonSerializerOptions DefaultOptions(Action<JsonSerializerOptions>? configure = null) - { - var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); - - options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. - options.Converters.Add(new StringConverter<PropertyPath>(x => x)); - options.Converters.Add(new JsonValueConverter()); - options.Converters.Add(new ReadonlyDictionaryConverterFactory()); - options.Converters.Add(new ReadonlyListConverterFactory()); - options.Converters.Add(new SurrogateJsonConverter<ClaimsPrincipal, ClaimsPrincipalSurrogate>()); - options.Converters.Add(new SurrogateJsonConverter<FilterNode<JsonValue>, JsonFilterSurrogate>()); - options.Converters.Add(new StringConverter<CompareOperator>()); - options.Converters.Add(new StringConverter<DomainId>()); - options.Converters.Add(new StringConverter<NamedId<DomainId>>()); - options.Converters.Add(new StringConverter<NamedId<Guid>>()); - options.Converters.Add(new StringConverter<NamedId<long>>()); - options.Converters.Add(new StringConverter<NamedId<string>>()); - options.Converters.Add(new StringConverter<Language>()); - options.Converters.Add(new StringConverter<RefToken>()); - options.Converters.Add(new JsonStringEnumConverter()); - configure?.Invoke(options); - - return options; - } + public static JsonSerializerOptions DefaultOptions(Action<JsonSerializerOptions>? configure = null) + { + var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. + options.Converters.Add(new StringConverter<PropertyPath>(x => x)); + options.Converters.Add(new JsonValueConverter()); + options.Converters.Add(new ReadonlyDictionaryConverterFactory()); + options.Converters.Add(new ReadonlyListConverterFactory()); + options.Converters.Add(new SurrogateJsonConverter<ClaimsPrincipal, ClaimsPrincipalSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter<FilterNode<JsonValue>, JsonFilterSurrogate>()); + options.Converters.Add(new StringConverter<CompareOperator>()); + options.Converters.Add(new StringConverter<DomainId>()); + options.Converters.Add(new StringConverter<NamedId<DomainId>>()); + options.Converters.Add(new StringConverter<NamedId<Guid>>()); + options.Converters.Add(new StringConverter<NamedId<long>>()); + options.Converters.Add(new StringConverter<NamedId<string>>()); + options.Converters.Add(new StringConverter<Language>()); + options.Converters.Add(new StringConverter<RefToken>()); + options.Converters.Add(new JsonStringEnumConverter()); + configure?.Invoke(options); + + return options; + } - public static T SerializeAndDeserializeBinary<T>(this T source) + public static T SerializeAndDeserializeBinary<T>(this T source) + { + using (var stream = new MemoryStream()) { - using (var stream = new MemoryStream()) - { - var formatter = new BinaryFormatter(); + var formatter = new BinaryFormatter(); - formatter.Serialize(stream, source!); + formatter.Serialize(stream, source!); - stream.Position = 0; + stream.Position = 0; - return (T)formatter.Deserialize(stream); - } + return (T)formatter.Deserialize(stream); } + } - public static T SerializeAndDeserializeBson<T>(this T value) - { - return SerializeAndDeserializeBson<T, T>(value); - } - - public static TOut SerializeAndDeserializeBson<TOut, TIn>(this TIn value) - { - using var stream = new MemoryStream(); - - using (var writer = new BsonBinaryWriter(stream)) - { - BsonSerializer.Serialize(writer, new ObjectHolder<TIn> { Value1 = value, Value2 = value }); - } + public static T SerializeAndDeserializeBson<T>(this T value) + { + return SerializeAndDeserializeBson<T, T>(value); + } - stream.Position = 0; + public static TOut SerializeAndDeserializeBson<TOut, TIn>(this TIn value) + { + using var stream = new MemoryStream(); - using (var reader = new BsonBinaryReader(stream)) - { - return BsonSerializer.Deserialize<ObjectHolder<TOut>>(reader).Value1; - } + using (var writer = new BsonBinaryWriter(stream)) + { + BsonSerializer.Serialize(writer, new ObjectHolder<TIn> { Value1 = value, Value2 = value }); } - public static T SerializeAndDeserialize<T>(this T value) + stream.Position = 0; + + using (var reader = new BsonBinaryReader(stream)) { - return SerializeAndDeserialize<T, T>(value); + return BsonSerializer.Deserialize<ObjectHolder<TOut>>(reader).Value1; } + } - public static TOut SerializeAndDeserialize<TOut, TIn>(this TIn value) - { - var json = DefaultSerializer.Serialize(new ObjectHolder<TIn> { Value1 = value, Value2 = value }); + public static T SerializeAndDeserialize<T>(this T value) + { + return SerializeAndDeserialize<T, T>(value); + } - return DefaultSerializer.Deserialize<ObjectHolder<TOut>>(json).Value1; - } + public static TOut SerializeAndDeserialize<TOut, TIn>(this TIn value) + { + var json = DefaultSerializer.Serialize(new ObjectHolder<TIn> { Value1 = value, Value2 = value }); - public static T Deserialize<T>(string value) - { - var json = DefaultSerializer.Serialize(new ObjectHolder<string> { Value1 = value, Value2 = value }); + return DefaultSerializer.Deserialize<ObjectHolder<TOut>>(json).Value1; + } - return DefaultSerializer.Deserialize<ObjectHolder<T>>(json).Value1; - } + public static T Deserialize<T>(string value) + { + var json = DefaultSerializer.Serialize(new ObjectHolder<string> { Value1 = value, Value2 = value }); - public static string CleanJson(this string json) - { - using var document = JsonDocument.Parse(json); + return DefaultSerializer.Deserialize<ObjectHolder<T>>(json).Value1; + } - return DefaultSerializer.Serialize(document, true); - } + public static string CleanJson(this string json) + { + using var document = JsonDocument.Parse(json); + + return DefaultSerializer.Serialize(document, true); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs index 8f6cd9937e..185cdd70a0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs @@ -7,26 +7,25 @@ using Xunit; -namespace Squidex.Infrastructure.Timers +namespace Squidex.Infrastructure.Timers; + +public class CompletionTimerTests { - public class CompletionTimerTests + [Fact] + public void Should_invoke_once_even_with_delay() { - [Fact] - public void Should_invoke_once_even_with_delay() - { - var called = false; + var called = false; - var timer = new CompletionTimer(2000, ct => - { - called = true; + var timer = new CompletionTimer(2000, ct => + { + called = true; - return Task.CompletedTask; - }, 2000); + return Task.CompletedTask; + }, 2000); - timer.SkipCurrentDelay(); - timer.StopAsync().Wait(); + timer.SkipCurrentDelay(); + timer.StopAsync().Wait(); - Assert.True(called); - } + Assert.True(called); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Translations/SampleResources.Designer.cs b/backend/tests/Squidex.Infrastructure.Tests/Translations/SampleResources.Designer.cs index 01960aec24..c41b027538 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Translations/SampleResources.Designer.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Translations/SampleResources.Designer.cs @@ -8,92 +8,91 @@ // </auto-generated> //------------------------------------------------------------------------------ -namespace Squidex.Infrastructure.Translations { - using System; +namespace Squidex.Infrastructure.Translations; +using System; + + +/// <summary> +/// A strongly-typed resource class, for looking up localized strings, etc. +/// </summary> +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +internal class SampleResources { + + private static global::System.Resources.ResourceManager resourceMan; + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SampleResources() { + } /// <summary> - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Returns the cached ResourceManager instance used by this class. /// </summary> - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class SampleResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal SampleResources() { - } - - /// <summary> - /// Returns the cached ResourceManager instance used by this class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Infrastructure.Translations.SampleResources", typeof(SampleResources).Assembly); - resourceMan = temp; - } - return resourceMan; + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Infrastructure.Translations.SampleResources", typeof(SampleResources).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// <summary> - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// </summary> - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// <summary> - /// Looks up a localized string similar to Simple Result. - /// </summary> - internal static string simple { - get { - return ResourceManager.GetString("simple", resourceCulture); - } + set { + resourceCulture = value; } - - /// <summary> - /// Looks up a localized string similar to Var: {var|lower}.. - /// </summary> - internal static string withLowerVar { - get { - return ResourceManager.GetString("withLowerVar", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Simple Result. + /// </summary> + internal static string simple { + get { + return ResourceManager.GetString("simple", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Var: {var|upper}.. - /// </summary> - internal static string withUpperVar { - get { - return ResourceManager.GetString("withUpperVar", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Var: {var|lower}.. + /// </summary> + internal static string withLowerVar { + get { + return ResourceManager.GetString("withLowerVar", resourceCulture); } - - /// <summary> - /// Looks up a localized string similar to Var: {var}.. - /// </summary> - internal static string withVar { - get { - return ResourceManager.GetString("withVar", resourceCulture); - } + } + + /// <summary> + /// Looks up a localized string similar to Var: {var|upper}.. + /// </summary> + internal static string withUpperVar { + get { + return ResourceManager.GetString("withUpperVar", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Var: {var}.. + /// </summary> + internal static string withVar { + get { + return ResourceManager.GetString("withVar", resourceCulture); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs index 984345c4bb..4e3f155c37 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs @@ -8,55 +8,54 @@ using System.Globalization; using Xunit; -namespace Squidex.Infrastructure.Translations +namespace Squidex.Infrastructure.Translations; + +public class TTests { - public class TTests - { - private readonly ILocalizer sut; + private readonly ILocalizer sut; - public TTests() - { - sut = new ResourcesLocalizer(SampleResources.ResourceManager); - } + public TTests() + { + sut = new ResourcesLocalizer(SampleResources.ResourceManager); + } - [Fact] - public void Should_return_key_if_not_found() - { - var actual = sut.Get(CultureInfo.CurrentUICulture, "key", "fallback"); + [Fact] + public void Should_return_key_if_not_found() + { + var actual = sut.Get(CultureInfo.CurrentUICulture, "key", "fallback"); - Assert.Equal(("fallback", false), actual); - } + Assert.Equal(("fallback", false), actual); + } - [Fact] - public void Should_return_simple_key() - { - var actual = sut.Get(CultureInfo.CurrentUICulture, "simple", "fallback"); + [Fact] + public void Should_return_simple_key() + { + var actual = sut.Get(CultureInfo.CurrentUICulture, "simple", "fallback"); - Assert.Equal(("Simple Result", true), actual); - } + Assert.Equal(("Simple Result", true), actual); + } - [Fact] - public void Should_return_text_with_variable() - { - var actual = sut.Get(CultureInfo.CurrentUICulture, "withVar", "fallback", new { var = 5 }); + [Fact] + public void Should_return_text_with_variable() + { + var actual = sut.Get(CultureInfo.CurrentUICulture, "withVar", "fallback", new { var = 5 }); - Assert.Equal(("Var: 5.", true), actual); - } + Assert.Equal(("Var: 5.", true), actual); + } - [Fact] - public void Should_return_text_with_lower_variable() - { - var actual = sut.Get(CultureInfo.CurrentUICulture, "withLowerVar", "fallback", new { var = "Lower" }); + [Fact] + public void Should_return_text_with_lower_variable() + { + var actual = sut.Get(CultureInfo.CurrentUICulture, "withLowerVar", "fallback", new { var = "Lower" }); - Assert.Equal(("Var: lower.", true), actual); - } + Assert.Equal(("Var: lower.", true), actual); + } - [Fact] - public void Should_return_text_with_upper_variable() - { - var actual = sut.Get(CultureInfo.CurrentUICulture, "withUpperVar", "fallback", new { var = "upper" }); + [Fact] + public void Should_return_text_with_upper_variable() + { + var actual = sut.Get(CultureInfo.CurrentUICulture, "withUpperVar", "fallback", new { var = "upper" }); - Assert.Equal(("Var: Upper.", true), actual); - } + Assert.Equal(("Var: Upper.", true), actual); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs index c88e8489e1..f9cec73329 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs @@ -8,16 +8,15 @@ using Squidex.Infrastructure.Reflection; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class TypeNameAttributeTests { - public class TypeNameAttributeTests + [Fact] + public void Should_instantiate() { - [Fact] - public void Should_instantiate() - { - var attribute = new TypeNameAttribute("MyTypeName"); + var attribute = new TypeNameAttribute("MyTypeName"); - Assert.Equal("MyTypeName", attribute.TypeName); - } + Assert.Equal("MyTypeName", attribute.TypeName); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs index b05a856a2b..c53b5f108b 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs @@ -10,167 +10,166 @@ using Squidex.Infrastructure.Reflection; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class TypeNameRegistryTests { - public class TypeNameRegistryTests + private readonly TypeNameRegistry sut = new TypeNameRegistry(); + + [TypeName("my")] + public sealed class MyType { - private readonly TypeNameRegistry sut = new TypeNameRegistry(); + } - [TypeName("my")] - public sealed class MyType - { - } + [EventType(nameof(MyAdded), 2)] + public sealed class MyAdded + { + } - [EventType(nameof(MyAdded), 2)] - public sealed class MyAdded - { - } + [Fact] + public void Should_call_provider_from_constructor() + { + var provider = A.Fake<ITypeProvider>(); + + var registry = new TypeNameRegistry(Enumerable.Repeat(provider, 1)); - [Fact] - public void Should_call_provider_from_constructor() - { - var provider = A.Fake<ITypeProvider>(); + A.CallTo(() => provider.Map(registry)) + .MustHaveHappened(); + } - var registry = new TypeNameRegistry(Enumerable.Repeat(provider, 1)); + [Fact] + public void Should_call_provider() + { + var provider = A.Fake<ITypeProvider>(); + + sut.Map(provider); + + A.CallTo(() => provider.Map(sut)) + .MustHaveHappened(); + } + + [Fact] + public void Should_register_and_retrieve_types() + { + sut.Map(typeof(int), "NumberField"); + + Assert.Equal("NumberField", sut.GetName<int>()); + Assert.Equal("NumberField", sut.GetName(typeof(int))); + + Assert.Equal(typeof(int), sut.GetType("NumberField")); + Assert.Equal(typeof(int), sut.GetType("NumberField")); + } - A.CallTo(() => provider.Map(registry)) - .MustHaveHappened(); - } - - [Fact] - public void Should_call_provider() - { - var provider = A.Fake<ITypeProvider>(); + [Fact] + public void Should_register_from_attribute() + { + sut.Map(typeof(MyType)); + + Assert.Equal("my", sut.GetName<MyType>()); + Assert.Equal("my", sut.GetName(typeof(MyType))); + + Assert.Equal(typeof(MyType), sut.GetType("my")); + Assert.Equal(typeof(MyType), sut.GetType("My")); + } + + [Fact] + public void Should_register_with_provider_from_assembly() + { + sut.Map(new AutoAssembyTypeProvider<TypeNameRegistryTests>()); + + Assert.Equal("my", sut.GetName<MyType>()); + Assert.Equal("my", sut.GetName(typeof(MyType))); + + Assert.Equal(typeof(MyType), sut.GetType("my")); + Assert.Equal(typeof(MyType), sut.GetType("My")); + } + + [Fact] + public void Should_register_from_assembly() + { + sut.MapUnmapped(typeof(TypeNameRegistryTests).Assembly); + + Assert.Equal("my", sut.GetName<MyType>()); + Assert.Equal("my", sut.GetName(typeof(MyType))); + + Assert.Equal(typeof(MyType), sut.GetType("my")); + Assert.Equal(typeof(MyType), sut.GetType("My")); + } + + [Fact] + public void Should_register_event_type_from_assembly() + { + sut.MapUnmapped(typeof(TypeNameRegistryTests).Assembly); - sut.Map(provider); - - A.CallTo(() => provider.Map(sut)) - .MustHaveHappened(); - } - - [Fact] - public void Should_register_and_retrieve_types() - { - sut.Map(typeof(int), "NumberField"); - - Assert.Equal("NumberField", sut.GetName<int>()); - Assert.Equal("NumberField", sut.GetName(typeof(int))); - - Assert.Equal(typeof(int), sut.GetType("NumberField")); - Assert.Equal(typeof(int), sut.GetType("NumberField")); - } - - [Fact] - public void Should_register_from_attribute() - { - sut.Map(typeof(MyType)); - - Assert.Equal("my", sut.GetName<MyType>()); - Assert.Equal("my", sut.GetName(typeof(MyType))); - - Assert.Equal(typeof(MyType), sut.GetType("my")); - Assert.Equal(typeof(MyType), sut.GetType("My")); - } - - [Fact] - public void Should_register_with_provider_from_assembly() - { - sut.Map(new AutoAssembyTypeProvider<TypeNameRegistryTests>()); - - Assert.Equal("my", sut.GetName<MyType>()); - Assert.Equal("my", sut.GetName(typeof(MyType))); - - Assert.Equal(typeof(MyType), sut.GetType("my")); - Assert.Equal(typeof(MyType), sut.GetType("My")); - } - - [Fact] - public void Should_register_from_assembly() - { - sut.MapUnmapped(typeof(TypeNameRegistryTests).Assembly); - - Assert.Equal("my", sut.GetName<MyType>()); - Assert.Equal("my", sut.GetName(typeof(MyType))); - - Assert.Equal(typeof(MyType), sut.GetType("my")); - Assert.Equal(typeof(MyType), sut.GetType("My")); - } - - [Fact] - public void Should_register_event_type_from_assembly() - { - sut.MapUnmapped(typeof(TypeNameRegistryTests).Assembly); - - Assert.Equal("MyAddedEventV2", sut.GetName<MyAdded>()); - Assert.Equal("MyAddedEventV2", sut.GetName(typeof(MyAdded))); - - Assert.Equal(typeof(MyAdded), sut.GetType("myAddedEventV2")); - Assert.Equal(typeof(MyAdded), sut.GetType("MyAddedEventV2")); - } - - [Fact] - public void Should_register_fallback_name() - { - sut.Map(typeof(MyType)); - sut.MapObsolete(typeof(MyType), "my-old"); - - Assert.Equal(typeof(MyType), sut.GetType("my")); - Assert.Equal(typeof(MyType), sut.GetType("my-old")); - } - - [Fact] - public void Should_not_throw_exception_if_type_is_already_registered_with_same_name() - { - sut.Map(typeof(long), "long"); - sut.Map(typeof(long), "long"); - } - - [Fact] - public void Should_throw_exception_if_type_is_already_registered() - { - sut.Map(typeof(long), "long"); - - Assert.Throws<ArgumentException>(() => sut.Map(typeof(long), "longer")); - } - - [Fact] - public void Should_throw_exception_if_name_is_already_registered() - { - sut.Map(typeof(short), "short"); - - Assert.Throws<ArgumentException>(() => sut.Map(typeof(byte), "short")); - } - - [Fact] - public void Should_throw_exception_if_obsolete_name_is_already_registered() - { - sut.MapObsolete(typeof(short), "short2"); - - Assert.Throws<ArgumentException>(() => sut.MapObsolete(typeof(byte), "short2")); - } - - [Fact] - public void Should_throw_exception_if_name_is_not_supported() - { - Assert.Throws<TypeNameNotFoundException>(() => sut.GetType("unsupported")); - } - - [Fact] - public void Should_return_null_if_name_is_not_supported() - { - Assert.Null(sut.GetTypeOrNull("unsupported")); - } - - [Fact] - public void Should_throw_exception_if_type_is_not_supported() - { - Assert.Throws<TypeNameNotFoundException>(() => sut.GetName<Guid>()); - } - - [Fact] - public void Should_return_null_if_type_is_not_supported() - { - Assert.Null(sut.GetNameOrNull<Guid>()); - } + Assert.Equal("MyAddedEventV2", sut.GetName<MyAdded>()); + Assert.Equal("MyAddedEventV2", sut.GetName(typeof(MyAdded))); + + Assert.Equal(typeof(MyAdded), sut.GetType("myAddedEventV2")); + Assert.Equal(typeof(MyAdded), sut.GetType("MyAddedEventV2")); + } + + [Fact] + public void Should_register_fallback_name() + { + sut.Map(typeof(MyType)); + sut.MapObsolete(typeof(MyType), "my-old"); + + Assert.Equal(typeof(MyType), sut.GetType("my")); + Assert.Equal(typeof(MyType), sut.GetType("my-old")); + } + + [Fact] + public void Should_not_throw_exception_if_type_is_already_registered_with_same_name() + { + sut.Map(typeof(long), "long"); + sut.Map(typeof(long), "long"); + } + + [Fact] + public void Should_throw_exception_if_type_is_already_registered() + { + sut.Map(typeof(long), "long"); + + Assert.Throws<ArgumentException>(() => sut.Map(typeof(long), "longer")); + } + + [Fact] + public void Should_throw_exception_if_name_is_already_registered() + { + sut.Map(typeof(short), "short"); + + Assert.Throws<ArgumentException>(() => sut.Map(typeof(byte), "short")); + } + + [Fact] + public void Should_throw_exception_if_obsolete_name_is_already_registered() + { + sut.MapObsolete(typeof(short), "short2"); + + Assert.Throws<ArgumentException>(() => sut.MapObsolete(typeof(byte), "short2")); + } + + [Fact] + public void Should_throw_exception_if_name_is_not_supported() + { + Assert.Throws<TypeNameNotFoundException>(() => sut.GetType("unsupported")); + } + + [Fact] + public void Should_return_null_if_name_is_not_supported() + { + Assert.Null(sut.GetTypeOrNull("unsupported")); + } + + [Fact] + public void Should_throw_exception_if_type_is_not_supported() + { + Assert.Throws<TypeNameNotFoundException>(() => sut.GetName<Guid>()); + } + + [Fact] + public void Should_return_null_if_type_is_not_supported() + { + Assert.Null(sut.GetNameOrNull<Guid>()); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs index 5068087f31..215959ae51 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs @@ -9,158 +9,157 @@ using FluentAssertions; using Xunit; -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public class ApiUsageTrackerTests { - public class ApiUsageTrackerTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IUsageTracker usageTracker = A.Fake<IUsageTracker>(); + private readonly string key = Guid.NewGuid().ToString(); + private readonly string category = Guid.NewGuid().ToString(); + private readonly DateTime date = DateTime.Today; + private readonly ApiUsageTracker sut; + + public ApiUsageTrackerTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IUsageTracker usageTracker = A.Fake<IUsageTracker>(); - private readonly string key = Guid.NewGuid().ToString(); - private readonly string category = Guid.NewGuid().ToString(); - private readonly DateTime date = DateTime.Today; - private readonly ApiUsageTracker sut; - - public ApiUsageTrackerTests() - { - ct = cts.Token; - - sut = new ApiUsageTracker(usageTracker); - } - - [Fact] - public async Task Should_forward_delete_call() - { - await sut.DeleteAsync(key, ct); + ct = cts.Token; - A.CallTo(() => usageTracker.DeleteAsync($"{key}_API", A<CancellationToken>._)) - .MustHaveHappened(); - } + sut = new ApiUsageTracker(usageTracker); + } - [Fact] - public async Task Should_track_usage() - { - Counters? measuredCounters = null; + [Fact] + public async Task Should_forward_delete_call() + { + await sut.DeleteAsync(key, ct); - A.CallTo(() => usageTracker.TrackAsync(date, $"{key}_API", null, A<Counters>.Ignored, ct)) - .Invokes(args => - { - measuredCounters = args.GetArgument<Counters>(3)!; - }); + A.CallTo(() => usageTracker.DeleteAsync($"{key}_API", A<CancellationToken>._)) + .MustHaveHappened(); + } - await sut.TrackAsync(date, key, null, 4, 120, 1024, ct); + [Fact] + public async Task Should_track_usage() + { + Counters? measuredCounters = null; - measuredCounters.Should().BeEquivalentTo(new Counters + A.CallTo(() => usageTracker.TrackAsync(date, $"{key}_API", null, A<Counters>.Ignored, ct)) + .Invokes(args => { - [ApiUsageTracker.CounterTotalBytes] = 1024, - [ApiUsageTracker.CounterTotalCalls] = 4, - [ApiUsageTracker.CounterTotalElapsedMs] = 120 + measuredCounters = args.GetArgument<Counters>(3)!; }); - } - [Fact] - public async Task Should_query_calls_from_tracker() + await sut.TrackAsync(date, key, null, 4, 120, 1024, ct); + + measuredCounters.Should().BeEquivalentTo(new Counters { - var counters = new Counters - { - [ApiUsageTracker.CounterTotalCalls] = 4 - }; + [ApiUsageTracker.CounterTotalBytes] = 1024, + [ApiUsageTracker.CounterTotalCalls] = 4, + [ApiUsageTracker.CounterTotalElapsedMs] = 120 + }); + } - A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category, ct)) - .Returns(counters); + [Fact] + public async Task Should_query_calls_from_tracker() + { + var counters = new Counters + { + [ApiUsageTracker.CounterTotalCalls] = 4 + }; - var actual = await sut.GetMonthCallsAsync(key, date, category, ct); + A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category, ct)) + .Returns(counters); - Assert.Equal(4, actual); - } + var actual = await sut.GetMonthCallsAsync(key, date, category, ct); - [Fact] - public async Task Should_query_bytes_from_tracker() + Assert.Equal(4, actual); + } + + [Fact] + public async Task Should_query_bytes_from_tracker() + { + var counters = new Counters { - var counters = new Counters - { - [ApiUsageTracker.CounterTotalBytes] = 14 - }; + [ApiUsageTracker.CounterTotalBytes] = 14 + }; + + A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category, ct)) + .Returns(counters); - A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category, ct)) - .Returns(counters); + var actual = await sut.GetMonthBytesAsync(key, date, category, ct); - var actual = await sut.GetMonthBytesAsync(key, date, category, ct); + Assert.Equal(14, actual); + } - Assert.Equal(14, actual); - } + [Fact] + public async Task Should_query_stats_from_tracker() + { + var dateFrom = date; + var dateTo = dateFrom.AddDays(4); - [Fact] - public async Task Should_query_stats_from_tracker() + var counters = new Dictionary<string, List<(DateTime Date, Counters Counters)>> { - var dateFrom = date; - var dateTo = dateFrom.AddDays(4); - - var counters = new Dictionary<string, List<(DateTime Date, Counters Counters)>> + ["my-category"] = new List<(DateTime Date, Counters Counters)> { - ["my-category"] = new List<(DateTime Date, Counters Counters)> - { - (dateFrom.AddDays(0), Counters(0, 0, 0)), - (dateFrom.AddDays(1), Counters(4, 100, 2048)), - (dateFrom.AddDays(2), Counters(0, 0, 0)), - (dateFrom.AddDays(3), Counters(2, 60, 1024)), - (dateFrom.AddDays(4), Counters(3, 30, 512)) - }, - ["*"] = new List<(DateTime Date, Counters Counters)> - { - (dateFrom.AddDays(0), Counters(1, 20, 128)), - (dateFrom.AddDays(1), Counters(0, 0, 0)), - (dateFrom.AddDays(2), Counters(5, 90, 16)), - (dateFrom.AddDays(3), Counters(0, 0, 0)), - (dateFrom.AddDays(4), Counters(0, 0, 0)) - } - }; - - var forMonth = new Counters + (dateFrom.AddDays(0), Counters(0, 0, 0)), + (dateFrom.AddDays(1), Counters(4, 100, 2048)), + (dateFrom.AddDays(2), Counters(0, 0, 0)), + (dateFrom.AddDays(3), Counters(2, 60, 1024)), + (dateFrom.AddDays(4), Counters(3, 30, 512)) + }, + ["*"] = new List<(DateTime Date, Counters Counters)> { - [ApiUsageTracker.CounterTotalCalls] = 120, - [ApiUsageTracker.CounterTotalBytes] = 400 - }; + (dateFrom.AddDays(0), Counters(1, 20, 128)), + (dateFrom.AddDays(1), Counters(0, 0, 0)), + (dateFrom.AddDays(2), Counters(5, 90, 16)), + (dateFrom.AddDays(3), Counters(0, 0, 0)), + (dateFrom.AddDays(4), Counters(0, 0, 0)) + } + }; + + var forMonth = new Counters + { + [ApiUsageTracker.CounterTotalCalls] = 120, + [ApiUsageTracker.CounterTotalBytes] = 400 + }; - A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", DateTime.Today, null, ct)) - .Returns(forMonth); + A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", DateTime.Today, null, ct)) + .Returns(forMonth); - A.CallTo(() => usageTracker.QueryAsync($"{key}_API", dateFrom, dateTo, ct)) - .Returns(counters); + A.CallTo(() => usageTracker.QueryAsync($"{key}_API", dateFrom, dateTo, ct)) + .Returns(counters); - var (summary, stats) = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var (summary, stats) = await sut.QueryAsync(key, dateFrom, dateTo, ct); - stats.Should().BeEquivalentTo(new Dictionary<string, List<ApiStats>> + stats.Should().BeEquivalentTo(new Dictionary<string, List<ApiStats>> + { + ["my-category"] = new List<ApiStats> { - ["my-category"] = new List<ApiStats> - { - new ApiStats(dateFrom.AddDays(0), 0, 0, 0), - new ApiStats(dateFrom.AddDays(1), 4, 25, 2048), - new ApiStats(dateFrom.AddDays(2), 0, 0, 0), - new ApiStats(dateFrom.AddDays(3), 2, 30, 1024), - new ApiStats(dateFrom.AddDays(4), 3, 10, 512) - }, - ["*"] = new List<ApiStats> - { - new ApiStats(dateFrom.AddDays(0), 1, 20, 128), - new ApiStats(dateFrom.AddDays(1), 0, 0, 0), - new ApiStats(dateFrom.AddDays(2), 5, 18, 16), - new ApiStats(dateFrom.AddDays(3), 0, 0, 0), - new ApiStats(dateFrom.AddDays(4), 0, 0, 0) - } - }); - - summary.Should().BeEquivalentTo(new ApiStatsSummary(20, 15, 3728, 120, 400)); - } + new ApiStats(dateFrom.AddDays(0), 0, 0, 0), + new ApiStats(dateFrom.AddDays(1), 4, 25, 2048), + new ApiStats(dateFrom.AddDays(2), 0, 0, 0), + new ApiStats(dateFrom.AddDays(3), 2, 30, 1024), + new ApiStats(dateFrom.AddDays(4), 3, 10, 512) + }, + ["*"] = new List<ApiStats> + { + new ApiStats(dateFrom.AddDays(0), 1, 20, 128), + new ApiStats(dateFrom.AddDays(1), 0, 0, 0), + new ApiStats(dateFrom.AddDays(2), 5, 18, 16), + new ApiStats(dateFrom.AddDays(3), 0, 0, 0), + new ApiStats(dateFrom.AddDays(4), 0, 0, 0) + } + }); + + summary.Should().BeEquivalentTo(new ApiStatsSummary(20, 15, 3728, 120, 400)); + } - private static Counters Counters(long calls, long elapsed, long bytes) + private static Counters Counters(long calls, long elapsed, long bytes) + { + return new Counters { - return new Counters - { - [ApiUsageTracker.CounterTotalBytes] = bytes, - [ApiUsageTracker.CounterTotalCalls] = calls, - [ApiUsageTracker.CounterTotalElapsedMs] = elapsed - }; - } + [ApiUsageTracker.CounterTotalBytes] = bytes, + [ApiUsageTracker.CounterTotalCalls] = calls, + [ApiUsageTracker.CounterTotalElapsedMs] = elapsed + }; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index a3fbfdb51d..5d4d55e49f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -10,265 +10,264 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public class BackgroundUsageTrackerTests { - public class BackgroundUsageTrackerTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IUsageRepository usageStore = A.Fake<IUsageRepository>(); + private readonly string key = Guid.NewGuid().ToString(); + private readonly DateTime date = DateTime.Today; + private readonly BackgroundUsageTracker sut; + + public BackgroundUsageTrackerTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly IUsageRepository usageStore = A.Fake<IUsageRepository>(); - private readonly string key = Guid.NewGuid().ToString(); - private readonly DateTime date = DateTime.Today; - private readonly BackgroundUsageTracker sut; - - public BackgroundUsageTrackerTests() - { - ct = cts.Token; - - var log = A.Fake<ILogger<BackgroundUsageTracker>>(); + ct = cts.Token; - sut = new BackgroundUsageTracker(usageStore, log) - { - ForceWrite = true - }; - } + var log = A.Fake<ILogger<BackgroundUsageTracker>>(); - [Fact] - public async Task Should_throw_exception_if_tracking_on_disposed_object() + sut = new BackgroundUsageTracker(usageStore, log) { - sut.Dispose(); + ForceWrite = true + }; + } - await Assert.ThrowsAsync<ObjectDisposedException>(() => sut.TrackAsync(date, key, "category1", new Counters(), ct)); - } + [Fact] + public async Task Should_throw_exception_if_tracking_on_disposed_object() + { + sut.Dispose(); - [Fact] - public async Task Should_throw_exception_if_querying_on_disposed_object() - { - sut.Dispose(); + await Assert.ThrowsAsync<ObjectDisposedException>(() => sut.TrackAsync(date, key, "category1", new Counters(), ct)); + } - await Assert.ThrowsAsync<ObjectDisposedException>(() => sut.QueryAsync(key, date, date.AddDays(1), ct)); - } + [Fact] + public async Task Should_throw_exception_if_querying_on_disposed_object() + { + sut.Dispose(); - [Fact] - public async Task Should_throw_exception_if_querying_monthly_counters_on_disposed_object() - { - sut.Dispose(); + await Assert.ThrowsAsync<ObjectDisposedException>(() => sut.QueryAsync(key, date, date.AddDays(1), ct)); + } - await Assert.ThrowsAsync<ObjectDisposedException>(() => sut.GetForMonthAsync(key, date, null, ct)); - } + [Fact] + public async Task Should_throw_exception_if_querying_monthly_counters_on_disposed_object() + { + sut.Dispose(); - [Fact] - public async Task Should_throw_exception_if_querying_summary_counters_on_disposed_object() - { - sut.Dispose(); + await Assert.ThrowsAsync<ObjectDisposedException>(() => sut.GetForMonthAsync(key, date, null, ct)); + } - await Assert.ThrowsAsync<ObjectDisposedException>(() => sut.GetAsync(key, date, date, null, ct)); - } + [Fact] + public async Task Should_throw_exception_if_querying_summary_counters_on_disposed_object() + { + sut.Dispose(); - [Fact] - public void Should_provide_fallback_category() - { - Assert.Equal("*", sut.FallbackCategory); - } + await Assert.ThrowsAsync<ObjectDisposedException>(() => sut.GetAsync(key, date, date, null, ct)); + } - [Fact] - public async Task Should_forward_delete_prefix_call() - { - await sut.DeleteByKeyPatternAsync("pattern", ct); + [Fact] + public void Should_provide_fallback_category() + { + Assert.Equal("*", sut.FallbackCategory); + } - A.CallTo(() => usageStore.DeleteByKeyPatternAsync("pattern", ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_forward_delete_prefix_call() + { + await sut.DeleteByKeyPatternAsync("pattern", ct); - [Fact] - public async Task Should_forward_delete_call() - { - await sut.DeleteAsync(key, ct); + A.CallTo(() => usageStore.DeleteByKeyPatternAsync("pattern", ct)) + .MustHaveHappened(); + } - A.CallTo(() => usageStore.DeleteAsync(key, ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_forward_delete_call() + { + await sut.DeleteAsync(key, ct); + + A.CallTo(() => usageStore.DeleteAsync(key, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_sum_up_if_getting_monthly_calls() + [Fact] + public async Task Should_sum_up_if_getting_monthly_calls() + { + var dateFrom = new DateTime(date.Year, date.Month, 1); + var dateTo = dateFrom.AddMonths(1).AddDays(-1); + + var originalData = new List<StoredUsage> { - var dateFrom = new DateTime(date.Year, date.Month, 1); - var dateTo = dateFrom.AddMonths(1).AddDays(-1); + new StoredUsage("category1", date.AddDays(1), Counters(a: 10, b: 15)), + new StoredUsage("category1", date.AddDays(3), Counters(a: 13, b: 18)), + new StoredUsage("category2", date.AddDays(5), Counters(a: 15)), + new StoredUsage("category2", date.AddDays(7), Counters(b: 22)) + }; - var originalData = new List<StoredUsage> - { - new StoredUsage("category1", date.AddDays(1), Counters(a: 10, b: 15)), - new StoredUsage("category1", date.AddDays(3), Counters(a: 13, b: 18)), - new StoredUsage("category2", date.AddDays(5), Counters(a: 15)), - new StoredUsage("category2", date.AddDays(7), Counters(b: 22)) - }; + A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) + .Returns(originalData); - A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) - .Returns(originalData); + var actual1 = await sut.GetForMonthAsync(key, date, null, ct); + var actual2 = await sut.GetForMonthAsync(key, date, "category2", ct); - var actual1 = await sut.GetForMonthAsync(key, date, null, ct); - var actual2 = await sut.GetForMonthAsync(key, date, "category2", ct); + Assert.Equal(38, actual1["A"]); + Assert.Equal(55, actual1["B"]); - Assert.Equal(38, actual1["A"]); - Assert.Equal(55, actual1["B"]); + Assert.Equal(22, actual2["B"]); + } - Assert.Equal(22, actual2["B"]); - } + [Fact] + public async Task Should_sum_up_if_getting_last_calls_calls() + { + var dateFrom = date; + var dateTo = dateFrom.AddDays(10); - [Fact] - public async Task Should_sum_up_if_getting_last_calls_calls() + var originalData = new List<StoredUsage> { - var dateFrom = date; - var dateTo = dateFrom.AddDays(10); + new StoredUsage("category1", date.AddDays(1), Counters(a: 10, b: 15)), + new StoredUsage("category1", date.AddDays(3), Counters(a: 13, b: 18)), + new StoredUsage("category2", date.AddDays(5), Counters(a: 15)), + new StoredUsage("category2", date.AddDays(7), Counters(b: 22)) + }; - var originalData = new List<StoredUsage> - { - new StoredUsage("category1", date.AddDays(1), Counters(a: 10, b: 15)), - new StoredUsage("category1", date.AddDays(3), Counters(a: 13, b: 18)), - new StoredUsage("category2", date.AddDays(5), Counters(a: 15)), - new StoredUsage("category2", date.AddDays(7), Counters(b: 22)) - }; + A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) + .Returns(originalData); - A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) - .Returns(originalData); + var actual1 = await sut.GetAsync(key, dateFrom, dateTo, null, ct); + var actual2 = await sut.GetAsync(key, dateFrom, dateTo, "category2", ct); - var actual1 = await sut.GetAsync(key, dateFrom, dateTo, null, ct); - var actual2 = await sut.GetAsync(key, dateFrom, dateTo, "category2", ct); + Assert.Equal(38, actual1["A"]); + Assert.Equal(55, actual1["B"]); - Assert.Equal(38, actual1["A"]); - Assert.Equal(55, actual1["B"]); - - Assert.Equal(22, actual2["B"]); - } + Assert.Equal(22, actual2["B"]); + } - [Fact] - public async Task Should_create_empty_actuals_with_default_category_is_actual_is_empty() - { - var dateFrom = date; - var dateTo = dateFrom.AddDays(4); + [Fact] + public async Task Should_create_empty_actuals_with_default_category_is_actual_is_empty() + { + var dateFrom = date; + var dateTo = dateFrom.AddDays(4); - A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) - .Returns(new List<StoredUsage>()); + A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) + .Returns(new List<StoredUsage>()); - var actual = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var actual = await sut.QueryAsync(key, dateFrom, dateTo, ct); - var expected = new Dictionary<string, List<(DateTime Date, Counters Counters)>> + var expected = new Dictionary<string, List<(DateTime Date, Counters Counters)>> + { + ["*"] = new List<(DateTime Date, Counters Counters)> { - ["*"] = new List<(DateTime Date, Counters Counters)> - { - (dateFrom.AddDays(0), new Counters()), - (dateFrom.AddDays(1), new Counters()), - (dateFrom.AddDays(2), new Counters()), - (dateFrom.AddDays(3), new Counters()), - (dateFrom.AddDays(4), new Counters()) - } - }; - - actual.Should().BeEquivalentTo(expected); - } + (dateFrom.AddDays(0), new Counters()), + (dateFrom.AddDays(1), new Counters()), + (dateFrom.AddDays(2), new Counters()), + (dateFrom.AddDays(3), new Counters()), + (dateFrom.AddDays(4), new Counters()) + } + }; - [Fact] - public async Task Should_create_actuals_with_filled_days() - { - var dateFrom = date; - var dateTo = dateFrom.AddDays(4); + actual.Should().BeEquivalentTo(expected); + } - var originalData = new List<StoredUsage> - { - new StoredUsage("my-category", dateFrom.AddDays(1), Counters(a: 10, b: 15)), - new StoredUsage("my-category", dateFrom.AddDays(3), Counters(a: 13, b: 18)), - new StoredUsage("my-category", dateFrom.AddDays(4), Counters(a: 15, b: 20)), - new StoredUsage(null, dateFrom.AddDays(0), Counters(a: 17, b: 22)), - new StoredUsage(null, dateFrom.AddDays(2), Counters(a: 11, b: 14)) - }; + [Fact] + public async Task Should_create_actuals_with_filled_days() + { + var dateFrom = date; + var dateTo = dateFrom.AddDays(4); - A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) - .Returns(originalData); + var originalData = new List<StoredUsage> + { + new StoredUsage("my-category", dateFrom.AddDays(1), Counters(a: 10, b: 15)), + new StoredUsage("my-category", dateFrom.AddDays(3), Counters(a: 13, b: 18)), + new StoredUsage("my-category", dateFrom.AddDays(4), Counters(a: 15, b: 20)), + new StoredUsage(null, dateFrom.AddDays(0), Counters(a: 17, b: 22)), + new StoredUsage(null, dateFrom.AddDays(2), Counters(a: 11, b: 14)) + }; - var actual = await sut.QueryAsync(key, dateFrom, dateTo, ct); + A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) + .Returns(originalData); - var expected = new Dictionary<string, List<(DateTime Date, Counters Counters)>> - { - ["my-category"] = new List<(DateTime Date, Counters Counters)> - { - (dateFrom.AddDays(0), Counters()), - (dateFrom.AddDays(1), Counters(a: 10, b: 15)), - (dateFrom.AddDays(2), Counters()), - (dateFrom.AddDays(3), Counters(a: 13, b: 18)), - (dateFrom.AddDays(4), Counters(a: 15, b: 20)) - }, - ["*"] = new List<(DateTime Date, Counters Counters)> - { - (dateFrom.AddDays(0), Counters(a: 17, b: 22)), - (dateFrom.AddDays(1), Counters()), - (dateFrom.AddDays(2), Counters(a: 11, b: 14)), - (dateFrom.AddDays(3), Counters()), - (dateFrom.AddDays(4), Counters()) - } - }; - - actual.Should().BeEquivalentTo(expected); - } + var actual = await sut.QueryAsync(key, dateFrom, dateTo, ct); - [Fact] - public async Task Should_write_usage_in_batches() + var expected = new Dictionary<string, List<(DateTime Date, Counters Counters)>> { - var key1 = Guid.NewGuid().ToString(); - var key2 = Guid.NewGuid().ToString(); - var key3 = Guid.NewGuid().ToString(); - - await sut.TrackAsync(date, key1, "my-category", Counters(a: 1, b: 1000), ct); + ["my-category"] = new List<(DateTime Date, Counters Counters)> + { + (dateFrom.AddDays(0), Counters()), + (dateFrom.AddDays(1), Counters(a: 10, b: 15)), + (dateFrom.AddDays(2), Counters()), + (dateFrom.AddDays(3), Counters(a: 13, b: 18)), + (dateFrom.AddDays(4), Counters(a: 15, b: 20)) + }, + ["*"] = new List<(DateTime Date, Counters Counters)> + { + (dateFrom.AddDays(0), Counters(a: 17, b: 22)), + (dateFrom.AddDays(1), Counters()), + (dateFrom.AddDays(2), Counters(a: 11, b: 14)), + (dateFrom.AddDays(3), Counters()), + (dateFrom.AddDays(4), Counters()) + } + }; - await sut.TrackAsync(date, key2, "my-category", Counters(a: 1.0, b: 2000), ct); - await sut.TrackAsync(date, key2, "my-category", Counters(a: 0.5, b: 3000), ct); + actual.Should().BeEquivalentTo(expected); + } - await sut.TrackAsync(date, key3, "my-category", Counters(a: 0.3, b: 4000), ct); - await sut.TrackAsync(date, key3, "my-category", Counters(a: 0.1, b: 5000), ct); + [Fact] + public async Task Should_write_usage_in_batches() + { + var key1 = Guid.NewGuid().ToString(); + var key2 = Guid.NewGuid().ToString(); + var key3 = Guid.NewGuid().ToString(); - await sut.TrackAsync(date, key3, null, Counters(a: 0.5, b: 2000), ct); - await sut.TrackAsync(date, key3, null, Counters(a: 0.5, b: 6000), ct); + await sut.TrackAsync(date, key1, "my-category", Counters(a: 1, b: 1000), ct); - UsageUpdate[]? updates = null; + await sut.TrackAsync(date, key2, "my-category", Counters(a: 1.0, b: 2000), ct); + await sut.TrackAsync(date, key2, "my-category", Counters(a: 0.5, b: 3000), ct); - A.CallTo(() => usageStore.TrackUsagesAsync(A<UsageUpdate[]>._, A<CancellationToken>._)) - .Invokes(args => - { - updates = args.GetArgument<UsageUpdate[]>(0)!; - }); + await sut.TrackAsync(date, key3, "my-category", Counters(a: 0.3, b: 4000), ct); + await sut.TrackAsync(date, key3, "my-category", Counters(a: 0.1, b: 5000), ct); - sut.Next(); - sut.Dispose(); + await sut.TrackAsync(date, key3, null, Counters(a: 0.5, b: 2000), ct); + await sut.TrackAsync(date, key3, null, Counters(a: 0.5, b: 6000), ct); - // Wait for the timer to trigger. - await Task.Delay(500, ct); + UsageUpdate[]? updates = null; - updates.Should().BeEquivalentTo(new[] + A.CallTo(() => usageStore.TrackUsagesAsync(A<UsageUpdate[]>._, A<CancellationToken>._)) + .Invokes(args => { - new UsageUpdate(date, key1, "my-category", Counters(a: 1.0, b: 1000)), - new UsageUpdate(date, key2, "my-category", Counters(a: 1.5, b: 5000)), - new UsageUpdate(date, key3, "my-category", Counters(a: 0.4, b: 9000)), - new UsageUpdate(date, key3, "*", Counters(1, 8000)) - }, o => o.ComparingByMembers<UsageUpdate>()); - - A.CallTo(() => usageStore.TrackUsagesAsync(A<UsageUpdate[]>._, A<CancellationToken>._)) - .MustHaveHappened(); - } + updates = args.GetArgument<UsageUpdate[]>(0)!; + }); + + sut.Next(); + sut.Dispose(); - private static Counters Counters(double? a = null, double? b = null) + // Wait for the timer to trigger. + await Task.Delay(500, ct); + + updates.Should().BeEquivalentTo(new[] { - var actual = new Counters(); + new UsageUpdate(date, key1, "my-category", Counters(a: 1.0, b: 1000)), + new UsageUpdate(date, key2, "my-category", Counters(a: 1.5, b: 5000)), + new UsageUpdate(date, key3, "my-category", Counters(a: 0.4, b: 9000)), + new UsageUpdate(date, key3, "*", Counters(1, 8000)) + }, o => o.ComparingByMembers<UsageUpdate>()); + + A.CallTo(() => usageStore.TrackUsagesAsync(A<UsageUpdate[]>._, A<CancellationToken>._)) + .MustHaveHappened(); + } - if (a != null) - { - actual["A"] = a.Value; - } + private static Counters Counters(double? a = null, double? b = null) + { + var actual = new Counters(); - if (b != null) - { - actual["B"] = b.Value; - } + if (a != null) + { + actual["A"] = a.Value; + } - return actual; + if (b != null) + { + actual["B"] = b.Value; } + + return actual; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs index f8f98f43cb..f0628c8fb0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs @@ -10,128 +10,127 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Squidex.Infrastructure.UsageTracking +namespace Squidex.Infrastructure.UsageTracking; + +public class CachingUsageTrackerTests { - public class CachingUsageTrackerTests + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly MemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly string key = Guid.NewGuid().ToString(); + private readonly string category = Guid.NewGuid().ToString(); + private readonly DateTime date = DateTime.Today; + private readonly IUsageTracker inner = A.Fake<IUsageTracker>(); + private readonly IUsageTracker sut; + + public CachingUsageTrackerTests() { - private readonly CancellationTokenSource cts = new CancellationTokenSource(); - private readonly CancellationToken ct; - private readonly MemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly string key = Guid.NewGuid().ToString(); - private readonly string category = Guid.NewGuid().ToString(); - private readonly DateTime date = DateTime.Today; - private readonly IUsageTracker inner = A.Fake<IUsageTracker>(); - private readonly IUsageTracker sut; - - public CachingUsageTrackerTests() - { - ct = cts.Token; - - sut = new CachingUsageTracker(inner, cache); - } + ct = cts.Token; - [Fact] - public void Should_forward_fallback_category() - { - A.CallTo(() => inner.FallbackCategory) - .Returns("*"); + sut = new CachingUsageTracker(inner, cache); + } - Assert.Equal("*", sut.FallbackCategory); - } + [Fact] + public void Should_forward_fallback_category() + { + A.CallTo(() => inner.FallbackCategory) + .Returns("*"); - [Fact] - public async Task Should_forward_delete_prefix_call() - { - await sut.DeleteByKeyPatternAsync("pattern", ct); + Assert.Equal("*", sut.FallbackCategory); + } - A.CallTo(() => inner.DeleteByKeyPatternAsync("pattern", ct)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_forward_delete_prefix_call() + { + await sut.DeleteByKeyPatternAsync("pattern", ct); - [Fact] - public async Task Should_forward_delete_call() - { - await sut.DeleteAsync(key, ct); + A.CallTo(() => inner.DeleteByKeyPatternAsync("pattern", ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_delete_call() + { + await sut.DeleteAsync(key, ct); - A.CallTo(() => inner.DeleteAsync(key, ct)) - .MustHaveHappened(); - } + A.CallTo(() => inner.DeleteAsync(key, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_forward_track_call() - { - var counters = new Counters(); + [Fact] + public async Task Should_forward_track_call() + { + var counters = new Counters(); - await sut.TrackAsync(date, key, "my-category", counters, ct); + await sut.TrackAsync(date, key, "my-category", counters, ct); - A.CallTo(() => inner.TrackAsync(date, key, "my-category", counters, ct)) - .MustHaveHappened(); - } + A.CallTo(() => inner.TrackAsync(date, key, "my-category", counters, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_forward_query_call() - { - var dateFrom = date; - var dateTo = dateFrom.AddDays(10); + [Fact] + public async Task Should_forward_query_call() + { + var dateFrom = date; + var dateTo = dateFrom.AddDays(10); - await sut.QueryAsync(key, dateFrom, dateTo, ct); + await sut.QueryAsync(key, dateFrom, dateTo, ct); - A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo, ct)) - .MustHaveHappened(); - } + A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo, ct)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_cache_monthly_usage() - { - var counters = new Counters(); + [Fact] + public async Task Should_cache_monthly_usage() + { + var counters = new Counters(); - A.CallTo(() => inner.GetForMonthAsync(key, date, category, ct)) - .Returns(counters); + A.CallTo(() => inner.GetForMonthAsync(key, date, category, ct)) + .Returns(counters); - var actual1 = await sut.GetForMonthAsync(key, date, category, ct); - var actual2 = await sut.GetForMonthAsync(key, date, category, ct); + var actual1 = await sut.GetForMonthAsync(key, date, category, ct); + var actual2 = await sut.GetForMonthAsync(key, date, category, ct); - Assert.Same(counters, actual1); - Assert.Same(counters, actual2); + Assert.Same(counters, actual1); + Assert.Same(counters, actual2); - A.CallTo(() => inner.GetForMonthAsync(key, DateTime.Today, category, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => inner.GetForMonthAsync(key, DateTime.Today, category, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_cache_days_usage() - { - var counters = new Counters(); + [Fact] + public async Task Should_cache_days_usage() + { + var counters = new Counters(); - var dateFrom = date; - var dateTo = dateFrom.AddDays(10); + var dateFrom = date; + var dateTo = dateFrom.AddDays(10); - A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category, ct)) - .Returns(counters); + A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category, ct)) + .Returns(counters); - var actual1 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); - var actual2 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); + var actual1 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); + var actual2 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); - Assert.Same(counters, actual1); - Assert.Same(counters, actual2); + Assert.Same(counters, actual1); + Assert.Same(counters, actual2); - A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category, ct)) - .MustHaveHappenedOnceExactly(); - } + A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category, ct)) + .MustHaveHappenedOnceExactly(); + } - [Fact] - public async Task Should_not_cache_queries() - { - var dateFrom = date; - var dateTo = dateFrom.AddDays(10); + [Fact] + public async Task Should_not_cache_queries() + { + var dateFrom = date; + var dateTo = dateFrom.AddDays(10); - var actual1 = await sut.QueryAsync(key, dateFrom, dateTo, ct); - var actual2 = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var actual1 = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var actual2 = await sut.QueryAsync(key, dateFrom, dateTo, ct); - Assert.NotSame(actual2, actual1); + Assert.NotSame(actual2, actual1); - A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo, ct)) - .MustHaveHappenedTwiceOrMore(); - } + A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo, ct)) + .MustHaveHappenedTwiceOrMore(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs index 876ddf5f82..ae5a322855 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs @@ -10,55 +10,54 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class ValidationExceptionTests { - public class ValidationExceptionTests + [Fact] + public void Should_format_message_from_error() { - [Fact] - public void Should_format_message_from_error() - { - var ex = new ValidationException("Error."); + var ex = new ValidationException("Error."); - Assert.Equal("Error.", ex.Message); - } + Assert.Equal("Error.", ex.Message); + } - [Fact] - public void Should_append_dot_to_error() - { - var ex = new ValidationException("Error"); + [Fact] + public void Should_append_dot_to_error() + { + var ex = new ValidationException("Error"); - Assert.Equal("Error.", ex.Message); - } + Assert.Equal("Error.", ex.Message); + } - [Fact] - public void Should_format_message_from_errors() + [Fact] + public void Should_format_message_from_errors() + { + var errors = new List<ValidationError> { - var errors = new List<ValidationError> - { - new ValidationError("Error1"), - new ValidationError("Error2") - }; + new ValidationError("Error1"), + new ValidationError("Error2") + }; - var ex = new ValidationException(errors); + var ex = new ValidationException(errors); - Assert.Equal("Error1. Error2.", ex.Message); - } + Assert.Equal("Error1. Error2.", ex.Message); + } - [Fact] - public void Should_serialize_and_deserialize() + [Fact] + public void Should_serialize_and_deserialize() + { + var errors = new List<ValidationError> { - var errors = new List<ValidationError> - { - new ValidationError("Error1"), - new ValidationError("Error2") - }; + new ValidationError("Error1"), + new ValidationError("Error2") + }; - var source = new ValidationException(errors); - var actual = source.SerializeAndDeserializeBinary(); + var source = new ValidationException(errors); + var actual = source.SerializeAndDeserializeBinary(); - actual.Errors.Should().BeEquivalentTo(source.Errors); + actual.Errors.Should().BeEquivalentTo(source.Errors); - Assert.Equal(source.Message, actual.Message); - } + Assert.Equal(source.Message, actual.Message); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs index 4fc0cda659..8b0c588647 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/ValidationExtensionsTests.cs @@ -8,49 +8,48 @@ using Squidex.Infrastructure.Validation; using Xunit; -namespace Squidex.Infrastructure +namespace Squidex.Infrastructure; + +public class ValidationExtensionsTests { - public class ValidationExtensionsTests + [Fact] + public void Should_return_true_if_is_between() + { + Assert.True(1.IsBetween(0, 2)); + } + + [Fact] + public void Should_return_false_if_is_not_between() + { + Assert.False(1.IsBetween(2, 3)); + } + + [Fact] + public void Should_return_true_if_is_valid_regex() + { + const string regex = "[a-z]*"; + + Assert.True(regex.IsValidRegex()); + } + + [Fact] + public void Should_return_true_if_is_not_a_valid_regex() + { + const string regex = "([a-z]*"; + + Assert.False(regex.IsValidRegex()); + } + + [Fact] + public void Should_return_true_if_enum_is_valid() + { + Assert.True(DateTimeKind.Local.IsEnumValue()); + } + + [Fact] + public void Should_return_false_if_enum_is_not_valid() { - [Fact] - public void Should_return_true_if_is_between() - { - Assert.True(1.IsBetween(0, 2)); - } - - [Fact] - public void Should_return_false_if_is_not_between() - { - Assert.False(1.IsBetween(2, 3)); - } - - [Fact] - public void Should_return_true_if_is_valid_regex() - { - const string regex = "[a-z]*"; - - Assert.True(regex.IsValidRegex()); - } - - [Fact] - public void Should_return_true_if_is_not_a_valid_regex() - { - const string regex = "([a-z]*"; - - Assert.False(regex.IsValidRegex()); - } - - [Fact] - public void Should_return_true_if_enum_is_valid() - { - Assert.True(DateTimeKind.Local.IsEnumValue()); - } - - [Fact] - public void Should_return_false_if_enum_is_not_valid() - { - Assert.False(((DateTimeKind)13).IsEnumValue()); - Assert.False(123.IsEnumValue()); - } + Assert.False(((DateTimeKind)13).IsEnumValue()); + Assert.False(123.IsEnumValue()); } } \ No newline at end of file diff --git a/backend/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs index e9d0487095..2ec7a8d75f 100644 --- a/backend/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiCostsAttributeTests.cs @@ -7,16 +7,15 @@ using Xunit; -namespace Squidex.Web +namespace Squidex.Web; + +public class ApiCostsAttributeTests { - public class ApiCostsAttributeTests + [Fact] + public void Should_assign_costs() { - [Fact] - public void Should_assign_costs() - { - var sut = new ApiCostsAttribute(10.5); + var sut = new ApiCostsAttribute(10.5); - Assert.Equal(10.5, sut.Costs); - } + Assert.Equal(10.5, sut.Costs); } } diff --git a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs index b2d665b89b..7152a97d0a 100644 --- a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs @@ -20,269 +20,268 @@ #pragma warning disable MA0015 // Specify the parameter name in ArgumentException -namespace Squidex.Web +namespace Squidex.Web; + +public class ApiExceptionFilterAttributeTests { - public class ApiExceptionFilterAttributeTests - { - private readonly ILogger<ApiExceptionFilterAttribute> log = A.Fake<ILogger<ApiExceptionFilterAttribute>>(); - private readonly ApiExceptionFilterAttribute sut = new ApiExceptionFilterAttribute(); + private readonly ILogger<ApiExceptionFilterAttribute> log = A.Fake<ILogger<ApiExceptionFilterAttribute>>(); + private readonly ApiExceptionFilterAttribute sut = new ApiExceptionFilterAttribute(); - [Fact] - public void Should_generate_400_for_ValidationException() + [Fact] + public void Should_generate_400_for_ValidationException() + { + var errors = new List<ValidationError> { - var errors = new List<ValidationError> - { - new ValidationError("Error1"), - new ValidationError("Error2", "Property0"), - new ValidationError("Error3", "Property1", "Property2"), - new ValidationError("Error4", "Property3.Property4"), - new ValidationError("Error5", "Property5[0].Property6") - }; - - var ex = new ValidationException(errors); - - var context = Error(ex); + new ValidationError("Error1"), + new ValidationError("Error2", "Property0"), + new ValidationError("Error3", "Property1", "Property2"), + new ValidationError("Error4", "Property3.Property4"), + new ValidationError("Error5", "Property5[0].Property6") + }; - sut.OnException(context); + var ex = new ValidationException(errors); - var actual = (ObjectResult)context.Result!; + var context = Error(ex); - Assert.Equal(400, actual.StatusCode); - Assert.Equal(400, (actual.Value as ErrorDto)?.StatusCode); + sut.OnException(context); - Assert.Equal(new[] - { - "Error1", - "property0: Error2", - "property1, property2: Error3", - "property3.property4: Error4", - "property5[0].property6: Error5" - }, ((ErrorDto)actual.Value!).Details); + var actual = (ObjectResult)context.Result!; - A.CallTo(log) - .MustNotHaveHappened(); - } + Assert.Equal(400, actual.StatusCode); + Assert.Equal(400, (actual.Value as ErrorDto)?.StatusCode); - [Fact] - public void Should_generate_404_for_DomainObjectNotFoundException() + Assert.Equal(new[] { - var context = Error(new DomainObjectNotFoundException("1")); + "Error1", + "property0: Error2", + "property1, property2: Error3", + "property3.property4: Error4", + "property5[0].property6: Error5" + }, ((ErrorDto)actual.Value!).Details); + + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_404_for_DomainObjectNotFoundException() + { + var context = Error(new DomainObjectNotFoundException("1")); - Assert.IsType<NotFoundResult>(context.Result); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Assert.IsType<NotFoundResult>(context.Result); - [Fact] - public void Should_generate_500_and_log_for_unknown_error() - { - var context = Error(new InvalidOperationException()); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_500_and_log_for_unknown_error() + { + var context = Error(new InvalidOperationException()); - Validate(500, context.Result, null); + sut.OnException(context); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<Exception>(3) == context.Exception) - .MustHaveHappened(); - } + Validate(500, context.Result, null); - [Fact] - public void Should_generate_400_for_DomainException() - { - var context = Error(new DomainException("NotAllowed")); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<Exception>(3) == context.Exception) + .MustHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_400_for_DomainException() + { + var context = Error(new DomainException("NotAllowed")); - Validate(400, context.Result, context.Exception); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(400, context.Result, context.Exception); - [Fact] - public void Should_generate_400_for_DomainException_with_error_code() - { - var context = Error(new DomainException("NotAllowed", "ERROR_CODE_XYZ")); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_400_for_DomainException_with_error_code() + { + var context = Error(new DomainException("NotAllowed", "ERROR_CODE_XYZ")); - Validate(400, context.Result, context.Exception, "ERROR_CODE_XYZ"); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(400, context.Result, context.Exception, "ERROR_CODE_XYZ"); - [Fact] - public void Should_generate_400_for_DecoderFallbackException() - { - var context = Error(new DecoderFallbackException("Decoder")); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_400_for_DecoderFallbackException() + { + var context = Error(new DecoderFallbackException("Decoder")); - Validate(400, context.Result, context.Exception); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(400, context.Result, context.Exception); - [Fact] - public void Should_generate_409_for_DomainObjectConflictException() - { - var context = Error(new DomainObjectConflictException("1")); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_409_for_DomainObjectConflictException() + { + var context = Error(new DomainObjectConflictException("1")); - Validate(409, context.Result, context.Exception, "OBJECT_CONFLICT"); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(409, context.Result, context.Exception, "OBJECT_CONFLICT"); - [Fact] - public void Should_generate_410_for_DomainObjectDeletedException() - { - var context = Error(new DomainObjectDeletedException("1")); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_410_for_DomainObjectDeletedException() + { + var context = Error(new DomainObjectDeletedException("1")); - Validate(410, context.Result, context.Exception, "OBJECT_DELETED"); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(410, context.Result, context.Exception, "OBJECT_DELETED"); - [Fact] - public void Should_generate_412_for_DomainObjectVersionException() - { - var context = Error(new DomainObjectVersionException("1", 1, 2)); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_412_for_DomainObjectVersionException() + { + var context = Error(new DomainObjectVersionException("1", 1, 2)); - Validate(412, context.Result, context.Exception, "OBJECT_VERSION_CONFLICT"); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(412, context.Result, context.Exception, "OBJECT_VERSION_CONFLICT"); - [Fact] - public void Should_generate_403_for_DomainForbiddenException() - { - var context = Error(new DomainForbiddenException("Forbidden")); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_403_for_DomainForbiddenException() + { + var context = Error(new DomainForbiddenException("Forbidden")); - Validate(403, context.Result, context.Exception, "FORBIDDEN"); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(403, context.Result, context.Exception, "FORBIDDEN"); - [Fact] - public void Should_generate_408_for_OperationCanceledException() - { - var context = Error(new OperationCanceledException()); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_408_for_OperationCanceledException() + { + var context = Error(new OperationCanceledException()); - Validate(408, context.Result, null); + sut.OnException(context); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(408, context.Result, null); - [Fact] - public void Should_generate_403_and_log_for_SecurityException() - { - var context = Error(new SecurityException()); + A.CallTo(log) + .MustNotHaveHappened(); + } - sut.OnException(context); + [Fact] + public void Should_generate_403_and_log_for_SecurityException() + { + var context = Error(new SecurityException()); - Validate(403, context.Result, null); + sut.OnException(context); - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<Exception>(3) == context.Exception) - .MustHaveHappened(); - } + Validate(403, context.Result, null); - [Fact] - public async Task Should_unify_errror() - { - var context = Problem(new ProblemDetails { Status = 403, Type = "type" }); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<Exception>(3) == context.Exception) + .MustHaveHappened(); + } - await sut.OnResultExecutionAsync(context, () => Task.FromResult(Result(context))); + [Fact] + public async Task Should_unify_errror() + { + var context = Problem(new ProblemDetails { Status = 403, Type = "type" }); - Validate(403, context.Result, null); + await sut.OnResultExecutionAsync(context, () => Task.FromResult(Result(context))); - A.CallTo(log) - .MustNotHaveHappened(); - } + Validate(403, context.Result, null); - private ResultExecutedContext Result(ResultExecutingContext context) - { - var actionContext = ActionContext(); + A.CallTo(log) + .MustNotHaveHappened(); + } - var actual = context.Result; + private ResultExecutedContext Result(ResultExecutingContext context) + { + var actionContext = ActionContext(); - return new ResultExecutedContext(actionContext, new List<IFilterMetadata>(), actual, context.Controller); - } + var actual = context.Result; - private ResultExecutingContext Problem(ProblemDetails problem) - { - var actionContext = ActionContext(); + return new ResultExecutedContext(actionContext, new List<IFilterMetadata>(), actual, context.Controller); + } - var actual = new ObjectResult(problem) { StatusCode = problem.Status }; + private ResultExecutingContext Problem(ProblemDetails problem) + { + var actionContext = ActionContext(); - return new ResultExecutingContext(actionContext, new List<IFilterMetadata>(), actual, null!); - } + var actual = new ObjectResult(problem) { StatusCode = problem.Status }; - private ExceptionContext Error(Exception exception) - { - var actionContext = ActionContext(); + return new ResultExecutingContext(actionContext, new List<IFilterMetadata>(), actual, null!); + } - return new ExceptionContext(actionContext, new List<IFilterMetadata>()) - { - Exception = exception - }; - } + private ExceptionContext Error(Exception exception) + { + var actionContext = ActionContext(); - private ActionContext ActionContext() + return new ExceptionContext(actionContext, new List<IFilterMetadata>()) { - var services = A.Fake<IServiceProvider>(); - - A.CallTo(() => services.GetService(typeof(ILogger<ApiExceptionFilterAttribute>))) - .Returns(log); + Exception = exception + }; + } - var httpContext = new DefaultHttpContext - { - RequestServices = services - }; + private ActionContext ActionContext() + { + var services = A.Fake<IServiceProvider>(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List<FilterDescriptor>() - }); + A.CallTo(() => services.GetService(typeof(ILogger<ApiExceptionFilterAttribute>))) + .Returns(log); - return actionContext; - } + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; - private static void Validate(int statusCode, IActionResult? actionResult, Exception? exception, string? errorCode = null) + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor { - var actual = actionResult as ObjectResult; + FilterDescriptors = new List<FilterDescriptor>() + }); - var error = actual?.Value as ErrorDto; + return actionContext; + } - Assert.NotNull(error?.Type); + private static void Validate(int statusCode, IActionResult? actionResult, Exception? exception, string? errorCode = null) + { + var actual = actionResult as ObjectResult; + + var error = actual?.Value as ErrorDto; - Assert.Equal(statusCode, actual?.StatusCode); - Assert.Equal(statusCode, error?.StatusCode); - Assert.Equal(errorCode, error?.ErrorCode); + Assert.NotNull(error?.Type); - if (exception != null) - { - Assert.Equal(exception.Message, error?.Message); - } + Assert.Equal(statusCode, actual?.StatusCode); + Assert.Equal(statusCode, error?.StatusCode); + Assert.Equal(errorCode, error?.ErrorCode); + + if (exception != null) + { + Assert.Equal(exception.Message, error?.Message); } } } diff --git a/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs index 86a75a485c..f3964d6e7b 100644 --- a/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs @@ -21,130 +21,129 @@ #pragma warning disable IDE0017 // Simplify object initialization -namespace Squidex.Web +namespace Squidex.Web; + +public class ApiPermissionAttributeTests { - public class ApiPermissionAttributeTests + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly ClaimsIdentity user = new ClaimsIdentity(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private bool isNextCalled; + + public ApiPermissionAttributeTests() { - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionExecutingContext actionExecutingContext; - private readonly ActionExecutionDelegate next; - private readonly ClaimsIdentity user = new ClaimsIdentity(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private bool isNextCalled; - - public ApiPermissionAttributeTests() + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor { - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List<FilterDescriptor>() - }); + FilterDescriptors = new List<FilterDescriptor>() + }); - actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?>(), this); - actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); + actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?>(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); - next = () => - { - isNextCalled = true; + next = () => + { + isNextCalled = true; - return Task.FromResult<ActionExecutedContext>(null!); - }; - } + return Task.FromResult<ActionExecutedContext>(null!); + }; + } - [Fact] - public void Should_use_custom_authorization_scheme() - { - var sut = new ApiPermissionAttribute(); + [Fact] + public void Should_use_custom_authorization_scheme() + { + var sut = new ApiPermissionAttribute(); - Assert.Equal(Constants.ApiSecurityScheme, sut.AuthenticationSchemes); - } + Assert.Equal(Constants.ApiSecurityScheme, sut.AuthenticationSchemes); + } - [Fact] - public async Task Should_make_permission_check_with_app_feature() - { - actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); + [Fact] + public async Task Should_make_permission_check_with_app_feature() + { + actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); - SetContext(); + SetContext(); - var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasCreate); + var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasCreate); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Null(actionExecutingContext.Result); - Assert.True(isNextCalled); - } + Assert.Null(actionExecutingContext.Result); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_make_permission_check_with_schema_feature() - { - actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); - actionExecutingContext.HttpContext.Features.Set<ISchemaFeature>(new SchemaFeature(Mocks.Schema(appId, schemaId))); + [Fact] + public async Task Should_make_permission_check_with_schema_feature() + { + actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); + actionExecutingContext.HttpContext.Features.Set<ISchemaFeature>(new SchemaFeature(Mocks.Schema(appId, schemaId))); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app.schemas.my-schema")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app.schemas.my-schema")); - SetContext(); + SetContext(); - var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasUpdate); + var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasUpdate); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Null(actionExecutingContext.Result); - Assert.True(isNextCalled); - } + Assert.Null(actionExecutingContext.Result); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_return_forbidden_if_user_has_wrong_permission() - { - actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); + [Fact] + public async Task Should_return_forbidden_if_user_has_wrong_permission() + { + actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - SetContext(); + SetContext(); - var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasCreate); + var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasCreate); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_return_forbidden_if_route_data_has_no_value() - { - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + [Fact] + public async Task Should_return_forbidden_if_route_data_has_no_value() + { + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - SetContext(); + SetContext(); - var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasCreate); + var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasCreate); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_return_forbidden_if_user_has_no_permission() - { - SetContext(); + [Fact] + public async Task Should_return_forbidden_if_user_has_no_permission() + { + SetContext(); - var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasCreate); + var sut = new ApiPermissionAttribute(PermissionIds.AppSchemasCreate); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } + Assert.Equal(403, (actionExecutingContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } - private void SetContext() - { - var context = new Context(new ClaimsPrincipal(actionExecutingContext.HttpContext.User), null!); + private void SetContext() + { + var context = new Context(new ClaimsPrincipal(actionExecutingContext.HttpContext.User), null!); - actionExecutingContext.HttpContext.Features.Set(context); - } + actionExecutingContext.HttpContext.Features.Set(context); } } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs index 8c1cb93bb7..96ae53f87a 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs @@ -15,88 +15,87 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public class ETagCommandMiddlewareTests { - public class ETagCommandMiddlewareTests - { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ETagCommandMiddleware sut; + private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ETagCommandMiddleware sut; - public ETagCommandMiddlewareTests() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); + public ETagCommandMiddlewareTests() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); - sut = new ETagCommandMiddleware(httpContextAccessor); - } + sut = new ETagCommandMiddleware(httpContextAccessor); + } - [Fact] - public async Task Should_do_nothing_if_context_is_null() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(null!); + [Fact] + public async Task Should_do_nothing_if_context_is_null() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(null!); - await HandleAsync(new CreateContent(), true); - } + await HandleAsync(new CreateContent(), true); + } - [Fact] - public async Task Should_do_nothing_if_command_has_etag_defined() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; + [Fact] + public async Task Should_do_nothing_if_command_has_etag_defined() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; - var context = await HandleAsync(new CreateContent { ExpectedVersion = 1 }, true); + var context = await HandleAsync(new CreateContent { ExpectedVersion = 1 }, true); - Assert.Equal(1, ((IAggregateCommand)context.Command).ExpectedVersion); - } + Assert.Equal(1, ((IAggregateCommand)context.Command).ExpectedVersion); + } - [Fact] - public async Task Should_add_expected_version_to_command() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; + [Fact] + public async Task Should_add_expected_version_to_command() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; - var context = await HandleAsync(new CreateContent(), true); + var context = await HandleAsync(new CreateContent(), true); - Assert.Equal(13, ((IAggregateCommand)context.Command).ExpectedVersion); - } + Assert.Equal(13, ((IAggregateCommand)context.Command).ExpectedVersion); + } - [Fact] - public async Task Should_add_weak_etag_as_expected_version_to_command() - { - httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13"; + [Fact] + public async Task Should_add_weak_etag_as_expected_version_to_command() + { + httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13"; - var context = await HandleAsync(new CreateContent(), true); + var context = await HandleAsync(new CreateContent(), true); - Assert.Equal(13, ((IAggregateCommand)context.Command).ExpectedVersion); - } + Assert.Equal(13, ((IAggregateCommand)context.Command).ExpectedVersion); + } - [Fact] - public async Task Should_add_version_from_actual_as_etag_to_response() - { - var actual = CommandResult.Empty(DomainId.Empty, 17, 16); + [Fact] + public async Task Should_add_version_from_actual_as_etag_to_response() + { + var actual = CommandResult.Empty(DomainId.Empty, 17, 16); - await HandleAsync(new CreateContent(), actual); + await HandleAsync(new CreateContent(), actual); - Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext!.Response.Headers[HeaderNames.ETag]); - } + Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext!.Response.Headers[HeaderNames.ETag]); + } - [Fact] - public async Task Should_add_version_from_entity_as_etag_to_response() - { - var actual = new ContentEntity { Version = 17 }; + [Fact] + public async Task Should_add_version_from_entity_as_etag_to_response() + { + var actual = new ContentEntity { Version = 17 }; - await HandleAsync(new CreateContent(), actual); + await HandleAsync(new CreateContent(), actual); - Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext!.Response.Headers[HeaderNames.ETag]); - } + Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext!.Response.Headers[HeaderNames.ETag]); + } - private async Task<CommandContext> HandleAsync(ICommand command, object actual) - { - var commandContext = new CommandContext(command, A.Fake<ICommandBus>()).Complete(actual); + private async Task<CommandContext> HandleAsync(ICommand command, object actual) + { + var commandContext = new CommandContext(command, A.Fake<ICommandBus>()).Complete(actual); - await sut.HandleAsync(commandContext, default); + await sut.HandleAsync(commandContext, default); - return commandContext; - } + return commandContext; } } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs index 3019f8cc0f..b5751d5e4f 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs @@ -15,94 +15,93 @@ using Squidex.Infrastructure.Security; using Xunit; -namespace Squidex.Web.CommandMiddlewares -{ - public class EnrichWithActorCommandMiddlewareTests - { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly EnrichWithActorCommandMiddleware sut; +namespace Squidex.Web.CommandMiddlewares; - public EnrichWithActorCommandMiddlewareTests() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); +public class EnrichWithActorCommandMiddlewareTests +{ + private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly EnrichWithActorCommandMiddleware sut; - sut = new EnrichWithActorCommandMiddleware(httpContextAccessor); - } + public EnrichWithActorCommandMiddlewareTests() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); - [Fact] - public async Task Should_throw_security_exception_if_no_subject_or_client_is_found() - { - await Assert.ThrowsAsync<DomainForbiddenException>(() => HandleAsync(new CreateContent())); - } + sut = new EnrichWithActorCommandMiddleware(httpContextAccessor); + } - [Fact] - public async Task Should_do_nothing_if_context_is_null() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(null!); + [Fact] + public async Task Should_throw_security_exception_if_no_subject_or_client_is_found() + { + await Assert.ThrowsAsync<DomainForbiddenException>(() => HandleAsync(new CreateContent())); + } - var context = - await HandleAsync( - new CreateContent()); + [Fact] + public async Task Should_do_nothing_if_context_is_null() + { + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(null!); - Assert.Null(((SquidexCommand)context.Command).Actor); - } + var context = + await HandleAsync( + new CreateContent()); - [Fact] - public async Task Should_assign_actor_from_subject() - { - httpContext.User = CreatePrincipal(OpenIdClaims.Subject, "my-user", "My User"); + Assert.Null(((SquidexCommand)context.Command).Actor); + } - var context = await HandleAsync(new CreateContent()); + [Fact] + public async Task Should_assign_actor_from_subject() + { + httpContext.User = CreatePrincipal(OpenIdClaims.Subject, "my-user", "My User"); - Assert.Equal(RefToken.User("my-user"), ((SquidexCommand)context.Command).Actor); - } + var context = await HandleAsync(new CreateContent()); - [Fact] - public async Task Should_assign_actor_from_client() - { - httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client", null); + Assert.Equal(RefToken.User("my-user"), ((SquidexCommand)context.Command).Actor); + } - var context = await HandleAsync(new CreateContent()); + [Fact] + public async Task Should_assign_actor_from_client() + { + httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client", null); - Assert.Equal(RefToken.Client("my-client"), ((SquidexCommand)context.Command).Actor); - } + var context = await HandleAsync(new CreateContent()); - [Fact] - public async Task Should_not_override_actor() - { - httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client", null); + Assert.Equal(RefToken.Client("my-client"), ((SquidexCommand)context.Command).Actor); + } - var customActor = RefToken.User("me"); + [Fact] + public async Task Should_not_override_actor() + { + httpContext.User = CreatePrincipal(OpenIdClaims.ClientId, "my-client", null); - var context = await HandleAsync(new CreateContent { Actor = customActor }); + var customActor = RefToken.User("me"); - Assert.Equal(customActor, ((SquidexCommand)context.Command).Actor); - } + var context = await HandleAsync(new CreateContent { Actor = customActor }); - private async Task<CommandContext> HandleAsync(ICommand command) - { - var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); + Assert.Equal(customActor, ((SquidexCommand)context.Command).Actor); + } - await sut.HandleAsync(commandContext, default); + private async Task<CommandContext> HandleAsync(ICommand command) + { + var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); - return commandContext; - } + await sut.HandleAsync(commandContext, default); - private static ClaimsPrincipal CreatePrincipal(string claimType, string claimValue, string? name) - { - var identity = new ClaimsIdentity(); + return commandContext; + } - identity.AddClaim(new Claim(claimType, claimValue)); + private static ClaimsPrincipal CreatePrincipal(string claimType, string claimValue, string? name) + { + var identity = new ClaimsIdentity(); - if (name != null) - { - identity.AddClaim(new Claim(OpenIdClaims.Name, name)); - } + identity.AddClaim(new Claim(claimType, claimValue)); - return new ClaimsPrincipal(identity); + if (name != null) + { + identity.AddClaim(new Claim(OpenIdClaims.Name, name)); } + + return new ClaimsPrincipal(identity); } } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs index df1eb8054a..c1645f3dee 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -13,59 +13,58 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public class EnrichWithAppIdCommandMiddlewareTests { - public class EnrichWithAppIdCommandMiddlewareTests - { - private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly Context requestContext; - private readonly EnrichWithAppIdCommandMiddleware sut; + private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly Context requestContext; + private readonly EnrichWithAppIdCommandMiddleware sut; - public EnrichWithAppIdCommandMiddlewareTests() - { - requestContext = Context.Anonymous(Mocks.App(appId)); + public EnrichWithAppIdCommandMiddlewareTests() + { + requestContext = Context.Anonymous(Mocks.App(appId)); - A.CallTo(() => contextProvider.Context) - .Returns(requestContext); + A.CallTo(() => contextProvider.Context) + .Returns(requestContext); - sut = new EnrichWithAppIdCommandMiddleware(contextProvider); - } + sut = new EnrichWithAppIdCommandMiddleware(contextProvider); + } - [Fact] - public async Task Should_throw_exception_if_app_not_found() - { - A.CallTo(() => contextProvider.Context) - .Returns(Context.Anonymous(null!)); + [Fact] + public async Task Should_throw_exception_if_app_not_found() + { + A.CallTo(() => contextProvider.Context) + .Returns(Context.Anonymous(null!)); - await Assert.ThrowsAsync<InvalidOperationException>(() => HandleAsync(new CreateContent())); - } + await Assert.ThrowsAsync<InvalidOperationException>(() => HandleAsync(new CreateContent())); + } - [Fact] - public async Task Should_assign_named_id_to_command() - { - var context = await HandleAsync(new CreateContent()); + [Fact] + public async Task Should_assign_named_id_to_command() + { + var context = await HandleAsync(new CreateContent()); - Assert.Equal(appId, ((IAppCommand)context.Command).AppId); - } + Assert.Equal(appId, ((IAppCommand)context.Command).AppId); + } - [Fact] - public async Task Should_not_override_existing_named_id() - { - var customId = NamedId.Of(DomainId.NewGuid(), "other-app"); + [Fact] + public async Task Should_not_override_existing_named_id() + { + var customId = NamedId.Of(DomainId.NewGuid(), "other-app"); - var context = await HandleAsync(new CreateContent { AppId = customId }); + var context = await HandleAsync(new CreateContent { AppId = customId }); - Assert.Equal(customId, ((IAppCommand)context.Command).AppId); - } + Assert.Equal(customId, ((IAppCommand)context.Command).AppId); + } - private async Task<CommandContext> HandleAsync(ICommand command) - { - var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); + private async Task<CommandContext> HandleAsync(ICommand command) + { + var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); - await sut.HandleAsync(commandContext, default); + await sut.HandleAsync(commandContext, default); - return commandContext; - } + return commandContext; } } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs index 60cb0fdaa7..143f262826 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs @@ -11,68 +11,67 @@ using Squidex.Infrastructure.Commands; using Xunit; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public class EnrichWithContentIdCommandMiddlewareTests { - public class EnrichWithContentIdCommandMiddlewareTests - { - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly EnrichWithContentIdCommandMiddleware sut; + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly EnrichWithContentIdCommandMiddleware sut; - public EnrichWithContentIdCommandMiddlewareTests() - { - sut = new EnrichWithContentIdCommandMiddleware(); - } + public EnrichWithContentIdCommandMiddlewareTests() + { + sut = new EnrichWithContentIdCommandMiddleware(); + } - [Fact] - public async Task Should_replace_content_id_with_schema_id_if_placeholder_used() + [Fact] + public async Task Should_replace_content_id_with_schema_id_if_placeholder_used() + { + var command = new UpdateContent { - var command = new UpdateContent - { - ContentId = DomainId.Create("_schemaId_") - }; + ContentId = DomainId.Create("_schemaId_") + }; - await HandleAsync(command); + await HandleAsync(command); - Assert.Equal(schemaId.Id, command.ContentId); - } + Assert.Equal(schemaId.Id, command.ContentId); + } - [Fact] - public async Task Should_not_replace_content_id_with_schema_for_create_command() + [Fact] + public async Task Should_not_replace_content_id_with_schema_for_create_command() + { + var command = new CreateContent { - var command = new CreateContent - { - ContentId = DomainId.Create("_schemaId_") - }; + ContentId = DomainId.Create("_schemaId_") + }; - await HandleAsync(command); + await HandleAsync(command); - Assert.NotEqual(schemaId.Id, command.ContentId); - } + Assert.NotEqual(schemaId.Id, command.ContentId); + } - [Fact] - public async Task Should_not_replace_content_id_with_schema_id_if_placeholder_not_used() + [Fact] + public async Task Should_not_replace_content_id_with_schema_id_if_placeholder_not_used() + { + var command = new UpdateContent { - var command = new UpdateContent - { - ContentId = DomainId.Create("{custom}") - }; + ContentId = DomainId.Create("{custom}") + }; - await HandleAsync(command); + await HandleAsync(command); - Assert.NotEqual(schemaId.Id, command.ContentId); - } + Assert.NotEqual(schemaId.Id, command.ContentId); + } - private async Task<CommandContext> HandleAsync(ContentCommand command) - { - command.AppId = appId; - command.SchemaId = schemaId; + private async Task<CommandContext> HandleAsync(ContentCommand command) + { + command.AppId = appId; + command.SchemaId = schemaId; - var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); + var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); - await sut.HandleAsync(commandContext, default); + await sut.HandleAsync(commandContext, default); - return commandContext; - } + return commandContext; } } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs index 432002a78a..4e341ee0d4 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs @@ -15,62 +15,61 @@ using Squidex.Web.Pipeline; using Xunit; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public class EnrichWithSchemaIdCommandMiddlewareTests { - public class EnrichWithSchemaIdCommandMiddlewareTests - { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly EnrichWithSchemaIdCommandMiddleware sut; + private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly EnrichWithSchemaIdCommandMiddleware sut; - public EnrichWithSchemaIdCommandMiddlewareTests() - { - httpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); - httpContext.Features.Set<ISchemaFeature>(new SchemaFeature(Mocks.Schema(appId, schemaId))); + public EnrichWithSchemaIdCommandMiddlewareTests() + { + httpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); + httpContext.Features.Set<ISchemaFeature>(new SchemaFeature(Mocks.Schema(appId, schemaId))); - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); - sut = new EnrichWithSchemaIdCommandMiddleware(httpContextAccessor); - } + sut = new EnrichWithSchemaIdCommandMiddleware(httpContextAccessor); + } - [Fact] - public async Task Should_throw_exception_if_schema_not_found() - { - httpContext.Features.Set<ISchemaFeature>(null); + [Fact] + public async Task Should_throw_exception_if_schema_not_found() + { + httpContext.Features.Set<ISchemaFeature>(null); - await Assert.ThrowsAsync<InvalidOperationException>(() => HandleAsync(new CreateContent())); - } + await Assert.ThrowsAsync<InvalidOperationException>(() => HandleAsync(new CreateContent())); + } - [Fact] - public async Task Should_assign_named_id_to_command() - { - var context = await HandleAsync(new CreateContent()); + [Fact] + public async Task Should_assign_named_id_to_command() + { + var context = await HandleAsync(new CreateContent()); - Assert.Equal(schemaId, ((ISchemaCommand)context.Command).SchemaId); - } + Assert.Equal(schemaId, ((ISchemaCommand)context.Command).SchemaId); + } - [Fact] - public async Task Should_not_override_existing_named_id() - { - var customId = NamedId.Of(DomainId.NewGuid(), "other-app"); + [Fact] + public async Task Should_not_override_existing_named_id() + { + var customId = NamedId.Of(DomainId.NewGuid(), "other-app"); - var context = await HandleAsync(new CreateContent { SchemaId = customId }); + var context = await HandleAsync(new CreateContent { SchemaId = customId }); - Assert.Equal(customId, ((ISchemaCommand)context.Command).SchemaId); - } + Assert.Equal(customId, ((ISchemaCommand)context.Command).SchemaId); + } - private async Task<CommandContext> HandleAsync(IAppCommand command) - { - command.AppId = appId; + private async Task<CommandContext> HandleAsync(IAppCommand command) + { + command.AppId = appId; - var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); + var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); - await sut.HandleAsync(commandContext, default); + await sut.HandleAsync(commandContext, default); - return commandContext; - } + return commandContext; } } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithTeamIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithTeamIdCommandMiddlewareTests.cs index 862d9e9857..27bedc8662 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithTeamIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithTeamIdCommandMiddlewareTests.cs @@ -15,58 +15,57 @@ using Squidex.Web.Pipeline; using Xunit; -namespace Squidex.Web.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares; + +public class EnrichWithTeamIdCommandMiddlewareTests { - public class EnrichWithTeamIdCommandMiddlewareTests - { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); - private readonly DomainId teamId = DomainId.NewGuid(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly EnrichWithTeamIdCommandMiddleware sut; + private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly EnrichWithTeamIdCommandMiddleware sut; - public EnrichWithTeamIdCommandMiddlewareTests() - { - httpContext.Features.Set<ITeamFeature>(new TeamFeature(Mocks.Team(teamId))); + public EnrichWithTeamIdCommandMiddlewareTests() + { + httpContext.Features.Set<ITeamFeature>(new TeamFeature(Mocks.Team(teamId))); - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); - sut = new EnrichWithTeamIdCommandMiddleware(httpContextAccessor); - } + sut = new EnrichWithTeamIdCommandMiddleware(httpContextAccessor); + } - [Fact] - public async Task Should_throw_exception_if_team_not_found() - { - httpContext.Features.Set<ITeamFeature>(null); + [Fact] + public async Task Should_throw_exception_if_team_not_found() + { + httpContext.Features.Set<ITeamFeature>(null); - await Assert.ThrowsAsync<InvalidOperationException>(() => HandleAsync(new UpdateTeam())); - } + await Assert.ThrowsAsync<InvalidOperationException>(() => HandleAsync(new UpdateTeam())); + } - [Fact] - public async Task Should_assign_id_to_command() - { - var context = await HandleAsync(new UpdateTeam()); + [Fact] + public async Task Should_assign_id_to_command() + { + var context = await HandleAsync(new UpdateTeam()); - Assert.Equal(teamId, ((ITeamCommand)context.Command).TeamId); - } + Assert.Equal(teamId, ((ITeamCommand)context.Command).TeamId); + } - [Fact] - public async Task Should_not_override_existing_id() - { - var customId = DomainId.NewGuid(); + [Fact] + public async Task Should_not_override_existing_id() + { + var customId = DomainId.NewGuid(); - var context = await HandleAsync(new UpdateTeam { TeamId = customId }); + var context = await HandleAsync(new UpdateTeam { TeamId = customId }); - Assert.Equal(customId, ((ITeamCommand)context.Command).TeamId); - } + Assert.Equal(customId, ((ITeamCommand)context.Command).TeamId); + } - private async Task<CommandContext> HandleAsync(ITeamCommand command) - { - var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); + private async Task<CommandContext> HandleAsync(ITeamCommand command) + { + var commandContext = new CommandContext(command, A.Fake<ICommandBus>()); - await sut.HandleAsync(commandContext, default); + await sut.HandleAsync(commandContext, default); - return commandContext; - } + return commandContext; } } diff --git a/backend/tests/Squidex.Web.Tests/ExposedValuesTests.cs b/backend/tests/Squidex.Web.Tests/ExposedValuesTests.cs index 5d5bedf242..39bd06d49c 100644 --- a/backend/tests/Squidex.Web.Tests/ExposedValuesTests.cs +++ b/backend/tests/Squidex.Web.Tests/ExposedValuesTests.cs @@ -9,79 +9,78 @@ using Microsoft.Extensions.Configuration; using Xunit; -namespace Squidex.Web +namespace Squidex.Web; + +public class ExposedValuesTests { - public class ExposedValuesTests + [Fact] + public void Should_create_from_configuration() { - [Fact] - public void Should_create_from_configuration() + var source = new ExposedConfiguration { - var source = new ExposedConfiguration - { - ["name1"] = "config1", - ["name2"] = "config2", - ["name3"] = "config3" - }; + ["name1"] = "config1", + ["name2"] = "config2", + ["name3"] = "config3" + }; - var configuration = A.Fake<IConfiguration>(); + var configuration = A.Fake<IConfiguration>(); - SetupConfiguration(configuration, "config1", "value1"); - SetupConfiguration(configuration, "config2", "value2"); + SetupConfiguration(configuration, "config1", "value1"); + SetupConfiguration(configuration, "config2", "value2"); - var values = new ExposedValues(source, configuration); + var values = new ExposedValues(source, configuration); - Assert.Equal(2, values.Count); - Assert.Equal("value1", values["name1"]); - Assert.Equal("value2", values["name2"]); - } + Assert.Equal(2, values.Count); + Assert.Equal("value1", values["name1"]); + Assert.Equal("value2", values["name2"]); + } - [Fact] - public void Should_use_version_from_assembly() - { - var source = new ExposedConfiguration(); + [Fact] + public void Should_use_version_from_assembly() + { + var source = new ExposedConfiguration(); - var values = new ExposedValues(source, A.Fake<IConfiguration>(), typeof(ExposedValuesTests).Assembly); + var values = new ExposedValues(source, A.Fake<IConfiguration>(), typeof(ExposedValuesTests).Assembly); - Assert.Equal("1.0.0.0", values["version"]); - } + Assert.Equal("1.0.0.0", values["version"]); + } - [Fact] - public void Should_format_empty_values() - { - var values = new ExposedValues(); + [Fact] + public void Should_format_empty_values() + { + var values = new ExposedValues(); - Assert.Empty(values.ToString()); - } + Assert.Empty(values.ToString()); + } - [Fact] - public void Should_format_from_single_value() + [Fact] + public void Should_format_from_single_value() + { + var values = new ExposedValues { - var values = new ExposedValues - { - ["name1"] = "value1" - }; + ["name1"] = "value1" + }; - Assert.Equal("name1: value1", values.ToString()); - } + Assert.Equal("name1: value1", values.ToString()); + } - [Fact] - public void Should_format_from_multiple_values() + [Fact] + public void Should_format_from_multiple_values() + { + var values = new ExposedValues { - var values = new ExposedValues - { - ["name1"] = "value1", - ["name2"] = "value2" - }; + ["name1"] = "value1", + ["name2"] = "value2" + }; - Assert.Equal("name1: value1, name2: value2", values.ToString()); - } + Assert.Equal("name1: value1, name2: value2", values.ToString()); + } - private static void SetupConfiguration(IConfiguration configuration, string key, string value) - { - var configSection = A.Fake<IConfigurationSection>(); + private static void SetupConfiguration(IConfiguration configuration, string key, string value) + { + var configSection = A.Fake<IConfigurationSection>(); - A.CallTo(() => configSection.Value).Returns(value); - A.CallTo(() => configuration.GetSection(key)).Returns(configSection); - } + A.CallTo(() => configSection.Value).Returns(value); + A.CallTo(() => configuration.GetSection(key)).Returns(configSection); } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs index c63530be3e..6eac852714 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs @@ -16,98 +16,97 @@ using Squidex.Domain.Apps.Entities.Billing; using Xunit; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class ApiCostsFilterTests { - public class ApiCostsFilterTests + private readonly IAppEntity appEntity = A.Fake<IAppEntity>(); + private readonly IUsageGate usageGate = A.Fake<IUsageGate>(); + private readonly ActionExecutingContext actionContext; + private readonly ActionExecutionDelegate next; + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ApiCostsFilter sut; + private bool isNextCalled; + + public ApiCostsFilterTests() { - private readonly IAppEntity appEntity = A.Fake<IAppEntity>(); - private readonly IUsageGate usageGate = A.Fake<IUsageGate>(); - private readonly ActionExecutingContext actionContext; - private readonly ActionExecutionDelegate next; - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ApiCostsFilter sut; - private bool isNextCalled; - - public ApiCostsFilterTests() - { - actionContext = - new ActionExecutingContext( - new ActionContext(httpContext, new RouteData(), - new ActionDescriptor()), - new List<IFilterMetadata>(), new Dictionary<string, object?>(), null!); + actionContext = + new ActionExecutingContext( + new ActionContext(httpContext, new RouteData(), + new ActionDescriptor()), + new List<IFilterMetadata>(), new Dictionary<string, object?>(), null!); - next = () => - { - isNextCalled = true; + next = () => + { + isNextCalled = true; - return Task.FromResult<ActionExecutedContext>(null!); - }; + return Task.FromResult<ActionExecutedContext>(null!); + }; - sut = new ApiCostsFilter(usageGate); - } + sut = new ApiCostsFilter(usageGate); + } - [Fact] - public async Task Should_return_429_status_code_if_blocked() - { - sut.FilterDefinition = new ApiCostsAttribute(1); + [Fact] + public async Task Should_return_429_status_code_if_blocked() + { + sut.FilterDefinition = new ApiCostsAttribute(1); - SetupApp(); + SetupApp(); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A<string>._, DateTime.Today, default)) - .Returns(true); + A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A<string>._, DateTime.Today, default)) + .Returns(true); - await sut.OnActionExecutionAsync(actionContext, next); + await sut.OnActionExecutionAsync(actionContext, next); - Assert.Equal(429, (actionContext.Result as StatusCodeResult)?.StatusCode); - Assert.False(isNextCalled); - } + Assert.Equal(429, (actionContext.Result as StatusCodeResult)?.StatusCode); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_continue_if_not_blocked() - { - sut.FilterDefinition = new ApiCostsAttribute(13); + [Fact] + public async Task Should_continue_if_not_blocked() + { + sut.FilterDefinition = new ApiCostsAttribute(13); - SetupApp(); + SetupApp(); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A<string>._, DateTime.Today, default)) - .Returns(false); + A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A<string>._, DateTime.Today, default)) + .Returns(false); - await sut.OnActionExecutionAsync(actionContext, next); + await sut.OnActionExecutionAsync(actionContext, next); - Assert.True(isNextCalled); - } + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_continue_if_costs_are_zero() - { - sut.FilterDefinition = new ApiCostsAttribute(0); + [Fact] + public async Task Should_continue_if_costs_are_zero() + { + sut.FilterDefinition = new ApiCostsAttribute(0); - SetupApp(); + SetupApp(); - await sut.OnActionExecutionAsync(actionContext, next); + await sut.OnActionExecutionAsync(actionContext, next); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A<string>._, DateTime.Today, default)) - .MustNotHaveHappened(); - } + A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A<string>._, DateTime.Today, default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_continue_if_not_app_request() - { - sut.FilterDefinition = new ApiCostsAttribute(12); + [Fact] + public async Task Should_continue_if_not_app_request() + { + sut.FilterDefinition = new ApiCostsAttribute(12); - await sut.OnActionExecutionAsync(actionContext, next); + await sut.OnActionExecutionAsync(actionContext, next); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A<string>._, DateTime.Today, default)) - .MustNotHaveHappened(); - } + A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A<string>._, DateTime.Today, default)) + .MustNotHaveHappened(); + } - private void SetupApp() - { - httpContext.Features.Set(Context.Anonymous(appEntity)); - } + private void SetupApp() + { + httpContext.Features.Set(Context.Anonymous(appEntity)); } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs index bb14c03366..87099df852 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ApiPermissionUnifierTests.cs @@ -10,50 +10,49 @@ using Squidex.Shared.Identity; using Xunit; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class ApiPermissionUnifierTests { - public class ApiPermissionUnifierTests - { - private readonly ApiPermissionUnifier sut = new ApiPermissionUnifier(); + private readonly ApiPermissionUnifier sut = new ApiPermissionUnifier(); - [Theory] - [InlineData("administrator")] - [InlineData("ADMINISTRATOR")] - public async Task Should_add_admin_permission_if_user_is_in_role(string role) - { - var userIdentity = new ClaimsIdentity(); - var userPrincipal = new ClaimsPrincipal(userIdentity); + [Theory] + [InlineData("administrator")] + [InlineData("ADMINISTRATOR")] + public async Task Should_add_admin_permission_if_user_is_in_role(string role) + { + var userIdentity = new ClaimsIdentity(); + var userPrincipal = new ClaimsPrincipal(userIdentity); - userIdentity.AddClaim(new Claim(userIdentity.RoleClaimType, role)); + userIdentity.AddClaim(new Claim(userIdentity.RoleClaimType, role)); - var actual = await sut.TransformAsync(userPrincipal); + var actual = await sut.TransformAsync(userPrincipal); - Assert.Equal(PermissionIds.Admin, actual.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.Permissions)?.Value); - Assert.Equal(role, actual.Claims.FirstOrDefault(x => x.Type == userIdentity.RoleClaimType)?.Value); - } + Assert.Equal(PermissionIds.Admin, actual.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.Permissions)?.Value); + Assert.Equal(role, actual.Claims.FirstOrDefault(x => x.Type == userIdentity.RoleClaimType)?.Value); + } - [Fact] - public async Task Should_not_add_admin_persmission_if_user_has_other_role() - { - var userIdentity = new ClaimsIdentity(); - var userPrincipal = new ClaimsPrincipal(userIdentity); + [Fact] + public async Task Should_not_add_admin_persmission_if_user_has_other_role() + { + var userIdentity = new ClaimsIdentity(); + var userPrincipal = new ClaimsPrincipal(userIdentity); - userIdentity.AddClaim(new Claim(userIdentity.RoleClaimType, "Developer")); + userIdentity.AddClaim(new Claim(userIdentity.RoleClaimType, "Developer")); - var actual = await sut.TransformAsync(userPrincipal); + var actual = await sut.TransformAsync(userPrincipal); - Assert.Single(actual.Claims); - } + Assert.Single(actual.Claims); + } - [Fact] - public async Task Should_not_add_admin_persmission_if_user_has_no_role() - { - var userIdentity = new ClaimsIdentity(); - var userPrincipal = new ClaimsPrincipal(userIdentity); + [Fact] + public async Task Should_not_add_admin_persmission_if_user_has_no_role() + { + var userIdentity = new ClaimsIdentity(); + var userPrincipal = new ClaimsPrincipal(userIdentity); - var actual = await sut.TransformAsync(userPrincipal); + var actual = await sut.TransformAsync(userPrincipal); - Assert.Empty(actual.Claims); - } + Assert.Empty(actual.Claims); } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index ffb62dfb8b..6685438b12 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -24,303 +24,302 @@ #pragma warning disable IDE0017 // Simplify object initialization -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class AppResolverTests { - public class AppResolverTests + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext; + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly string appName = "my-app"; + private readonly AppResolver sut; + private bool isNextCalled; + + public AppResolverTests() { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionContext actionContext; - private readonly ActionExecutingContext actionExecutingContext; - private readonly ActionExecutionDelegate next; - private readonly string appName = "my-app"; - private readonly AppResolver sut; - private bool isNextCalled; - - public AppResolverTests() + actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor { - actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - EndpointMetadata = new List<object>() - }); + EndpointMetadata = new List<object>() + }); - actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?>(), this); - actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.RouteData.Values["app"] = appName; + actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?>(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.RouteData.Values["app"] = appName; - next = () => - { - isNextCalled = true; + next = () => + { + isNextCalled = true; - return Task.FromResult<ActionExecutedContext>(null!); - }; + return Task.FromResult<ActionExecutedContext>(null!); + }; - sut = new AppResolver(appProvider); - } + sut = new AppResolver(appProvider); + } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task Should_return_404_if_app_name_is_null(string? app) - { - SetupUser(); + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Should_return_404_if_app_name_is_null(string? app) + { + SetupUser(); - actionExecutingContext.RouteData.Values["app"] = app; + actionExecutingContext.RouteData.Values["app"] = app; - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<NotFoundResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); + Assert.IsType<NotFoundResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); - A.CallTo(() => appProvider.GetAppAsync(A<string>._, false, httpContext.RequestAborted)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetAppAsync(A<string>._, false, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_404_if_app_not_found() - { - SetupUser(); + [Fact] + public async Task Should_return_404_if_app_not_found() + { + SetupUser(); - A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) - .Returns(Task.FromResult<IAppEntity?>(null)); + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) + .Returns(Task.FromResult<IAppEntity?>(null)); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<NotFoundResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); - } + Assert.IsType<NotFoundResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_return_401_if_user_is_anonymous() - { - SetupUser(null); + [Fact] + public async Task Should_return_401_if_user_is_anonymous() + { + SetupUser(null); - var app = CreateApp(appName); + var app = CreateApp(appName); - A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<UnauthorizedResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); - } + Assert.IsType<UnauthorizedResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_resolve_app_from_user() - { - var user = SetupUser(); + [Fact] + public async Task Should_resolve_app_from_user() + { + var user = SetupUser(); - var app = CreateApp(appName); + var app = CreateApp(appName); - user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, $"squidex.apps.{appName}")); + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, $"squidex.apps.{appName}")); - A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); + var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Any()); - Assert.True(permissions.Count < 3); - Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); - Assert.True(isNextCalled); - } + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Any()); + Assert.True(permissions.Count < 3); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_resolve_app_from_contributor() - { - var user = SetupUser(); + [Fact] + public async Task Should_resolve_app_from_contributor() + { + var user = SetupUser(); - var app = CreateApp(appName, user: "user1"); + var app = CreateApp(appName, user: "user1"); - user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); + var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Count() > 2); - Assert.True(permissions.Count < 3); - Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); - Assert.True(isNextCalled); - } + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(permissions.Count < 3); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_provide_extra_permissions_if_client_is_frontend() - { - var user = SetupUser(); + [Fact] + public async Task Should_provide_extra_permissions_if_client_is_frontend() + { + var user = SetupUser(); - var app = CreateApp(appName, user: "user1"); + var app = CreateApp(appName, user: "user1"); - user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); - A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); + var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Count() > 2); - Assert.True(permissions.Count > 10); - Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); - Assert.True(isNextCalled); - } + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(permissions.Count > 10); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.apps.{appName}", StringComparison.OrdinalIgnoreCase))); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_resolve_app_from_client() - { - var user = SetupUser(); + [Fact] + public async Task Should_resolve_app_from_client() + { + var user = SetupUser(); - user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); - var app = CreateApp(appName, client: "client1"); + var app = CreateApp(appName, client: "client1"); - A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Count() > 2); - Assert.True(isNextCalled); - } + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_resolve_app_from_anonymous_client() - { - var user = SetupUser(); + [Fact] + public async Task Should_resolve_app_from_anonymous_client() + { + var user = SetupUser(); - var app = CreateApp(appName, client: "client1", allowAnonymous: true); + var app = CreateApp(appName, client: "client1", allowAnonymous: true); - A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Same(app, httpContext.Context().App); - Assert.True(user.Claims.Count() > 2); - Assert.True(isNextCalled); - Assert.Contains(user.Claims, x => x.Type == OpenIdClaims.ClientId && x.Value == "client1"); - } + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + Assert.Contains(user.Claims, x => x.Type == OpenIdClaims.ClientId && x.Value == "client1"); + } - [Fact] - public async Task Should_resolve_app_if_action_allows_anonymous_but_user_has_no_permissions() - { - var user = SetupUser(); + [Fact] + public async Task Should_resolve_app_if_action_allows_anonymous_but_user_has_no_permissions() + { + var user = SetupUser(); - user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - var app = CreateApp(appName); + var app = CreateApp(appName); - actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); + actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); - A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Same(app, httpContext.Context().App); - Assert.Equal(2, user.Claims.Count()); - Assert.True(isNextCalled); - } + Assert.Same(app, httpContext.Context().App); + Assert.Equal(2, user.Claims.Count()); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_return_404_if_user_has_no_permissions() - { - var user = SetupUser(); + [Fact] + public async Task Should_return_404_if_user_has_no_permissions() + { + var user = SetupUser(); - user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - var app = CreateApp(appName); + var app = CreateApp(appName); - A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<NotFoundResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); - } + Assert.IsType<NotFoundResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_return_404_if_client_is_from_another_app() - { - var user = SetupUser(); + [Fact] + public async Task Should_return_404_if_client_is_from_another_app() + { + var user = SetupUser(); - user.AddClaim(new Claim(OpenIdClaims.ClientId, "other:client1")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, "other:client1")); - var app = CreateApp(appName, client: "client1"); + var app = CreateApp(appName, client: "client1"); - A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) - .Returns(app); + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) + .Returns(app); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<NotFoundResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); - } + Assert.IsType<NotFoundResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_do_nothing_if_parameter_not_set() - { - actionExecutingContext.RouteData.Values.Remove("app"); + [Fact] + public async Task Should_do_nothing_if_parameter_not_set() + { + actionExecutingContext.RouteData.Values.Remove("app"); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - A.CallTo(() => appProvider.GetAppAsync(A<string>._, false, httpContext.RequestAborted)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetAppAsync(A<string>._, false, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } - private ClaimsIdentity SetupUser(string? type = "OIDC") - { - var userIdentity = new ClaimsIdentity(type); - var userPrincipal = new ClaimsPrincipal(userIdentity); + private ClaimsIdentity SetupUser(string? type = "OIDC") + { + var userIdentity = new ClaimsIdentity(type); + var userPrincipal = new ClaimsPrincipal(userIdentity); - actionExecutingContext.HttpContext.User = userPrincipal; + actionExecutingContext.HttpContext.User = userPrincipal; - return userIdentity; - } + return userIdentity; + } - private static IAppEntity CreateApp(string name, string? user = null, string? client = null, bool? allowAnonymous = null) - { - var app = A.Fake<IAppEntity>(); + private static IAppEntity CreateApp(string name, string? user = null, string? client = null, bool? allowAnonymous = null) + { + var app = A.Fake<IAppEntity>(); - var contributors = Contributors.Empty; + var contributors = Contributors.Empty; - if (user != null) - { - contributors = contributors.Assign(user, Role.Reader); - } + if (user != null) + { + contributors = contributors.Assign(user, Role.Reader); + } - var clients = AppClients.Empty; + var clients = AppClients.Empty; - if (client != null) - { - clients = clients.Add(client, "secret").Update(client, allowAnonymous: allowAnonymous); - } + if (client != null) + { + clients = clients.Add(client, "secret").Update(client, allowAnonymous: allowAnonymous); + } - A.CallTo(() => app.Contributors).Returns(contributors); - A.CallTo(() => app.Clients).Returns(clients); - A.CallTo(() => app.Name).Returns(name); - A.CallTo(() => app.Roles).Returns(Roles.Empty); + A.CallTo(() => app.Contributors).Returns(contributors); + A.CallTo(() => app.Clients).Returns(clients); + A.CallTo(() => app.Name).Returns(name); + A.CallTo(() => app.Roles).Returns(Roles.Empty); - return app; - } + return app; } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs index f4b0b2d9f7..25e29e724e 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs @@ -15,99 +15,98 @@ using Microsoft.Net.Http.Headers; using Xunit; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class CachingFilterTests { - public class CachingFilterTests + private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionExecutingContext executingContext; + private readonly ActionExecutedContext executedContext; + private readonly CachingOptions cachingOptions = new CachingOptions(); + private readonly CachingManager cachingManager; + private readonly CachingFilter sut; + + public CachingFilterTests() { - private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionExecutingContext executingContext; - private readonly ActionExecutedContext executedContext; - private readonly CachingOptions cachingOptions = new CachingOptions(); - private readonly CachingManager cachingManager; - private readonly CachingFilter sut; - - public CachingFilterTests() - { - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); - cachingManager = new CachingManager(httpContextAccessor, Options.Create(cachingOptions)); + cachingManager = new CachingManager(httpContextAccessor, Options.Create(cachingOptions)); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var actionFilters = new List<IFilterMetadata>(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var actionFilters = new List<IFilterMetadata>(); - executingContext = new ActionExecutingContext(actionContext, actionFilters, new Dictionary<string, object?>(), this); - executedContext = new ActionExecutedContext(actionContext, actionFilters, this) - { - Result = new OkResult() - }; + executingContext = new ActionExecutingContext(actionContext, actionFilters, new Dictionary<string, object?>(), this); + executedContext = new ActionExecutedContext(actionContext, actionFilters, this) + { + Result = new OkResult() + }; - sut = new CachingFilter(cachingManager); - } + sut = new CachingFilter(cachingManager); + } - [Theory] - [InlineData("13", "13")] - [InlineData("13", "W/13")] - [InlineData("W/13", "13")] - [InlineData("W/13", "W/13")] - public async Task Should_return_304_for_same_etags(string ifNoneMatch, string etag) - { - httpContext.Request.Method = HttpMethods.Get; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = ifNoneMatch; + [Theory] + [InlineData("13", "13")] + [InlineData("13", "W/13")] + [InlineData("W/13", "13")] + [InlineData("W/13", "W/13")] + public async Task Should_return_304_for_same_etags(string ifNoneMatch, string etag) + { + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = ifNoneMatch; - httpContext.Response.Headers[HeaderNames.ETag] = etag; + httpContext.Response.Headers[HeaderNames.ETag] = etag; - await sut.OnActionExecutionAsync(executingContext, Next()); + await sut.OnActionExecutionAsync(executingContext, Next()); - Assert.Equal(304, ((StatusCodeResult)executedContext.Result!).StatusCode); - } + Assert.Equal(304, ((StatusCodeResult)executedContext.Result!).StatusCode); + } - [Fact] - public async Task Should_return_304_for_same_etags_from_cache_manager() - { - httpContext.Request.Method = HttpMethods.Get; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "2C70E12B7A0646F92279F427C7B38E7334D8E5389CFF167A1DC30E73F826B683"; + [Fact] + public async Task Should_return_304_for_same_etags_from_cache_manager() + { + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "2C70E12B7A0646F92279F427C7B38E7334D8E5389CFF167A1DC30E73F826B683"; - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddDependency("key"); + await sut.OnActionExecutionAsync(executingContext, () => + { + cachingManager.AddDependency("key"); - return Task.FromResult(executedContext); - }); + return Task.FromResult(executedContext); + }); - Assert.Equal(304, ((StatusCodeResult)executedContext.Result!).StatusCode); - } + Assert.Equal(304, ((StatusCodeResult)executedContext.Result!).StatusCode); + } - [Fact] - public async Task Should_not_return_304_for_different_etags() - { - httpContext.Request.Method = HttpMethods.Get; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; + [Fact] + public async Task Should_not_return_304_for_different_etags() + { + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; - httpContext.Response.Headers[HeaderNames.ETag] = "W/11"; + httpContext.Response.Headers[HeaderNames.ETag] = "W/11"; - await sut.OnActionExecutionAsync(executingContext, Next()); + await sut.OnActionExecutionAsync(executingContext, Next()); - Assert.Equal(200, ((StatusCodeResult)executedContext.Result!).StatusCode); - } + Assert.Equal(200, ((StatusCodeResult)executedContext.Result!).StatusCode); + } - [Fact] - public async Task Should_not_return_304_for_post() - { - httpContext.Request.Method = HttpMethods.Post; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; + [Fact] + public async Task Should_not_return_304_for_post() + { + httpContext.Request.Method = HttpMethods.Post; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; - httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; + httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; - await sut.OnActionExecutionAsync(executingContext, Next()); + await sut.OnActionExecutionAsync(executingContext, Next()); - Assert.Equal(200, ((StatusCodeResult)executedContext.Result!).StatusCode); - } + Assert.Equal(200, ((StatusCodeResult)executedContext.Result!).StatusCode); + } - private ActionExecutionDelegate Next() - { - return () => Task.FromResult(executedContext); - } + private ActionExecutionDelegate Next() + { + return () => Task.FromResult(executedContext); } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs index ad067fd9bd..f8e8fdcd90 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs @@ -16,402 +16,401 @@ using Squidex.Infrastructure.Security; using Xunit; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class CachingKeysMiddlewareTests { - public class CachingKeysMiddlewareTests + private readonly List<(object, Func<object, Task>)> callbacks = new List<(object, Func<object, Task>)>(); + private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); + private readonly IHttpResponseBodyFeature httpResponseBodyFeature = A.Fake<IHttpResponseBodyFeature>(); + private readonly IHttpResponseFeature httpResponseFeature = A.Fake<IHttpResponseFeature>(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly CachingOptions cachingOptions = new CachingOptions(); + private readonly CachingManager cachingManager; + private readonly RequestDelegate next; + private readonly CachingKeysMiddleware sut; + private bool isNextCalled; + + public CachingKeysMiddlewareTests() { - private readonly List<(object, Func<object, Task>)> callbacks = new List<(object, Func<object, Task>)>(); - private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>(); - private readonly IHttpResponseBodyFeature httpResponseBodyFeature = A.Fake<IHttpResponseBodyFeature>(); - private readonly IHttpResponseFeature httpResponseFeature = A.Fake<IHttpResponseFeature>(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly CachingOptions cachingOptions = new CachingOptions(); - private readonly CachingManager cachingManager; - private readonly RequestDelegate next; - private readonly CachingKeysMiddleware sut; - private bool isNextCalled; - - public CachingKeysMiddlewareTests() - { - var headers = new HeaderDictionary(); + var headers = new HeaderDictionary(); - A.CallTo(() => httpResponseFeature.Headers) - .Returns(headers); - - A.CallTo(() => httpResponseFeature.OnStarting(A<Func<object, Task>>._, A<object>._)) - .Invokes(c => - { - callbacks.Add(( - c.GetArgument<object>(1)!, - c.GetArgument<Func<object, Task>>(0)!)); - }); + A.CallTo(() => httpResponseFeature.Headers) + .Returns(headers); - A.CallTo(() => httpResponseBodyFeature.StartAsync(A<CancellationToken>._)) - .Invokes(c => - { - foreach (var (state, callback) in callbacks) - { - callback(state).Wait(httpContext.RequestAborted); - } - }); - - httpContext.Features.Set(httpResponseBodyFeature); - httpContext.Features.Set(httpResponseFeature); + A.CallTo(() => httpResponseFeature.OnStarting(A<Func<object, Task>>._, A<object>._)) + .Invokes(c => + { + callbacks.Add(( + c.GetArgument<object>(1)!, + c.GetArgument<Func<object, Task>>(0)!)); + }); - next = context => + A.CallTo(() => httpResponseBodyFeature.StartAsync(A<CancellationToken>._)) + .Invokes(c => { - isNextCalled = true; + foreach (var (state, callback) in callbacks) + { + callback(state).Wait(httpContext.RequestAborted); + } + }); - return Task.CompletedTask; - }; + httpContext.Features.Set(httpResponseBodyFeature); + httpContext.Features.Set(httpResponseFeature); - A.CallTo(() => httpContextAccessor.HttpContext) - .Returns(httpContext); + next = context => + { + isNextCalled = true; - cachingManager = new CachingManager(httpContextAccessor, Options.Create(cachingOptions)); + return Task.CompletedTask; + }; - sut = new CachingKeysMiddleware(cachingManager, Options.Create(cachingOptions), next); - } + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); - [Fact] - public async Task Should_invoke_next() - { - await MakeRequestAsync(); + cachingManager = new CachingManager(httpContextAccessor, Options.Create(cachingOptions)); - Assert.True(isNextCalled); - } + sut = new CachingKeysMiddleware(cachingManager, Options.Create(cachingOptions), next); + } - [Fact] - public async Task Should_not_append_etag_if_not_found() - { - await MakeRequestAsync(); + [Fact] + public async Task Should_invoke_next() + { + await MakeRequestAsync(); - Assert.Equal(StringValues.Empty, httpContext.Response.Headers[HeaderNames.ETag]); - } + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_append_authorization_header_as_vary() - { - await MakeRequestAsync(); + [Fact] + public async Task Should_not_append_etag_if_not_found() + { + await MakeRequestAsync(); - Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); - } + Assert.Equal(StringValues.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + } - [Fact] - public async Task Should_append_authorization_as_header_if_user_has_subject() - { - var identity = (ClaimsIdentity)httpContext.User.Identity!; + [Fact] + public async Task Should_append_authorization_header_as_vary() + { + await MakeRequestAsync(); - identity.AddClaim(new Claim(OpenIdClaims.Subject, "my-id")); + Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); + } - await MakeRequestAsync(); + [Fact] + public async Task Should_append_authorization_as_header_if_user_has_subject() + { + var identity = (ClaimsIdentity)httpContext.User.Identity!; - Assert.Equal("Auth-State,Authorization", httpContext.Response.Headers[HeaderNames.Vary]); - } + identity.AddClaim(new Claim(OpenIdClaims.Subject, "my-id")); - [Fact] - public async Task Should_append_client_id_as_header_if_user_has_client_but_no_subject() - { - var identity = (ClaimsIdentity)httpContext.User.Identity!; + await MakeRequestAsync(); - identity.AddClaim(new Claim(OpenIdClaims.ClientId, "my-client")); + Assert.Equal("Auth-State,Authorization", httpContext.Response.Headers[HeaderNames.Vary]); + } - await MakeRequestAsync(); + [Fact] + public async Task Should_append_client_id_as_header_if_user_has_client_but_no_subject() + { + var identity = (ClaimsIdentity)httpContext.User.Identity!; - Assert.Equal("Auth-State,Auth-ClientId", httpContext.Response.Headers[HeaderNames.Vary]); - } + identity.AddClaim(new Claim(OpenIdClaims.ClientId, "my-client")); - [Fact] - public async Task Should_not_append_null_header_as_vary() - { - await MakeRequestAsync(() => - { - cachingManager.AddHeader(null!); - }); + await MakeRequestAsync(); - Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); - } + Assert.Equal("Auth-State,Auth-ClientId", httpContext.Response.Headers[HeaderNames.Vary]); + } - [Fact] - public async Task Should_not_append_empty_header_as_vary() + [Fact] + public async Task Should_not_append_null_header_as_vary() + { + await MakeRequestAsync(() => { - await MakeRequestAsync(() => - { - cachingManager.AddHeader(string.Empty); - }); + cachingManager.AddHeader(null!); + }); - Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); - } + Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); + } - [Fact] - public async Task Should_append_custom_header_as_vary() + [Fact] + public async Task Should_not_append_empty_header_as_vary() + { + await MakeRequestAsync(() => { - await MakeRequestAsync(() => - { - cachingManager.AddHeader("X-Header"); - }); + cachingManager.AddHeader(string.Empty); + }); - Assert.Equal("Auth-State,X-Header", httpContext.Response.Headers[HeaderNames.Vary]); - } + Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); + } - [Fact] - public async Task Should_not_append_etag_if_empty() + [Fact] + public async Task Should_append_custom_header_as_vary() + { + await MakeRequestAsync(() => { - await MakeRequestAsync(() => - { - httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; - }); + cachingManager.AddHeader("X-Header"); + }); - Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); - } + Assert.Equal("Auth-State,X-Header", httpContext.Response.Headers[HeaderNames.Vary]); + } - [Fact] - public async Task Should_not_convert_strong_etag_if_disabled() + [Fact] + public async Task Should_not_append_etag_if_empty() + { + await MakeRequestAsync(() => { - cachingOptions.StrongETag = true; + httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; + }); - await MakeRequestAsync(() => - { - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - }); + Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + } - Assert.Equal("13", httpContext.Response.Headers[HeaderNames.ETag]); - } + [Fact] + public async Task Should_not_convert_strong_etag_if_disabled() + { + cachingOptions.StrongETag = true; - [Fact] - public async Task Should_not_convert_already_weak_tag() + await MakeRequestAsync(() => { - await MakeRequestAsync(() => - { - httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; - }); + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + }); - Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); - } + Assert.Equal("13", httpContext.Response.Headers[HeaderNames.ETag]); + } - [Fact] - public async Task Should_convert_strong_to_weak_tag() + [Fact] + public async Task Should_not_convert_already_weak_tag() + { + await MakeRequestAsync(() => { - await MakeRequestAsync(() => - { - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - }); + httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; + }); - Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); - } + Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); + } - [Fact] - public async Task Should_not_convert_empty_string_to_weak_tag() + [Fact] + public async Task Should_convert_strong_to_weak_tag() + { + await MakeRequestAsync(() => { - httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + }); - await MakeRequestAsync(); + Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); + } - Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); - } + [Fact] + public async Task Should_not_convert_empty_string_to_weak_tag() + { + httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; - [Fact] - public async Task Should_append_surrogate_and_ecape_if_necessary() - { - var id = DomainId.Create("id@domain"); + await MakeRequestAsync(); - cachingOptions.MaxSurrogateKeysSize = 100; + Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + } - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id, 12); - }); + [Fact] + public async Task Should_append_surrogate_and_ecape_if_necessary() + { + var id = DomainId.Create("id@domain"); - Assert.Equal("id%40domain", httpContext.Response.Headers["Surrogate-Key"]); - } + cachingOptions.MaxSurrogateKeysSize = 100; - [Fact] - public async Task Should_append_surrogate_keys() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + cachingManager.AddDependency(id, 12); + }); - cachingOptions.MaxSurrogateKeysSize = 100; + Assert.Equal("id%40domain", httpContext.Response.Headers["Surrogate-Key"]); + } - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - }); + [Fact] + public async Task Should_append_surrogate_keys() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - Assert.Equal($"{id1} {id2}", httpContext.Response.Headers["Surrogate-Key"]); - } + cachingOptions.MaxSurrogateKeysSize = 100; - [Fact] - public async Task Should_append_surrogate_keys_if_just_enough_space_for_one() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + }); - cachingOptions.MaxSurrogateKeysSize = 36; + Assert.Equal($"{id1} {id2}", httpContext.Response.Headers["Surrogate-Key"]); + } - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - }); + [Fact] + public async Task Should_append_surrogate_keys_if_just_enough_space_for_one() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - Assert.Equal($"{id1}", httpContext.Response.Headers["Surrogate-Key"]); - } + cachingOptions.MaxSurrogateKeysSize = 36; - [Fact] - public async Task Should_not_append_surrogate_keys_if_maximum_is_exceeded() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + }); - cachingOptions.MaxSurrogateKeysSize = 20; + Assert.Equal($"{id1}", httpContext.Response.Headers["Surrogate-Key"]); + } - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - }); + [Fact] + public async Task Should_not_append_surrogate_keys_if_maximum_is_exceeded() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); - } + cachingOptions.MaxSurrogateKeysSize = 20; - [Fact] - public async Task Should_not_append_surrogate_keys_if_maximum_is_overriden() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + }); - httpContext.Request.Headers[CachingManager.SurrogateKeySizeHeader] = "20"; + Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); + } - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - }); + [Fact] + public async Task Should_not_append_surrogate_keys_if_maximum_is_overriden() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); - } + httpContext.Request.Headers[CachingManager.SurrogateKeySizeHeader] = "20"; - [Fact] - public async Task Should_add_header_to_etag() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + }); - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - }); - - var etag1 = httpContext.Response.Headers[HeaderNames.ETag].ToString(); + Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); + } - httpContext.Response.Headers.Remove(HeaderNames.ETag); - httpContext.Request.Headers["X-Custom"] = "123"; + [Fact] + public async Task Should_add_header_to_etag() + { + var id1 = DomainId.NewGuid(); - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddHeader("X-Custom"); - }); + await MakeRequestAsync(() => + { + cachingManager.AddDependency(id1, 12); + }); - var etag2 = httpContext.Response.Headers[HeaderNames.ETag].ToString(); + var etag1 = httpContext.Response.Headers[HeaderNames.ETag].ToString(); - Assert.NotEqual(etag1, etag2); - } + httpContext.Response.Headers.Remove(HeaderNames.ETag); + httpContext.Request.Headers["X-Custom"] = "123"; - [Fact] - public async Task Should_not_add_header_to_etag_if_not_found_in_request() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); + cachingManager.AddDependency(id1, 12); + cachingManager.AddHeader("X-Custom"); + }); - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - }); + var etag2 = httpContext.Response.Headers[HeaderNames.ETag].ToString(); - var etag1 = httpContext.Response.Headers[HeaderNames.ETag].ToString(); + Assert.NotEqual(etag1, etag2); + } - httpContext.Response.Headers.Remove(HeaderNames.ETag); - httpContext.Request.Headers["X-Other"] = "123"; + [Fact] + public async Task Should_not_add_header_to_etag_if_not_found_in_request() + { + var id1 = DomainId.NewGuid(); - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddHeader("X-Custom"); - }); + await MakeRequestAsync(() => + { + cachingManager.AddDependency(id1, 12); + }); - var etag2 = httpContext.Response.Headers[HeaderNames.ETag].ToString(); + var etag1 = httpContext.Response.Headers[HeaderNames.ETag].ToString(); - Assert.Equal(etag1, etag2); - } + httpContext.Response.Headers.Remove(HeaderNames.ETag); + httpContext.Request.Headers["X-Other"] = "123"; - [Fact] - public async Task Should_generate_etag_from_ids_and_versions() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + cachingManager.AddDependency(id1, 12); + cachingManager.AddHeader("X-Custom"); + }); - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - cachingManager.AddDependency(12); - }); + var etag2 = httpContext.Response.Headers[HeaderNames.ETag].ToString(); - var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString(); + Assert.Equal(etag1, etag2); + } - Assert.True(ETagUtils.IsWeakEtag(etag)); - Assert.True(etag.Length > 20); - } + [Fact] + public async Task Should_generate_etag_from_ids_and_versions() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - [Fact] - public async Task Should_generate_strong_etag_from_ids_and_versions() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + cachingManager.AddDependency(12); + }); - cachingOptions.StrongETag = true; + var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString(); - await MakeRequestAsync(() => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - cachingManager.AddDependency(12); - }); + Assert.True(ETagUtils.IsWeakEtag(etag)); + Assert.True(etag.Length > 20); + } - var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString(); + [Fact] + public async Task Should_generate_strong_etag_from_ids_and_versions() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - Assert.True(ETagUtils.IsStrongEtag(etag)); - Assert.True(etag.Length > 20); - } + cachingOptions.StrongETag = true; - [Fact] - public async Task Should_not_generate_etag_if_already_added() + await MakeRequestAsync(() => { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + cachingManager.AddDependency(12); + }); - await MakeRequestAsync(() => - { - cachingManager.AddDependency(DomainId.NewGuid(), 12); - cachingManager.AddDependency(DomainId.NewGuid(), 12); - cachingManager.AddDependency(12); + var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString(); - httpContext.Response.Headers[HeaderNames.ETag] = "W/20"; - }); + Assert.True(ETagUtils.IsStrongEtag(etag)); + Assert.True(etag.Length > 20); + } - Assert.Equal("W/20", httpContext.Response.Headers[HeaderNames.ETag]); - } + [Fact] + public async Task Should_not_generate_etag_if_already_added() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); - private async Task MakeRequestAsync(Action? action = null) + await MakeRequestAsync(() => { - cachingManager.Reset(httpContext); + cachingManager.AddDependency(DomainId.NewGuid(), 12); + cachingManager.AddDependency(DomainId.NewGuid(), 12); + cachingManager.AddDependency(12); + + httpContext.Response.Headers[HeaderNames.ETag] = "W/20"; + }); + + Assert.Equal("W/20", httpContext.Response.Headers[HeaderNames.ETag]); + } + + private async Task MakeRequestAsync(Action? action = null) + { + cachingManager.Reset(httpContext); - await sut.InvokeAsync(httpContext); + await sut.InvokeAsync(httpContext); - action?.Invoke(); + action?.Invoke(); - await httpContext.Response.StartAsync(httpContext.RequestAborted); - } + await httpContext.Response.StartAsync(httpContext.RequestAborted); } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs index d0dcb3c185..d2365fc095 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs @@ -13,167 +13,166 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class RequestExceptionMiddlewareTests { - public class RequestExceptionMiddlewareTests + private readonly ILogger<RequestExceptionMiddleware> log = A.Fake<ILogger<RequestExceptionMiddleware>>(); + private readonly IActionResultExecutor<ObjectResult> actualWriter = A.Fake<IActionResultExecutor<ObjectResult>>(); + private readonly IHttpResponseFeature responseFeature = A.Fake<IHttpResponseFeature>(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly RequestDelegate next; + private bool isNextCalled; + + public RequestExceptionMiddlewareTests() { - private readonly ILogger<RequestExceptionMiddleware> log = A.Fake<ILogger<RequestExceptionMiddleware>>(); - private readonly IActionResultExecutor<ObjectResult> actualWriter = A.Fake<IActionResultExecutor<ObjectResult>>(); - private readonly IHttpResponseFeature responseFeature = A.Fake<IHttpResponseFeature>(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly RequestDelegate next; - private bool isNextCalled; - - public RequestExceptionMiddlewareTests() + next = context => { - next = context => - { - isNextCalled = true; + isNextCalled = true; - return Task.CompletedTask; - }; + return Task.CompletedTask; + }; - httpContext.Features.Set(responseFeature); - } + httpContext.Features.Set(responseFeature); + } - [Fact] - public async Task Should_write_test_error_if_valid_status_code() - { - httpContext.Request.QueryString = new QueryString("?error=412"); + [Fact] + public async Task Should_write_test_error_if_valid_status_code() + { + httpContext.Request.QueryString = new QueryString("?error=412"); - var sut = new RequestExceptionMiddleware(next); + var sut = new RequestExceptionMiddleware(next); - await sut.InvokeAsync(httpContext, actualWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - Assert.False(isNextCalled); + Assert.False(isNextCalled); - A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, - A<ObjectResult>.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) - .MustHaveHappened(); - } + A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, + A<ObjectResult>.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_test_error_if_invalid_status_code() - { - httpContext.Request.QueryString = new QueryString("?error=hello"); + [Fact] + public async Task Should_not_test_error_if_invalid_status_code() + { + httpContext.Request.QueryString = new QueryString("?error=hello"); - var sut = new RequestExceptionMiddleware(next); + var sut = new RequestExceptionMiddleware(next); - await sut.InvokeAsync(httpContext, actualWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_not_test_error_if_invalid_error_status_code() - { - httpContext.Request.QueryString = new QueryString("?error=99"); + [Fact] + public async Task Should_not_test_error_if_invalid_error_status_code() + { + httpContext.Request.QueryString = new QueryString("?error=99"); - var sut = new RequestExceptionMiddleware(next); + var sut = new RequestExceptionMiddleware(next); - await sut.InvokeAsync(httpContext, actualWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_handle_exception() + [Fact] + public async Task Should_handle_exception() + { + var failingNext = new RequestDelegate(context => { - var failingNext = new RequestDelegate(context => - { - throw new InvalidOperationException(); - }); + throw new InvalidOperationException(); + }); - var sut = new RequestExceptionMiddleware(failingNext); + var sut = new RequestExceptionMiddleware(failingNext); - await sut.InvokeAsync(httpContext, actualWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, - A<ObjectResult>.That.Matches(x => x.StatusCode == 500 && x.Value is ErrorDto))) - .MustHaveHappened(); - } + A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, + A<ObjectResult>.That.Matches(x => x.StatusCode == 500 && x.Value is ErrorDto))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_log_exception() + { + var ex = new InvalidOperationException(); - [Fact] - public async Task Should_log_exception() + var failingNext = new RequestDelegate(context => { - var ex = new InvalidOperationException(); + throw ex; + }); - var failingNext = new RequestDelegate(context => - { - throw ex; - }); + var sut = new RequestExceptionMiddleware(failingNext); - var sut = new RequestExceptionMiddleware(failingNext); + await sut.InvokeAsync(httpContext, actualWriter, log); - await sut.InvokeAsync(httpContext, actualWriter, log); + A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Error) + .MustHaveHappened(); + } - A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument<LogLevel>(0) == LogLevel.Error) - .MustHaveHappened(); - } + [Fact] + public async Task Should_not_handle_exception_if_response_body_written() + { + A.CallTo(() => responseFeature.HasStarted) + .Returns(true); - [Fact] - public async Task Should_not_handle_exception_if_response_body_written() + var failingNext = new RequestDelegate(context => { - A.CallTo(() => responseFeature.HasStarted) - .Returns(true); - - var failingNext = new RequestDelegate(context => - { - throw new InvalidOperationException(); - }); + throw new InvalidOperationException(); + }); - var sut = new RequestExceptionMiddleware(failingNext); + var sut = new RequestExceptionMiddleware(failingNext); - await sut.InvokeAsync(httpContext, actualWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_handle_error_status_code() + [Fact] + public async Task Should_handle_error_status_code() + { + var failingNext = new RequestDelegate(context => { - var failingNext = new RequestDelegate(context => - { - context.Response.StatusCode = 412; + context.Response.StatusCode = 412; + + return Task.CompletedTask; + }); - return Task.CompletedTask; - }); + var sut = new RequestExceptionMiddleware(failingNext); - var sut = new RequestExceptionMiddleware(failingNext); + await sut.InvokeAsync(httpContext, actualWriter, log); - await sut.InvokeAsync(httpContext, actualWriter, log); + A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, + A<ObjectResult>.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) + .MustHaveHappened(); + } - A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, - A<ObjectResult>.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) - .MustHaveHappened(); - } + [Fact] + public async Task Should_not_handle_error_status_code_if_response_body_written() + { + A.CallTo(() => responseFeature.HasStarted) + .Returns(true); - [Fact] - public async Task Should_not_handle_error_status_code_if_response_body_written() + var failingNext = new RequestDelegate(context => { - A.CallTo(() => responseFeature.HasStarted) - .Returns(true); - - var failingNext = new RequestDelegate(context => - { - context.Response.StatusCode = 412; + context.Response.StatusCode = 412; - return Task.CompletedTask; - }); + return Task.CompletedTask; + }); - var sut = new RequestExceptionMiddleware(failingNext); + var sut = new RequestExceptionMiddleware(failingNext); - await sut.InvokeAsync(httpContext, actualWriter, log); + await sut.InvokeAsync(httpContext, actualWriter, log); - A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._)) - .MustNotHaveHappened(); - } + A.CallTo(() => actualWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._)) + .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs index 14f4bfbcce..bbe0abd127 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs @@ -23,219 +23,218 @@ #pragma warning disable IDE0017 // Simplify object initialization -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class SchemaResolverTests { - public class SchemaResolverTests + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext; + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly ClaimsIdentity user = new ClaimsIdentity(); + private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly SchemaResolver sut; + private bool isNextCalled; + + public SchemaResolverTests() { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionContext actionContext; - private readonly ActionExecutingContext actionExecutingContext; - private readonly ActionExecutionDelegate next; - private readonly ClaimsIdentity user = new ClaimsIdentity(); - private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly SchemaResolver sut; - private bool isNextCalled; - - public SchemaResolverTests() + actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor { - actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - EndpointMetadata = new List<object>() - }); + EndpointMetadata = new List<object>() + }); - actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?>(), this); - actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); - actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); + actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?>(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); + actionExecutingContext.HttpContext.Features.Set<IAppFeature>(new AppFeature(Mocks.App(appId))); - next = () => - { - isNextCalled = true; + next = () => + { + isNextCalled = true; - return Task.FromResult<ActionExecutedContext>(null!); - }; + return Task.FromResult<ActionExecutedContext>(null!); + }; - sut = new SchemaResolver(appProvider); - } + sut = new SchemaResolver(appProvider); + } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task Should_return_404_if_schema_name_is_null(string? schema) - { - actionContext.RouteData.Values["schema"] = schema; + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Should_return_404_if_schema_name_is_null(string? schema) + { + actionContext.RouteData.Values["schema"] = schema; - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - AssertNotFound(); + AssertNotFound(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, true, httpContext.RequestAborted)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, true, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_404_if_schema_not_published_when_attribute_applied() - { - actionContext.ActionDescriptor.EndpointMetadata.Add(new SchemaMustBePublishedAttribute()); - actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); + [Fact] + public async Task Should_return_404_if_schema_not_published_when_attribute_applied() + { + actionContext.ActionDescriptor.EndpointMetadata.Add(new SchemaMustBePublishedAttribute()); + actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); - var schema = CreateSchema(false); + var schema = CreateSchema(false); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, true, httpContext.RequestAborted)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, true, httpContext.RequestAborted)) + .Returns(schema); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - AssertNotFound(); - } + AssertNotFound(); + } - [Fact] - public async Task Should_resolve_schema_if_schema_not_published() - { - actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); + [Fact] + public async Task Should_resolve_schema_if_schema_not_published() + { + actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); - var schema = CreateSchema(false); + var schema = CreateSchema(false); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, true, httpContext.RequestAborted)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, true, httpContext.RequestAborted)) + .Returns(schema); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - AssertSchema(schema); - } + AssertSchema(schema); + } - [Fact] - public async Task Should_return_404_if_schema_not_found() - { - actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); + [Fact] + public async Task Should_return_404_if_schema_not_found() + { + actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, true, httpContext.RequestAborted)) - .Returns(Task.FromResult<ISchemaEntity?>(null)); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A<DomainId>._, true, httpContext.RequestAborted)) + .Returns(Task.FromResult<ISchemaEntity?>(null)); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - AssertNotFound(); - } + AssertNotFound(); + } - [Fact] - public async Task Should_resolve_schema_from_id() - { - actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); + [Fact] + public async Task Should_resolve_schema_from_id() + { + actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); - var schema = CreateSchema(true); + var schema = CreateSchema(true); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true, httpContext.RequestAborted)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true, httpContext.RequestAborted)) + .Returns(schema); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - AssertSchema(schema); - } + AssertSchema(schema); + } - [Fact] - public async Task Should_resolve_schema_from_id_without_caching_if_frontend() - { - user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); + [Fact] + public async Task Should_resolve_schema_from_id_without_caching_if_frontend() + { + user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); - actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); + actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); - var schema = CreateSchema(true); + var schema = CreateSchema(true); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, httpContext.RequestAborted)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, httpContext.RequestAborted)) + .Returns(schema); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - AssertSchema(schema); - } + AssertSchema(schema); + } - [Fact] - public async Task Should_resolve_schema_from_name() - { - actionContext.RouteData.Values["schema"] = schemaId.Name; + [Fact] + public async Task Should_resolve_schema_from_name() + { + actionContext.RouteData.Values["schema"] = schemaId.Name; - var schema = CreateSchema(true); + var schema = CreateSchema(true); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true, httpContext.RequestAborted)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true, httpContext.RequestAborted)) + .Returns(schema); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - AssertSchema(schema); - } + AssertSchema(schema); + } - [Fact] - public async Task Should_resolve_schema_from_name_without_caching_if_frontend() - { - user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); + [Fact] + public async Task Should_resolve_schema_from_name_without_caching_if_frontend() + { + user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); - actionContext.RouteData.Values["schema"] = schemaId.Name; + actionContext.RouteData.Values["schema"] = schemaId.Name; - var schema = CreateSchema(true); + var schema = CreateSchema(true); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, false, httpContext.RequestAborted)) - .Returns(schema); + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, false, httpContext.RequestAborted)) + .Returns(schema); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - AssertSchema(schema); - } + AssertSchema(schema); + } - [Fact] - public async Task Should_do_nothing_if_app_feature_not_set() - { - actionExecutingContext.HttpContext.Features.Set<IAppFeature>(null!); - actionExecutingContext.RouteData.Values["schema"] = schemaId.Name; + [Fact] + public async Task Should_do_nothing_if_app_feature_not_set() + { + actionExecutingContext.HttpContext.Features.Set<IAppFeature>(null!); + actionExecutingContext.RouteData.Values["schema"] = schemaId.Name; - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - A.CallTo(() => appProvider.GetAppAsync(A<string>._, false, httpContext.RequestAborted)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetAppAsync(A<string>._, false, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_do_nothing_if_parameter_not_set() - { - await sut.OnActionExecutionAsync(actionExecutingContext, next); + [Fact] + public async Task Should_do_nothing_if_parameter_not_set() + { + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - A.CallTo(() => appProvider.GetAppAsync(A<string>._, false, httpContext.RequestAborted)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetAppAsync(A<string>._, false, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } - private void AssertNotFound() - { - Assert.IsType<NotFoundResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); - } + private void AssertNotFound() + { + Assert.IsType<NotFoundResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); + } - private void AssertSchema(ISchemaEntity schema) - { - Assert.Equal(schema, actionContext.HttpContext.Features.Get<ISchemaFeature>()!.Schema); - Assert.True(isNextCalled); - } + private void AssertSchema(ISchemaEntity schema) + { + Assert.Equal(schema, actionContext.HttpContext.Features.Get<ISchemaFeature>()!.Schema); + Assert.True(isNextCalled); + } - private ISchemaEntity CreateSchema(bool published) - { - var schema = new Schema(schemaId.Name); + private ISchemaEntity CreateSchema(bool published) + { + var schema = new Schema(schemaId.Name); - if (published) - { - schema = schema.Publish(); - } + if (published) + { + schema = schema.Publish(); + } - var schemaEntity = A.Fake<ISchemaEntity>(); + var schemaEntity = A.Fake<ISchemaEntity>(); - A.CallTo(() => schemaEntity.Id).Returns(schemaId.Id); - A.CallTo(() => schemaEntity.SchemaDef).Returns(schema); + A.CallTo(() => schemaEntity.Id).Returns(schemaId.Id); + A.CallTo(() => schemaEntity.SchemaDef).Returns(schema); - return schemaEntity; - } + return schemaEntity; } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/TeamResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/TeamResolverTests.cs index 636b582081..101b2b9286 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/TeamResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/TeamResolverTests.cs @@ -24,215 +24,214 @@ #pragma warning disable IDE0017 // Simplify object initialization -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class TeamResolverTests { - public class TeamResolverTests + private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext; + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly DomainId teamId = DomainId.NewGuid(); + private readonly TeamResolver sut; + private bool isNextCalled; + + public TeamResolverTests() { - private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly ActionContext actionContext; - private readonly ActionExecutingContext actionExecutingContext; - private readonly ActionExecutionDelegate next; - private readonly DomainId teamId = DomainId.NewGuid(); - private readonly TeamResolver sut; - private bool isNextCalled; - - public TeamResolverTests() + actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor { - actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor - { - EndpointMetadata = new List<object>() - }); + EndpointMetadata = new List<object>() + }); - actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?>(), this); - actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.RouteData.Values["team"] = teamId.ToString(); + actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?>(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.RouteData.Values["team"] = teamId.ToString(); - next = () => - { - isNextCalled = true; + next = () => + { + isNextCalled = true; - return Task.FromResult<ActionExecutedContext>(null!); - }; + return Task.FromResult<ActionExecutedContext>(null!); + }; - sut = new TeamResolver(appProvider); - } + sut = new TeamResolver(appProvider); + } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task Should_return_404_if_team_name_is_null(string? team) - { - SetupUser(); + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Should_return_404_if_team_name_is_null(string? team) + { + SetupUser(); - actionExecutingContext.RouteData.Values["team"] = team; + actionExecutingContext.RouteData.Values["team"] = team; - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<NotFoundResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); + Assert.IsType<NotFoundResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); - A.CallTo(() => appProvider.GetTeamAsync(A<DomainId>._, httpContext.RequestAborted)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetTeamAsync(A<DomainId>._, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_return_404_if_team_not_found() - { - SetupUser(); + [Fact] + public async Task Should_return_404_if_team_not_found() + { + SetupUser(); - A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) - .Returns(Task.FromResult<ITeamEntity?>(null)); + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(Task.FromResult<ITeamEntity?>(null)); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<NotFoundResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); - } + Assert.IsType<NotFoundResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_return_401_if_user_is_anonymous() - { - SetupUser(null); + [Fact] + public async Task Should_return_401_if_user_is_anonymous() + { + SetupUser(null); - var team = CreateTeam(teamId); + var team = CreateTeam(teamId); - A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<UnauthorizedResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); - } + Assert.IsType<UnauthorizedResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_resolve_team_from_user() - { - var user = SetupUser(); + [Fact] + public async Task Should_resolve_team_from_user() + { + var user = SetupUser(); - var team = CreateTeam(teamId); + var team = CreateTeam(teamId); - user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, $"squidex.teams.{teamId}")); + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, $"squidex.teams.{teamId}")); - A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); + var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); - Assert.Same(team, httpContext.Features.Get<ITeamFeature>()!.Team); - Assert.True(user.Claims.Any()); - Assert.True(permissions.Count < 3); - Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.teams.{teamId}", StringComparison.OrdinalIgnoreCase))); - Assert.True(isNextCalled); - } + Assert.Same(team, httpContext.Features.Get<ITeamFeature>()!.Team); + Assert.True(user.Claims.Any()); + Assert.True(permissions.Count < 3); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.teams.{teamId}", StringComparison.OrdinalIgnoreCase))); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_resolve_team_from_contributor() - { - var user = SetupUser(); + [Fact] + public async Task Should_resolve_team_from_contributor() + { + var user = SetupUser(); - var team = CreateTeam(teamId, user: "user1"); + var team = CreateTeam(teamId, user: "user1"); - user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); + var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList(); - Assert.Same(team, httpContext.Features.Get<ITeamFeature>()!.Team); - Assert.True(user.Claims.Count() > 2); - Assert.True(permissions.Count < 3); - Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.teams.{teamId}", StringComparison.OrdinalIgnoreCase))); - Assert.True(isNextCalled); - } + Assert.Same(team, httpContext.Features.Get<ITeamFeature>()!.Team); + Assert.True(user.Claims.Count() > 2); + Assert.True(permissions.Count < 3); + Assert.True(permissions.All(x => x.Value.StartsWith($"squidex.teams.{teamId}", StringComparison.OrdinalIgnoreCase))); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_resolve_team_if_action_allows_anonymous_but_user_has_no_permissions() - { - var user = SetupUser(); + [Fact] + public async Task Should_resolve_team_if_action_allows_anonymous_but_user_has_no_permissions() + { + var user = SetupUser(); - user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{teamId}:client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.teams.other-team")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{teamId}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.teams.other-team")); - var team = CreateTeam(teamId); + var team = CreateTeam(teamId); - actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); + actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); - A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.Same(team, httpContext.Features.Get<ITeamFeature>()!.Team); - Assert.Equal(2, user.Claims.Count()); - Assert.True(isNextCalled); - } + Assert.Same(team, httpContext.Features.Get<ITeamFeature>()!.Team); + Assert.Equal(2, user.Claims.Count()); + Assert.True(isNextCalled); + } - [Fact] - public async Task Should_return_404_if_user_has_no_permissions() - { - var user = SetupUser(); + [Fact] + public async Task Should_return_404_if_user_has_no_permissions() + { + var user = SetupUser(); - user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{teamId}:client1")); - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.teams.other-team")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{teamId}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.teams.other-team")); - var team = CreateTeam(teamId); + var team = CreateTeam(teamId); - A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) - .Returns(team); + A.CallTo(() => appProvider.GetTeamAsync(teamId, httpContext.RequestAborted)) + .Returns(team); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.IsType<NotFoundResult>(actionExecutingContext.Result); - Assert.False(isNextCalled); - } + Assert.IsType<NotFoundResult>(actionExecutingContext.Result); + Assert.False(isNextCalled); + } - [Fact] - public async Task Should_do_nothing_if_parameter_not_set() - { - actionExecutingContext.RouteData.Values.Remove("team"); + [Fact] + public async Task Should_do_nothing_if_parameter_not_set() + { + actionExecutingContext.RouteData.Values.Remove("team"); - await sut.OnActionExecutionAsync(actionExecutingContext, next); + await sut.OnActionExecutionAsync(actionExecutingContext, next); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - A.CallTo(() => appProvider.GetTeamAsync(A<DomainId>._, httpContext.RequestAborted)) - .MustNotHaveHappened(); - } + A.CallTo(() => appProvider.GetTeamAsync(A<DomainId>._, httpContext.RequestAborted)) + .MustNotHaveHappened(); + } - private ClaimsIdentity SetupUser(string? type = "OIDC") - { - var userIdentity = new ClaimsIdentity(type); - var userPrincipal = new ClaimsPrincipal(userIdentity); + private ClaimsIdentity SetupUser(string? type = "OIDC") + { + var userIdentity = new ClaimsIdentity(type); + var userPrincipal = new ClaimsPrincipal(userIdentity); - actionExecutingContext.HttpContext.User = userPrincipal; + actionExecutingContext.HttpContext.User = userPrincipal; - return userIdentity; - } + return userIdentity; + } - private static ITeamEntity CreateTeam(DomainId id, string? user = null) - { - var team = A.Fake<ITeamEntity>(); + private static ITeamEntity CreateTeam(DomainId id, string? user = null) + { + var team = A.Fake<ITeamEntity>(); - var contributors = Contributors.Empty; + var contributors = Contributors.Empty; - if (user != null) - { - contributors = contributors.Assign(user, Role.Owner); - } + if (user != null) + { + contributors = contributors.Assign(user, Role.Owner); + } - A.CallTo(() => team.Id).Returns(id); - A.CallTo(() => team.Contributors).Returns(contributors); + A.CallTo(() => team.Id).Returns(id); + A.CallTo(() => team.Contributors).Returns(contributors); - return team; - } + return team; } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs index bafeaf0e48..55d287464c 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs @@ -15,226 +15,225 @@ using Squidex.Infrastructure; using Xunit; -namespace Squidex.Web.Pipeline +namespace Squidex.Web.Pipeline; + +public class UsageMiddlewareTests { - public class UsageMiddlewareTests + private readonly IAppLogStore usageLog = A.Fake<IAppLogStore>(); + private readonly IUsageGate usageGate = A.Fake<IUsageGate>(); + private readonly IClock clock = A.Fake<IClock>(); + private readonly Instant instant = SystemClock.Instance.GetCurrentInstant(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly RequestDelegate next; + private readonly UsageMiddleware sut; + private bool isNextCalled; + + public UsageMiddlewareTests() { - private readonly IAppLogStore usageLog = A.Fake<IAppLogStore>(); - private readonly IUsageGate usageGate = A.Fake<IUsageGate>(); - private readonly IClock clock = A.Fake<IClock>(); - private readonly Instant instant = SystemClock.Instance.GetCurrentInstant(); - private readonly HttpContext httpContext = new DefaultHttpContext(); - private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly RequestDelegate next; - private readonly UsageMiddleware sut; - private bool isNextCalled; - - public UsageMiddlewareTests() + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(instant); + + next = x => { - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(instant); + isNextCalled = true; - next = x => - { - isNextCalled = true; + return Task.CompletedTask; + }; - return Task.CompletedTask; - }; + sut = new UsageMiddleware(usageLog, usageGate) + { + Clock = clock + }; + } - sut = new UsageMiddleware(usageLog, usageGate) - { - Clock = clock - }; - } + [Fact] + public async Task Should_not_track_if_app_not_defined() + { + await sut.InvokeAsync(httpContext, next); - [Fact] - public async Task Should_not_track_if_app_not_defined() - { - await sut.InvokeAsync(httpContext, next); + Assert.True(isNextCalled); - Assert.True(isNextCalled); + var date = instant.ToDateTimeUtc().Date; - var date = instant.ToDateTimeUtc().Date; + A.CallTo(() => usageGate.TrackRequestAsync(A<IAppEntity>._, A<string>._, A<DateTime>._, A<double>._, A<long>._, A<long>._, default)) + .MustNotHaveHappened(); + } - A.CallTo(() => usageGate.TrackRequestAsync(A<IAppEntity>._, A<string>._, A<DateTime>._, A<double>._, A<long>._, A<long>._, default)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_not_track_if_call_blocked() + { + var app = Mocks.App(appId); - [Fact] - public async Task Should_not_track_if_call_blocked() - { - var app = Mocks.App(appId); + httpContext.Features.Set<IAppFeature>(new AppFeature(app)); + httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); - httpContext.Features.Set<IAppFeature>(new AppFeature(app)); - httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); + httpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; - httpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; + await sut.InvokeAsync(httpContext, next); - await sut.InvokeAsync(httpContext, next); + Assert.True(isNextCalled); - Assert.True(isNextCalled); + var date = instant.ToDateTimeUtc().Date; - var date = instant.ToDateTimeUtc().Date; + A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, A<double>._, A<long>._, A<long>._, default)) + .MustNotHaveHappened(); + } - A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, A<double>._, A<long>._, A<long>._, default)) - .MustNotHaveHappened(); - } + [Fact] + public async Task Should_track_if_calls_left() + { + var app = Mocks.App(appId); - [Fact] - public async Task Should_track_if_calls_left() - { - var app = Mocks.App(appId); + httpContext.Features.Set<IAppFeature>(new AppFeature(app)); + httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); - httpContext.Features.Set<IAppFeature>(new AppFeature(app)); - httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); + await sut.InvokeAsync(httpContext, next); - await sut.InvokeAsync(httpContext, next); + Assert.True(isNextCalled); - Assert.True(isNextCalled); + var date = instant.ToDateTimeUtc().Date; - var date = instant.ToDateTimeUtc().Date; + A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, A<long>._, default)) + .MustHaveHappened(); + } - A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, A<long>._, default)) - .MustHaveHappened(); - } + [Fact] + public async Task Should_track_request_bytes() + { + var app = Mocks.App(appId); - [Fact] - public async Task Should_track_request_bytes() - { - var app = Mocks.App(appId); + httpContext.Features.Set<IAppFeature>(new AppFeature(app)); + httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); + httpContext.Request.ContentLength = 1024; + + await sut.InvokeAsync(httpContext, next); - httpContext.Features.Set<IAppFeature>(new AppFeature(app)); - httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); - httpContext.Request.ContentLength = 1024; + Assert.True(isNextCalled); - await sut.InvokeAsync(httpContext, next); + var date = instant.ToDateTimeUtc().Date; - Assert.True(isNextCalled); + A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, 1024, default)) + .MustHaveHappened(); + } - var date = instant.ToDateTimeUtc().Date; + [Fact] + public async Task Should_track_response_bytes_with_writer() + { + var app = Mocks.App(appId); - A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, 1024, default)) - .MustHaveHappened(); - } + httpContext.Features.Set<IAppFeature>(new AppFeature(app)); + httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); - [Fact] - public async Task Should_track_response_bytes_with_writer() + await sut.InvokeAsync(httpContext, async x => { - var app = Mocks.App(appId); + await x.Response.BodyWriter.WriteAsync(Encoding.Default.GetBytes("Hello World"), httpContext.RequestAborted); - httpContext.Features.Set<IAppFeature>(new AppFeature(app)); - httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); + await next(x); + }); - await sut.InvokeAsync(httpContext, async x => - { - await x.Response.BodyWriter.WriteAsync(Encoding.Default.GetBytes("Hello World"), httpContext.RequestAborted); + Assert.True(isNextCalled); - await next(x); - }); + var date = instant.ToDateTimeUtc().Date; - Assert.True(isNextCalled); + A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, 11, default)) + .MustHaveHappened(); + } - var date = instant.ToDateTimeUtc().Date; + [Fact] + public async Task Should_track_response_bytes_with_stream() + { + var app = Mocks.App(appId); - A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, 11, default)) - .MustHaveHappened(); - } + httpContext.Features.Set<IAppFeature>(new AppFeature(app)); + httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); - [Fact] - public async Task Should_track_response_bytes_with_stream() + await sut.InvokeAsync(httpContext, async x => { - var app = Mocks.App(appId); + await x.Response.Body.WriteAsync(Encoding.Default.GetBytes("Hello World"), httpContext.RequestAborted); - httpContext.Features.Set<IAppFeature>(new AppFeature(app)); - httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); + await next(x); + }); - await sut.InvokeAsync(httpContext, async x => - { - await x.Response.Body.WriteAsync(Encoding.Default.GetBytes("Hello World"), httpContext.RequestAborted); + Assert.True(isNextCalled); - await next(x); - }); + var date = instant.ToDateTimeUtc().Date; - Assert.True(isNextCalled); + A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, 11, default)) + .MustHaveHappened(); + } - var date = instant.ToDateTimeUtc().Date; + [Fact] + public async Task Should_track_response_bytes_with_file() + { + var app = Mocks.App(appId); - A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, 11, default)) - .MustHaveHappened(); - } + httpContext.Features.Set<IAppFeature>(new AppFeature(app)); + httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); - [Fact] - public async Task Should_track_response_bytes_with_file() + var tempFileName = Path.GetTempFileName(); + try { - var app = Mocks.App(appId); - - httpContext.Features.Set<IAppFeature>(new AppFeature(app)); - httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); + await File.WriteAllTextAsync(tempFileName, "Hello World", httpContext.RequestAborted); - var tempFileName = Path.GetTempFileName(); - try + await sut.InvokeAsync(httpContext, async x => { - await File.WriteAllTextAsync(tempFileName, "Hello World", httpContext.RequestAborted); - - await sut.InvokeAsync(httpContext, async x => - { - await x.Response.SendFileAsync(tempFileName, 0, new FileInfo(tempFileName).Length, httpContext.RequestAborted); + await x.Response.SendFileAsync(tempFileName, 0, new FileInfo(tempFileName).Length, httpContext.RequestAborted); - await next(x); - }); - } - finally - { - File.Delete(tempFileName); - } + await next(x); + }); + } + finally + { + File.Delete(tempFileName); + } - Assert.True(isNextCalled); + Assert.True(isNextCalled); - var date = instant.ToDateTimeUtc().Date; + var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, 11, default)) - .MustHaveHappened(); - } + A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, 13, A<long>._, 11, default)) + .MustHaveHappened(); + } - [Fact] - public async Task Should_not_track_if_costs_are_zero() - { - var app = Mocks.App(appId); + [Fact] + public async Task Should_not_track_if_costs_are_zero() + { + var app = Mocks.App(appId); - httpContext.Features.Set<IAppFeature>(new AppFeature(app)); - httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(0)); + httpContext.Features.Set<IAppFeature>(new AppFeature(app)); + httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(0)); - await sut.InvokeAsync(httpContext, next); + await sut.InvokeAsync(httpContext, next); - Assert.True(isNextCalled); + Assert.True(isNextCalled); - var date = instant.ToDateTimeUtc().Date; + var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, A<double>._, A<long>._, A<long>._, default)) - .MustNotHaveHappened(); - } + A.CallTo(() => usageGate.TrackRequestAsync(app, A<string>._, date, A<double>._, A<long>._, A<long>._, default)) + .MustNotHaveHappened(); + } - [Fact] - public async Task Should_log_request_even_if_costs_are_zero() - { - var app = Mocks.App(appId); + [Fact] + public async Task Should_log_request_even_if_costs_are_zero() + { + var app = Mocks.App(appId); - httpContext.Features.Set<IAppFeature>(new AppFeature(app)); - httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(0)); + httpContext.Features.Set<IAppFeature>(new AppFeature(app)); + httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(0)); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/my-path"; + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/my-path"; - await sut.InvokeAsync(httpContext, next); + await sut.InvokeAsync(httpContext, next); - A.CallTo(() => usageLog.LogAsync(appId.Id, - A<RequestLog>.That.Matches(x => - x.Timestamp == instant && - x.RequestMethod == "GET" && - x.RequestPath == "/my-path" && - x.Costs == 0), - default)) - .MustHaveHappened(); - } + A.CallTo(() => usageLog.LogAsync(appId.Id, + A<RequestLog>.That.Matches(x => + x.Timestamp == instant && + x.RequestMethod == "GET" && + x.RequestPath == "/my-path" && + x.Costs == 0), + default)) + .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Web.Tests/Services/StringLocalizerTests.cs b/backend/tests/Squidex.Web.Tests/Services/StringLocalizerTests.cs index 36191151f0..0cf2285f9d 100644 --- a/backend/tests/Squidex.Web.Tests/Services/StringLocalizerTests.cs +++ b/backend/tests/Squidex.Web.Tests/Services/StringLocalizerTests.cs @@ -9,47 +9,46 @@ using Squidex.Shared; using Xunit; -namespace Squidex.Web.Services +namespace Squidex.Web.Services; + +public class StringLocalizerTests { - public class StringLocalizerTests - { - private readonly StringLocalizer sut; + private readonly StringLocalizer sut; - public StringLocalizerTests() - { - var translations = new ResourcesLocalizer(Texts.ResourceManager); + public StringLocalizerTests() + { + var translations = new ResourcesLocalizer(Texts.ResourceManager); - sut = new StringLocalizer(translations); - } + sut = new StringLocalizer(translations); + } - [Fact] - public void Should_provide_translation() - { - var key = "annotations_Required"; + [Fact] + public void Should_provide_translation() + { + var key = "annotations_Required"; - var name = sut[key]; + var name = sut[key]; - Assert.Equal("The field '{0}' is required.", name); - } + Assert.Equal("The field '{0}' is required.", name); + } - [Fact] - public void Should_format_translation() - { - var key = "annotations_Required"; + [Fact] + public void Should_format_translation() + { + var key = "annotations_Required"; - var name = sut[key, "MyField"]; + var name = sut[key, "MyField"]; - Assert.Equal("The field 'MyField' is required.", name); - } + Assert.Equal("The field 'MyField' is required.", name); + } - [Fact] - public void Should_translate_property_name() - { - var key = "annotations_Required"; + [Fact] + public void Should_translate_property_name() + { + var key = "annotations_Required"; - var name = sut[key, "ClientId"]; + var name = sut[key, "ClientId"]; - Assert.Equal("The field 'Client ID' is required.", name); - } + Assert.Equal("The field 'Client ID' is required.", name); } } diff --git a/backend/tests/Squidex.Web.Tests/UrlDecodeRouteParamsAttributeTests.cs b/backend/tests/Squidex.Web.Tests/UrlDecodeRouteParamsAttributeTests.cs index 1212ab3edc..f3c378e7a5 100644 --- a/backend/tests/Squidex.Web.Tests/UrlDecodeRouteParamsAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/UrlDecodeRouteParamsAttributeTests.cs @@ -12,28 +12,27 @@ using Microsoft.AspNetCore.Routing; using Xunit; -namespace Squidex.Web +namespace Squidex.Web; + +public class UrlDecodeRouteParamsAttributeTests { - public class UrlDecodeRouteParamsAttributeTests + [Fact] + public void Should_url_decode_params() { - [Fact] - public void Should_url_decode_params() - { - var sut = new UrlDecodeRouteParamsAttribute(); + var sut = new UrlDecodeRouteParamsAttribute(); - var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor - { - FilterDescriptors = new List<FilterDescriptor>() - }); + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor + { + FilterDescriptors = new List<FilterDescriptor>() + }); - var actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?> - { - ["key"] = "path%2Fto%2Fsomething" - }, null!); + var actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object?> + { + ["key"] = "path%2Fto%2Fsomething" + }, null!); - sut.OnActionExecuting(actionExecutingContext); + sut.OnActionExecuting(actionExecutingContext); - Assert.Equal("path/to/something", actionExecutingContext.ActionArguments["key"]); - } + Assert.Equal("path/to/something", actionExecutingContext.ActionArguments["key"]); } } diff --git a/backend/tools/GenerateLanguages/Program.cs b/backend/tools/GenerateLanguages/Program.cs index 1d24571a7d..502893d956 100644 --- a/backend/tools/GenerateLanguages/Program.cs +++ b/backend/tools/GenerateLanguages/Program.cs @@ -9,122 +9,121 @@ using System.Text; using System.Text.Json; -namespace GenerateLanguages +namespace GenerateLanguages; + +public static class Program { - public static class Program + public static void Main() { - public static void Main() + var languageCodesFile = new FileInfo("../../../../../src/Squidex.Infrastructure/Languages.cs"); + + var languages = ReadLanguages(); + + var writer = new SourceBuilder(); + writer.WriteLine("// =========================================================================="); + writer.WriteLine("// Languages.cs"); + writer.WriteLine("// Squidex Headless CMS"); + writer.WriteLine("// =========================================================================="); + writer.WriteLine("// Copyright (c) Squidex UG (haftungsbeschränkt)"); + writer.WriteLine("// All rights reserved. Licensed under the MIT license."); + writer.WriteLine("// =========================================================================="); + writer.WriteLine("// <autogenerated/>"); + writer.WriteLine(); + writer.WriteLine("using System.CodeDom.Compiler;"); + writer.WriteLine(); + writer.WriteLine("namespace Squidex.Infrastructure"); + writer.WriteLine("{"); + writer.WriteLine("[GeneratedCode(\"LanguagesGenerator\", \"1.0\")]"); + writer.WriteLine("public partial record Language"); + writer.WriteLine("{"); + writer.WriteLine("private static readonly Dictionary<string, Language> LanguageByCode = new Dictionary<string, Language>(StringComparer.OrdinalIgnoreCase);"); + writer.WriteLine("private static readonly Dictionary<string, string> NamesEnglish = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);"); + writer.WriteLine("private static readonly Dictionary<string, string> NamesNative = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);"); + writer.WriteLine(); + writer.WriteLine("internal static Language AddLanguage(string iso2Code, string englishName, string nativeName)"); + writer.WriteLine("{"); + writer.WriteLine("NamesEnglish[iso2Code] = englishName;"); + writer.WriteLine("NamesNative[iso2Code] = nativeName;"); + writer.WriteLine(); + writer.WriteLine("return LanguageByCode.GetOrAdd(iso2Code, code => new Language(code));"); + writer.WriteLine("}"); + writer.WriteLine(); + + foreach (var language in languages) { - var languageCodesFile = new FileInfo("../../../../../src/Squidex.Infrastructure/Languages.cs"); - - var languages = ReadLanguages(); - - var writer = new SourceBuilder(); - writer.WriteLine("// =========================================================================="); - writer.WriteLine("// Languages.cs"); - writer.WriteLine("// Squidex Headless CMS"); - writer.WriteLine("// =========================================================================="); - writer.WriteLine("// Copyright (c) Squidex UG (haftungsbeschränkt)"); - writer.WriteLine("// All rights reserved. Licensed under the MIT license."); - writer.WriteLine("// =========================================================================="); - writer.WriteLine("// <autogenerated/>"); - writer.WriteLine(); - writer.WriteLine("using System.CodeDom.Compiler;"); - writer.WriteLine(); - writer.WriteLine("namespace Squidex.Infrastructure"); - writer.WriteLine("{"); - writer.WriteLine("[GeneratedCode(\"LanguagesGenerator\", \"1.0\")]"); - writer.WriteLine("public partial record Language"); - writer.WriteLine("{"); - writer.WriteLine("private static readonly Dictionary<string, Language> LanguageByCode = new Dictionary<string, Language>(StringComparer.OrdinalIgnoreCase);"); - writer.WriteLine("private static readonly Dictionary<string, string> NamesEnglish = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);"); - writer.WriteLine("private static readonly Dictionary<string, string> NamesNative = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);"); - writer.WriteLine(); - writer.WriteLine("internal static Language AddLanguage(string iso2Code, string englishName, string nativeName)"); - writer.WriteLine("{"); - writer.WriteLine("NamesEnglish[iso2Code] = englishName;"); - writer.WriteLine("NamesNative[iso2Code] = nativeName;"); - writer.WriteLine(); - writer.WriteLine("return LanguageByCode.GetOrAdd(iso2Code, code => new Language(code));"); - writer.WriteLine("}"); - writer.WriteLine(); - - foreach (var language in languages) - { - writer.WriteLine($"public static readonly Language {language.FieldName} = AddLanguage(\"{language.Code}\", \"{language.NameEnglish}\", \"{language.NameNative}\");"); - } + writer.WriteLine($"public static readonly Language {language.FieldName} = AddLanguage(\"{language.Code}\", \"{language.NameEnglish}\", \"{language.NameNative}\");"); + } - writer.WriteLine("}"); - writer.WriteLine("}"); + writer.WriteLine("}"); + writer.WriteLine("}"); - File.WriteAllText(languageCodesFile.FullName, writer.ToString()); - } + File.WriteAllText(languageCodesFile.FullName, writer.ToString()); + } - private static List<Language> ReadLanguages() - { - var languages = new List<Language>(); + private static List<Language> ReadLanguages() + { + var languages = new List<Language>(); - var json = File.ReadAllText("languages.json"); + var json = File.ReadAllText("languages.json"); - var deserialized = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(json); + var deserialized = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(json); - static string ToFieldName(string name) - { - var sb = new StringBuilder(); + static string ToFieldName(string name) + { + var sb = new StringBuilder(); - foreach (var c in name) + foreach (var c in name) + { + if (char.IsLetterOrDigit(c)) { - if (char.IsLetterOrDigit(c)) - { - sb.Append(c); - } + sb.Append(c); } - - return sb.ToString(); } - foreach (var (code, value) in deserialized) + return sb.ToString(); + } + + foreach (var (code, value) in deserialized) + { + var name = value["name"]; + + languages.Add(new Language { - var name = value["name"]; + Code = code, + NameNative = value["nativeName"], + NameEnglish = value["name"], + FieldName = code.ToUpperInvariant() + }); + } + + foreach (var culture in CultureInfo.GetCultures(CultureTypes.SpecificCultures)) + { + var code = culture.ToString(); + if (code.Length == 5 && languages.Any(l => l.Code.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))) + { languages.Add(new Language { Code = code, - NameNative = value["nativeName"], - NameEnglish = value["name"], - FieldName = code.ToUpperInvariant() + NameNative = culture.NativeName, + NameEnglish = culture.EnglishName, + FieldName = ToFieldName(culture.EnglishName) }); } + } - foreach (var culture in CultureInfo.GetCultures(CultureTypes.SpecificCultures)) - { - var code = culture.ToString(); - - if (code.Length == 5 && languages.Any(l => l.Code.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))) - { - languages.Add(new Language - { - Code = code, - NameNative = culture.NativeName, - NameEnglish = culture.EnglishName, - FieldName = ToFieldName(culture.EnglishName) - }); - } - } + var oldFile = File.ReadAllLines("old-languages.csv"); - var oldFile = File.ReadAllLines("old-languages.csv"); + foreach (var line in oldFile.Skip(1)) + { + var code = line.Substring(1, 2); - foreach (var line in oldFile.Skip(1)) + if (!languages.Any(x => x.Code == code)) { - var code = line.Substring(1, 2); - - if (!languages.Any(x => x.Code == code)) - { - throw new InvalidOperationException($"Cannot find old language code {code}."); - } + throw new InvalidOperationException($"Cannot find old language code {code}."); } - - return languages; } + + return languages; } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AdminUsersTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AdminUsersTests.cs index 927eb875c8..433559686d 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AdminUsersTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AdminUsersTests.cs @@ -11,114 +11,113 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public sealed class AdminUsersTests : IClassFixture<ClientFixture> { - public sealed class AdminUsersTests : IClassFixture<ClientFixture> - { - private readonly string email = $"{Guid.NewGuid()}@squidex.io"; + private readonly string email = $"{Guid.NewGuid()}@squidex.io"; - public ClientFixture _ { get; } + public ClientFixture _ { get; } - public AdminUsersTests(ClientFixture fixture) - { - _ = fixture; - } + public AdminUsersTests(ClientFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_create_user() - { - // STEP 1: Create user. - var user_0 = await CreateUserAsync(); + [Fact] + public async Task Should_create_user() + { + // STEP 1: Create user. + var user_0 = await CreateUserAsync(); - Assert.Equal(email, user_0.Email); - Assert.Equal(email, user_0.DisplayName); + Assert.Equal(email, user_0.Email); + Assert.Equal(email, user_0.DisplayName); - // STEP 2: Get user by ID. - var userById = await _.UserManagement.GetUserAsync(user_0.Id); + // STEP 2: Get user by ID. + var userById = await _.UserManagement.GetUserAsync(user_0.Id); - Assert.Equal(email, userById.Email); - Assert.Equal(email, userById.DisplayName); + Assert.Equal(email, userById.Email); + Assert.Equal(email, userById.DisplayName); - // STEP 2: Get users by email. - var usersByEmail = await _.UserManagement.GetUsersAsync(user_0.Email); + // STEP 2: Get users by email. + var usersByEmail = await _.UserManagement.GetUsersAsync(user_0.Email); - Assert.Equal(email, usersByEmail.Items.First().Email); - Assert.Equal(email, usersByEmail.Items.First().DisplayName); - } + Assert.Equal(email, usersByEmail.Items.First().Email); + Assert.Equal(email, usersByEmail.Items.First().DisplayName); + } - [Fact] - public async Task Should_update_user() - { - // STEP 0: Create user. - var user_0 = await CreateUserAsync(); + [Fact] + public async Task Should_update_user() + { + // STEP 0: Create user. + var user_0 = await CreateUserAsync(); - // STEP 2: Update user. - var updateRequest = new UpdateUserDto - { - DisplayName = Guid.NewGuid().ToString(), - // The API requests to also set the email address. - Email = email, - }; + // STEP 2: Update user. + var updateRequest = new UpdateUserDto + { + DisplayName = Guid.NewGuid().ToString(), + // The API requests to also set the email address. + Email = email, + }; - var user_1 = await _.UserManagement.PutUserAsync(user_0.Id, updateRequest); + var user_1 = await _.UserManagement.PutUserAsync(user_0.Id, updateRequest); - Assert.Equal(updateRequest.DisplayName, user_1.DisplayName); - } + Assert.Equal(updateRequest.DisplayName, user_1.DisplayName); + } - [Fact] - public async Task Should_lock_user() - { - // STEP 0: Create user. - var user_0 = await CreateUserAsync(); + [Fact] + public async Task Should_lock_user() + { + // STEP 0: Create user. + var user_0 = await CreateUserAsync(); - // STEP 1: Lock user. - var user_1 = await _.UserManagement.LockUserAsync(user_0.Id); + // STEP 1: Lock user. + var user_1 = await _.UserManagement.LockUserAsync(user_0.Id); - Assert.True(user_1.IsLocked); + Assert.True(user_1.IsLocked); - // STEP 2: Unlock user. - var user_2 = await _.UserManagement.UnlockUserAsync(user_0.Id); + // STEP 2: Unlock user. + var user_2 = await _.UserManagement.UnlockUserAsync(user_0.Id); - Assert.False(user_2.IsLocked); - } + Assert.False(user_2.IsLocked); + } - [Fact] - public async Task Should_delete_user() - { - // STEP 0: Create user. - var user_0 = await CreateUserAsync(); + [Fact] + public async Task Should_delete_user() + { + // STEP 0: Create user. + var user_0 = await CreateUserAsync(); - Assert.Equal(email, user_0.Email); - Assert.Equal(email, user_0.DisplayName); + Assert.Equal(email, user_0.Email); + Assert.Equal(email, user_0.DisplayName); - // STEP 1: Delete user - await _.UserManagement.DeleteUserAsync(user_0.Id); + // STEP 1: Delete user + await _.UserManagement.DeleteUserAsync(user_0.Id); - // STEP 2: Get user by email. - var usersByEmail = await _.UserManagement.GetUsersAsync(user_0.Email); + // STEP 2: Get user by email. + var usersByEmail = await _.UserManagement.GetUsersAsync(user_0.Email); - Assert.Empty(usersByEmail.Items); - } + Assert.Empty(usersByEmail.Items); + } - private async Task<UserDto> CreateUserAsync() + private async Task<UserDto> CreateUserAsync() + { + var createRequest = new CreateUserDto { - var createRequest = new CreateUserDto - { - Email = email, - Password = "1q2w3e$R", - Permissions = new List<string>(), - // The API requests to also set the display name. - DisplayName = email, - }; - - return await _.UserManagement.PostUserAsync(createRequest); - } + Email = email, + Password = "1q2w3e$R", + Permissions = new List<string>(), + // The API requests to also set the display name. + DisplayName = email, + }; + + return await _.UserManagement.PostUserAsync(createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.cs index 6ebf1c6b21..b94d46e5e1 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AnonymousTests.cs @@ -12,105 +12,104 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public class AnonymousTests : IClassFixture<ClientFixture> { - [UsesVerify] - public class AnonymousTests : IClassFixture<ClientFixture> + public ClientFixture _ { get; } + + public AnonymousTests(ClientFixture fixture) { - public ClientFixture _ { get; } + _ = fixture; + } - public AnonymousTests(ClientFixture fixture) - { - _ = fixture; - } + [Fact] + public async Task Should_create_app_with_anonymous_read_access() + { + var appName = Guid.NewGuid().ToString(); - [Fact] - public async Task Should_create_app_with_anonymous_read_access() + // STEP 1: Create app + var createRequest = new CreateAppDto { - var appName = Guid.NewGuid().ToString(); - - // STEP 1: Create app - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - var app = await _.Apps.PostAppAsync(createRequest); + var app = await _.Apps.PostAppAsync(createRequest); - // Should return create app with correct name. - Assert.Equal(appName, app.Name); + // Should return create app with correct name. + Assert.Equal(appName, app.Name); - // STEP 2: Make the client anonymous. - var clientRequest = new UpdateClientDto - { - AllowAnonymous = true - }; - - await _.Apps.PutClientAsync(appName, "default", clientRequest); + // STEP 2: Make the client anonymous. + var clientRequest = new UpdateClientDto + { + AllowAnonymous = true + }; + await _.Apps.PutClientAsync(appName, "default", clientRequest); - // STEP 3: Check anonymous permission - var url = $"{_.ClientManager.Options.Url}api/apps/{appName}/settings"; - using (var httpClient = new HttpClient()) - { - var response = await httpClient.GetAsync(url); + // STEP 3: Check anonymous permission + var url = $"{_.ClientManager.Options.Url}api/apps/{appName}/settings"; - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + using (var httpClient = new HttpClient()) + { + var response = await httpClient.GetAsync(url); - await Verify(app); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); } - [Fact] - public async Task Should_create_app_with_anonymous_write_access() - { - var appName = Guid.NewGuid().ToString(); + await Verify(app); + } - // STEP 1: Create app - var createRequest = new CreateAppDto - { - Name = appName - }; + [Fact] + public async Task Should_create_app_with_anonymous_write_access() + { + var appName = Guid.NewGuid().ToString(); - var app = await _.Apps.PostAppAsync(createRequest); + // STEP 1: Create app + var createRequest = new CreateAppDto + { + Name = appName + }; - // Should return create app with correct name. - Assert.Equal(appName, app.Name); + var app = await _.Apps.PostAppAsync(createRequest); + // Should return create app with correct name. + Assert.Equal(appName, app.Name); - // STEP 2: Make the client anonymous. - var clientRequest = new UpdateClientDto - { - AllowAnonymous = true - }; - await _.Apps.PutClientAsync(appName, "default", clientRequest); + // STEP 2: Make the client anonymous. + var clientRequest = new UpdateClientDto + { + AllowAnonymous = true + }; + await _.Apps.PutClientAsync(appName, "default", clientRequest); - // STEP 3: Create schema - var schemaRequest = new CreateSchemaDto - { - Name = "my-content", - // Schema must be published to create content. - IsPublished = true - }; - await _.Schemas.PostSchemaAsync(appName, schemaRequest); + // STEP 3: Create schema + var schemaRequest = new CreateSchemaDto + { + Name = "my-content", + // Schema must be published to create content. + IsPublished = true + }; + await _.Schemas.PostSchemaAsync(appName, schemaRequest); - // STEP 3: Create a content. - var url = $"{_.ClientManager.Options.Url}api/content/{appName}/my-content"; - using (var httpClient = new HttpClient()) - { - var response = await httpClient.PostAsync(url, new StringContent("{}", null, "text/json")); + // STEP 3: Create a content. + var url = $"{_.ClientManager.Options.Url}api/content/{appName}/my-content"; - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } + using (var httpClient = new HttpClient()) + { + var response = await httpClient.PostAsync(url, new StringContent("{}", null, "text/json")); - await Verify(app); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); } + + await Verify(app); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs index 44e8c80056..1def631c40 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs @@ -11,118 +11,117 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests -{ - [UsesVerify] - public sealed class AppClientsTests : IClassFixture<ClientFixture> - { - private readonly string appName = Guid.NewGuid().ToString(); - private readonly string id = Guid.NewGuid().ToString(); - private readonly string clientRole = "Editor"; - private readonly string clientName = "My Client"; - - public ClientFixture _ { get; } +namespace TestSuite.ApiTests; - public AppClientsTests(ClientFixture fixture) - { - _ = fixture; - } - - [Fact] - public async Task Should_create_client() - { - // STEP 0: Create app. - await CreateAppAsync(); +[UsesVerify] +public sealed class AppClientsTests : IClassFixture<ClientFixture> +{ + private readonly string appName = Guid.NewGuid().ToString(); + private readonly string id = Guid.NewGuid().ToString(); + private readonly string clientRole = "Editor"; + private readonly string clientName = "My Client"; + public ClientFixture _ { get; } - // STEP 1: Create client. - var client = await CreateAsync(); + public AppClientsTests(ClientFixture fixture) + { + _ = fixture; + } - // Should return client with correct name and id. - Assert.Equal(clientRole, client.Role); - Assert.Equal(id, client.Name); + [Fact] + public async Task Should_create_client() + { + // STEP 0: Create app. + await CreateAppAsync(); - await Verify(client) - .IgnoreMember<ClientDto>(x => x.Secret); - } - [Fact] - public async Task Should_update_client() - { - // STEP 0: Create app. - await CreateAppAsync(); + // STEP 1: Create client. + var client = await CreateAsync(); + // Should return client with correct name and id. + Assert.Equal(clientRole, client.Role); + Assert.Equal(id, client.Name); - // STEP 0: Create client. - var client = await CreateAsync(); - + await Verify(client) + .IgnoreMember<ClientDto>(x => x.Secret); + } - // STEP 1: Update client name. - var updateNameRequest = new UpdateClientDto - { - Name = clientName, - AllowAnonymous = true, - ApiCallsLimit = 100, - ApiTrafficLimit = 200, - Role = "Owner" - }; + [Fact] + public async Task Should_update_client() + { + // STEP 0: Create app. + await CreateAppAsync(); - var clients_2 = await _.Apps.PutClientAsync(appName, client.Id, updateNameRequest); - var client_2 = clients_2.Items.Find(x => x.Id == client.Id); - // Should update client name. - Assert.Equal(updateNameRequest.Name, client_2.Name); - Assert.Equal(updateNameRequest.AllowAnonymous, client_2.AllowAnonymous); - Assert.Equal(updateNameRequest.ApiCallsLimit, client_2.ApiCallsLimit); - Assert.Equal(updateNameRequest.ApiTrafficLimit, client_2.ApiTrafficLimit); - Assert.Equal(updateNameRequest.Role, client_2.Role); + // STEP 0: Create client. + var client = await CreateAsync(); - await Verify(clients_2) - .IgnoreMember<ClientDto>(x => x.Secret); - } - [Fact] - public async Task Should_delete_client() + // STEP 1: Update client name. + var updateNameRequest = new UpdateClientDto { - // STEP 0: Create app. - await CreateAppAsync(); + Name = clientName, + AllowAnonymous = true, + ApiCallsLimit = 100, + ApiTrafficLimit = 200, + Role = "Owner" + }; + + var clients_2 = await _.Apps.PutClientAsync(appName, client.Id, updateNameRequest); + var client_2 = clients_2.Items.Find(x => x.Id == client.Id); + + // Should update client name. + Assert.Equal(updateNameRequest.Name, client_2.Name); + Assert.Equal(updateNameRequest.AllowAnonymous, client_2.AllowAnonymous); + Assert.Equal(updateNameRequest.ApiCallsLimit, client_2.ApiCallsLimit); + Assert.Equal(updateNameRequest.ApiTrafficLimit, client_2.ApiTrafficLimit); + Assert.Equal(updateNameRequest.Role, client_2.Role); + + await Verify(clients_2) + .IgnoreMember<ClientDto>(x => x.Secret); + } + + [Fact] + public async Task Should_delete_client() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 0: Create client. - var client = await CreateAsync(); + // STEP 0: Create client. + var client = await CreateAsync(); - // STEP 1: Delete client - var clients_2 = await _.Apps.DeleteClientAsync(appName, client.Id); + // STEP 1: Delete client + var clients_2 = await _.Apps.DeleteClientAsync(appName, client.Id); - // Should not return deleted client. - Assert.DoesNotContain(clients_2.Items, x => x.Id == client.Id); + // Should not return deleted client. + Assert.DoesNotContain(clients_2.Items, x => x.Id == client.Id); - await Verify(clients_2) - .IgnoreMember<ClientDto>(x => x.Secret); - } + await Verify(clients_2) + .IgnoreMember<ClientDto>(x => x.Secret); + } - private async Task<ClientDto> CreateAsync() + private async Task<ClientDto> CreateAsync() + { + var createRequest = new CreateClientDto { - var createRequest = new CreateClientDto - { - Id = id - }; + Id = id + }; - var clients = await _.Apps.PostClientAsync(appName, createRequest); - var client = clients.Items.Find(x => x.Id == id); + var clients = await _.Apps.PostClientAsync(appName, createRequest); + var client = clients.Items.Find(x => x.Id == id); - return client; - } + return client; + } - private async Task CreateAppAsync() + private async Task CreateAppAsync() + { + var createRequest = new CreateAppDto { - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - await _.Apps.PostAppAsync(createRequest); - } + await _.Apps.PostAppAsync(createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs index 55b71af118..bbd7af44f1 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs @@ -11,127 +11,126 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public sealed class AppContributorsTests : IClassFixture<ClientFixture> { - [UsesVerify] - public sealed class AppContributorsTests : IClassFixture<ClientFixture> - { - private readonly string appName = Guid.NewGuid().ToString(); - private readonly string email = $"{Guid.NewGuid()}@squidex.io"; + private readonly string appName = Guid.NewGuid().ToString(); + private readonly string email = $"{Guid.NewGuid()}@squidex.io"; - public ClientFixture _ { get; } + public ClientFixture _ { get; } - public AppContributorsTests(ClientFixture fixture) - { - _ = fixture; - } + public AppContributorsTests(ClientFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_not_invite_contributor_if_flag_is_false() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_not_invite_contributor_if_flag_is_false() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Do not invite contributors when flag is false. - var createRequest = new AssignContributorDto - { - ContributorId = "test@squidex.io" - }; + // STEP 1: Do not invite contributors when flag is false. + var createRequest = new AssignContributorDto + { + ContributorId = "test@squidex.io" + }; - var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => - { - return _.Apps.PostContributorAsync(appName, createRequest); - }); + var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => + { + return _.Apps.PostContributorAsync(appName, createRequest); + }); - Assert.Equal(404, ex.StatusCode); - } + Assert.Equal(404, ex.StatusCode); + } - [Fact] - public async Task Should_invite_contributor() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_invite_contributor() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Assign contributor. - ContributorDto contributor_1 = await InviteAsync(); + // STEP 1: Assign contributor. + ContributorDto contributor_1 = await InviteAsync(); - Assert.Equal("Developer", contributor_1?.Role); + Assert.Equal("Developer", contributor_1?.Role); - await Verify(contributor_1) - .IgnoreMember<ContributorDto>(x => x.ContributorId) - .IgnoreMember<ContributorDto>(x => x.ContributorEmail) - .IgnoreMember<ContributorDto>(x => x.ContributorName); - } + await Verify(contributor_1) + .IgnoreMember<ContributorDto>(x => x.ContributorId) + .IgnoreMember<ContributorDto>(x => x.ContributorEmail) + .IgnoreMember<ContributorDto>(x => x.ContributorName); + } - [Fact] - public async Task Should_update_contributor() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_update_contributor() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Assign contributor. - var contributor = await InviteAsync(); + // STEP 1: Assign contributor. + var contributor = await InviteAsync(); - // STEP 1: Update contributor role. - var updateRequest = new AssignContributorDto - { - ContributorId = email, - // Test update of role. - Role = "Owner" - }; + // STEP 1: Update contributor role. + var updateRequest = new AssignContributorDto + { + ContributorId = email, + // Test update of role. + Role = "Owner" + }; - var contributors_2 = await _.Apps.PostContributorAsync(appName, updateRequest); - var contributor_2 = contributors_2.Items.Find(x => x.ContributorId == contributor.ContributorId); + var contributors_2 = await _.Apps.PostContributorAsync(appName, updateRequest); + var contributor_2 = contributors_2.Items.Find(x => x.ContributorId == contributor.ContributorId); - Assert.Equal(updateRequest.Role, contributor_2?.Role); - } + Assert.Equal(updateRequest.Role, contributor_2?.Role); + } - [Fact] - public async Task Should_remove_contributor() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_remove_contributor() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Assign contributor. - var contributor = await InviteAsync(); + // STEP 1: Assign contributor. + var contributor = await InviteAsync(); - // STEP 1: Remove contributor. - var contributors_2 = await _.Apps.DeleteContributorAsync(appName, contributor.ContributorId); + // STEP 1: Remove contributor. + var contributors_2 = await _.Apps.DeleteContributorAsync(appName, contributor.ContributorId); - Assert.DoesNotContain(contributors_2.Items, x => x.ContributorId == contributor.ContributorId); + Assert.DoesNotContain(contributors_2.Items, x => x.ContributorId == contributor.ContributorId); - await Verify(contributors_2); - } + await Verify(contributors_2); + } - private async Task<ContributorDto> InviteAsync() + private async Task<ContributorDto> InviteAsync() + { + var createInviteRequest = new AssignContributorDto { - var createInviteRequest = new AssignContributorDto - { - ContributorId = email, - // Invite must be true, otherwise new users are not created. - Invite = true - }; + ContributorId = email, + // Invite must be true, otherwise new users are not created. + Invite = true + }; - var contributors = await _.Apps.PostContributorAsync(appName, createInviteRequest); - var contributor = contributors.Items.Find(x => x.ContributorName == email); + var contributors = await _.Apps.PostContributorAsync(appName, createInviteRequest); + var contributor = contributors.Items.Find(x => x.ContributorName == email); - return contributor; - } + return contributor; + } - private async Task CreateAppAsync() + private async Task CreateAppAsync() + { + var createRequest = new CreateAppDto { - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - await _.Apps.PostAppAsync(createRequest); - } + await _.Apps.PostAppAsync(createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs index c7b42bf582..a2529231ae 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs @@ -11,151 +11,150 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public class AppCreationTests : IClassFixture<ClientFixture> { - [UsesVerify] - public class AppCreationTests : IClassFixture<ClientFixture> - { - private readonly string appName = Guid.NewGuid().ToString(); + private readonly string appName = Guid.NewGuid().ToString(); - public ClientFixture _ { get; } + public ClientFixture _ { get; } - public AppCreationTests(ClientFixture fixture) - { - _ = fixture; - } + public AppCreationTests(ClientFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_create_app() + [Fact] + public async Task Should_create_app() + { + // STEP 1: Create app + var createRequest = new CreateAppDto { - // STEP 1: Create app - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - var app = await _.Apps.PostAppAsync(createRequest); + var app = await _.Apps.PostAppAsync(createRequest); - // Should return created app with correct name. - Assert.Equal(appName, app.Name); + // Should return created app with correct name. + Assert.Equal(appName, app.Name); - // STEP 2: Get all apps - var apps = await _.Apps.GetAppsAsync(); + // STEP 2: Get all apps + var apps = await _.Apps.GetAppsAsync(); - // Should provide new app when apps are queried. - Assert.Contains(apps, x => x.Name == appName); + // Should provide new app when apps are queried. + Assert.Contains(apps, x => x.Name == appName); - // STEP 3: Check contributors - var contributors = await _.Apps.GetContributorsAsync(appName); + // STEP 3: Check contributors + var contributors = await _.Apps.GetContributorsAsync(appName); - // Should not add client itself as a contributor. - Assert.Empty(contributors.Items); + // Should not add client itself as a contributor. + Assert.Empty(contributors.Items); - // STEP 4: Check clients - var clients = await _.Apps.GetClientsAsync(appName); + // STEP 4: Check clients + var clients = await _.Apps.GetClientsAsync(appName); - // Should create default client. - Assert.Contains(clients.Items, x => x.Id == "default"); - - await Verify(app); - } - - [Fact] - public async Task Should_not_allow_creation_if_name_used() - { - // STEP 1: Create app - await CreateAppAsync(); + // Should create default client. + Assert.Contains(clients.Items, x => x.Id == "default"); + await Verify(app); + } - // STEP 2: Create again and fail - var createRequest = new CreateAppDto - { - Name = appName - }; + [Fact] + public async Task Should_not_allow_creation_if_name_used() + { + // STEP 1: Create app + await CreateAppAsync(); - var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => - { - return _.Apps.PostAppAsync(createRequest); - }); - Assert.Equal(400, ex.StatusCode); - } + // STEP 2: Create again and fail + var createRequest = new CreateAppDto + { + Name = appName + }; - [Fact] - public async Task Should_archive_app() + var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => { - // STEP 1: Create app - await CreateAppAsync(); + return _.Apps.PostAppAsync(createRequest); + }); + Assert.Equal(400, ex.StatusCode); + } - // STEP 2: Archive app - await _.Apps.DeleteAppAsync(appName); + [Fact] + public async Task Should_archive_app() + { + // STEP 1: Create app + await CreateAppAsync(); - var apps = await _.Apps.GetAppsAsync(); - // Should not provide deleted app when apps are queried. - Assert.DoesNotContain(apps, x => x.Name == appName); - } + // STEP 2: Archive app + await _.Apps.DeleteAppAsync(appName); - [Fact] - public async Task Should_recreate_after_archived() - { - // STEP 1: Create app - await CreateAppAsync(); + var apps = await _.Apps.GetAppsAsync(); + // Should not provide deleted app when apps are queried. + Assert.DoesNotContain(apps, x => x.Name == appName); + } - // STEP 2: Archive app - await _.Apps.DeleteAppAsync(appName); + [Fact] + public async Task Should_recreate_after_archived() + { + // STEP 1: Create app + await CreateAppAsync(); - // STEP 3: Create app again - var createRequest = new CreateAppDto - { - Name = appName - }; + // STEP 2: Archive app + await _.Apps.DeleteAppAsync(appName); - await _.Apps.PostAppAsync(createRequest); - } - [Fact] - public async Task Should_create_app_from_templates() + // STEP 3: Create app again + var createRequest = new CreateAppDto { - // STEP 1: Get template. - var templates = await _.Templates.GetTemplatesAsync(); + Name = appName + }; + + await _.Apps.PostAppAsync(createRequest); + } + + [Fact] + public async Task Should_create_app_from_templates() + { + // STEP 1: Get template. + var templates = await _.Templates.GetTemplatesAsync(); - var template = templates.Items.First(x => x.IsStarter); + var template = templates.Items.First(x => x.IsStarter); - // STEP 2: Create app. - var createRequest = new CreateAppDto - { - Name = appName, - // The template is just referenced by the name. - Template = template.Name - }; + // STEP 2: Create app. + var createRequest = new CreateAppDto + { + Name = appName, + // The template is just referenced by the name. + Template = template.Name + }; - await _.Apps.PostAppAsync(createRequest); + await _.Apps.PostAppAsync(createRequest); - // STEP 3: Get schemas - var schemas = await _.Schemas.GetSchemasAsync(appName); + // STEP 3: Get schemas + var schemas = await _.Schemas.GetSchemasAsync(appName); - Assert.NotEmpty(schemas.Items); + Assert.NotEmpty(schemas.Items); - await Verify(schemas); - } + await Verify(schemas); + } - private async Task CreateAppAsync() + private async Task CreateAppAsync() + { + var createRequest = new CreateAppDto { - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - await _.Apps.PostAppAsync(createRequest); - } + await _.Apps.PostAppAsync(createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppLanguagesTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppLanguagesTests.cs index 424ebfbbdf..67d2936511 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppLanguagesTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppLanguagesTests.cs @@ -11,191 +11,190 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public sealed class AppLanguagesTests : IClassFixture<ClientFixture> { - [UsesVerify] - public sealed class AppLanguagesTests : IClassFixture<ClientFixture> - { - private readonly string appName = Guid.NewGuid().ToString(); + private readonly string appName = Guid.NewGuid().ToString(); - public ClientFixture _ { get; } + public ClientFixture _ { get; } - public AppLanguagesTests(ClientFixture fixture) - { - _ = fixture; - } + public AppLanguagesTests(ClientFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_add_language() - { - // STEP 0: Add app. - await CreateAppAsync(); + [Fact] + public async Task Should_add_language() + { + // STEP 0: Add app. + await CreateAppAsync(); - // STEP 1: Add languages. - await AddLanguageAsync("de"); - await AddLanguageAsync("it"); + // STEP 1: Add languages. + await AddLanguageAsync("de"); + await AddLanguageAsync("it"); - var languages_1 = await _.Apps.GetLanguagesAsync(appName); + var languages_1 = await _.Apps.GetLanguagesAsync(appName); - Assert.Equal(new string[] { "en", "de", "it" }, languages_1.Items.Select(x => x.Iso2Code).ToArray()); + Assert.Equal(new string[] { "en", "de", "it" }, languages_1.Items.Select(x => x.Iso2Code).ToArray()); - await Verify(languages_1); - } + await Verify(languages_1); + } - [Fact] - public async Task Should_add_custom_language() - { - // STEP 0: Add app. - await CreateAppAsync(); + [Fact] + public async Task Should_add_custom_language() + { + // STEP 0: Add app. + await CreateAppAsync(); - // STEP 1: Add languages. - await AddLanguageAsync("abc"); - await AddLanguageAsync("xyz"); + // STEP 1: Add languages. + await AddLanguageAsync("abc"); + await AddLanguageAsync("xyz"); - var languages_1 = await _.Apps.GetLanguagesAsync(appName); + var languages_1 = await _.Apps.GetLanguagesAsync(appName); - Assert.Equal(new string[] { "en", "abc", "xyz" }, languages_1.Items.Select(x => x.Iso2Code).ToArray()); + Assert.Equal(new string[] { "en", "abc", "xyz" }, languages_1.Items.Select(x => x.Iso2Code).ToArray()); - await Verify(languages_1); - } + await Verify(languages_1); + } - [Fact] - public async Task Should_update_language() - { - // STEP 0: Add app. - await CreateAppAsync(); + [Fact] + public async Task Should_update_language() + { + // STEP 0: Add app. + await CreateAppAsync(); - // STEP 1: Add languages. - await AddLanguageAsync("de"); - await AddLanguageAsync("it"); + // STEP 1: Add languages. + await AddLanguageAsync("de"); + await AddLanguageAsync("it"); - // STEP 3: Update German language. - var updateRequest = new UpdateLanguageDto + // STEP 3: Update German language. + var updateRequest = new UpdateLanguageDto + { + Fallback = new List<string> { - Fallback = new List<string> - { - "it" - }, - IsOptional = true - }; + "it" + }, + IsOptional = true + }; - var languages_2 = await _.Apps.PutLanguageAsync(appName, "de", updateRequest); - var language_2_DE = languages_2.Items.Find(x => x.Iso2Code == "de"); + var languages_2 = await _.Apps.PutLanguageAsync(appName, "de", updateRequest); + var language_2_DE = languages_2.Items.Find(x => x.Iso2Code == "de"); - Assert.Equal(updateRequest.Fallback, language_2_DE.Fallback); - Assert.Equal(updateRequest.IsOptional, language_2_DE.IsOptional); + Assert.Equal(updateRequest.Fallback, language_2_DE.Fallback); + Assert.Equal(updateRequest.IsOptional, language_2_DE.IsOptional); - await Verify(languages_2); - } + await Verify(languages_2); + } - [Fact] - public async Task Should_update_master_language() - { - // STEP 0: Add app. - await CreateAppAsync(); + [Fact] + public async Task Should_update_master_language() + { + // STEP 0: Add app. + await CreateAppAsync(); - // STEP 1: Add languages. - await AddLanguageAsync("de"); - await AddLanguageAsync("it"); + // STEP 1: Add languages. + await AddLanguageAsync("de"); + await AddLanguageAsync("it"); - // STEP 2: Update Italian language. - var updateRequest = new UpdateLanguageDto + // STEP 2: Update Italian language. + var updateRequest = new UpdateLanguageDto + { + Fallback = new List<string> { - Fallback = new List<string> - { - "de" - }, - IsOptional = true - }; + "de" + }, + IsOptional = true + }; - await _.Apps.PutLanguageAsync(appName, "it", updateRequest); + await _.Apps.PutLanguageAsync(appName, "it", updateRequest); - // STEP 3: Change master language to Italian. - var masterRequest = new UpdateLanguageDto - { - IsMaster = true - }; + // STEP 3: Change master language to Italian. + var masterRequest = new UpdateLanguageDto + { + IsMaster = true + }; - var languages_4 = await _.Apps.PutLanguageAsync(appName, "it", masterRequest); - var language_4_IT = languages_4.Items.Find(x => x.Iso2Code == "it"); - var language_4_EN = languages_4.Items.Find(x => x.Iso2Code == "en"); + var languages_4 = await _.Apps.PutLanguageAsync(appName, "it", masterRequest); + var language_4_IT = languages_4.Items.Find(x => x.Iso2Code == "it"); + var language_4_EN = languages_4.Items.Find(x => x.Iso2Code == "en"); - Assert.True(language_4_IT.IsMaster); + Assert.True(language_4_IT.IsMaster); - // Old master language is unset. - Assert.False(language_4_EN.IsMaster); + // Old master language is unset. + Assert.False(language_4_EN.IsMaster); - // Master language cannot be optional. - Assert.False(language_4_IT.IsOptional); + // Master language cannot be optional. + Assert.False(language_4_IT.IsOptional); - // Fallback for new master language must be removed. - Assert.Empty(language_4_IT.Fallback); + // Fallback for new master language must be removed. + Assert.Empty(language_4_IT.Fallback); - await Verify(languages_4); - } + await Verify(languages_4); + } - [Fact] - public async Task Should_delete_language() - { - // STEP 0: Add app. - await CreateAppAsync(); + [Fact] + public async Task Should_delete_language() + { + // STEP 0: Add app. + await CreateAppAsync(); - // STEP 1: Add languages. - await AddLanguageAsync("de"); - await AddLanguageAsync("it"); + // STEP 1: Add languages. + await AddLanguageAsync("de"); + await AddLanguageAsync("it"); - // STEP 2: Update Italian language. - var updateRequest = new UpdateLanguageDto + // STEP 2: Update Italian language. + var updateRequest = new UpdateLanguageDto + { + Fallback = new List<string> { - Fallback = new List<string> - { - "de" - }, - IsOptional = true - }; + "de" + }, + IsOptional = true + }; - await _.Apps.PutLanguageAsync(appName, "it", updateRequest); + await _.Apps.PutLanguageAsync(appName, "it", updateRequest); - // STEP 3: Remove language. - var languages_2 = await _.Apps.DeleteLanguageAsync(appName, "de"); - var language_2_IT = languages_2.Items.Find(x => x.Iso2Code == "it"); + // STEP 3: Remove language. + var languages_2 = await _.Apps.DeleteLanguageAsync(appName, "de"); + var language_2_IT = languages_2.Items.Find(x => x.Iso2Code == "it"); - // Fallback language must be removed. - Assert.Empty(language_2_IT.Fallback); + // Fallback language must be removed. + Assert.Empty(language_2_IT.Fallback); - Assert.Equal(new string[] { "en", "it" }, languages_2.Items.Select(x => x.Iso2Code).ToArray()); + Assert.Equal(new string[] { "en", "it" }, languages_2.Items.Select(x => x.Iso2Code).ToArray()); - await Verify(languages_2); - } + await Verify(languages_2); + } - private async Task CreateAppAsync() + private async Task CreateAppAsync() + { + var createRequest = new CreateAppDto { - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - await _.Apps.PostAppAsync(createRequest); - } + await _.Apps.PostAppAsync(createRequest); + } - private async Task AddLanguageAsync(string code) + private async Task AddLanguageAsync(string code) + { + var createRequest = new AddLanguageDto { - var createRequest = new AddLanguageDto - { - Language = code - }; + Language = code + }; - await _.Apps.PostLanguageAsync(appName, createRequest); - } + await _.Apps.PostLanguageAsync(appName, createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs index c77ff8863d..5e8b6f3a76 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs @@ -11,183 +11,182 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public sealed class AppRolesTests : IClassFixture<CreatedAppFixture> { - [UsesVerify] - public sealed class AppRolesTests : IClassFixture<CreatedAppFixture> + private readonly string roleName = Guid.NewGuid().ToString(); + private readonly string client = Guid.NewGuid().ToString(); + private readonly string contributor = $"{Guid.NewGuid()}@squidex.io"; + + public CreatedAppFixture _ { get; } + + public AppRolesTests(CreatedAppFixture fixture) { - private readonly string roleName = Guid.NewGuid().ToString(); - private readonly string client = Guid.NewGuid().ToString(); - private readonly string contributor = $"{Guid.NewGuid()}@squidex.io"; + _ = fixture; + } - public CreatedAppFixture _ { get; } + [Fact] + public async Task Should_create_role() + { + // STEP 1: Add role. + var role = await CreateRoleAsync(roleName); - public AppRolesTests(CreatedAppFixture fixture) - { - _ = fixture; - } + // Should return role with correct name. + Assert.Empty(role.Permissions); - [Fact] - public async Task Should_create_role() - { - // STEP 1: Add role. - var role = await CreateRoleAsync(roleName); + await Verify(role) + .IgnoreMember<RoleDto>(x => x.Name); + } - // Should return role with correct name. - Assert.Empty(role.Permissions); + [Fact] + public async Task Should_create_role_with_buggy_name() + { + // STEP 1: Add role. + var role = await CreateRoleAsync($"{Guid.NewGuid()}/1"); - await Verify(role) - .IgnoreMember<RoleDto>(x => x.Name); - } + // Should return role with correct name. + Assert.Empty(role.Permissions); - [Fact] - public async Task Should_create_role_with_buggy_name() - { - // STEP 1: Add role. - var role = await CreateRoleAsync($"{Guid.NewGuid()}/1"); + await Verify(role) + .IgnoreMember<RoleDto>(x => x.Name); + } - // Should return role with correct name. - Assert.Empty(role.Permissions); + [Fact] + public async Task Should_update_role() + { + // STEP 1: Add role. + var role = await CreateRoleAsync(roleName); - await Verify(role) - .IgnoreMember<RoleDto>(x => x.Name); - } - [Fact] - public async Task Should_update_role() + // STEP 2: Update role. + var updateRequest = new UpdateRoleDto { - // STEP 1: Add role. - var role = await CreateRoleAsync(roleName); + Permissions = new List<string> { "a", "b" } + }; + var roles_2 = await _.Apps.PutRoleAsync(_.AppName, roleName, updateRequest); + var role_2 = roles_2.Items.Find(x => x.Name == roleName); - // STEP 2: Update role. - var updateRequest = new UpdateRoleDto - { - Permissions = new List<string> { "a", "b" } - }; + // Should return role with correct name. + Assert.Equal(updateRequest.Permissions, role_2.Permissions); - var roles_2 = await _.Apps.PutRoleAsync(_.AppName, roleName, updateRequest); - var role_2 = roles_2.Items.Find(x => x.Name == roleName); + await Verify(role_2) + .IgnoreMember<RoleDto>(x => x.Name); + } - // Should return role with correct name. - Assert.Equal(updateRequest.Permissions, role_2.Permissions); + [Fact] + public async Task Should_prevent_deletion_if_client_assigned() + { + // STEP 1: Add role. + var role = await CreateRoleAsync(roleName); - await Verify(role_2) - .IgnoreMember<RoleDto>(x => x.Name); - } - [Fact] - public async Task Should_prevent_deletion_if_client_assigned() + // STEP 2 Assign client and contributor. + var createClientRequest = new CreateClientDto { - // STEP 1: Add role. - var role = await CreateRoleAsync(roleName); + Id = client + }; + await _.Apps.PostClientAsync(_.AppName, createClientRequest); - // STEP 2 Assign client and contributor. - var createClientRequest = new CreateClientDto - { - Id = client - }; + await AssignClient(roleName); - await _.Apps.PostClientAsync(_.AppName, createClientRequest); + var roles_2 = await _.Apps.GetRolesAsync(_.AppName); + var role_2 = roles_2.Items.Find(x => x.Name == roleName); - await AssignClient(roleName); + // Should return role with correct number of users and clients. + Assert.Equal(1, role_2.NumClients); + Assert.Equal(0, role_2.NumContributors); - var roles_2 = await _.Apps.GetRolesAsync(_.AppName); - var role_2 = roles_2.Items.Find(x => x.Name == roleName); - // Should return role with correct number of users and clients. - Assert.Equal(1, role_2.NumClients); - Assert.Equal(0, role_2.NumContributors); - - - // STEP 4: Try to delete role. - var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => - { - return _.Apps.DeleteRoleAsync(_.AppName, roleName); - }); + // STEP 4: Try to delete role. + var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => + { + return _.Apps.DeleteRoleAsync(_.AppName, roleName); + }); - Assert.Equal(400, ex.StatusCode); + Assert.Equal(400, ex.StatusCode); - // STEP 5: AssignClient client. - await AssignClient("Developer"); + // STEP 5: AssignClient client. + await AssignClient("Developer"); - var roles_3 = await _.Apps.DeleteRoleAsync(_.AppName, roleName); + var roles_3 = await _.Apps.DeleteRoleAsync(_.AppName, roleName); - Assert.DoesNotContain(roles_3.Items, x => x.Name == roleName); - } + Assert.DoesNotContain(roles_3.Items, x => x.Name == roleName); + } - [Fact] - public async Task Should_prevent_deletion_if_contributor_assigned() - { - // STEP 1: Add role. - var role = await CreateRoleAsync(roleName); + [Fact] + public async Task Should_prevent_deletion_if_contributor_assigned() + { + // STEP 1: Add role. + var role = await CreateRoleAsync(roleName); - // STEP 2 Assign contributor. - await AssignContributor(roleName); + // STEP 2 Assign contributor. + await AssignContributor(roleName); - var roles_2 = await _.Apps.GetRolesAsync(_.AppName); - var role_2 = roles_2.Items.Find(x => x.Name == roleName); + var roles_2 = await _.Apps.GetRolesAsync(_.AppName); + var role_2 = roles_2.Items.Find(x => x.Name == roleName); - // Should return role with correct number of users and clients. - Assert.Equal(0, role_2.NumClients); - Assert.Equal(1, role_2.NumContributors); + // Should return role with correct number of users and clients. + Assert.Equal(0, role_2.NumClients); + Assert.Equal(1, role_2.NumContributors); - // STEP 4: Try to delete role. - var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => - { - return _.Apps.DeleteRoleAsync(_.AppName, roleName); - }); + // STEP 4: Try to delete role. + var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => + { + return _.Apps.DeleteRoleAsync(_.AppName, roleName); + }); - Assert.Equal(400, ex.StatusCode); + Assert.Equal(400, ex.StatusCode); - // STEP 5: Remove role after contributor removed. - await AssignContributor("Developer"); + // STEP 5: Remove role after contributor removed. + await AssignContributor("Developer"); - var roles_3 = await _.Apps.DeleteRoleAsync(_.AppName, roleName); + var roles_3 = await _.Apps.DeleteRoleAsync(_.AppName, roleName); - Assert.DoesNotContain(roles_3.Items, x => x.Name == roleName); - } + Assert.DoesNotContain(roles_3.Items, x => x.Name == roleName); + } - private async Task AssignContributor(string role = null) + private async Task AssignContributor(string role = null) + { + var assignRequest = new AssignContributorDto { - var assignRequest = new AssignContributorDto - { - ContributorId = contributor, - // Test diffferent role names. - Role = role, - // Invite must be true, otherwise new users are not created. - Invite = true - }; - - await _.Apps.PostContributorAsync(_.AppName, assignRequest); - } - - private async Task AssignClient(string role = null) + ContributorId = contributor, + // Test diffferent role names. + Role = role, + // Invite must be true, otherwise new users are not created. + Invite = true + }; + + await _.Apps.PostContributorAsync(_.AppName, assignRequest); + } + + private async Task AssignClient(string role = null) + { + var updateRequest = new UpdateClientDto { - var updateRequest = new UpdateClientDto - { - Role = role - }; + Role = role + }; - await _.Apps.PutClientAsync(_.AppName, client, updateRequest); - } + await _.Apps.PutClientAsync(_.AppName, client, updateRequest); + } - private async Task<RoleDto> CreateRoleAsync(string name) + private async Task<RoleDto> CreateRoleAsync(string name) + { + var createRequest = new AddRoleDto { - var createRequest = new AddRoleDto - { - Name = name - }; + Name = name + }; - var roles = await _.Apps.PostRoleAsync(_.AppName, createRequest); - var role = roles.Items.Find(x => x.Name == name); + var roles = await _.Apps.PostRoleAsync(_.AppName, createRequest); + var role = roles.Items.Find(x => x.Name == name); - return role; - } + return role; } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs index c5241d2801..8b55cba19c 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs @@ -11,141 +11,140 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public sealed class AppTests : IClassFixture<CreatedAppFixture> { - [UsesVerify] - public sealed class AppTests : IClassFixture<CreatedAppFixture> - { - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public AppTests(CreatedAppFixture fixture) - { - _ = fixture; - } + public AppTests(CreatedAppFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_get_app() - { - // STEP 1: Get app. - var app = await _.Apps.GetAppAsync(_.AppName); + [Fact] + public async Task Should_get_app() + { + // STEP 1: Get app. + var app = await _.Apps.GetAppAsync(_.AppName); - Assert.Equal(_.AppName, app.Name); - } + Assert.Equal(_.AppName, app.Name); + } - [Fact] - public async Task Should_set_label() + [Fact] + public async Task Should_set_label() + { + // STEP 1: Update app + var updateRequest = new UpdateAppDto { - // STEP 1: Update app - var updateRequest = new UpdateAppDto - { - Label = Guid.NewGuid().ToString() - }; + Label = Guid.NewGuid().ToString() + }; - var app_1 = await _.Apps.PutAppAsync(_.AppName, updateRequest); + var app_1 = await _.Apps.PutAppAsync(_.AppName, updateRequest); - Assert.Equal(updateRequest.Label, app_1.Label); - } + Assert.Equal(updateRequest.Label, app_1.Label); + } - [Fact] - public async Task Should_set_description() + [Fact] + public async Task Should_set_description() + { + // STEP 1: Update app + var updateRequest = new UpdateAppDto { - // STEP 1: Update app - var updateRequest = new UpdateAppDto - { - Description = Guid.NewGuid().ToString() - }; + Description = Guid.NewGuid().ToString() + }; - var app_1 = await _.Apps.PutAppAsync(_.AppName, updateRequest); + var app_1 = await _.Apps.PutAppAsync(_.AppName, updateRequest); - Assert.Equal(updateRequest.Description, app_1.Description); - } + Assert.Equal(updateRequest.Description, app_1.Description); + } - [Fact] - public async Task Should_upload_image() + [Fact] + public async Task Should_upload_image() + { + // STEP 1: Upload image. + await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) { - // STEP 1: Upload image. - await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) - { - var file = new FileParameter(stream, "logo-squared.png", "image/png"); - - var app_1 = await _.Apps.UploadImageAsync(_.AppName, file); + var file = new FileParameter(stream, "logo-squared.png", "image/png"); - // Should contain image link. - Assert.True(app_1._links.ContainsKey("image")); - } + var app_1 = await _.Apps.UploadImageAsync(_.AppName, file); + // Should contain image link. + Assert.True(app_1._links.ContainsKey("image")); + } - // STEP 2: Download image. - await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) - { - var temp = new MemoryStream(); - var downloaded = new MemoryStream(); + // STEP 2: Download image. + await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var temp = new MemoryStream(); - using (var imageStream = await _.Apps.GetImageAsync(_.AppName)) - { - await imageStream.Stream.CopyToAsync(downloaded); - } + var downloaded = new MemoryStream(); - // Should dowload with correct size. - Assert.True(downloaded.Length < stream.Length); + using (var imageStream = await _.Apps.GetImageAsync(_.AppName)) + { + await imageStream.Stream.CopyToAsync(downloaded); } + + // Should dowload with correct size. + Assert.True(downloaded.Length < stream.Length); } + } - [Fact] - public async Task Should_delete_image() + [Fact] + public async Task Should_delete_image() + { + // STEP 1: Upload image. + await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) { - // STEP 1: Upload image. - await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) - { - var file = new FileParameter(stream, "logo-squared.png", "image/png"); + var file = new FileParameter(stream, "logo-squared.png", "image/png"); - var app_1 = await _.Apps.UploadImageAsync(_.AppName, file); + var app_1 = await _.Apps.UploadImageAsync(_.AppName, file); - // Should contain image link. - Assert.True(app_1._links.ContainsKey("image")); - } + // Should contain image link. + Assert.True(app_1._links.ContainsKey("image")); + } - // STEP 2: Delete Image. - var app_2 = await _.Apps.DeleteImageAsync(_.AppName); + // STEP 2: Delete Image. + var app_2 = await _.Apps.DeleteImageAsync(_.AppName); - // Should contain image link. - Assert.False(app_2._links.ContainsKey("image")); - } + // Should contain image link. + Assert.False(app_2._links.ContainsKey("image")); + } - [Fact] - public async Task Should_get_settings() - { - // STEP 1: Get initial settings. - var settings_0 = await _.Apps.GetSettingsAsync(_.AppName); + [Fact] + public async Task Should_get_settings() + { + // STEP 1: Get initial settings. + var settings_0 = await _.Apps.GetSettingsAsync(_.AppName); - Assert.NotEmpty(settings_0.Patterns); - } + Assert.NotEmpty(settings_0.Patterns); + } - [Fact] - public async Task Should_update_settings() + [Fact] + public async Task Should_update_settings() + { + // STEP 1: Update settings with new state. + var updateRequest = new UpdateAppSettingsDto { - // STEP 1: Update settings with new state. - var updateRequest = new UpdateAppSettingsDto + Patterns = new List<PatternDto> { - Patterns = new List<PatternDto> - { - new PatternDto { Name = "pattern", Regex = ".*" } - }, - Editors = new List<EditorDto> - { - new EditorDto { Name = "editor", Url = "http://squidex.io/path/to/editor" } - } - }; - - var settings_1 = await _.Apps.PutSettingsAsync(_.AppName, updateRequest); - - Assert.NotEmpty(settings_1.Patterns); - Assert.NotEmpty(settings_1.Editors); - - await Verify(settings_1) - .IgnoreMember<AppSettingsDto>(x => x.Version); - } + new PatternDto { Name = "pattern", Regex = ".*" } + }, + Editors = new List<EditorDto> + { + new EditorDto { Name = "editor", Url = "http://squidex.io/path/to/editor" } + } + }; + + var settings_1 = await _.Apps.PutSettingsAsync(_.AppName, updateRequest); + + Assert.NotEmpty(settings_1.Patterns); + Assert.NotEmpty(settings_1.Editors); + + await Verify(settings_1) + .IgnoreMember<AppSettingsDto>(x => x.Version); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTests.cs index 531b6d8c42..3959dc3c06 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTests.cs @@ -11,117 +11,116 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public sealed class AppWorkflowsTests : IClassFixture<ClientFixture> { - [UsesVerify] - public sealed class AppWorkflowsTests : IClassFixture<ClientFixture> - { - private readonly string appName = Guid.NewGuid().ToString(); - private readonly string name = Guid.NewGuid().ToString(); + private readonly string appName = Guid.NewGuid().ToString(); + private readonly string name = Guid.NewGuid().ToString(); - public ClientFixture _ { get; } + public ClientFixture _ { get; } - public AppWorkflowsTests(ClientFixture fixture) - { - _ = fixture; - } + public AppWorkflowsTests(ClientFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_create_workflow() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_create_workflow() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Create workflow. - var workflow = await CreateAsync(); + // STEP 1: Create workflow. + var workflow = await CreateAsync(); - Assert.NotNull(workflow); - Assert.NotNull(workflow.Name); - Assert.Equal(3, workflow.Steps.Count); + Assert.NotNull(workflow); + Assert.NotNull(workflow.Name); + Assert.Equal(3, workflow.Steps.Count); - await Verify(workflow); - } + await Verify(workflow); + } - [Fact] - public async Task Should_update_workflow() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_update_workflow() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 0: Create workflow. - var workflow = await CreateAsync(); + // STEP 0: Create workflow. + var workflow = await CreateAsync(); - // STEP 1: Update workflow. - var updateRequest = new UpdateWorkflowDto + // STEP 1: Update workflow. + var updateRequest = new UpdateWorkflowDto + { + Initial = "Draft", + Steps = new Dictionary<string, WorkflowStepDto> { - Initial = "Draft", - Steps = new Dictionary<string, WorkflowStepDto> + ["Draft"] = new WorkflowStepDto { - ["Draft"] = new WorkflowStepDto + Transitions = new Dictionary<string, WorkflowTransitionDto> { - Transitions = new Dictionary<string, WorkflowTransitionDto> - { - ["Published"] = new WorkflowTransitionDto() - } - }, - ["Published"] = new WorkflowStepDto(), + ["Published"] = new WorkflowTransitionDto() + } }, - Name = name - }; + ["Published"] = new WorkflowStepDto(), + }, + Name = name + }; - var workflows_2 = await _.Apps.PutWorkflowAsync(appName, workflow.Id, updateRequest); - var workflow_2 = workflows_2.Items.Find(x => x.Name == name); + var workflows_2 = await _.Apps.PutWorkflowAsync(appName, workflow.Id, updateRequest); + var workflow_2 = workflows_2.Items.Find(x => x.Name == name); - Assert.NotNull(workflow_2); - Assert.NotNull(workflow_2.Name); - Assert.Equal(2, workflow_2.Steps.Count); + Assert.NotNull(workflow_2); + Assert.NotNull(workflow_2.Name); + Assert.Equal(2, workflow_2.Steps.Count); - await Verify(workflows_2); - } + await Verify(workflows_2); + } - [Fact] - public async Task Should_delete_workflow() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_delete_workflow() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 0: Create workflow. - var workflow = await CreateAsync(); + // STEP 0: Create workflow. + var workflow = await CreateAsync(); - // STEP 1: Delete workflow. - var workflows_2 = await _.Apps.DeleteWorkflowAsync(appName, workflow.Id); + // STEP 1: Delete workflow. + var workflows_2 = await _.Apps.DeleteWorkflowAsync(appName, workflow.Id); - Assert.DoesNotContain(workflows_2.Items, x => x.Name == name); + Assert.DoesNotContain(workflows_2.Items, x => x.Name == name); - await Verify(workflows_2); - } + await Verify(workflows_2); + } - private async Task<WorkflowDto> CreateAsync() + private async Task<WorkflowDto> CreateAsync() + { + var createRequest = new AddWorkflowDto { - var createRequest = new AddWorkflowDto - { - Name = name - }; + Name = name + }; - var workflows = await _.Apps.PostWorkflowAsync(appName, createRequest); - var workflow = workflows.Items.Find(x => x.Name == name); + var workflows = await _.Apps.PostWorkflowAsync(appName, createRequest); + var workflow = workflows.Items.Find(x => x.Name == name); - return workflow; - } + return workflow; + } - private async Task CreateAppAsync() + private async Task CreateAppAsync() + { + var createRequest = new CreateAppDto { - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - await _.Apps.PostAppAsync(createRequest); - } + await _.Apps.PostAppAsync(createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs index b4a185579d..1850fc86d1 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs @@ -11,244 +11,243 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable CS0618 // Type or member is obsolete -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public class AssetFormatTests : IClassFixture<CreatedAppFixture> { - [UsesVerify] - public class AssetFormatTests : IClassFixture<CreatedAppFixture> + public CreatedAppFixture _ { get; } + + public AssetFormatTests(CreatedAppFixture fixture) { - public CreatedAppFixture _ { get; } + _ = fixture; + } - public AssetFormatTests(CreatedAppFixture fixture) - { - _ = fixture; - } + [Fact] + public async Task Should_upload_image_gif_without_extension() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_150kb.gif", "image/gif", Guid.NewGuid().ToString()); + + // Should parse image metadata. + Assert.True(asset.IsImage); + Assert.Equal(600L, (long)asset.PixelWidth); + Assert.Equal(600L, asset.Metadata["pixelWidth"]); + Assert.Equal(400L, (long)asset.PixelHeight); + Assert.Equal(400L, asset.Metadata["pixelHeight"]); + Assert.Equal(AssetType.Image, asset.Type); + + await Verify(asset) + .IgnoreMember<AssetDto>(x => x.FileName) + .IgnoreMember<AssetDto>(x => x.FileHash) + .IgnoreMember<AssetDto>(x => x.Slug); + } - [Fact] - public async Task Should_upload_image_gif_without_extension() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_150kb.gif", "image/gif", Guid.NewGuid().ToString()); - - // Should parse image metadata. - Assert.True(asset.IsImage); - Assert.Equal(600L, (long)asset.PixelWidth); - Assert.Equal(600L, asset.Metadata["pixelWidth"]); - Assert.Equal(400L, (long)asset.PixelHeight); - Assert.Equal(400L, asset.Metadata["pixelHeight"]); - Assert.Equal(AssetType.Image, asset.Type); - - await Verify(asset) - .IgnoreMember<AssetDto>(x => x.FileName) - .IgnoreMember<AssetDto>(x => x.FileHash) - .IgnoreMember<AssetDto>(x => x.Slug); - } + [Fact] + public async Task Should_upload_image_gif_and_resize() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_150kb.gif", "image/gif"); - [Fact] - public async Task Should_upload_image_gif_and_resize() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_150kb.gif", "image/gif"); + await AssertImageAsync(asset); + } - await AssertImageAsync(asset); - } + [Fact] + public async Task Should_upload_image_png_and_resize() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_400kb.png", "image/png"); - [Fact] - public async Task Should_upload_image_png_and_resize() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_400kb.png", "image/png"); + await AssertImageAsync(asset); + } - await AssertImageAsync(asset); - } + [Fact] + public async Task Should_upload_image_jpg_and_resize() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_62kb.jpg", "image/jpg"); - [Fact] - public async Task Should_upload_image_jpg_and_resize() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_62kb.jpg", "image/jpg"); + await AssertImageAsync(asset); - await AssertImageAsync(asset); + Assert.Equal(79L, asset.Metadata["imageQuality"]); + } - Assert.Equal(79L, asset.Metadata["imageQuality"]); - } + [Fact] + public async Task Should_upload_image_webp_and_resize() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_100kb.webp", "image/jpg"); - [Fact] - public async Task Should_upload_image_webp_and_resize() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_100kb.webp", "image/jpg"); + await AssertImageAsync(asset); + } - await AssertImageAsync(asset); - } + [Fact] + public async Task Should_upload_image_tiff_and_resize() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_400kb.tiff", "image/jpg"); - [Fact] - public async Task Should_upload_image_tiff_and_resize() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_400kb.tiff", "image/jpg"); + await AssertImageAsync(asset); + } - await AssertImageAsync(asset); - } + [Fact] + public async Task Should_upload_image_tga_and_resize() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_600kb.tga", "image/x-tga"); - [Fact] - public async Task Should_upload_image_tga_and_resize() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_600kb.tga", "image/x-tga"); + await AssertImageAsync(asset); + } - await AssertImageAsync(asset); - } + [Fact] + public async Task Should_upload_image_bmp_and_resize() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_700kb.bmp", "image/bmp"); - [Fact] - public async Task Should_upload_image_bmp_and_resize() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_700kb.bmp", "image/bmp"); + await AssertImageAsync(asset); + } - await AssertImageAsync(asset); - } + [Fact] + public async Task Should_upload_image_bmp_and_encode_to_webp() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_700kb.bmp", "image/bmp"); - [Fact] - public async Task Should_upload_image_bmp_and_encode_to_webp() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleImage_700kb.bmp", "image/bmp"); + var (size, type) = await GetReformattedLength(asset.Id, "WEBP"); - var (size, type) = await GetReformattedLength(asset.Id, "WEBP"); + Assert.True(size < asset.FileSize); + Assert.Equal("image/webp", type); - Assert.True(size < asset.FileSize); - Assert.Equal("image/webp", type); + await Verify(asset); + } - await Verify(asset); - } + private async Task AssertImageAsync(AssetDto asset) + { + await Verify(asset); - private async Task AssertImageAsync(AssetDto asset) - { - await Verify(asset); + // Should parse image metadata. + Assert.True(asset.IsImage); + Assert.Equal(600L, (long)asset.PixelWidth); + Assert.Equal(600L, asset.Metadata["pixelWidth"]); + Assert.Equal(400L, (long)asset.PixelHeight); + Assert.Equal(400L, asset.Metadata["pixelHeight"]); + Assert.Equal(AssetType.Image, asset.Type); - // Should parse image metadata. - Assert.True(asset.IsImage); - Assert.Equal(600L, (long)asset.PixelWidth); - Assert.Equal(600L, asset.Metadata["pixelWidth"]); - Assert.Equal(400L, (long)asset.PixelHeight); - Assert.Equal(400L, asset.Metadata["pixelHeight"]); - Assert.Equal(AssetType.Image, asset.Type); + var resized = await GetResizedLengthAsync(asset.Id, 100, 100); - var resized = await GetResizedLengthAsync(asset.Id, 100, 100); + Assert.True(resized < asset.FileSize * .25); + } - Assert.True(resized < asset.FileSize * .25); - } + [Fact] + public async Task Should_fix_orientation() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-wide-rotated.jpg", "image/jpg"); + + // Should parse image metadata and fix orientation. + Assert.True(asset.IsImage); + Assert.Equal(600L, (long)asset.PixelWidth); + Assert.Equal(600L, asset.Metadata["pixelWidth"]); + Assert.Equal(135L, (long)asset.PixelHeight); + Assert.Equal(135L, asset.Metadata["pixelHeight"]); + Assert.Equal(79L, asset.Metadata["imageQuality"]); + Assert.Equal(AssetType.Image, asset.Type); + + await Verify(asset); + } - [Fact] - public async Task Should_fix_orientation() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-wide-rotated.jpg", "image/jpg"); - - // Should parse image metadata and fix orientation. - Assert.True(asset.IsImage); - Assert.Equal(600L, (long)asset.PixelWidth); - Assert.Equal(600L, asset.Metadata["pixelWidth"]); - Assert.Equal(135L, (long)asset.PixelHeight); - Assert.Equal(135L, asset.Metadata["pixelHeight"]); - Assert.Equal(79L, asset.Metadata["imageQuality"]); - Assert.Equal(AssetType.Image, asset.Type); - - await Verify(asset); - } + [Fact] + public async Task Should_upload_audio_mp3() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleAudio_0.4mb.mp3", "audio/mp3"); - [Fact] - public async Task Should_upload_audio_mp3() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleAudio_0.4mb.mp3", "audio/mp3"); + // Should parse audio metadata. + Assert.False(asset.IsImage); + Assert.Equal("00:00:28.2708750", asset.Metadata["duration"]); + Assert.Equal(128L, asset.Metadata["audioBitrate"]); + Assert.Equal(2L, asset.Metadata["audioChannels"]); + Assert.Equal(44100L, asset.Metadata["audioSampleRate"]); + Assert.Equal(AssetType.Audio, asset.Type); - // Should parse audio metadata. - Assert.False(asset.IsImage); - Assert.Equal("00:00:28.2708750", asset.Metadata["duration"]); - Assert.Equal(128L, asset.Metadata["audioBitrate"]); - Assert.Equal(2L, asset.Metadata["audioChannels"]); - Assert.Equal(44100L, asset.Metadata["audioSampleRate"]); - Assert.Equal(AssetType.Audio, asset.Type); + await Verify(asset); + } - await Verify(asset); - } + [Fact] + public async Task Should_upload_video_mp4() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleVideo_1280x720_1mb.mp4", "audio/mp4"); + + // Should parse video metadata. + Assert.False(asset.IsImage); + Assert.Equal("00:00:05.3120000", asset.Metadata["duration"]); + Assert.Equal(384L, asset.Metadata["audioBitrate"]); + Assert.Equal(2L, asset.Metadata["audioChannels"]); + Assert.Equal(48000L, asset.Metadata["audioSampleRate"]); + Assert.Equal(1280L, asset.Metadata["videoWidth"]); + Assert.Equal(720L, asset.Metadata["videoHeight"]); + Assert.Equal(AssetType.Video, asset.Type); + + await Verify(asset); + } - [Fact] - public async Task Should_upload_video_mp4() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleVideo_1280x720_1mb.mp4", "audio/mp4"); - - // Should parse video metadata. - Assert.False(asset.IsImage); - Assert.Equal("00:00:05.3120000", asset.Metadata["duration"]); - Assert.Equal(384L, asset.Metadata["audioBitrate"]); - Assert.Equal(2L, asset.Metadata["audioChannels"]); - Assert.Equal(48000L, asset.Metadata["audioSampleRate"]); - Assert.Equal(1280L, asset.Metadata["videoWidth"]); - Assert.Equal(720L, asset.Metadata["videoHeight"]); - Assert.Equal(AssetType.Video, asset.Type); - - await Verify(asset); - } + [Fact] + public async Task Should_upload_video_mkv() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleVideo_1280x720_1mb.flv", "audio/webm"); - [Fact] - public async Task Should_upload_video_mkv() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleVideo_1280x720_1mb.flv", "audio/webm"); + // Should not parse yet. + Assert.Equal(AssetType.Unknown, asset.Type); - // Should not parse yet. - Assert.Equal(AssetType.Unknown, asset.Type); + await Verify(asset); + } - await Verify(asset); - } + [Fact] + public async Task Should_upload_video_flv() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleVideo_1280x720_1mb.flv", "audio/x-flv"); - [Fact] - public async Task Should_upload_video_flv() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleVideo_1280x720_1mb.flv", "audio/x-flv"); + // Should not parse yet. + Assert.Equal(AssetType.Unknown, asset.Type); - // Should not parse yet. - Assert.Equal(AssetType.Unknown, asset.Type); + await Verify(asset); + } - await Verify(asset); - } + [Fact] + public async Task Should_upload_video_3gp() + { + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleVideo_176x144_1mb.3gp", "audio/3gpp"); - [Fact] - public async Task Should_upload_video_3gp() - { - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/SampleVideo_176x144_1mb.3gp", "audio/3gpp"); + // Should not parse yet. + Assert.Equal(AssetType.Unknown, asset.Type); - // Should not parse yet. - Assert.Equal(AssetType.Unknown, asset.Type); + await Verify(asset); + } - await Verify(asset); - } + private async Task<long> GetResizedLengthAsync(string imageId, int width, int height) + { + var url = $"{_.ClientManager.GenerateImageUrl(imageId)}?width={width}&height={height}"; - private async Task<long> GetResizedLengthAsync(string imageId, int width, int height) + using (var httpClient = _.ClientManager.CreateHttpClient()) { - var url = $"{_.ClientManager.GenerateImageUrl(imageId)}?width={width}&height={height}"; + var response = await httpClient.GetAsync(url); - using (var httpClient = _.ClientManager.CreateHttpClient()) + await using (var stream = await response.Content.ReadAsStreamAsync()) { - var response = await httpClient.GetAsync(url); - - await using (var stream = await response.Content.ReadAsStreamAsync()) - { - var buffer = new MemoryStream(); + var buffer = new MemoryStream(); - await stream.CopyToAsync(buffer); + await stream.CopyToAsync(buffer); - return buffer.Length; - } + return buffer.Length; } } + } + + private async Task<(long, string)> GetReformattedLength(string imageId, string format) + { + var url = $"{_.ClientManager.GenerateImageUrl(imageId)}?format={format}"; - private async Task<(long, string)> GetReformattedLength(string imageId, string format) + using (var httpClient = _.ClientManager.CreateHttpClient()) { - var url = $"{_.ClientManager.GenerateImageUrl(imageId)}?format={format}"; + var response = await httpClient.GetAsync(url); - using (var httpClient = _.ClientManager.CreateHttpClient()) + await using (var stream = await response.Content.ReadAsStreamAsync()) { - var response = await httpClient.GetAsync(url); - - await using (var stream = await response.Content.ReadAsStreamAsync()) - { - var buffer = new MemoryStream(); + var buffer = new MemoryStream(); - await stream.CopyToAsync(buffer); + await stream.CopyToAsync(buffer); - return (buffer.Length, response.Content.Headers.ContentType.ToString()); - } + return (buffer.Length, response.Content.Headers.ContentType.ToString()); } } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs index 7436b321be..d9e042007a 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -13,52 +13,98 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public class AssetTests : IClassFixture<CreatedAppFixture> { - [UsesVerify] - public class AssetTests : IClassFixture<CreatedAppFixture> + private ProgressHandler progress = new ProgressHandler(); + + public CreatedAppFixture _ { get; } + + public AssetTests(CreatedAppFixture fixture) { - private ProgressHandler progress = new ProgressHandler(); + _ = fixture; + } - public CreatedAppFixture _ { get; } + [Fact] + public async Task Should_upload_asset() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - public AssetTests(CreatedAppFixture fixture) + await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) { - _ = fixture; + var downloaded = await _.DownloadAsync(asset_1); + + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); } - [Fact] - public async Task Should_upload_asset() + await Verify(asset_1); + } + + [Fact] + public async Task Should_upload_asset_using_tus() + { + // STEP 1: Create asset + var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); + + await using (fileParameter.Data) { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + await _.Assets.UploadAssetAsync(_.AppName, fileParameter, + progress.AsOptions()); + } - await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) - { - var downloaded = await _.DownloadAsync(asset_1); + Assert.NotEmpty(progress.Progress); + Assert.NotNull(progress.Asset); + Assert.Null(progress.Exception); - // Should dowload with correct size. - Assert.Equal(stream.Length, downloaded.Length); - } + await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open)) + { + var downloaded = await _.DownloadAsync(progress.Asset); - await Verify(asset_1); + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); } + } - [Fact] - public async Task Should_upload_asset_using_tus() + [Fact] + public async Task Should_upload_asset_using_tus_in_chunks() + { + for (var i = 0; i < 5; i++) { // STEP 1: Create asset + progress = new ProgressHandler(); + var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); - await using (fileParameter.Data) + var pausingStream = new PauseStream(fileParameter.Data, 0.5); + var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType); + + var numUploads = 0; + + await using (pausingFile.Data) { - await _.Assets.UploadAssetAsync(_.AppName, fileParameter, - progress.AsOptions()); + using var cts = new CancellationTokenSource(5000); + + while (progress.Asset == null && progress.Exception == null) + { + pausingStream.Reset(); + + await _.Assets.UploadAssetAsync(_.AppName, pausingFile, + progress.AsOptions(), cts.Token); + + await Task.Delay(50, cts.Token); + + numUploads++; + } } Assert.NotEmpty(progress.Progress); Assert.NotNull(progress.Asset); Assert.Null(progress.Exception); + Assert.True(numUploads > 1); await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open)) { @@ -68,156 +114,160 @@ await _.Assets.UploadAssetAsync(_.AppName, fileParameter, Assert.Equal(stream.Length, downloaded.Length); } } + } - [Fact] - public async Task Should_upload_asset_using_tus_in_chunks() - { - for (var i = 0; i < 5; i++) - { - // STEP 1: Create asset - progress = new ProgressHandler(); - - var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); + [Fact] + public async Task Should_upload_asset_with_custom_id() + { + var id = Guid.NewGuid().ToString(); - var pausingStream = new PauseStream(fileParameter.Data, 0.5); - var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType); + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", id: id); - var numUploads = 0; + Assert.Equal(id, asset_1.Id); - await using (pausingFile.Data) - { - using var cts = new CancellationTokenSource(5000); + await Verify(asset_1); + } - while (progress.Asset == null && progress.Exception == null) - { - pausingStream.Reset(); + [Fact] + public async Task Should_upload_asset_with_custom_id_using_tus() + { + var id = Guid.NewGuid().ToString(); - await _.Assets.UploadAssetAsync(_.AppName, pausingFile, - progress.AsOptions(), cts.Token); + // STEP 1: Create asset + var fileParameter = FileParameter.FromPath("Assets/logo-squared.png"); - await Task.Delay(50, cts.Token); + await _.Assets.UploadAssetAsync(_.AppName, fileParameter, + progress.AsOptions(id)); - numUploads++; - } - } + Assert.Equal(id, progress.Asset?.Id); + } - Assert.NotEmpty(progress.Progress); - Assert.NotNull(progress.Asset); - Assert.Null(progress.Exception); - Assert.True(numUploads > 1); + [Fact] + public async Task Should_not_create_asset_with_custom_id_twice() + { + var id = Guid.NewGuid().ToString(); - await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open)) - { - var downloaded = await _.DownloadAsync(progress.Asset); + // STEP 1: Create asset + await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", id: id); - // Should dowload with correct size. - Assert.Equal(stream.Length, downloaded.Length); - } - } - } - [Fact] - public async Task Should_upload_asset_with_custom_id() + // STEP 2: Create a new item with a custom id. + var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => { - var id = Guid.NewGuid().ToString(); + return _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", id: id); + }); - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", id: id); + Assert.Equal(409, ex.StatusCode); + } - Assert.Equal(id, asset_1.Id); + [Fact] + public async Task Should_not_create_very_big_asset() + { + // STEP 1: Create small asset + await _.Assets.UploadRandomFileAsync(_.AppName, 1_000_000); - await Verify(asset_1); - } - [Fact] - public async Task Should_upload_asset_with_custom_id_using_tus() + // STEP 2: Create big asset + var ex = await Assert.ThrowsAnyAsync<Exception>(() => { - var id = Guid.NewGuid().ToString(); + return _.Assets.UploadRandomFileAsync(_.AppName, 10_000_000); + }); - // STEP 1: Create asset - var fileParameter = FileParameter.FromPath("Assets/logo-squared.png"); + // Client library cannot catch this exception properly. + Assert.True(ex is HttpRequestException || ex is SquidexManagementException); + } - await _.Assets.UploadAssetAsync(_.AppName, fileParameter, - progress.AsOptions(id)); + [Fact] + public async Task Should_replace_asset() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - Assert.Equal(id, progress.Asset?.Id); - } - [Fact] - public async Task Should_not_create_asset_with_custom_id_twice() + // STEP 2: Reupload asset + var asset_2 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-wide.png", asset_1); + + await using (var stream = new FileStream("Assets/logo-wide.png", FileMode.Open)) { - var id = Guid.NewGuid().ToString(); + var downloaded = await _.DownloadAsync(asset_2); - // STEP 1: Create asset - await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", id: id); + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); + } + await Verify(asset_2); + } - // STEP 2: Create a new item with a custom id. - var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => - { - return _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", id: id); - }); + [Fact] + public async Task Should_replace_asset_using_tus() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - Assert.Equal(409, ex.StatusCode); - } - [Fact] - public async Task Should_not_create_very_big_asset() + // STEP 2: Reupload asset + var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); + + await using (fileParameter.Data) { - // STEP 1: Create small asset - await _.Assets.UploadRandomFileAsync(_.AppName, 1_000_000); + await _.Assets.UploadAssetAsync(_.AppName, fileParameter, + progress.AsOptions(asset_1.Id)); + } + Assert.NotNull(progress.Asset); + Assert.NotEmpty(progress.Progress); + Assert.Null(progress.Exception); - // STEP 2: Create big asset - var ex = await Assert.ThrowsAnyAsync<Exception>(() => - { - return _.Assets.UploadRandomFileAsync(_.AppName, 10_000_000); - }); + await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open)) + { + var downloaded = await _.DownloadAsync(progress.Asset); - // Client library cannot catch this exception properly. - Assert.True(ex is HttpRequestException || ex is SquidexManagementException); + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); } + } - [Fact] - public async Task Should_replace_asset() + [Fact] + public async Task Should_replace_asset_using_tus_in_chunks() + { + for (var i = 0; i < 5; i++) { // STEP 1: Create asset var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); // STEP 2: Reupload asset - var asset_2 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-wide.png", asset_1); + progress = new ProgressHandler(); - await using (var stream = new FileStream("Assets/logo-wide.png", FileMode.Open)) - { - var downloaded = await _.DownloadAsync(asset_2); + var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); - // Should dowload with correct size. - Assert.Equal(stream.Length, downloaded.Length); - } + var pausingStream = new PauseStream(fileParameter.Data, 0.5); + var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType); - await Verify(asset_2); - } + var numUploads = 0; - [Fact] - public async Task Should_replace_asset_using_tus() - { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + await using (pausingFile.Data) + { + using var cts = new CancellationTokenSource(5000); + while (progress.Asset == null && progress.Exception == null) + { + pausingStream.Reset(); - // STEP 2: Reupload asset - var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); + await _.Assets.UploadAssetAsync(_.AppName, pausingFile, + progress.AsOptions(asset_1.Id), cts.Token); - await using (fileParameter.Data) - { - await _.Assets.UploadAssetAsync(_.AppName, fileParameter, - progress.AsOptions(asset_1.Id)); + await Task.Delay(50, cts.Token); + + numUploads++; + } } - Assert.NotNull(progress.Asset); Assert.NotEmpty(progress.Progress); + Assert.NotNull(progress.Asset); Assert.Null(progress.Exception); + Assert.True(numUploads > 1); await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open)) { @@ -227,540 +277,489 @@ await _.Assets.UploadAssetAsync(_.AppName, fileParameter, Assert.Equal(stream.Length, downloaded.Length); } } + } - [Fact] - public async Task Should_replace_asset_using_tus_in_chunks() - { - for (var i = 0; i < 5; i++) - { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - - - // STEP 2: Reupload asset - progress = new ProgressHandler(); - - var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); - - var pausingStream = new PauseStream(fileParameter.Data, 0.5); - var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType); + [Fact] + public async Task Should_annote_asset_file_name() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - var numUploads = 0; - await using (pausingFile.Data) - { - using var cts = new CancellationTokenSource(5000); - - while (progress.Asset == null && progress.Exception == null) - { - pausingStream.Reset(); + // STEP 2: Annotate file name. + var fileNameRequest = new AnnotateAssetDto + { + FileName = "My Image" + }; - await _.Assets.UploadAssetAsync(_.AppName, pausingFile, - progress.AsOptions(asset_1.Id), cts.Token); + var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, fileNameRequest); - await Task.Delay(50, cts.Token); + // Should provide updated file name. + Assert.Equal(fileNameRequest.FileName, asset_2.FileName); - numUploads++; - } - } + await Verify(asset_2); + } - Assert.NotEmpty(progress.Progress); - Assert.NotNull(progress.Asset); - Assert.Null(progress.Exception); - Assert.True(numUploads > 1); + [Fact] + public async Task Should_annote_asset_metadata() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open)) - { - var downloaded = await _.DownloadAsync(progress.Asset); - // Should dowload with correct size. - Assert.Equal(stream.Length, downloaded.Length); - } - } - } - - [Fact] - public async Task Should_annote_asset_file_name() + // STEP 2: Annotate metadata. + var metadataRequest = new AnnotateAssetDto { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + Metadata = new Dictionary<string, object> + { + ["pw"] = 100L, + ["ph"] = 20L + } + }; + var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest); - // STEP 2: Annotate file name. - var fileNameRequest = new AnnotateAssetDto - { - FileName = "My Image" - }; + // Should provide metadata. + Assert.Equal(metadataRequest.Metadata, asset_2.Metadata); - var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, fileNameRequest); + await Verify(asset_2); + } - // Should provide updated file name. - Assert.Equal(fileNameRequest.FileName, asset_2.FileName); + [Fact] + public async Task Should_annote_asset_slug() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - await Verify(asset_2); - } - [Fact] - public async Task Should_annote_asset_metadata() + // STEP 2: Annotate slug. + var slugRequest = new AnnotateAssetDto { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + Slug = "my-image" + }; + var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, slugRequest); - // STEP 2: Annotate metadata. - var metadataRequest = new AnnotateAssetDto - { - Metadata = new Dictionary<string, object> - { - ["pw"] = 100L, - ["ph"] = 20L - } - }; + // Should provide updated slug. + Assert.Equal(slugRequest.Slug, asset_2.Slug); - var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest); + await Verify(asset_2); + } - // Should provide metadata. - Assert.Equal(metadataRequest.Metadata, asset_2.Metadata); + [Fact] + public async Task Should_annote_asset_tags() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - await Verify(asset_2); - } - [Fact] - public async Task Should_annote_asset_slug() + // STEP 2: Annotate tags. + var tagsRequest = new AnnotateAssetDto { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - - - // STEP 2: Annotate slug. - var slugRequest = new AnnotateAssetDto + Tags = new List<string> { - Slug = "my-image" - }; - - var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, slugRequest); + "tag1", + "tag2" + } + }; - // Should provide updated slug. - Assert.Equal(slugRequest.Slug, asset_2.Slug); + var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, tagsRequest); - await Verify(asset_2); - } + // Should provide updated tags. + Assert.Equal(tagsRequest.Tags, asset_2.Tags); - [Fact] - public async Task Should_annote_asset_tags() - { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + await Verify(asset_2); + } + [Fact] + public async Task Should_annotate_asset_in_parallel() + { + var tag1 = $"tag_{Guid.NewGuid()}"; + var tag2 = $"tag_{Guid.NewGuid()}"; - // STEP 2: Annotate tags. - var tagsRequest = new AnnotateAssetDto + var metadataRequest = new AnnotateAssetDto + { + Tags = new List<string> { - Tags = new List<string> - { - "tag1", - "tag2" - } - }; + tag1, + tag2 + } + }; - var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, tagsRequest); - // Should provide updated tags. - Assert.Equal(tagsRequest.Tags, asset_2.Tags); + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - await Verify(asset_2); - } - [Fact] - public async Task Should_annotate_asset_in_parallel() - { - var tag1 = $"tag_{Guid.NewGuid()}"; - var tag2 = $"tag_{Guid.NewGuid()}"; + var numErrors = 0; + var numSuccess = 0; - var metadataRequest = new AnnotateAssetDto + // STEP 3: Make parallel upserts. + await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) => + { + try { - Tags = new List<string> - { - tag1, - tag2 - } - }; + await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest); + Interlocked.Increment(ref numSuccess); + } + catch (SquidexManagementException ex) when (ex.StatusCode is 409 or 412) + { + Interlocked.Increment(ref numErrors); + return; + } + }); - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + // At least some errors and success should have happened. + Assert.True(numErrors >= 0); + Assert.True(numSuccess >= 0); - var numErrors = 0; - var numSuccess = 0; + // STEP 3: Make an normal update to ensure nothing is corrupt. + var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest); - // STEP 3: Make parallel upserts. - await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) => - { - try - { - await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest); - Interlocked.Increment(ref numSuccess); - } - catch (SquidexManagementException ex) when (ex.StatusCode is 409 or 412) - { - Interlocked.Increment(ref numErrors); - return; - } - }); + // STEP 4: Check tags + var tags = await _.Assets.WaitForTagsAsync(_.AppName, tag1, TimeSpan.FromMinutes(2)); - // At least some errors and success should have happened. - Assert.True(numErrors >= 0); - Assert.True(numSuccess >= 0); + Assert.Contains(tag1, tags); + Assert.Contains(tag2, tags); + Assert.Equal(1, tags[tag1]); + Assert.Equal(1, tags[tag2]); + await Verify(asset_2) + .IgnoreMember<AssetDto>(x => x.Tags); + } - // STEP 3: Make an normal update to ensure nothing is corrupt. - var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest); + [Fact] + public async Task Should_protect_asset() + { + var fileName = $"{Guid.NewGuid()}.png"; + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - // STEP 4: Check tags - var tags = await _.Assets.WaitForTagsAsync(_.AppName, tag1, TimeSpan.FromMinutes(2)); - Assert.Contains(tag1, tags); - Assert.Contains(tag2, tags); - Assert.Equal(1, tags[tag1]); - Assert.Equal(1, tags[tag2]); + // STEP 2: Download asset + await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var downloaded = await _.DownloadAsync(asset_1); - await Verify(asset_2) - .IgnoreMember<AssetDto>(x => x.Tags); + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); } - [Fact] - public async Task Should_protect_asset() + + // STEP 4: Protect asset + var protectRequest = new AnnotateAssetDto { - var fileName = $"{Guid.NewGuid()}.png"; + IsProtected = true + }; - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, protectRequest); - // STEP 2: Download asset - await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) - { - var downloaded = await _.DownloadAsync(asset_1); - - // Should dowload with correct size. - Assert.Equal(stream.Length, downloaded.Length); - } - + // STEP 5: Download asset with authentication. + await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var downloaded = new MemoryStream(); - // STEP 4: Protect asset - var protectRequest = new AnnotateAssetDto + using (var assetStream = await _.Assets.GetAssetContentBySlugAsync(_.AppName, asset_2.Id, string.Empty)) { - IsProtected = true - }; + await assetStream.Stream.CopyToAsync(downloaded); + } - var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, protectRequest); + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); + } - // STEP 5: Download asset with authentication. - await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + // STEP 5: Download asset without key. + await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var ex = await Assert.ThrowsAnyAsync<HttpRequestException>(() => { - var downloaded = new MemoryStream(); - - using (var assetStream = await _.Assets.GetAssetContentBySlugAsync(_.AppName, asset_2.Id, string.Empty)) - { - await assetStream.Stream.CopyToAsync(downloaded); - } + return _.DownloadAsync(asset_1); + }); - // Should dowload with correct size. - Assert.Equal(stream.Length, downloaded.Length); - } + // Should return 403 when not authenticated. + Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); + } - // STEP 5: Download asset without key. - await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + // STEP 6: Download asset without key and version. + await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var ex = await Assert.ThrowsAnyAsync<HttpRequestException>(() => { - var ex = await Assert.ThrowsAnyAsync<HttpRequestException>(() => - { - return _.DownloadAsync(asset_1); - }); - - // Should return 403 when not authenticated. - Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); - } + return _.DownloadAsync(asset_1, 0); + }); + // Should return 403 when not authenticated. + Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); + } - // STEP 6: Download asset without key and version. - await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) - { - var ex = await Assert.ThrowsAnyAsync<HttpRequestException>(() => - { - return _.DownloadAsync(asset_1, 0); - }); + await Verify(asset_2); + } - // Should return 403 when not authenticated. - Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); - } + [Fact] + public async Task Should_query_asset_by_metadata() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - await Verify(asset_2); - } - [Fact] - public async Task Should_query_asset_by_metadata() + // STEP 2: Query asset by pixel width. + var assets_1 = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + Filter = "metadata/pixelWidth eq 600" + }); + Assert.Contains(assets_1.Items, x => x.Id == asset_1.Id); - // STEP 2: Query asset by pixel width. - var assets_1 = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery - { - Filter = "metadata/pixelWidth eq 600" - }); - Assert.Contains(assets_1.Items, x => x.Id == asset_1.Id); + // STEP 3: Add custom metadata. + asset_1.Metadata["custom"] = "foo"; + await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, new AnnotateAssetDto + { + Metadata = asset_1.Metadata + }); - // STEP 3: Add custom metadata. - asset_1.Metadata["custom"] = "foo"; - await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, new AnnotateAssetDto - { - Metadata = asset_1.Metadata - }); + // STEP 4: Query asset by custom metadata + var assets_2 = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery + { + Filter = "metadata/custom eq 'foo'" + }); + Assert.Contains(assets_2.Items, x => x.Id == asset_1.Id); + } - // STEP 4: Query asset by custom metadata - var assets_2 = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery - { - Filter = "metadata/custom eq 'foo'" - }); + [Fact] + public async Task Should_query_asset_by_root_folder() + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - Assert.Contains(assets_2.Items, x => x.Id == asset_1.Id); - } - [Fact] - public async Task Should_query_asset_by_root_folder() + // STEP 2: Query asset by root folder. + var assets_1 = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + ParentId = Guid.Empty.ToString() + }); + Assert.Contains(assets_1.Items, x => x.Id == asset_1.Id); + } - // STEP 2: Query asset by root folder. - var assets_1 = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery - { - ParentId = Guid.Empty.ToString() - }); + [Fact] + public async Task Should_query_asset_by_subfolder() + { + // STEP 1: Create asset folder + var folderRequest = new CreateAssetFolderDto + { + FolderName = "sub" + }; - Assert.Contains(assets_1.Items, x => x.Id == asset_1.Id); - } + var folder = await _.Assets.PostAssetFolderAsync(_.AppName, folderRequest); - [Fact] - public async Task Should_query_asset_by_subfolder() - { - // STEP 1: Create asset folder - var folderRequest = new CreateAssetFolderDto - { - FolderName = "sub" - }; - var folder = await _.Assets.PostAssetFolderAsync(_.AppName, folderRequest); + // STEP 1: Create asset in folder + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", parentId: folder.Id); - // STEP 1: Create asset in folder - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", parentId: folder.Id); + // STEP 2: Query asset by root folder. + var assets_1 = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery + { + ParentId = folder.Id + }); + Assert.Single(assets_1.Items, x => x.Id == asset_1.Id); + } - // STEP 2: Query asset by root folder. - var assets_1 = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery - { - ParentId = folder.Id - }); + [Fact] + public async Task Should_delete_recursively() + { + // STEP 1: Create asset folder + var createRequest1 = new CreateAssetFolderDto + { + FolderName = "folder1" + }; + + var folder_1 = await _.Assets.PostAssetFolderAsync(_.AppName, createRequest1); - Assert.Single(assets_1.Items, x => x.Id == asset_1.Id); - } - [Fact] - public async Task Should_delete_recursively() + // STEP 2: Create nested asset folder + var createRequest2 = new CreateAssetFolderDto { - // STEP 1: Create asset folder - var createRequest1 = new CreateAssetFolderDto - { - FolderName = "folder1" - }; + FolderName = "subfolder", + // Reference the parent folder by Id, so it must exist first. + ParentId = folder_1.Id + }; - var folder_1 = await _.Assets.PostAssetFolderAsync(_.AppName, createRequest1); + var folder_2 = await _.Assets.PostAssetFolderAsync(_.AppName, createRequest2); - // STEP 2: Create nested asset folder - var createRequest2 = new CreateAssetFolderDto - { - FolderName = "subfolder", - // Reference the parent folder by Id, so it must exist first. - ParentId = folder_1.Id - }; + // STEP 3: Create asset in folder + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", null, folder_2.Id); - var folder_2 = await _.Assets.PostAssetFolderAsync(_.AppName, createRequest2); + // STEP 4: Create asset outside folder + var asset_2 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - // STEP 3: Create asset in folder - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png", null, folder_2.Id); + // STEP 5: Delete folder. + await _.Assets.DeleteAssetFolderAsync(_.AppName, folder_1.Id); - // STEP 4: Create asset outside folder - var asset_2 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + // Ensure that asset in folder is deleted. + Assert.True(await _.Assets.WaitForDeletionAsync(_.AppName, asset_1.Id, TimeSpan.FromSeconds(30))); + // Ensure that other asset is not deleted. + Assert.NotNull(await _.Assets.GetAssetAsync(_.AppName, asset_2.Id)); + } - // STEP 5: Delete folder. - await _.Assets.DeleteAssetFolderAsync(_.AppName, folder_1.Id); + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Should_delete_asset(bool permanent) + { + // STEP 1: Create asset + var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - // Ensure that asset in folder is deleted. - Assert.True(await _.Assets.WaitForDeletionAsync(_.AppName, asset_1.Id, TimeSpan.FromSeconds(30))); - // Ensure that other asset is not deleted. - Assert.NotNull(await _.Assets.GetAssetAsync(_.AppName, asset_2.Id)); - } + // STEP 2: Delete asset + await _.Assets.DeleteAssetAsync(_.AppName, asset.Id, permanent: permanent); - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task Should_delete_asset(bool permanent) + // Should return 404 when asset deleted. + var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => { - // STEP 1: Create asset - var asset = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + return _.Assets.GetAssetAsync(_.AppName, asset.Id); + }); + Assert.Equal(404, ex.StatusCode); - // STEP 2: Delete asset - await _.Assets.DeleteAssetAsync(_.AppName, asset.Id, permanent: permanent); - // Should return 404 when asset deleted. - var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => - { - return _.Assets.GetAssetAsync(_.AppName, asset.Id); - }); + // STEP 3: Retrieve all items and ensure that the deleted item does not exist. + var updated = await _.Assets.GetAssetsAsync(_.AppName, (AssetQuery)null); - Assert.Equal(404, ex.StatusCode); + Assert.DoesNotContain(updated.Items, x => x.Id == asset.Id); - // STEP 3: Retrieve all items and ensure that the deleted item does not exist. - var updated = await _.Assets.GetAssetsAsync(_.AppName, (AssetQuery)null); + // STEP 4: Retrieve all deleted items and check if found. + var deleted = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery + { + Filter = "isDeleted eq true" + }); - Assert.DoesNotContain(updated.Items, x => x.Id == asset.Id); + Assert.Equal(!permanent, deleted.Items.Any(x => x.Id == asset.Id)); + } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Should_recreate_deleted_asset(bool permanent) + { + // STEP 1: Create asset + var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); - // STEP 4: Retrieve all deleted items and check if found. - var deleted = await _.Assets.GetAssetsAsync(_.AppName, new AssetQuery - { - Filter = "isDeleted eq true" - }); - Assert.Equal(!permanent, deleted.Items.Any(x => x.Id == asset.Id)); - } + // STEP 2: Delete asset + await _.Assets.DeleteAssetAsync(_.AppName, asset_1.Id, permanent: permanent); - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task Should_recreate_deleted_asset(bool permanent) - { - // STEP 1: Create asset - var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); + // STEP 3: Recreate asset + var asset_2 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-wide.png", "image/png"); - // STEP 2: Delete asset - await _.Assets.DeleteAssetAsync(_.AppName, asset_1.Id, permanent: permanent); + Assert.NotEqual(asset_1.FileSize, asset_2.FileSize); + } + public class ProgressHandler : IAssetProgressHandler + { + public string FileId { get; private set; } - // STEP 3: Recreate asset - var asset_2 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-wide.png", "image/png"); + public List<int> Progress { get; } = new List<int>(); - Assert.NotEqual(asset_1.FileSize, asset_2.FileSize); - } + public Exception Exception { get; private set; } - public class ProgressHandler : IAssetProgressHandler - { - public string FileId { get; private set; } + public AssetDto Asset { get; private set; } - public List<int> Progress { get; } = new List<int>(); - - public Exception Exception { get; private set; } + public AssetUploadOptions AsOptions(string id = null) + { + var options = default(AssetUploadOptions); + options.ProgressHandler = this; + options.FileId = FileId; + options.Id = id; - public AssetDto Asset { get; private set; } + return options; + } - public AssetUploadOptions AsOptions(string id = null) - { - var options = default(AssetUploadOptions); - options.ProgressHandler = this; - options.FileId = FileId; - options.Id = id; + public Task OnCompletedAsync(AssetUploadCompletedEvent @event, + CancellationToken ct) + { + Asset = @event.Asset; + return Task.CompletedTask; + } - return options; - } + public Task OnCreatedAsync(AssetUploadCreatedEvent @event, + CancellationToken ct) + { + FileId = @event.FileId; + return Task.CompletedTask; + } - public Task OnCompletedAsync(AssetUploadCompletedEvent @event, - CancellationToken ct) - { - Asset = @event.Asset; - return Task.CompletedTask; - } + public Task OnProgressAsync(AssetUploadProgressEvent @event, + CancellationToken ct) + { + Progress.Add(@event.Progress); + return Task.CompletedTask; + } - public Task OnCreatedAsync(AssetUploadCreatedEvent @event, - CancellationToken ct) + public Task OnFailedAsync(AssetUploadExceptionEvent @event, + CancellationToken ct) + { + if (!@event.Exception.ToString().Contains("PAUSED", StringComparison.OrdinalIgnoreCase)) { - FileId = @event.FileId; - return Task.CompletedTask; + Exception = @event.Exception; } - public Task OnProgressAsync(AssetUploadProgressEvent @event, - CancellationToken ct) - { - Progress.Add(@event.Progress); - return Task.CompletedTask; - } + return Task.CompletedTask; + } + } - public Task OnFailedAsync(AssetUploadExceptionEvent @event, - CancellationToken ct) - { - if (!@event.Exception.ToString().Contains("PAUSED", StringComparison.OrdinalIgnoreCase)) - { - Exception = @event.Exception; - } + public class PauseStream : DelegateStream + { + private readonly double pauseAfter = 1; + private int totalRead; - return Task.CompletedTask; - } + public PauseStream(Stream innerStream, double pauseAfter) + : base(innerStream) + { + this.pauseAfter = pauseAfter; } - public class PauseStream : DelegateStream + public void Reset() { - private readonly double pauseAfter = 1; - private int totalRead; + totalRead = 0; + } - public PauseStream(Stream innerStream, double pauseAfter) - : base(innerStream) + public override async ValueTask<int> ReadAsync(Memory<byte> buffer, + CancellationToken cancellationToken = default) + { + if (Position >= Length) { - this.pauseAfter = pauseAfter; + return 0; } - public void Reset() + if (totalRead >= Length * pauseAfter) { - totalRead = 0; + throw new InvalidOperationException("PAUSED"); } - public override async ValueTask<int> ReadAsync(Memory<byte> buffer, - CancellationToken cancellationToken = default) - { - if (Position >= Length) - { - return 0; - } - - if (totalRead >= Length * pauseAfter) - { - throw new InvalidOperationException("PAUSED"); - } + var bytesRead = await base.ReadAsync(buffer, cancellationToken); - var bytesRead = await base.ReadAsync(buffer, cancellationToken); + totalRead += bytesRead; - totalRead += bytesRead; - - return bytesRead; - } + return bytesRead; } } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs index 8c3500a5ae..6a89c1e442 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs @@ -13,160 +13,159 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class BackupTests : IClassFixture<ClientFixture> { - public class BackupTests : IClassFixture<ClientFixture> + private readonly string appName = Guid.NewGuid().ToString(); + private readonly string schemaName = $"schema-{Guid.NewGuid()}"; + + public ClientFixture _ { get; } + + public BackupTests(ClientFixture fixture) { - private readonly string appName = Guid.NewGuid().ToString(); - private readonly string schemaName = $"schema-{Guid.NewGuid()}"; + _ = fixture; + } - public ClientFixture _ { get; } + [Fact] + public async Task Should_backup_and_restore_app() + { + // Load the backup from another URL, because the public URL is might not be accessible for the server. + var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.Url); - public BackupTests(ClientFixture fixture) - { - _ = fixture; - } + var appNameRestore = $"{appName}-restore"; - [Fact] - public async Task Should_backup_and_restore_app() + // STEP 1: Create app + var createRequest = new CreateAppDto { - // Load the backup from another URL, because the public URL is might not be accessible for the server. - var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.Url); - - var appNameRestore = $"{appName}-restore"; + Name = appName + }; - // STEP 1: Create app - var createRequest = new CreateAppDto - { - Name = appName - }; + await _.Apps.PostAppAsync(createRequest); - await _.Apps.PostAppAsync(createRequest); + // STEP 2: Prepare app. + await PrepareAppAsync(appName); - // STEP 2: Prepare app. - await PrepareAppAsync(appName); + // STEP 3: Create backup + await _.Backups.PostBackupAsync(appName); - // STEP 3: Create backup - await _.Backups.PostBackupAsync(appName); + var backups = await _.Backups.WaitForBackupsAsync(appName, x => x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); + var backup = backups.FirstOrDefault(x => x.Status is JobStatus.Completed or JobStatus.Failed); - var backups = await _.Backups.WaitForBackupsAsync(appName, x => x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); - var backup = backups.FirstOrDefault(x => x.Status is JobStatus.Completed or JobStatus.Failed); + Assert.Equal(JobStatus.Completed, backup?.Status); - Assert.Equal(JobStatus.Completed, backup?.Status); + // STEP 4: Restore backup + var uri = new Uri(new Uri(backupUrl), backup._links["download"].Href); - // STEP 4: Restore backup - var uri = new Uri(new Uri(backupUrl), backup._links["download"].Href); + var restoreRequest = new RestoreRequestDto + { + Url = uri, + // Choose a new app name, because the old one is deleted. + Name = appNameRestore + }; - var restoreRequest = new RestoreRequestDto - { - Url = uri, - // Choose a new app name, because the old one is deleted. - Name = appNameRestore - }; + await _.Backups.PostRestoreJobAsync(restoreRequest); - await _.Backups.PostRestoreJobAsync(restoreRequest); + // STEP 5: Wait for the backup. + var restore = await _.Backups.WaitForRestoreAsync(x => x.Url == uri && x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); - // STEP 5: Wait for the backup. - var restore = await _.Backups.WaitForRestoreAsync(x => x.Url == uri && x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); + Assert.Equal(JobStatus.Completed, restore?.Status); + } - Assert.Equal(JobStatus.Completed, restore?.Status); - } + [Fact] + public async Task Should_backup_and_restore_app_with_deleted_app() + { + // Load the backup from another URL, because the public URL is might not be accessible for the server. + var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.Url); - [Fact] - public async Task Should_backup_and_restore_app_with_deleted_app() + // STEP 1: Create app + var createRequest = new CreateAppDto { - // Load the backup from another URL, because the public URL is might not be accessible for the server. - var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.Url); + Name = appName + }; - // STEP 1: Create app - var createRequest = new CreateAppDto - { - Name = appName - }; + await _.Apps.PostAppAsync(createRequest); - await _.Apps.PostAppAsync(createRequest); + // STEP 2: Prepare app. + await PrepareAppAsync(appName); - // STEP 2: Prepare app. - await PrepareAppAsync(appName); + // STEP 3: Create backup + await _.Backups.PostBackupAsync(appName); - // STEP 3: Create backup - await _.Backups.PostBackupAsync(appName); + var backups = await _.Backups.WaitForBackupsAsync(appName, x => x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); + var backup = backups.FirstOrDefault(x => x.Status is JobStatus.Completed or JobStatus.Failed); - var backups = await _.Backups.WaitForBackupsAsync(appName, x => x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); - var backup = backups.FirstOrDefault(x => x.Status is JobStatus.Completed or JobStatus.Failed); + Assert.Equal(JobStatus.Completed, backup?.Status); - Assert.Equal(JobStatus.Completed, backup?.Status); + // STEP 4: Delete app + await _.Apps.DeleteAppAsync(appName); - // STEP 4: Delete app - await _.Apps.DeleteAppAsync(appName); + // STEP 5: Restore backup + var uri = new Uri(new Uri(backupUrl), backup._links["download"].Href); - // STEP 5: Restore backup - var uri = new Uri(new Uri(backupUrl), backup._links["download"].Href); - - var restoreRequest = new RestoreRequestDto - { - Url = uri, - // Restore the old app name, because it has been deleted anyway. - Name = appName - }; + var restoreRequest = new RestoreRequestDto + { + Url = uri, + // Restore the old app name, because it has been deleted anyway. + Name = appName + }; - await _.Backups.PostRestoreJobAsync(restoreRequest); + await _.Backups.PostRestoreJobAsync(restoreRequest); - // STEP 6: Wait for the backup. - var restore = await _.Backups.WaitForRestoreAsync(x => x.Url == uri && x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); + // STEP 6: Wait for the backup. + var restore = await _.Backups.WaitForRestoreAsync(x => x.Url == uri && x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); - Assert.Equal(JobStatus.Completed, restore?.Status); - } + Assert.Equal(JobStatus.Completed, restore?.Status); + } - private async Task PrepareAppAsync(string appName) - { - // Create a test schema. - await TestEntity.CreateSchemaAsync(_.Schemas, appName, schemaName); + private async Task PrepareAppAsync(string appName) + { + // Create a test schema. + await TestEntity.CreateSchemaAsync(_.Schemas, appName, schemaName); - var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(appName, schemaName); + var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(appName, schemaName); - await contents.CreateAsync(new TestEntityData - { - Number = 1 - }); + await contents.CreateAsync(new TestEntityData + { + Number = 1 + }); - // Upload a test asset - var fileInfo = new FileInfo("Assets/logo-squared.png"); + // Upload a test asset + var fileInfo = new FileInfo("Assets/logo-squared.png"); - await using (var stream = fileInfo.OpenRead()) - { - var upload = new FileParameter(stream, fileInfo.Name, "image/png"); + await using (var stream = fileInfo.OpenRead()) + { + var upload = new FileParameter(stream, fileInfo.Name, "image/png"); - await _.Assets.PostAssetAsync(appName, file: upload); - } + await _.Assets.PostAssetAsync(appName, file: upload); + } - // Create a workflow - var workflowRequest = new AddWorkflowDto - { - Name = appName - }; + // Create a workflow + var workflowRequest = new AddWorkflowDto + { + Name = appName + }; - await _.Apps.PostWorkflowAsync(appName, workflowRequest); + await _.Apps.PostWorkflowAsync(appName, workflowRequest); - // Create a language - var languageRequest = new AddLanguageDto - { - Language = "de" - }; + // Create a language + var languageRequest = new AddLanguageDto + { + Language = "de" + }; - await _.Apps.PostLanguageAsync(appName, languageRequest); - } + await _.Apps.PostLanguageAsync(appName, languageRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs index c542174215..f41e5c044b 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs @@ -10,90 +10,89 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[Trait("Category", "NotAutomated")] +public class CDNTests : IClassFixture<ClientCloudFixture> { - [Trait("Category", "NotAutomated")] - public class CDNTests : IClassFixture<ClientCloudFixture> - { - public ClientCloudFixture _ { get; } + public ClientCloudFixture _ { get; } - public CDNTests(ClientCloudFixture fixture) - { - _ = fixture; - } + public CDNTests(ClientCloudFixture fixture) + { + _ = fixture; + } - [Fact] - public void Should_provide_asset_url_from_cdn() - { - var id = "ef4286f9-8b1d-4dda-bd52-c5bd191c47bb"; + [Fact] + public void Should_provide_asset_url_from_cdn() + { + var id = "ef4286f9-8b1d-4dda-bd52-c5bd191c47bb"; - var url = _.CDNClientManager.GenerateImageUrl(id); + var url = _.CDNClientManager.GenerateImageUrl(id); - Assert.StartsWith("https://assets.squidex.io/", url, StringComparison.Ordinal); - } + Assert.StartsWith("https://assets.squidex.io/", url, StringComparison.Ordinal); + } - [Fact] - public async Task Should_download_asset_url_from_cdn() - { - var id = "ef4286f9-8b1d-4dda-bd52-c5bd191c47bb"; + [Fact] + public async Task Should_download_asset_url_from_cdn() + { + var id = "ef4286f9-8b1d-4dda-bd52-c5bd191c47bb"; - var url = _.CDNClientManager.GenerateImageUrl(id); + var url = _.CDNClientManager.GenerateImageUrl(id); - await DownloadAsync(url); - } + await DownloadAsync(url); + } - [Fact] - public void Should_provide_asset_url_from_cloud_when_cdn_not_configured() - { - var id = "ef4286f9-8b1d-4dda-bd52-c5bd191c47bb"; + [Fact] + public void Should_provide_asset_url_from_cloud_when_cdn_not_configured() + { + var id = "ef4286f9-8b1d-4dda-bd52-c5bd191c47bb"; - var url = _.ClientManager.GenerateImageUrl(id); + var url = _.ClientManager.GenerateImageUrl(id); - Assert.StartsWith("https://cloud.squidex.io/", url, StringComparison.Ordinal); - } + Assert.StartsWith("https://cloud.squidex.io/", url, StringComparison.Ordinal); + } - [Fact] - public async Task Should_download_asset_url_from_cloud_when_cdn_not_configured() - { - var id = "ef4286f9-8b1d-4dda-bd52-c5bd191c47bb"; + [Fact] + public async Task Should_download_asset_url_from_cloud_when_cdn_not_configured() + { + var id = "ef4286f9-8b1d-4dda-bd52-c5bd191c47bb"; - var url = _.ClientManager.GenerateImageUrl(id); + var url = _.ClientManager.GenerateImageUrl(id); - await DownloadAsync(url); - } + await DownloadAsync(url); + } - [Fact] - public async Task Should_get_blog_items_from_cdn() - { - var client = _.CDNClientManager.CreateContentsClient<TestEntity, TestEntityData>("blog"); + [Fact] + public async Task Should_get_blog_items_from_cdn() + { + var client = _.CDNClientManager.CreateContentsClient<TestEntity, TestEntityData>("blog"); - var result = await client.GetAsync(); + var result = await client.GetAsync(); - Assert.NotEmpty(result.Items); - } + Assert.NotEmpty(result.Items); + } - [Fact] - public async Task Should_get_blog_items_from_cloud_when_cdn_not_configured() - { - var client = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>("blog"); + [Fact] + public async Task Should_get_blog_items_from_cloud_when_cdn_not_configured() + { + var client = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>("blog"); - var result = await client.GetAsync(); + var result = await client.GetAsync(); - Assert.NotEmpty(result.Items); - } + Assert.NotEmpty(result.Items); + } - private static async Task DownloadAsync(string url) + private static async Task DownloadAsync(string url) + { + using (var client = new HttpClient()) { - using (var client = new HttpClient()) - { - var response = await client.GetAsync(url); + var response = await client.GetAsync(url); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - var buffer = await response.Content.ReadAsByteArrayAsync(); + var buffer = await response.Content.ReadAsByteArrayAsync(); - Assert.True(buffer.Length > 1000); - } + Assert.True(buffer.Length > 1000); } } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/CommentsTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/CommentsTests.cs index eaafa2316d..4f6300d213 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/CommentsTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/CommentsTests.cs @@ -11,102 +11,101 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public class CommentsTests : IClassFixture<CreatedAppFixture> { - [UsesVerify] - public class CommentsTests : IClassFixture<CreatedAppFixture> - { - private readonly string resource = Guid.NewGuid().ToString(); + private readonly string resource = Guid.NewGuid().ToString(); - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public CommentsTests(CreatedAppFixture fixture) - { - _ = fixture; - } + public CommentsTests(CreatedAppFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_make_watch_request() - { - var result = await _.Comments.GetWatchingUsersAsync(_.AppName, resource); + [Fact] + public async Task Should_make_watch_request() + { + var result = await _.Comments.GetWatchingUsersAsync(_.AppName, resource); - Assert.NotNull(result); - } + Assert.NotNull(result); + } - [Fact] - public async Task Should_create_comment() + [Fact] + public async Task Should_create_comment() + { + // STEP 1: Create the comment. + var createRequest = new UpsertCommentDto { - // STEP 1: Create the comment. - var createRequest = new UpsertCommentDto - { - Text = resource - }; + Text = resource + }; - await _.Comments.PostCommentAsync(_.AppName, resource, createRequest); + await _.Comments.PostCommentAsync(_.AppName, resource, createRequest); - // STEP 2: Get comments - var comments = await _.Comments.GetCommentsAsync(_.AppName, resource); + // STEP 2: Get comments + var comments = await _.Comments.GetCommentsAsync(_.AppName, resource); - Assert.Contains(comments.CreatedComments, x => x.Text == createRequest.Text); + Assert.Contains(comments.CreatedComments, x => x.Text == createRequest.Text); - await Verify(comments) - .IgnoreMember<CommentDto>(x => x.Text); - } + await Verify(comments) + .IgnoreMember<CommentDto>(x => x.Text); + } - [Fact] - public async Task Should_update_comment() + [Fact] + public async Task Should_update_comment() + { + // STEP 1: Create the comment. + var createRequest = new UpsertCommentDto { - // STEP 1: Create the comment. - var createRequest = new UpsertCommentDto - { - Text = resource - }; + Text = resource + }; - var comment = await _.Comments.PostCommentAsync(_.AppName, resource, createRequest); + var comment = await _.Comments.PostCommentAsync(_.AppName, resource, createRequest); - // STEP 2: Update comment. - var updateRequest = new UpsertCommentDto - { - Text = $"{resource}_Update" - }; + // STEP 2: Update comment. + var updateRequest = new UpsertCommentDto + { + Text = $"{resource}_Update" + }; - await _.Comments.PutCommentAsync(_.AppName, resource, comment.Id, updateRequest); + await _.Comments.PutCommentAsync(_.AppName, resource, comment.Id, updateRequest); - // STEP 3: Get comments since create. - var comments = await _.Comments.GetCommentsAsync(_.AppName, resource, 0); + // STEP 3: Get comments since create. + var comments = await _.Comments.GetCommentsAsync(_.AppName, resource, 0); - Assert.Contains(comments.UpdatedComments, x => x.Text == updateRequest.Text); + Assert.Contains(comments.UpdatedComments, x => x.Text == updateRequest.Text); - await Verify(comments) - .IgnoreMember<CommentDto>(x => x.Text); - } + await Verify(comments) + .IgnoreMember<CommentDto>(x => x.Text); + } - [Fact] - public async Task Should_delete_comment() + [Fact] + public async Task Should_delete_comment() + { + // STEP 1: Create the comment. + var createRequest = new UpsertCommentDto { - // STEP 1: Create the comment. - var createRequest = new UpsertCommentDto - { - Text = resource - }; + Text = resource + }; - var comment = await _.Comments.PostCommentAsync(_.AppName, resource, createRequest); + var comment = await _.Comments.PostCommentAsync(_.AppName, resource, createRequest); - // STEP 2: Delete comment. - await _.Comments.DeleteCommentAsync(_.AppName, resource, comment.Id); + // STEP 2: Delete comment. + await _.Comments.DeleteCommentAsync(_.AppName, resource, comment.Id); - // STEP 3: Get comments since create. - var comments = await _.Comments.GetCommentsAsync(_.AppName, resource, 0); + // STEP 3: Get comments since create. + var comments = await _.Comments.GetCommentsAsync(_.AppName, resource, 0); - Assert.Contains(comment.Id, comments.DeletedComments); + Assert.Contains(comment.Id, comments.DeletedComments); - await Verify(comments) - .IgnoreMember<CommentDto>(x => x.Text); - } + await Verify(comments) + .IgnoreMember<CommentDto>(x => x.Text); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs index 0327dc736b..f28832a403 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs @@ -12,86 +12,85 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class ContentCleanupTests : IClassFixture<CreatedAppFixture> { - public class ContentCleanupTests : IClassFixture<CreatedAppFixture> - { - private readonly string schemaName = $"schema-{Guid.NewGuid()}"; + private readonly string schemaName = $"schema-{Guid.NewGuid()}"; - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public ContentCleanupTests(CreatedAppFixture fixture) - { - _ = fixture; - } + public ContentCleanupTests(CreatedAppFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_cleanup_old_data_from_update_response() - { - // STEP 1: Create a schema. - var schema = await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); + [Fact] + public async Task Should_cleanup_old_data_from_update_response() + { + // STEP 1: Create a schema. + var schema = await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); - var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); + var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); - // STEP 2: Create a content for this schema. - var content_1 = await contents.CreateAsync(new TestEntityData - { - String = "hello" - }); + // STEP 2: Create a content for this schema. + var content_1 = await contents.CreateAsync(new TestEntityData + { + String = "hello" + }); - Assert.Equal("hello", content_1.Data.String); + Assert.Equal("hello", content_1.Data.String); - // STEP 3: Delete a field from schema. - await _.Schemas.DeleteFieldAsync(_.AppName, schema.Name, schema.Fields.First(x => x.Name == TestEntityData.StringField).FieldId); + // STEP 3: Delete a field from schema. + await _.Schemas.DeleteFieldAsync(_.AppName, schema.Name, schema.Fields.First(x => x.Name == TestEntityData.StringField).FieldId); - // STEP 4: Make any update. - var content_2 = await contents.ChangeStatusAsync(content_1.Id, new ChangeStatus - { - Status = "Published" - }); + // STEP 4: Make any update. + var content_2 = await contents.ChangeStatusAsync(content_1.Id, new ChangeStatus + { + Status = "Published" + }); - // Should not return deleted field. - Assert.Null(content_2.Data.String); - } + // Should not return deleted field. + Assert.Null(content_2.Data.String); + } - [Fact] - public async Task Should_cleanup_old_references() - { - // STEP 1: Create a schema. - await TestEntityWithReferences.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); + [Fact] + public async Task Should_cleanup_old_references() + { + // STEP 1: Create a schema. + await TestEntityWithReferences.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); - var contents = _.ClientManager.CreateContentsClient<TestEntityWithReferences, TestEntityWithReferencesData>(schemaName); + var contents = _.ClientManager.CreateContentsClient<TestEntityWithReferences, TestEntityWithReferencesData>(schemaName); - // STEP 2: Create a referenced content. - var contentA_1 = await contents.CreateAsync(new TestEntityWithReferencesData - { - References = null - }); + // STEP 2: Create a referenced content. + var contentA_1 = await contents.CreateAsync(new TestEntityWithReferencesData + { + References = null + }); - // STEP 3: Create a content with a reference. - var contentB_1 = await contents.CreateAsync(new TestEntityWithReferencesData - { - References = new[] { contentA_1.Id } - }); + // STEP 3: Create a content with a reference. + var contentB_1 = await contents.CreateAsync(new TestEntityWithReferencesData + { + References = new[] { contentA_1.Id } + }); - // STEP 3: Delete a reference - await contents.DeleteAsync(contentA_1.Id); + // STEP 3: Delete a reference + await contents.DeleteAsync(contentA_1.Id); - // STEP 4: Make any update. - var contentB_2 = await contents.ChangeStatusAsync(contentB_1.Id, new ChangeStatus - { - Status = "Published" - }); + // STEP 4: Make any update. + var contentB_2 = await contents.ChangeStatusAsync(contentB_1.Id, new ChangeStatus + { + Status = "Published" + }); - // Should not return deleted field. - Assert.Empty(contentB_2.Data.References); - } + // Should not return deleted field. + Assert.Empty(contentB_2.Data.References); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs index 2a680b24f0..b4103e9960 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs @@ -7,13 +7,12 @@ using TestSuite.Fixtures; -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public sealed class ContentFixture : TestSchemaFixtureBase { - public sealed class ContentFixture : TestSchemaFixtureBase + public ContentFixture() + : base("my-writes") { - public ContentFixture() - : base("my-writes") - { - } } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs index fa9a121daa..5a8cbf9bb2 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs @@ -11,121 +11,120 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class ContentLanguageTests : IClassFixture<ContentFixture> { - public class ContentLanguageTests : IClassFixture<ContentFixture> - { - public ContentFixture _ { get; } + public ContentFixture _ { get; } - public ContentLanguageTests(ContentFixture fixture) - { - _ = fixture; - } + public ContentLanguageTests(ContentFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_filter_language() + [Fact] + public async Task Should_filter_language() + { + // STEP 1: Create content + var content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create content - var content = await _.Contents.CreateAsync(new TestEntityData + Localized = new Dictionary<string, string> { - Localized = new Dictionary<string, string> - { - ["de"] = "Hallo", - ["en"] = "Hello" - } - }, ContentCreateOptions.AsPublish, QueryContext.Default.WithLanguages("de")); - - Assert.False(content.Data.Localized.ContainsKey("en")); - Assert.Equal("Hallo", content.Data.Localized["de"]); - } + ["de"] = "Hallo", + ["en"] = "Hello" + } + }, ContentCreateOptions.AsPublish, QueryContext.Default.WithLanguages("de")); - [Theory] - [InlineData("de", "Hallo")] - [InlineData("en", "Hello")] - [InlineData("custom", "Custom")] - public async Task Should_flatten_language(string code, string expected) + Assert.False(content.Data.Localized.ContainsKey("en")); + Assert.Equal("Hallo", content.Data.Localized["de"]); + } + + [Theory] + [InlineData("de", "Hallo")] + [InlineData("en", "Hello")] + [InlineData("custom", "Custom")] + public async Task Should_flatten_language(string code, string expected) + { + // STEP 1: Create content + var content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create content - var content = await _.Contents.CreateAsync(new TestEntityData + Localized = new Dictionary<string, string> { - Localized = new Dictionary<string, string> - { - ["de"] = "Hallo", - ["en"] = "Hello", - ["custom"] = "Custom" - } - }, ContentCreateOptions.AsPublish); + ["de"] = "Hallo", + ["en"] = "Hello", + ["custom"] = "Custom" + } + }, ContentCreateOptions.AsPublish); - // STEP 2: Get content. - var contents = _.ClientManager.CreateDynamicContentsClient(_.Contents.SchemaName); + // STEP 2: Get content. + var contents = _.ClientManager.CreateDynamicContentsClient(_.Contents.SchemaName); - var contentFlatten = await contents.GetAsync(content.Id, QueryContext.Default.Flatten().WithLanguages(code)); + var contentFlatten = await contents.GetAsync(content.Id, QueryContext.Default.Flatten().WithLanguages(code)); - Assert.Equal(expected, (string)contentFlatten.Data["localized"]); - } + Assert.Equal(expected, (string)contentFlatten.Data["localized"]); + } - [Fact] - public async Task Should_provide_etag_based_on_headers() + [Fact] + public async Task Should_provide_etag_based_on_headers() + { + // STEP 1: Create content + var content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create content - var content = await _.Contents.CreateAsync(new TestEntityData + Localized = new Dictionary<string, string> { - Localized = new Dictionary<string, string> - { - ["de"] = "Hallo", - ["en"] = "Hello" - } - }, ContentCreateOptions.AsPublish); - + ["de"] = "Hallo", + ["en"] = "Hello" + } + }, ContentCreateOptions.AsPublish); - // STEP 2: Get content. - var (etag1, _) = await GetEtagAsync(content.Id, new Dictionary<string, string>()); - var (etag2, _) = await GetEtagAsync(content.Id, new Dictionary<string, string> - { - ["X-Flatten"] = "1" - }); + // STEP 2: Get content. + var (etag1, _) = await GetEtagAsync(content.Id, new Dictionary<string, string>()); - var (etag3, _) = await GetEtagAsync(content.Id, new Dictionary<string, string> - { - ["X-Languages"] = "en" - }); + var (etag2, _) = await GetEtagAsync(content.Id, new Dictionary<string, string> + { + ["X-Flatten"] = "1" + }); - var (etag4, _) = await GetEtagAsync(content.Id, new Dictionary<string, string> - { - ["X-Languages"] = "en", - ["X-Flatten"] = "1" - }); + var (etag3, _) = await GetEtagAsync(content.Id, new Dictionary<string, string> + { + ["X-Languages"] = "en" + }); - static void AssertValue(string value, string not = null) - { - Assert.NotNull(value); - Assert.NotEmpty(value); - Assert.NotEqual(not, value); - } + var (etag4, _) = await GetEtagAsync(content.Id, new Dictionary<string, string> + { + ["X-Languages"] = "en", + ["X-Flatten"] = "1" + }); - AssertValue(etag1); - AssertValue(etag2, etag1); - AssertValue(etag3, etag1); - AssertValue(etag4, etag1); + static void AssertValue(string value, string not = null) + { + Assert.NotNull(value); + Assert.NotEmpty(value); + Assert.NotEqual(not, value); } - private async Task<(string, string)> GetEtagAsync(string id, Dictionary<string, string> headers) - { - var url = $"{_.ClientManager.Options.Url}api/content/{_.AppName}/{_.SchemaName}/{id}"; + AssertValue(etag1); + AssertValue(etag2, etag1); + AssertValue(etag3, etag1); + AssertValue(etag4, etag1); + } + + private async Task<(string, string)> GetEtagAsync(string id, Dictionary<string, string> headers) + { + var url = $"{_.ClientManager.Options.Url}api/content/{_.AppName}/{_.SchemaName}/{id}"; - using (var httpClient = _.ClientManager.CreateHttpClient()) + using (var httpClient = _.ClientManager.CreateHttpClient()) + { + foreach (var (key, value) in headers) { - foreach (var (key, value) in headers) - { - httpClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); - } + httpClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); + } - var response = await httpClient.GetAsync(url); + var response = await httpClient.GetAsync(url); - return (response.Headers.GetValues("ETag").FirstOrDefault(), response.Headers.Vary.ToString()); - } + return (response.Headers.GetValues("ETag").FirstOrDefault(), response.Headers.Vary.ToString()); } } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs index 704d8f4811..ca3862bf4f 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs @@ -11,55 +11,54 @@ using TestSuite.Fixtures; using TestSuite.Model; -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public sealed class ContentQueryFixture : TestSchemaFixtureBase { - public sealed class ContentQueryFixture : TestSchemaFixtureBase + public ContentQueryFixture() + : base("my-reads") { - public ContentQueryFixture() - : base("my-reads") - { - } + } - public override async Task InitializeAsync() - { - await base.InitializeAsync(); + public override async Task InitializeAsync() + { + await base.InitializeAsync(); - await DisposeAsync(); + await DisposeAsync(); - for (var index = 10; index > 0; index--) + for (var index = 10; index > 0; index--) + { + var data = new TestEntityData { - var data = new TestEntityData + Number = index, + Json = JObject.FromObject(new { - Number = index, - Json = JObject.FromObject(new + nested1 = new { - nested1 = new - { - nested2 = index - } - }), - Geo = GeoJson.Point(index, index, oldFormat: index % 2 == 1), - Localized = new Dictionary<string, string> - { - ["en"] = index.ToString(CultureInfo.InvariantCulture) - }, - String = index.ToString(CultureInfo.InvariantCulture), - }; + nested2 = index + } + }), + Geo = GeoJson.Point(index, index, oldFormat: index % 2 == 1), + Localized = new Dictionary<string, string> + { + ["en"] = index.ToString(CultureInfo.InvariantCulture) + }, + String = index.ToString(CultureInfo.InvariantCulture), + }; - await Contents.CreateAsync(data, ContentCreateOptions.AsPublish); - } + await Contents.CreateAsync(data, ContentCreateOptions.AsPublish); } + } - public override async Task DisposeAsync() - { - await base.DisposeAsync(); + public override async Task DisposeAsync() + { + await base.DisposeAsync(); - var contents = await Contents.GetAsync(); + var contents = await Contents.GetAsync(); - foreach (var content in contents.Items) - { - await Contents.DeleteAsync(content); - } + foreach (var content in contents.Items) + { + await Contents.DeleteAsync(content); } } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 0e5cc8034a..b8d43cb398 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -14,477 +14,477 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class ContentQueryTests : IClassFixture<ContentQueryFixture> { - public class ContentQueryTests : IClassFixture<ContentQueryFixture> - { - public ContentQueryFixture _ { get; } + public ContentQueryFixture _ { get; } - public ContentQueryTests(ContentQueryFixture fixture) - { - _ = fixture; - } + public ContentQueryTests(ContentQueryFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_query_newly_created_schema() + [Fact] + public async Task Should_query_newly_created_schema() + { + for (var i = 0; i < 20; i++) { - for (var i = 0; i < 20; i++) - { - var schemaName = $"schema-{Guid.NewGuid()}"; + var schemaName = $"schema-{Guid.NewGuid()}"; - await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); + await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); - var contentClient = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); - var contentItems = await contentClient.GetAsync(); + var contentClient = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); + var contentItems = await contentClient.GetAsync(); - Assert.Equal(0, contentItems.Total); - } + Assert.Equal(0, contentItems.Total); } + } - [Fact] - public async Task Should_query_by_ids() - { - var q = new ContentQuery { OrderBy = "data/number/iv asc" }; + [Fact] + public async Task Should_query_by_ids() + { + var q = new ContentQuery { OrderBy = "data/number/iv asc" }; - var items_0 = await _.Contents.GetAsync(q); - var itemsIds = items_0.Items.Take(3).Select(x => x.Id).ToHashSet(); + var items_0 = await _.Contents.GetAsync(q); + var itemsIds = items_0.Items.Take(3).Select(x => x.Id).ToHashSet(); - var items_1 = await _.Contents.GetAsync(new ContentQuery { Ids = itemsIds }); + var items_1 = await _.Contents.GetAsync(new ContentQuery { Ids = itemsIds }); - Assert.Equal(3, items_1.Items.Count); - Assert.Equal(3, items_1.Total); + Assert.Equal(3, items_1.Items.Count); + Assert.Equal(3, items_1.Total); - foreach (var item in items_1.Items) - { - Assert.Equal(_.AppName, item.AppName); - Assert.Equal(_.SchemaName, item.SchemaName); - } + foreach (var item in items_1.Items) + { + Assert.Equal(_.AppName, item.AppName); + Assert.Equal(_.SchemaName, item.SchemaName); } + } - [Fact] - public async Task Should_query_by_ids_across_schemas() - { - var q = new ContentQuery { OrderBy = "data/number/iv asc" }; + [Fact] + public async Task Should_query_by_ids_across_schemas() + { + var q = new ContentQuery { OrderBy = "data/number/iv asc" }; - var items_0 = await _.Contents.GetAsync(q); - var itemsIds = items_0.Items.Take(3).Select(x => x.Id).ToHashSet(); + var items_0 = await _.Contents.GetAsync(q); + var itemsIds = items_0.Items.Take(3).Select(x => x.Id).ToHashSet(); - var items_1 = await _.SharedContents.GetAsync(itemsIds); + var items_1 = await _.SharedContents.GetAsync(itemsIds); - Assert.Equal(3, items_1.Items.Count); - Assert.Equal(3, items_1.Total); + Assert.Equal(3, items_1.Items.Count); + Assert.Equal(3, items_1.Total); - foreach (var item in items_1.Items) - { - Assert.Equal(_.AppName, item.AppName); - Assert.Equal(_.SchemaName, item.SchemaName); - } + foreach (var item in items_1.Items) + { + Assert.Equal(_.AppName, item.AppName); + Assert.Equal(_.SchemaName, item.SchemaName); } + } - [Fact] - public async Task Should_query_by_ids_filter() - { - var q0 = new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }; + [Fact] + public async Task Should_query_by_ids_filter() + { + var q0 = new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }; - var items_0 = await _.Contents.GetAsync(q0); + var items_0 = await _.Contents.GetAsync(q0); - var q1 = new ContentQuery + var q1 = new ContentQuery + { + JsonQuery = new { - JsonQuery = new + sort = new[] { - sort = new[] - { - new - { - path = "data.number.iv" - } - }, - filter = new + new { - or = items_0.Items.Select(x => new - { - path = "id", - op = "eq", - value = x.Id - }).ToArray() + path = "data.number.iv" } + }, + filter = new + { + or = items_0.Items.Select(x => new + { + path = "id", + op = "eq", + value = x.Id + }).ToArray() } - }; + } + }; - var items_1 = await _.Contents.GetAsync(q1); + var items_1 = await _.Contents.GetAsync(q1); - AssertItems(items_0, 3, new[] { 4, 5, 6 }); - AssertItems(items_1, 3, new[] { 4, 5, 6 }); - } + AssertItems(items_0, 3, new[] { 4, 5, 6 }); + AssertItems(items_1, 3, new[] { 4, 5, 6 }); + } - [Fact] - public async Task Should_query_all_with_odata() - { - var q = new ContentQuery { OrderBy = "data/number/iv asc" }; + [Fact] + public async Task Should_query_all_with_odata() + { + var q = new ContentQuery { OrderBy = "data/number/iv asc" }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); - } + AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + } - [Fact] - public async Task Should_query_all_with_json() + [Fact] + public async Task Should_query_all_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new + sort = new[] { - sort = new[] + new { - new - { - path = "data.number.iv" - } + path = "data.number.iv" } } - }; + } + }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); - } + AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + } - [Fact] - public async Task Should_query_random_with_odata() - { - var q = new ContentQuery { Random = 5 }; + [Fact] + public async Task Should_query_random_with_odata() + { + var q = new ContentQuery { Random = 5 }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - Assert.Equal(5, items.Items.Count); - } + Assert.Equal(5, items.Items.Count); + } - [Fact] - public async Task Should_query_random_with_json() + [Fact] + public async Task Should_query_random_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new - { - random = 5 - } - }; + random = 5 + } + }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - Assert.Equal(5, items.Items.Count); - } + Assert.Equal(5, items.Items.Count); + } - [Fact] - public async Task Should_query_by_skip_with_odata() - { - var q = new ContentQuery { OrderBy = "data/number/iv asc", Skip = 5 }; + [Fact] + public async Task Should_query_by_skip_with_odata() + { + var q = new ContentQuery { OrderBy = "data/number/iv asc", Skip = 5 }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 }); - } + AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 }); + } - [Fact] - public async Task Should_query_by_skip_with_json() + [Fact] + public async Task Should_query_by_skip_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new + sort = new[] { - sort = new[] + new { - new - { - path = "data.number.iv" - } - }, - skip = 5 - } - }; + path = "data.number.iv" + } + }, + skip = 5 + } + }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 }); - } + AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 }); + } - [Fact] - public async Task Should_query_by_skip_and_top_with_odata() - { - var q = new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" }; + [Fact] + public async Task Should_query_by_skip_and_top_with_odata() + { + var q = new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 }); - } + AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 }); + } - [Fact] - public async Task Should_query_by_skip_and_top_with_json() + [Fact] + public async Task Should_query_by_skip_and_top_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new + skip = 2, + sort = new[] { - skip = 2, - sort = new[] + new { - new - { - path = "data.number.iv" - } - }, - top = 5 - } - }; + path = "data.number.iv" + } + }, + top = 5 + } + }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 }); - } + AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 }); + } - [Fact] - public async Task Should_query_by_filter_with_odata() - { - var q = new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }; + [Fact] + public async Task Should_query_by_filter_with_odata() + { + var q = new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 3, new[] { 4, 5, 6 }); - } + AssertItems(items, 3, new[] { 4, 5, 6 }); + } - [Fact] - public async Task Should_query_by_filter_with_json() + [Fact] + public async Task Should_query_by_filter_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new + sort = new[] { - sort = new[] + new + { + path = "data.number.iv" + } + }, + filter = new + { + and = new[] { new { - path = "data.number.iv" - } - }, - filter = new - { - and = new[] + path = "data.number.iv", + op = "gt", + value = 3 + }, + new { - new - { - path = "data.number.iv", - op = "gt", - value = 3 - }, - new - { - path = "data.number.iv", - op = "lt", - value = 7 - } + path = "data.number.iv", + op = "lt", + value = 7 } } } - }; + } + }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 3, new[] { 4, 5, 6 }); - } + AssertItems(items, 3, new[] { 4, 5, 6 }); + } - [Fact] - public async Task Should_query_by_json_filter_with_json() + [Fact] + public async Task Should_query_by_json_filter_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new + sort = new[] + { + new + { + path = "data.json.iv.nested1.nested2" + } + }, + filter = new { - sort = new[] + and = new[] { new { - path = "data.json.iv.nested1.nested2" - } - }, - filter = new - { - and = new[] + path = "data.json.iv.nested1.nested2", + op = "gt", + value = 3 + }, + new { - new - { - path = "data.json.iv.nested1.nested2", - op = "gt", - value = 3 - }, - new - { - path = "data.json.iv.nested1.nested2", - op = "lt", - value = 7 - } + path = "data.json.iv.nested1.nested2", + op = "lt", + value = 7 } } } - }; + } + }; - var items = await _.Contents.GetAsync(q); + var items = await _.Contents.GetAsync(q); - AssertItems(items, 3, new[] { 4, 5, 6 }); - } + AssertItems(items, 3, new[] { 4, 5, 6 }); + } - [Fact] - public async Task Should_query_by_full_text_with_odata() - { - var q = new ContentQuery { Search = "2" }; + [Fact] + public async Task Should_query_by_full_text_with_odata() + { + var q = new ContentQuery { Search = "2" }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); - AssertItems(items, 1, new[] { 2 }); - } + AssertItems(items, 1, new[] { 2 }); + } - [Fact] - public async Task Should_query_by_full_text_with_json() + [Fact] + public async Task Should_query_by_full_text_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new - { - fullText = "2" - } - }; + fullText = "2" + } + }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); - AssertItems(items, 1, new[] { 2 }); - } + AssertItems(items, 1, new[] { 2 }); + } - [Fact] - public async Task Should_query_by_near_location_with_odata() - { - var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(3 3)') lt 1000" }; + [Fact] + public async Task Should_query_by_near_location_with_odata() + { + var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(3 3)') lt 1000" }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); - AssertItems(items, 1, new[] { 3 }); - } + AssertItems(items, 1, new[] { 3 }); + } - [Fact] - public async Task Should_query_by_near_location_with_json() + [Fact] + public async Task Should_query_by_near_location_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new + filter = new { - filter = new + path = "data.geo.iv", + op = "lt", + value = new { - path = "data.geo.iv", - op = "lt", - value = new - { - longitude = 3, - latitude = 3, - distance = 1000 - } + longitude = 3, + latitude = 3, + distance = 1000 } } - }; + } + }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); - AssertItems(items, 1, new[] { 3 }); - } + AssertItems(items, 1, new[] { 3 }); + } - [Fact] - public async Task Should_query_by_near_geoson_location_with_odata() - { - var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(4 4)') lt 1000" }; + [Fact] + public async Task Should_query_by_near_geoson_location_with_odata() + { + var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(4 4)') lt 1000" }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); - AssertItems(items, 1, new[] { 4 }); - } + AssertItems(items, 1, new[] { 4 }); + } - [Fact] - public async Task Should_query_json_with_dot() + [Fact] + public async Task Should_query_json_with_dot() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a content item with a text that caused a bug before. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a content item with a text that caused a bug before. - content = await _.Contents.CreateAsync(new TestEntityData + Json = new JObject { - Json = new JObject - { - ["search.field.with.dot"] = 42 - } - }, ContentCreateOptions.AsPublish); + ["search.field.with.dot"] = 42 + } + }, ContentCreateOptions.AsPublish); - // STEP 2: Get the item and ensure that the text is the same. - var q = new ContentQuery + // STEP 2: Get the item and ensure that the text is the same. + var q = new ContentQuery + { + JsonQuery = new { - JsonQuery = new + filter = new { - filter = new + and = new[] { - and = new[] + new { - new - { - path = "data.json.iv.search\\.field\\.with\\.dot", - op = "eq", - value = 42 - } + path = "data.json.iv.search\\.field\\.with\\.dot", + op = "eq", + value = 42 } } } - }; + } + }; - var queried = await _.Contents.GetAsync(q); + var queried = await _.Contents.GetAsync(q); - Assert.Equal(42, (int)queried.Items[0].Data.Json["search.field.with.dot"]); - } - finally + Assert.Equal(42, (int)queried.Items[0].Data.Json["search.field.with.dot"]); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_query_by_near_geoson_location_with_json() + [Fact] + public async Task Should_query_by_near_geoson_location_with_json() + { + var q = new ContentQuery { - var q = new ContentQuery + JsonQuery = new { - JsonQuery = new + filter = new { - filter = new + path = "data.geo.iv", + op = "lt", + value = new { - path = "data.geo.iv", - op = "lt", - value = new - { - longitude = 4, - latitude = 4, - distance = 1000 - } + longitude = 4, + latitude = 4, + distance = 1000 } } - }; + } + }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); - AssertItems(items, 1, new[] { 4 }); - } + AssertItems(items, 1, new[] { 4 }); + } - [Fact] - public async Task Should_create_and_query_with_inline_graphql() + [Fact] + public async Task Should_create_and_query_with_inline_graphql() + { + var query = new { - var query = new - { - query = @" + query = @" mutation { createMyReadsContent(data: { number: { @@ -499,21 +499,21 @@ public async Task Should_create_and_query_with_inline_graphql() } } }" - }; + }; - var result = await _.SharedContents.GraphQlAsync<JObject>(query); + var result = await _.SharedContents.GraphQlAsync<JObject>(query); - var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>(); + var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>(); - Assert.Equal(555, value); - } + Assert.Equal(555, value); + } - [Fact] - public async Task Should_create_and_query_with_variable_graphql() + [Fact] + public async Task Should_create_and_query_with_variable_graphql() + { + var query = new { - var query = new - { - query = @" + query = @" mutation Mutation($data: MyReadsDataInputDto!) { createMyReadsContent(data: $data) { id, @@ -524,31 +524,31 @@ mutation Mutation($data: MyReadsDataInputDto!) { } } }", - variables = new + variables = new + { + data = new { - data = new + number = new { - number = new - { - iv = 998 - } + iv = 998 } } - }; + } + }; - var result = await _.SharedContents.GraphQlAsync<JObject>(query); + var result = await _.SharedContents.GraphQlAsync<JObject>(query); - var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>(); + var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>(); - Assert.Equal(998, value); - } + Assert.Equal(998, value); + } - [Fact] - public async Task Should_query_with_graphql_batching() + [Fact] + public async Task Should_query_with_graphql_batching() + { + var query1 = new { - var query1 = new - { - query = @" + query = @" query ContentsQuery($filter: String!) { queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { id, @@ -559,15 +559,15 @@ query ContentsQuery($filter: String!) { } } }", - variables = new - { - filter = @"data/number/iv gt 3 and data/number/iv lt 7" - } - }; - - var query2 = new + variables = new { - query = @" + filter = @"data/number/iv gt 3 and data/number/iv lt 7" + } + }; + + var query2 = new + { + query = @" query ContentsQuery($filter: String!) { queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { id, @@ -578,27 +578,27 @@ query ContentsQuery($filter: String!) { } } }", - variables = new - { - filter = @"data/number/iv gt 4 and data/number/iv lt 7" - } - }; + variables = new + { + filter = @"data/number/iv gt 4 and data/number/iv lt 7" + } + }; - var results = await _.SharedContents.GraphQlAsync<QueryResult>(new[] { query1, query2 }); + var results = await _.SharedContents.GraphQlAsync<QueryResult>(new[] { query1, query2 }); - var items1 = results.ElementAt(0).Data.Items; - var items2 = results.ElementAt(1).Data.Items; + var items1 = results.ElementAt(0).Data.Items; + var items2 = results.ElementAt(1).Data.Items; - Assert.Equal(items1.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); - Assert.Equal(items2.Select(x => x.Data.Number).ToArray(), new[] { 5, 6 }); - } + Assert.Equal(items1.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); + Assert.Equal(items2.Select(x => x.Data.Number).ToArray(), new[] { 5, 6 }); + } - [Fact] - public async Task Should_query_with_graphql() + [Fact] + public async Task Should_query_with_graphql() + { + var query = new { - var query = new - { - query = @" + query = @" query ContentsQuery($filter: String!) { queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { id, @@ -609,24 +609,24 @@ query ContentsQuery($filter: String!) { } } }", - variables = new - { - filter = @"data/number/iv gt 3 and data/number/iv lt 7" - } - }; + variables = new + { + filter = @"data/number/iv gt 3 and data/number/iv lt 7" + } + }; - var result = await _.SharedContents.GraphQlAsync<QueryResult>(query); - var items = result.Items; + var result = await _.SharedContents.GraphQlAsync<QueryResult>(query); + var items = result.Items; - Assert.Equal(items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); - } + Assert.Equal(items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); + } - [Fact] - public async Task Should_query_with_graphql_get() + [Fact] + public async Task Should_query_with_graphql_get() + { + var query = new { - var query = new - { - query = @" + query = @" query ContentsQuery($filter: String!) { queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { id, @@ -637,24 +637,24 @@ query ContentsQuery($filter: String!) { } } }", - variables = new - { - filter = @"data/number/iv gt 3 and data/number/iv lt 7" - } - }; + variables = new + { + filter = @"data/number/iv gt 3 and data/number/iv lt 7" + } + }; - var result = await _.SharedContents.GraphQlGetAsync<QueryResult>(query); - var items = result.Items; + var result = await _.SharedContents.GraphQlGetAsync<QueryResult>(query); + var items = result.Items; - Assert.Equal(items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); - } + Assert.Equal(items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); + } - [Fact] - public async Task Should_query_with_graphql_with_dynamic() + [Fact] + public async Task Should_query_with_graphql_with_dynamic() + { + var query = new { - var query = new - { - query = @" + query = @" { queryMyReadsContents(filter: ""data/number/iv gt 3 and data/number/iv lt 7"", orderby: ""data/number/iv asc"") { id, @@ -665,20 +665,20 @@ public async Task Should_query_with_graphql_with_dynamic() } } }" - }; + }; - var result = await _.SharedContents.GraphQlAsync<JObject>(query); - var items = result["queryMyReadsContents"]; + var result = await _.SharedContents.GraphQlAsync<JObject>(query); + var items = result["queryMyReadsContents"]; - Assert.Equal(items.Select(x => x["data"]["number"]["iv"].Value<int>()).ToArray(), new[] { 4, 5, 6 }); - } + Assert.Equal(items.Select(x => x["data"]["number"]["iv"].Value<int>()).ToArray(), new[] { 4, 5, 6 }); + } - [Fact] - public async Task Should_query_with_grapqhl_complex_search() + [Fact] + public async Task Should_query_with_grapqhl_complex_search() + { + var query = new { - var query = new - { - query = @" + query = @" query ContentsQuery($search: String!) { queryMyReadsContents(search: $search) { id, @@ -689,21 +689,21 @@ query ContentsQuery($search: String!) { } } }", - variables = new - { - search = @"The answer is 42" - } - }; + variables = new + { + search = @"The answer is 42" + } + }; - await _.SharedContents.GraphQlAsync<QueryResult>(query); - } + await _.SharedContents.GraphQlAsync<QueryResult>(query); + } - [Fact] - public async Task Should_query_correct_content_type_for_graphql() + [Fact] + public async Task Should_query_correct_content_type_for_graphql() + { + var query = new { - var query = new - { - query = @" + query = @" query ContentsQuery($filter: String!) { queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { id, @@ -714,44 +714,43 @@ query ContentsQuery($filter: String!) { } } }", - variables = new - { - filter = @"data/number/iv gt 3 and data/number/iv lt 7" - } - }; - - using (var client = _.ClientManager.CreateHttpClient()) + variables = new { - // Create the request manually to check the content type. - var response = await client.PostAsync(_.ClientManager.GenerateUrl($"api/content/{_.AppName}/graphql/batch"), query.ToContent()); - - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + filter = @"data/number/iv gt 3 and data/number/iv lt 7" } - } + }; - private sealed class QueryResult + using (var client = _.ClientManager.CreateHttpClient()) { - [JsonProperty("queryMyReadsContents")] - public QueryItem[] Items { get; set; } + // Create the request manually to check the content type. + var response = await client.PostAsync(_.ClientManager.GenerateUrl($"api/content/{_.AppName}/graphql/batch"), query.ToContent()); + + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); } + } - private sealed class QueryItem - { - public Guid Id { get; set; } + private sealed class QueryResult + { + [JsonProperty("queryMyReadsContents")] + public QueryItem[] Items { get; set; } + } - public QueryItemData Data { get; set; } - } + private sealed class QueryItem + { + public Guid Id { get; set; } - private sealed class QueryItemData - { - [JsonConverter(typeof(InvariantConverter))] - public int Number { get; set; } - } + public QueryItemData Data { get; set; } + } - private static void AssertItems(ContentsResult<TestEntity, TestEntityData> entities, int total, int[] expected) - { - Assert.Equal(total, entities.Total); - Assert.Equal(expected, entities.Items.Select(x => x.Data.Number).ToArray()); - } + private sealed class QueryItemData + { + [JsonConverter(typeof(InvariantConverter))] + public int Number { get; set; } + } + + private static void AssertItems(ContentsResult<TestEntity, TestEntityData> entities, int total, int[] expected) + { + Assert.Equal(total, entities.Total); + Assert.Equal(expected, entities.Items.Select(x => x.Data.Number).ToArray()); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesFixture.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesFixture.cs index 9506fbc1c5..4df52a73a5 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesFixture.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesFixture.cs @@ -7,13 +7,12 @@ using TestSuite.Fixtures; -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public sealed class ContentReferencesFixture : TestSchemaWithReferencesFixtureBase { - public sealed class ContentReferencesFixture : TestSchemaWithReferencesFixtureBase + public ContentReferencesFixture() + : base("my-references") { - public ContentReferencesFixture() - : base("my-references") - { - } } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs index db0d031515..fa4b010c7f 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs @@ -11,225 +11,224 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class ContentReferencesTests : IClassFixture<ContentReferencesFixture> { - public class ContentReferencesTests : IClassFixture<ContentReferencesFixture> - { - public ContentReferencesFixture _ { get; } + public ContentReferencesFixture _ { get; } - public ContentReferencesTests(ContentReferencesFixture fixture) - { - _ = fixture; - } + public ContentReferencesTests(ContentReferencesFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_not_deliver_unpublished_references() + [Fact] + public async Task Should_not_deliver_unpublished_references() + { + // STEP 1: Create a referenced content. + var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData { - // STEP 1: Create a referenced content. - var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = null - }); + References = null + }); - // STEP 2: Create a content with a reference. - var contentB_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = new[] { contentA_1.Id } - }, ContentCreateOptions.AsPublish); + // STEP 2: Create a content with a reference. + var contentB_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData + { + References = new[] { contentA_1.Id } + }, ContentCreateOptions.AsPublish); - // STEP 3: Query new item - var contentB_2 = await _.Contents.GetAsync(contentB_1.Id); + // STEP 3: Query new item + var contentB_2 = await _.Contents.GetAsync(contentB_1.Id); - Assert.Empty(contentB_2.Data.References); + Assert.Empty(contentB_2.Data.References); - // STEP 4: Publish reference - await _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus - { - Status = "Published" - }); + // STEP 4: Publish reference + await _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus + { + Status = "Published" + }); - // STEP 5: Query new item again - var contentB_3 = await _.Contents.GetAsync(contentB_1.Id); + // STEP 5: Query new item again + var contentB_3 = await _.Contents.GetAsync(contentB_1.Id); - Assert.Equal(new string[] { contentA_1.Id }, contentB_3.Data.References); - } + Assert.Equal(new string[] { contentA_1.Id }, contentB_3.Data.References); + } - [Fact] - public async Task Should_not_delete_when_referenced() + [Fact] + public async Task Should_not_delete_when_referenced() + { + // STEP 1: Create a referenced content. + var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData { - // STEP 1: Create a referenced content. - var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = null - }, ContentCreateOptions.AsPublish); + References = null + }, ContentCreateOptions.AsPublish); - // STEP 2: Create a content with a reference. - await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = new[] { contentA_1.Id } - }, ContentCreateOptions.AsPublish); + // STEP 2: Create a content with a reference. + await _.Contents.CreateAsync(new TestEntityWithReferencesData + { + References = new[] { contentA_1.Id } + }, ContentCreateOptions.AsPublish); - // STEP 3: Try to delete with referrer check. - var options = new ContentDeleteOptions { CheckReferrers = true }; + // STEP 3: Try to delete with referrer check. + var options = new ContentDeleteOptions { CheckReferrers = true }; - await Assert.ThrowsAnyAsync<SquidexException>(() => - { - return _.Contents.DeleteAsync(contentA_1.Id, options); - }); + await Assert.ThrowsAnyAsync<SquidexException>(() => + { + return _.Contents.DeleteAsync(contentA_1.Id, options); + }); - // STEP 4: Delete without referrer check - await _.Contents.DeleteAsync(contentA_1.Id); - } + // STEP 4: Delete without referrer check + await _.Contents.DeleteAsync(contentA_1.Id); + } - [Fact] - public async Task Should_not_unpublish_when_referenced() + [Fact] + public async Task Should_not_unpublish_when_referenced() + { + // STEP 1: Create a published referenced content. + var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData { - // STEP 1: Create a published referenced content. - var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = null - }, ContentCreateOptions.AsPublish); + References = null + }, ContentCreateOptions.AsPublish); - // STEP 2: Create a content with a reference. - await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = new[] { contentA_1.Id } - }, ContentCreateOptions.AsPublish); + // STEP 2: Create a content with a reference. + await _.Contents.CreateAsync(new TestEntityWithReferencesData + { + References = new[] { contentA_1.Id } + }, ContentCreateOptions.AsPublish); - // STEP 3: Try to ThrowsAnyAsync with referrer check. - await Assert.ThrowsAnyAsync<SquidexException>(() => + // STEP 3: Try to ThrowsAnyAsync with referrer check. + await Assert.ThrowsAnyAsync<SquidexException>(() => + { + return _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus { - return _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus - { - Status = "Draft", - // Ensure that the flag is true. - CheckReferrers = true - }); + Status = "Draft", + // Ensure that the flag is true. + CheckReferrers = true }); + }); - // STEP 4: Delete without referrer check - await _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus - { - Status = "Draft", - // It is the default anyway, just to make it more explicit. - CheckReferrers = false - }); - } + // STEP 4: Delete without referrer check + await _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus + { + Status = "Draft", + // It is the default anyway, just to make it more explicit. + CheckReferrers = false + }); + } - [Fact] - public async Task Should_not_delete_with_bulk_when_referenced() + [Fact] + public async Task Should_not_delete_with_bulk_when_referenced() + { + // STEP 1: Create a referenced content. + var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData { - // STEP 1: Create a referenced content. - var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = null - }, ContentCreateOptions.AsPublish); + References = null + }, ContentCreateOptions.AsPublish); - // STEP 2: Create a content with a reference. - await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = new[] { contentA_1.Id } - }, ContentCreateOptions.AsPublish); + // STEP 2: Create a content with a reference. + await _.Contents.CreateAsync(new TestEntityWithReferencesData + { + References = new[] { contentA_1.Id } + }, ContentCreateOptions.AsPublish); - // STEP 3: Try to delete with referrer check. - var result1 = await _.Contents.BulkUpdateAsync(new BulkUpdate + // STEP 3: Try to delete with referrer check. + var result1 = await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob - { - Id = contentA_1.Id, - Type = BulkUpdateType.Delete, - Status = "Draft" - } - }, - CheckReferrers = true - }); + Id = contentA_1.Id, + Type = BulkUpdateType.Delete, + Status = "Draft" + } + }, + CheckReferrers = true + }); - Assert.NotNull(result1[0].Error); + Assert.NotNull(result1[0].Error); - // STEP 4: Delete without referrer check - var result2 = await _.Contents.BulkUpdateAsync(new BulkUpdate + // STEP 4: Delete without referrer check + var result2 = await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob - { - Id = contentA_1.Id, - Type = BulkUpdateType.Delete, - Status = "Draft" - } - }, - CheckReferrers = false - }); - - Assert.Null(result2[0].Error); - } + Id = contentA_1.Id, + Type = BulkUpdateType.Delete, + Status = "Draft" + } + }, + CheckReferrers = false + }); + + Assert.Null(result2[0].Error); + } - [Fact] - public async Task Should_not_unpublish_with_bulk_when_referenced() + [Fact] + public async Task Should_not_unpublish_with_bulk_when_referenced() + { + // STEP 1: Create a published referenced content. + var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData { - // STEP 1: Create a published referenced content. - var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = null - }, ContentCreateOptions.AsPublish); + References = null + }, ContentCreateOptions.AsPublish); - // STEP 2: Create a published content with a reference. - await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = new[] { contentA_1.Id } - }, ContentCreateOptions.AsPublish); + // STEP 2: Create a published content with a reference. + await _.Contents.CreateAsync(new TestEntityWithReferencesData + { + References = new[] { contentA_1.Id } + }, ContentCreateOptions.AsPublish); - // STEP 3: Try to delete with referrer check. - var result1 = await _.Contents.BulkUpdateAsync(new BulkUpdate + // STEP 3: Try to delete with referrer check. + var result1 = await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob - { - Id = contentA_1.Id, - Type = BulkUpdateType.ChangeStatus, - Status = "Draft" - } - }, - CheckReferrers = true - }); + Id = contentA_1.Id, + Type = BulkUpdateType.ChangeStatus, + Status = "Draft" + } + }, + CheckReferrers = true + }); - Assert.NotNull(result1[0].Error); + Assert.NotNull(result1[0].Error); - // STEP 4: Delete without referrer check - var result2 = await _.Contents.BulkUpdateAsync(new BulkUpdate + // STEP 4: Delete without referrer check + var result2 = await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob - { - Id = contentA_1.Id, - Type = BulkUpdateType.ChangeStatus, - Status = "Draft" - } - }, - CheckReferrers = false - }); - - Assert.Null(result2[0].Error); - } + Id = contentA_1.Id, + Type = BulkUpdateType.ChangeStatus, + Status = "Draft" + } + }, + CheckReferrers = false + }); + + Assert.Null(result2[0].Error); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs index 1a3519d9ae..9604bb3771 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs @@ -13,190 +13,189 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class ContentScriptingTests : IClassFixture<CreatedAppFixture> { - public class ContentScriptingTests : IClassFixture<CreatedAppFixture> - { - private readonly string schemaName = $"schema-{Guid.NewGuid()}"; + private readonly string schemaName = $"schema-{Guid.NewGuid()}"; - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public ContentScriptingTests(CreatedAppFixture fixture) - { - _ = fixture; - } + public ContentScriptingTests(CreatedAppFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_create_content_with_scripting() + [Fact] + public async Task Should_create_content_with_scripting() + { + var scripts = new SchemaScriptsDto { - var scripts = new SchemaScriptsDto - { - Create = @$" + Create = @$" ctx.data.{TestEntityData.NumberField}.iv *= 2; replace()" - }; + }; - // STEP 1: Create a schema. - await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); + // STEP 1: Create a schema. + await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); - // STEP 2: Create content - var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); + // STEP 2: Create content + var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); - var content = await contents.CreateAsync(new TestEntityData - { - Number = 13 - }); + var content = await contents.CreateAsync(new TestEntityData + { + Number = 13 + }); - Assert.Equal(26, content.Data.Number); - } + Assert.Equal(26, content.Data.Number); + } - [Fact] - public async Task Should_query_content_with_scripting() + [Fact] + public async Task Should_query_content_with_scripting() + { + var scripts = new SchemaScriptsDto { - var scripts = new SchemaScriptsDto - { - Query = @$" + Query = @$" ctx.data.{TestEntityData.NumberField}.iv *= 2; replace()", - }; + }; - // STEP 1: Create a schema. - await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); + // STEP 1: Create a schema. + await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); - // STEP 2: Create content - var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); + // STEP 2: Create content + var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); - var content = await contents.CreateAsync(new TestEntityData - { - Number = 13 - }, ContentCreateOptions.AsPublish); + var content = await contents.CreateAsync(new TestEntityData + { + Number = 13 + }, ContentCreateOptions.AsPublish); - Assert.Equal(26, content.Data.Number); - } + Assert.Equal(26, content.Data.Number); + } - [Fact] - public async Task Should_query_content_with_scripting_and_pre_query() + [Fact] + public async Task Should_query_content_with_scripting_and_pre_query() + { + var scripts = new SchemaScriptsDto { - var scripts = new SchemaScriptsDto - { - QueryPre = @$" + QueryPre = @$" ctx.test = 17", - Query = @$" + Query = @$" ctx.data.{TestEntityData.NumberField}.iv = ctx.test + 2; replace()", - }; + }; - // STEP 1: Create a schema. - await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); + // STEP 1: Create a schema. + await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); - // STEP 2: Create content - var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); + // STEP 2: Create content + var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); - var content = await contents.CreateAsync(new TestEntityData - { - Number = 99 - }, ContentCreateOptions.AsPublish); + var content = await contents.CreateAsync(new TestEntityData + { + Number = 99 + }, ContentCreateOptions.AsPublish); - Assert.Equal(19, content.Data.Number); - } + Assert.Equal(19, content.Data.Number); + } - [Fact] - public async Task Should_create_bulk_content_with_scripting() + [Fact] + public async Task Should_create_bulk_content_with_scripting() + { + // STEP 1: Create a schema. + var scripts = new SchemaScriptsDto { - // STEP 1: Create a schema. - var scripts = new SchemaScriptsDto - { - Create = @$" + Create = @$" ctx.data.{TestEntityData.NumberField}.iv = incrementCounter('${schemaName}'); replace()" - }; + }; - await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); + await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); - // STEP 2: Create content with a value that triggers the schema. - var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); + // STEP 2: Create content with a value that triggers the schema. + var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); - var results = await contents.BulkUpdateAsync(new BulkUpdate + var results = await contents.BulkUpdateAsync(new BulkUpdate + { + DoNotScript = false, + DoNotValidate = false, + Jobs = new List<BulkUpdateJob> { - DoNotScript = false, - DoNotValidate = false, - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob + Type = BulkUpdateType.Upsert, + Data = new { - Type = BulkUpdateType.Upsert, - Data = new + number = new { - number = new - { - iv = 99 - } + iv = 99 } } - }, - Publish = true - }); + } + }, + Publish = true + }); - Assert.Single(results); - Assert.Null(results[0].Error); + Assert.Single(results); + Assert.Null(results[0].Error); - // STEP 2: Query content. - var content = await contents.GetAsync(results[0].ContentId); + // STEP 2: Query content. + var content = await contents.GetAsync(results[0].ContentId); - Assert.True(content.Data.Number > 0); - } + Assert.True(content.Data.Number > 0); + } - [Fact] - public async Task Should_create_bulk_content_with_scripting_but_disabled() + [Fact] + public async Task Should_create_bulk_content_with_scripting_but_disabled() + { + // STEP 1: Create a schema. + var scripts = new SchemaScriptsDto { - // STEP 1: Create a schema. - var scripts = new SchemaScriptsDto - { - Create = @$" + Create = @$" ctx.data.{TestEntityData.NumberField}.iv = incrementCounter('${schemaName}'); replace()" - }; + }; - await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); + await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName, scripts); - // STEP 1: Create content with a value that triggers the schema. - var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); + // STEP 1: Create content with a value that triggers the schema. + var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); - var results = await contents.BulkUpdateAsync(new BulkUpdate + var results = await contents.BulkUpdateAsync(new BulkUpdate + { + DoNotScript = true, + DoNotValidate = false, + Jobs = new List<BulkUpdateJob> { - DoNotScript = true, - DoNotValidate = false, - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob + Type = BulkUpdateType.Upsert, + Data = new { - Type = BulkUpdateType.Upsert, - Data = new + number = new { - number = new - { - iv = 99 - } + iv = 99 } } - }, - Publish = true - }); + } + }, + Publish = true + }); - Assert.Single(results); - Assert.Null(results[0].Error); + Assert.Single(results); + Assert.Null(results[0].Error); - // STEP 2: Query content. - var content = await contents.GetAsync(results[0].ContentId); + // STEP 2: Query content. + var content = await contents.GetAsync(results[0].ContentId); - Assert.Equal(99, content.Data.Number); - } + Assert.Equal(99, content.Data.Number); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index 7b615a07fa..404d895e29 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -14,1235 +14,1234 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public class ContentUpdateTests : IClassFixture<ContentFixture> { - [UsesVerify] - public class ContentUpdateTests : IClassFixture<ContentFixture> - { - public ContentFixture _ { get; } + public ContentFixture _ { get; } - public ContentUpdateTests(ContentFixture fixture) - { - _ = fixture; - } + public ContentUpdateTests(ContentFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_return_published_content() + [Fact] + public async Task Should_return_published_content() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create the item unpublished. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create the item unpublished. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }); + Number = 1 + }); - // STEP 2: Publish the item. - await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus - { - Status = "Published" - }); + // STEP 2: Publish the item. + await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus + { + Status = "Published" + }); - // STEP 3: Retrieve the item. - await _.Contents.GetAsync(content.Id); - } - finally + // STEP 3: Retrieve the item. + await _.Contents.GetAsync(content.Id); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_not_return_archived_content() + [Fact] + public async Task Should_not_return_archived_content() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create the item published. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create the item published. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }, ContentCreateOptions.AsPublish); + Number = 1 + }, ContentCreateOptions.AsPublish); - // STEP 2: Archive the item. - await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus - { - Status = "Archived" - }); + // STEP 2: Archive the item. + await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus + { + Status = "Archived" + }); - // STEP 3. Get a 404 for the item because it is not published anymore. - await Assert.ThrowsAnyAsync<SquidexException>(() => - { - return _.Contents.GetAsync(content.Id); - }); - } - finally + // STEP 3. Get a 404 for the item because it is not published anymore. + await Assert.ThrowsAnyAsync<SquidexException>(() => { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + return _.Contents.GetAsync(content.Id); + }); + } + finally + { + if (content != null) + { + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_not_return_unpublished_content() + [Fact] + public async Task Should_not_return_unpublished_content() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create the item unpublished. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create the item unpublished. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }); + Number = 1 + }); - // STEP 2: Change the status to publiushed and then to draft. - await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus - { - Status = "Published" - }); - await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus - { - Status = "Draft" - }); + // STEP 2: Change the status to publiushed and then to draft. + await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus + { + Status = "Published" + }); + await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus + { + Status = "Draft" + }); - // STEP 3. Get a 404 for the item because it is not published anymore. - await Assert.ThrowsAnyAsync<SquidexException>(() => - { - return _.Contents.GetAsync(content.Id); - }); - } - finally + // STEP 3. Get a 404 for the item because it is not published anymore. + await Assert.ThrowsAnyAsync<SquidexException>(() => { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + return _.Contents.GetAsync(content.Id); + }); + } + finally + { + if (content != null) + { + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_create_strange_text() - { - const string text = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"; + [Fact] + public async Task Should_create_strange_text() + { + const string text = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"; - TestEntity content = null; - try + TestEntity content = null; + try + { + // STEP 1: Create a content item with a text that caused a bug before. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a content item with a text that caused a bug before. - content = await _.Contents.CreateAsync(new TestEntityData - { - String = text - }, ContentCreateOptions.AsPublish); + String = text + }, ContentCreateOptions.AsPublish); - // STEP 2: Get the item and ensure that the text is the same. - var queried = await _.Contents.GetAsync(content.Id); + // STEP 2: Get the item and ensure that the text is the same. + var queried = await _.Contents.GetAsync(content.Id); - Assert.Equal(text, queried.Data.String); + Assert.Equal(text, queried.Data.String); - await Verify(queried); - } - finally + await Verify(queried); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_create_null_text() + [Fact] + public async Task Should_create_null_text() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a content item with a text that caused a bug before. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a content item with a text that caused a bug before. - content = await _.Contents.CreateAsync(new TestEntityData + Localized = new Dictionary<string, string> { - Localized = new Dictionary<string, string> - { - ["en"] = null - } - }, ContentCreateOptions.AsPublish); + ["en"] = null + } + }, ContentCreateOptions.AsPublish); - // STEP 2: Get the item and ensure that the text is the same. - var queried = await _.Contents.GetAsync(content.Id, QueryContext.Default.IgnoreFallback()); + // STEP 2: Get the item and ensure that the text is the same. + var queried = await _.Contents.GetAsync(content.Id, QueryContext.Default.IgnoreFallback()); - Assert.Null(queried.Data.Localized["en"]); + Assert.Null(queried.Data.Localized["en"]); - await Verify(queried); - } - finally + await Verify(queried); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_create_json_with_dot() + [Fact] + public async Task Should_create_json_with_dot() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a content item with a text that caused a bug before. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a content item with a text that caused a bug before. - content = await _.Contents.CreateAsync(new TestEntityData + Json = new JObject { - Json = new JObject - { - ["field.with.dot"] = 42 - } - }, ContentCreateOptions.AsPublish); + ["field.with.dot"] = 42 + } + }, ContentCreateOptions.AsPublish); - // STEP 2: Get the item and ensure that the text is the same. - var queried = await _.Contents.GetAsync(content.Id, QueryContext.Default.IgnoreFallback()); + // STEP 2: Get the item and ensure that the text is the same. + var queried = await _.Contents.GetAsync(content.Id, QueryContext.Default.IgnoreFallback()); - Assert.Equal(42, (int)queried.Data.Json["field.with.dot"]); + Assert.Equal(42, (int)queried.Data.Json["field.with.dot"]); - await Verify(queried); - } - finally + await Verify(queried); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_create_default_text() + [Fact] + public async Task Should_create_default_text() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a content item with a text that caused a bug before. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a content item with a text that caused a bug before. - content = await _.Contents.CreateAsync(new TestEntityData - { - Localized = new Dictionary<string, string>() - }, ContentCreateOptions.AsPublish); + Localized = new Dictionary<string, string>() + }, ContentCreateOptions.AsPublish); - // STEP 2: Get the item and ensure that the text is the same. - var updated = await _.Contents.GetAsync(content.Id); + // STEP 2: Get the item and ensure that the text is the same. + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal("default", updated.Data.Localized["en"]); - } - finally + Assert.Equal("default", updated.Data.Localized["en"]); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_create_non_published_content() + [Fact] + public async Task Should_create_non_published_content() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create the item unpublished. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create the item unpublished. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }); + Number = 1 + }); - // STEP 2. Get a 404 for the item because it is not published. - await Assert.ThrowsAnyAsync<SquidexException>(() => - { - return _.Contents.GetAsync(content.Id); - }); + // STEP 2. Get a 404 for the item because it is not published. + await Assert.ThrowsAnyAsync<SquidexException>(() => + { + return _.Contents.GetAsync(content.Id); + }); - await Verify(content); - } - finally + await Verify(content); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_create_published_content() + [Fact] + public async Task Should_create_published_content() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create the item published. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create the item published. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }, ContentCreateOptions.AsPublish); + Number = 1 + }, ContentCreateOptions.AsPublish); - // STEP 2: Get the item. - await _.Contents.GetAsync(content.Id); - } - finally + // STEP 2: Get the item. + await _.Contents.GetAsync(content.Id); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } + + [Fact] + public async Task Should_create_content_with_custom_id() + { + var id = Guid.NewGuid().ToString(); - [Fact] - public async Task Should_create_content_with_custom_id() + TestEntity content = null; + try { - var id = Guid.NewGuid().ToString(); + // STEP 1: Create a new item with a custom id. + var options = new ContentCreateOptions { Id = id, Publish = true }; - TestEntity content = null; - try + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item with a custom id. - var options = new ContentCreateOptions { Id = id, Publish = true }; - - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }, options); + Number = 1 + }, options); - Assert.Equal(id, content.Id); - } - finally + Assert.Equal(id, content.Id); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } + + [Fact] + public async Task Should_not_create_content_with_custom_id_twice() + { + var id = Guid.NewGuid().ToString(); - [Fact] - public async Task Should_not_create_content_with_custom_id_twice() + TestEntity content = null; + try { - var id = Guid.NewGuid().ToString(); + // STEP 1: Create a new item with a custom id. + var options = new ContentCreateOptions { Id = id, Publish = true }; - TestEntity content = null; - try + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item with a custom id. - var options = new ContentCreateOptions { Id = id, Publish = true }; + Number = 1 + }, options); - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }, options); + Assert.Equal(id, content.Id); - Assert.Equal(id, content.Id); - - // STEP 2: Create a new item with a custom id. - var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => + // STEP 2: Create a new item with a custom id. + var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => + { + return _.Contents.CreateAsync(new TestEntityData { - return _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }, options); - }); + Number = 1 + }, options); + }); - Assert.Equal(409, ex.StatusCode); - } - finally + Assert.Equal(409, ex.StatusCode); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_create_content_with_custom_id_and_upsert() - { - var id = Guid.NewGuid().ToString(); + [Fact] + public async Task Should_create_content_with_custom_id_and_upsert() + { + var id = Guid.NewGuid().ToString(); - TestEntity content = null; - try + TestEntity content = null; + try + { + // STEP 1: Upsert a new item with a custom id. + content = await _.Contents.UpsertAsync(id, new TestEntityData { - // STEP 1: Upsert a new item with a custom id. - content = await _.Contents.UpsertAsync(id, new TestEntityData - { - Number = 1 - }, ContentUpsertOptions.AsPublish); + Number = 1 + }, ContentUpsertOptions.AsPublish); - Assert.Equal(id, content.Id); + Assert.Equal(id, content.Id); - // STEP 2: Make an update with the upsert endpoint. - content = await _.Contents.UpsertAsync(id, new TestEntityData - { - Number = 2 - }); + // STEP 2: Make an update with the upsert endpoint. + content = await _.Contents.UpsertAsync(id, new TestEntityData + { + Number = 2 + }); - Assert.Equal(2, content.Data.Number); + Assert.Equal(2, content.Data.Number); - // STEP 3: Make an update with the update endpoint. - content = await _.Contents.UpdateAsync(id, new TestEntityData - { - Number = 3 - }); + // STEP 3: Make an update with the update endpoint. + content = await _.Contents.UpdateAsync(id, new TestEntityData + { + Number = 3 + }); - Assert.Equal(3, content.Data.Number); - } - finally + Assert.Equal(3, content.Data.Number); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_update_content() + [Fact] + public async Task Should_update_content() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 2 - }, ContentCreateOptions.AsPublish); + Number = 2 + }, ContentCreateOptions.AsPublish); - // STEP 2: Update the item and ensure that the data has changed. - await _.Contents.UpdateAsync(content.Id, new TestEntityData - { - Number = 2 - }); + // STEP 2: Update the item and ensure that the data has changed. + await _.Contents.UpdateAsync(content.Id, new TestEntityData + { + Number = 2 + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, content.Data.Number); - } - finally + Assert.Equal(2, content.Data.Number); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_update_content_in_parallel() + [Fact] + public async Task Should_update_content_in_parallel() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 2 - }, ContentCreateOptions.AsPublish); + Number = 2 + }, ContentCreateOptions.AsPublish); - var numErrors = 0; - var numSuccess = 0; + var numErrors = 0; + var numSuccess = 0; - // STEP 3: Make parallel updates. - await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) => + // STEP 3: Make parallel updates. + await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) => + { + try { - try + await _.Contents.UpdateAsync(content.Id, new TestEntityData { - await _.Contents.UpdateAsync(content.Id, new TestEntityData - { - Number = i - }); + Number = i + }); - Interlocked.Increment(ref numSuccess); - } - catch (SquidexException ex) when (ex.StatusCode is 409 or 412) - { - Interlocked.Increment(ref numErrors); - return; - } - }); + Interlocked.Increment(ref numSuccess); + } + catch (SquidexException ex) when (ex.StatusCode is 409 or 412) + { + Interlocked.Increment(ref numErrors); + return; + } + }); - // At least some errors and success should have happened. - Assert.True(numErrors >= 0); - Assert.True(numSuccess >= 0); + // At least some errors and success should have happened. + Assert.True(numErrors >= 0); + Assert.True(numSuccess >= 0); - // STEP 3: Make an normal update to ensure nothing is corrupt. - await _.Contents.UpdateAsync(content.Id, new TestEntityData - { - Number = 2 - }); + // STEP 3: Make an normal update to ensure nothing is corrupt. + await _.Contents.UpdateAsync(content.Id, new TestEntityData + { + Number = 2 + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, content.Data.Number); - } - finally + Assert.Equal(2, content.Data.Number); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_upsert_content_in_parallel() + [Fact] + public async Task Should_upsert_content_in_parallel() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 2 - }, ContentCreateOptions.AsPublish); + Number = 2 + }, ContentCreateOptions.AsPublish); - var numErrors = 0; - var numSuccess = 0; + var numErrors = 0; + var numSuccess = 0; - // STEP 3: Make parallel upserts. - await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) => + // STEP 3: Make parallel upserts. + await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) => + { + try { - try + await _.Contents.UpsertAsync(content.Id, new TestEntityData { - await _.Contents.UpsertAsync(content.Id, new TestEntityData - { - Number = i - }); + Number = i + }); - Interlocked.Increment(ref numSuccess); - } - catch (SquidexException ex) when (ex.StatusCode is 409 or 412) - { - Interlocked.Increment(ref numErrors); - return; - } - }); + Interlocked.Increment(ref numSuccess); + } + catch (SquidexException ex) when (ex.StatusCode is 409 or 412) + { + Interlocked.Increment(ref numErrors); + return; + } + }); - // At least some errors and success should have happened. - Assert.True(numErrors > 0); - Assert.True(numSuccess >= 0); + // At least some errors and success should have happened. + Assert.True(numErrors > 0); + Assert.True(numSuccess >= 0); - // STEP 3: Make an normal update to ensure nothing is corrupt. - await _.Contents.UpdateAsync(content.Id, new TestEntityData - { - Number = 2 - }); + // STEP 3: Make an normal update to ensure nothing is corrupt. + await _.Contents.UpdateAsync(content.Id, new TestEntityData + { + Number = 2 + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, content.Data.Number); - } - finally + Assert.Equal(2, content.Data.Number); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_update_content_to_null() + [Fact] + public async Task Should_update_content_to_null() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - String = "initial" - }, ContentCreateOptions.AsPublish); + String = "initial" + }, ContentCreateOptions.AsPublish); - // STEP 2: Update the item and ensure that the data has changed. - await _.Contents.UpdateAsync(content.Id, new TestEntityData - { - String = null - }); + // STEP 2: Update the item and ensure that the data has changed. + await _.Contents.UpdateAsync(content.Id, new TestEntityData + { + String = null + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Null(updated.Data.String); - } - finally + Assert.Null(updated.Data.String); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_patch_content() + [Fact] + public async Task Should_patch_content() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - String = "test" - }, ContentCreateOptions.AsPublish); + String = "test" + }, ContentCreateOptions.AsPublish); - // STEP 2: Patch an item. - await _.Contents.PatchAsync(content.Id, new TestEntityData - { - Number = 1 - }); + // STEP 2: Patch an item. + await _.Contents.PatchAsync(content.Id, new TestEntityData + { + Number = 1 + }); - // STEP 3: Update the item and ensure that the data has changed. - await _.Contents.PatchAsync(content.Id, new TestEntityData - { - Number = 2 - }); + // STEP 3: Update the item and ensure that the data has changed. + await _.Contents.PatchAsync(content.Id, new TestEntityData + { + Number = 2 + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, updated.Data.Number); + Assert.Equal(2, updated.Data.Number); - // Should not change other value with patch. - Assert.Equal("test", updated.Data.String); + // Should not change other value with patch. + Assert.Equal("test", updated.Data.String); - await Verify(updated); - } - finally + await Verify(updated); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_patch_id_data_value() + [Fact] + public async Task Should_patch_id_data_value() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - Id = "id1" - }, ContentCreateOptions.AsPublish); + Id = "id1" + }, ContentCreateOptions.AsPublish); - // STEP 2: Update the item and ensure that the data has changed. - await _.Contents.PatchAsync(content.Id, new TestEntityData - { - Id = "id2" - }); + // STEP 2: Update the item and ensure that the data has changed. + await _.Contents.PatchAsync(content.Id, new TestEntityData + { + Id = "id2" + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal("id2", updated.Data.Id); + Assert.Equal("id2", updated.Data.Id); - await Verify(updated); - } - finally + await Verify(updated); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_patch_content_to_null() + [Fact] + public async Task Should_patch_content_to_null() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - String = "initial" - }, ContentCreateOptions.AsPublish); + String = "initial" + }, ContentCreateOptions.AsPublish); - // STEP 2: Update the item and ensure that the data has changed. - await _.Contents.PatchAsync(content.Id, new + // STEP 2: Update the item and ensure that the data has changed. + await _.Contents.PatchAsync(content.Id, new + { + @string = new { - @string = new - { - iv = (object)null - } - }); + iv = (object)null + } + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Null(updated.Data.String); + Assert.Null(updated.Data.String); - await Verify(updated); - } - finally + await Verify(updated); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_patch_content_with_upsert() + [Fact] + public async Task Should_patch_content_with_upsert() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - String = "test" - }, ContentCreateOptions.AsPublish); + String = "test" + }, ContentCreateOptions.AsPublish); - // STEP 2: Patch an item. - await _.Contents.UpsertAsync(content.Id, new TestEntityData - { - Number = 1 - }, ContentUpsertOptions.AsPatch); + // STEP 2: Patch an item. + await _.Contents.UpsertAsync(content.Id, new TestEntityData + { + Number = 1 + }, ContentUpsertOptions.AsPatch); - // STEP 3: Update the item and ensure that the data has changed. - await _.Contents.UpsertAsync(content.Id, new TestEntityData - { - Number = 2 - }, ContentUpsertOptions.AsPatch); + // STEP 3: Update the item and ensure that the data has changed. + await _.Contents.UpsertAsync(content.Id, new TestEntityData + { + Number = 2 + }, ContentUpsertOptions.AsPatch); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, updated.Data.Number); + Assert.Equal(2, updated.Data.Number); - // Should not change other value with patch. - Assert.Equal("test", updated.Data.String); + // Should not change other value with patch. + Assert.Equal("test", updated.Data.String); - await Verify(updated); - } - finally + await Verify(updated); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_patch_content_with_bulk() + [Fact] + public async Task Should_patch_content_with_bulk() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - String = "test" - }, ContentCreateOptions.AsPublish); + String = "test" + }, ContentCreateOptions.AsPublish); - // STEP 2: Patch an item. - await _.Contents.BulkUpdateAsync(new BulkUpdate + // STEP 2: Patch an item. + await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob + Id = content.Id, + Data = new { - Id = content.Id, - Data = new + number = new { - number = new - { - iv = 1 - } - }, - Patch = true - } + iv = 1 + } + }, + Patch = true } - }); + } + }); - // STEP 3: Update the item and ensure that the data has changed. - await _.Contents.BulkUpdateAsync(new BulkUpdate + // STEP 3: Update the item and ensure that the data has changed. + await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob + Id = content.Id, + Data = new { - Id = content.Id, - Data = new + number = new { - number = new - { - iv = 2 - } - }, - Patch = true - } + iv = 2 + } + }, + Patch = true } - }); + } + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, updated.Data.Number); + Assert.Equal(2, updated.Data.Number); - // Should not change other value with patch. - Assert.Equal("test", updated.Data.String); + // Should not change other value with patch. + Assert.Equal("test", updated.Data.String); - await Verify(updated); - } - finally + await Verify(updated); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_update_content_with_bulk_and_overriden_schema_name() + [Fact] + public async Task Should_update_content_with_bulk_and_overriden_schema_name() + { + TestEntity content = null; + try { - TestEntity content = null; - try - { - var schemaName = $"schema-{Guid.NewGuid()}"; + var schemaName = $"schema-{Guid.NewGuid()}"; - // STEP 0: Create dummy schema. - var createSchema = new CreateSchemaDto - { - Name = schemaName, + // STEP 0: Create dummy schema. + var createSchema = new CreateSchemaDto + { + Name = schemaName, - // Publish it to avoid validations issues. - IsPublished = true - }; + // Publish it to avoid validations issues. + IsPublished = true + }; - await _.Schemas.PostSchemaAsync(_.AppName, createSchema); + await _.Schemas.PostSchemaAsync(_.AppName, createSchema); - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - String = "test" - }, ContentCreateOptions.AsPublish); + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData + { + String = "test" + }, ContentCreateOptions.AsPublish); - // STEP 2: Patch an item. - var client = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); + // STEP 2: Patch an item. + var client = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(schemaName); - await client.BulkUpdateAsync(new BulkUpdate + await client.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob + Id = content.Id, + Data = new { - Id = content.Id, - Data = new + number = new { - number = new - { - iv = 1 - } - }, - Schema = _.SchemaName - } + iv = 1 + } + }, + Schema = _.SchemaName } - }); + } + }); - // STEP 3: Update the item and ensure that the data has changed. - await client.BulkUpdateAsync(new BulkUpdate + // STEP 3: Update the item and ensure that the data has changed. + await client.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob + Id = content.Id, + Data = new { - Id = content.Id, - Data = new + number = new { - number = new - { - iv = 2 - } - }, - Schema = _.SchemaName - } + iv = 2 + } + }, + Schema = _.SchemaName } - }); + } + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, updated.Data.Number); - } - finally + Assert.Equal(2, updated.Data.Number); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_update_content_with_bulk_and_shared_client() + [Fact] + public async Task Should_update_content_with_bulk_and_shared_client() + { + TestEntity content = null; + try { - TestEntity content = null; - try - { - var schemaName = $"schema-{Guid.NewGuid()}"; + var schemaName = $"schema-{Guid.NewGuid()}"; - // STEP 0: Create dummy schema. - var createSchema = new CreateSchemaDto - { - Name = schemaName, + // STEP 0: Create dummy schema. + var createSchema = new CreateSchemaDto + { + Name = schemaName, - // Publish it to avoid validations issues. - IsPublished = true - }; + // Publish it to avoid validations issues. + IsPublished = true + }; - await _.Schemas.PostSchemaAsync(_.AppName, createSchema); + await _.Schemas.PostSchemaAsync(_.AppName, createSchema); - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - String = "test" - }, ContentCreateOptions.AsPublish); + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData + { + String = "test" + }, ContentCreateOptions.AsPublish); - // STEP 2: Patch an item. - await _.SharedContents.BulkUpdateAsync(new BulkUpdate + // STEP 2: Patch an item. + await _.SharedContents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob + Id = content.Id, + Data = new { - Id = content.Id, - Data = new + number = new { - number = new - { - iv = 1 - } - }, - Schema = _.SchemaName - } + iv = 1 + } + }, + Schema = _.SchemaName } - }); + } + }); - // STEP 3: Update the item and ensure that the data has changed. - await _.SharedContents.BulkUpdateAsync(new BulkUpdate + // STEP 3: Update the item and ensure that the data has changed. + await _.SharedContents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List<BulkUpdateJob> { - Jobs = new List<BulkUpdateJob> + new BulkUpdateJob { - new BulkUpdateJob + Id = content.Id, + Data = new { - Id = content.Id, - Data = new + number = new { - number = new - { - iv = 2 - } - }, - Schema = _.SchemaName - } + iv = 2 + } + }, + Schema = _.SchemaName } - }); + } + }); - var updated = await _.Contents.GetAsync(content.Id); + var updated = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, updated.Data.Number); - } - finally + Assert.Equal(2, updated.Data.Number); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Fact] - public async Task Should_create_draft_version() + [Fact] + public async Task Should_create_draft_version() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }, ContentCreateOptions.AsPublish); + Number = 1 + }, ContentCreateOptions.AsPublish); - // STEP 2: Create draft. - content = await _.Contents.CreateDraftAsync(content.Id); + // STEP 2: Create draft. + content = await _.Contents.CreateDraftAsync(content.Id); - // STEP 3: Update the item and ensure that the data has not changed. - await _.Contents.PatchAsync(content.Id, new TestEntityData - { - Number = 2 - }); + // STEP 3: Update the item and ensure that the data has not changed. + await _.Contents.PatchAsync(content.Id, new TestEntityData + { + Number = 2 + }); - var updated_1 = await _.Contents.GetAsync(content.Id); + var updated_1 = await _.Contents.GetAsync(content.Id); - Assert.Equal(1, updated_1.Data.Number); + Assert.Equal(1, updated_1.Data.Number); - // STEP 4: Get the unpublished version - var unpublished = await _.Contents.GetAsync(content.Id, QueryContext.Default.Unpublished()); + // STEP 4: Get the unpublished version + var unpublished = await _.Contents.GetAsync(content.Id, QueryContext.Default.Unpublished()); - Assert.Equal(2, unpublished.Data.Number); + Assert.Equal(2, unpublished.Data.Number); - // STEP 5: Publish draft and ensure that it has been updated. - await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus - { - Status = "Published" - }); + // STEP 5: Publish draft and ensure that it has been updated. + await _.Contents.ChangeStatusAsync(content.Id, new ChangeStatus + { + Status = "Published" + }); - var updated_2 = await _.Contents.GetAsync(content.Id); + var updated_2 = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, updated_2.Data.Number); - } - finally + Assert.Equal(2, updated_2.Data.Number); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_delete_content(bool permanent) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_delete_content(bool permanent) + { + // STEP 1: Create a new item. + var content_1 = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - var content_1 = await _.Contents.CreateAsync(new TestEntityData - { - Number = 2 - }, ContentCreateOptions.AsPublish); + Number = 2 + }, ContentCreateOptions.AsPublish); - // STEP 2: Delete the item. - await _.Contents.DeleteAsync(content_1.Id, new ContentDeleteOptions { Permanent = permanent }); + // STEP 2: Delete the item. + await _.Contents.DeleteAsync(content_1.Id, new ContentDeleteOptions { Permanent = permanent }); - // STEP 3: Retrieve all items and ensure that the deleted item does not exist. - var updated = await _.Contents.GetAsync(); + // STEP 3: Retrieve all items and ensure that the deleted item does not exist. + var updated = await _.Contents.GetAsync(); - Assert.DoesNotContain(updated.Items, x => x.Id == content_1.Id); + Assert.DoesNotContain(updated.Items, x => x.Id == content_1.Id); - // STEP 4: Retrieve all deleted items and check if found. - var q = new ContentQuery { Filter = "isDeleted eq true" }; + // STEP 4: Retrieve all deleted items and check if found. + var q = new ContentQuery { Filter = "isDeleted eq true" }; - var deleted = await _.Contents.GetAsync(q, QueryContext.Default.Unpublished(true)); + var deleted = await _.Contents.GetAsync(q, QueryContext.Default.Unpublished(true)); - Assert.Equal(!permanent, deleted.Items.Any(x => x.Id == content_1.Id)); - } + Assert.Equal(!permanent, deleted.Items.Any(x => x.Id == content_1.Id)); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_recreate_deleted_content(bool permanent) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_recreate_deleted_content(bool permanent) + { + // STEP 1: Create a new item. + var content_1 = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - var content_1 = await _.Contents.CreateAsync(new TestEntityData - { - Number = 2 - }, ContentCreateOptions.AsPublish); + Number = 2 + }, ContentCreateOptions.AsPublish); - // STEP 2: Delete the item. - var createOptions = new ContentDeleteOptions { Permanent = permanent }; + // STEP 2: Delete the item. + var createOptions = new ContentDeleteOptions { Permanent = permanent }; - await _.Contents.DeleteAsync(content_1.Id, createOptions); + await _.Contents.DeleteAsync(content_1.Id, createOptions); - // STEP 3: Recreate the item with the same id. - var deleteOptions = new ContentCreateOptions { Id = content_1.Id, Publish = true }; + // STEP 3: Recreate the item with the same id. + var deleteOptions = new ContentCreateOptions { Id = content_1.Id, Publish = true }; - var content_2 = await _.Contents.CreateAsync(new TestEntityData - { - Number = 2 - }, deleteOptions); + var content_2 = await _.Contents.CreateAsync(new TestEntityData + { + Number = 2 + }, deleteOptions); - Assert.Equal(Status.Published, content_2.Status); + Assert.Equal(Status.Published, content_2.Status); - // STEP 4: Check if we can find it again with a query. - var q = new ContentQuery { Filter = $"id eq '{content_1.Id}'" }; + // STEP 4: Check if we can find it again with a query. + var q = new ContentQuery { Filter = $"id eq '{content_1.Id}'" }; - var contents_4 = await _.Contents.GetAsync(q); + var contents_4 = await _.Contents.GetAsync(q); - Assert.NotNull(contents_4.Items.Find(x => x.Id == content_1.Id)); - } + Assert.NotNull(contents_4.Items.Find(x => x.Id == content_1.Id)); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_recreate_deleted_content_with_upsert(bool permanent) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_recreate_deleted_content_with_upsert(bool permanent) + { + // STEP 1: Create a new item. + var content_1 = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - var content_1 = await _.Contents.CreateAsync(new TestEntityData - { - Number = 2 - }, ContentCreateOptions.AsPublish); + Number = 2 + }, ContentCreateOptions.AsPublish); - // STEP 2: Delete the item. - var deleteOptions = new ContentDeleteOptions { Permanent = permanent }; + // STEP 2: Delete the item. + var deleteOptions = new ContentDeleteOptions { Permanent = permanent }; - await _.Contents.DeleteAsync(content_1.Id, deleteOptions); + await _.Contents.DeleteAsync(content_1.Id, deleteOptions); - // STEP 3: Recreate the item with the same id. - var content_2 = await _.Contents.UpsertAsync(content_1.Id, new TestEntityData - { - Number = 2 - }, ContentUpsertOptions.AsPublish); + // STEP 3: Recreate the item with the same id. + var content_2 = await _.Contents.UpsertAsync(content_1.Id, new TestEntityData + { + Number = 2 + }, ContentUpsertOptions.AsPublish); - Assert.Equal(Status.Published, content_2.Status); + Assert.Equal(Status.Published, content_2.Status); - // STEP 4: Check if we can find it again with a query. - var q = new ContentQuery { Filter = $"id eq '{content_1.Id}'" }; + // STEP 4: Check if we can find it again with a query. + var q = new ContentQuery { Filter = $"id eq '{content_1.Id}'" }; - var contents_4 = await _.Contents.GetAsync(q); + var contents_4 = await _.Contents.GetAsync(q); - Assert.NotNull(contents_4.Items.Find(x => x.Id == content_1.Id)); - } + Assert.NotNull(contents_4.Items.Find(x => x.Id == content_1.Id)); + } - [Fact] - public async Task Should_update_singleton_content_with_special_id() - { - var schemaName = $"schema-{Guid.NewGuid()}"; + [Fact] + public async Task Should_update_singleton_content_with_special_id() + { + var schemaName = $"schema-{Guid.NewGuid()}"; - // STEP 1: Create singleton. - var createRequest = new CreateSchemaDto + // STEP 1: Create singleton. + var createRequest = new CreateSchemaDto + { + Name = schemaName, + IsPublished = true, + IsSingleton = true, + Fields = new List<UpsertSchemaFieldDto> { - Name = schemaName, - IsPublished = true, - IsSingleton = true, - Fields = new List<UpsertSchemaFieldDto> + new UpsertSchemaFieldDto { - new UpsertSchemaFieldDto - { - Name = "my-field", - Properties = new StringFieldPropertiesDto() - } + Name = "my-field", + Properties = new StringFieldPropertiesDto() } - }; + } + }; - await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - var client = _.ClientManager.CreateDynamicContentsClient(schemaName); + var client = _.ClientManager.CreateDynamicContentsClient(schemaName); - // STEP 2: Get content. - var content_1 = await client.GetAsync("_schemaId_"); + // STEP 2: Get content. + var content_1 = await client.GetAsync("_schemaId_"); - Assert.NotNull(content_1); + Assert.NotNull(content_1); - // STEP 3: Update content. - var content_2 = await client.UpdateAsync("_schemaId_", new DynamicData + // STEP 3: Update content. + var content_2 = await client.UpdateAsync("_schemaId_", new DynamicData + { + ["my-field"] = new JObject { - ["my-field"] = new JObject - { - ["iv"] = "singleton" - } - }); + ["iv"] = "singleton" + } + }); - Assert.Equal("singleton", content_2.Data["my-field"]["iv"]); + Assert.Equal("singleton", content_2.Data["my-field"]["iv"]); - await Verify(content_2); - } + await Verify(content_2); + } - [Fact] - public async Task Should_get_content_by_version() + [Fact] + public async Task Should_get_content_by_version() + { + TestEntity content = null; + try { - TestEntity content = null; - try + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a new item. - content = await _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }, ContentCreateOptions.AsPublish); + Number = 1 + }, ContentCreateOptions.AsPublish); - // STEP 2: Update content. - content = await _.Contents.UpdateAsync(content.Id, new TestEntityData - { - Number = 2 - }); + // STEP 2: Update content. + content = await _.Contents.UpdateAsync(content.Id, new TestEntityData + { + Number = 2 + }); - // STEP 3: Get current version. - var content_latest = await _.Contents.GetAsync(content.Id); + // STEP 3: Get current version. + var content_latest = await _.Contents.GetAsync(content.Id); - Assert.Equal(2, content_latest.Data.Number); + Assert.Equal(2, content_latest.Data.Number); - // STEP 4: Get current version. - var data_2 = await _.Contents.GetDataAsync(content.Id, content.Version); + // STEP 4: Get current version. + var data_2 = await _.Contents.GetDataAsync(content.Id, content.Version); - Assert.Equal(2, data_2.Number); + Assert.Equal(2, data_2.Number); - // STEP 4: Get previous version - var data_1 = await _.Contents.GetDataAsync(content.Id, content.Version - 1); + // STEP 4: Get previous version + var data_1 = await _.Contents.GetDataAsync(content.Id, content.Version - 1); - Assert.Equal(1, data_1.Number); + Assert.Equal(1, data_1.Number); - await Verify(data_1); - } - finally + await Verify(data_1); + } + finally + { + if (content != null) { - if (content != null) - { - await _.Contents.DeleteAsync(content.Id); - } + await _.Contents.DeleteAsync(content.Id); } } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/DiagnosticsTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/DiagnosticsTests.cs index 47275ad6bc..7f3646880d 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/DiagnosticsTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/DiagnosticsTests.cs @@ -9,21 +9,20 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class DiagnosticsTests : IClassFixture<CreatedAppFixture> { - public class DiagnosticsTests : IClassFixture<CreatedAppFixture> - { - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public DiagnosticsTests(CreatedAppFixture fixture) - { - _ = fixture; - } + public DiagnosticsTests(CreatedAppFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_create_gc_dump() - { - await _.Diagnostics.GetGCDumpAsync(); - } + [Fact] + public async Task Should_create_gc_dump() + { + await _.Diagnostics.GetGCDumpAsync(); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/FrontendTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/FrontendTests.cs index 96b8046399..09ca646fba 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/FrontendTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/FrontendTests.cs @@ -11,51 +11,50 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public sealed class FrontendTests : IClassFixture<ClientFixture> { - public sealed class FrontendTests : IClassFixture<ClientFixture> + public ClientFixture _ { get; } + + public FrontendTests(ClientFixture fixture) { - public ClientFixture _ { get; } + _ = fixture; + } - public FrontendTests(ClientFixture fixture) + [Theory] + [InlineData("Frontend_Home", "")] + [InlineData("Frontend_Login", "identity-server/account/login")] + public async Task Should_render_properly(string name, string url) + { + using (var browserFetcher = new BrowserFetcher()) { - _ = fixture; + await browserFetcher.DownloadAsync(); } - [Theory] - [InlineData("Frontend_Home", "")] - [InlineData("Frontend_Login", "identity-server/account/login")] - public async Task Should_render_properly(string name, string url) + await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { - using (var browserFetcher = new BrowserFetcher()) + Headless = true, + DefaultViewport = new ViewPortOptions + { + Height = 800, + IsLandscape = true, + IsMobile = false, + Width = 1000 + }, + Args = new string[] { - await browserFetcher.DownloadAsync(); + "--no-sandbox" } + }); - await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions - { - Headless = true, - DefaultViewport = new ViewPortOptions - { - Height = 800, - IsLandscape = true, - IsMobile = false, - Width = 1000 - }, - Args = new string[] - { - "--no-sandbox" - } - }); - - await using var page = await browser.NewPageAsync(); - - await page.GoToAsync(_.ClientManager.Options.Url + url + "?skip-setup"); - await page.ScreenshotAsync($"__{name}.jpg"); - - var diff = ImageSharpCompare.CalcDiff($"__{name}.jpg", $"Assets/{name}.jpg"); - - Assert.InRange(diff.MeanError, 0, 10); - } + await using var page = await browser.NewPageAsync(); + + await page.GoToAsync(_.ClientManager.Options.Url + url + "?skip-setup"); + await page.ScreenshotAsync($"__{name}.jpg"); + + var diff = ImageSharpCompare.CalcDiff($"__{name}.jpg", $"Assets/{name}.jpg"); + + Assert.InRange(diff.MeanError, 0, 10); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs index ad80217e26..b9ef3008b3 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs @@ -13,76 +13,76 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public sealed class GraphQLTests : IClassFixture<ContentFixture> { - public sealed class GraphQLTests : IClassFixture<ContentFixture> - { - public ContentFixture _ { get; } + public ContentFixture _ { get; } - public GraphQLTests(ContentFixture fixture) - { - _ = fixture; - } + public GraphQLTests(ContentFixture fixture) + { + _ = fixture; + } - public sealed class DynamicEntity : Content<object> - { - } + public sealed class DynamicEntity : Content<object> + { + } - public sealed class Country - { - public CountryData Data { get; set; } - } + public sealed class Country + { + public CountryData Data { get; set; } + } - public sealed class CountryData - { - public string Name { get; set; } + public sealed class CountryData + { + public string Name { get; set; } - public List<State> States { get; set; } - } + public List<State> States { get; set; } + } - public sealed class State - { - public StateData Data { get; set; } - } + public sealed class State + { + public StateData Data { get; set; } + } - public sealed class StateData - { - public string Name { get; set; } + public sealed class StateData + { + public string Name { get; set; } - public List<City> Cities { get; set; } - } + public List<City> Cities { get; set; } + } - public sealed class City - { - public CityData Data { get; set; } - } + public sealed class City + { + public CityData Data { get; set; } + } - public sealed class CityData - { - public string Name { get; set; } - } + public sealed class CityData + { + public string Name { get; set; } + } - [Fact] - public async Task Should_query_json() + [Fact] + public async Task Should_query_json() + { + // STEP 1: Create a content with JSON. + var content_0 = await _.Contents.CreateAsync(new TestEntityData { - // STEP 1: Create a content with JSON. - var content_0 = await _.Contents.CreateAsync(new TestEntityData + Json = JToken.FromObject(new { - Json = JToken.FromObject(new + value = 1, + obj = new { - value = 1, - obj = new - { - value = 2 - } - }) - }, ContentCreateOptions.AsPublish); + value = 2 + } + }) + }, ContentCreateOptions.AsPublish); - // STEP 2: Query this content. - var query = new - { - query = @" + // STEP 2: Query this content. + var query = new + { + query = @" { findMyWritesContent(id: ""<ID>"") { flatData { @@ -90,38 +90,38 @@ public async Task Should_query_json() } } }".Replace("<ID>", content_0.Id, StringComparison.Ordinal) - }; + }; - var result1 = await _.SharedContents.GraphQlAsync<JToken>(query); + var result1 = await _.SharedContents.GraphQlAsync<JToken>(query); - Assert.Equal(1, result1["findMyWritesContent"]["flatData"]["json"]["value"].Value<int>()); - Assert.Equal(2, result1["findMyWritesContent"]["flatData"]["json"]["obj"]["value"].Value<int>()); - } + Assert.Equal(1, result1["findMyWritesContent"]["flatData"]["json"]["value"].Value<int>()); + Assert.Equal(2, result1["findMyWritesContent"]["flatData"]["json"]["obj"]["value"].Value<int>()); + } - [Fact] - public async Task Should_create_and_query_with_graphql() + [Fact] + public async Task Should_create_and_query_with_graphql() + { + try { - try - { - await CreateSchemasAsync(); - } - catch - { - // Do nothing - } + await CreateSchemasAsync(); + } + catch + { + // Do nothing + } - try - { - await CreateContentsAsync(); - } - catch - { - // Do nothing - } + try + { + await CreateContentsAsync(); + } + catch + { + // Do nothing + } - var query = new - { - query = @" + var query = new + { + query = @" { queryCountriesContents { data: flatData { @@ -139,137 +139,136 @@ public async Task Should_create_and_query_with_graphql() } } }" - }; + }; - var result1 = await _.SharedContents.GraphQlAsync<JToken>(query); + var result1 = await _.SharedContents.GraphQlAsync<JToken>(query); - var typed = result1["queryCountriesContents"].ToObject<List<Country>>(); + var typed = result1["queryCountriesContents"].ToObject<List<Country>>(); - Assert.Equal("Leipzig", typed[0].Data.States[0].Data.Cities[0].Data.Name); - } + Assert.Equal("Leipzig", typed[0].Data.States[0].Data.Cities[0].Data.Name); + } - private async Task CreateSchemasAsync() + private async Task CreateSchemasAsync() + { + // STEP 1: Create cities schema. + var createCitiesRequest = new CreateSchemaDto { - // STEP 1: Create cities schema. - var createCitiesRequest = new CreateSchemaDto + Name = "cities", + Fields = new List<UpsertSchemaFieldDto> { - Name = "cities", - Fields = new List<UpsertSchemaFieldDto> + new UpsertSchemaFieldDto { - new UpsertSchemaFieldDto - { - Name = "name", - Properties = new StringFieldPropertiesDto() - } - }, - IsPublished = true - }; + Name = "name", + Properties = new StringFieldPropertiesDto() + } + }, + IsPublished = true + }; - var cities = await _.Schemas.PostSchemaAsync(_.AppName, createCitiesRequest); + var cities = await _.Schemas.PostSchemaAsync(_.AppName, createCitiesRequest); - // STEP 2: Create states schema. - var createStatesRequest = new CreateSchemaDto + // STEP 2: Create states schema. + var createStatesRequest = new CreateSchemaDto + { + Name = "states", + Fields = new List<UpsertSchemaFieldDto> { - Name = "states", - Fields = new List<UpsertSchemaFieldDto> + new UpsertSchemaFieldDto { - new UpsertSchemaFieldDto - { - Name = "name", - Properties = new StringFieldPropertiesDto() - }, - new UpsertSchemaFieldDto + Name = "name", + Properties = new StringFieldPropertiesDto() + }, + new UpsertSchemaFieldDto + { + Name = "cities", + Properties = new ReferencesFieldPropertiesDto { - Name = "cities", - Properties = new ReferencesFieldPropertiesDto - { - SchemaIds = new List<string> { cities.Id } - } + SchemaIds = new List<string> { cities.Id } } - }, - IsPublished = true - }; + } + }, + IsPublished = true + }; - var states = await _.Schemas.PostSchemaAsync(_.AppName, createStatesRequest); + var states = await _.Schemas.PostSchemaAsync(_.AppName, createStatesRequest); - // STEP 3: Create countries schema. - var createCountriesRequest = new CreateSchemaDto + // STEP 3: Create countries schema. + var createCountriesRequest = new CreateSchemaDto + { + Name = "countries", + Fields = new List<UpsertSchemaFieldDto> { - Name = "countries", - Fields = new List<UpsertSchemaFieldDto> + new UpsertSchemaFieldDto { - new UpsertSchemaFieldDto - { - Name = "name", - Properties = new StringFieldPropertiesDto() - }, - new UpsertSchemaFieldDto + Name = "name", + Properties = new StringFieldPropertiesDto() + }, + new UpsertSchemaFieldDto + { + Name = "states", + Properties = new ReferencesFieldPropertiesDto { - Name = "states", - Properties = new ReferencesFieldPropertiesDto - { - SchemaIds = new List<string> { states.Id } - } + SchemaIds = new List<string> { states.Id } } - }, - IsPublished = true - }; + } + }, + IsPublished = true + }; - await _.Schemas.PostSchemaAsync(_.AppName, createCountriesRequest); - } + await _.Schemas.PostSchemaAsync(_.AppName, createCountriesRequest); + } - private async Task CreateContentsAsync() + private async Task CreateContentsAsync() + { + // STEP 1: Create city + var cityData = new { - // STEP 1: Create city - var cityData = new + name = new { - name = new - { - iv = "Leipzig" - } - }; + iv = "Leipzig" + } + }; - var citiesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("cities"); + var citiesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("cities"); - var city = await citiesClient.CreateAsync(cityData, ContentCreateOptions.AsPublish); + var city = await citiesClient.CreateAsync(cityData, ContentCreateOptions.AsPublish); - // STEP 2: Create city - var stateData = new + // STEP 2: Create city + var stateData = new + { + name = new { - name = new - { - iv = "Saxony" - }, - cities = new - { - iv = new[] { city.Id } - } - }; + iv = "Saxony" + }, + cities = new + { + iv = new[] { city.Id } + } + }; - var statesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("states"); + var statesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("states"); - var state = await statesClient.CreateAsync(stateData, ContentCreateOptions.AsPublish); + var state = await statesClient.CreateAsync(stateData, ContentCreateOptions.AsPublish); - // STEP 3: Create country - var countryData = new + // STEP 3: Create country + var countryData = new + { + name = new { - name = new - { - iv = "Germany" - }, - states = new - { - iv = new[] { state.Id } - } - }; + iv = "Germany" + }, + states = new + { + iv = new[] { state.Id } + } + }; - var countriesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("countries"); + var countriesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("countries"); - await countriesClient.CreateAsync(countryData, ContentCreateOptions.AsPublish); - } + await countriesClient.CreateAsync(countryData, ContentCreateOptions.AsPublish); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/HistoryTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/HistoryTests.cs index 888b8fc19a..57de1e0e8e 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/HistoryTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/HistoryTests.cs @@ -9,23 +9,22 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class HistoryTests : IClassFixture<CreatedAppFixture> { - public class HistoryTests : IClassFixture<CreatedAppFixture> - { - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public HistoryTests(CreatedAppFixture fixture) - { - _ = fixture; - } + public HistoryTests(CreatedAppFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_get_history() - { - var history = await _.History.WaitForHistoryAsync(_.AppName, null, x => true, TimeSpan.FromSeconds(30)); + [Fact] + public async Task Should_get_history() + { + var history = await _.History.WaitForHistoryAsync(_.AppName, null, x => true, TimeSpan.FromSeconds(30)); - Assert.NotEmpty(history); - } + Assert.NotEmpty(history); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/LanguagesTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/LanguagesTests.cs index ad9ea1af17..a9e2cd7d9f 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/LanguagesTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/LanguagesTests.cs @@ -9,23 +9,22 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class LanguagesTests : IClassFixture<ClientFixture> { - public class LanguagesTests : IClassFixture<ClientFixture> - { - public ClientFixture _ { get; } + public ClientFixture _ { get; } - public LanguagesTests(ClientFixture fixture) - { - _ = fixture; - } + public LanguagesTests(ClientFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_provide_languages() - { - var languages = await _.Languages.GetLanguagesAsync(); + [Fact] + public async Task Should_provide_languages() + { + var languages = await _.Languages.GetLanguagesAsync(); - Assert.True(languages.Count > 100); - } + Assert.True(languages.Count > 100); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/OpenApiTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/OpenApiTests.cs index 4fea145e5a..2431ace8a1 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/OpenApiTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/OpenApiTests.cs @@ -9,45 +9,44 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class OpenApiTests : IClassFixture<ContentFixture> { - public class OpenApiTests : IClassFixture<ContentFixture> - { - public ContentFixture _ { get; } + public ContentFixture _ { get; } - public OpenApiTests(ContentFixture fixture) - { - _ = fixture; - } + public OpenApiTests(ContentFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_provide_general_spec() - { - var url = $"{_.ClientManager.Options.Url}api/swagger/v1/swagger.json"; + [Fact] + public async Task Should_provide_general_spec() + { + var url = $"{_.ClientManager.Options.Url}api/swagger/v1/swagger.json"; - var document = await OpenApiDocument.FromUrlAsync(url); + var document = await OpenApiDocument.FromUrlAsync(url); - Assert.NotNull(document); - } + Assert.NotNull(document); + } - [Fact] - public async Task Should_provide_content_spec() - { - var url = $"{_.ClientManager.Options.Url}api/content/{_.AppName}/swagger/v1/swagger.json"; + [Fact] + public async Task Should_provide_content_spec() + { + var url = $"{_.ClientManager.Options.Url}api/content/{_.AppName}/swagger/v1/swagger.json"; - var document = await OpenApiDocument.FromUrlAsync(url); + var document = await OpenApiDocument.FromUrlAsync(url); - Assert.NotNull(document); - } + Assert.NotNull(document); + } - [Fact] - public async Task Should_provide_flat_content_spec() - { - var url = $"{_.ClientManager.Options.Url}api/content/{_.AppName}/flat/swagger/v1/swagger.json"; + [Fact] + public async Task Should_provide_flat_content_spec() + { + var url = $"{_.ClientManager.Options.Url}api/content/{_.AppName}/flat/swagger/v1/swagger.json"; - var document = await OpenApiDocument.FromUrlAsync(url); + var document = await OpenApiDocument.FromUrlAsync(url); - Assert.NotNull(document); - } + Assert.NotNull(document); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/PingTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/PingTests.cs index 7e294c4f5a..4847d5a732 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/PingTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/PingTests.cs @@ -9,35 +9,34 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class PingTests : IClassFixture<CreatedAppFixture> { - public class PingTests : IClassFixture<CreatedAppFixture> + public CreatedAppFixture _ { get; } + + public PingTests(CreatedAppFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_ping_service() + { + await _.Ping.GetPingAsync(); + } + + [Fact] + public async Task Should_ping_app() + { + await _.Ping.GetAppPingAsync(_.AppName); + } + + [Fact] + public async Task Should_get_info() { - public CreatedAppFixture _ { get; } - - public PingTests(CreatedAppFixture fixture) - { - _ = fixture; - } - - [Fact] - public async Task Should_ping_service() - { - await _.Ping.GetPingAsync(); - } - - [Fact] - public async Task Should_ping_app() - { - await _.Ping.GetAppPingAsync(_.AppName); - } - - [Fact] - public async Task Should_get_info() - { - var infos = await _.Ping.GetInfoAsync(); - - Assert.NotNull(infos); - } + var infos = await _.Ping.GetInfoAsync(); + + Assert.NotNull(infos); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/PlansTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/PlansTests.cs index 3e22028b05..3f9af1a96c 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/PlansTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/PlansTests.cs @@ -9,23 +9,22 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class PlansTests : IClassFixture<CreatedAppFixture> { - public class PlansTests : IClassFixture<CreatedAppFixture> - { - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public PlansTests(CreatedAppFixture fixture) - { - _ = fixture; - } + public PlansTests(CreatedAppFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_get_plans() - { - var plans = await _.Plans.GetPlansAsync(_.AppName); + [Fact] + public async Task Should_get_plans() + { + var plans = await _.Plans.GetPlansAsync(_.AppName); - Assert.NotNull(plans); - } + Assert.NotNull(plans); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs index a46464cbed..24aa152daa 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs @@ -13,302 +13,301 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class RuleRunnerTests : IClassFixture<ClientFixture>, IClassFixture<WebhookCatcherFixture> { - public class RuleRunnerTests : IClassFixture<ClientFixture>, IClassFixture<WebhookCatcherFixture> - { - private readonly string secret = Guid.NewGuid().ToString(); - private readonly string appName = Guid.NewGuid().ToString(); - private readonly string schemaName = $"schema-{Guid.NewGuid()}"; - private readonly string contentString = Guid.NewGuid().ToString(); - private readonly WebhookCatcherClient webhookCatcher; + private readonly string secret = Guid.NewGuid().ToString(); + private readonly string appName = Guid.NewGuid().ToString(); + private readonly string schemaName = $"schema-{Guid.NewGuid()}"; + private readonly string contentString = Guid.NewGuid().ToString(); + private readonly WebhookCatcherClient webhookCatcher; - public ClientFixture _ { get; } + public ClientFixture _ { get; } - public RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhookCatcher) - { - _ = fixture; + public RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhookCatcher) + { + _ = fixture; - this.webhookCatcher = webhookCatcher.Client; - } + this.webhookCatcher = webhookCatcher.Client; + } - [Fact] - public async Task Should_run_rules_on_content_change() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_run_rules_on_content_change() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Start webhook session - var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); + // STEP 1: Start webhook session + var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); - // STEP 2: Create rule - var createRule = new CreateRuleDto + // STEP 2: Create rule + var createRule = new CreateRuleDto + { + Action = new WebhookRuleActionDto + { + SharedSecret = secret, + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri(url) + }, + Trigger = new ContentChangedRuleTriggerDto { - Action = new WebhookRuleActionDto - { - SharedSecret = secret, - Method = WebhookMethod.POST, - Payload = null, - PayloadType = null, - Url = new Uri(url) - }, - Trigger = new ContentChangedRuleTriggerDto - { - HandleAll = true - } - }; + HandleAll = true + } + }; - var rule = await _.Rules.PostRuleAsync(appName, createRule); + var rule = await _.Rules.PostRuleAsync(appName, createRule); - // STEP 3: Create test content - await CreateContentAsync(); + // STEP 3: Create test content + await CreateContentAsync(); - // Get requests. - var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2)); - var request = requests.FirstOrDefault(x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); + // Get requests. + var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2)); + var request = requests.FirstOrDefault(x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(request); - Assert.NotNull(request.Headers["X-Signature"]); - Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); + Assert.NotNull(request); + Assert.NotNull(request.Headers["X-Signature"]); + Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); - // STEP 4: Get events - var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); - var eventsRule = await _.Rules.GetEventsAsync(appName); + // STEP 4: Get events + var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); + var eventsRule = await _.Rules.GetEventsAsync(appName); - Assert.Single(eventsAll.Items); - Assert.Single(eventsRule.Items); - } + Assert.Single(eventsAll.Items); + Assert.Single(eventsRule.Items); + } - [Fact] - public async Task Should_run_rules_on_asset_change() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_run_rules_on_asset_change() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Start webhook session - var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); + // STEP 1: Start webhook session + var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); - // STEP 2: Create rule - var createRule = new CreateRuleDto + // STEP 2: Create rule + var createRule = new CreateRuleDto + { + Action = new WebhookRuleActionDto { - Action = new WebhookRuleActionDto - { - SharedSecret = secret, - Method = WebhookMethod.POST, - Payload = null, - PayloadType = null, - Url = new Uri(url) - }, - Trigger = new AssetChangedRuleTriggerDto() - }; + SharedSecret = secret, + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri(url) + }, + Trigger = new AssetChangedRuleTriggerDto() + }; - var rule = await _.Rules.PostRuleAsync(appName, createRule); + var rule = await _.Rules.PostRuleAsync(appName, createRule); - // STEP 3: Create test asset - await CreateAssetAsync(); + // STEP 3: Create test asset + await CreateAssetAsync(); - // Get requests. - var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2)); - var request = requests.FirstOrDefault(x => x.Method == "POST" && x.Content.Contains("logo-squared", StringComparison.OrdinalIgnoreCase)); + // Get requests. + var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2)); + var request = requests.FirstOrDefault(x => x.Method == "POST" && x.Content.Contains("logo-squared", StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(request); - Assert.NotNull(request.Headers["X-Signature"]); - Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); + Assert.NotNull(request); + Assert.NotNull(request.Headers["X-Signature"]); + Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); - // STEP 4: Get events - var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); - var eventsRule = await _.Rules.GetEventsAsync(appName); + // STEP 4: Get events + var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); + var eventsRule = await _.Rules.GetEventsAsync(appName); - Assert.Single(eventsAll.Items); - Assert.Single(eventsRule.Items); - } + Assert.Single(eventsAll.Items); + Assert.Single(eventsRule.Items); + } - [Fact] - public async Task Should_run_rules_on_schema_change() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_run_rules_on_schema_change() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Start webhook session - var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); + // STEP 1: Start webhook session + var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); - // STEP 2: Create rule - var createRule = new CreateRuleDto + // STEP 2: Create rule + var createRule = new CreateRuleDto + { + Action = new WebhookRuleActionDto { - Action = new WebhookRuleActionDto - { - SharedSecret = secret, - Method = WebhookMethod.POST, - Payload = null, - PayloadType = null, - Url = new Uri(url) - }, - Trigger = new SchemaChangedRuleTriggerDto() - }; + SharedSecret = secret, + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri(url) + }, + Trigger = new SchemaChangedRuleTriggerDto() + }; - var rule = await _.Rules.PostRuleAsync(appName, createRule); + var rule = await _.Rules.PostRuleAsync(appName, createRule); - // STEP 3: Create test schema - await TestEntity.CreateSchemaAsync(_.Schemas, appName, schemaName); + // STEP 3: Create test schema + await TestEntity.CreateSchemaAsync(_.Schemas, appName, schemaName); - // Get requests. - var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2)); - var request = requests.FirstOrDefault(x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); + // Get requests. + var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromMinutes(2)); + var request = requests.FirstOrDefault(x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(request); - Assert.NotNull(request.Headers["X-Signature"]); - Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); + Assert.NotNull(request); + Assert.NotNull(request.Headers["X-Signature"]); + Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); - // STEP 4: Get events - var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); - var eventsRule = await _.Rules.GetEventsAsync(appName); + // STEP 4: Get events + var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); + var eventsRule = await _.Rules.GetEventsAsync(appName); - Assert.Single(eventsAll.Items); - Assert.Single(eventsRule.Items); - } + Assert.Single(eventsAll.Items); + Assert.Single(eventsRule.Items); + } - [Fact] - public async Task Should_run_rule_manually() - { - // STEP 0: Create app. - await CreateAppAsync(); + [Fact] + public async Task Should_run_rule_manually() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Start webhook session - var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); + // STEP 1: Start webhook session + var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); - // STEP 2: Create rule - var createRule = new CreateRuleDto + // STEP 2: Create rule + var createRule = new CreateRuleDto + { + Action = new WebhookRuleActionDto { - Action = new WebhookRuleActionDto - { - SharedSecret = secret, - Method = WebhookMethod.POST, - Payload = null, - PayloadType = null, - Url = new Uri(url) - }, - Trigger = new ManualRuleTriggerDto() - }; + SharedSecret = secret, + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri(url) + }, + Trigger = new ManualRuleTriggerDto() + }; - var rule = await _.Rules.PostRuleAsync(appName, createRule); + var rule = await _.Rules.PostRuleAsync(appName, createRule); - // STEP 3: Trigger rule - await _.Rules.TriggerRuleAsync(appName, rule.Id); + // STEP 3: Trigger rule + await _.Rules.TriggerRuleAsync(appName, rule.Id); - // Get requests. - var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromSeconds(30)); - var request = requests.FirstOrDefault(x => x.Method == "POST"); + // Get requests. + var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromSeconds(30)); + var request = requests.FirstOrDefault(x => x.Method == "POST"); - Assert.NotNull(request); - Assert.NotNull(request.Headers["X-Signature"]); - Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); + Assert.NotNull(request); + Assert.NotNull(request.Headers["X-Signature"]); + Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); - // STEP 4: Get events - var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); - var eventsRule = await _.Rules.GetEventsAsync(appName); + // STEP 4: Get events + var eventsAll = await _.Rules.GetEventsAsync(appName, rule.Id); + var eventsRule = await _.Rules.GetEventsAsync(appName); - Assert.Single(eventsAll.Items); - Assert.Single(eventsRule.Items); - } + Assert.Single(eventsAll.Items); + Assert.Single(eventsRule.Items); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_rerun_rules(bool fromSnapshots) - { - // STEP 0: Create app. - await CreateAppAsync(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_rerun_rules(bool fromSnapshots) + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Start webhook session - var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); + // STEP 1: Start webhook session + var (url, sessionId) = await webhookCatcher.CreateSessionAsync(); - // STEP 2: Create disabled rule - var createRule = new CreateRuleDto + // STEP 2: Create disabled rule + var createRule = new CreateRuleDto + { + Action = new WebhookRuleActionDto + { + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri(url) + }, + Trigger = new ContentChangedRuleTriggerDto { - Action = new WebhookRuleActionDto - { - Method = WebhookMethod.POST, - Payload = null, - PayloadType = null, - Url = new Uri(url) - }, - Trigger = new ContentChangedRuleTriggerDto - { - HandleAll = true - } - }; + HandleAll = true + } + }; - var rule = await _.Rules.PostRuleAsync(appName, createRule); + var rule = await _.Rules.PostRuleAsync(appName, createRule); - // Disable rule, so that we do not create the event from the rule itself. - await _.Rules.DisableRuleAsync(appName, rule.Id); + // Disable rule, so that we do not create the event from the rule itself. + await _.Rules.DisableRuleAsync(appName, rule.Id); - // STEP 3: Create test content before rule - await CreateContentAsync(); + // STEP 3: Create test content before rule + await CreateContentAsync(); - // STEP 4: Run rule. - await _.Rules.PutRuleRunAsync(appName, rule.Id, fromSnapshots); + // STEP 4: Run rule. + await _.Rules.PutRuleRunAsync(appName, rule.Id, fromSnapshots); - // Get requests. - var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromSeconds(30)); + // Get requests. + var requests = await webhookCatcher.WaitForRequestsAsync(sessionId, TimeSpan.FromSeconds(30)); - Assert.Contains(requests, x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); - } + Assert.Contains(requests, x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); + } - private async Task CreateContentAsync() - { - await TestEntity.CreateSchemaAsync(_.Schemas, appName, schemaName); + private async Task CreateContentAsync() + { + await TestEntity.CreateSchemaAsync(_.Schemas, appName, schemaName); - // Create a test content. - var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(appName, schemaName); + // Create a test content. + var contents = _.ClientManager.CreateContentsClient<TestEntity, TestEntityData>(appName, schemaName); - await contents.CreateAsync(new TestEntityData - { - String = contentString - }); - } - - private async Task CreateAssetAsync() + await contents.CreateAsync(new TestEntityData { - // Upload a test asset - var fileInfo = new FileInfo("Assets/logo-squared.png"); + String = contentString + }); + } - await using (var stream = fileInfo.OpenRead()) - { - var upload = new FileParameter(stream, fileInfo.Name, "image/png"); + private async Task CreateAssetAsync() + { + // Upload a test asset + var fileInfo = new FileInfo("Assets/logo-squared.png"); - await _.Assets.PostAssetAsync(appName, file: upload); - } + await using (var stream = fileInfo.OpenRead()) + { + var upload = new FileParameter(stream, fileInfo.Name, "image/png"); + + await _.Assets.PostAssetAsync(appName, file: upload); } + } - private async Task CreateAppAsync() + private async Task CreateAppAsync() + { + var createRequest = new CreateAppDto { - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - await _.Apps.PostAppAsync(createRequest); - } + await _.Apps.PostAppAsync(createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/RuleTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/RuleTests.cs index 7915756001..ac7723322b 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/RuleTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/RuleTests.cs @@ -11,156 +11,155 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public class RuleTests : IClassFixture<ClientFixture> { - [UsesVerify] - public class RuleTests : IClassFixture<ClientFixture> + private readonly string appName = Guid.NewGuid().ToString(); + private readonly string ruleName = Guid.NewGuid().ToString(); + + public ClientFixture _ { get; } + + public RuleTests(ClientFixture fixture) { - private readonly string appName = Guid.NewGuid().ToString(); - private readonly string ruleName = Guid.NewGuid().ToString(); + _ = fixture; + } - public ClientFixture _ { get; } + [Fact] + public async Task Should_create_rule() + { + // STEP 0: Create app. + await CreateAppAsync(); - public RuleTests(ClientFixture fixture) - { - _ = fixture; - } - [Fact] - public async Task Should_create_rule() + // STEP 1: Create rule + var createRule = new CreateRuleDto { - // STEP 0: Create app. - await CreateAppAsync(); + Action = new WebhookRuleActionDto + { + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri("http://squidex.io") + }, + Trigger = new ContentChangedRuleTriggerDto + { + HandleAll = true + } + }; + var rule = await _.Rules.PostRuleAsync(appName, createRule); - // STEP 1: Create rule - var createRule = new CreateRuleDto - { - Action = new WebhookRuleActionDto - { - Method = WebhookMethod.POST, - Payload = null, - PayloadType = null, - Url = new Uri("http://squidex.io") - }, - Trigger = new ContentChangedRuleTriggerDto - { - HandleAll = true - } - }; - - var rule = await _.Rules.PostRuleAsync(appName, createRule); - - Assert.IsType<WebhookRuleActionDto>(rule.Action); - - await Verify(rule); - } - - [Fact] - public async Task Should_update_rule() - { - // STEP 0: Create app. - await CreateAppAsync(); + Assert.IsType<WebhookRuleActionDto>(rule.Action); + + await Verify(rule); + } + + [Fact] + public async Task Should_update_rule() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Create rule - var createRequest = new CreateRuleDto + // STEP 1: Create rule + var createRequest = new CreateRuleDto + { + Action = new WebhookRuleActionDto { - Action = new WebhookRuleActionDto - { - Method = WebhookMethod.POST, - Payload = null, - PayloadType = null, - Url = new Uri("http://squidex.io") - }, - Trigger = new ContentChangedRuleTriggerDto - { - HandleAll = true - } - }; - - var rule_0 = await _.Rules.PostRuleAsync(appName, createRequest); - - - // STEP 2: Update rule - var updateRequest = new UpdateRuleDto + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri("http://squidex.io") + }, + Trigger = new ContentChangedRuleTriggerDto { - Name = ruleName - }; - - var rule_1 = await _.Rules.PutRuleAsync(appName, rule_0.Id, updateRequest); + HandleAll = true + } + }; - Assert.Equal(ruleName, rule_1.Name); + var rule_0 = await _.Rules.PostRuleAsync(appName, createRequest); - await Verify(rule_1); - } - [Fact] - public async Task Should_delete_rule() + // STEP 2: Update rule + var updateRequest = new UpdateRuleDto { - // STEP 0: Create app. - await CreateAppAsync(); + Name = ruleName + }; + + var rule_1 = await _.Rules.PutRuleAsync(appName, rule_0.Id, updateRequest); + + Assert.Equal(ruleName, rule_1.Name); + await Verify(rule_1); + } + + [Fact] + public async Task Should_delete_rule() + { + // STEP 0: Create app. + await CreateAppAsync(); - // STEP 1: Create rule - var createRequest = new CreateRuleDto + + // STEP 1: Create rule + var createRequest = new CreateRuleDto + { + Action = new WebhookRuleActionDto { - Action = new WebhookRuleActionDto - { - Method = WebhookMethod.POST, - Payload = null, - PayloadType = null, - Url = new Uri("http://squidex.io") - }, - Trigger = new ContentChangedRuleTriggerDto - { - HandleAll = true - } - }; + Method = WebhookMethod.POST, + Payload = null, + PayloadType = null, + Url = new Uri("http://squidex.io") + }, + Trigger = new ContentChangedRuleTriggerDto + { + HandleAll = true + } + }; - var rule = await _.Rules.PostRuleAsync(appName, createRequest); + var rule = await _.Rules.PostRuleAsync(appName, createRequest); - // STEP 2: Delete rule - await _.Rules.DeleteRuleAsync(appName, rule.Id); + // STEP 2: Delete rule + await _.Rules.DeleteRuleAsync(appName, rule.Id); - var rules = await _.Rules.GetRulesAsync(appName); + var rules = await _.Rules.GetRulesAsync(appName); - Assert.DoesNotContain(rules.Items, x => x.Id == rule.Id); - } + Assert.DoesNotContain(rules.Items, x => x.Id == rule.Id); + } - [Fact] - public async Task Should_get_actions() - { - var actions = await _.Rules.GetActionsAsync(); + [Fact] + public async Task Should_get_actions() + { + var actions = await _.Rules.GetActionsAsync(); - Assert.NotEmpty(actions); - } + Assert.NotEmpty(actions); + } - [Fact] - public async Task Should_get_event_schemas() - { - var schema = await _.Rules.GetEventSchemaAsync("EnrichedContentEvent"); + [Fact] + public async Task Should_get_event_schemas() + { + var schema = await _.Rules.GetEventSchemaAsync("EnrichedContentEvent"); - Assert.NotNull(schema); - } + Assert.NotNull(schema); + } - [Fact] - public async Task Should_get_event_types() - { - var eventTypes = await _.Rules.GetEventTypesAsync(); + [Fact] + public async Task Should_get_event_types() + { + var eventTypes = await _.Rules.GetEventTypesAsync(); - Assert.NotEmpty(eventTypes); - } + Assert.NotEmpty(eventTypes); + } - private async Task CreateAppAsync() + private async Task CreateAppAsync() + { + var createRequest = new CreateAppDto { - var createRequest = new CreateAppDto - { - Name = appName - }; + Name = appName + }; - await _.Apps.PostAppAsync(createRequest); - } + await _.Apps.PostAppAsync(createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/SchemaTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/SchemaTests.cs index 75b92a5ce3..a4913a51cf 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/SchemaTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/SchemaTests.cs @@ -12,219 +12,218 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +[UsesVerify] +public class SchemaTests : IClassFixture<CreatedAppFixture> { - [UsesVerify] - public class SchemaTests : IClassFixture<CreatedAppFixture> - { - private readonly string schemaName = $"schema-{Guid.NewGuid()}"; + private readonly string schemaName = $"schema-{Guid.NewGuid()}"; - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public SchemaTests(CreatedAppFixture fixture) - { - _ = fixture; - } + public SchemaTests(CreatedAppFixture fixture) + { + _ = fixture; + } - [Fact] - public async Task Should_create_schema() + [Fact] + public async Task Should_create_schema() + { + // STEP 1: Create schema + var createRequest = new CreateSchemaDto { - // STEP 1: Create schema - var createRequest = new CreateSchemaDto - { - Name = schemaName - }; + Name = schemaName + }; - var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - // Should return created schemas with correct name. - Assert.Equal(schemaName, schema.Name); + // Should return created schemas with correct name. + Assert.Equal(schemaName, schema.Name); - // STEP 2: Get all schemas - var schemas = await _.Schemas.GetSchemasAsync(_.AppName); + // STEP 2: Get all schemas + var schemas = await _.Schemas.GetSchemasAsync(_.AppName); - // Should provide new schema when apps are schemas. - Assert.Contains(schemas.Items, x => x.Name == schemaName); - } + // Should provide new schema when apps are schemas. + Assert.Contains(schemas.Items, x => x.Name == schemaName); + } - [Fact] - public async Task Should_not_allow_creation_if_name_used() + [Fact] + public async Task Should_not_allow_creation_if_name_used() + { + // STEP 1: Create schema + var createRequest = new CreateSchemaDto { - // STEP 1: Create schema - var createRequest = new CreateSchemaDto - { - Name = schemaName - }; + Name = schemaName + }; - var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - // STEP 2: Create again and fail - var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => - { - return _.Schemas.PostSchemaAsync(_.AppName, createRequest); - }); + // STEP 2: Create again and fail + var ex = await Assert.ThrowsAnyAsync<SquidexManagementException>(() => + { + return _.Schemas.PostSchemaAsync(_.AppName, createRequest); + }); - Assert.Equal(400, ex.StatusCode); - } + Assert.Equal(400, ex.StatusCode); + } - [Fact] - public async Task Should_create_singleton_schema() + [Fact] + public async Task Should_create_singleton_schema() + { + // STEP 1: Create schema + var createRequest = new CreateSchemaDto { - // STEP 1: Create schema - var createRequest = new CreateSchemaDto - { - Name = schemaName, - // Use the new property to create a singleton. - Type = SchemaType.Singleton, - // Must be pusblished to query content. - IsPublished = true - }; + Name = schemaName, + // Use the new property to create a singleton. + Type = SchemaType.Singleton, + // Must be pusblished to query content. + IsPublished = true + }; - var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - // Should return created schemas with correct name. - Assert.Equal(schemaName, schema.Name); + // Should return created schemas with correct name. + Assert.Equal(schemaName, schema.Name); - await Verify(schema) - .IgnoreMember<SchemaDto>(x => x.Name); + await Verify(schema) + .IgnoreMember<SchemaDto>(x => x.Name); - // STEP 2: Get all schemas - var schemas = await _.Schemas.GetSchemasAsync(_.AppName); + // STEP 2: Get all schemas + var schemas = await _.Schemas.GetSchemasAsync(_.AppName); - // Should provide new schema when apps are schemas. - Assert.Contains(schemas.Items, x => x.Name == schemaName); + // Should provide new schema when apps are schemas. + Assert.Contains(schemas.Items, x => x.Name == schemaName); - // STEP 3: Get singleton content - var client = _.ClientManager.CreateDynamicContentsClient(schemaName); + // STEP 3: Get singleton content + var client = _.ClientManager.CreateDynamicContentsClient(schemaName); - var content = await client.GetAsync(schema.Id); + var content = await client.GetAsync(schema.Id); - Assert.NotNull(content); - } + Assert.NotNull(content); + } - [Fact] - public async Task Should_create_singleton_schema_with_obsolete_property() + [Fact] + public async Task Should_create_singleton_schema_with_obsolete_property() + { + // STEP 1: Create schema + var createRequest = new CreateSchemaDto { - // STEP 1: Create schema - var createRequest = new CreateSchemaDto - { - Name = schemaName, - // Use the old property to create a singleton. - IsSingleton = true, - // Must be pusblished to query content. - IsPublished = true - }; + Name = schemaName, + // Use the old property to create a singleton. + IsSingleton = true, + // Must be pusblished to query content. + IsPublished = true + }; - var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - // Should return created schemas with correct name. - Assert.Equal(schemaName, schema.Name); + // Should return created schemas with correct name. + Assert.Equal(schemaName, schema.Name); - await Verify(schema) - .IgnoreMember<SchemaDto>(x => x.Name); + await Verify(schema) + .IgnoreMember<SchemaDto>(x => x.Name); - // STEP 2: Get all schemas - var schemas = await _.Schemas.GetSchemasAsync(_.AppName); + // STEP 2: Get all schemas + var schemas = await _.Schemas.GetSchemasAsync(_.AppName); - // Should provide new schema when apps are schemas. - Assert.Contains(schemas.Items, x => x.Name == schemaName); + // Should provide new schema when apps are schemas. + Assert.Contains(schemas.Items, x => x.Name == schemaName); - // STEP 3: Get singleton content - var client = _.ClientManager.CreateDynamicContentsClient(schemaName); + // STEP 3: Get singleton content + var client = _.ClientManager.CreateDynamicContentsClient(schemaName); - var content = await client.GetAsync(schema.Id); + var content = await client.GetAsync(schema.Id); - Assert.NotNull(content); - } + Assert.NotNull(content); + } - [Fact] - public async Task Should_create_schema_with_checkboxes() + [Fact] + public async Task Should_create_schema_with_checkboxes() + { + // STEP 1: Create schema + var createRequest = new CreateSchemaDto { - // STEP 1: Create schema - var createRequest = new CreateSchemaDto + Name = schemaName, + Fields = new List<UpsertSchemaFieldDto> { - Name = schemaName, - Fields = new List<UpsertSchemaFieldDto> + new UpsertSchemaFieldDto { - new UpsertSchemaFieldDto + Name = "references", + Partitioning = "invariant", + Properties = new ReferencesFieldPropertiesDto { - Name = "references", - Partitioning = "invariant", - Properties = new ReferencesFieldPropertiesDto - { - Editor = ReferencesFieldEditor.Checkboxes - } - }, - new UpsertSchemaFieldDto + Editor = ReferencesFieldEditor.Checkboxes + } + }, + new UpsertSchemaFieldDto + { + Name = "tags", + Partitioning = "invariant", + Properties = new TagsFieldPropertiesDto { - Name = "tags", - Partitioning = "invariant", - Properties = new TagsFieldPropertiesDto - { - Editor = TagsFieldEditor.Checkboxes, - AllowedValues = new List<string> { "value1" } - } + Editor = TagsFieldEditor.Checkboxes, + AllowedValues = new List<string> { "value1" } } } - }; + } + }; - var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - // Should return created schemas with correct name. - Assert.Equal(schemaName, schema.Name); - } + // Should return created schemas with correct name. + Assert.Equal(schemaName, schema.Name); + } - [Fact] - public async Task Should_delete_Schema() + [Fact] + public async Task Should_delete_Schema() + { + // STEP 1: Create schema + var createRequest = new CreateSchemaDto { - // STEP 1: Create schema - var createRequest = new CreateSchemaDto - { - Name = schemaName - }; + Name = schemaName + }; - var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - // Should return created schemas with correct name. - Assert.Equal(schemaName, schema.Name); + // Should return created schemas with correct name. + Assert.Equal(schemaName, schema.Name); - // STEP 2: Delete schema - await _.Schemas.DeleteSchemaAsync(_.AppName, schemaName); + // STEP 2: Delete schema + await _.Schemas.DeleteSchemaAsync(_.AppName, schemaName); - var schemas = await _.Schemas.GetSchemasAsync(_.AppName); + var schemas = await _.Schemas.GetSchemasAsync(_.AppName); - // Should not provide deleted schema when schema are queried. - Assert.DoesNotContain(schemas.Items, x => x.Name == schemaName); - } + // Should not provide deleted schema when schema are queried. + Assert.DoesNotContain(schemas.Items, x => x.Name == schemaName); + } - [Fact] - public async Task Should_recreate_after_deleted() + [Fact] + public async Task Should_recreate_after_deleted() + { + // STEP 1: Create schema + var createRequest = new CreateSchemaDto { - // STEP 1: Create schema - var createRequest = new CreateSchemaDto - { - Name = schemaName - }; + Name = schemaName + }; - var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + var schema = await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - // Should return created schemas with correct name. - Assert.Equal(schemaName, schema.Name); + // Should return created schemas with correct name. + Assert.Equal(schemaName, schema.Name); - // STEP 2: Delete schema. - await _.Schemas.DeleteSchemaAsync(_.AppName, schemaName); + // STEP 2: Delete schema. + await _.Schemas.DeleteSchemaAsync(_.AppName, schemaName); - // STEP 3: Create app again - await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - } + // STEP 3: Create app again + await _.Schemas.PostSchemaAsync(_.AppName, createRequest); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/SearchTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/SearchTests.cs index bc242c2ba5..0d1edb7cf1 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/SearchTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/SearchTests.cs @@ -12,87 +12,86 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row -namespace TestSuite.ApiTests +namespace TestSuite.ApiTests; + +public class SearchTests : IClassFixture<ContentFixture> { - public class SearchTests : IClassFixture<ContentFixture> + public ContentFixture _ { get; } + + public SearchTests(ContentFixture fixture) { - public ContentFixture _ { get; } + _ = fixture; + } - public SearchTests(ContentFixture fixture) - { - _ = fixture; - } + [Fact] + public async Task Should_search_asset() + { + // STEP 1: Create asset + await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-wide.png", "image/png"); - [Fact] - public async Task Should_search_asset() - { - // STEP 1: Create asset - await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-wide.png", "image/png"); + // STEP 2: Search for schema. + var result = await _.Search.GetSearchResultsAsync(_.AppName, "logo"); - // STEP 2: Search for schema. - var result = await _.Search.GetSearchResultsAsync(_.AppName, "logo"); + Assert.Contains(result, x => x.Type == SearchResultType.Asset); + } - Assert.Contains(result, x => x.Type == SearchResultType.Asset); - } + [Fact] + public async Task Should_search_schema() + { + // STEP 1: Create schema + var schemaName = Guid.NewGuid().ToString(); - [Fact] - public async Task Should_search_schema() + var createRequest = new CreateSchemaDto { - // STEP 1: Create schema - var schemaName = Guid.NewGuid().ToString(); + Name = schemaName + }; - var createRequest = new CreateSchemaDto - { - Name = schemaName - }; + await _.Schemas.PostSchemaAsync(_.AppName, createRequest); - await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + // STEP 2: Search for schema. + var result = await _.Search.WaitForSearchAsync(_.AppName, schemaName, x => x.Type == SearchResultType.Content, TimeSpan.FromSeconds(30)); - // STEP 2: Search for schema. - var result = await _.Search.WaitForSearchAsync(_.AppName, schemaName, x => x.Type == SearchResultType.Content, TimeSpan.FromSeconds(30)); + Assert.NotEmpty(result); + } - Assert.NotEmpty(result); - } + [Fact] + public async Task Should_search_content() + { + // STEP 1: Create content + var contentString = Guid.NewGuid().ToString(); - [Fact] - public async Task Should_search_content() - { - // STEP 1: Create content - var contentString = Guid.NewGuid().ToString(); - - var createRequest = new TestEntityData - { - String = contentString - }; - - await _.Contents.CreateAsync(createRequest, ContentCreateOptions.AsPublish); - - - // STEP 2: Search for schema. - var result = await _.Search.WaitForSearchAsync(_.AppName, contentString, x => x.Type == SearchResultType.Content, TimeSpan.FromSeconds(30)); - - Assert.NotEmpty(result); - } - - [Theory] - [InlineData(SearchResultType.Asset, "Assets")] - [InlineData(SearchResultType.Dashboard, "Dashboard")] - [InlineData(SearchResultType.Rule, "Rules")] - [InlineData(SearchResultType.Schema, "Schemas")] - [InlineData(SearchResultType.Setting, "Backups")] - [InlineData(SearchResultType.Setting, "Clients")] - [InlineData(SearchResultType.Setting, "Contributors")] - [InlineData(SearchResultType.Setting, "Languages")] - [InlineData(SearchResultType.Setting, "Roles")] - [InlineData(SearchResultType.Setting, "Subscription")] - [InlineData(SearchResultType.Setting, "Workflows")] - public async Task Should_search_for_dashboard_pages(SearchResultType expectedType, string query) + var createRequest = new TestEntityData { - var result = await _.Search.GetSearchResultsAsync(_.AppName, query); + String = contentString + }; + + await _.Contents.CreateAsync(createRequest, ContentCreateOptions.AsPublish); + + + // STEP 2: Search for schema. + var result = await _.Search.WaitForSearchAsync(_.AppName, contentString, x => x.Type == SearchResultType.Content, TimeSpan.FromSeconds(30)); + + Assert.NotEmpty(result); + } + + [Theory] + [InlineData(SearchResultType.Asset, "Assets")] + [InlineData(SearchResultType.Dashboard, "Dashboard")] + [InlineData(SearchResultType.Rule, "Rules")] + [InlineData(SearchResultType.Schema, "Schemas")] + [InlineData(SearchResultType.Setting, "Backups")] + [InlineData(SearchResultType.Setting, "Clients")] + [InlineData(SearchResultType.Setting, "Contributors")] + [InlineData(SearchResultType.Setting, "Languages")] + [InlineData(SearchResultType.Setting, "Roles")] + [InlineData(SearchResultType.Setting, "Subscription")] + [InlineData(SearchResultType.Setting, "Workflows")] + public async Task Should_search_for_dashboard_pages(SearchResultType expectedType, string query) + { + var result = await _.Search.GetSearchResultsAsync(_.AppName, query); - Assert.Contains(result, x => x.Type == expectedType); - } + Assert.Contains(result, x => x.Type == expectedType); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Settings/VerifySettings.cs b/backend/tools/TestSuite/TestSuite.ApiTests/Settings/VerifySettings.cs index 6732366d24..2abcdb21f8 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/Settings/VerifySettings.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/Settings/VerifySettings.cs @@ -7,19 +7,18 @@ using System.Runtime.CompilerServices; -namespace TestSuite.ApiTests.Settings +namespace TestSuite.ApiTests.Settings; + +public static class VerifySettings { - public static class VerifySettings + [ModuleInitializer] + public static void Initialize() { - [ModuleInitializer] - public static void Initialize() + VerifierSettings.DerivePathInfo((sourceFile, projectDirectory, type, method) => { - VerifierSettings.DerivePathInfo((sourceFile, projectDirectory, type, method) => - { - var path = Path.Combine(projectDirectory, "Verify"); + var path = Path.Combine(projectDirectory, "Verify"); - return new PathInfo(path, type.Name, method.Name); - }); - } + return new PathInfo(path, type.Name, method.Name); + }); } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs index aad54e1dc1..9ac653e5ee 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs @@ -11,63 +11,62 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.LoadTests +namespace TestSuite.LoadTests; + +public class ReadingBenchmarks : IClassFixture<CreatedAppFixture> { - public class ReadingBenchmarks : IClassFixture<CreatedAppFixture> - { - private readonly ITestOutputHelper testOutput; + private readonly ITestOutputHelper testOutput; - public CreatedAppFixture _ { get; } + public CreatedAppFixture _ { get; } - public ReadingBenchmarks(CreatedAppFixture fixture, ITestOutputHelper testOutput) - { - this.testOutput = testOutput; + public ReadingBenchmarks(CreatedAppFixture fixture, ITestOutputHelper testOutput) + { + this.testOutput = testOutput; - _ = fixture; - } + _ = fixture; + } - public static IEnumerable<object[]> Loads() + public static IEnumerable<object[]> Loads() + { + int[] users = { - int[] users = - { - 1, - 5, - 10, - 20, - 50, - 100 - }; + 1, + 5, + 10, + 20, + 50, + 100 + }; - int[] loads = - { - 1, - 5, - 10, - 20, - 50, - 100, - 1000 - }; + int[] loads = + { + 1, + 5, + 10, + 20, + 50, + 100, + 1000 + }; - foreach (var user in users) + foreach (var user in users) + { + foreach (var load in loads) { - foreach (var load in loads) - { - yield return new object[] { user, load }; - } + yield return new object[] { user, load }; } - - yield return new object[] { 1, 20000 }; } - [Theory] - [MemberData(nameof(Loads))] - public async Task Should_return_clients(int numUsers, int numIterationsPerUser) + yield return new object[] { 1, 20000 }; + } + + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_clients(int numUsers, int numIterationsPerUser) + { + await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Run.Parallel(numUsers, numIterationsPerUser, async () => - { - await _.Apps.GetClientsAsync(_.AppName); - }, 100, testOutput); - } + await _.Apps.GetClientsAsync(_.AppName); + }, 100, testOutput); } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs index 11934c0b5a..45f24b9085 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs @@ -10,99 +10,98 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.LoadTests +namespace TestSuite.LoadTests; + +public class ReadingContentBenchmarks : IClassFixture<ReadingFixture> { - public class ReadingContentBenchmarks : IClassFixture<ReadingFixture> + public ReadingFixture _ { get; } + + public ReadingContentBenchmarks(ReadingFixture fixture) { - public ReadingFixture _ { get; } + _ = fixture; + } - public ReadingContentBenchmarks(ReadingFixture fixture) + public static IEnumerable<object[]> Loads() + { + int[] users = { - _ = fixture; - } + 1, + 5, + 10, + 20, + 50, + 100 + }; - public static IEnumerable<object[]> Loads() + int[] loads = { - int[] users = - { - 1, - 5, - 10, - 20, - 50, - 100 - }; - - int[] loads = - { - 1, - 5, - 10, - 20, - 50, - 100, - 1000 - }; + 1, + 5, + 10, + 20, + 50, + 100, + 1000 + }; - foreach (var user in users) + foreach (var user in users) + { + foreach (var load in loads) { - foreach (var load in loads) - { - yield return new object[] { user, load }; - } + yield return new object[] { user, load }; } - - yield return new object[] { 1, 20000 }; } - [Theory] - [MemberData(nameof(Loads))] - public async Task Should_return_all(int numUsers, int numIterationsPerUser) + yield return new object[] { 1, 20000 }; + } + + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_all(int numUsers, int numIterationsPerUser) + { + await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Run.Parallel(numUsers, numIterationsPerUser, async () => - { - await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/number/iv asc" }); - }); - } + await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/number/iv asc" }); + }); + } - [Theory] - [MemberData(nameof(Loads))] - public async Task Should_return_items_with_skip(int numUsers, int numIterationsPerUser) + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_items_with_skip(int numUsers, int numIterationsPerUser) + { + await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Run.Parallel(numUsers, numIterationsPerUser, async () => - { - await _.Contents.GetAsync(new ContentQuery { Skip = 5, OrderBy = "data/number/iv asc" }); - }); - } + await _.Contents.GetAsync(new ContentQuery { Skip = 5, OrderBy = "data/number/iv asc" }); + }); + } - [Theory] - [MemberData(nameof(Loads))] - public async Task Should_return_items_with_skip_and_top(int numUsers, int numIterationsPerUser) + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_items_with_skip_and_top(int numUsers, int numIterationsPerUser) + { + await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Run.Parallel(numUsers, numIterationsPerUser, async () => - { - await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" }); - }); - } + await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" }); + }); + } - [Theory] - [MemberData(nameof(Loads))] - public async Task Should_return_items_with_ordering(int numUsers, int numIterationsPerUser) + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_items_with_ordering(int numUsers, int numIterationsPerUser) + { + await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Run.Parallel(numUsers, numIterationsPerUser, async () => - { - await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv desc" }); - }); - } + await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv desc" }); + }); + } - [Theory] - [MemberData(nameof(Loads))] - public async Task Should_return_items_with_filter(int numUsers, int numIterationsPerUser) + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_items_with_filter(int numUsers, int numIterationsPerUser) + { + await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Run.Parallel(numUsers, numIterationsPerUser, async () => - { - await _.Contents.GetAsync(new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }); - }); - } + await _.Contents.GetAsync(new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }); + }); } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs index a6d97b8c72..37eb84a764 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs @@ -11,59 +11,58 @@ using TestSuite.Fixtures; using TestSuite.Model; -namespace TestSuite.LoadTests +namespace TestSuite.LoadTests; + +public sealed class ReadingFixture : TestSchemaFixtureBase { - public sealed class ReadingFixture : TestSchemaFixtureBase + public ReadingFixture() + : base("benchmark-reading") { - public ReadingFixture() - : base("benchmark-reading") - { - } + } - public override async Task InitializeAsync() - { - await base.InitializeAsync(); + public override async Task InitializeAsync() + { + await base.InitializeAsync(); - await DisposeAsync(); + await DisposeAsync(); - var current = await Contents.GetAsync(new ContentQuery - { - Top = 0 - }); + var current = await Contents.GetAsync(new ContentQuery + { + Top = 0 + }); - var countTotal = (int)current.Total; - var countMissing = 100 - countTotal; + var countTotal = (int)current.Total; + var countMissing = 100 - countTotal; - for (var index = countMissing; index > 0; index--) + for (var index = countMissing; index > 0; index--) + { + var data = new TestEntityData { - var data = new TestEntityData + Number = index, + Json = JObject.FromObject(new { - Number = index, - Json = JObject.FromObject(new + nested0 = index, + nested1 = new { - nested0 = index, - nested1 = new - { - nested2 = index - } - }), - String = index.ToString(CultureInfo.InvariantCulture) - }; + nested2 = index + } + }), + String = index.ToString(CultureInfo.InvariantCulture) + }; - await Contents.CreateAsync(data, ContentCreateOptions.AsPublish); - } + await Contents.CreateAsync(data, ContentCreateOptions.AsPublish); } + } - public override async Task DisposeAsync() - { - await base.DisposeAsync(); + public override async Task DisposeAsync() + { + await base.DisposeAsync(); - var contents = await Contents.GetAsync(); + var contents = await Contents.GetAsync(); - foreach (var content in contents.Items) - { - await Contents.DeleteAsync(content); - } + foreach (var content in contents.Items) + { + await Contents.DeleteAsync(content); } } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs b/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs index 3e902fcd62..0ab367439c 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs @@ -10,64 +10,63 @@ using Xunit; using Xunit.Abstractions; -namespace TestSuite.LoadTests +namespace TestSuite.LoadTests; + +public static class Run { - public static class Run + public static async Task Parallel(int numUsers, int numIterationsPerUser, Func<Task> action, int expectedAvg = 100, ITestOutputHelper testOutput = null) { - public static async Task Parallel(int numUsers, int numIterationsPerUser, Func<Task> action, int expectedAvg = 100, ITestOutputHelper testOutput = null) - { - await action(); + await action(); - var elapsedMs = new ConcurrentBag<long>(); + var elapsedMs = new ConcurrentBag<long>(); - var errors = 0; + var errors = 0; - async Task RunAsync() + async Task RunAsync() + { + for (var i = 0; i < numIterationsPerUser; i++) { - for (var i = 0; i < numIterationsPerUser; i++) + try { - try - { - var watch = Stopwatch.StartNew(); + var watch = Stopwatch.StartNew(); - await action(); + await action(); - watch.Stop(); + watch.Stop(); - elapsedMs.Add(watch.ElapsedMilliseconds); - } - catch - { - Interlocked.Increment(ref errors); - } + elapsedMs.Add(watch.ElapsedMilliseconds); + } + catch + { + Interlocked.Increment(ref errors); } } + } - var tasks = new List<Task>(); + var tasks = new List<Task>(); - for (var i = 0; i < numUsers; i++) - { - tasks.Add(Task.Run(RunAsync)); - } - - await Task.WhenAll(tasks); + for (var i = 0; i < numUsers; i++) + { + tasks.Add(Task.Run(RunAsync)); + } - var count = elapsedMs.Count; + await Task.WhenAll(tasks); - var avg = elapsedMs.Average(); + var count = elapsedMs.Count; - if (testOutput != null) - { - testOutput.WriteLine("Total Errors: {0}/{1}", errors, numUsers * numIterationsPerUser); - testOutput.WriteLine("Total Count: {0}/{1}", count, numUsers * numIterationsPerUser); + var avg = elapsedMs.Average(); - testOutput.WriteLine(string.Empty); - testOutput.WriteLine("Performance Average: {0}", avg); - testOutput.WriteLine("Performance Max: {0}", elapsedMs.Max()); - testOutput.WriteLine("Performance Min: {0}", elapsedMs.Min()); - } + if (testOutput != null) + { + testOutput.WriteLine("Total Errors: {0}/{1}", errors, numUsers * numIterationsPerUser); + testOutput.WriteLine("Total Count: {0}/{1}", count, numUsers * numIterationsPerUser); - Assert.InRange(avg, 0, expectedAvg); + testOutput.WriteLine(string.Empty); + testOutput.WriteLine("Performance Average: {0}", avg); + testOutput.WriteLine("Performance Max: {0}", elapsedMs.Max()); + testOutput.WriteLine("Performance Min: {0}", elapsedMs.Min()); } + + Assert.InRange(avg, 0, expectedAvg); } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs b/backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs index d9939793f7..7e6559c94b 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs @@ -11,62 +11,61 @@ #pragma warning disable SA1300 // Element should begin with upper-case letter -namespace TestSuite.LoadTests +namespace TestSuite.LoadTests; + +public class WritingBenchmarks : IClassFixture<WritingFixture> { - public class WritingBenchmarks : IClassFixture<WritingFixture> + public WritingFixture _ { get; } + + public WritingBenchmarks(WritingFixture fixture) { - public WritingFixture _ { get; } + _ = fixture; + } - public WritingBenchmarks(WritingFixture fixture) + public static IEnumerable<object[]> Loads() + { + int[] users = { - _ = fixture; - } + 1, + 5, + 10, + 20, + 50, + 100 + }; - public static IEnumerable<object[]> Loads() + int[] loads = { - int[] users = - { - 1, - 5, - 10, - 20, - 50, - 100 - }; - - int[] loads = - { - 5, - 10, - 20, - 50, - 100 - }; + 5, + 10, + 20, + 50, + 100 + }; - foreach (var user in users) + foreach (var user in users) + { + foreach (var load in loads) { - foreach (var load in loads) - { - yield return new object[] { user, load }; - } + yield return new object[] { user, load }; } - - yield return new object[] { 1, 50000 }; } - [Theory] - [MemberData(nameof(Loads))] - public async Task Should_create_items(int numUsers, int numIterationsPerUser) - { - var random = new Random(); + yield return new object[] { 1, 50000 }; + } - await Run.Parallel(numUsers, numIterationsPerUser, async () => + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_create_items(int numUsers, int numIterationsPerUser) + { + var random = new Random(); + + await Run.Parallel(numUsers, numIterationsPerUser, async () => + { + await _.Contents.CreateAsync(new TestEntityData { - await _.Contents.CreateAsync(new TestEntityData - { - Number = random.Next() - }, ContentCreateOptions.AsPublish); - }); - } + Number = random.Next() + }, ContentCreateOptions.AsPublish); + }); } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/WritingFixture.cs b/backend/tools/TestSuite/TestSuite.LoadTests/WritingFixture.cs index 1c5c7d8451..b66b6d71c2 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/WritingFixture.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/WritingFixture.cs @@ -7,13 +7,12 @@ using TestSuite.Fixtures; -namespace TestSuite.LoadTests +namespace TestSuite.LoadTests; + +public sealed class WritingFixture : TestSchemaFixtureBase { - public sealed class WritingFixture : TestSchemaFixtureBase + public WritingFixture() + : base("benchmark_writing") { - public WritingFixture() - : base("benchmark_writing") - { - } } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs b/backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs index 33ef508566..6da647582a 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs @@ -9,271 +9,270 @@ using Squidex.ClientLibrary.Management; using TestSuite.Fixtures; -namespace TestSuite +namespace TestSuite; + +public static class ClientExtensions { - public static class ClientExtensions + public static async Task<bool> WaitForDeletionAsync(this IAssetsClient assetsClient, string app, string id, TimeSpan timeout) { - public static async Task<bool> WaitForDeletionAsync(this IAssetsClient assetsClient, string app, string id, TimeSpan timeout) + try { - try - { - using var cts = new CancellationTokenSource(timeout); + using var cts = new CancellationTokenSource(timeout); - while (!cts.IsCancellationRequested) + while (!cts.IsCancellationRequested) + { + try { - try - { - await assetsClient.GetAssetAsync(app, id, cts.Token); - } - catch (SquidexManagementException ex) when (ex.StatusCode == 404) - { - return true; - } - - await Task.Delay(200, cts.Token); + await assetsClient.GetAssetAsync(app, id, cts.Token); + } + catch (SquidexManagementException ex) when (ex.StatusCode == 404) + { + return true; } - } - catch (OperationCanceledException) - { - } - return false; + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { } - public static async Task<ContentsResult<TEntity, TData>> WaitForContentAsync<TEntity, TData>(this IContentsClient<TEntity, TData> contentsClient, ContentQuery q, Func<TEntity, bool> predicate, TimeSpan timeout) where TEntity : Content<TData> where TData : class, new() + return false; + } + + public static async Task<ContentsResult<TEntity, TData>> WaitForContentAsync<TEntity, TData>(this IContentsClient<TEntity, TData> contentsClient, ContentQuery q, Func<TEntity, bool> predicate, TimeSpan timeout) where TEntity : Content<TData> where TData : class, new() + { + try { - try + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) { - using var cts = new CancellationTokenSource(timeout); + var result = await contentsClient.GetAsync(q, null, cts.Token); - while (!cts.IsCancellationRequested) + if (result.Items.Any(predicate)) { - var result = await contentsClient.GetAsync(q, null, cts.Token); - - if (result.Items.Any(predicate)) - { - return result; - } - - await Task.Delay(200, cts.Token); + return result; } - } - catch (OperationCanceledException) - { - } - return new ContentsResult<TEntity, TData>(); + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { } - public static async Task<IList<SearchResultDto>> WaitForSearchAsync(this ISearchClient searchClient, string app, string query, Func<SearchResultDto, bool> predicate, TimeSpan timeout) + return new ContentsResult<TEntity, TData>(); + } + + public static async Task<IList<SearchResultDto>> WaitForSearchAsync(this ISearchClient searchClient, string app, string query, Func<SearchResultDto, bool> predicate, TimeSpan timeout) + { + try { - try + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) { - using var cts = new CancellationTokenSource(timeout); + var result = await searchClient.GetSearchResultsAsync(app, query, cts.Token); - while (!cts.IsCancellationRequested) + if (result.Any(predicate)) { - var result = await searchClient.GetSearchResultsAsync(app, query, cts.Token); - - if (result.Any(predicate)) - { - return result.ToList(); - } - - await Task.Delay(200, cts.Token); + return result.ToList(); } - } - catch (OperationCanceledException) - { - } - return new List<SearchResultDto>(); + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { } - public static async Task<IList<HistoryEventDto>> WaitForHistoryAsync(this IHistoryClient historyClient, string app, string channel, Func<HistoryEventDto, bool> predicate, TimeSpan timeout) + return new List<SearchResultDto>(); + } + + public static async Task<IList<HistoryEventDto>> WaitForHistoryAsync(this IHistoryClient historyClient, string app, string channel, Func<HistoryEventDto, bool> predicate, TimeSpan timeout) + { + try { - try + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) { - using var cts = new CancellationTokenSource(timeout); + var result = await historyClient.GetAppHistoryAsync(app, channel, cts.Token); - while (!cts.IsCancellationRequested) + if (result.Any(predicate)) { - var result = await historyClient.GetAppHistoryAsync(app, channel, cts.Token); - - if (result.Any(predicate)) - { - return result.ToList(); - } - - await Task.Delay(200, cts.Token); + return result.ToList(); } - } - catch (OperationCanceledException) - { - } - return new List<HistoryEventDto>(); + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { } - public static async Task<IDictionary<string, int>> WaitForTagsAsync(this IAssetsClient assetsClient, string app, string id, TimeSpan timeout) + return new List<HistoryEventDto>(); + } + + public static async Task<IDictionary<string, int>> WaitForTagsAsync(this IAssetsClient assetsClient, string app, string id, TimeSpan timeout) + { + try { - try + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) { - using var cts = new CancellationTokenSource(timeout); + var result = await assetsClient.GetTagsAsync(app, cts.Token); - while (!cts.IsCancellationRequested) + if (result.TryGetValue(id, out var count) && count > 0) { - var result = await assetsClient.GetTagsAsync(app, cts.Token); - - if (result.TryGetValue(id, out var count) && count > 0) - { - return result; - } - - await Task.Delay(200, cts.Token); + return result; } - } - catch (OperationCanceledException) - { - } - return await assetsClient.GetTagsAsync(app); + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { } - public static async Task<IList<BackupJobDto>> WaitForBackupsAsync(this IBackupsClient backupsClient, string app, Func<BackupJobDto, bool> predicate, TimeSpan timeout) + return await assetsClient.GetTagsAsync(app); + } + + public static async Task<IList<BackupJobDto>> WaitForBackupsAsync(this IBackupsClient backupsClient, string app, Func<BackupJobDto, bool> predicate, TimeSpan timeout) + { + try { - try + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) { - using var cts = new CancellationTokenSource(timeout); + var result = await backupsClient.GetBackupsAsync(app, cts.Token); - while (!cts.IsCancellationRequested) + if (result.Items.Any(predicate)) { - var result = await backupsClient.GetBackupsAsync(app, cts.Token); - - if (result.Items.Any(predicate)) - { - return result.Items; - } - - await Task.Delay(200, cts.Token); + return result.Items; } - } - catch (OperationCanceledException) - { - } - return null; + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { } - public static async Task<RestoreJobDto> WaitForRestoreAsync(this IBackupsClient backupsClient, Func<RestoreJobDto, bool> predicate, TimeSpan timeout) + return null; + } + + public static async Task<RestoreJobDto> WaitForRestoreAsync(this IBackupsClient backupsClient, Func<RestoreJobDto, bool> predicate, TimeSpan timeout) + { + try { - try + using var cts = new CancellationTokenSource(timeout); + + while (!cts.IsCancellationRequested) { - using var cts = new CancellationTokenSource(timeout); + var result = await backupsClient.GetRestoreJobAsync(cts.Token); - while (!cts.IsCancellationRequested) + if (predicate(result)) { - var result = await backupsClient.GetRestoreJobAsync(cts.Token); - - if (predicate(result)) - { - return result; - } - - await Task.Delay(200, cts.Token); + return result; } - } - catch (OperationCanceledException) - { - } - return null; + await Task.Delay(200, cts.Token); + } } - - public static async Task<MemoryStream> DownloadAsync(this ClientFixture fixture, AssetDto asset, int? version = null) + catch (OperationCanceledException) { - var temp = new MemoryStream(); + } - using (var httpClient = new HttpClient()) - { - httpClient.BaseAddress = new Uri(fixture.Url); + return null; + } - var url = asset._links["content"].Href[1..]; + public static async Task<MemoryStream> DownloadAsync(this ClientFixture fixture, AssetDto asset, int? version = null) + { + var temp = new MemoryStream(); - if (version > 0) - { - url += $"?version={version}"; - } + using (var httpClient = new HttpClient()) + { + httpClient.BaseAddress = new Uri(fixture.Url); - using (var response = await httpClient.GetAsync(url)) - { - response.EnsureSuccessStatusCode(); + var url = asset._links["content"].Href[1..]; - await using (var stream = await response.Content.ReadAsStreamAsync()) - { - await stream.CopyToAsync(temp); - } - } + if (version > 0) + { + url += $"?version={version}"; } - return temp; - } - - public static async Task<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string app, string path, AssetDto asset, string fileName = null) - { - var fileInfo = new FileInfo(path); - - await using (var stream = fileInfo.OpenRead()) + using (var response = await httpClient.GetAsync(url)) { - var upload = new FileParameter(stream, fileName ?? fileInfo.Name, asset.MimeType); + response.EnsureSuccessStatusCode(); - return await assetsClients.PutAssetContentAsync(app, asset.Id, upload); + await using (var stream = await response.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(temp); + } } } - public static async Task<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string app, string path, string fileType, string fileName = null, string parentId = null, string id = null) - { - var fileInfo = new FileInfo(path); + return temp; + } - await using (var stream = fileInfo.OpenRead()) - { - var upload = new FileParameter(stream, fileName ?? fileInfo.Name, fileType); + public static async Task<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string app, string path, AssetDto asset, string fileName = null) + { + var fileInfo = new FileInfo(path); - return await assetsClients.PostAssetAsync(app, parentId, id, true, upload); - } + await using (var stream = fileInfo.OpenRead()) + { + var upload = new FileParameter(stream, fileName ?? fileInfo.Name, asset.MimeType); + + return await assetsClients.PutAssetContentAsync(app, asset.Id, upload); } + } + + public static async Task<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string app, string path, string fileType, string fileName = null, string parentId = null, string id = null) + { + var fileInfo = new FileInfo(path); - public static async Task<AssetDto> UploadRandomFileAsync(this IAssetsClient assetsClients, string app, int size, string parentId = null, string id = null) + await using (var stream = fileInfo.OpenRead()) { - using (var stream = RandomAsset(size)) - { - var upload = new FileParameter(stream, RandomName(".txt"), "text/csv"); + var upload = new FileParameter(stream, fileName ?? fileInfo.Name, fileType); - return await assetsClients.PostAssetAsync(app, parentId, id, true, upload); - } + return await assetsClients.PostAssetAsync(app, parentId, id, true, upload); } + } - private static MemoryStream RandomAsset(int length) + public static async Task<AssetDto> UploadRandomFileAsync(this IAssetsClient assetsClients, string app, int size, string parentId = null, string id = null) + { + using (var stream = RandomAsset(size)) { - var stream = new MemoryStream(length); + var upload = new FileParameter(stream, RandomName(".txt"), "text/csv"); - var random = new Random(); + return await assetsClients.PostAssetAsync(app, parentId, id, true, upload); + } + } - for (var i = 0; i < length; i++) - { - stream.WriteByte((byte)random.Next()); - } + private static MemoryStream RandomAsset(int length) + { + var stream = new MemoryStream(length); - stream.Position = 0; + var random = new Random(); - return stream; + for (var i = 0; i < length; i++) + { + stream.WriteByte((byte)random.Next()); } - private static string RandomName(string extension) - { - var fileName = $"{Guid.NewGuid()}{extension}"; + stream.Position = 0; - return fileName; - } + return stream; + } + + private static string RandomName(string extension) + { + var fileName = $"{Guid.NewGuid()}{extension}"; + + return fileName; } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs b/backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs index fef2d9412c..48abdb86d8 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs @@ -10,77 +10,76 @@ using Squidex.ClientLibrary; using TestSuite.Utils; -namespace TestSuite +namespace TestSuite; + +public sealed class ClientManagerWrapper { - public sealed class ClientManagerWrapper - { - public ISquidexClientManager ClientManager { get; } + public ISquidexClientManager ClientManager { get; } - public ClientManagerWrapper() - { - var services = - new ServiceCollection() - .AddSquidexClient(options => + public ClientManagerWrapper() + { + var services = + new ServiceCollection() + .AddSquidexClient(options => + { + options.AppName = TestHelpers.GetAndPrintValue("config:app:name", "integration-tests"); + options.ClientId = TestHelpers.GetAndPrintValue("config:client:id", "root"); + options.ClientSecret = TestHelpers.GetAndPrintValue("config:client:secret", "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="); + options.Url = TestHelpers.GetAndPrintValue("config:server:url", "https://localhost:5001"); + options.ReadResponseAsString = true; + }) + .AddSquidexHttpClient() + .ConfigurePrimaryHttpMessageHandler(() => { - options.AppName = TestHelpers.GetAndPrintValue("config:app:name", "integration-tests"); - options.ClientId = TestHelpers.GetAndPrintValue("config:client:id", "root"); - options.ClientSecret = TestHelpers.GetAndPrintValue("config:client:secret", "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="); - options.Url = TestHelpers.GetAndPrintValue("config:server:url", "https://localhost:5001"); - options.ReadResponseAsString = true; - }) - .AddSquidexHttpClient() - .ConfigurePrimaryHttpMessageHandler(() => + return new HttpClientHandler { - return new HttpClientHandler - { - ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }; - }).Services - .BuildServiceProvider(); + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }; + }).Services + .BuildServiceProvider(); - ClientManager = services.GetRequiredService<ISquidexClientManager>(); - } + ClientManager = services.GetRequiredService<ISquidexClientManager>(); + } + + public async Task<ClientManagerWrapper> ConnectAsync() + { + var waitSeconds = TestHelpers.Configuration.GetValue<int>("config:wait"); - public async Task<ClientManagerWrapper> ConnectAsync() + if (waitSeconds > 10) { - var waitSeconds = TestHelpers.Configuration.GetValue<int>("config:wait"); + Console.WriteLine("Waiting {0} seconds to access server", waitSeconds); - if (waitSeconds > 10) + var pingClient = ClientManager.CreatePingClient(); + try { - Console.WriteLine("Waiting {0} seconds to access server", waitSeconds); - - var pingClient = ClientManager.CreatePingClient(); - try + using (var cts = new CancellationTokenSource(waitSeconds * 1000)) { - using (var cts = new CancellationTokenSource(waitSeconds * 1000)) + while (!cts.IsCancellationRequested) { - while (!cts.IsCancellationRequested) + try + { + await pingClient.GetPingAsync(cts.Token); + break; + } + catch { - try - { - await pingClient.GetPingAsync(cts.Token); - break; - } - catch - { - await Task.Delay(100, cts.Token); - } + await Task.Delay(100, cts.Token); } } } - catch (OperationCanceledException) - { - throw new InvalidOperationException("Cannot connect to test system."); - } - - Console.WriteLine("Connected to server."); } - else + catch (OperationCanceledException) { - Console.WriteLine("Waiting for server is skipped."); + throw new InvalidOperationException("Cannot connect to test system."); } - return this; + Console.WriteLine("Connected to server."); + } + else + { + Console.WriteLine("Waiting for server is skipped."); } + + return this; } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Factories.cs b/backend/tools/TestSuite/TestSuite.Shared/Factories.cs index b76ced8395..2bba0dd371 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Factories.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Factories.cs @@ -7,15 +7,14 @@ using System.Collections.Concurrent; -namespace TestSuite +namespace TestSuite; + +public static class Factories { - public static class Factories - { - private static readonly ConcurrentDictionary<string, Task<object>> Instances = new ConcurrentDictionary<string, Task<object>>(); + private static readonly ConcurrentDictionary<string, Task<object>> Instances = new ConcurrentDictionary<string, Task<object>>(); - public static async Task<T> CreateAsync<T>(string key, Func<Task<T>> factory) - { - return (T)await Instances.GetOrAdd(key, async (_, f) => await f(), factory); - } + public static async Task<T> CreateAsync<T>(string key, Func<Task<T>> factory) + { + return (T)await Instances.GetOrAdd(key, async (_, f) => await f(), factory); } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientCloudFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientCloudFixture.cs index 377a942db1..4eb4347a8e 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientCloudFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientCloudFixture.cs @@ -8,40 +8,39 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.ClientLibrary; -namespace TestSuite.Fixtures +namespace TestSuite.Fixtures; + +public sealed class ClientCloudFixture { - public sealed class ClientCloudFixture - { - public ISquidexClientManager ClientManager { get; private set; } + public ISquidexClientManager ClientManager { get; private set; } - public ISquidexClientManager CDNClientManager { get; private set; } + public ISquidexClientManager CDNClientManager { get; private set; } - public ClientCloudFixture() - { - ClientManager = - new ServiceCollection() - .AddSquidexClient(options => - { - options.AppName = "squidex-website"; - options.ClientId = "squidex-website:reader"; - options.ClientSecret = "yy9x4dcxsnp1s34r2z19t88wedbzxn1tfq7uzmoxf60x"; - options.ReadResponseAsString = true; - }) - .BuildServiceProvider() - .GetRequiredService<ISquidexClientManager>(); + public ClientCloudFixture() + { + ClientManager = + new ServiceCollection() + .AddSquidexClient(options => + { + options.AppName = "squidex-website"; + options.ClientId = "squidex-website:reader"; + options.ClientSecret = "yy9x4dcxsnp1s34r2z19t88wedbzxn1tfq7uzmoxf60x"; + options.ReadResponseAsString = true; + }) + .BuildServiceProvider() + .GetRequiredService<ISquidexClientManager>(); - CDNClientManager = - new ServiceCollection() - .AddSquidexClient(options => - { - options.AppName = "squidex-website"; - options.AssetCDN = "https://assets.squidex.io"; - options.ClientId = "squidex-website:reader"; - options.ClientSecret = "yy9x4dcxsnp1s34r2z19t88wedbzxn1tfq7uzmoxf60x"; - options.ContentCDN = "https://contents.squidex.io"; - }) - .BuildServiceProvider() - .GetRequiredService<ISquidexClientManager>(); - } + CDNClientManager = + new ServiceCollection() + .AddSquidexClient(options => + { + options.AppName = "squidex-website"; + options.AssetCDN = "https://assets.squidex.io"; + options.ClientId = "squidex-website:reader"; + options.ClientSecret = "yy9x4dcxsnp1s34r2z19t88wedbzxn1tfq7uzmoxf60x"; + options.ContentCDN = "https://contents.squidex.io"; + }) + .BuildServiceProvider() + .GetRequiredService<ISquidexClientManager>(); } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs index d8e40e7c8e..07d18e8d20 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs @@ -9,131 +9,130 @@ using Squidex.ClientLibrary.Management; using Xunit; -namespace TestSuite.Fixtures +namespace TestSuite.Fixtures; + +public class ClientFixture : IAsyncLifetime { - public class ClientFixture : IAsyncLifetime - { - public ClientManagerWrapper Squidex { get; private set; } + public ClientManagerWrapper Squidex { get; private set; } - public string AppName => ClientManager.Options.AppName; + public string AppName => ClientManager.Options.AppName; - public string ClientId => ClientManager.Options.ClientId; + public string ClientId => ClientManager.Options.ClientId; - public string ClientSecret => ClientManager.Options.ClientSecret; + public string ClientSecret => ClientManager.Options.ClientSecret; - public string Url => ClientManager.Options.Url; + public string Url => ClientManager.Options.Url; - public ISquidexClientManager ClientManager => Squidex.ClientManager; + public ISquidexClientManager ClientManager => Squidex.ClientManager; - public IAppsClient Apps - { - get => ClientManager.CreateAppsClient(); - } + public IAppsClient Apps + { + get => ClientManager.CreateAppsClient(); + } - public IAssetsClient Assets - { - get => ClientManager.CreateAssetsClient(); - } + public IAssetsClient Assets + { + get => ClientManager.CreateAssetsClient(); + } - public IBackupsClient Backups - { - get => ClientManager.CreateBackupsClient(); - } + public IBackupsClient Backups + { + get => ClientManager.CreateBackupsClient(); + } - public ICommentsClient Comments - { - get => ClientManager.CreateCommentsClient(); - } + public ICommentsClient Comments + { + get => ClientManager.CreateCommentsClient(); + } - public IDiagnosticsClient Diagnostics - { - get => ClientManager.CreateDiagnosticsClient(); - } + public IDiagnosticsClient Diagnostics + { + get => ClientManager.CreateDiagnosticsClient(); + } - public IHistoryClient History - { - get => ClientManager.CreateHistoryClient(); - } + public IHistoryClient History + { + get => ClientManager.CreateHistoryClient(); + } - public ILanguagesClient Languages - { - get => ClientManager.CreateLanguagesClient(); - } + public ILanguagesClient Languages + { + get => ClientManager.CreateLanguagesClient(); + } - public IPingClient Ping - { - get => ClientManager.CreatePingClient(); - } + public IPingClient Ping + { + get => ClientManager.CreatePingClient(); + } - public IPlansClient Plans - { - get => ClientManager.CreatePlansClient(); - } + public IPlansClient Plans + { + get => ClientManager.CreatePlansClient(); + } - public IRulesClient Rules - { - get => ClientManager.CreateRulesClient(); - } + public IRulesClient Rules + { + get => ClientManager.CreateRulesClient(); + } - public ISchemasClient Schemas - { - get => ClientManager.CreateSchemasClient(); - } + public ISchemasClient Schemas + { + get => ClientManager.CreateSchemasClient(); + } - public ISearchClient Search - { - get => ClientManager.CreateSearchClient(); - } + public ISearchClient Search + { + get => ClientManager.CreateSearchClient(); + } - public ITemplatesClient Templates - { - get => ClientManager.CreateTemplatesClient(); - } + public ITemplatesClient Templates + { + get => ClientManager.CreateTemplatesClient(); + } - public ITranslationsClient Translations - { - get => ClientManager.CreateTranslationsClient(); - } + public ITranslationsClient Translations + { + get => ClientManager.CreateTranslationsClient(); + } - public IUserManagementClient UserManagement - { - get => ClientManager.CreateUserManagementClient(); - } + public IUserManagementClient UserManagement + { + get => ClientManager.CreateUserManagementClient(); + } - public IContentsSharedClient<DynamicContent, DynamicData> SharedContents - { - get => ClientManager.CreateSharedDynamicContentsClient(); - } + public IContentsSharedClient<DynamicContent, DynamicData> SharedContents + { + get => ClientManager.CreateSharedDynamicContentsClient(); + } - static ClientFixture() - { - VerifierSettings.IgnoreMember("AppName"); - VerifierSettings.IgnoreMember("Created"); - VerifierSettings.IgnoreMember("CreatedBy"); - VerifierSettings.IgnoreMember("EditToken"); - VerifierSettings.IgnoreMember("Href"); - VerifierSettings.IgnoreMember("LastModified"); - VerifierSettings.IgnoreMember("LastModifiedBy"); - VerifierSettings.IgnoreMember("RoleProperties"); - VerifierSettings.IgnoreMember("SchemaName"); - VerifierSettings.IgnoreMembersWithType<DateTimeOffset>(); - } - - public virtual async Task InitializeAsync() + static ClientFixture() + { + VerifierSettings.IgnoreMember("AppName"); + VerifierSettings.IgnoreMember("Created"); + VerifierSettings.IgnoreMember("CreatedBy"); + VerifierSettings.IgnoreMember("EditToken"); + VerifierSettings.IgnoreMember("Href"); + VerifierSettings.IgnoreMember("LastModified"); + VerifierSettings.IgnoreMember("LastModifiedBy"); + VerifierSettings.IgnoreMember("RoleProperties"); + VerifierSettings.IgnoreMember("SchemaName"); + VerifierSettings.IgnoreMembersWithType<DateTimeOffset>(); + } + + public virtual async Task InitializeAsync() + { + Squidex = await Factories.CreateAsync(nameof(ClientManagerWrapper), async () => { - Squidex = await Factories.CreateAsync(nameof(ClientManagerWrapper), async () => - { - var clientManager = new ClientManagerWrapper(); + var clientManager = new ClientManagerWrapper(); - await clientManager.ConnectAsync(); + await clientManager.ConnectAsync(); - return clientManager; - }); - } + return clientManager; + }); + } - public virtual Task DisposeAsync() - { - return Task.CompletedTask; - } + public virtual Task DisposeAsync() + { + return Task.CompletedTask; } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CreatedAppFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CreatedAppFixture.cs index 09416f37a0..9b92f8cf35 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CreatedAppFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CreatedAppFixture.cs @@ -7,63 +7,62 @@ using Squidex.ClientLibrary.Management; -namespace TestSuite.Fixtures +namespace TestSuite.Fixtures; + +public class CreatedAppFixture : ClientFixture { - public class CreatedAppFixture : ClientFixture + public override async Task InitializeAsync() { - public override async Task InitializeAsync() - { - await base.InitializeAsync(); + await base.InitializeAsync(); - await Factories.CreateAsync(AppName, async () => + await Factories.CreateAsync(AppName, async () => + { + try { - try + await Apps.PostAppAsync(new CreateAppDto { - await Apps.PostAppAsync(new CreateAppDto - { - Name = AppName - }); - } - catch (SquidexManagementException ex) + Name = AppName + }); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) { - if (ex.StatusCode != 400) - { - throw; - } + throw; } + } - try + try + { + await Apps.PostLanguageAsync(AppName, new AddLanguageDto { - await Apps.PostLanguageAsync(AppName, new AddLanguageDto - { - Language = "de" - }); - } - catch (SquidexManagementException ex) + Language = "de" + }); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) { - if (ex.StatusCode != 400) - { - throw; - } + throw; } + } - try + try + { + await Apps.PostLanguageAsync(AppName, new AddLanguageDto { - await Apps.PostLanguageAsync(AppName, new AddLanguageDto - { - Language = "custom" - }); - } - catch (SquidexManagementException ex) + Language = "custom" + }); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) { - if (ex.StatusCode != 400) - { - throw; - } + throw; } + } - return true; - }); - } + return true; + }); } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/TestSchemaFixtureBase.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/TestSchemaFixtureBase.cs index b00e67d321..80a7d4d9b7 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/TestSchemaFixtureBase.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/TestSchemaFixtureBase.cs @@ -9,41 +9,40 @@ using Squidex.ClientLibrary.Management; using TestSuite.Model; -namespace TestSuite.Fixtures +namespace TestSuite.Fixtures; + +public abstract class TestSchemaFixtureBase : CreatedAppFixture { - public abstract class TestSchemaFixtureBase : CreatedAppFixture - { - public IContentsClient<TestEntity, TestEntityData> Contents { get; private set; } + public IContentsClient<TestEntity, TestEntityData> Contents { get; private set; } - public string SchemaName { get; } + public string SchemaName { get; } - protected TestSchemaFixtureBase(string schemaName) - { - SchemaName = schemaName; - } + protected TestSchemaFixtureBase(string schemaName) + { + SchemaName = schemaName; + } - public override async Task InitializeAsync() - { - await base.InitializeAsync(); + public override async Task InitializeAsync() + { + await base.InitializeAsync(); - await Factories.CreateAsync($"{nameof(TestEntity)}_{SchemaName}", async () => + await Factories.CreateAsync($"{nameof(TestEntity)}_{SchemaName}", async () => + { + try { - try - { - await TestEntity.CreateSchemaAsync(Schemas, AppName, SchemaName); - } - catch (SquidexManagementException ex) + await TestEntity.CreateSchemaAsync(Schemas, AppName, SchemaName); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) { - if (ex.StatusCode != 400) - { - throw; - } + throw; } + } - return true; - }); + return true; + }); - Contents = ClientManager.CreateContentsClient<TestEntity, TestEntityData>(SchemaName); - } + Contents = ClientManager.CreateContentsClient<TestEntity, TestEntityData>(SchemaName); } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/TestSchemaWithReferencesFixtureBase.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/TestSchemaWithReferencesFixtureBase.cs index 33485ae431..f3641912db 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/TestSchemaWithReferencesFixtureBase.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/TestSchemaWithReferencesFixtureBase.cs @@ -9,41 +9,40 @@ using Squidex.ClientLibrary.Management; using TestSuite.Model; -namespace TestSuite.Fixtures +namespace TestSuite.Fixtures; + +public abstract class TestSchemaWithReferencesFixtureBase : CreatedAppFixture { - public abstract class TestSchemaWithReferencesFixtureBase : CreatedAppFixture - { - public IContentsClient<TestEntityWithReferences, TestEntityWithReferencesData> Contents { get; private set; } + public IContentsClient<TestEntityWithReferences, TestEntityWithReferencesData> Contents { get; private set; } - public string SchemaName { get; } + public string SchemaName { get; } - protected TestSchemaWithReferencesFixtureBase(string schemaName) - { - SchemaName = schemaName; - } + protected TestSchemaWithReferencesFixtureBase(string schemaName) + { + SchemaName = schemaName; + } - public override async Task InitializeAsync() - { - await base.InitializeAsync(); + public override async Task InitializeAsync() + { + await base.InitializeAsync(); - await Factories.CreateAsync($"{nameof(TestEntityWithReferences)}_{SchemaName}", async () => + await Factories.CreateAsync($"{nameof(TestEntityWithReferences)}_{SchemaName}", async () => + { + try { - try - { - await TestEntityWithReferences.CreateSchemaAsync(Schemas, AppName, SchemaName); - } - catch (SquidexManagementException ex) + await TestEntityWithReferences.CreateSchemaAsync(Schemas, AppName, SchemaName); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) { - if (ex.StatusCode != 400) - { - throw; - } + throw; } + } - return true; - }); + return true; + }); - Contents = ClientManager.CreateContentsClient<TestEntityWithReferences, TestEntityWithReferencesData>(SchemaName); - } + Contents = ClientManager.CreateContentsClient<TestEntityWithReferences, TestEntityWithReferencesData>(SchemaName); } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherClient.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherClient.cs index 2e33d1ae76..af58e92227 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherClient.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherClient.cs @@ -11,130 +11,129 @@ #pragma warning disable MA0048 // File name must match type name -namespace TestSuite.Fixtures +namespace TestSuite.Fixtures; + +public sealed class WebhookSession { - public sealed class WebhookSession - { - public string Uuid { get; set; } - } + public string Uuid { get; set; } +} - public sealed class WebhookRequest - { - [JsonPropertyName("uuid")] - public string Uuid { get; set; } +public sealed class WebhookRequest +{ + [JsonPropertyName("uuid")] + public string Uuid { get; set; } - [JsonPropertyName("method")] - public string Method { get; set; } + [JsonPropertyName("method")] + public string Method { get; set; } - [JsonPropertyName("content_base64")] - public string Content { get; set; } + [JsonPropertyName("content_base64")] + public string Content { get; set; } - [JsonIgnore] - public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(); + [JsonIgnore] + public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(); - [JsonPropertyName("headers")] - public WebhookHeader[] HeaderValues { get; set; } - } + [JsonPropertyName("headers")] + public WebhookHeader[] HeaderValues { get; set; } +} - public sealed class WebhookHeader - { - [JsonPropertyName("name")] - public string Name { get; set; } +public sealed class WebhookHeader +{ + [JsonPropertyName("name")] + public string Name { get; set; } - [JsonPropertyName("value")] - public string Value { get; set; } - } + [JsonPropertyName("value")] + public string Value { get; set; } +} - public sealed class WebhookCatcherClient - { - private readonly HttpClient httpClient; +public sealed class WebhookCatcherClient +{ + private readonly HttpClient httpClient; - public string EndpointHost { get; } + public string EndpointHost { get; } - public int EndpointPort { get; } + public int EndpointPort { get; } - public WebhookCatcherClient(string apiHost, int apiPort, string endpointHost, int endpointPort) + public WebhookCatcherClient(string apiHost, int apiPort, string endpointHost, int endpointPort) + { + if (string.IsNullOrWhiteSpace(apiHost)) { - if (string.IsNullOrWhiteSpace(apiHost)) - { - apiHost = "localhost"; - } - - if (string.IsNullOrWhiteSpace(endpointHost)) - { - endpointHost = "localhost"; - } - - EndpointHost = endpointHost; - EndpointPort = endpointPort; + apiHost = "localhost"; + } - httpClient = new HttpClient - { - BaseAddress = new Uri($"http://{apiHost}:{apiPort}") - }; + if (string.IsNullOrWhiteSpace(endpointHost)) + { + endpointHost = "localhost"; } - public async Task<(string, string)> CreateSessionAsync( - CancellationToken ct = default) + EndpointHost = endpointHost; + EndpointPort = endpointPort; + + httpClient = new HttpClient { - var response = await httpClient.PostAsJsonAsync("/api/session", new { }, ct); + BaseAddress = new Uri($"http://{apiHost}:{apiPort}") + }; + } - response.EnsureSuccessStatusCode(); + public async Task<(string, string)> CreateSessionAsync( + CancellationToken ct = default) + { + var response = await httpClient.PostAsJsonAsync("/api/session", new { }, ct); - var responseObj = await response.Content.ReadFromJsonAsync<WebhookSession>(cancellationToken: ct); + response.EnsureSuccessStatusCode(); - return ($"http://{EndpointHost}:{EndpointPort}/{responseObj.Uuid}", responseObj.Uuid); - } + var responseObj = await response.Content.ReadFromJsonAsync<WebhookSession>(cancellationToken: ct); - public async Task<WebhookRequest[]> GetRequestsAsync(string sessionId, - CancellationToken ct = default) - { - var result = await httpClient.GetFromJsonAsync<WebhookRequest[]>($"/api/session/{sessionId}/requests", ct); + return ($"http://{EndpointHost}:{EndpointPort}/{responseObj.Uuid}", responseObj.Uuid); + } - foreach (var request in result) + public async Task<WebhookRequest[]> GetRequestsAsync(string sessionId, + CancellationToken ct = default) + { + var result = await httpClient.GetFromJsonAsync<WebhookRequest[]>($"/api/session/{sessionId}/requests", ct); + + foreach (var request in result) + { + if (request.Content != null) { - if (request.Content != null) - { - request.Content = Encoding.Default.GetString(Convert.FromBase64String(request.Content)); - } + request.Content = Encoding.Default.GetString(Convert.FromBase64String(request.Content)); + } - if (request.HeaderValues != null) + if (request.HeaderValues != null) + { + foreach (var header in request.HeaderValues) { - foreach (var header in request.HeaderValues) - { - request.Headers[header.Name] = header.Value; - } + request.Headers[header.Name] = header.Value; } } - - return result; } - public async Task<WebhookRequest[]> WaitForRequestsAsync(string sessionId, TimeSpan timeout) + return result; + } + + public async Task<WebhookRequest[]> WaitForRequestsAsync(string sessionId, TimeSpan timeout) + { + var requests = Array.Empty<WebhookRequest>(); + + try { - var requests = Array.Empty<WebhookRequest>(); + using var cts = new CancellationTokenSource(timeout); - try + while (!cts.IsCancellationRequested) { - using var cts = new CancellationTokenSource(timeout); + requests = await GetRequestsAsync(sessionId, cts.Token); - while (!cts.IsCancellationRequested) + if (requests.Length > 0) { - requests = await GetRequestsAsync(sessionId, cts.Token); - - if (requests.Length > 0) - { - break; - } - - await Task.Delay(50, cts.Token); + break; } - } - catch (OperationCanceledException) - { - } - return requests; + await Task.Delay(50, cts.Token); + } } + catch (OperationCanceledException) + { + } + + return requests; } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherFixture.cs index 0bada8f02f..358f595c4d 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherFixture.cs @@ -7,17 +7,16 @@ using TestSuite.Utils; -namespace TestSuite.Fixtures +namespace TestSuite.Fixtures; + +public sealed class WebhookCatcherFixture { - public sealed class WebhookCatcherFixture - { - public WebhookCatcherClient Client { get; } + public WebhookCatcherClient Client { get; } - public WebhookCatcherFixture() - { - Client = new WebhookCatcherClient( - TestHelpers.GetAndPrintValue("webhookcatcher:host:api", "localhost"), 1026, - TestHelpers.GetAndPrintValue("webhookcatcher:host:endpoint", "localhost"), 1026); - } + public WebhookCatcherFixture() + { + Client = new WebhookCatcherClient( + TestHelpers.GetAndPrintValue("webhookcatcher:host:api", "localhost"), 1026, + TestHelpers.GetAndPrintValue("webhookcatcher:host:endpoint", "localhost"), 1026); } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs index a10d2b1863..7c5af2b0a2 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs @@ -13,142 +13,141 @@ #pragma warning disable MA0048 // File name must match type name -namespace TestSuite.Model +namespace TestSuite.Model; + +public sealed class TestEntity : Content<TestEntityData> { - public sealed class TestEntity : Content<TestEntityData> - { - public const int ScriptTrigger = -99; + public const int ScriptTrigger = -99; - public static async Task<SchemaDto> CreateSchemaAsync(ISchemasClient schemas, string appName, string name, SchemaScriptsDto scripts = null) + public static async Task<SchemaDto> CreateSchemaAsync(ISchemasClient schemas, string appName, string name, SchemaScriptsDto scripts = null) + { + var schema = await schemas.PostSchemaAsync(appName, new CreateSchemaDto { - var schema = await schemas.PostSchemaAsync(appName, new CreateSchemaDto + Name = name, + Fields = new List<UpsertSchemaFieldDto> { - Name = name, - Fields = new List<UpsertSchemaFieldDto> + new UpsertSchemaFieldDto { - new UpsertSchemaFieldDto + Name = TestEntityData.NumberField, + Properties = new NumberFieldPropertiesDto { - Name = TestEntityData.NumberField, - Properties = new NumberFieldPropertiesDto - { - IsRequired = true - } - }, - new UpsertSchemaFieldDto - { - Name = TestEntityData.StringField, - Properties = new StringFieldPropertiesDto - { - IsRequired = false - } - }, - new UpsertSchemaFieldDto + IsRequired = true + } + }, + new UpsertSchemaFieldDto + { + Name = TestEntityData.StringField, + Properties = new StringFieldPropertiesDto { - Name = TestEntityData.GeoField, - Properties = new GeolocationFieldPropertiesDto - { - IsRequired = false - } - }, - new UpsertSchemaFieldDto + IsRequired = false + } + }, + new UpsertSchemaFieldDto + { + Name = TestEntityData.GeoField, + Properties = new GeolocationFieldPropertiesDto { - Name = TestEntityData.JsonField, - Properties = new JsonFieldPropertiesDto - { - IsRequired = false - } - }, - new UpsertSchemaFieldDto + IsRequired = false + } + }, + new UpsertSchemaFieldDto + { + Name = TestEntityData.JsonField, + Properties = new JsonFieldPropertiesDto { - Name = TestEntityData.LocalizedField, - Partitioning = "language", - Properties = new StringFieldPropertiesDto - { - DefaultValue = "default" - } - }, - new UpsertSchemaFieldDto + IsRequired = false + } + }, + new UpsertSchemaFieldDto + { + Name = TestEntityData.LocalizedField, + Partitioning = "language", + Properties = new StringFieldPropertiesDto { - Name = TestEntityData.IdField, - Properties = new StringFieldPropertiesDto - { - IsRequired = false - } + DefaultValue = "default" } }, - Scripts = scripts, - IsPublished = true - }); + new UpsertSchemaFieldDto + { + Name = TestEntityData.IdField, + Properties = new StringFieldPropertiesDto + { + IsRequired = false + } + } + }, + Scripts = scripts, + IsPublished = true + }); - return schema; - } + return schema; + } - public static TestEntityData CreateTestEntry(int index) + public static TestEntityData CreateTestEntry(int index) + { + var data = new TestEntityData { - var data = new TestEntityData + Number = index, + Json = JObject.FromObject(new { - Number = index, - Json = JObject.FromObject(new + nested0 = index, + nested1 = new { - nested0 = index, - nested1 = new - { - nested2 = index - } - }), - String = index.ToString(CultureInfo.InvariantCulture) - }; + nested2 = index + } + }), + String = index.ToString(CultureInfo.InvariantCulture) + }; - if (index % 2 == 0) + if (index % 2 == 0) + { + data.Geo = new { - data.Geo = new + type = "Point", + coordinates = new[] { - type = "Point", - coordinates = new[] - { - index, - index - } - }; - } - else - { - data.Geo = new { longitude = index, latitude = index }; - } - - return data; + index, + index + } + }; } + else + { + data.Geo = new { longitude = index, latitude = index }; + } + + return data; } +} - public sealed class TestEntityData - { - public static readonly string LocalizedField = nameof(Localized).ToLowerInvariant(); +public sealed class TestEntityData +{ + public static readonly string LocalizedField = nameof(Localized).ToLowerInvariant(); - public static readonly string StringField = nameof(String).ToLowerInvariant(); + public static readonly string StringField = nameof(String).ToLowerInvariant(); - public static readonly string NumberField = nameof(Number).ToLowerInvariant(); + public static readonly string NumberField = nameof(Number).ToLowerInvariant(); - public static readonly string JsonField = nameof(Json).ToLowerInvariant(); + public static readonly string JsonField = nameof(Json).ToLowerInvariant(); - public static readonly string GeoField = nameof(Geo).ToLowerInvariant(); + public static readonly string GeoField = nameof(Geo).ToLowerInvariant(); - public static readonly string IdField = nameof(Id).ToLowerInvariant(); + public static readonly string IdField = nameof(Id).ToLowerInvariant(); - public Dictionary<string, string> Localized { get; set; } + public Dictionary<string, string> Localized { get; set; } - [JsonConverter(typeof(InvariantConverter))] - public int Number { get; set; } + [JsonConverter(typeof(InvariantConverter))] + public int Number { get; set; } - [JsonConverter(typeof(InvariantConverter))] - public string Id { get; set; } + [JsonConverter(typeof(InvariantConverter))] + public string Id { get; set; } - [JsonConverter(typeof(InvariantConverter))] - public string String { get; set; } + [JsonConverter(typeof(InvariantConverter))] + public string String { get; set; } - [JsonConverter(typeof(InvariantConverter))] - public JToken Json { get; set; } + [JsonConverter(typeof(InvariantConverter))] + public JToken Json { get; set; } - [JsonConverter(typeof(InvariantConverter))] - public object Geo { get; set; } - } + [JsonConverter(typeof(InvariantConverter))] + public object Geo { get; set; } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs index 41d0028c80..832ca3e577 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs @@ -11,36 +11,35 @@ #pragma warning disable MA0048 // File name must match type name -namespace TestSuite.Model +namespace TestSuite.Model; + +public sealed class TestEntityWithReferences : Content<TestEntityWithReferencesData> { - public sealed class TestEntityWithReferences : Content<TestEntityWithReferencesData> + public static async Task<SchemaDto> CreateSchemaAsync(ISchemasClient schemas, string appName, string name) { - public static async Task<SchemaDto> CreateSchemaAsync(ISchemasClient schemas, string appName, string name) + var schema = await schemas.PostSchemaAsync(appName, new CreateSchemaDto { - var schema = await schemas.PostSchemaAsync(appName, new CreateSchemaDto + Name = name, + Fields = new List<UpsertSchemaFieldDto> { - Name = name, - Fields = new List<UpsertSchemaFieldDto> + new UpsertSchemaFieldDto { - new UpsertSchemaFieldDto + Name = nameof(TestEntityWithReferencesData.References).ToLowerInvariant(), + Properties = new ReferencesFieldPropertiesDto { - Name = nameof(TestEntityWithReferencesData.References).ToLowerInvariant(), - Properties = new ReferencesFieldPropertiesDto - { - IsRequired = false - } + IsRequired = false } - }, - IsPublished = true - }); + } + }, + IsPublished = true + }); - return schema; - } + return schema; } +} - public sealed class TestEntityWithReferencesData - { - [JsonConverter(typeof(InvariantConverter))] - public string[] References { get; set; } - } +public sealed class TestEntityWithReferencesData +{ + [JsonConverter(typeof(InvariantConverter))] + public string[] References { get; set; } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Utils/TestHelpers.cs b/backend/tools/TestSuite/TestSuite.Shared/Utils/TestHelpers.cs index c416380735..42ff0b7f15 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Utils/TestHelpers.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Utils/TestHelpers.cs @@ -7,38 +7,37 @@ using Microsoft.Extensions.Configuration; -namespace TestSuite.Utils +namespace TestSuite.Utils; + +public static class TestHelpers { - public static class TestHelpers + public static IConfiguration Configuration { get; } + + static TestHelpers() + { + var basePath = Path.GetFullPath("../../../"); + + Configuration = new ConfigurationBuilder() + .SetBasePath(basePath) + .AddJsonFile("appsettings.json", true) + .AddJsonFile("appsettings.Development.json", true) + .AddEnvironmentVariables() + .Build(); + } + + public static string GetAndPrintValue(string name, string fallback) { - public static IConfiguration Configuration { get; } + var value = Configuration[name]; - static TestHelpers() + if (string.IsNullOrWhiteSpace(value)) { - var basePath = Path.GetFullPath("../../../"); - - Configuration = new ConfigurationBuilder() - .SetBasePath(basePath) - .AddJsonFile("appsettings.json", true) - .AddJsonFile("appsettings.Development.json", true) - .AddEnvironmentVariables() - .Build(); + value = fallback; } - - public static string GetAndPrintValue(string name, string fallback) + else { - var value = Configuration[name]; - - if (string.IsNullOrWhiteSpace(value)) - { - value = fallback; - } - else - { - Console.WriteLine("Using <{0}>=<{1}>", name, value); - } - - return value; + Console.WriteLine("Using <{0}>=<{1}>", name, value); } + + return value; } }